Compare commits

...

240 commits

Author SHA1 Message Date
Slyghtning
6d1dbcb599
Merge pull request #1194 from hieblmi/instantout-fixes
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
instantout: improve reservation and swap handling
2026-08-12 10:55:24 +02:00
Slyghtning
f826260266
docs: document instant out reliability improvements
Record the Instant Out and reservation validation, recovery,
fee-limit, lifecycle, and custom macaroon updates in the next release
notes.
2026-08-11 21:27:36 +02:00
Slyghtning
e772f8ccfa
instantout: enforce the accepted swap fee
Carry the accepted quote into each request, persist it, and reject
invoices above that limit. Preserve compatibility for requests that
omit the cap while distinguishing an explicit zero.
2026-08-11 21:27:35 +02:00
Slyghtning
b596eabab3
instantout: recheck reservation timing during recovery
Refresh the chain height when a swap resumes, verify both the
reservation and HTLC windows, and select the HTLC path when either
remaining window is too short.
2026-08-11 21:27:35 +02:00
Slyghtning
b689e361c1
instantout: close unfinished MuSig2 sessions
Clean up abandoned signing sessions on error paths while leaving
completed sessions to lnd.
2026-08-11 21:27:35 +02:00
Slyghtning
6853e69a05
instantout: verify finalized MuSig2 witnesses
Run script validation for every combined signature before accepting a
finalized transaction, surfacing invalid witnesses immediately.
2026-08-11 21:27:35 +02:00
Slyghtning
b3776e92f4
instantout: validate MuSig2 response dimensions
Check nonce, signature, session, and transaction input counts before
indexing signing vectors, returning clear errors for incomplete data.
2026-08-11 21:27:34 +02:00
Slyghtning
51eb8f410a
reservation: validate confirmed output amounts
Compare each confirmed transaction output with the expected reservation
amount before advancing the state machine.
2026-08-11 21:27:34 +02:00
Slyghtning
c75d7e3e81
reservation: bound and prune active state machines
Limit newly accepted reservation state machines, resume all persisted
reservations, remove terminal entries from memory, and make cleanup
resilient to observer-driven eviction.
2026-08-11 21:27:34 +02:00
Slyghtning
41b884f610
reservation: reject duplicate reservation entries
Check active and persisted reservations before creating a new state
machine, preserving the existing reservation when a duplicate arrives.
2026-08-11 21:21:53 +02:00
Slyghtning
2989ceee42
reservation: isolate asynchronous initialization errors
Use a goroutine-local result for event dispatch so observer errors
remain independent and initialization outcomes stay deterministic.
2026-08-11 21:21:53 +02:00
Slyghtning
806c1f9356
reservation: keep processing after notification errors
Log individual reservation initialization failures and continue
consuming later notifications instead of stopping the manager.
2026-08-11 21:21:53 +02:00
Slyghtning
7cab89d45b
looprpc: align instant out permissions with loop out
Apply the loop:out permission to Instant Out and reservation RPCs so
their authorization requirements match the rest of the Loop Out API.
2026-08-11 21:21:52 +02:00
Boris Nagaev
aa79fa5262
Merge pull request #1193 from starius/loop-out-validation-reserves
loopd: account for channel reserves in loop out validation
2026-08-11 10:07:26 -05:00
Boris Nagaev
95e184a40f
loopd: account for channel reserves in loop out validation
Use each channel's local balance minus its reserve when simulating Loop
Out payment bandwidth, clamping the result at zero. This prevents swaps
from starting when the gross balance covers the amount and routing fee
but the spendable balance does not.

Add one-sat boundary tests and a bug-fix release note. Leave a TODO for
server-fee and prepay capacity because enforcing their maximum fee caps
could reject viable swaps whose actual routing fees are lower.
2026-08-10 18:48:56 -05:00
Slyghtning
1b81a06b42
Merge pull request #1192 from hieblmi/no-change-label
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
github: allow no-changelog to skip release notes
2026-08-06 11:06:38 +02:00
Slyghtning
57fb007f95
docs: document no-changelog release note opt-out 2026-08-04 11:02:33 +02:00
Slyghtning
6f0d817db4
github: allow no-changelog to skip release notes 2026-08-04 11:02:22 +02:00
Slyghtning
f5f76a61cc
Merge pull request #1191 from hieblmi/docs-gate
Some checks failed
CI / Release Notes (push) Has been cancelled
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
github: require release notes for pull requests
2026-08-03 11:48:15 +02:00
Slyghtning
4016f4a8f2
docs: instruct agents to add release notes 2026-08-03 11:18:14 +02:00
Slyghtning
414760c607
github: require release notes for pull requests 2026-08-03 11:17:54 +02:00
Slyghtning
a8e3e9c853
Merge pull request #1189 from hieblmi/tapd-client-fixes
assets: harden tapd client lifecycle and RFQ handling
2026-08-03 11:10:36 +02:00
Slyghtning
b686d97445
docs: note tapd client hardening
Document the RFQ validation, cache responsiveness, and tapd connection
lifecycle fixes in the rolling release notes.
2026-08-03 10:05:06 +02:00
Slyghtning
9e97992e18
assets: validate accepted RFQ asset rates
Validate the bid rate before returning an accepted asset sell quote.
This prevents malformed rates from reaching downstream quote arithmetic,
where nil or non-positive values can panic. Cover valid and malformed
responses with table-driven tests.
2026-08-03 10:05:05 +02:00
Slyghtning
cfbade1239
assets: reject malformed RFQ asset rates
Validate the rate pointer and decimal coefficient before converting
asset units. Return errors for nil, malformed, non-positive, and
oversized-scale rates instead of allowing nil dereferences or
division-by-zero panics. Add regression tests for each case.
2026-08-03 10:05:05 +02:00
Slyghtning
dc39d63d8d
assets: avoid locking cache during RPC
Restrict the asset-name cache mutex to map access so a slow
QueryAssetStats call cannot block cached readers. Use an RWMutex for
independent cache reads and add a concurrent regression test.
2026-08-03 10:05:05 +02:00
Slyghtning
c7d5e466cd
assets: validate RFQ timeout conversion
Convert the configured duration once during client creation. Round
positive fractional durations up to the whole seconds accepted by tapd.
Reject zero, negative, and overflowing values, and cover the conversion
boundaries with unit tests.
2026-08-03 10:05:05 +02:00
Slyghtning
514c3f06ad
loopd: close tapd client connections
Close the TapdClient when daemon initialization fails, during normal
shutdown, and after the view command completes. This prevents gRPC
transport resources from leaking across embedded daemon lifecycles and
error paths.
2026-08-03 10:05:04 +02:00
Slyghtning
8f831e85ae
Merge pull request #1188 from lightninglabs/gateway-v0.6.0
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
github: pin gateway v0.6.0 (Opus 5, /gateway check)
2026-07-28 09:07:41 +02:00
saubyk
2dda482660
github: pin gateway v0.6.0 (Opus 5, /gateway check)
Bumps both pins together: the gateway-action SHA and the runtime_ref it
resolves. runtime_ref is pinned explicitly rather than left to the
action's default, so bumping only the action would leave the job on the
v0.5.0 runtime.

v0.6.0 adds no trigger and no input, so the rest of the shim is
unchanged.

Also corrects a copy-paste artifact in the adjacent comment, which said
runtime upgrades go through an lnd PR.
2026-07-27 20:46:33 -07:00
Slyghtning
dede47e727
Merge pull request #1187 from hieblmi/review-gateway
github: add gateway code-review workflow
2026-07-27 21:53:55 +02:00
Slyghtning
f51148a3a0
github: add gateway code-review workflow 2026-07-27 13:25:29 +02:00
Slyghtning
4acabf72ee
Merge pull request #1181 from hieblmi/release-notes
docs: backdated and new release notes
2026-07-27 12:55:37 +02:00
Slyghtning
6b942941fb
docs: complete historical release notes
Document every published Loop release and preserve authoritative notes.

Add the next-release workflow and rebuild chronological navigation.

Rename the reproducible-build guide for clarity.
2026-07-27 11:54:05 +02:00
Slyghtning
edad26b876
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
2026-07-24 15:19:11 +02:00
Slyghtning
4732ec71e9
Merge pull request #1185 from lightninglabs/dependabot/go_modules/swapserverrpc/google.golang.org/grpc-1.82.1
build(deps): bump google.golang.org/grpc from 1.79.3 to 1.82.1 in /swapserverrpc
2026-07-24 10:57:12 +02:00
Slyghtning
4ca7a9c5ec
Merge pull request #1180 from hieblmi/multi-address-proto
Some checks are pending
CI / Commit Message (push) Waiting to run
CI / RPC compilation check (push) Waiting to run
CI / SQL compilation check (push) Waiting to run
CI / go mod check (push) Waiting to run
CI / build and lint code (push) Waiting to run
CI / verify that auto-generated documentation is up-to-date (push) Waiting to run
CI / run unit-test sqlite3 race (push) Waiting to run
CI / run unit-test postgres race (push) Waiting to run
CI / run LiT itests (push) Waiting to run
CI / run LiT unit tests (push) Waiting to run
swapserverrpc: add multi-address proofs
2026-07-23 21:14:50 +02:00
Slyghtning
cb97b2a37f
swapserverrpc: add multi-address proofs
Carry each deposit client derivation key and an optional generated
change descriptor in static-address loop-in and withdrawal requests. The
server can use these fields to validate scripts it does not store
directly.
2026-07-23 20:59:45 +02:00
Boris Nagaev
4161f5d128
multi: bump google.golang.org/grpc to v1.82.1 2026-07-23 13:13:57 -05:00
Alex Bosworth
d80733a710
Merge pull request #1184 from lightninglabs/alexbosworth-patch-19
version: bump version to v0.34.0-beta
2026-07-23 10:10:51 -07:00
Slyghtning
dbf1205b57
Merge pull request #1183 from hieblmi/gate-commit-messages
Some checks are pending
CI / Commit Message (push) Waiting to run
CI / RPC compilation check (push) Waiting to run
CI / SQL compilation check (push) Waiting to run
CI / go mod check (push) Waiting to run
CI / build and lint code (push) Waiting to run
CI / verify that auto-generated documentation is up-to-date (push) Waiting to run
CI / run unit-test sqlite3 race (push) Waiting to run
CI / run unit-test postgres race (push) Waiting to run
CI / run LiT itests (push) Waiting to run
CI / run LiT unit tests (push) Waiting to run
build: add commit message tooling
2026-07-22 21:16:13 +02:00
Alex Bosworth
a463c734ea
version: bump version to v0.34.0-beta 2026-07-22 08:53:15 -07:00
Slyghtning
eb63b0b4fe
build: add commit message tooling
Add a repository-local tool to lint, format, and safely reword commit
messages. Preserve markdown lists, quotes, code blocks, and trailers
while enforcing subject and body width limits.

Expose linting, formatting, and rewording through Make targets and
document the supported workflows. Run the linter in CI for pull request
and push commit ranges so malformed messages are caught before merging.
2026-07-22 14:27:25 +02:00
Slyghtning
41bc5f85df
Merge pull request #1182 from hieblmi/release-notes-34-beta
docs: add `v0.34.0-beta` release notes
2026-07-22 10:09:20 +02:00
Slyghtning
2cc2b9cbb6
docs: add v0.34.0-beta release notes 2026-07-22 09:47:03 +02:00
Gustavo Stingelin
d5fb8b8da8
cmd/loop: clarify traditional swap command scope 2026-07-21 11:33:48 -03:00
Gustavo Stingelin
4a2bcb3374
cmd/loop: display static loop-in monitor stages 2026-07-21 11:33:48 -03:00
Gustavo Stingelin
e3e6205a4b
staticaddr/loopin: publish status updates after persistence 2026-07-21 11:33:47 -03:00
Gustavo Stingelin
f5cc85f7f3
loopd: include static loop-ins in monitor snapshots 2026-07-21 11:33:47 -03:00
Gustavo Stingelin
7200a7ce09
loopd: synchronize cached swap lookups 2026-07-21 11:33:47 -03:00
Gustavo Stingelin
34cf98e8e8
loopd: map static address loop-ins to swap status 2026-07-21 11:33:47 -03:00
Gustavo Stingelin
df7dfba12a
staticaddr/loopin: preserve loop-in last update time 2026-07-21 11:33:46 -03:00
Gustavo Stingelin
4c03e46928
looprpc: expose static loop-in monitor state 2026-07-21 11:33:46 -03:00
Slyghtning
4bb7d5ee29
Merge pull request #1179 from GustavoStingelin/feat/static-address-fsm-diagrams
Some checks are pending
CI / RPC compilation check (push) Waiting to run
CI / SQL compilation check (push) Waiting to run
CI / go mod check (push) Waiting to run
CI / build and lint code (push) Waiting to run
CI / verify that auto-generated documentation is up-to-date (push) Waiting to run
CI / run unit-test sqlite3 race (push) Waiting to run
CI / run unit-test postgres race (push) Waiting to run
CI / run LiT itests (push) Waiting to run
CI / run LiT unit tests (push) Waiting to run
fsm: generate and verify static address diagrams
2026-07-21 14:59:35 +02:00
Boris Nagaev
9bccb649f4
Merge pull request #1175 from starius/bump-for-dependabot
build: fix dependency security alerts
2026-07-21 02:40:48 -05:00
Boris Nagaev
359997ca7b
Merge pull request #1177 from chandachewe10/master
Some checks are pending
CI / RPC compilation check (push) Waiting to run
CI / SQL compilation check (push) Waiting to run
CI / go mod check (push) Waiting to run
CI / build and lint code (push) Waiting to run
CI / verify that auto-generated documentation is up-to-date (push) Waiting to run
CI / run unit-test sqlite3 race (push) Waiting to run
CI / run unit-test postgres race (push) Waiting to run
CI / run LiT itests (push) Waiting to run
CI / run LiT unit tests (push) Waiting to run
fix dead error variable and silent invoice cancel in setStateAbandoned
2026-07-21 01:12:14 -05:00
Gustavo Stingelin
2e80b92cf9 build: rename reservation FSM diagram 2026-07-20 12:08:22 -03:00
Gustavo Stingelin
9f2d724c3e build: verify generated FSM diagrams 2026-07-20 12:08:02 -03:00
Gustavo Stingelin
0176e96e3c staticaddr: generate FSM diagrams 2026-07-20 12:07:26 -03:00
Gustavo Stingelin
e77a1ab634 fsm: harden diagram generation 2026-07-20 12:07:26 -03:00
Gustavo Stingelin
1d5b1c564a fsm: make diagram generation deterministic 2026-07-18 16:27:11 -03:00
Boris Nagaev
378825414b
assets: make tapd macaroon path network-aware
Build the default tapd admin macaroon path from the configured Bitcoin
network while preserving explicit path overrides. Add regression tests for
tapd credential loading and network-specific path selection.
2026-07-18 00:43:17 -05:00
Boris Nagaev
8716a51722
loopin: classify already-settled invoice RPC errors
lnd returns ErrInvoiceAlreadySettled from its RPC handler as an
uncoded gRPC error. On the client it is reconstructed as an Unknown
status, so comparing it directly with the Go sentinel never matches.

This made the abandon path log a warning for an expected condition.
The older timeout path had the same comparison and could return the
RPC error instead of finishing normal timeout processing.

Add a shared classifier that accepts the local sentinel and the exact
gRPC status representation, and use it in both cancellation paths.
Add coverage for the classifier and timeout handling.
2026-07-17 15:47:21 -05:00
Chanda Chewe
89f2dcff8b
loopin: fix dead error variable and silent invoice cancel in setStateAbandoned
The err variable is always nil when the final fmt.Errorf is reached
(a non-nil err exits earlier), producing a trailing ', <nil>' in the
error string visible to callers and in logs.

Additionally, CancelInvoice errors were silently swallowed with a
bare '_' assignment. The timeout path in the same file correctly
checks for ErrInvoiceAlreadySettled; this commit makes the abandon
path consistent: ignore already-settled invoices and log any other
unexpected error so operators can diagnose issues without failing
the abandon itself.

Co-authored-by: Chanda Chewe <chandachewe10@users.noreply.github.com>
2026-07-17 11:57:28 +00:00
Boris Nagaev
74cbb651bc
cmd/loop: normalize static channel memo help 2026-07-16 13:08:01 -05:00
Boris Nagaev
338a6ef9f1
build: fix dependency security alerts 2026-07-16 13:08:01 -05:00
Slyghtning
39c97e60d2
Merge pull request #1141 from lightninglabs/dyn-conf-tracker-staging
Some checks failed
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
staticaddr: dyn-conf-tracker
2026-07-16 15:25:58 +02:00
Slyghtning
43b16ff3e1
cmd/loop: warn for low-confirmation static deposits
Warn before dispatching a static loop-in that selects deposits below
the conservative six-confirmation threshold. Mirror automatic coin
selection before prompting so the warning reflects both manual and
auto-selected deposits.

Cover manual and auto-selected warning paths in CLI tests.
2026-07-15 16:52:39 +02:00
Slyghtning
c718e9a9fb
staticaddr/loopin: retry failed deposit state transitions
Keep the loop-in monitor in its recoverable state when a required deposit transition or unlock fails. Only advance after every selected deposit reaches the expected state, and retry only deposits that remain pending after a partial transition.

Preserve shutdown semantics when observer cancellation races with a completed deposit update, and add regression coverage for transition, partial-transition, and unlock failures.
2026-07-15 16:52:39 +02:00
Slyghtning
6927c67b96
staticaddr/loopin: use risk decision watcher 2026-07-12 09:14:34 +02:00
Slyghtning
332aab79ed
staticaddr/loopin: add risk decision watcher 2026-07-12 09:14:34 +02:00
Slyghtning
6593afc8bc
staticaddr/loopin: recover risk-decision deadlines
Record replayed server risk decisions through the loop-in store,
recover accepted payment-deadline timers using the persisted decision
time, and handle persisted rejections on restart. This lets recovered
static loop-ins keep pending confirmation-risk state instead of
restarting payment timing from scratch.
2026-07-12 09:14:34 +02:00
Slyghtning
f01a1f02d9
staticaddr/loopin: clean up deposits after unpaid htlc timeout
Track whether the invoice was canceled for non-payment while monitoring
the HTLC. If the HTLC never confirms before timeout, unlock the
deposits; if it did confirm, transition them to the HTLC-timeout sweep
state without issuing duplicate transitions.
2026-07-12 09:14:34 +02:00
Slyghtning
3e2b9452fe
loopd: wire static risk decision persistence
Create the static loop-in SQL store before the notification manager and
pass a persistence callback so server confirmation-risk decisions are
durably recorded before fanout.
2026-07-12 09:14:34 +02:00
Slyghtning
e53bb67e73
notifications: persist static loop-in risk decisions
Persist static loop-in confirmation-risk decisions before fanout when a
persistence callback is configured. Keep unpersisted decisions cached
for replay so notification delivery is not lost if the swap row is not
available yet.
2026-07-12 09:14:33 +02:00
Slyghtning
491fddbc34
loopdb: persist static loop-in risk decisions
Add schema, sqlc queries, store fields, and SqlStore support for
recording server confirmation-risk decisions with static loop-in swaps.
Store the decision timestamp so payment-deadline recovery can
reconstruct elapsed time after restart.
2026-07-12 09:14:33 +02:00
Slyghtning
b0f43bbe1e
staticaddr/loopin: wait for risk decisions
Subscribe to static loop-in confirmation-risk notifications before
starting the payment deadline. Start that deadline only after server
acceptance or the legacy confirmation fallback, and cancel the swap
invoice when the server rejects the risk wait. Refresh selected
deposits before the legacy fallback so recovered monitors use current
confirmation heights.
2026-07-12 09:14:33 +02:00
Slyghtning
da96842264
notifications: fan out static loop-in risk decisions
Add cached per-swap notification fanout for static loop-in
confirmation-risk acceptance and rejection notifications so loop-in
FSMs can subscribe by swap hash and receive decisions that arrived
before subscription.
2026-07-12 09:14:33 +02:00
Slyghtning
ef78c85e88
staticaddr/loopin: cancel signing for unavailable deposits
Check the originally selected deposit outpoints before signing a static
loop-in HTLC transaction. If any selected outpoint is no longer
available, cancel the swap invoice and fail the signing action instead
of producing signatures for stale inputs.
2026-07-12 09:14:33 +02:00
Slyghtning
1d935c657f
loopd: filter static deposit views through active set
Use the deposit manager's visible-deposit view for normal list and
summary RPCs so historical Deposited rows whose outpoints vanished from
lnd's wallet view are not exposed as available funds.
2026-07-12 09:14:32 +02:00
Slyghtning
dc7da41b28
staticaddr: refresh deposits before spend selection
Refresh the active static-address deposit set against lnd's wallet view
before quote, loop-in, withdrawal, channel-open, and autoloop selection
paths. This prevents stale persisted Deposited records from being
selected after replacement, reorg, or an external spend.
2026-07-12 09:14:32 +02:00
Slyghtning
1c89ff83f1
staticaddr/loopin: account for autoloop deposit expiry
Use the shared deposit-expiry helper when building autoloop DP
candidates so unconfirmed deposits do not look like the
earliest-expiring options.

This keeps the no-change selector's expiry tie-break aligned with the
generic loop-in deposit selection rules.
2026-07-12 09:14:32 +02:00
Slyghtning
0223caa370
staticaddr/loopin: treat unconfirmed deposits as swappable
Static address deposits with no confirmation height have not started
their CSV timeout yet, so keep them eligible for loop-in selection
instead of treating them as already near expiry. Prefer confirmed
deposits before unconfirmed ones during automatic selection, and share
the remaining-lifetime calculation used by the selector.
2026-07-12 09:14:32 +02:00
Slyghtning
557ba99513
staticaddr: require confirmed deposits for withdraws and channel opens
Deposited can now include mempool outputs for static loop-ins, but
withdrawals and static channel opens still require confirmed funding
inputs. Filter automatic channel-open selection to confirmed deposits
and reject explicit unconfirmed selections, including withdraw-all
requests that would otherwise silently include mempool deposits.
2026-07-12 09:14:32 +02:00
Slyghtning
e8dd3aa009
staticaddr: expose tracked deposit availability
Build list and summary responses from tracked deposit records instead
of raw wallet UTXOs so RPC clients see the manager availability state.
Split unconfirmed value from confirmed deposited value in summaries,
and reject manual loop-in quotes for selected deposits that are not
currently Deposited.
2026-07-12 09:14:32 +02:00
Slyghtning
e4bcc94a36
staticaddr/deposit: reconcile active deposits with wallet
Treat lnd's wallet view as the source of spendable static-address
outpoints while keeping historical deposit records in the DB. Reconcile
active FSMs against the current wallet view, reactivate known deposits
that reappear, and hide stale Deposited records from the visible
deposit set.
2026-07-12 09:14:32 +02:00
Slyghtning
ac12d251f5
staticaddr/deposit: replay startup block after recovery
The first block epoch is consumed before recovered deposit FSMs exist.
Replay that startup height after recovery so already-expired deposits
can run expiry handling immediately after restart.
2026-07-12 09:14:31 +02:00
Slyghtning
1abe617991
staticaddr/deposit: track unconfirmed deposits
Retain static-address deposits as soon as lnd reports the UTXO, even
when the output is still unconfirmed. Store the first confirmation
height once the output confirms.

Derive confirmation heights from the current wallet view because lnd
reports confirmation counts instead of first-confirmation heights.
2026-07-12 09:14:31 +02:00
Slyghtning
12ebeabd3d
Merge pull request #1161 from hieblmi/dyn-conf-prep
Some checks failed
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
staticaddr: harden deposit and loop-in lifecycle handling
2026-07-08 11:04:11 +02:00
Slyghtning
aed7df97a0
build: replace disappeared Go vanity modules 2026-07-08 10:42:19 +02:00
Slyghtning
c87273d32e
build: increase lint timeout 2026-07-08 09:02:12 +02:00
Slyghtning
814f046d8b
staticaddr/openchannel: fix nolint directive 2026-07-08 09:02:12 +02:00
Slyghtning
6582aa0807
staticaddr/loopin: check deposits before htlc signing
Before we send HTLC signatures to the server, the server cannot publish
the HTLC transaction. After those signatures are handed over, the server
can publish an HTLC that spends the selected deposits even if it never
pays the swap invoice.

Defend against stale local deposit state by checking the wallet's current
txout view immediately before signing. A deposit can have been spent by a
known withdrawal, channel open, timeout sweep, replacement, or another
wallet transaction while the loop-in FSM is recovering or while earlier
state still marked it as selected.

Failing before signing leaves the server without spend authority over an
unavailable input. Include mempool spends in the check so wallet-known
unconfirmed spends are treated as unavailable too.
2026-07-08 09:02:06 +02:00
Slyghtning
bd3882d5b0
staticaddr/loopin: use payment timeout duration helper 2026-07-08 09:01:18 +02:00
Slyghtning
3fdd9e2250
staticaddr/loopin: recover deposits by current outpoint
Recovered loop-ins carry two outpoint views. DepositOutpoints is the
immutable swap input snapshot sent to the server and used to validate
sweep requests. Deposits comes from the store's swap_hash/deposit-id
join and reflects the current deposit rows.

The active-deposit lookup takes a detour through the reconstructed
deposit rows before asking the deposit manager for active deposits.
That keeps recovery from depending on the historical input snapshot.

A future replacement path can RBF a deposit from its original funding
outpoint to a replacement outpoint while the swap still needs to retain
the original input list. Looking up active deposits by DepositOutpoints
would then fail recovery even though the store still maps the correct
deposit IDs to the swap hash.

Keep list responses on the store reconstruction too, so they do not
re-resolve deposits through historical outpoints.
2026-07-08 09:01:17 +02:00
Slyghtning
67252a84de
staticaddr/deposit: canonicalize multi-deposit locks 2026-07-08 09:01:17 +02:00
Slyghtning
814af6aadf
staticaddr/loopin: keep htlc monitor resumable
After the client gives the server HTLC signatures, shutdown must not drive the monitor state through the generic error path. That path cancels the invoice and attempts to unlock deposits even though the server can still publish the HTLC.

Return NoOp for monitor-state cancellation races and cover shutdown with a regression test that asserts no invoice cancellation or deposit unlock occurs.
2026-07-08 09:01:13 +02:00
Slyghtning
0c0cee377b
staticaddr/loopin: keep htlc timeout sweep resumable
A shutdown while publishing or monitoring the HTLC timeout sweep should not
transition the loop-in to Failed.

Return NoOp on context cancellation in those actions so the persisted
state remains a recovery point. Add focused tests for shutdown during
publication retry and confirmation monitoring.
2026-07-08 09:01:02 +02:00
Slyghtning
58fbe2230e
staticaddr/deposit: guard confirmation height access
Document deposit lock ownership for mutable confirmation state and
route production reads through deposit accessors.

Keep store persistence on no-lock helpers while callers hold the
deposit lock, preserving the existing transition behavior without
leaving direct field reads in user-facing paths.
2026-07-08 09:01:01 +02:00
Slyghtning
f468f24e6f
staticaddr/deposit: ignore queued expiry in final states
A block notification can queue OnExpiry before a deposit reaches a final
state. If the final transition wins that race first, the stale expiry event
must not overwrite the terminal outcome.

Keep LoopedIn and Withdrawn as self-loops on OnExpiry, matching the other
final states. Add a focused FSM test that sends OnExpiry directly to each
final state and verifies the state is preserved.
2026-07-08 09:01:01 +02:00
Slyghtning
d8b24d31c0
staticaddr/loopin: default payment timeout duration
Add a duration helper that falls back to the default payment timeout.

Recovered legacy swaps can have a zero persisted timeout, so later
deadline logic can use this without treating zero as immediate expiry.
2026-07-08 09:01:01 +02:00
Slyghtning
7f57f6fc6b
staticaddr/deposit: reject invalid transitions
Reject nil deposits and final-state deposits before sending FSM
events.

This keeps callers from transitioning stale or completed deposits and
uses the no-lock state helper while deposits are already locked.
2026-07-08 09:01:01 +02:00
Slyghtning
77d9335da2
staticaddr/deposit: serialize deposit reconciliation
Guard reconcileDeposits with a dedicated mutex.

Polling and block-driven reconciliation can overlap, so serialize the
path before it updates confirmation data and active FSM state.
2026-07-08 09:01:01 +02:00
Slyghtning
38dce3685f
staticaddr/deposit: factor active deposit notifications
Move active-deposit block notification fan-out into a helper.

This keeps the event loop small and gives later startup replay logic
a single path for notifying recovered deposit FSMs.
2026-07-08 09:01:01 +02:00
Slyghtning
6ce13ba8a4
staticaddr/deposit: document lock ordering
Document the lock-order invariant between Manager.mu and individual
deposit locks.

Later changes need both locks in the same path, so make the rule
explicit before the locking surface grows.
2026-07-08 09:01:00 +02:00
Slyghtning
f8e9d11d04
staticaddr/loopin: add lnd txout checker
Add a TxOutChecker interface for checking whether a selected deposit
outpoint is still available before signing the HTLC transaction.

Back the implementation with lnd wallet transaction data so known
confirmed and mempool spends mark the outpoint unavailable.
2026-07-08 09:00:56 +02:00
Slyghtning
508bf90a1c
staticaddr/loopin: preserve selected deposit outpoints
Store an independent snapshot of the outpoints selected for a
static loop-in.

Recovered swaps remain tied to the original funding outputs even if
deposit records later change confirmation or replacement metadata.

Avoid decoding an empty database outpoint string as a synthetic
outpoint.
2026-07-08 09:00:34 +02:00
Slyghtning
07d87c23a6
staticaddr/deposit: stop removed fsms
Add an explicit Stop method for deposit FSM block-notification
loops.

Call it when the manager removes a finalized active deposit so stale
FSM goroutines stop consuming block updates.
2026-07-08 09:00:26 +02:00
Slyghtning
8456314155
staticaddr/loopin: handle closed invoice updates
Treat closed invoice update channels as terminal for the monitor loop.

This avoids spinning when lnd closes the subscription after invoice cancellation or shutdown.
2026-07-08 09:00:26 +02:00
Slyghtning
a6d061c568
staticaddr/loopin: factor invoice update handling
Extract the monitor invoice update semantics into a helper and cover the existing state mapping with a dedicated test.
2026-07-08 09:00:26 +02:00
Slyghtning
ee5d84b323
staticaddr/deposit: reject duplicate outpoints
Reject duplicate static-address deposit outpoints before creating
withdrawal, loop-in, or channel-open requests.

Use the shared outpoint duplicate helper so each flow reports the
same input validation failure.
2026-07-08 09:00:26 +02:00
Slyghtning
0611832030
staticaddr/deposit: ignore expiry blocks in final states
Return early when block notifications reach deposits that already
moved into a terminal state.

This prevents final deposits from retrying expiry handling after
recovery or while their FSM is still draining block updates.
2026-07-08 09:00:26 +02:00
Slyghtning
ba1c37c0da
staticaddr/deposit: handle loop-in htlc timeout
Keep deposits locked when the server publishes the loop-in HTLC
without paying the invoice.

This lets the client sweep through the HTLC timeout path instead
of making the same outputs available for another action.
2026-07-08 09:00:25 +02:00
Boris Nagaev
eea3f96ada
Merge pull request #1162 from starius/fix-cli-tests
Some checks failed
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
cmd/loop: normalize numeric replay time zones
2026-07-02 09:41:43 -05:00
Boris Nagaev
baecbbfe4c
cmd/loop: normalize numeric replay time zones
Recorded session replay already normalizes timestamps before
comparing CLI output, but the time.String timestamp regex only
matched alphabetic zone names like UTC and EST. Some systems
render the same offset with a numeric zone name, for example
-0300 -03, which left the publication deadline unnormalized
and caused fixture comparisons to fail.

Match numeric zone names as well, and parse the timestamp using
the explicit numeric offset instead of the display zone name. Add a
regression test covering RFC3339, alphabetic time.String zones, and
numeric time.String zones.
2026-07-02 02:50:13 -05:00
Slyghtning
a134cb1f22
Merge pull request #1158 from hieblmi/log-htlc-tx-fee
Some checks failed
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
staticaddr/loopin: log HTLC fee guard inputs
2026-06-24 14:39:36 +02:00
Slyghtning
22cec7486c
staticaddr/loopin: log HTLC fee guard inputs
Log the static address loop-in HTLC weight, fee rates, computed fees, and configured fee caps before fee validation. This keeps the existing fee guard behavior intact while making validation failures easier to diagnose.
2026-06-24 14:22:15 +02:00
Boris Nagaev
375c02e0f6
Merge pull request #1156 from starius/lnd-21-improvements
Some checks are pending
CI / RPC compilation check (push) Waiting to run
CI / SQL compilation check (push) Waiting to run
CI / go mod check (push) Waiting to run
CI / build and lint code (push) Waiting to run
CI / verify that auto-generated documentation is up-to-date (push) Waiting to run
CI / run unit-test sqlite3 race (push) Waiting to run
CI / run unit-test postgres race (push) Waiting to run
CI / run LiT itests (push) Waiting to run
CI / run LiT unit tests (push) Waiting to run
Use new features of LND 0.21
2026-06-23 09:56:53 -05:00
Boris Nagaev
682d45405c
cmd/loop: expose both taproot channel types
LND v0.21 added the production TAPROOT commitment type while
SIMPLE_TAPROOT remains available as the legacy enum.

The static open-channel CLI previously used "taproot" for
SIMPLE_TAPROOT. Keep both choices available by renaming that legacy
spelling to "simple-taproot" and mapping "taproot" to TAPROOT. This
makes the CLI spelling match the channel type it requests while still
leaving an explicit path for users that need SIMPLE_TAPROOT.
2026-06-22 15:06:25 -05:00
Boris Nagaev
5d8a5019cf
lint: enable deprecation checks
Enable staticcheck's SA1019 check in golangci-lint so deprecated
identifiers are caught in CI.

Replace deprecated standard library and bbolt APIs with their current
equivalents. Keep intentional compatibility reads and writes of
deprecated Loop RPC fields behind narrow nolint annotations, because
older clients and persisted liquidity parameters still depend on those
fields.
2026-06-22 12:46:12 -05:00
Boris Nagaev
d324b4bfd8
loop: omit payment hops in cost migration
The cost cleanup migration pages through LND payments only to build
a payment-hash to fee map. It does not inspect HTLC attempts,
routes, or per-hop data; pagination still uses the top-level index
offsets returned by ListPayments.

Setting OmitHops is safe for this migration because LND only strips
hop-level route data from HTLC attempts, while preserving the top-level
payment fields the migration reads: hash, fee, and response offsets.
This reduces response size and query cost for nodes with many or large
MPP payments without changing the calculated swap costs.

The migration test records the mocked ListPayments requests and asserts
that OmitHops is set.
2026-06-22 12:46:12 -05:00
Boris Nagaev
09c92527ea
staticaddr: accept production taproot channels
LND v0.21 exposes CommitmentType_TAPROOT as the production taproot
channel commitment type, while SIMPLE_TAPROOT remains a legacy taproot
enum. Static address channel opens previously rejected TAPROOT and only
classified SIMPLE_TAPROOT as a taproot output for fee and weight
estimates.

Accept TAPROOT in the static address open-channel validator and keep
accepting SIMPLE_TAPROOT for compatibility. Treat both taproot
commitment enums as P2TR outputs for deposit-selection and withdrawal
fee estimates. Callers using the production enum then get the same
weight accounting as the legacy taproot enum.

This does not change the CLI mapping for user-facing
channel_type=taproot. It only makes the static address path compatible
with callers that already send LND production taproot commitment type.
2026-06-22 12:46:12 -05:00
Boris Nagaev
a92f6bc875
build: bump lndclient to v0.21.0-2
Use the tagged lndclient release that exposes the ListPayments
request fields needed by Loop optimizations.
2026-06-22 12:41:03 -05:00
Slyghtning
129d9c1d26
Merge pull request #1154 from hieblmi/pre-dyn-conf-fixes
Some checks are pending
CI / RPC compilation check (push) Waiting to run
CI / SQL compilation check (push) Waiting to run
CI / go mod check (push) Waiting to run
CI / build and lint code (push) Waiting to run
CI / verify that auto-generated documentation is up-to-date (push) Waiting to run
CI / run unit-test sqlite3 race (push) Waiting to run
CI / run unit-test postgres race (push) Waiting to run
CI / run LiT itests (push) Waiting to run
CI / run LiT unit tests (push) Waiting to run
confrisk: preparatory changes
2026-06-22 11:35:02 +02:00
Slyghtning
0bb06b0ba3
notifications: queue blocking fanout
Required notification fanout should not block the manager lock, but subscribers still need ordered delivery once brief backpressure clears. Sending must-deliver notifications directly can couple manager progress to subscriber receive timing, while queueing optional reservation notifications would contradict their best-effort delivery semantics.

Add bounded per-subscriber queues for must-deliver notifications, let those queues own channel shutdown instead of relying on recover for closed-channel sends, keep reservation fanout best-effort, and cover queued delivery, queue cleanup, and capacity drops in manager tests.
2026-06-22 10:58:37 +02:00
Slyghtning
d0c613e5c9 notifications: drop best-effort messages for slow subscribers
A slow optional notification subscriber must not stall the manager lock, while recovery and sweep work requests still need reliable local delivery to keep daemon progress deterministic. Treating all subscribers as blocking makes best-effort fanout a backpressure source for unrelated required work.

Make reservation fanout nonblocking for slow subscribers, keep recovery and sweep notifications on a required cancellation-aware delivery path, and cover both slow best-effort subscribers and required subscribers in manager tests.
2026-06-22 10:18:07 +02:00
Slyghtning
f982ceb753 staticaddr/loopin: include failed swaps in state queries
The final-state query previously formatted the state list with braces, which broke the comma-delimited match for both boundary entries: the first state was compared with a leading brace and the last state with a trailing brace. Final-state callers need swaps at both ends of the list, including HtlcTimeoutSwept and Failed, to remain visible once they reach a terminal status.

Return the final-state list as plain comma-separated state names, update the store comment to match the query format, and extend the store test so it inserts and retrieves swaps in the first final state, a middle final state, and the last final state without relying on result order.
2026-06-22 10:18:07 +02:00
Slyghtning
bbeb813bfa staticaddr/deposit: finalize deposits asynchronously
Final deposit states should not stall while deposit locks are held, because a blocked manager receive loop can otherwise hold up the deposit FSM; if shutdown happens before notification delivery, startup recovery can still resume from the final state.

Send finalization notifications from a goroutine so final states are recorded without waiting on the manager receive loop, and add tests for blocked manager delivery and shutdown races.
2026-06-22 10:18:07 +02:00
Slyghtning
44a6c7a144 staticaddr/loopin: cancel orphan invoice on init failure
A static address loop-in that creates an invoice but then fails before storage cannot be recovered after restart, and persisted error paths that unlock deposits should not leave the swap invoice live either. The payment-deadline path also needs to report the real deposit unlock error instead of checking the FSM event returned by UnlockDepositsAction, because that action returns OnError on both success and failure.

Cancel private swap invoices on pre-storage init failures, monitor timeouts, and stored unlock paths using a detached timeout-limited helper. Factor deposit unlocking into an error-returning helper so deadline handling can log transition failures, and add coverage for early-init cancellation plus generic unlock invoice cleanup and error propagation.
2026-06-22 10:18:07 +02:00
Alex Bosworth
b44f8484f2
Merge pull request #1155 from lightninglabs/alexbosworth-patch-18
Some checks are pending
CI / RPC compilation check (push) Waiting to run
CI / SQL compilation check (push) Waiting to run
CI / go mod check (push) Waiting to run
CI / build and lint code (push) Waiting to run
CI / verify that auto-generated documentation is up-to-date (push) Waiting to run
CI / run unit-test sqlite3 race (push) Waiting to run
CI / run unit-test postgres race (push) Waiting to run
CI / run LiT itests (push) Waiting to run
CI / run LiT unit tests (push) Waiting to run
version: bump version to v0.33.3-beta
2026-06-21 12:50:10 -07:00
Alex Bosworth
a7fb06e205
version: bump version to v0.33.3-beta 2026-06-20 13:39:49 -07:00
Boris Nagaev
8087c7aa13
Merge pull request #1153 from starius/bump-lnd-21
Some checks are pending
CI / RPC compilation check (push) Waiting to run
CI / SQL compilation check (push) Waiting to run
CI / go mod check (push) Waiting to run
CI / build and lint code (push) Waiting to run
CI / verify that auto-generated documentation is up-to-date (push) Waiting to run
CI / run unit-test sqlite3 race (push) Waiting to run
CI / run unit-test postgres race (push) Waiting to run
CI / run LiT itests (push) Waiting to run
CI / run LiT unit tests (push) Waiting to run
build: bump lnd to v0.21
2026-06-20 13:55:04 -05:00
Boris Nagaev
914a3eeced
loopd: raise minimum lnd version to v0.18.4-beta
LoopMinRequiredLndVersion was 0.17.0, a value that only ever tracked the
go.mod lnd dependency rounded down and was never updated as the client
started depending on newer lnd RPC APIs. The client today uses RPC
fields that do not exist in 0.17.0:

  - routerrpc.SendPaymentRequest.first_hop_custom_records and
    lnrpc.Route.custom_channel_data, used by asset loop outs in
    loopout.go: both added in lnd v0.18.4-beta.
  - walletrpc.EstimateFeeResponse.min_relay_fee_sat_per_kw, read by the
    sweep batcher fee floor via lndclient WalletKit.MinRelayFee
    (sweepbatcher/, loopd/sweep_htlc.go): added in lnd v0.18.3-beta. On
    older lnd it silently decodes to 0, disabling the min-relay floor.

Raise the floor to the highest of these (v0.18.4-beta) so loopd fails
fast at startup rather than misbehaving at runtime, and document in
AGENTS.md the rule to keep this value pinned to the lnd APIs the client
actually uses instead of tracking go.mod.
2026-06-16 01:12:03 -05:00
Boris Nagaev
950b5f1772
test: refresh route hint preservation comments
lndclient now forwards route hints through AddInvoice. These tests
assert that Loop preserves explicit hints on generated loop-in invoices.
Remove stale comments that described the old wrapper behavior as
dropping hints.
2026-06-11 18:08:05 -05:00
Boris Nagaev
2d446b3b9e
test: remove unused PayInvoice mock path
Loop no longer pays invoices through LightningClient.PayInvoice.
Production payment paths use the router client wrappers. lndclient now
also implements PayInvoice on top of router payment APIs, so the old
SendPaymentChannel mock path is no longer exercised.

Remove the dead PayInvoice mock, its SendPaymentChannel plumbing, and
the loop-in resume assertion that watched that channel.
2026-06-11 18:08:02 -05:00
Boris Nagaev
cfe9e401c4
build: bump lnd to v0.21
Update lnd to v0.21.0-beta and lndclient to v0.21.0-1.

Regenerate the LND-derived swagger and CLI manpage outputs after the
dependency bump.
2026-06-11 16:24:30 -05:00
Alex Bosworth
af6af819cf
Merge pull request #1151 from lightninglabs/alexbosworth-patch-17
Some checks failed
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
version: bump version to v0.33.2-beta
2026-06-08 11:45:43 -07:00
Alex Bosworth
9807c95344
version: bump version to v0.33.2-beta 2026-06-04 08:20:43 -07:00
Slyghtning
f879c52e65
Merge pull request #1150 from hieblmi/show-static-details
Some checks failed
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
looprpc: populate static address swap timing and cost fields
2026-06-03 20:48:18 +02:00
Boris Nagaev
63affeefd6
cmd/loop: clarify static swap list help 2026-06-03 13:15:01 -05:00
Boris Nagaev
14e540fdc6
loopd: test static address swap rpc fields 2026-06-03 13:15:01 -05:00
Slyghtning
c6b4362e2d
gomod: replace dario.cat/mergo 2026-06-03 18:41:17 +02:00
Slyghtning
51f984f7c0
looprpc: populate static swap timing and costs 2026-06-03 15:33:02 +02:00
Boris Nagaev
5818b986ae
Merge pull request #1120 from starius/htlc-notification2
Some checks failed
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
loopout: handle htlc confirmed notification
2026-05-30 00:36:41 -05:00
Boris Nagaev
f443fe4f62
loopd: recover confirmed htlcs with direct sweep 2026-05-29 23:00:29 -05:00
Boris Nagaev
33917e1dbd
loopd: prefer stored destination in sweephtlc 2026-05-29 23:00:29 -05:00
Boris Nagaev
6c57f05d8a
loopd: add missing debugf logging helper 2026-05-29 23:00:29 -05:00
Boris Nagaev
c7ee8f27e9
notifications: log unknown ntfns at debug
Log unknown server notification variants at Debug level instead of Warn
to avoid noisy logs during feature rollout mismatches.
2026-05-29 23:00:29 -05:00
Boris Nagaev
c56c6d3563
notifications: handle htlc confirmed ntfn
Add NotificationTypeHtlcConfirmed and SubscribeHtlcConfirmed so consumers
can subscribe to HTLC-confirmed notifications. Also add dispatch in
handleNotification to forward htlc_confirmed messages to subscribers.
2026-05-29 23:00:29 -05:00
Boris Nagaev
410a4551fc
swapserverrpc: add htlc confirmed notification
Add ServerHtlcConfirmedNotification message to SubscribeNotificationsResponse
oneof. This new notification type allows the server to inform the client that
a loop out HTLC has been confirmed on-chain, including the confirmed outpoint.
2026-05-29 23:00:29 -05:00
Slyghtning
e0e1da5551
Merge pull request #1148 from hieblmi/harden-musig2-handling
multi: validate server-provided signing data and clean up comments
2026-05-29 12:57:23 +02:00
Slyghtning
cc0392af3f
client: reject malformed server public keys
Loop-in and loop-out responses carry compressed server public keys that are copied into fixed-size fields and later used for HTLC construction. Validate the length and parse each compressed key before storing it, and validate the MuSig2 loop-in receiver internal key as well.

This turns short or unparsable server keys into explicit errors instead of silently zero-padding short responses or accepting an invalid internal key. Update root test mocks to return size-correct MuSig2 signing data under the stricter checks.
2026-05-29 11:53:33 +02:00
Slyghtning
605e72a261
sweepbatcher: reject malformed MuSig2 cosign data
The cooperative batch sweep path receives a server nonce and partial signature before constructing a keyspend witness. Validate both byte slice lengths before registering the nonce or combining signatures, so malformed server responses fail explicitly instead of being zero-padded into fixed-size MuSig2 buffers.

Update batcher test helpers to return size-correct placeholder signing data under the stricter validation.
2026-05-29 11:53:33 +02:00
Slyghtning
db9bd06629
staticaddr: reject malformed MuSig2 signing data
Server-supplied nonces and partial signatures are consumed by the static address loop-in and withdrawal MuSig2 signing paths. Reject nil signing info, wrong nonce lengths, and wrong partial signature lengths before registering nonces or combining signatures, so malformed responses cannot be silently zero-padded into signing attempts.

Add withdrawal coverage for nil and malformed server signing data.
2026-05-29 11:53:32 +02:00
Slyghtning
c78988291c
loopout: fix test wording 2026-05-27 11:51:09 +02:00
Slyghtning
11bf5ca219
loopdb: clean up comments 2026-05-27 11:50:30 +02:00
Slyghtning
089743dacc
liquidity: fix comment typos 2026-05-27 11:50:08 +02:00
Slyghtning
1fa919711f
staticaddr: fix timeout comment typo 2026-05-27 11:49:49 +02:00
Alex Bosworth
aa772faf98
version: bump version to v0.33.1-beta
version: bump version to v0.33.1-beta
2026-05-26 07:32:04 -07:00
Alex Bosworth
92b2004127
version: bump version to v0.33.1-beta 2026-05-24 13:15:50 -07:00
Boris Nagaev
772e67fb86
Merge pull request #1146 from starius/go-mod-fix
ci: fix mod-check
2026-05-24 12:42:57 -05:00
Boris Nagaev
4b8bdfdbb1
ci: fix mod-check
GitLab probably blocked GitHub's CI runner IP address. So we can't download
packages from GitLab directly during this check. Workaround applied: download
these particular packages from proxy.golang.org which works in CI.
2026-05-24 11:38:10 -05:00
Boris Nagaev
12ef083e91
go mod tidy 2026-05-23 15:32:08 -05:00
Boris Nagaev
859ba4508e
Merge pull request #1119 from starius/static-autoloop
autoloop: support static loop-ins
2026-05-22 02:50:42 -05:00
Boris Nagaev
83534a283c
cmd/loop: record static autoloop CLI
Add recorded CLI coverage for selecting the static-address autoloop
loop-in source, displaying the resulting parameters, and showing a static
loop-in suggestion. Also record the non-experimental rejection so the
opt-in gate is covered.

Update existing liquidity fixtures with the default loop-in source field
emitted by the newer RPC shape.
2026-05-22 02:32:15 -05:00
Boris Nagaev
7fcc508193
liquidity: gate static autoloop
Static-address loop-ins in autoloop are still experimental. Reject
loop_in_source=static-address at the RPC boundary unless loopd was started
with --experimental, and pass the same opt-in into the liquidity manager so
persisted params cannot bypass the gate after restart.

The existing static swap accounting remains wired through the manager; the
gate only controls accepting and planning new static-address autoloops.
2026-05-22 02:32:15 -05:00
Boris Nagaev
8d6df2bf20
staticaddr: use dp autoloop selector
Replace the recursive full-deposit autoloop selector with a bounded-memory
DP implementation in staticaddr/loopin/autoloop_dp.go. The new selector
keeps the existing no-change semantics, first finds the best reachable
total, then applies the 25 percent band rule so earlier-expiring deposits
can win inside that near-optimal range.

The DP table is capped at 128 MiB and keeps exact satoshi sums alongside
compressed bucket weights, so planning stays memory-bounded without
allowing oversized candidates. The compressed weighting now rounds down
with a minimum of one bucket, which avoids rejecting valid sums after
multiple per-deposit rounding steps while leaving the exact-sum check
as the real safety boundary.
2026-05-22 02:20:36 -05:00
Boris Nagaev
616b0534a6
docs: update (autoloop supports static) 2026-05-22 02:20:36 -05:00
Boris Nagaev
8ea9adbd35
looprpc: map custom channel reason
Static autoloop testing surfaced a SuggestSwaps failure when the
planner disqualified a custom asset channel.

Add the missing AutoReason enum value and handle ReasonCustomChannelData.
2026-05-22 02:20:36 -05:00
Boris Nagaev
58ebb042ff
liquidity: add static autoloop planner
Wire static-address-backed loop-ins into the existing autoloop
planner and dispatch path. Loop-in rules can now be converted into
static candidates, prepared after global sorting, filtered with
static fee limits, and dispatched through the static manager.

This also fixes MaxAutoInFlight enforcement across all suggested
swap types and adds planner tests for missing static candidates
and mixed in-flight filtering.
2026-05-22 02:20:36 -05:00
Boris Nagaev
e467ac932b
looprpc: expose static autoloop output
Extend the public rpc surface for static autoloop integration
without turning the planner on yet. SuggestSwaps responses can
now carry static-address loop-in requests and the new planner
reason for missing static candidates is mapped over rpc.
2026-05-22 02:20:36 -05:00
Boris Nagaev
7cf0a87c2b
liquidity: count static loop-ins
Teach the liquidity manager to include persisted static loop-ins
in budget accounting, in-flight limits, and peer traffic backoff.
This adds the static fee model used for conservative accounting
and passes storage errors through the relevant planner helpers.

The daemon wiring now exposes static loop-ins to liquidity so the
manager can see the same ongoing swaps that the static-address
subsystem persists, while easy autoloop keeps working with the new
fallible traffic lookup path.
2026-05-22 02:20:36 -05:00
Boris Nagaev
94fc04a71a
staticaddr: add autoloop loop-in prep
Add the static-address helper that prepares full-deposit autoloop loop-ins
without dispatching them. The helper selects no-change deposit sets, records
explicit outpoints, and quotes the exact selected amount before the planner
tries to dispatch anything.

The tests cover the full-deposit selector, the quoted request construction,
and excluded outpoint handling so later liquidity work can rely on a stable
preparation surface.
2026-05-22 02:20:36 -05:00
Boris Nagaev
48a7bdc7d0
staticaddr/loopin: makeDeposit gets confheight arg
Test-only change. This is needed to reuse it in another test.
2026-05-22 02:20:36 -05:00
Boris Nagaev
562416e99b
loopd: validate static loop-in labels at rpc
Move static loop-in label validation to the rpc boundary and
remove the same check from the internal manager path.

This keeps external requests aligned with the existing swap rpc
surface while allowing internal autoloop callers to keep using
reserved labels for automated swaps. The tests cover both sides of
that contract: rpc requests still reject reserved labels, and the
manager path accepts them.
2026-05-22 02:20:36 -05:00
Boris Nagaev
ac58336104
looprpc: add autoloop loop-in source
Add a dedicated loop-in source enum to the liquidity parameters
rpc and wire it through the internal parameter model and CLI.

This keeps the source selection explicit before any static
autoloop planning lands, so operators can choose between the
legacy wallet-funded path and a future static-address-backed
path without relying on implicit fallback behavior.
2026-05-22 02:20:36 -05:00
Boris Nagaev
8d5cc2168a
cmd/loop: avoid escaped session text
Disable JSON HTML escaping for recorded session files and for the nested
session payloads rewritten by bless mode. This keeps CLI text such as
"> 1 sat/vByte" readable instead of turning it into "\u003e".

Keep the metadata field order aligned with existing fixtures so a bless
pass does not rewrite unrelated sessions just because the encoder changed.
2026-05-22 02:20:36 -05:00
Slyghtning
72a38c535b
Merge pull request #1133 from 0xfandom/refactor/1056-move-parameters-to-script
staticaddr: move address.Parameters to script package
2026-05-20 22:02:57 +02:00
Boris Nagaev
60d43898f0
Merge pull request #1072 from starius/cli-tests20
cmd/loop: CLI session recording and replay tests
2026-05-20 13:35:37 -05:00
Boris Nagaev
3abede2d8e
cmd/loop: fix openchannel help typo
Correct the duplicated article in the static openchannel
help text so the command summary reads cleanly. Refresh the
recorded help fixtures that surface that summary directly,
including the dedicated openchannel help output and the
parent static command listing.
2026-05-20 03:12:04 -05:00
Boris Nagaev
ca1ce698f0
cmd/loop: bless text-only session updates
Add an env-gated bless mode to recorded-session replay so
CLI-only text changes can refresh fixtures without a live
recording pass. The replay still uses the recorded gRPC
traffic, stdin, and environment, and it only rewrites
stdout, stderr, and run_error after the command preserves
the recorded success or failure shape.

Keep the updater strict by refusing to bless sessions when
replay leaves recorded gRPC events unconsumed, and add
focused tests for the rewrite rules. Document the bless
workflow next to the session fixtures, including the need
for -count=1 so the Go test cache does not skip updates.
2026-05-20 03:12:04 -05:00
Boris Nagaev
e6e3820a76
testdata: add sessions for newly added features 2026-05-20 03:12:04 -05:00
Boris Nagaev
7683da65ff
testdata: add static address and swaps sessions 2026-05-20 03:12:04 -05:00
Boris Nagaev
662f954e0f
testdata: add liquidity sessions 2026-05-20 03:12:03 -05:00
Boris Nagaev
541e0e00fc
testdata: add loop in/out and quote sessions 2026-05-20 03:12:03 -05:00
Boris Nagaev
074257e348
testdata: add instantout and l402 sessions 2026-05-20 03:12:03 -05:00
Boris Nagaev
5157dad741
testdata: add basic swap sessions 2026-05-20 03:12:03 -05:00
Boris Nagaev
9640e3bd4c
cmd/loop: add session recording guide 2026-05-20 03:12:03 -05:00
Boris Nagaev
c3009182b3
cmd/loop: add session replay tests 2026-05-20 03:12:03 -05:00
Boris Nagaev
137097915c
cmd/loop: add session recorder plumbing 2026-05-20 03:12:03 -05:00
Boris Nagaev
c3e102527a
cmd/loop: add session gRPC transport hook 2026-05-20 03:12:03 -05:00
Boris Nagaev
d0591e62b9
cmd/loop: add stdio hooks 2026-05-20 03:12:03 -05:00
Boris Nagaev
cdec0cacef
cmd/loop: add cli clock and hook 2026-05-20 03:12:03 -05:00
Boris Nagaev
3e9a99dd55
cmd/loop: factor newRootCommand 2026-05-20 03:12:03 -05:00
Boris Nagaev
74c7274b03
cmd/loop: add client conn cleanup 2026-05-20 03:12:03 -05:00
Boris Nagaev
3182be8bf1
cmd/loop: remove orphan flag access
abandonSwap used to access flag --id which does not exist.
2026-05-20 03:12:03 -05:00
Boris Nagaev
61945bd9f9
cmd/loop: print homedir in help messages as ~
Make the output homedir-independent to facilitate tests.
2026-05-20 03:12:03 -05:00
Slyghtning
ea03440f29
Merge pull request #1130 from hieblmi/fix-racae
sweepbatcher: harden AddSweep against ctx closure
2026-05-19 07:46:06 +02:00
Boris Nagaev
1b25d8a122
sweepbatcher: assert close-during-add run exit
TestSweepBatcherCloseDuringAdding previously started Batcher.Run in a
detached goroutine and called test assertions from that goroutine. The
test only waited for the add/cancel workers, so a Run-side shutdown
error could be missed or reported unreliably.

Route the Run result through a channel and wait for it in the main test
goroutine. While waiting, keep draining spend registrations so shutdown
cannot deadlock on mock notifier traffic.

This makes the existing shutdown-race test cover both sides of the race:
AddSweep callers may exit with cancellation, and Batcher.Run must also
terminate with an expected shutdown error.
2026-05-18 11:58:54 +02:00
Boris Nagaev
707fef340b
sweepbatcher: mask presign cancellation races
PresignSweepsGroup uses context-sensitive wallet and presigned-helper
calls, but it previously returned their raw wrapped errors even when the
caller context or batcher shutdown state had already become terminal.
That leaves backend/helper errors visible during normal cancellation.

Check for shutdown/cancellation before presigning and after fee lookup
or presigning failures, preferring context.Canceled or
ErrBatcherShuttingDown over lower-level errors.

Log the original presign-path error before returning the shutdown or
cancellation error so normal shutdown remains debuggable without
changing the returned error.

Add a regression test with a presigned helper that cancels the caller
context while returning driver.ErrBadConn from SignTx. The test asserts
PresignSweepsGroup reports context.Canceled and does not wrap the driver
error, and runs against both mock and SQL-backed stores.
2026-05-18 11:58:54 +02:00
Boris Nagaev
8a4e2f1941
sweepbatcher: normalize run-loop shutdown errors
AddSweep now masks cancellation races during its caller-side setup, but
a request can still be accepted by the batcher run loop before the run
context cancellation is selected. If handleSweeps then performs a
context-sensitive store call while shutdown is in progress, a
backend-specific error such as driver.ErrBadConn can still bubble out
through Batcher.Run.

Prefer the run context's terminal error when startup, handleSweeps, or
asynchronous errChan failures happen after the run context has been
canceled. This keeps normal shutdown reporting consistent and avoids
surfacing backend driver errors from the accepted-request and async
error paths.

Log the original run-loop error before returning the context error so
normal shutdown remains debuggable without changing the returned error.

Add regression tests for both covered run-loop races. One lets AddSweep
successfully hand a sweep request to Run, then cancels the run context
from the second GetSweepStatus call while returning driver.ErrBadConn.
The other queues an errChan error from the event loop while canceling
the run context. Both tests assert that Run returns context.Canceled and
does not wrap the driver error.
2026-05-18 11:58:54 +02:00
Slyghtning
b542b50f7f
sweepbatcher: harden AddSweep against ctx closure 2026-05-18 11:58:53 +02:00
Boris Nagaev
010212044a
Merge pull request #1140 from starius/fix-go-mod-check
go.mod: fix gonum replaces (fix CI)
2026-05-11 12:37:38 -05:00
Slyghtning
de279bca67
Merge pull request #1137 from hieblmi/param-validation
staticaddr: validate server address parameters
2026-05-11 09:52:42 +02:00
Slyghtning
6aa6979e77
staticaddr: validate server address parameters 2026-05-11 08:59:03 +02:00
Boris Nagaev
38481fe4a9
go.mod: fix gonum replaces
Remove gonum/plot replace in looprpc - not needed.

Add gonum replace to swapserverrpc. It was missing and caused recent CI errors.
2026-05-10 22:03:36 -05:00
Boris Nagaev
7c3eb29665
Merge pull request #1138 from starius/fix-perms
looprpc: fix build and permissions of two RPCs
2026-05-08 22:17:32 -05:00
Boris Nagaev
02dc98232c
looprpc: fix build
It depends on our protobuf-go fork. It failed with compilation errors when
building the module itself.
2026-05-07 02:37:00 -05:00
Boris Nagaev
e340797225
looprpc: fix permissions of two RPCs
Both SweepHtlc and StaticAddressLoopIn are active, so the "swap" entity
must have action=execute, not read.
2026-05-07 02:24:17 -05:00
Slyghtning
6ea1c79834
Merge pull request #1135 from hieblmi/risk-notif
swapserverrpc: risk acceptance/rejection notifications
2026-05-05 13:18:59 +02:00
Slyghtning
38654b91af
swapserverrpc: risk rejection notification 2026-05-05 11:37:06 +02:00
Slyghtning
1ec0f77a24
swapserverrpc: risk acceptance notification 2026-05-05 11:37:02 +02:00
0xfandom
8b89f77529 staticaddr: move address.Parameters to script package
The Parameters struct describes the keys, expiry and pkScript that
define the static address script, so its natural home is the script
package. Moving it there lets staticutil drop its dependency on the
address package and lets callers reuse a single type alongside
script.StaticAddress and script.NewStaticAddress.

No behavior change.

Closes #1056
2026-05-01 15:54:37 +05:30
Boris Nagaev
62177f9d62
Merge pull request #1122 from starius/addinvoice-coverage
loop-ins: fix route hints passing, add test coverage
2026-05-01 00:17:01 -05:00
Slyghtning
807aa61ae6
Merge pull request #1090 from Olexandr88/master
fix: guard against short reservation id when printing shortid
2026-04-29 16:33:28 +02:00
Olexandr88
6fd752f7e7
cmd/loop: fix panic on zero index in instantout reservation selection 2026-04-29 16:18:36 +02:00
Olexandr88
7b1c60dba2
fix: guard against short reservation id when printing shortid 2026-04-29 16:18:36 +02:00
Slyghtning
5b68d40920 Revert "Merge pull request #1095 from hieblmi/claude-review-approve"
This reverts commit 864029135a, reversing
changes made to ff800df41d.
2026-04-29 12:16:04 +02:00
Slyghtning
787519ea7c
Merge pull request #1125 from hieblmi/correct-static-text
cmd/loop: drop misleading static loop-in fee warning
2026-04-23 19:46:12 +02:00
Slyghtning
b5d26775a5
cmd/loop: drop misleading static loop-in fee warning 2026-04-23 19:21:49 +02:00
Slyghtning
c8ccc1eba9
Merge pull request #1127 from lightninglabs/dependabot/go_modules/looprpc/github.com/jackc/pgx/v5-5.9.2
build: bump github.com/jackc/pgx/v5 from 5.9.0 to 5.9.2 in /looprpc
2026-04-23 18:37:38 +02:00
Slyghtning
93669c2b84
build: sync root pgx dependency for looprpc bump 2026-04-23 08:07:45 +02:00
Slyghtning
d295fbc459
Merge pull request #1126 from lightninglabs/dependabot/go_modules/github.com/jackc/pgx/v5-5.9.2
build: bump github.com/jackc/pgx/v5 from 5.9.0 to 5.9.2
2026-04-23 07:59:14 +02:00
dependabot[bot]
fc47f8ee3a
build: bump github.com/jackc/pgx/v5 from 5.9.0 to 5.9.2 in /looprpc
Bumps [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) from 5.9.0 to 5.9.2.
- [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jackc/pgx/compare/v5.9.0...v5.9.2)

---
updated-dependencies:
- dependency-name: github.com/jackc/pgx/v5
  dependency-version: 5.9.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 00:34:27 +00:00
dependabot[bot]
0031ed5eba
build: bump github.com/jackc/pgx/v5 from 5.9.0 to 5.9.2
Bumps [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) from 5.9.0 to 5.9.2.
- [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jackc/pgx/compare/v5.9.0...v5.9.2)

---
updated-dependencies:
- dependency-name: github.com/jackc/pgx/v5
  dependency-version: 5.9.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 00:32:10 +00:00
Slyghtning
13ded17d9e
Merge pull request #1123 from lightninglabs/dependabot/go_modules/looprpc/github.com/jackc/pgx/v5-5.9.0
build: bump github.com/jackc/pgx/v5 from 5.7.4 to 5.9.0 in /looprpc
2026-04-21 09:40:03 +02:00
Slyghtning
7396653e55
build: update root pgx/v5 to 5.9.0 2026-04-21 09:22:56 +02:00
Boris Nagaev
51bb2b6ac5
staticaddr/loopin: add route hint regression test 2026-04-17 21:38:36 -05:00
Boris Nagaev
e1f1077f44
loop+test: add loop-in route hint regression 2026-04-17 21:38:36 -05:00
Boris Nagaev
bfbc180e4b
test: add route hint assertion helper 2026-04-17 21:38:36 -05:00
Boris Nagaev
118c73a0bd
go.mod: pin fixed lndclient addinvoice hints
Include https://github.com/lightninglabs/lndclient/pull/268
Preserve AddInvoiceData fields across invoice wrappers
2026-04-17 21:37:47 -05:00
dependabot[bot]
e8bae4380c
build: bump github.com/jackc/pgx/v5 from 5.7.4 to 5.9.0 in /looprpc
Bumps [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) from 5.7.4 to 5.9.0.
- [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jackc/pgx/compare/v5.7.4...v5.9.0)

---
updated-dependencies:
- dependency-name: github.com/jackc/pgx/v5
  dependency-version: 5.9.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 23:06:13 +00:00
Boris Nagaev
2f5e821049
Merge pull request #1111 from starius/fix-deposit-test
staticaddr: deflake deposit manager test
2026-04-14 10:06:52 -05:00
Boris Nagaev
aa699b16e6
staticaddr: deflake deposit manager test
The sqlite and postgres race jobs were both hanging in
deposit.TestManager. The test was observing the manager through
implementation details that were not safe to share with the manager
itself:

- it replaced the manager's internal finalizedDepositChan and then
  waited on the same channel the manager consumes
- it reused package-level block and confirmation channels across runs
- it treated confirmationHeight+expiry as the last pre-expiry block
  even though the production IsExpired check uses >=
- it relied on scheduler timing when asserting that no sign request
  had happened yet

Make the test assert on stable effects instead of internal channel
ownership:

- create per-test notifier channels in the test context
- run the manager from a cancellable t.Context-derived context and
  assert clean shutdown
- send the actual last pre-expiry height, then the expiry height
- wait for the expiry sign and publish steps with bounded timeouts
- verify finalization by waiting for the manager to remove the
  deposit from activeDeposits instead of racing its private
  finalization channel

This keeps the test aligned with the production expiry semantics
and removes the race that only showed up reliably under -race.
2026-04-14 04:10:33 -05:00
Boris Nagaev
bf47c92477
Merge pull request #1112 from starius/security-md
add SECURITY policy file
2026-04-14 04:05:46 -05:00
Slyghtning
d95da382d9
Merge pull request #1118 from lightninglabs/dependabot/go_modules/looprpc/go.opentelemetry.io/otel/sdk-1.43.0
build: bump go.opentelemetry.io/otel/sdk from 1.40.0 to 1.43.0 in /looprpc
2026-04-13 07:41:12 +02:00
dependabot[bot]
f128203ef3
build: bump go.opentelemetry.io/otel/sdk in /looprpc
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.40.0 to 1.43.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.40.0...v1.43.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.43.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-11 05:31:54 +00:00
Slyghtning
b5eeeecd6a
Merge pull request #1117 from lightninglabs/dependabot/go_modules/go.opentelemetry.io/otel/sdk-1.43.0
build: bump go.opentelemetry.io/otel/sdk from 1.40.0 to 1.43.0
2026-04-11 07:30:35 +02:00
dependabot[bot]
af79d5480c
build: bump go.opentelemetry.io/otel/sdk from 1.40.0 to 1.43.0
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.40.0 to 1.43.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.40.0...v1.43.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.43.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-08 21:53:40 +00:00
Boris Nagaev
1614375fd9
add SECURITY policy file
It refers to LND Security policy and asks not to open Github issues
for security and privacy issues.
2026-04-06 00:07:25 -05:00
408 changed files with 42532 additions and 3527 deletions

View file

@ -1,2 +1,3 @@
#### Pull Request Checklist #### Pull Request Checklist
- [ ] Update `release_notes.md` if your PR contains major features, breaking changes or bugfixes - [ ] Add an entry to `docs/release-notes/release-notes-next.md`, or apply the
`no-changelog` label (required by CI)

View file

@ -5,8 +5,6 @@ on:
types: [created] types: [created]
pull_request: pull_request:
types: [labeled, synchronize] types: [labeled, synchronize]
pull_request_target:
types: [labeled, synchronize]
jobs: jobs:
claude-review: claude-review:
@ -55,75 +53,3 @@ jobs:
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
claude-approve:
if: |
github.event_name == 'pull_request_target' &&
contains(github.event.pull_request.labels.*.name, 'low-risk') &&
(github.event.action == 'labeled' || github.event.action == 'synchronize')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review and Approve
id: claude-approve
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
Review this pull request thoroughly, checking for:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
Use the repository's AGENTS.md for guidance on style and conventions.
Classify the PR's intrinsic risk severity using one of:
- critical: must block merge
- high: large blast radius or very sensitive code paths
- medium: meaningful production or maintainability risk
- low: constrained blast radius and straightforward rollback
Intrinsic risk is about the impact and sensitivity of changed code,
even when no concrete bug is found.
Any changes touching fund movement, signing/sweeping, swap state
transitions, security/authz/authn logic, secrets handling, or DB
schema/migrations are at least medium risk.
Also classify each finding using one of:
- critical
- high
- medium
- low
- nit
Decision rule:
- Approve ONLY if intrinsic PR risk is low AND highest finding
severity is low or nit.
- If intrinsic PR risk is medium/high/critical, DO NOT approve and
submit a changes-requested review.
- If any finding is medium/high/critical, DO NOT approve and submit
a changes-requested review.
- If uncertain, treat as medium risk.
If approving:
gh pr review ${{ github.event.pull_request.number }} --approve --body "Claude review: intrinsic PR risk low and findings low/nit; safe for low-risk path."
If NOT approving:
gh pr review ${{ github.event.pull_request.number }} --request-changes --body "Claude review: not eligible for low-risk auto-approval. Include intrinsic PR risk and findings with severities."
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh pr review:*)"'

66
.github/workflows/gateway.yml vendored Normal file
View file

@ -0,0 +1,66 @@
name: gateway
# Opt-in code-review bot. Triggered by a `/gateway <command>` comment on a PR
# (e.g. `/gateway review`); review/approve commands are gated to maintainers.
# Comment-commands only — no pull_request triggers — so fork PRs (which receive
# no secrets) never spawn failing runs. v0.5.0 added the
# pull_request_review_comment trigger: /gateway dismiss, promote, and explain
# now also work as replies on a finding's inline thread (finding id inferred
# from the thread when omitted). Also a comment event — same fork-PR safety
# profile as issue_comment.
#
# Thin shim: the public lightninglabs/gateway-action mints an App token and
# checks out the private gateway runtime at execution time. The runtime stays
# private; only this entry point is public.
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
permissions:
# The action mints an App installation token internally; the GITHUB_TOKEN
# handed to this shim is unused, so we minimise it.
contents: read
jobs:
review:
# issue_comment fires for all issues and every PR comment. Filter to PR
# comments that look like a /gateway command so unrelated comments don't
# spin up a no-op runner. `contains` (not `startsWith`) because the runtime
# accepts the command at column 0 of any line, including multi-line bodies.
if: >-
${{
(github.event_name == 'issue_comment'
&& github.event.issue.pull_request != null
&& contains(github.event.comment.body, '/gateway')) ||
(github.event_name == 'pull_request_review_comment'
&& contains(github.event.comment.body, '/gateway'))
}}
runs-on: ubuntu-latest
timeout-minutes: 15
env:
GATEWAY_REVIEW_MODE: multi
steps:
- uses: lightninglabs/gateway-action@334a8455ee316e40668ae3ac85249150c62704ec # v0.6.0
with:
# Pin the private runtime to an immutable commit (matches the action
# SHA-pin above) so runtime upgrades go through a loop PR, not a moved
# tag. Without this, runtime_ref defaults to the v0.6.0 tag.
runtime_ref: 75f6e67deac362bdcfc10d10629ddcf69c0e2615 # gateway v0.6.0
event_name: ${{ github.event_name }}
event_action: ${{ github.event.action }}
repo: ${{ github.repository }}
pr_number: ${{ github.event.issue.number || github.event.pull_request.number }}
actor: ${{ github.event.sender.login }}
comment_body: ${{ github.event.comment.body }}
comment_id: ${{ github.event.comment.id }}
comment_in_reply_to: ${{ github.event.comment.in_reply_to_id }}
# installation_id intentionally omitted: as of gateway v0.4.4 the
# runtime resolves the App installation covering this repo from
# app_id/private_key, so a hardcoded (and easily wrong-org) id is no
# longer needed.
app_id: ${{ secrets.GATEWAY_APP_ID }}
private_key: ${{ secrets.GATEWAY_PRIVATE_KEY }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

View file

@ -24,6 +24,37 @@ env:
LITD_ITEST_BRANCH: master LITD_ITEST_BRANCH: master
jobs: jobs:
########################
# commit message lint
########################
commit-message:
name: Commit Message
runs-on: ubuntu-latest
steps:
- name: git checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Lint commit messages
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BEFORE_SHA: ${{ github.event.before }}
GITHUB_SHA: ${{ github.sha }}
run: |
if [ "$EVENT_NAME" = "pull_request" ]; then
range="$BASE_SHA..$HEAD_SHA"
elif [ "$EVENT_NAME" = "push" ] &&
[ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then
range="$BEFORE_SHA..$GITHUB_SHA"
else
range="$(git rev-parse HEAD^ 2>/dev/null)..HEAD"
fi
make commitmsg-lint range="$range"
######################## ########################
# RPC compile and check # RPC compile and check
######################## ########################
@ -124,6 +155,9 @@ jobs:
- name: check - name: check
run: make docs-check run: make docs-check
- name: check generated FSM diagrams
run: make fsm-check
######################## ########################
# run unit-test sqlite3 race # run unit-test sqlite3 race
######################## ########################

58
.github/workflows/release-notes.yml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Release Notes
on:
pull_request:
branches:
- "*"
types:
- opened
- reopened
- synchronize
- labeled
- unlabeled
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
release-notes:
name: Release Notes
runs-on: ubuntu-latest
steps:
- name: Skip release notes check
if: contains(github.event.pull_request.labels.*.name, 'no-changelog')
run: echo "Release notes check skipped by the no-changelog label."
- name: git checkout
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-changelog') }}
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for a release note
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-changelog') }}
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
RELEASE_NOTES_FILE: docs/release-notes/release-notes-next.md
run: |
if git diff --unified=0 "$BASE_SHA...$HEAD_SHA" -- \
"$RELEASE_NOTES_FILE" |
awk '
/^@@/ { in_hunk = 1; next }
in_hunk && /^\+/ {
line = substr($0, 2)
if (line ~ /[^[:space:]]/) found = 1
}
END { exit found ? 0 : 1 }
'
then
exit 0
fi
echo "::error file=$RELEASE_NOTES_FILE::Add a release note or apply the no-changelog label."
exit 1

View file

@ -3,7 +3,7 @@ run:
go: "1.26" go: "1.26"
# timeout for analysis # timeout for analysis
timeout: 4m timeout: 6m
linters: linters:
default: all default: all
@ -55,14 +55,16 @@ linters:
- wsl_v5 - wsl_v5
- noinlineerr - noinlineerr
settings: settings:
staticcheck:
checks:
- all
- -QF*
- -ST*
gosec: gosec:
excludes: excludes:
- G402 - G402
- G306 - G306
- G115 - G115
staticcheck:
checks:
- -SA1019
tagliatelle: tagliatelle:
case: case:
rules: rules:

View file

@ -51,3 +51,33 @@ The project is a client daemon (`loopd`) that connects to a user's `lnd` node an
* **Cryptography:** Extensively uses Taproot and MuSig2 for efficiency, privacy, and complex spending conditions, especially in the `instantout` and `staticaddr` features. * **Cryptography:** Extensively uses Taproot and MuSig2 for efficiency, privacy, and complex spending conditions, especially in the `instantout` and `staticaddr` features.
* **Labeling (`labels/`):** A utility to create and validate labels for swaps, which helps distinguish between user-initiated and automated swaps (e.g., `[reserved]: autoloop-out`). * **Labeling (`labels/`):** A utility to create and validate labels for swaps, which helps distinguish between user-initiated and automated swaps (e.g., `[reserved]: autoloop-out`).
* **Assets (`assets/`):** Contains logic for interacting with `tapd` (Taproot Assets Protocol Daemon), allowing Loop to facilitate swaps involving assets other than Bitcoin. * **Assets (`assets/`):** Contains logic for interacting with `tapd` (Taproot Assets Protocol Daemon), allowing Loop to facilitate swaps involving assets other than Bitcoin.
**5. Minimum `lnd` Version (`loopd/run.go`):**
`loopd` enforces a minimum `lnd` version at startup through
`LoopMinRequiredLndVersion` in `loopd/run.go` (handed to `lndclient` as
`CheckVersion`). This is a hard gate: loopd refuses to start against an older
`lnd` node.
**Maintenance rule:** whenever you start using an `lnd` gRPC method or message
field that does not exist in older `lnd`, bump `LoopMinRequiredLndVersion` to the
`lnd` release that introduced that API, and record which API drove the bump in the
comment above the variable. Pin it to the *real* floor of the APIs the client uses
— do **not** just track the `go.mod` dependency. Historically this value only
tracked `go.mod` and drifted out of sync with the APIs actually called (it sat at
0.17.0 while the client already depended on 0.18.4 APIs). To find the introducing
release, grep the field/method across `lnd` version tags, e.g.
`git grep <field> <tag> -- <proto-file>`. As of the current floor (**v0.18.4-beta**)
the binding dependencies are the asset loop-out fields
`routerrpc.SendPaymentRequest.first_hop_custom_records` and
`lnrpc.Route.custom_channel_data` (lnd v0.18.4-beta), plus the sweep-batcher fee
floor `walletrpc.EstimateFeeResponse.min_relay_fee_sat_per_kw` (lnd v0.18.3-beta).
**6. Pull Request Release Notes:**
Every pull request must either add at least one non-empty line to
`docs/release-notes/release-notes-next.md` or carry the `no-changelog` label.
Before finishing any change intended for a pull request, add a concise entry
under the appropriate section (`New Features`, `Breaking Changes`, `Bug Fixes`,
or `Maintenance`). For a change that does not warrant a release note, ensure the
pull request uses the `no-changelog` label instead. This is enforced by CI.

View file

@ -15,6 +15,15 @@ GOBUILD := CGO_ENABLED=0 GO111MODULE=on go build -v
GOINSTALL := CGO_ENABLED=0 GO111MODULE=on go install -v GOINSTALL := CGO_ENABLED=0 GO111MODULE=on go install -v
GOMOD := GO111MODULE=on go mod GOMOD := GO111MODULE=on go mod
# GitLab-backed module sources can reject CI runner traffic. Seed GitLab and
# modernc.org modules through the public Go proxy before the direct check.
MOD_CHECK_FILES := go.mod go.sum swapserverrpc/go.mod swapserverrpc/go.sum \
looprpc/go.mod looprpc/go.sum tools/go.mod tools/go.sum
MOD_CHECK_PREFETCH := $(shell awk '($$1 ~ /^(gitlab\.com\/|modernc\.org\/)/) { \
version=$$2; sub("/go.mod$$", "", version); \
if (version ~ /^v/) print $$1 "@" version; \
}' $(MOD_CHECK_FILES) | sort -u)
COMMIT := $(shell git describe --abbrev=40 --dirty --tags) COMMIT := $(shell git describe --abbrev=40 --dirty --tags)
COMMIT_HASH := $(shell git rev-parse HEAD) COMMIT_HASH := $(shell git rev-parse HEAD)
DIRTY := $(shell git diff-index --quiet HEAD -- || echo dirty) DIRTY := $(shell git diff-index --quiet HEAD -- || echo dirty)
@ -156,12 +165,52 @@ mod-tidy:
mod-check: mod-check:
@$(call print, "Checking modules.") @$(call print, "Checking modules.")
ifneq ($(MOD_CHECK_PREFETCH),)
@GOPROXY=https://proxy.golang.org $(GOMOD) download $(MOD_CHECK_PREFETCH)
endif
GOPROXY=direct $(GOMOD) tidy GOPROXY=direct $(GOMOD) tidy
cd swapserverrpc/ && GOPROXY=direct $(GOMOD) tidy cd swapserverrpc/ && GOPROXY=direct $(GOMOD) tidy
cd looprpc/ && GOPROXY=direct $(GOMOD) tidy cd looprpc/ && GOPROXY=direct $(GOMOD) tidy
cd tools/ && GOPROXY=direct $(GOMOD) tidy cd tools/ && GOPROXY=direct $(GOMOD) tidy
if test -n "$$(git status --porcelain)"; then echo "Running go mod tidy changes go.mod/go.sum"; git status; git diff; exit 1; fi if test -n "$$(git status --porcelain)"; then echo "Running go mod tidy changes go.mod/go.sum"; git status; git diff; exit 1; fi
commitmsg-lint:
@$(call print, "Linting commit message(s).")
@if [ -n "$(range)" ]; then \
./scripts/commit_message.py lint --range "$(range)"; \
elif [ -n "$(commit)" ]; then \
./scripts/commit_message.py lint --commit "$(commit)"; \
elif [ -n "$(file)" ]; then \
./scripts/commit_message.py lint --file "$(file)"; \
else \
./scripts/commit_message.py lint --commit HEAD; \
fi
commitmsg-fmt:
@$(call print, "Formatting commit message.")
@if [ -n "$(file)" ]; then \
if [ "$(inplace)" = "1" ]; then \
./scripts/commit_message.py fmt --file "$(file)" --in-place \
$(if $(filter 1,$(decode)),--decode-escaped-newlines,); \
else \
./scripts/commit_message.py fmt --file "$(file)" \
$(if $(filter 1,$(decode)),--decode-escaped-newlines,); \
fi; \
elif [ -n "$(commit)" ]; then \
./scripts/commit_message.py fmt --commit "$(commit)" \
$(if $(filter 1,$(decode)),--decode-escaped-newlines,); \
else \
echo "Error: provide file=<path> or commit=<rev>"; \
exit 1; \
fi
commitmsg-reword:
@$(call print, "Rewording commit with formatted message.")
@./scripts/commit_message.py reword \
--commit "$(if $(commit),$(commit),HEAD)" \
$(if $(filter 1,$(decode)),--decode-escaped-newlines,) \
$(if $(filter 1,$(dryrun)),--dry-run,)
sqlc: sqlc:
@$(call print, "Generating sql models and queries in Go") @$(call print, "Generating sql models and queries in Go")
./scripts/gen_sqlc_docker.sh ./scripts/gen_sqlc_docker.sh
@ -182,4 +231,20 @@ docs-check: docs
fsm: fsm:
@$(call print, "Generating state machine docs") @$(call print, "Generating state machine docs")
./scripts/fsm-generate.sh; ./scripts/fsm-generate.sh;
.PHONY: fsm
FSM_FILES := \
fsm/example_fsm.md \
instantout/fsm.md \
instantout/reservation/fsm.md \
staticaddr/deposit/fsm.md \
staticaddr/loopin/fsm.md
fsm-check: fsm
@$(call print, "Verifying generated state machine docs")
if test -n "$$(git status --porcelain -- $(FSM_FILES))"; then \
echo "Generated FSM diagrams are not up-to-date!"; \
git status --porcelain -- $(FSM_FILES); \
git diff -- $(FSM_FILES); \
exit 1; \
fi
.PHONY: fsm fsm-check

View file

@ -85,6 +85,8 @@ loopd --network=testnet
### Submit feature requests ### Submit feature requests
The [GitHub issue tracker](https://github.com/lightninglabs/loop/issues) can be The [GitHub issue tracker](https://github.com/lightninglabs/loop/issues) can be
used to request specific improvements or report bugs. used to request specific improvements or report bugs.
Do not use public issues for security disclosures.
Follow the instructions in [SECURITY.md](./SECURITY.md).
### Join us on Slack ### Join us on Slack
Join us on the Join us on the
@ -131,4 +133,4 @@ go install ./...
## Reproducible builds ## Reproducible builds
If you want to build release files yourself, follow If you want to build release files yourself, follow
[the guide](./docs/release.md). [the guide](./docs/reproducible_release.md).

9
SECURITY.md Normal file
View file

@ -0,0 +1,9 @@
# Security Policy
## Reporting a Vulnerability
If you believe you have found a security or privacy issue in Lightning Loop,
please do not open a public GitHub issue.
Instead, disclose it responsibly by following the instructions in the
[lnd security policy](https://github.com/lightningnetwork/lnd/blob/master/SECURITY.md).

View file

@ -4,14 +4,15 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math"
"math/big"
"os" "os"
"path/filepath"
"sync" "sync"
"time" "time"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmath"
"github.com/lightninglabs/taproot-assets/rpcutils"
"github.com/lightninglabs/taproot-assets/tapcfg"
"github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc"
"github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
@ -49,13 +50,24 @@ type TapdConfig struct {
// DefaultTapdConfig returns a default configuration to connect to a taproot // DefaultTapdConfig returns a default configuration to connect to a taproot
// assets daemon. // assets daemon.
func DefaultTapdConfig() *TapdConfig { func DefaultTapdConfig() *TapdConfig {
defaultConf := tapcfg.DefaultConfig() return DefaultTapdConfigForNetwork(
btcutil.AppDataDir("tapd", false), "mainnet",
)
}
// DefaultTapdConfigForNetwork returns the default tapd configuration rooted in
// defaultTapdDir for the given Bitcoin network. Passing the directory
// explicitly keeps path construction testable without accessing the user's
// real tapd data.
func DefaultTapdConfigForNetwork(defaultTapdDir, network string) *TapdConfig {
return &TapdConfig{ return &TapdConfig{
Activate: false, Activate: false,
Host: "localhost:10029", Host: "localhost:10029",
MacaroonPath: defaultConf.RpcConf.MacaroonPath, MacaroonPath: filepath.Join(
TLSPath: defaultConf.RpcConf.TLSCertPath, defaultTapdDir, "data", network, "admin.macaroon",
RFQtimeout: defaultRfqTimeout, ),
TLSPath: filepath.Join(defaultTapdDir, "tls.cert"),
RFQtimeout: defaultRfqTimeout,
} }
} }
@ -67,14 +79,19 @@ type TapdClient struct {
rfqrpc.RfqClient rfqrpc.RfqClient
universerpc.UniverseClient universerpc.UniverseClient
cfg *TapdConfig rfqTimeoutSeconds uint32
assetNameCache map[string]string assetNameCache map[string]string
assetNameMutex sync.Mutex assetNameMutex sync.RWMutex
cc *grpc.ClientConn cc *grpc.ClientConn
} }
// NewTapdClient returns a new taproot assets client. // NewTapdClient returns a new taproot assets client.
func NewTapdClient(config *TapdConfig) (*TapdClient, error) { func NewTapdClient(config *TapdConfig) (*TapdClient, error) {
rfqTimeoutSeconds, err := getRfqTimeoutSeconds(config.RFQtimeout)
if err != nil {
return nil, err
}
// Create the client connection to the server. // Create the client connection to the server.
conn, err := getClientConn(config) conn, err := getClientConn(config)
if err != nil { if err != nil {
@ -85,7 +102,7 @@ func NewTapdClient(config *TapdConfig) (*TapdClient, error) {
client := &TapdClient{ client := &TapdClient{
assetNameCache: make(map[string]string), assetNameCache: make(map[string]string),
cc: conn, cc: conn,
cfg: config, rfqTimeoutSeconds: rfqTimeoutSeconds,
TaprootAssetsClient: taprpc.NewTaprootAssetsClient(conn), TaprootAssetsClient: taprpc.NewTaprootAssetsClient(conn),
TaprootAssetChannelsClient: tapchannelrpc.NewTaprootAssetChannelsClient(conn), TaprootAssetChannelsClient: tapchannelrpc.NewTaprootAssetChannelsClient(conn),
PriceOracleClient: priceoraclerpc.NewPriceOracleClient(conn), PriceOracleClient: priceoraclerpc.NewPriceOracleClient(conn),
@ -128,7 +145,7 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context,
PeerPubKey: peerPubkey, PeerPubKey: peerPubkey,
PaymentMaxAmt: uint64(paymentMaxAmt), PaymentMaxAmt: uint64(paymentMaxAmt),
Expiry: uint64(expiry), Expiry: uint64(expiry),
TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()), TimeoutSeconds: c.rfqTimeoutSeconds,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -141,21 +158,26 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context,
rfq.GetRejectedQuote()) rfq.GetRejectedQuote())
} }
if rfq.GetAcceptedQuote() != nil { acceptedQuote := rfq.GetAcceptedQuote()
return rfq.GetAcceptedQuote(), nil if acceptedQuote == nil {
return nil, fmt.Errorf("no accepted quote")
} }
return nil, fmt.Errorf("no accepted quote") _, err = unmarshalAssetRate(acceptedQuote.BidAssetRate)
if err != nil {
return nil, fmt.Errorf("invalid accepted quote asset rate: %w",
err)
}
return acceptedQuote, nil
} }
// GetAssetName returns the human-readable name of the asset. // GetAssetName returns the human-readable name of the asset.
func (c *TapdClient) GetAssetName(ctx context.Context, func (c *TapdClient) GetAssetName(ctx context.Context,
assetId []byte) (string, error) { assetId []byte) (string, error) {
c.assetNameMutex.Lock()
defer c.assetNameMutex.Unlock()
assetIdStr := hex.EncodeToString(assetId) assetIdStr := hex.EncodeToString(assetId)
if name, ok := c.assetNameCache[assetIdStr]; ok { if name, ok := c.getCachedAssetName(assetIdStr); ok {
return name, nil return name, nil
} }
@ -181,11 +203,28 @@ func (c *TapdClient) GetAssetName(ctx context.Context,
assetName = assetStats.AssetStats[0].Asset.AssetName assetName = assetStats.AssetStats[0].Asset.AssetName
} }
c.assetNameCache[assetIdStr] = assetName c.cacheAssetName(assetIdStr, assetName)
return assetName, nil return assetName, nil
} }
// getCachedAssetName returns an asset name from the cache.
func (c *TapdClient) getCachedAssetName(assetID string) (string, bool) {
c.assetNameMutex.RLock()
defer c.assetNameMutex.RUnlock()
name, ok := c.assetNameCache[assetID]
return name, ok
}
// cacheAssetName adds an asset name to the cache.
func (c *TapdClient) cacheAssetName(assetID, name string) {
c.assetNameMutex.Lock()
defer c.assetNameMutex.Unlock()
c.assetNameCache[assetID] = name
}
// GetAssetPrice returns the price of an asset in satoshis. NOTE: this currently // GetAssetPrice returns the price of an asset in satoshis. NOTE: this currently
// uses the rfq process for the asset price. A future implementation should // uses the rfq process for the asset price. A future implementation should
// use a price oracle to not spam a peer. // use a price oracle to not spam a peer.
@ -209,7 +248,7 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string,
}, },
PaymentMaxAmt: uint64(msatAmt), PaymentMaxAmt: uint64(msatAmt),
Expiry: uint64(rfqExpiry), Expiry: uint64(rfqExpiry),
TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()), TimeoutSeconds: c.rfqTimeoutSeconds,
PeerPubKey: peerPubkey, PeerPubKey: peerPubkey,
}) })
if err != nil { if err != nil {
@ -243,7 +282,7 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string,
func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) ( func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) (
btcutil.Amount, error) { btcutil.Amount, error) {
rateFP, err := rpcutils.UnmarshalRfqFixedPoint(assetRate) rateFP, err := unmarshalAssetRate(assetRate)
if err != nil { if err != nil {
return 0, fmt.Errorf("cannot unmarshal asset rate: %w", err) return 0, fmt.Errorf("cannot unmarshal asset rate: %w", err)
} }
@ -255,6 +294,33 @@ func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) (
return msatAmt.ToSatoshis(), nil return msatAmt.ToSatoshis(), nil
} }
// unmarshalAssetRate validates and converts an RPC asset rate to the fixed
// point representation used for RFQ arithmetic.
func unmarshalAssetRate(assetRate *rfqrpc.FixedPoint) (
*rfqmath.BigIntFixedPoint, error) {
if assetRate == nil {
return nil, fmt.Errorf("asset rate cannot be nil")
}
if assetRate.Scale > math.MaxUint8 {
return nil, fmt.Errorf("scale value overflow: %v", assetRate.Scale)
}
coefficient, ok := new(big.Int).SetString(assetRate.Coefficient, 10)
if !ok {
return nil, fmt.Errorf("invalid asset rate coefficient: %q",
assetRate.Coefficient)
}
if coefficient.Sign() <= 0 {
return nil, fmt.Errorf("asset rate coefficient must be positive")
}
return &rfqmath.BigIntFixedPoint{
Coefficient: rfqmath.NewBigInt(coefficient),
Scale: uint8(assetRate.Scale),
}, nil
}
// getPaymentMaxAmount returns the milisat amount we are willing to pay for the // getPaymentMaxAmount returns the milisat amount we are willing to pay for the
// payment. // payment.
func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) (
@ -277,6 +343,26 @@ func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) (
) )
} }
// getRfqTimeoutSeconds converts the configured RFQ timeout to the whole
// seconds accepted by tapd. Fractional seconds are rounded up so tapd's
// timeout is never shorter than the configured duration.
func getRfqTimeoutSeconds(timeout time.Duration) (uint32, error) {
if timeout <= 0 {
return 0, fmt.Errorf("RFQ timeout must be greater than zero")
}
seconds := timeout / time.Second
if timeout%time.Second != 0 {
seconds++
}
if seconds > time.Duration(math.MaxUint32) {
return 0, fmt.Errorf("RFQ timeout exceeds maximum of %v seconds",
uint64(math.MaxUint32))
}
return uint32(seconds), nil
}
func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) { func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) {
// Load the specified TLS certificate and build transport credentials. // Load the specified TLS certificate and build transport credentials.
creds, err := credentials.NewClientTLSFromFile(config.TLSPath, "") creds, err := credentials.NewClientTLSFromFile(config.TLSPath, "")

View file

@ -1,14 +1,269 @@
package assets package assets
import ( import (
"context"
"encoding/hex"
"encoding/pem"
"math"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing" "testing"
"time"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/grpc"
"gopkg.in/macaroon.v2"
) )
type blockingUniverseClient struct {
universerpc.UniverseClient
queryStarted chan struct{}
releaseQuery chan struct{}
}
func (b *blockingUniverseClient) QueryAssetStats(context.Context,
*universerpc.AssetStatsQuery, ...grpc.CallOption) (
*universerpc.UniverseAssetStats, error) {
close(b.queryStarted)
<-b.releaseQuery
return &universerpc.UniverseAssetStats{
AssetStats: []*universerpc.AssetStatsSnapshot{
{
Asset: &universerpc.AssetStatsAsset{
AssetName: "queried asset",
},
},
},
}, nil
}
type staticRfqClient struct {
rfqrpc.RfqClient
response *rfqrpc.AddAssetSellOrderResponse
}
func (s *staticRfqClient) AddAssetSellOrder(context.Context,
*rfqrpc.AddAssetSellOrderRequest, ...grpc.CallOption) (
*rfqrpc.AddAssetSellOrderResponse, error) {
return s.response, nil
}
// TestDefaultTapdConfig tests that the default tapd connection paths match
// tapd's mainnet defaults.
func TestDefaultTapdConfig(t *testing.T) {
defaultTapdDir := btcutil.AppDataDir("tapd", false)
config := DefaultTapdConfig()
require.Equal(t, filepath.Join(
defaultTapdDir, "data", "mainnet", "admin.macaroon",
), config.MacaroonPath)
require.Equal(
t, filepath.Join(defaultTapdDir, "tls.cert"), config.TLSPath,
)
}
// TestTapdConfigClientConn tests that the default tapd file layout can be used
// to construct a client connection.
func TestTapdConfigClientConn(t *testing.T) {
// Use an isolated tapd root so the test never reads from or writes to a
// user's real tapd data directory.
defaultTapdDir := t.TempDir()
network := "regtest"
macaroonPath := filepath.Join(
defaultTapdDir, "data", network, "admin.macaroon",
)
require.NoError(t, os.MkdirAll(filepath.Dir(macaroonPath), 0o700))
// NewTapdClient parses the configured TLS certificate before creating
// its gRPC client. An httptest server provides a valid certificate
// without requiring a running tapd instance.
tlsServer := httptest.NewTLSServer(http.NotFoundHandler())
t.Cleanup(tlsServer.Close)
cert := tlsServer.Certificate()
certBytes := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Bytes: cert.Raw,
})
require.NoError(t, os.WriteFile(
filepath.Join(defaultTapdDir, "tls.cert"), certBytes, 0o600,
))
// Store a valid serialized macaroon at tapd's production path. This
// ensures connection setup tests the path itself rather than failing on
// malformed credentials.
mac, err := macaroon.New(
[]byte("root-key"), []byte("id"), "tapd",
macaroon.LatestVersion,
)
require.NoError(t, err)
macBytes, err := mac.MarshalBinary()
require.NoError(t, err)
require.NoError(t, os.WriteFile(macaroonPath, macBytes, 0o600))
// grpc.NewClient connects lazily, so constructing the tapd client verifies
// that both credentials can be loaded and parsed without needing a live
// tapd server.
config := DefaultTapdConfigForNetwork(defaultTapdDir, network)
client, err := NewTapdClient(config)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, client.cc.Close())
})
require.Equal(t, macaroonPath, config.MacaroonPath)
require.Equal(
t, filepath.Join(defaultTapdDir, "tls.cert"), config.TLSPath,
)
}
// TestGetAssetNameCachedLookupNotBlocked verifies that a slow universe query
// for one asset does not prevent another caller from reading a cached name.
func TestGetAssetNameCachedLookupNotBlocked(t *testing.T) {
const cachedName = "cached asset"
cachedAssetID := []byte{1}
queryStarted := make(chan struct{})
releaseQuery := make(chan struct{})
client := &TapdClient{
UniverseClient: &blockingUniverseClient{
queryStarted: queryStarted,
releaseQuery: releaseQuery,
},
assetNameCache: map[string]string{
hex.EncodeToString(cachedAssetID): cachedName,
},
}
queryResult := make(chan error, 1)
go func() {
_, err := client.GetAssetName(context.Background(), []byte{2})
queryResult <- err
}()
select {
case <-queryStarted:
case <-time.After(time.Second):
t.Fatal("universe query did not start")
}
type nameResult struct {
name string
err error
}
cachedResult := make(chan nameResult, 1)
go func() {
name, err := client.GetAssetName(
context.Background(), cachedAssetID,
)
cachedResult <- nameResult{name: name, err: err}
}()
select {
case result := <-cachedResult:
require.NoError(t, result.err)
require.Equal(t, cachedName, result.name)
case <-time.After(time.Second):
close(releaseQuery)
t.Fatal("cached lookup blocked behind universe query")
}
close(releaseQuery)
require.NoError(t, <-queryResult)
}
// TestGetRfqForAssetValidatesRate verifies that malformed accepted quote rates
// are rejected before they reach downstream RFQ arithmetic.
func TestGetRfqForAssetValidatesRate(t *testing.T) {
tests := []struct {
name string
assetRate *rfqrpc.FixedPoint
expectError bool
}{
{
name: "valid",
assetRate: &rfqrpc.FixedPoint{
Coefficient: "100000", Scale: 0,
},
},
{
name: "nil",
assetRate: nil,
expectError: true,
},
{
name: "malformed coefficient",
assetRate: &rfqrpc.FixedPoint{
Coefficient: "not-a-number", Scale: 0,
},
expectError: true,
},
{
name: "zero coefficient",
assetRate: &rfqrpc.FixedPoint{
Coefficient: "0", Scale: 0,
},
expectError: true,
},
{
name: "negative coefficient",
assetRate: &rfqrpc.FixedPoint{
Coefficient: "-1", Scale: 0,
},
expectError: true,
},
{
name: "scale overflow",
assetRate: &rfqrpc.FixedPoint{
Coefficient: "1", Scale: 256,
},
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
acceptedQuote := &rfqrpc.PeerAcceptedSellQuote{
BidAssetRate: test.assetRate,
}
acceptedResponse :=
&rfqrpc.AddAssetSellOrderResponse_AcceptedQuote{
AcceptedQuote: acceptedQuote,
}
client := &TapdClient{
RfqClient: &staticRfqClient{
response: &rfqrpc.AddAssetSellOrderResponse{
Response: acceptedResponse,
},
},
rfqTimeoutSeconds: 60,
}
quote, err := client.GetRfqForAsset(
context.Background(), 1000, []byte{1}, []byte{2},
time.Now().Add(time.Minute).Unix(), 1,
)
if test.expectError {
require.Error(t, err)
require.Nil(t, quote)
return
}
require.NoError(t, err)
require.Same(t, acceptedQuote, quote)
})
}
}
func TestGetPaymentMaxAmount(t *testing.T) { func TestGetPaymentMaxAmount(t *testing.T) {
tests := []struct { tests := []struct {
satAmount btcutil.Amount satAmount btcutil.Amount
@ -68,6 +323,62 @@ func TestGetPaymentMaxAmount(t *testing.T) {
} }
} }
// TestGetRfqTimeoutSeconds verifies that configured durations are safely
// converted to tapd's whole-second timeout field.
func TestGetRfqTimeoutSeconds(t *testing.T) {
tests := []struct {
name string
timeout time.Duration
expectedSeconds uint32
expectError bool
}{
{
name: "whole seconds",
timeout: 60 * time.Second,
expectedSeconds: 60,
},
{
name: "sub-second rounded up",
timeout: time.Millisecond,
expectedSeconds: 1,
},
{
name: "fractional second rounded up",
timeout: time.Second + time.Nanosecond,
expectedSeconds: 2,
},
{
name: "zero",
timeout: 0,
expectError: true,
},
{
name: "negative",
timeout: -time.Second,
expectError: true,
},
{
name: "overflow",
timeout: time.Duration(math.MaxUint32)*time.Second +
time.Nanosecond,
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
seconds, err := getRfqTimeoutSeconds(test.timeout)
if test.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, test.expectedSeconds, seconds)
})
}
}
func TestGetSatsFromAssetAmt(t *testing.T) { func TestGetSatsFromAssetAmt(t *testing.T) {
tests := []struct { tests := []struct {
assetAmt uint64 assetAmt uint64
@ -93,6 +404,39 @@ func TestGetSatsFromAssetAmt(t *testing.T) {
expected: btcutil.Amount(0), expected: btcutil.Amount(0),
expectError: false, expectError: false,
}, },
{
assetAmt: 1000,
assetRate: nil,
expectError: true,
},
{
assetAmt: 1000,
assetRate: &rfqrpc.FixedPoint{
Coefficient: "not-a-number", Scale: 0,
},
expectError: true,
},
{
assetAmt: 1000,
assetRate: &rfqrpc.FixedPoint{
Coefficient: "0", Scale: 0,
},
expectError: true,
},
{
assetAmt: 1000,
assetRate: &rfqrpc.FixedPoint{
Coefficient: "-1", Scale: 0,
},
expectError: true,
},
{
assetAmt: 1000,
assetRate: &rfqrpc.FixedPoint{
Coefficient: "1", Scale: 256,
},
expectError: true,
},
} }
for _, test := range tests { for _, test := range tests {

View file

@ -43,16 +43,10 @@ func forceAutoloop(ctx context.Context, cmd *cli.Command) error {
} }
func getDebugClient(ctx context.Context, cmd *cli.Command) (looprpc.DebugClient, func(), error) { func getDebugClient(ctx context.Context, cmd *cli.Command) (looprpc.DebugClient, func(), error) {
rpcServer := cmd.String("rpcserver") conn, cleanup, err := sessionTransport.Dial(cmd)
tlsCertPath, macaroonPath, err := extractPathArgs(cmd)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
conn, err := getClientConn(rpcServer, tlsCertPath, macaroonPath)
if err != nil {
return nil, nil, err
}
cleanup := func() { conn.Close() }
debugClient := looprpc.NewDebugClient(conn) debugClient := looprpc.NewDebugClient(conn)
return debugClient, cleanup, nil return debugClient, cleanup, nil

View file

@ -0,0 +1,82 @@
package main
import (
"errors"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
// TestDefaultPathText verifies HOME-based path elision behavior used for help
// defaults without relying on the real environment.
func TestDefaultPathText(t *testing.T) {
sep := string(filepath.Separator)
home := filepath.Clean(sep + "home" + sep + "alice")
homeDir := func() (string, error) { return home, nil }
tests := []struct {
name string
value string
homeFn func() (string, error)
want string
}{
{
name: "empty value",
value: "",
homeFn: func() (string, error) {
return home, nil
},
want: "",
},
{
name: "nil homedir func",
value: home + sep + "data",
homeFn: nil,
want: home + sep + "data",
},
{
name: "homedir error",
value: home + sep + "data",
homeFn: func() (string, error) {
return "", errors.New("homedir error")
},
want: home + sep + "data",
},
{
name: "exact home",
value: home,
homeFn: homeDir,
want: "~",
},
{
name: "home prefix",
value: home + sep + "dir" + sep + "file",
homeFn: homeDir,
want: "~" + sep + "dir" + sep + "file",
},
{
name: "non-home path",
value: filepath.Clean(sep + "var" + sep + "tmp"),
homeFn: homeDir,
want: filepath.Clean(sep + "var" + sep + "tmp"),
},
{
name: "prefix but not path segment",
value: home + "x" + sep + "dir",
homeFn: homeDir,
want: home + "x" + sep + "dir",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := defaultPathText(test.value, test.homeFn)
require.Equalf(
t, test.want, got,
"defaultPathText(%q)", test.value,
)
})
}
}

View file

@ -88,6 +88,12 @@ func instantOut(ctx context.Context, cmd *cli.Command) error {
fmt.Printf("Available reservations: \n\n") fmt.Printf("Available reservations: \n\n")
for _, res := range confirmedReservations { for _, res := range confirmedReservations {
idx++ idx++
if len(res.ReservationId) != reservation.IdLength {
return fmt.Errorf("invalid reservation id length: "+
"got %d, expected %d", len(res.ReservationId),
reservation.IdLength)
}
fmt.Printf("Reservation %v: shortid %x, amt %v, expiry "+ fmt.Printf("Reservation %v: shortid %x, amt %v, expiry "+
"height %v \n", idx, res.ReservationId[:3], res.Amount, "height %v \n", idx, res.ReservationId[:3], res.Amount,
res.Expiry) res.Expiry)
@ -131,7 +137,7 @@ func instantOut(ctx context.Context, cmd *cli.Command) error {
if err != nil { if err != nil {
return err return err
} }
if idx < 0 { if idx <= 0 {
return fmt.Errorf("invalid index %v", idx) return fmt.Errorf("invalid index %v", idx)
} }
@ -177,6 +183,8 @@ func instantOut(ctx context.Context, cmd *cli.Command) error {
fmt.Println("Starting instant swap out") fmt.Println("Starting instant swap out")
maxSwapFee := quote.ServiceFeeSat
// Now we can request the instant out swap. // Now we can request the instant out swap.
instantOutRes, err := client.InstantOut( instantOutRes, err := client.InstantOut(
ctx, ctx,
@ -184,6 +192,9 @@ func instantOut(ctx context.Context, cmd *cli.Command) error {
ReservationIds: selectedReservations, ReservationIds: selectedReservations,
OutgoingChanSet: outgoingChanSet, OutgoingChanSet: outgoingChanSet,
DestAddr: cmd.String("addr"), DestAddr: cmd.String("addr"),
MaxSwapFee: &looprpc.InstantOutRequest_MaxSwapFeeSat{
MaxSwapFeeSat: maxSwapFee,
},
}, },
) )
if err != nil { if err != nil {

View file

@ -340,6 +340,12 @@ var setParamsCommand = &cli.Command{
Usage: "the confirmation target for loop in on-chain " + Usage: "the confirmation target for loop in on-chain " +
"htlcs.", "htlcs.",
}, },
&cli.StringFlag{
Name: "loopinsource",
Usage: "the loop-in source to use for autoloop rules: " +
"wallet or static-address. Static-address " +
"requires loopd --experimental.",
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "easyautoloop", Name: "easyautoloop",
Usage: "set to true to enable easy autoloop, which " + Usage: "set to true to enable easy autoloop, which " +
@ -555,6 +561,25 @@ func setParams(ctx context.Context, cmd *cli.Command) error {
flagSet = true flagSet = true
} }
if cmd.IsSet("loopinsource") {
switch cmd.String("loopinsource") {
case "wallet":
params.LoopInSource =
looprpc.LoopInSource_LOOP_IN_SOURCE_WALLET
case "static-address", "static_address", "static":
params.LoopInSource =
looprpc.LoopInSource_LOOP_IN_SOURCE_STATIC_ADDRESS
default:
return fmt.Errorf("unknown loopinsource value %q "+
"(use \"wallet\" or \"static-address\")",
cmd.String("loopinsource"))
}
flagSet = true
}
// If we are setting easy autoloop parameters, we need to ensure that // If we are setting easy autoloop parameters, we need to ensure that
// the asset ID is set, and that we have a valid entry in our params // the asset ID is set, and that we have a valid entry in our params
// map. // map.

View file

@ -207,7 +207,7 @@ func loopIn(ctx context.Context, cmd *cli.Command) error {
} }
fmt.Printf("Swap initiated\n") fmt.Printf("Swap initiated\n")
fmt.Printf("ID: %v\n", resp.Id) fmt.Printf("ID: %x\n", resp.IdBytes)
if resp.HtlcAddressP2Tr != "" { if resp.HtlcAddressP2Tr != "" {
fmt.Printf("HTLC address (P2TR): %v\n", resp.HtlcAddressP2Tr) fmt.Printf("HTLC address (P2TR): %v\n", resp.HtlcAddressP2Tr)

View file

@ -248,9 +248,9 @@ func loopOut(ctx context.Context, cmd *cli.Command) error {
// Set our maximum swap wait time. If a fast swap is requested we set // Set our maximum swap wait time. If a fast swap is requested we set
// it to now, otherwise to 30 minutes in the future. // it to now, otherwise to 30 minutes in the future.
fast := cmd.Bool("fast") fast := cmd.Bool("fast")
swapDeadline := time.Now() swapDeadline := cliClock.Now()
if !fast { if !fast {
swapDeadline = time.Now().Add(defaultSwapWaitTime) swapDeadline = cliClock.Now().Add(defaultSwapWaitTime)
} }
sweepConfTarget := int32(cmd.Uint64("conf_target")) sweepConfTarget := int32(cmd.Uint64("conf_target"))

View file

@ -6,11 +6,12 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"os/signal"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
@ -19,6 +20,7 @@ import (
"github.com/lightninglabs/loop/loopd" "github.com/lightninglabs/loop/loopd"
"github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/macaroons"
@ -52,10 +54,11 @@ var (
defaultInitiator = "loop-cli" defaultInitiator = "loop-cli"
loopDirFlag = &cli.StringFlag{ loopDirFlag = &cli.StringFlag{
Name: "loopdir", Name: "loopdir",
Value: loopd.LoopDirBase, Value: loopd.LoopDirBase,
Usage: "path to loop's base directory", DefaultText: defaultPathText(loopd.LoopDirBase, os.UserHomeDir),
Sources: cli.EnvVars(envVarLoopDir), Usage: "path to loop's base directory",
Sources: cli.EnvVars(envVarLoopDir),
} }
networkFlag = &cli.StringFlag{ networkFlag = &cli.StringFlag{
Name: "network", Name: "network",
@ -66,15 +69,21 @@ var (
} }
tlsCertFlag = &cli.StringFlag{ tlsCertFlag = &cli.StringFlag{
Name: "tlscertpath", Name: "tlscertpath",
Usage: "path to loop's TLS certificate", Usage: "path to loop's TLS certificate",
Value: loopd.DefaultTLSCertPath, Value: loopd.DefaultTLSCertPath,
DefaultText: defaultPathText(
loopd.DefaultTLSCertPath, os.UserHomeDir,
),
Sources: cli.EnvVars(envVarTLSCertPath), Sources: cli.EnvVars(envVarTLSCertPath),
} }
macaroonPathFlag = &cli.StringFlag{ macaroonPathFlag = &cli.StringFlag{
Name: "macaroonpath", Name: "macaroonpath",
Usage: "path to macaroon file", Usage: "path to macaroon file",
Value: loopd.DefaultMacaroonPath, Value: loopd.DefaultMacaroonPath,
DefaultText: defaultPathText(
loopd.DefaultMacaroonPath, os.UserHomeDir,
),
Sources: cli.EnvVars(envVarMacaroonPath), Sources: cli.EnvVars(envVarMacaroonPath),
} }
verboseFlag = &cli.BoolFlag{ verboseFlag = &cli.BoolFlag{
@ -92,8 +101,53 @@ var (
instantOutCommand, listInstantOutsCommand, stopCommand, instantOutCommand, listInstantOutsCommand, stopCommand,
printManCommand, printMarkdownCommand, printManCommand, printMarkdownCommand,
} }
// cliClock provides the time source used by CLI commands.
cliClock clock.Clock = clock.NewDefaultClock()
// sessionRec is the active recorder when session capture is enabled.
sessionRec *sessionRecorder
// forceDeterministicJSON is enabled by tests to obtain stable JSON
// output.
forceDeterministicJSON bool
) )
// installSessionSignalHandler records signals and cancels the root context.
func installSessionSignalHandler(cancel context.CancelFunc) func() {
if sessionRec == nil {
return func() {}
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
done := make(chan struct{})
go func() {
defer close(done)
interrupted := false
for sig := range sigCh {
sessionRec.LogSignal(sig)
if !interrupted {
interrupted = true
cancel()
continue
}
// A second signal requests immediate shutdown. Finalize the
// session first because os.Exit skips deferred cleanup.
_ = sessionRec.Finalize(fmt.Errorf("signal: %s", sig))
os.Exit(130)
}
}()
return func() {
signal.Stop(sigCh)
close(sigCh)
<-done
}
}
const ( const (
// satAmtFmt formats a satoshi value into a one line string, intended to // satAmtFmt formats a satoshi value into a one line string, intended to
@ -132,6 +186,37 @@ const (
envVarMacaroonPath = "LOOPCLI_MACAROONPATH" envVarMacaroonPath = "LOOPCLI_MACAROONPATH"
) )
// defaultPathText returns a help-friendly path string that replaces the user's
// home directory with "~". The homeDir function is injected so callers can
// control environment-dependent behavior in tests.
func defaultPathText(value string, homeDir func() (string, error)) string {
if value == "" {
return value
}
if homeDir == nil {
return value
}
home, err := homeDir()
if err != nil || home == "" {
return value
}
cleanHome := filepath.Clean(home)
cleanValue := filepath.Clean(value)
if cleanValue == cleanHome {
return "~"
}
prefix := cleanHome + string(filepath.Separator)
if suffix, ok := strings.CutPrefix(cleanValue, prefix); ok {
return "~" + string(filepath.Separator) + suffix
}
return value
}
func printJSON(resp any) { func printJSON(resp any) {
b, err := json.Marshal(resp) b, err := json.Marshal(resp)
if err != nil { if err != nil {
@ -144,7 +229,8 @@ func printJSON(resp any) {
fatal(err) fatal(err)
} }
out.WriteString("\n") out.WriteString("\n")
_, _ = out.WriteTo(os.Stdout) printBytes := maybeNormalizeJSON(out.Bytes())
_, _ = os.Stdout.Write(printBytes)
} }
func printRespJSON(resp proto.Message) { func printRespJSON(resp proto.Message) {
@ -154,16 +240,76 @@ func printRespJSON(resp proto.Message) {
return return
} }
fmt.Println(string(jsonBytes)) fmt.Println(string(maybeNormalizeJSON(jsonBytes)))
} }
func fatal(err error) { func fatal(err error) {
fmt.Fprintf(os.Stderr, "[loop] %v\n", err) fmt.Fprintf(os.Stderr, "[loop] %v\n", err)
if sessionRec != nil {
if finalizeErr := sessionRec.Finalize(err); finalizeErr != nil {
fmt.Fprintf(os.Stderr, "[loop] unable to finalize "+
"session: %v\n", finalizeErr)
}
}
os.Exit(1) os.Exit(1)
} }
func main() { func main() {
rootCmd := &cli.Command{ var err error
sessionRec, err = newSessionRecorder(os.Args)
if err != nil {
fatal(err)
}
// Intercept clock and stdio if needed.
if sessionRec != nil {
restoreClock := hookClock(
clock.NewTestClock(sessionRec.ClockStart()),
)
defer restoreClock()
if err := sessionRec.Start(nil, nil, nil); err != nil {
fatal(err)
}
}
// Intercept clock calls if needed.
var restoreTransport func()
if sessionRec != nil {
restoreTransport = hookGrpc(sessionRec)
defer restoreTransport()
}
rootCmd := newRootCommand()
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if sessionRec != nil {
ctx = sessionRec.InjectContext(ctx)
}
if sessionRec != nil {
signalStop := installSessionSignalHandler(cancel)
defer signalStop()
}
if err := rootCmd.Run(ctx, os.Args); err != nil {
fatal(err)
}
if sessionRec != nil {
if err := sessionRec.Finalize(nil); err != nil {
fmt.Fprintf(os.Stderr, "[loop] unable to finalize "+
"session: %v\n", err)
}
}
}
// newRootCommand constructs the CLI root command for loop.
func newRootCommand() *cli.Command {
return &cli.Command{
Name: "loop", Name: "loop",
Usage: "control plane for your loopd", Usage: "control plane for your loopd",
Version: loop.RichVersion(), Version: loop.RichVersion(),
@ -184,17 +330,11 @@ func main() {
return cli.ShowRootCommandHelp(cmd) return cli.ShowRootCommandHelp(cmd)
}, },
} }
if err := rootCmd.Run(context.Background(), os.Args); err != nil {
fatal(err)
}
} }
// getClient establishes a SwapClient RPC connection and returns the client and // getClient establishes a SwapClient RPC connection and returns the client and
// a cleanup handler. // a cleanup handler.
func getClient(cmd *cli.Command) (looprpc.SwapClientClient, func getClient(cmd *cli.Command) (looprpc.SwapClientClient, func(), error) {
func(), error) {
client, _, cleanup, err := getClientWithConn(cmd) client, _, cleanup, err := getClientWithConn(cmd)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -206,23 +346,54 @@ func getClient(cmd *cli.Command) (looprpc.SwapClientClient,
// getClientWithConn returns both the SwapClient RPC client and the underlying // getClientWithConn returns both the SwapClient RPC client and the underlying
// gRPC connection so callers can perform connection-aware actions. // gRPC connection so callers can perform connection-aware actions.
func getClientWithConn(cmd *cli.Command) (looprpc.SwapClientClient, func getClientWithConn(cmd *cli.Command) (looprpc.SwapClientClient,
*grpc.ClientConn, func(), error) { daemonConn, func(), error) {
rpcServer := cmd.String("rpcserver") conn, cleanup, err := sessionTransport.Dial(cmd)
tlsCertPath, macaroonPath, err := extractPathArgs(cmd)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
conn, err := getClientConn(rpcServer, tlsCertPath, macaroonPath)
if err != nil {
return nil, nil, nil, err
}
cleanup := func() { conn.Close() }
loopClient := looprpc.NewSwapClientClient(conn) loopClient := looprpc.NewSwapClientClient(conn)
return loopClient, conn, cleanup, nil return loopClient, conn, cleanup, nil
} }
// hookClock overrides cliClock until the returned callback is called.
func hookClock(c clock.Clock) func() {
prev := cliClock
cliClock = c
return func() {
cliClock = prev
}
}
// maybeNormalizeJSON rewrites JSON output to avoid the build-dependent spacing
// introduced by google.golang.org/protobuf/internal/encoding/json (see
// WriteName in protobuf-go-hex-display/internal/encoding/json/encode.go, which
// uses internal/detrand.Bool). When recording or replaying sessions we ensure
// stable output by re-encoding with the standard library.
func maybeNormalizeJSON(raw []byte) []byte {
if sessionRec == nil && !forceDeterministicJSON {
return raw
}
var parsed any
if err := json.Unmarshal(raw, &parsed); err != nil {
return raw
}
normalized, err := json.MarshalIndent(parsed, "", " ")
if err != nil {
return raw
}
if len(raw) > 0 && raw[len(raw)-1] == '\n' {
normalized = append(normalized, '\n')
}
return normalized
}
func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount { func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
return swap.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate) return swap.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate)
} }
@ -329,12 +500,6 @@ func displayInDetails(req *looprpc.QuoteRequest,
"wallet.\n\n") "wallet.\n\n")
} }
if req.DepositOutpoints != nil {
fmt.Printf("On-chain fees for static address loop-ins are not " +
"included.\nThey were already paid when the deposits " +
"were created.\n\n")
}
printQuoteInResp(req, resp, verbose) printQuoteInResp(req, resp, verbose)
fmt.Printf("\nCONTINUE SWAP? (y/n): ") fmt.Printf("\nCONTINUE SWAP? (y/n): ")
@ -395,10 +560,7 @@ func parseAmt(text string) (btcutil.Amount, error) {
func logSwap(swap *looprpc.SwapStatus) { func logSwap(swap *looprpc.SwapStatus) {
// If our swap failed, we add our failure reason to the state. // If our swap failed, we add our failure reason to the state.
swapState := fmt.Sprintf("%v", swap.State) swapState := monitorSwapState(swap)
if swap.State == looprpc.SwapState_FAILED {
swapState = fmt.Sprintf("%v (%v)", swapState, swap.FailureReason)
}
if swap.Type == looprpc.SwapType_LOOP_OUT { if swap.Type == looprpc.SwapType_LOOP_OUT {
fmt.Printf("%v %v %v %v - %v", fmt.Printf("%v %v %v %v - %v",
@ -420,10 +582,32 @@ func logSwap(swap *looprpc.SwapStatus) {
} }
} }
if swap.State != looprpc.SwapState_INITIATED && showCost := shouldShowSwapCost(swap.GetState())
swap.State != looprpc.SwapState_HTLC_PUBLISHED && if swap.Type == looprpc.SwapType_STATIC_LOOP_IN {
swap.State != looprpc.SwapState_PREIMAGE_REVEALED { 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)", fmt.Printf(" (cost: server %v, onchain %v, offchain %v)",
swap.CostServer, swap.CostOnchain, swap.CostOffchain, swap.CostServer, swap.CostOnchain, swap.CostOffchain,
) )
@ -432,13 +616,46 @@ func logSwap(swap *looprpc.SwapStatus) {
fmt.Println() fmt.Println()
} }
func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn, // monitorSwapState returns the static loop-in FSM label for static swaps and
error) { // 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) {
// We always need to send a macaroon. // We always need to send a macaroon.
macOption, err := readMacaroon(macaroonPath) macOption, err := readMacaroon(macaroonPath)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
opts := []grpc.DialOption{ opts := []grpc.DialOption{
@ -446,28 +663,40 @@ func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn,
macOption, macOption,
} }
// Install gRPC interceptors for session recording if needed.
if unary := sessionTransport.UnaryInterceptor(); unary != nil {
opts = append(opts, grpc.WithChainUnaryInterceptor(unary))
}
if stream := sessionTransport.StreamInterceptor(); stream != nil {
opts = append(opts, grpc.WithChainStreamInterceptor(stream))
}
// Since TLS cannot be disabled, we'll always have a cert file to read. // Since TLS cannot be disabled, we'll always have a cert file to read.
creds, err := credentials.NewClientTLSFromFile(tlsCertPath, "") creds, err := credentials.NewClientTLSFromFile(tlsCertPath, "")
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
opts = append(opts, grpc.WithTransportCredentials(creds)) opts = append(opts, grpc.WithTransportCredentials(creds))
conn, err := grpc.NewClient(address, opts...) conn, err := grpc.NewClient(address, opts...)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to create RPC client: %v", return nil, nil, fmt.Errorf("unable to create RPC client: %v",
err) err)
} }
return conn, nil cleanup := func() {
_ = conn.Close()
}
return conn, cleanup, nil
} }
// readMacaroon tries to read the macaroon file at the specified path and create // readMacaroon tries to read the macaroon file at the specified path and create
// gRPC dial options from it. // gRPC dial options from it.
func readMacaroon(macPath string) (grpc.DialOption, error) { func readMacaroon(macPath string) (grpc.DialOption, error) {
// Load the specified macaroon file. // Load the specified macaroon file.
macBytes, err := ioutil.ReadFile(macPath) macBytes, err := os.ReadFile(macPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to read macaroon path : %v", err) return nil, fmt.Errorf("unable to read macaroon path : %v", err)
} }

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

@ -7,24 +7,22 @@ import (
"strconv" "strconv"
"github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/looprpc"
lndcommands "github.com/lightningnetwork/lnd/cmd/commands" "github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
const ( const (
defaultUtxoMinConf = 1 defaultUtxoMinConf = 1
)
var (
channelTypeTweakless = "tweakless" channelTypeTweakless = "tweakless"
channelTypeAnchors = "anchors" channelTypeAnchors = "anchors"
channelTypeSimpleTaproot = "taproot" channelTypeSimpleTaproot = "simple-taproot"
channelTypeTaproot = "taproot"
) )
var openChannelCommand = &cli.Command{ var openChannelCommand = &cli.Command{
Name: "openchannel", Name: "openchannel",
Usage: "Open a channel to a an existing peer.", Usage: "Open a channel to an existing peer.",
Description: ` Description: `
Attempt to open a new channel to an existing peer with the key Attempt to open a new channel to an existing peer with the key
node-key. node-key.
@ -137,9 +135,9 @@ var openChannelCommand = &cli.Command{
&cli.StringFlag{ &cli.StringFlag{
Name: "channel_type", Name: "channel_type",
Usage: fmt.Sprintf("(optional) the type of channel to "+ Usage: fmt.Sprintf("(optional) the type of channel to "+
"propose to the remote peer (%q, %q, %q)", "propose to the remote peer (%q, %q, %q, %q)",
channelTypeTweakless, channelTypeAnchors, channelTypeTweakless, channelTypeAnchors,
channelTypeSimpleTaproot), channelTypeSimpleTaproot, channelTypeTaproot),
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "zero_conf", Name: "zero_conf",
@ -160,11 +158,11 @@ var openChannelCommand = &cli.Command{
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "memo", Name: "memo",
Usage: `(optional) a note-to-self containing some useful Usage: "(optional) a note-to-self containing some useful " +
information about the channel. This is stored "information about the channel. This is stored " +
locally only, and is purely for reference. It "locally only, and is purely for reference. It " +
has no bearing on the channel's operation. Max "has no bearing on the channel's operation. Max " +
allowed length is 500 characters`, "allowed length is 500 characters",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "fundmax", Name: "fundmax",
@ -271,7 +269,7 @@ func openChannel(ctx context.Context, cmd *cli.Command) error {
if cmd.IsSet("utxo") { if cmd.IsSet("utxo") {
utxos := cmd.StringSlice("utxo") utxos := cmd.StringSlice("utxo")
outpoints, err := lndcommands.UtxosToOutpoints(utxos) outpoints, err := lnd.UtxosToOutpoints(utxos)
if err != nil { if err != nil {
return fmt.Errorf("unable to decode utxos: %w", err) return fmt.Errorf("unable to decode utxos: %w", err)
} }
@ -322,6 +320,7 @@ func openChannel(ctx context.Context, cmd *cli.Command) error {
switch channelType { switch channelType {
case "": case "":
break break
case channelTypeTweakless: case channelTypeTweakless:
req.CommitmentType = lnrpc.CommitmentType_STATIC_REMOTE_KEY req.CommitmentType = lnrpc.CommitmentType_STATIC_REMOTE_KEY
@ -330,6 +329,10 @@ func openChannel(ctx context.Context, cmd *cli.Command) error {
case channelTypeSimpleTaproot: case channelTypeSimpleTaproot:
req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT
case channelTypeTaproot:
req.CommitmentType = lnrpc.CommitmentType_TAPROOT
default: default:
return fmt.Errorf("unsupported channel type %v", channelType) return fmt.Errorf("unsupported channel type %v", channelType)
} }

View file

@ -206,9 +206,9 @@ func quoteOut(ctx context.Context, cmd *cli.Command) error {
defer cleanup() defer cleanup()
fast := cmd.Bool("fast") fast := cmd.Bool("fast")
swapDeadline := time.Now() swapDeadline := cliClock.Now()
if !fast { if !fast {
swapDeadline = time.Now().Add(defaultSwapWaitTime) swapDeadline = cliClock.Now().Add(defaultSwapWaitTime)
} }
quoteReq := &looprpc.QuoteRequest{ quoteReq := &looprpc.QuoteRequest{

View file

@ -0,0 +1,619 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"slices"
"strconv"
"testing"
"github.com/stretchr/testify/require"
)
// updateRecordedSessionsEnvVar enables replay bless mode for text-only fixture
// updates.
const updateRecordedSessionsEnvVar = "LOOP_UPDATE_RECORDED_SESSIONS"
// replayedSessionOutput captures the user-visible output produced by an
// offline session replay.
type replayedSessionOutput struct {
stdout string
stderr string
stdoutChunks []string
stderrChunks []string
runError *string
}
// updateRecordedSessionsEnabled reports whether replay should bless text-only
// fixture updates.
func updateRecordedSessionsEnabled() (bool, error) {
raw, ok := os.LookupEnv(updateRecordedSessionsEnvVar)
if !ok {
return false, nil
}
enabled, err := strconv.ParseBool(raw)
if err != nil {
return false, fmt.Errorf("invalid %s value %q",
updateRecordedSessionsEnvVar, raw)
}
return enabled, nil
}
// sessionFixturePath resolves a relative fixture path under
// cmd/loop/testdata/sessions to an absolute path.
func sessionFixturePath(rel string) (string, error) {
_, filename, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("locate session fixture dir")
}
loopDir := filepath.Dir(filename)
return filepath.Join(
loopDir, "testdata", "sessions", filepath.FromSlash(rel),
), nil
}
// loadSessionFilePath reads and decodes a session fixture from disk.
func loadSessionFilePath(path string) (sessionFile, error) {
blob, err := os.ReadFile(path)
if err != nil {
return sessionFile{}, err
}
var fixture sessionFile
if err := json.Unmarshal(blob, &fixture); err != nil {
return sessionFile{}, err
}
return fixture, nil
}
// writeSessionFilePath writes a session fixture using the recorder's JSON
// formatting.
func writeSessionFilePath(path string, fixture sessionFile) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
return encoder.Encode(fixture)
}
// maybeUpdateSessionFixture rewrites the text-only portions of a recorded
// session if the replay output changed.
func maybeUpdateSessionFixture(path string, fixture sessionFile,
output replayedSessionOutput) (bool, error) {
updated, changed, err := rewriteSessionFixture(fixture, output)
if err != nil || !changed {
return changed, err
}
if err := writeSessionFilePath(path, updated); err != nil {
return false, err
}
return true, nil
}
// rewriteSessionFixture rewrites stdout, stderr and run_error while leaving
// the rest of the recorded interaction unchanged.
func rewriteSessionFixture(fixture sessionFile,
output replayedSessionOutput) (sessionFile, bool, error) {
updated := fixture
changed := false
if !optionalTextEqual(updated.Metadata.RunError, output.runError) {
updated.Metadata.RunError = cloneOptionalString(output.runError)
changed = true
}
var err error
updated.Events, changed, err = rewriteExitEventRunError(
updated.Events, output.runError, changed,
)
if err != nil {
return sessionFile{}, false, err
}
updated.Events, changed, err = rewriteTextEvents(
updated.Events, eventStdout, fixtureStdout(fixture),
output.stdout, output.stdoutChunks, changed,
)
if err != nil {
return sessionFile{}, false, err
}
updated.Events, changed, err = rewriteTextEvents(
updated.Events, eventStderr, fixtureStderr(fixture),
output.stderr, output.stderrChunks, changed,
)
if err != nil {
return sessionFile{}, false, err
}
return updated, changed, nil
}
// rewriteExitEventRunError updates the exit payload to match the replayed run
// error.
func rewriteExitEventRunError(events []sessionEvent, runError *string,
changed bool) ([]sessionEvent, bool, error) {
data, err := marshalSessionJSON(exitPayload{
RunError: cloneOptionalString(runError),
})
if err != nil {
return nil, changed, err
}
updated := append([]sessionEvent(nil), events...)
for i, event := range slices.Backward(updated) {
if event.Kind != eventExit {
continue
}
if bytes.Equal(event.Data, data) {
return updated, changed, nil
}
updated[i].Data = data
return updated, true, nil
}
updated = append(updated, sessionEvent{
Kind: eventExit,
Data: data,
})
return updated, true, nil
}
// rewriteTextEvents updates one text stream when the normalized aggregate text
// changed.
func rewriteTextEvents(events []sessionEvent, kind, recorded, actual string,
chunks []string, changed bool) ([]sessionEvent, bool, error) {
normalizedEqual := normalizeTimestamps(recorded) ==
normalizeTimestamps(actual)
sourceChunks := chunks
sourceCombined := actual
if normalizedEqual {
sourceChunks = fixtureTextChunksByKind(events, kind)
sourceCombined = recorded
}
replacement, err := canonicalTextEvents(
events, kind, sourceChunks, sourceCombined,
)
if err != nil {
return nil, changed, err
}
if normalizedEqual && textEventsMatch(events, kind, replacement) {
return events, changed, nil
}
updated, err := replaceTextEvents(events, kind, replacement)
if err != nil {
return nil, changed, err
}
return updated, true, nil
}
// canonicalTextEvents builds the canonical replacement events for one text
// stream.
func canonicalTextEvents(events []sessionEvent, kind string, chunks []string,
combined string) ([]sessionEvent, error) {
indices := textEventIndices(events, kind)
replacement := replacementTextChunks(chunks, combined)
if len(indices) != len(replacement) {
replacement = replacementTextChunks(nil, combined)
}
return buildReplacementTextEvents(events, indices, kind, replacement)
}
// replaceTextEvents rewrites the recorded events for a text stream using the
// provided canonical replacement events.
func replaceTextEvents(events []sessionEvent, kind string,
replacements []sessionEvent) ([]sessionEvent, error) {
indices := textEventIndices(events, kind)
if len(indices) == 0 && len(replacements) == 0 {
return append([]sessionEvent(nil), events...), nil
}
if replacements == nil {
replacements = []sessionEvent{}
}
return replaceTextEventsWithCanonical(
events, kind, indices, replacements,
)
}
// replaceTextEventsWithCanonical splices canonical replacement events into the
// event stream.
func replaceTextEventsWithCanonical(events []sessionEvent, kind string,
indices []int, replacements []sessionEvent) ([]sessionEvent, error) {
if len(indices) == len(replacements) {
updated := append([]sessionEvent(nil), events...)
for i, idx := range indices {
updated[idx].Data = replacements[i].Data
}
return updated, nil
}
if len(indices) > 0 {
firstIdx := indices[0]
updated := make([]sessionEvent, 0,
len(events)-len(indices)+len(replacements))
inserted := false
for i, event := range events {
if event.Kind == kind {
if !inserted && i == firstIdx {
updated = append(updated, replacements...)
inserted = true
}
continue
}
updated = append(updated, event)
}
return updated, nil
}
if len(replacements) == 0 {
return append([]sessionEvent(nil), events...), nil
}
insertAt := len(events)
for i, event := range events {
if event.Kind == eventExit {
insertAt = i
break
}
}
updated := make([]sessionEvent, 0, len(events)+len(replacements))
updated = append(updated, events[:insertAt]...)
updated = append(updated, replacements...)
updated = append(updated, events[insertAt:]...)
return updated, nil
}
// textEventsMatch reports whether the existing events already use the canonical
// representation for a text stream.
func textEventsMatch(events []sessionEvent, kind string,
replacements []sessionEvent) bool {
indices := textEventIndices(events, kind)
if len(indices) != len(replacements) {
return false
}
for i, idx := range indices {
if events[idx].TimeMS != replacements[i].TimeMS {
return false
}
if !bytes.Equal(events[idx].Data, replacements[i].Data) {
return false
}
}
return true
}
// buildReplacementTextEvents creates replacement text events while reusing the
// closest recorded timestamps when possible.
func buildReplacementTextEvents(events []sessionEvent, indices []int,
kind string, chunks []string) ([]sessionEvent, error) {
if len(chunks) == 0 {
return nil, nil
}
replacements := make([]sessionEvent, 0, len(chunks))
for i, chunk := range chunks {
data, err := marshalSessionJSON(newTextPayload(chunk))
if err != nil {
return nil, err
}
replacements = append(replacements, sessionEvent{
TimeMS: replacementEventTime(events, indices, i),
Kind: kind,
Data: data,
})
}
return replacements, nil
}
// replacementEventTime picks a stable timestamp for a replacement text event.
func replacementEventTime(events []sessionEvent, indices []int, idx int) int64 {
if len(indices) == 0 {
return 0
}
if idx < len(indices) {
return events[indices[idx]].TimeMS
}
return events[indices[len(indices)-1]].TimeMS
}
// textEventIndices returns the indexes of text events with the given kind.
func textEventIndices(events []sessionEvent, kind string) []int {
var indices []int
for i, event := range events {
if event.Kind == kind {
indices = append(indices, i)
}
}
return indices
}
// replacementTextChunks returns the recorded write chunks to persist. If the
// hook callbacks did not observe any chunks, fall back to the aggregate text so
// the fixture still captures the visible output.
func replacementTextChunks(chunks []string, combined string) []string {
if len(chunks) > 0 {
return append([]string(nil), chunks...)
}
if combined == "" {
return nil
}
return []string{combined}
}
// fixtureStdout returns the concatenated stdout text stored in a fixture.
func fixtureStdout(fixture sessionFile) string {
return fixtureTextByKind(fixture.Events, eventStdout)
}
// fixtureStderr returns the concatenated stderr text stored in a fixture.
func fixtureStderr(fixture sessionFile) string {
return fixtureTextByKind(fixture.Events, eventStderr)
}
// fixtureTextByKind concatenates all text payloads for a given event kind.
func fixtureTextByKind(events []sessionEvent, kind string) string {
var out bytes.Buffer
for _, text := range fixtureTextChunksByKind(events, kind) {
out.WriteString(text)
}
return out.String()
}
// fixtureTextChunksByKind returns the decoded text for each event of a given
// kind.
func fixtureTextChunksByKind(events []sessionEvent, kind string) []string {
var chunks []string
for _, event := range events {
if event.Kind != kind {
continue
}
var payload textPayload
if err := json.Unmarshal(event.Data, &payload); err != nil {
continue
}
chunks = append(chunks, payload.text())
}
return chunks
}
// optionalTextEqual compares optional text values using the same timestamp
// normalization rules as replay assertions.
func optionalTextEqual(a, b *string) bool {
switch {
case a == nil && b == nil:
return true
case a == nil || b == nil:
return false
}
return normalizeTimestamps(*a) == normalizeTimestamps(*b)
}
// cloneOptionalString copies an optional string.
func cloneOptionalString(value *string) *string {
if value == nil {
return nil
}
cloned := *value
return &cloned
}
// TestUpdateRecordedSessionsEnabled verifies update-mode env parsing.
func TestUpdateRecordedSessionsEnabled(t *testing.T) {
if current, ok := os.LookupEnv(updateRecordedSessionsEnvVar); ok {
t.Setenv(updateRecordedSessionsEnvVar, current)
} else {
t.Setenv(updateRecordedSessionsEnvVar, "false")
}
_ = os.Unsetenv(updateRecordedSessionsEnvVar)
enabled, err := updateRecordedSessionsEnabled()
require.NoError(t, err)
require.False(t, enabled)
t.Setenv(updateRecordedSessionsEnvVar, "true")
enabled, err = updateRecordedSessionsEnabled()
require.NoError(t, err)
require.True(t, enabled)
t.Setenv(updateRecordedSessionsEnvVar, "invalid")
enabled, err = updateRecordedSessionsEnabled()
require.False(t, enabled)
require.EqualError(t, err,
"invalid LOOP_UPDATE_RECORDED_SESSIONS value \"invalid\"")
}
// TestRewriteSessionFixtureUpdatesMatchingChunks verifies that bless mode
// rewrites text and run errors in place when the write counts are unchanged.
func TestRewriteSessionFixtureUpdatesMatchingChunks(t *testing.T) {
oldErr := "old error"
newErr := "new error"
fixture := sessionFile{
Metadata: sessionMetadata{
RunError: &oldErr,
},
Events: []sessionEvent{
textEvent(t, 5, eventStdout, "old stdout\n"),
textEvent(t, 6, eventStderr, "old stderr\n"),
exitEvent(t, 7, &oldErr),
},
}
updated, changed, err := rewriteSessionFixture(fixture,
replayedSessionOutput{
stdout: "new stdout\n",
stderr: "new stderr\n",
stdoutChunks: []string{"new stdout\n"},
stderrChunks: []string{"new stderr\n"},
runError: &newErr,
},
)
require.NoError(t, err)
require.True(t, changed)
require.Equal(t, &newErr, updated.Metadata.RunError)
require.Equal(t, int64(5), updated.Events[0].TimeMS)
require.Equal(t, "new stdout\n", textEventText(t, updated.Events[0]))
require.Equal(t, int64(6), updated.Events[1].TimeMS)
require.Equal(t, "new stderr\n", textEventText(t, updated.Events[1]))
require.Equal(t, &newErr, exitEventRunError(t, updated.Events[2]))
}
// TestRewriteSessionFixtureReplacesChangedChunkCount verifies that bless mode
// collapses mismatched write counts to one text event to keep fixture diffs
// small.
func TestRewriteSessionFixtureReplacesChangedChunkCount(t *testing.T) {
fixture := sessionFile{
Events: []sessionEvent{
textEvent(t, 10, eventStdout, "old "),
textEvent(t, 11, eventStdout, "text\n"),
exitEvent(t, 12, nil),
},
}
updated, changed, err := rewriteSessionFixture(fixture,
replayedSessionOutput{
stdout: "new output\n",
stdoutChunks: []string{"new ", "output", "\n"},
},
)
require.NoError(t, err)
require.True(t, changed)
require.Len(t, updated.Events, 2)
require.Equal(t, "new output\n", textEventText(t, updated.Events[0]))
require.Equal(t, int64(10), updated.Events[0].TimeMS)
require.Equal(t, eventExit, updated.Events[1].Kind)
}
// TestRewriteSessionFixtureSkipsNormalizedTimestampNoise verifies that bless
// mode does not churn fixtures when only timestamp formatting differs.
func TestRewriteSessionFixtureSkipsNormalizedTimestampNoise(t *testing.T) {
fixture := sessionFile{
Events: []sessionEvent{
textEvent(t, 3, eventStdout,
"2026-05-04T10:00:00-05:00\n"),
exitEvent(t, 4, nil),
},
}
updated, changed, err := rewriteSessionFixture(fixture,
replayedSessionOutput{
stdout: "2026-05-04T15:00:00Z\n",
stdoutChunks: []string{"2026-05-04T15:00:00Z\n"},
},
)
require.NoError(t, err)
require.False(t, changed)
require.Equal(t, fixture, updated)
}
// textEvent builds a text session event for tests.
func textEvent(t *testing.T, timeMS int64, kind, text string) sessionEvent {
t.Helper()
data, err := marshalSessionJSON(newTextPayload(text))
require.NoError(t, err)
return sessionEvent{
TimeMS: timeMS,
Kind: kind,
Data: data,
}
}
// exitEvent builds an exit session event for tests.
func exitEvent(t *testing.T, timeMS int64, runError *string) sessionEvent {
t.Helper()
data, err := marshalSessionJSON(exitPayload{
RunError: cloneOptionalString(runError),
})
require.NoError(t, err)
return sessionEvent{
TimeMS: timeMS,
Kind: eventExit,
Data: data,
}
}
// textEventText decodes a text payload for assertions.
func textEventText(t *testing.T, event sessionEvent) string {
t.Helper()
var payload textPayload
require.NoError(t, json.Unmarshal(event.Data, &payload))
return payload.text()
}
// exitEventRunError decodes an exit payload for assertions.
func exitEventRunError(t *testing.T, event sessionEvent) *string {
t.Helper()
var payload exitPayload
require.NoError(t, json.Unmarshal(event.Data, &payload))
return payload.RunError
}

View file

@ -0,0 +1,712 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/lightninglabs/loop"
"github.com/urfave/cli/v3"
statuspb "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
grpcstatus "google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
const (
sessionEnvVar = "LOOP_SESSION_RECORD"
sessionDefaultDir = "cmd/loop/testdata/sessions"
sessionFileExt = ".json"
defaultSessionSlug = "session"
)
// Event identifiers in recorded sessions.
const (
eventStdout = "stdout"
eventStderr = "stderr"
eventStdin = "stdin"
eventGrpc = "grpc"
eventExit = "exit"
eventSignal = "signal"
)
// grpcMarshalOptions is marshalling options to encode gRPC messages in recorded
// sessions.
var grpcMarshalOptions = protojson.MarshalOptions{
UseProtoNames: true,
EmitUnpopulated: true,
}
// marshalSessionJSON encodes nested session payloads without HTML escaping so
// recorded fixture strings stay byte-for-byte close to the CLI text.
func marshalSessionJSON(value any) ([]byte, error) {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(value); err != nil {
return nil, err
}
return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil
}
// sessionRecorder captures CLI IO and gRPC traffic for replay.
type sessionRecorder struct {
mu sync.Mutex
started time.Time
filePath string
slug string
metadata sessionMetadata
events []sessionEvent
eventErr error
finalizeOnce sync.Once
hooksMu sync.Mutex
hooksStarted bool
stdoutUnhook func() error
stderrUnhook func() error
stdinUnhook func() error
}
// sessionFile is a single recorded CLI session in JSON format. They are
// stored inside sessionDefaultDir.
type sessionFile struct {
Metadata sessionMetadata `json:"metadata"`
Events []sessionEvent `json:"events"`
}
// sessionMetadata stores static session details and runtime metadata.
type sessionMetadata struct {
Args []string `json:"args"`
Env map[string]string `json:"env"`
Version string `json:"version"`
RunError *string `json:"run_error,omitempty"`
Duration *time.Duration `json:"duration,omitempty"`
ClockStartUnix int64 `json:"clock_start_unix"`
}
// sessionEvent records a single timestamped payload entry.
type sessionEvent struct {
TimeMS int64 `json:"time_ms"`
Kind string `json:"kind"`
Data json.RawMessage `json:"data"`
}
// textPayload records stdout/stderr text chunks as newline-preserving lines.
type textPayload struct {
Lines []string `json:"lines"`
}
// newTextPayload splits text into newline-preserving lines for fixture output.
func newTextPayload(text string) textPayload {
return textPayload{
Lines: splitTextLines(text),
}
}
// text joins a recorded text payload back into the original output.
func (p textPayload) text() string {
return joinTextLines(p.Lines)
}
// stdinPayload records stdin chunks.
type stdinPayload struct {
Text string `json:"text"`
}
// splitTextLines preserves line endings while splitting text for fixture
// storage.
func splitTextLines(text string) []string {
if text == "" {
return nil
}
lines := strings.SplitAfter(text, "\n")
if lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
return lines
}
// joinTextLines reconstructs text stored as newline-preserving lines.
func joinTextLines(lines []string) string {
return strings.Join(lines, "")
}
// grpcPayload records a gRPC message or error event.
type grpcPayload struct {
Method string `json:"method"`
Event string `json:"event"`
MessageType string `json:"message_type,omitempty"`
Payload json.RawMessage `json:"payload,omitempty"`
Error string `json:"error,omitempty"`
Status *statuspb.Status `json:"status,omitempty"`
}
// exitPayload records the final run error, if any.
type exitPayload struct {
RunError *string `json:"run_error,omitempty"`
}
// signalPayload records handled signals.
type signalPayload struct {
Signal string `json:"signal"`
}
// newSessionRecorder creates a recorder when session recording is enabled.
func newSessionRecorder(args []string) (*sessionRecorder, error) {
// Session recording is disabled unless the env var is set.
envValue, ok := os.LookupEnv(sessionEnvVar)
if !ok {
return nil, nil
}
// Allow explicit disable by setting the env var to false.
enabled, err := strconv.ParseBool(envValue)
if err != nil {
return nil, fmt.Errorf("invalid %s value %q", sessionEnvVar,
envValue)
}
if !enabled {
return nil, nil
}
// Initialize the recorder before collecting metadata.
recorder := &sessionRecorder{
started: time.Now(),
}
// Derive the slug before resolving the output file path.
recorder.slug = deriveSessionSlug(args)
// Capture metadata that remains stable for the session.
metadata := sessionMetadata{
Args: append([]string(nil), args...),
Env: collectSessionEnv(),
Version: loop.RichVersion(),
ClockStartUnix: recorder.started.Unix(),
}
recorder.metadata = metadata
// Resolve the session file location.
baseDir, fileName, err := recorder.resolveFilePath()
if err != nil {
return nil, err
}
recorder.filePath = filepath.Join(baseDir, fileName)
return recorder, nil
}
// collectSessionEnv extracts the environment variables recorded in sessions.
func collectSessionEnv() map[string]string {
env := make(map[string]string)
// Record only LOOPCLI_ variables, excluding the recording toggle.
for _, kv := range os.Environ() {
parts := strings.SplitN(kv, "=", 2)
if len(parts) != 2 {
continue
}
key := parts[0]
value := parts[1]
if key == sessionEnvVar {
continue
}
if strings.HasPrefix(key, "LOOPCLI_") {
env[key] = value
}
}
return env
}
// resolveFilePath chooses the output directory and filename.
func (r *sessionRecorder) resolveFilePath() (string, string, error) {
if err := ensureSessionBaseDir(sessionDefaultDir); err != nil {
return "", "", err
}
counter, err := nextSessionCounter(sessionDefaultDir)
if err != nil {
return "", "", err
}
slug := r.slug
if slug == "" {
slug = defaultSessionSlug
}
name := fmt.Sprintf("%02d_%s%s", counter, slug, sessionFileExt)
return sessionDefaultDir, name, nil
}
// ensureSessionBaseDir verifies that recording is running from the repo root.
func ensureSessionBaseDir(baseDir string) error {
info, err := os.Stat(baseDir)
if errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("%s does not exist; run session recording "+
"from the repository root", baseDir)
}
if err != nil {
return err
}
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", baseDir)
}
return nil
}
// logEvent records a new event with the elapsed timestamp.
func (r *sessionRecorder) logEvent(kind string, payload any) {
data, err := marshalSessionJSON(payload)
if err != nil {
r.mu.Lock()
if r.eventErr == nil {
r.eventErr = fmt.Errorf("marshal session %s event: %w",
kind, err)
}
r.mu.Unlock()
return
}
event := sessionEvent{
TimeMS: time.Since(r.started).Milliseconds(),
Kind: kind,
Data: data,
}
r.mu.Lock()
defer r.mu.Unlock()
r.events = append(r.events, event)
}
// Start attaches stdin/stdout/stderr hooks for session recording.
func (r *sessionRecorder) Start(stdinSource io.Reader,
stdoutForward, stderrForward io.Writer) error {
r.hooksMu.Lock()
defer r.hooksMu.Unlock()
if r.hooksStarted {
return nil
}
// Capture stdout and stderr first, then stdin.
origStdout := os.Stdout
if stdoutForward == nil {
stdoutForward = origStdout
}
outHook, err := hookStdout(origStdout, stdoutForward, func(p []byte) {
r.logEvent(eventStdout, newTextPayload(string(p)))
})
if err != nil {
return err
}
origStderr := os.Stderr
if stderrForward == nil {
stderrForward = origStderr
}
errHook, err := hookStderr(origStderr, stderrForward, func(p []byte) {
r.logEvent(eventStderr, newTextPayload(string(p)))
})
if err != nil {
_ = outHook()
return err
}
origStdin := os.Stdin
if stdinSource == nil {
stdinSource = origStdin
}
stdinHook, err := hookStdin(origStdin, stdinSource, func(p []byte) {
r.logEvent(eventStdin, stdinPayload{Text: string(p)})
})
if err != nil {
_ = errHook()
_ = outHook()
return err
}
r.stdoutUnhook = outHook
r.stderrUnhook = errHook
r.stdinUnhook = stdinHook
r.hooksStarted = true
return nil
}
// stopHooks detaches any active IO hooks.
func (r *sessionRecorder) stopHooks() error {
r.hooksMu.Lock()
defer r.hooksMu.Unlock()
var firstErr error
if r.stdoutUnhook != nil {
if err := r.stdoutUnhook(); err != nil && firstErr == nil {
firstErr = err
}
r.stdoutUnhook = nil
}
if r.stderrUnhook != nil {
if err := r.stderrUnhook(); err != nil && firstErr == nil {
firstErr = err
}
r.stderrUnhook = nil
}
if r.stdinUnhook != nil {
if err := r.stdinUnhook(); err != nil && firstErr == nil {
firstErr = err
}
r.stdinUnhook = nil
}
r.hooksStarted = false
return firstErr
}
// logExit records the final outcome and duration.
func (r *sessionRecorder) logExit(runErr error) {
var payload exitPayload
if runErr != nil {
msg := runErr.Error()
payload.RunError = &msg
}
// Store the exit event first for the event stream.
r.logEvent(eventExit, payload)
// Update metadata with the final run state.
duration := time.Since(r.started)
r.mu.Lock()
defer r.mu.Unlock()
if runErr != nil {
msg := runErr.Error()
r.metadata.RunError = &msg
} else {
r.metadata.RunError = nil
}
r.metadata.Duration = &duration
}
// finalize writes the recorded session to disk once.
func (r *sessionRecorder) finalize(runErr error) error {
var finalizeErr error
r.finalizeOnce.Do(func() {
hookErr := r.stopHooks()
r.logExit(runErr)
r.mu.Lock()
metadata := r.metadata
events := append([]sessionEvent(nil), r.events...)
eventErr := r.eventErr
r.mu.Unlock()
fileContent := sessionFile{
Metadata: metadata,
Events: events,
}
err := os.MkdirAll(filepath.Dir(r.filePath), 0o755)
if err != nil {
finalizeErr = errors.Join(err, eventErr, hookErr)
return
}
file, err := os.Create(r.filePath)
if err != nil {
finalizeErr = errors.Join(err, eventErr, hookErr)
return
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
if err := encoder.Encode(fileContent); err != nil {
finalizeErr = errors.Join(err, eventErr, hookErr)
return
}
finalizeErr = errors.Join(eventErr, hookErr)
})
return finalizeErr
}
// Finalize records the exit event and flushes the session to disk.
func (r *sessionRecorder) Finalize(runErr error) error {
return r.finalize(runErr)
}
// ClockStart returns the fixed session clock used while recording CLI actions.
func (r *sessionRecorder) ClockStart() time.Time {
return time.Unix(r.metadata.ClockStartUnix, 0)
}
// Dial uses the direct gRPC connection for recording.
func (r *sessionRecorder) Dial(cmd *cli.Command) (daemonConn, func(), error) {
return dialDirectConn(cmd)
}
// UnaryInterceptor captures unary RPCs for session playback.
func (r *sessionRecorder) UnaryInterceptor() grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply any,
cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
opts ...grpc.CallOption) error {
r.logGRPCMessage(method, "request", req, nil)
err := invoker(ctx, method, req, reply, cc, opts...)
if err != nil {
r.logGRPCMessage(method, "error", nil, err)
return err
}
r.logGRPCMessage(method, "response", reply, nil)
return nil
}
}
// StreamInterceptor captures stream RPCs for session playback.
func (r *sessionRecorder) StreamInterceptor() grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc,
cc *grpc.ClientConn, method string, streamer grpc.Streamer,
opts ...grpc.CallOption) (grpc.ClientStream, error) {
clientStream, err := streamer(ctx, desc, cc, method, opts...)
if err != nil {
r.logGRPCMessage(method, "error", nil, err)
return nil, err
}
return &recordingClientStream{
ClientStream: clientStream,
recorder: r,
method: method,
}, nil
}
}
// recordingClientStream wraps a gRPC stream and logs message events.
type recordingClientStream struct {
grpc.ClientStream
recorder *sessionRecorder
method string
}
// SendMsg records the outgoing stream message.
func (s *recordingClientStream) SendMsg(m any) error {
s.recorder.logGRPCMessage(s.method, "send", m, nil)
err := s.ClientStream.SendMsg(m)
if err != nil {
s.recorder.logGRPCMessage(s.method, "error", nil, err)
}
return err
}
// RecvMsg records the incoming stream message.
func (s *recordingClientStream) RecvMsg(m any) error {
err := s.ClientStream.RecvMsg(m)
if err != nil {
s.recorder.logGRPCMessage(s.method, "error", nil, err)
return err
}
s.recorder.logGRPCMessage(s.method, "recv", m, nil)
return nil
}
// logGRPCMessage captures gRPC request/response data in the event stream.
func (r *sessionRecorder) logGRPCMessage(method, event string, msg any,
receptionErr error) {
payload := grpcPayload{Method: method, Event: event}
if receptionErr != nil {
payload.Error = receptionErr.Error()
if grpcStatus, ok := grpcstatus.FromError(receptionErr); ok {
payload.Status = grpcStatus.Proto()
}
r.logEvent(eventGrpc, payload)
return
}
if msg != nil {
if protoMsg, ok := msg.(proto.Message); ok {
payload.MessageType = string(
proto.MessageName(protoMsg),
)
data, err := grpcMarshalOptions.Marshal(protoMsg)
if err == nil {
payload.Payload = data
}
} else {
data, err := marshalSessionJSON(msg)
if err == nil {
payload.Payload = data
}
}
}
r.logEvent(eventGrpc, payload)
}
// LogSignal records an incoming signal event.
func (r *sessionRecorder) LogSignal(sig os.Signal) {
r.logEvent(eventSignal, signalPayload{Signal: sig.String()})
}
// InjectContext tags the outgoing context with the session name.
func (r *sessionRecorder) InjectContext(ctx context.Context) context.Context {
return metadata.AppendToOutgoingContext(
ctx, "loop-session", filepath.Base(r.filePath),
)
}
// deriveSessionSlug builds a stable slug from the command arguments.
func deriveSessionSlug(args []string) string {
if len(args) == 0 {
return ""
}
base := filepath.Base(args[0])
tokens := []string{base}
for _, arg := range args[1:] {
if strings.HasPrefix(arg, "-") {
break
}
if arg == "" {
continue
}
tokens = append(tokens, arg)
}
return sanitizeSlug(strings.Join(tokens, "-"))
}
// sanitizeSlug normalizes a session slug to a safe filename.
func sanitizeSlug(value string) string {
value = strings.ToLower(value)
var builder strings.Builder
lastDash := false
for _, r := range value {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
builder.WriteRune(r)
lastDash = false
continue
}
if !lastDash {
builder.WriteRune('-')
lastDash = true
}
}
slug := strings.Trim(builder.String(), "-")
if slug == "" {
return defaultSessionSlug
}
return slug
}
// nextSessionCounter finds the next available session number.
func nextSessionCounter(baseDir string) (int, error) {
maxCounter := 0
entries, err := os.ReadDir(baseDir)
if errors.Is(err, fs.ErrNotExist) {
return 1, nil
}
if err != nil {
return 0, err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
if filepath.Ext(entry.Name()) != sessionFileExt {
continue
}
counter, ok := parseSessionCounter(entry.Name())
if !ok {
continue
}
if counter > maxCounter {
maxCounter = counter
}
}
return maxCounter + 1, nil
}
// parseSessionCounter extracts the numeric prefix from a session filename.
func parseSessionCounter(name string) (int, bool) {
base := strings.TrimSuffix(name, filepath.Ext(name))
if base == "" {
return 0, false
}
parts := strings.SplitN(base, "_", 2)
if parts[0] == "" {
return 0, false
}
for _, r := range parts[0] {
if r < '0' || r > '9' {
return 0, false
}
}
value, err := strconv.Atoi(parts[0])
if err != nil {
return 0, false
}
if value < 0 {
return 0, false
}
return value, true
}

View file

@ -0,0 +1,343 @@
package main
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
)
const (
testLoopArg = "loop"
testOutArg = "out"
testLoopOutSlug = "loop-out"
testVersion = "test-version"
testClockStart = int64(1769407086)
)
// TestDeriveSessionSlug verifies slug derivation from CLI arguments.
func TestDeriveSessionSlug(t *testing.T) {
tests := []struct {
name string
args []string
want string
}{
{
name: "empty_args",
want: "",
},
{
name: "binary_only",
args: []string{"/usr/local/bin/loop"},
want: testLoopArg,
},
{
name: "with_subcommand",
args: []string{testLoopArg, testOutArg},
want: testLoopOutSlug,
},
{
name: "stops_at_flag",
args: []string{testLoopArg, testOutArg, "--network", "regtest"},
want: testLoopOutSlug,
},
{
name: "skips_empty_args",
args: []string{testLoopArg, "", "quote", testOutArg},
want: "loop-quote-out",
},
{
name: "sanitizes_tokens",
args: []string{testLoopArg, "Quote", "Out"},
want: "loop-quote-out",
},
{
name: "sanitizes_path_base",
args: []string{"/tmp/loop-cli", testOutArg},
want: "loop-cli-out",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
slug := deriveSessionSlug(test.args)
require.Equal(t, test.want, slug)
})
}
}
// TestSanitizeSlug verifies slug normalization behavior.
func TestSanitizeSlug(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "already_clean",
input: testLoopOutSlug,
want: testLoopOutSlug,
},
{
name: "upper_and_spaces",
input: "Loop Out",
want: testLoopOutSlug,
},
{
name: "symbols_collapsed",
input: "loop@@@out",
want: testLoopOutSlug,
},
{
name: "trims_dashes",
input: "--loop-out--",
want: testLoopOutSlug,
},
{
name: "empty_to_default",
input: "!!!",
want: defaultSessionSlug,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
slug := sanitizeSlug(test.input)
require.Equal(t, test.want, slug)
})
}
}
// TestParseSessionCounter verifies session counter parsing.
func TestParseSessionCounter(t *testing.T) {
tests := []struct {
name string
input string
want int
ok bool
}{
{
name: "with_suffix",
input: "01_loop-out.json",
want: 1,
ok: true,
},
{
name: "with_extra_underscores",
input: "12_loop_in.json",
want: 12,
ok: true,
},
{
name: "no_suffix",
input: "99",
want: 99,
ok: true,
},
{
name: "no_extension",
input: "7_loop-out",
want: 7,
ok: true,
},
{
name: "empty_name",
input: "",
ok: false,
},
{
name: "missing_prefix",
input: "_loop.json",
ok: false,
},
{
name: "non_numeric_prefix",
input: "loop.json",
ok: false,
},
{
name: "mixed_prefix",
input: "1a_loop.json",
ok: false,
},
{
name: "negative_prefix",
input: "-1_loop.json",
ok: false,
},
{
name: "plus_prefix",
input: "+1_loop.json",
ok: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
value, ok := parseSessionCounter(test.input)
require.Equal(t, test.ok, ok)
if test.ok {
require.Equal(t, test.want, value)
}
})
}
}
// TestSessionFinalizeWritesFileWhenHookRestoreFails verifies that session
// data is still written to disk when hook restoration returns an error.
func TestSessionFinalizeWritesFileWhenHookRestoreFails(t *testing.T) {
recorder := &sessionRecorder{
started: time.Now().Add(-time.Second),
filePath: filepath.Join(t.TempDir(), "session.json"),
metadata: sessionMetadata{
Args: []string{testLoopArg, testOutArg},
Env: map[string]string{},
Version: testVersion,
ClockStartUnix: testClockStart,
},
hooksStarted: true,
stdoutUnhook: func() error {
return errors.New("stdout restore failed")
},
}
err := recorder.Finalize(nil)
require.EqualError(t, err, "stdout restore failed")
blob, err := os.ReadFile(recorder.filePath)
require.NoError(t, err)
var data sessionFile
require.NoError(t, json.Unmarshal(blob, &data))
require.Equal(t, recorder.metadata.Args, data.Metadata.Args)
require.Equal(t, recorder.metadata.Version, data.Metadata.Version)
require.NotNil(t, data.Metadata.Duration)
require.Nil(t, data.Metadata.RunError)
require.Len(t, data.Events, 1)
require.Equal(t, eventExit, data.Events[0].Kind)
}
// TestSessionFinalizeReportsWriteAndHookErrors verifies that a write failure
// and hook restoration failure are both returned.
func TestSessionFinalizeReportsWriteAndHookErrors(t *testing.T) {
recorder := &sessionRecorder{
started: time.Now().Add(-time.Second),
filePath: t.TempDir(),
metadata: sessionMetadata{
Args: []string{testLoopArg, testOutArg},
Env: map[string]string{},
Version: testVersion,
ClockStartUnix: testClockStart,
},
hooksStarted: true,
stdoutUnhook: func() error {
return errors.New("stdout restore failed")
},
}
err := recorder.Finalize(nil)
require.ErrorContains(t, err, "is a directory")
require.ErrorContains(t, err, "stdout restore failed")
}
// TestSessionFinalizeReportsDeferredMarshalError verifies that marshal
// failures encountered while recording are reported from Finalize while the
// session file is still written to disk.
func TestSessionFinalizeReportsDeferredMarshalError(t *testing.T) {
recorder := &sessionRecorder{
started: time.Now().Add(-time.Second),
filePath: filepath.Join(t.TempDir(), "session.json"),
metadata: sessionMetadata{
Args: []string{testLoopArg, testOutArg},
Env: map[string]string{},
Version: testVersion,
ClockStartUnix: testClockStart,
},
}
// This logEvent will fail, because chan int is not JSON marshallable.
recorder.logEvent(eventStdout, make(chan int))
err := recorder.Finalize(nil)
require.ErrorContains(t, err, "marshal session stdout event")
require.ErrorContains(t, err, "unsupported type: chan int")
blob, err := os.ReadFile(recorder.filePath)
require.NoError(t, err)
var data sessionFile
require.NoError(t, json.Unmarshal(blob, &data))
require.Equal(t, recorder.metadata.Args, data.Metadata.Args)
require.Equal(t, recorder.metadata.Version, data.Metadata.Version)
require.NotNil(t, data.Metadata.Duration)
require.Nil(t, data.Metadata.RunError)
require.Len(t, data.Events, 1)
require.Equal(t, eventExit, data.Events[0].Kind)
}
// TestNewSessionRecorderRequiresRepoRoot verifies that recording fails clearly
// when the session fixture directory is not available from the current working
// directory.
func TestNewSessionRecorderRequiresRepoRoot(t *testing.T) {
t.Chdir(t.TempDir())
t.Setenv(sessionEnvVar, "true")
recorder, err := newSessionRecorder([]string{testLoopArg, testOutArg})
require.Nil(t, recorder)
require.EqualError(t, err, "cmd/loop/testdata/sessions does not "+
"exist; run session recording from the repository root")
}
// TestNewSessionRecorderCapturesClockStart verifies that new recordings store
// the clock start used for deterministic replay.
func TestNewSessionRecorderCapturesClockStart(t *testing.T) {
repoRoot := t.TempDir()
require.NoError(
t, os.MkdirAll(filepath.Join(repoRoot, sessionDefaultDir), 0o755),
)
t.Chdir(repoRoot)
t.Setenv(sessionEnvVar, "true")
before := time.Now().Unix()
recorder, err := newSessionRecorder([]string{testLoopArg, testOutArg})
after := time.Now().Unix()
require.NoError(t, err)
require.NotNil(t, recorder)
require.GreaterOrEqual(t, recorder.metadata.ClockStartUnix, before)
require.LessOrEqual(t, recorder.metadata.ClockStartUnix, after)
require.Equal(
t, time.Unix(recorder.metadata.ClockStartUnix, 0),
recorder.ClockStart(),
)
}
// TestLogGRPCMessageRecordsStatus verifies that gRPC status errors retain
// their structured status details in recorded sessions.
func TestLogGRPCMessageRecordsStatus(t *testing.T) {
recorder := &sessionRecorder{
started: time.Now(),
}
statusErr := grpcstatus.Error(
codes.FailedPrecondition, "no rules set",
)
recorder.logGRPCMessage(
"/looprpc.SwapClient/SuggestSwaps", "error", nil, statusErr,
)
require.Len(t, recorder.events, 1)
require.Equal(t, eventGrpc, recorder.events[0].Kind)
var payload grpcPayload
require.NoError(t, json.Unmarshal(recorder.events[0].Data, &payload))
require.Equal(t, statusErr.Error(), payload.Error)
require.NotNil(t, payload.Status)
require.Equal(t, int32(codes.FailedPrecondition), payload.Status.Code)
require.Equal(t, "no rules set", payload.Status.Message)
}

File diff suppressed because it is too large Load diff

143
cmd/loop/session_stdio.go Normal file
View file

@ -0,0 +1,143 @@
package main
import (
"io"
"os"
"sync"
)
// hookStdout redirects stdout and returns a function to restore it.
func hookStdout(orig *os.File, forward io.Writer,
onChunk func([]byte)) (func() error, error) {
return hookOutput(
func(f *os.File) { os.Stdout = f },
orig,
forward,
onChunk,
)
}
// hookStderr redirects stderr and returns a function to restore it.
func hookStderr(orig *os.File, forward io.Writer,
onChunk func([]byte)) (func() error, error) {
return hookOutput(
func(f *os.File) { os.Stderr = f },
orig,
forward,
onChunk,
)
}
// hookOutput redirects an output stream and returns a restore function.
func hookOutput(setDest func(*os.File), orig *os.File, forward io.Writer,
onChunk func([]byte)) (func() error, error) {
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
setDest(w)
var wg sync.WaitGroup
wg.Go(func() {
defer r.Close()
writer := composeWriter(forward, onChunk)
// Restoring the original descriptor closes the pipe from the
// other side, so io.Copy can fail as part of normal teardown.
_, _ = io.Copy(writer, r)
})
return func() error {
setDest(orig)
_ = w.Close()
wg.Wait()
return nil
}, nil
}
// hookStdin redirects stdin and returns a function to restore it.
func hookStdin(orig *os.File, source io.Reader,
onChunk func([]byte)) (func() error, error) {
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
useOrig := false
if source == nil {
source = orig
useOrig = true
} else if source == orig {
useOrig = true
}
if onChunk != nil {
source = io.TeeReader(source, chunkWriter(onChunk))
}
os.Stdin = r
var wg sync.WaitGroup
wg.Go(func() {
defer w.Close()
// Closing the pipe during hook restoration can terminate the
// copy loop with an expected error.
_, _ = io.Copy(w, source)
})
return func() error {
os.Stdin = orig
_ = r.Close()
_ = w.Close()
// When stdin is still backed by the original terminal, the copy
// goroutine can block indefinitely on user input. In that case
// we rely on closing the pipe or process exit to end the copy
// loop instead of waiting here and hanging the CLI shutdown
// path.
if !useOrig {
wg.Wait()
}
return nil
}, nil
}
// composeWriter builds a writer that forwards to the provided sinks.
func composeWriter(forward io.Writer, onChunk func([]byte)) io.Writer {
switch {
case forward != nil && onChunk != nil:
return io.MultiWriter(forward, chunkWriter(onChunk))
case forward != nil:
return forward
case onChunk != nil:
return chunkWriter(onChunk)
default:
return io.Discard
}
}
// chunkWriter writes copy-safe chunks to a callback.
type chunkWriter func([]byte)
// Write copies p before forwarding to the callback.
func (w chunkWriter) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
copyBuf := make([]byte, len(p))
copy(copyBuf, p)
w(copyBuf)
return len(p), nil
}

View file

@ -0,0 +1,83 @@
package main
import (
"context"
"github.com/urfave/cli/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
)
// grpcTransport customizes gRPC dialing and interceptors for sessions.
type grpcTransport interface {
// Dial returns a gRPC connection for the CLI.
Dial(cmd *cli.Command) (daemonConn, func(), error)
// UnaryInterceptor returns the unary interceptor to apply for session
// flows.
UnaryInterceptor() grpc.UnaryClientInterceptor
// StreamInterceptor returns the stream interceptor to apply for session
// flows.
StreamInterceptor() grpc.StreamClientInterceptor
}
// daemonConn is the client connection interface required by stop/wait flows.
type daemonConn interface {
grpc.ClientConnInterface
// GetState reports the current connectivity state of the client
// channel.
GetState() connectivity.State
// WaitForStateChange blocks until the state changes or the context
// expires.
WaitForStateChange(ctx context.Context,
sourceState connectivity.State) bool
// Connect forces the channel out of idle mode.
Connect()
}
// directGrpcTransport establishes real gRPC connections to loopd.
type directGrpcTransport struct{}
// Dial opens a direct gRPC connection.
func (t *directGrpcTransport) Dial(
cmd *cli.Command) (daemonConn, func(), error) {
return dialDirectConn(cmd)
}
// UnaryInterceptor returns nil because direct connections do not need wrapping.
func (t *directGrpcTransport) UnaryInterceptor() grpc.UnaryClientInterceptor {
return nil
}
// StreamInterceptor returns nil because direct connections do not need
// wrapping.
func (t *directGrpcTransport) StreamInterceptor() grpc.StreamClientInterceptor {
return nil
}
// sessionTransport defines the active gRPC transport for CLI commands.
var sessionTransport grpcTransport = &directGrpcTransport{}
// hookGrpc installs the active gRPC session transport hook.
func hookGrpc(transport grpcTransport) func() {
prev := sessionTransport
sessionTransport = transport
return func() { sessionTransport = prev }
}
// dialDirectConn returns the standard CLI gRPC connection.
func dialDirectConn(cmd *cli.Command) (daemonConn, func(), error) {
rpcServer := cmd.String("rpcserver")
tlsCertPath, macaroonPath, err := extractPathArgs(cmd)
if err != nil {
return nil, nil, err
}
return getClientConn(rpcServer, tlsCertPath, macaroonPath)
}

View file

@ -4,14 +4,17 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sort"
"strings"
"github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/loopin" "github.com/lightninglabs/loop/staticaddr/loopin"
"github.com/lightninglabs/loop/swapserverrpc" "github.com/lightninglabs/loop/swapserverrpc"
lndcommands "github.com/lightningnetwork/lnd/cmd/commands" "github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -192,7 +195,7 @@ func withdraw(ctx context.Context, cmd *cli.Command) error {
case isAllSelected: case isAllSelected:
case isUtxoSelected: case isUtxoSelected:
utxos := cmd.StringSlice("utxo") utxos := cmd.StringSlice("utxo")
outpoints, err = lndcommands.UtxosToOutpoints(utxos) outpoints, err = lnd.UtxosToOutpoints(utxos)
if err != nil { if err != nil {
return err return err
} }
@ -349,7 +352,7 @@ func listWithdrawals(ctx context.Context, cmd *cli.Command) error {
var listStaticAddressSwapsCommand = &cli.Command{ var listStaticAddressSwapsCommand = &cli.Command{
Name: "listswaps", Name: "listswaps",
Usage: "Shows a list of finalized static address swaps.", Usage: "Shows a list of static address swaps.",
Description: ` Description: `
`, `,
Action: listStaticAddressSwaps, Action: listStaticAddressSwaps,
@ -553,11 +556,7 @@ func staticAddressLoopIn(ctx context.Context, cmd *cli.Command) error {
allDeposits := depositList.FilteredDeposits allDeposits := depositList.FilteredDeposits
if len(allDeposits) == 0 { if len(allDeposits) == 0 {
errString := fmt.Sprintf("no confirmed deposits available, "+ return errors.New("no deposited outputs available")
"deposits need at least %v confirmations",
deposit.MinConfs)
return errors.New(errString)
} }
var depositOutpoints []string var depositOutpoints []string
@ -614,6 +613,28 @@ func staticAddressLoopIn(ctx context.Context, cmd *cli.Command) error {
return err return err
} }
// Warn the user if any selected deposits have fewer than 6
// confirmations, as the swap payment won't be received immediately
// for those.
summary, err := client.GetStaticAddressSummary(
ctx, &looprpc.StaticAddressSummaryRequest{},
)
if err != nil {
return err
}
depositsToCheck := warningDepositOutpoints(
allDeposits, depositOutpoints, autoSelectDepositsForQuote,
quoteReq.Amt,
)
warning := lowConfDepositWarning(
allDeposits, depositsToCheck,
int64(summary.RelativeExpiryBlocks),
)
if warning != "" {
fmt.Println(warning)
}
if !(cmd.Bool("force") || cmd.Bool("f")) { if !(cmd.Bool("force") || cmd.Bool("f")) {
err = displayInDetails(quoteReq, quote, cmd.Bool("verbose")) err = displayInDetails(quoteReq, quote, cmd.Bool("verbose"))
if err != nil { if err != nil {
@ -669,6 +690,162 @@ func depositsToOutpoints(deposits []*looprpc.Deposit) []string {
return outpoints return outpoints
} }
var warningSelectionDustLimit = int64(lnwallet.DustLimitForSize(input.P2TRSize))
// warningDepositOutpoints returns the deposit outpoints to check for
// low-confirmation warnings.
func warningDepositOutpoints(allDeposits []*looprpc.Deposit,
selectedOutpoints []string, autoSelect bool, targetAmount int64) []string {
if !autoSelect {
return selectedOutpoints
}
return autoSelectedWarningOutpoints(allDeposits, targetAmount)
}
// autoSelectedWarningOutpoints returns the outpoints selected by the same
// ordering used for automatic static loop-in deposit selection.
func autoSelectedWarningOutpoints(allDeposits []*looprpc.Deposit,
targetAmount int64) []string {
if targetAmount <= 0 {
return nil
}
// KEEP IN SYNC with staticaddr/loopin.SelectDeposits.
deposits := filterSwappableWarningDeposits(allDeposits)
sort.Slice(deposits, func(i, j int) bool {
iConfirmed := deposits[i].ConfirmationHeight > 0
jConfirmed := deposits[j].ConfirmationHeight > 0
if iConfirmed != jConfirmed {
return iConfirmed
}
if deposits[i].Value == deposits[j].Value {
return deposits[i].BlocksUntilExpiry <
deposits[j].BlocksUntilExpiry
}
return deposits[i].Value > deposits[j].Value
})
selectedOutpoints := make([]string, 0, len(deposits))
var selectedAmount int64
for _, deposit := range deposits {
selectedOutpoints = append(selectedOutpoints, deposit.Outpoint)
selectedAmount += deposit.Value
if selectedAmount == targetAmount {
return selectedOutpoints
}
if selectedAmount > targetAmount &&
selectedAmount-targetAmount >= warningSelectionDustLimit {
return selectedOutpoints
}
}
return nil
}
// filterSwappableWarningDeposits filters deposits for CLI warning selection.
func filterSwappableWarningDeposits(
allDeposits []*looprpc.Deposit) []*looprpc.Deposit {
swappable := make([]*looprpc.Deposit, 0, len(allDeposits))
minBlocksUntilExpiry := int64(
loopin.DefaultLoopInOnChainCltvDelta + loopin.DepositHtlcDelta,
)
for _, deposit := range allDeposits {
// Unconfirmed deposits remain swappable because their CSV timeout has
// not started yet. This mirrors loopin.IsSwappable.
if deposit.ConfirmationHeight > 0 &&
deposit.BlocksUntilExpiry < minBlocksUntilExpiry {
continue
}
swappable = append(swappable, deposit)
}
return swappable
}
// conservativeWarningConfs is the highest default confirmation tier used by
// the server's dynamic confirmation-risk policy.
//
// The CLI does not currently know the server's exact policy, so we use this
// conservative threshold for warnings without promising immediate execution.
const conservativeWarningConfs = 6
// lowConfDepositWarning checks the selected deposits against a conservative
// confirmation threshold and returns a warning string if any are found.
func lowConfDepositWarning(allDeposits []*looprpc.Deposit,
selectedOutpoints []string, csvExpiry int64) string {
depositMap := make(map[string]*looprpc.Deposit, len(allDeposits))
for _, d := range allDeposits {
depositMap[d.Outpoint] = d
}
var lowConfEntries []string
for _, op := range selectedOutpoints {
d, ok := depositMap[op]
if !ok {
continue
}
var confs int64
switch {
case d.ConfirmationHeight <= 0:
confs = 0
case csvExpiry > 0:
// For confirmed deposits we can compute
// confirmations as CSVExpiry - BlocksUntilExpiry + 1.
confs = csvExpiry - d.BlocksUntilExpiry + 1
default:
// Can't determine confirmations without the CSV expiry.
continue
}
if confs >= conservativeWarningConfs {
continue
}
if confs == 0 {
lowConfEntries = append(
lowConfEntries,
fmt.Sprintf(" - %s (unconfirmed)", op),
)
} else {
lowConfEntries = append(
lowConfEntries,
fmt.Sprintf(
" - %s (%d confirmations)", op,
confs,
),
)
}
}
if len(lowConfEntries) == 0 {
return ""
}
return fmt.Sprintf(
"\nWARNING: The following deposits are below the "+
"conservative %d-confirmation threshold:\n%s\n"+
"The swap payment for these deposits may wait for "+
"more confirmations depending on the server's "+
"confirmation-risk policy.\n",
conservativeWarningConfs,
strings.Join(lowConfEntries, "\n"),
)
}
func displayNewAddressWarning() error { func displayNewAddressWarning() error {
fmt.Printf("\nWARNING: Be aware that loosing your l402.token file in " + fmt.Printf("\nWARNING: Be aware that loosing your l402.token file in " +
".loop under your home directory will take your ability to " + ".loop under your home directory will take your ability to " +

220
cmd/loop/staticaddr_test.go Normal file
View file

@ -0,0 +1,220 @@
package main
import (
"strings"
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/loopin"
"github.com/stretchr/testify/require"
)
// TestLowConfDepositWarningConfirmedOnly verifies confirmed deposits below the
// conservative warning threshold are included in the warning text.
func TestLowConfDepositWarningConfirmedOnly(t *testing.T) {
t.Parallel()
deposits := []*looprpc.Deposit{
{
Outpoint: "confirmed-low",
ConfirmationHeight: 100,
BlocksUntilExpiry: 140,
},
{
Outpoint: "confirmed-high",
ConfirmationHeight: 95,
BlocksUntilExpiry: 139,
},
}
warning := lowConfDepositWarning(
deposits, []string{"confirmed-low", "confirmed-high"}, 144,
)
require.Contains(t, warning, "confirmed-low (5 confirmations)")
require.NotContains(t, warning, "confirmed-high")
}
// TestLowConfDepositWarningUnconfirmed verifies unconfirmed deposits get a
// warning that the swap may wait for confirmation-risk acceptance.
func TestLowConfDepositWarningUnconfirmed(t *testing.T) {
t.Parallel()
deposits := []*looprpc.Deposit{
{
Outpoint: "mempool",
ConfirmationHeight: 0,
BlocksUntilExpiry: 144,
},
}
warning := lowConfDepositWarning(deposits, []string{"mempool"}, 144)
require.Contains(t, warning, "mempool (unconfirmed)")
require.True(
t,
strings.Contains(
warning,
"conservative 6-confirmation threshold",
),
)
require.NotContains(t, warning, "executed immediately")
}
// TestWarningDepositOutpointsAutoSelectPrefersConfirmed verifies automatic
// warning selection keeps the loop-in preference for confirmed outputs.
func TestWarningDepositOutpointsAutoSelectPrefersConfirmed(t *testing.T) {
t.Parallel()
const csvExpiry = 1100
deposits := []*looprpc.Deposit{
{
Outpoint: "mempool-large",
Value: 2_000_000,
ConfirmationHeight: 0,
BlocksUntilExpiry: csvExpiry,
},
{
Outpoint: "confirmed",
Value: 1_500_000,
ConfirmationHeight: 100,
BlocksUntilExpiry: csvExpiry - 5,
},
}
selected := warningDepositOutpoints(deposits, nil, true, 1_000_000)
require.Equal(t, []string{"confirmed"}, selected)
require.Empty(t, lowConfDepositWarning(deposits, selected, csvExpiry))
}
// TestWarningDepositOutpointsAutoSelectIncludesNeededUnconfirmed verifies the
// warning path includes mempool deposits when they are needed for the target.
func TestWarningDepositOutpointsAutoSelectIncludesNeededUnconfirmed(t *testing.T) {
t.Parallel()
const csvExpiry = 1100
deposits := []*looprpc.Deposit{
{
Outpoint: "confirmed-small",
Value: 500_000,
ConfirmationHeight: 100,
BlocksUntilExpiry: csvExpiry - 5,
},
{
Outpoint: "mempool-large",
Value: 2_000_000,
ConfirmationHeight: 0,
BlocksUntilExpiry: csvExpiry,
},
}
selected := warningDepositOutpoints(deposits, nil, true, 1_000_000)
require.Equal(
t, []string{"confirmed-small", "mempool-large"}, selected,
)
warning := lowConfDepositWarning(deposits, selected, csvExpiry)
require.Contains(t, warning, "mempool-large (unconfirmed)")
require.NotContains(t, warning, "confirmed-small")
}
// TestWarningDepositSelectionMatchesLoopInSelection verifies CLI warning
// selection matches the loop-in selector.
func TestWarningDepositSelectionMatchesLoopInSelection(t *testing.T) {
t.Parallel()
const (
blockHeight = uint32(10_000)
csvExpiry = uint32(1_200)
targetAmount = int64(2_500_000)
)
type fixture struct {
name string
value int64
confirmationHeight int64
}
fixtures := []fixture{
{
name: "mempool-huge",
value: 3_000_000,
confirmationHeight: 0,
},
{
name: "confirmed-later-expiry",
value: 2_000_000,
confirmationHeight: 9_900,
},
{
name: "confirmed-earlier-expiry",
value: 2_000_000,
confirmationHeight: 9_890,
},
{
name: "confirmed-small",
value: 600_000,
confirmationHeight: 9_900,
},
{
name: "confirmed-too-close-to-expiry",
value: 5_000_000,
confirmationHeight: 9_849,
},
}
rpcDeposits := make([]*looprpc.Deposit, 0, len(fixtures))
loopInDeposits := make([]*deposit.Deposit, 0, len(fixtures))
for idx, fixture := range fixtures {
hash := chainhash.Hash{byte(idx + 1)}
outpoint := wire.OutPoint{
Hash: hash,
Index: uint32(idx),
}
blocksUntilExpiry := int64(0)
if fixture.confirmationHeight > 0 {
blocksUntilExpiry = fixture.confirmationHeight +
int64(csvExpiry) - int64(blockHeight)
}
rpcDeposits = append(rpcDeposits, &looprpc.Deposit{
Outpoint: outpoint.String(),
Value: fixture.value,
ConfirmationHeight: fixture.confirmationHeight,
BlocksUntilExpiry: blocksUntilExpiry,
})
loopInDeposits = append(loopInDeposits, &deposit.Deposit{
OutPoint: outpoint,
Value: btcutil.Amount(fixture.value),
ConfirmationHeight: fixture.confirmationHeight,
})
}
cliSelected := autoSelectedWarningOutpoints(
rpcDeposits, targetAmount,
)
loopInSelected, err := loopin.SelectDeposits(
btcutil.Amount(targetAmount), loopInDeposits, csvExpiry,
blockHeight,
)
require.NoError(t, err)
loopInSelectedOutpoints := make([]string, 0, len(loopInSelected))
for _, selected := range loopInSelected {
loopInSelectedOutpoints = append(
loopInSelectedOutpoints, selected.OutPoint.String(),
)
}
require.Equal(t, loopInSelectedOutpoints, cliSelected)
}

View file

@ -6,7 +6,6 @@ import (
"github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity" "google.golang.org/grpc/connectivity"
) )
@ -63,7 +62,7 @@ func stopDaemon(ctx context.Context, cmd *cli.Command) error {
// waitForDaemonShutdown monitors the gRPC connectivity state until the daemon // waitForDaemonShutdown monitors the gRPC connectivity state until the daemon
// disappears. To avoid getting stuck in idle mode we nudge the connection to // disappears. To avoid getting stuck in idle mode we nudge the connection to
// reconnect when needed. // reconnect when needed.
func waitForDaemonShutdown(ctx context.Context, conn *grpc.ClientConn) error { func waitForDaemonShutdown(ctx context.Context, conn daemonConn) error {
for { for {
state := conn.GetState() state := conn.GetState()

View file

@ -15,9 +15,10 @@ import (
var listSwapsCommand = &cli.Command{ var listSwapsCommand = &cli.Command{
Name: "listswaps", Name: "listswaps",
Usage: "list all swaps in the local database", Usage: "list traditional Loop In and Loop Out swaps",
Description: "Allows the user to get a list of all swaps that are " + Description: "Lists traditional Loop In and Loop Out swaps that are " +
"currently stored in the database", "currently stored in the database. Static address loop-ins are not " +
"included; use `loop static listswaps` to view them.",
Action: listSwaps, Action: listSwaps,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.BoolFlag{ &cli.BoolFlag{
@ -128,10 +129,12 @@ func listSwaps(ctx context.Context, cmd *cli.Command) error {
var swapInfoCommand = &cli.Command{ var swapInfoCommand = &cli.Command{
Name: "swapinfo", Name: "swapinfo",
Usage: "show the status of a swap", Usage: "show the status of a traditional swap",
ArgsUsage: "id", ArgsUsage: "id",
Description: "Allows the user to get the status of a single swap " + Description: "Shows the status of a traditional Loop In or Loop Out " +
"currently stored in the database", "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{ Flags: []cli.Flag{
&cli.Uint64Flag{ &cli.Uint64Flag{
Name: "id", Name: "id",
@ -206,9 +209,6 @@ func abandonSwap(ctx context.Context, cmd *cli.Command) error {
var id string var id string
switch { switch {
case cmd.IsSet("id"):
id = cmd.String("id")
case cmd.NArg() > 0: case cmd.NArg() > 0:
id = args.First() id = args.First()

81
cmd/loop/testdata/sessions/AGENTS.md vendored Normal file
View file

@ -0,0 +1,81 @@
# Session Recording Notes (Loop CLI)
## How to record sessions
- Use the local CLI binary: `/home/user/bin/loop`.
- Always include `--network regtest` **after** the main command and subcommands so it does not become part of the session filename.
- Record with `LOOP_SESSION_RECORD=true`, e.g.:
- `LOOP_SESSION_RECORD=true /home/user/bin/loop quote out --network regtest 500000`
- After recording several related commands, group the resulting JSON files into a subdir under `cmd/loop/testdata/sessions/`.
## Control panel HTTP server (regtest helpers)
Base URL: `http://127.0.0.1:12345`
- Implementation: [httpcmd](https://github.com/starius/httpcmd/)
- Config: [control panel config](https://gist.github.com/starius/6604ffe27f51d55f4cf715b4202637dd)
- `/mine`:
- Mines a block. Returns JSON with `exit_code`, `stdout`, `stderr`, `duration`, `timed_out`.
- `/deposit`:
- Sends a deposit to the client static address. Returns JSON with a `txid` on success.
- May fail with insufficient funds unless blocks have been mined first.
- `/reservation`:
- Opens a reservation for instant out. Returns JSON with reservation details (id, amount, state, etc.).
- `/loop-log`:
- Returns loopd logs (text). Use a tail filter when inspecting.
- `/loop-server-log`:
- Returns loop server logs (text). Use a tail filter when inspecting.
- Tip: The response is JSON with a large `stdout` field; extract first, then tail:
- `curl -s http://127.0.0.1:12345/loop-server-log | jq -r '.stdout' > /tmp/loop-server-log.txt`
- `tail -n 50 /tmp/loop-server-log.txt`
## Useful regtest flow observations
- Deposits may be unconfirmed for a few blocks; mine multiple blocks to move a deposit into `DEPOSITED`.
- After making a deposit, `loop static summary` reflects unconfirmed + confirmed values; `loop static listdeposits` lists confirmed deposits.
- Reservations should be opened (via `/reservation`) before `instantout`; list them with `loop reservations list`.
- `instantout` uses interactive selection; `ALL` then `y` works for a simple scenario:
- `printf "ALL\ny\n" | LOOP_SESSION_RECORD=true /home/user/bin/loop instantout --network regtest`
## Coverage notes (high-level)
- Sessions were recorded for: terms, getinfo, quote in/out, listauth, fetchl402, getparams, setrule (error + success), suggestswaps (error + success), reservations list, instantout, listinstantouts, static withdraw/listwithdrawals/listswaps, listswaps, swapinfo, loop out (forced), abandon swap (help path), plus existing static-loop-in/basic-swaps.
- Some paths are intentionally skipped for now:
- `stop` (would shut down loopd; replay expects a real gRPC conn).
- Asset quote paths (`getAssetAmt`, `unmarshalFixedPoint`) require tapd/asset quotes.
- Real dialer/macaroon path handling (`extractPathArgs`, `readMacaroon`, `getClientConn`) arent exercised by replay.
## Replay stability notes
- Session replay now clones the CLI command tree per run, so flag state (`IsSet`) does not leak between sessions.
- Historical warning: earlier replays could have sticky flags across runs; if you see odd ordering-dependent failures, re-check that the replay uses the cloned command path.
## Bless mode for CLI text changes
- Use `LOOP_UPDATE_RECORDED_SESSIONS=true` to let `TestRecordedSessions` rewrite recorded `stdout`, `stderr`, and `run_error` values after a successful offline replay.
- Always run bless mode with `-count=1` so the Go test cache does not skip the update:
- `LOOP_UPDATE_RECORDED_SESSIONS=true go test ./cmd/loop -run TestRecordedSessions -count=1 -v`
- You can target a narrower subset with `-run`, for example:
- `LOOP_UPDATE_RECORDED_SESSIONS=true go test ./cmd/loop -run 'TestRecordedSessions/static-openchannel' -count=1 -v`
- Bless mode is intentionally narrow:
- it reuses the recorded gRPC stream, stdin, and env,
- it refuses to bless a session if the command changes from success to failure or vice versa,
- and it refuses to bless a session if replay no longer consumes the same recorded gRPC interaction.
- Use live recording, not bless mode, when a commands behavior or RPC flow changed.
## Session coverage map
| Subdir | Commands / scenarios |
| --- | --- |
| `basic-swaps/` | `loop out` (success), `loop in` (success), `loop monitor` |
| `getinfo/` | `loop getinfo` |
| `instantout/` | `loop reservations list`, `loop instantout` (ALL + confirm), `loop instantout` (channel flag), `loop instantout` (select index), `loop instantout` (no confirmed reservations), `loop instantout` (cancel), `loop instantout` (invalid selection), `loop listinstantouts` |
| `l402/` | `loop listauth`, `loop fetchl402` |
| `liquidity/` | `loop getparams`, `loop setparams` (no flags, feepercent, conflict, many flags/categories, destaddr, account, includeallpeers; includes error cases), `loop setrule` (missing threshold, no args, incoming/outgoing/type in/out, clear), `loop suggestswaps` (error + success) |
| `loopin/` | `loop in` (invalid amount), `loop in` (external + conf_target error), `loop in` (route_hints + private error), `loop in` (external cancel + verbose, last_hop/amt flag), `loop in` (external force) |
| `loopout/` | `loop out` (forced success), `loop out` (invalid amount), `loop out` (addr + account error), `loop out` (invalid account address type), `loop out` (amt flag + channel + max routing fee + payment timeout), `loop out` (addr flag), `loop out` (positional addr), `loop out` (account + account_addr_type) |
| `misc/` | `loop terms` |
| `quote/` | `loop quote out` (success + verbose), `loop quote in` (help + verbose), `loop quote out` (help), `loop quote in` (deposit_outpoint success), `loop quote in` (positional + last_hop) |
| `static/` | `loop static withdraw` (no selection error), `loop static withdraw` (invalid utxo), `loop static withdraw` (all success), `loop static withdraw` (utxo + dest_addr success), `loop static listwithdrawals`, `loop static listswaps` |
| `static-autoloop/` | `loop setparams --loopinsource static-address` (success + no-experimental error), `loop getparams` (static-address loop-in source), `loop suggestswaps` (static loop-in suggestion) |
| `static-loop-in/` | `loop static new`, `loop static` (help), `loop static listunspent` (incl alias), `loop static listdeposits`, `loop static summary`, `loop static in` (multiple args/flags cases), `loop static in` (duplicate outpoints), `loop static in` (positional low amount error), `loop static in` (positional + last_hop + payment_timeout), `loop static in` (all cancel), `loop static in` (explicit and automatically selected low-confirmation warnings) |
| `static-filters/` | `loop static listdeposits --filter ...` for each state (deposited/withdrawing/withdrawn/looping_in/looped_in/publish_expired_deposit/sweep_htlc_timeout/htlc_timeout_swept/wait_for_expiry_sweep/expired/failed) |
| `swaps/` | `loop listswaps` (success + conflicting filters + loop_out_only filters + loop_in_only), `loop swapinfo` (success + invalid id + id flag errors), `loop abandonswap` (help + invalid id + success) |
## Missing / not covered yet
- `loop stop` (would terminate loopd; replay expects a live gRPC server).
- Docs generators: `loop man`, `loop markdown` (handled in a separate docs pipeline).
- Asset/tapd scenarios (explicitly out of scope for now).

View file

@ -0,0 +1,146 @@
{
"metadata": {
"args": [
"loop",
"out",
"--network",
"regtest",
"500000",
"--fast"
],
"env": {},
"version": "0.31.7-beta commit=vbump-lndclient-64-g3b735173216470e023b3b72949dd82af9adbebe1 commit_hash=3b735173216470e023b3b72949dd82af9adbebe1",
"duration": 1332472129,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "500000",
"conf_target": 9,
"external_htlc": false,
"swap_publication_deadline": "1769407086",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 22,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "response",
"message_type": "looprpc.OutQuoteResponse",
"payload": {
"swap_fee_sat": "3875",
"prepay_amt_sat": "1337",
"htlc_sweep_fee_sat": "3793",
"swap_payment_dest": "AhbcGtFvFiW2ru6gxtm66Xv9DpxBdSBXK4a7EX7jbSWd",
"cltv_delta": 0,
"conf_target": 9,
"asset_rfq_info": null
}
}
},
{
"time_ms": 23,
"kind": "stdout",
"data": {
"lines": [
"Send off-chain: 500000 sat\n",
"Receive on-chain: 492332 sat\n",
"Estimated total fee: 7668 sat\n",
"\n",
"Fast swap requested.\n",
"\n",
"CONTINUE SWAP? (y/n): "
]
}
},
{
"time_ms": 1240,
"kind": "stdin",
"data": {
"text": "y\n"
}
},
{
"time_ms": 1241,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "request",
"message_type": "looprpc.LoopOutRequest",
"payload": {
"amt": "500000",
"dest": "",
"max_swap_routing_fee": "10010",
"max_prepay_routing_fee": "36",
"max_swap_fee": "3875",
"max_prepay_amt": "1337",
"max_miner_fee": "948250",
"loop_out_channel": "0",
"outgoing_chan_set": [],
"sweep_conf_target": 9,
"htlc_confirmations": 1,
"swap_publication_deadline": "1769407086",
"label": "",
"initiator": "loop-cli",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"is_external_addr": false,
"reservation_ids": [],
"payment_timeout": 0,
"asset_info": null,
"asset_rfq_info": null
}
}
},
{
"time_ms": 1327,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "response",
"message_type": "looprpc.SwapResponse",
"payload": {
"id": "e5f3a1d5cee33e239571d0c1e8cbc6926cbc1d784b9fe185ff6589e949db4549",
"id_bytes": "5fOh1c7jPiOVcdDB6MvGkmy8HXhLn+GF/2WJ6UnbRUk=",
"htlc_address": "bcrt1p69ekm32epfcy8ymkqdj5clvh37vmlgw76k9x5nfs76c7qgghnpqqmtf80x",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1p69ekm32epfcy8ymkqdj5clvh37vmlgw76k9x5nfs76c7qgghnpqqmtf80x",
"server_message": ""
}
}
},
{
"time_ms": 1329,
"kind": "stdout",
"data": {
"lines": [
"Swap initiated\n",
"ID: e5f3a1d5cee33e239571d0c1e8cbc6926cbc1d784b9fe185ff6589e949db4549\n",
"\n",
"Run `loop monitor` to monitor progress.\n"
]
}
},
{
"time_ms": 1332,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,131 @@
{
"metadata": {
"args": [
"loop",
"in",
"--network",
"regtest",
"500000"
],
"env": {},
"version": "0.31.7-beta commit=vbump-lndclient-69-g057ef82340ab279a5659ab6ae7edab61448a2acd commit_hash=057ef82340ab279a5659ab6ae7edab61448a2acd",
"duration": 2146386627,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 5,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "500000",
"conf_target": 0,
"external_htlc": false,
"swap_publication_deadline": "0",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 444,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "response",
"message_type": "looprpc.InQuoteResponse",
"payload": {
"swap_fee_sat": "1825",
"htlc_publish_fee_sat": "3875",
"cltv_delta": 0,
"conf_target": 6,
"quoted_amt": "500000"
}
}
},
{
"time_ms": 445,
"kind": "stdout",
"data": {
"lines": [
"Send on-chain: 500000 sat\n",
"Receive off-chain: 494300 sat\n",
"Estimated total fee: 5700 sat\n",
"\n",
"CONTINUE SWAP? (y/n): "
]
}
},
{
"time_ms": 1682,
"kind": "stdin",
"data": {
"text": "y\n"
}
},
{
"time_ms": 1682,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopIn",
"event": "request",
"message_type": "looprpc.LoopInRequest",
"payload": {
"amt": "500000",
"max_swap_fee": "1825",
"max_miner_fee": "11625",
"last_hop": "",
"external_htlc": false,
"htlc_conf_target": 0,
"label": "",
"initiator": "loop-cli",
"route_hints": [],
"private": false
}
}
},
{
"time_ms": 2140,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopIn",
"event": "response",
"message_type": "looprpc.SwapResponse",
"payload": {
"id": "33b431ce62ab7f8f3ff5e2aee9452ec9aa18713e734ecd79fc9cfb4bd77cc7d8",
"id_bytes": "M7QxzmKrf48/9eKu6UUuyaoYcT5zTs15/Jz7S9d8x9g=",
"htlc_address": "bcrt1pmtw7fch8de39wxfqunuucudgulkvyxm59epz9p0ph673squ7l2ns3gpcsu",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1pmtw7fch8de39wxfqunuucudgulkvyxm59epz9p0ph673squ7l2ns3gpcsu",
"server_message": ""
}
}
},
{
"time_ms": 2141,
"kind": "stdout",
"data": {
"lines": [
"Swap initiated\n",
"ID: 33b431ce62ab7f8f3ff5e2aee9452ec9aa18713e734ecd79fc9cfb4bd77cc7d8\n",
"HTLC address (P2TR): bcrt1pmtw7fch8de39wxfqunuucudgulkvyxm59epz9p0ph673squ7l2ns3gpcsu\n",
"\n",
"Run `loop monitor` to monitor progress.\n"
]
}
},
{
"time_ms": 2146,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,428 @@
{
"metadata": {
"args": [
"loop",
"monitor",
"--network",
"regtest"
],
"env": {},
"version": "0.31.7-beta commit=vbump-lndclient-70-g352a68cd43f1976a937faaf76041bc078fdd16f6 commit_hash=352a68cd43f1976a937faaf76041bc078fdd16f6",
"run_error": "recv: rpc error: code = Canceled desc = context canceled",
"duration": 58532880576,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 8,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "send",
"message_type": "looprpc.MonitorRequest",
"payload": {}
}
},
{
"time_ms": 8,
"kind": "stdout",
"data": {
"lines": [
"Note: offchain cost may report as 0 after loopd restart during swap\n"
]
}
},
{
"time_ms": 13,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "33b431ce62ab7f8f3ff5e2aee9452ec9aa18713e734ecd79fc9cfb4bd77cc7d8",
"id_bytes": "M7QxzmKrf48/9eKu6UUuyaoYcT5zTs15/Jz7S9d8x9g=",
"type": "LOOP_IN",
"state": "INVOICE_SETTLED",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769413263544127799",
"last_update_time": "1769413312288899857",
"htlc_address": "bcrt1pmtw7fch8de39wxfqunuucudgulkvyxm59epz9p0ph673squ7l2ns3gpcsu",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1pmtw7fch8de39wxfqunuucudgulkvyxm59epz9p0ph673squ7l2ns3gpcsu",
"cost_server": "0",
"cost_onchain": "3850",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 13,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "4a8f1dd97addf8222c7613f6a41e3942f4bf9ca64026107c484c1f7e551df658",
"id_bytes": "So8d2Xrd+CIsdhP2pB45QvS/nKZAJhB8SEwfflUd9lg=",
"type": "LOOP_OUT",
"state": "SUCCESS",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769407041704702192",
"last_update_time": "1769413318432807159",
"htlc_address": "bcrt1pmvvj28wnuwe70xfy6kjzwxcg7cx0kc2dy8l6armutegs8y3p0j7qg0atua",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1pmvvj28wnuwe70xfy6kjzwxcg7cx0kc2dy8l6armutegs8y3p0j7qg0atua",
"cost_server": "3875",
"cost_onchain": "2775",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 14,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "e5f3a1d5cee33e239571d0c1e8cbc6926cbc1d784b9fe185ff6589e949db4549",
"id_bytes": "5fOh1c7jPiOVcdDB6MvGkmy8HXhLn+GF/2WJ6UnbRUk=",
"type": "LOOP_OUT",
"state": "SUCCESS",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769407087874686997",
"last_update_time": "1769413322775449292",
"htlc_address": "bcrt1p69ekm32epfcy8ymkqdj5clvh37vmlgw76k9x5nfs76c7qgghnpqqmtf80x",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1p69ekm32epfcy8ymkqdj5clvh37vmlgw76k9x5nfs76c7qgghnpqqmtf80x",
"cost_server": "3875",
"cost_onchain": "2775",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 14,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "39424d7e6cf6d9c613c4026f5e6da48fd2b65989007895a0db7b937b7f8563e4",
"id_bytes": "OUJNfmz22cYTxAJvXm2kj9K2WYkAeJWg23uTe3+FY+Q=",
"type": "LOOP_OUT",
"state": "SUCCESS",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769413204269210604",
"last_update_time": "1769413329442159169",
"htlc_address": "bcrt1prz80rm04fhf5y73ps9txxslnhj8e3nqcsnmz6ca6vs6jkrce2l9svtd8sl",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1prz80rm04fhf5y73ps9txxslnhj8e3nqcsnmz6ca6vs6jkrce2l9svtd8sl",
"cost_server": "3875",
"cost_onchain": "2774",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 14,
"kind": "stdout",
"data": {
"lines": [
"2026-01-26T02:41:52-05:00 LOOP_IN INVOICE_SETTLED 0.00500000 BTC - P2TR: bcrt1pmtw7fch8de39wxfqunuucudgulkvyxm59epz9p0ph673squ7l2ns3gpcsu (cost: server 0, onchain 3850, offchain 0)\n",
"2026-01-26T02:41:58-05:00 LOOP_OUT SUCCESS 0.00500000 BTC - (cost: server 3875, onchain 2775, offchain 0)\n",
"2026-01-26T02:42:02-05:00 LOOP_OUT SUCCESS 0.00500000 BTC - (cost: server 3875, onchain 2775, offchain 0)\n",
"2026-01-26T02:42:09-05:00 LOOP_OUT SUCCESS 0.00500000 BTC - (cost: server 3875, onchain 2774, offchain 0)\n"
]
}
},
{
"time_ms": 19978,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "13da0be986d8a1f095794f78c5e716150e93c92834e3be52f6eb2fbfc84b8eb0",
"id_bytes": "E9oL6YbYofCVeU94xecWFQ6TySg0475S9usvv8hLjrA=",
"type": "LOOP_OUT",
"state": "INITIATED",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769414430311530071",
"last_update_time": "1769414430311530071",
"htlc_address": "bcrt1ps8vvwey05ezrmhe4mpz60nre256ggl2uyazs7fayh83z7ptnclnstaxvu2",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1ps8vvwey05ezrmhe4mpz60nre256ggl2uyazs7fayh83z7ptnclnstaxvu2",
"cost_server": "0",
"cost_onchain": "0",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 19978,
"kind": "stdout",
"data": {
"lines": [
"2026-01-26T03:00:30-05:00 LOOP_OUT INITIATED 0.00500000 BTC - \n"
]
}
},
{
"time_ms": 32802,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "3f4ec343ba4087eec3cdf501b4e19733d9685cc151db360bb67d2c924677ce71",
"id_bytes": "P07DQ7pAh+7DzfUBtOGXM9loXMFR2zYLtn0skkZ3znE=",
"type": "LOOP_IN",
"state": "INITIATED",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769414443188873986",
"last_update_time": "1769414443188873986",
"htlc_address": "bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch",
"cost_server": "0",
"cost_onchain": "0",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 32802,
"kind": "stdout",
"data": {
"lines": [
"2026-01-26T03:00:43-05:00 LOOP_IN INITIATED 0.00500000 BTC - P2TR: bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch\n"
]
}
},
{
"time_ms": 32829,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "3f4ec343ba4087eec3cdf501b4e19733d9685cc151db360bb67d2c924677ce71",
"id_bytes": "P07DQ7pAh+7DzfUBtOGXM9loXMFR2zYLtn0skkZ3znE=",
"type": "LOOP_IN",
"state": "HTLC_PUBLISHED",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769414443188873986",
"last_update_time": "1769414443212089572",
"htlc_address": "bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch",
"cost_server": "0",
"cost_onchain": "0",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 32829,
"kind": "stdout",
"data": {
"lines": [
"2026-01-26T03:00:43-05:00 LOOP_IN HTLC_PUBLISHED 0.00500000 BTC - P2TR: bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch\n"
]
}
},
{
"time_ms": 39513,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "3f4ec343ba4087eec3cdf501b4e19733d9685cc151db360bb67d2c924677ce71",
"id_bytes": "P07DQ7pAh+7DzfUBtOGXM9loXMFR2zYLtn0skkZ3znE=",
"type": "LOOP_IN",
"state": "INVOICE_SETTLED",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769414443188873986",
"last_update_time": "1769414449902523194",
"htlc_address": "bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch",
"cost_server": "0",
"cost_onchain": "3850",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 39513,
"kind": "stdout",
"data": {
"lines": [
"2026-01-26T03:00:49-05:00 LOOP_IN INVOICE_SETTLED 0.00500000 BTC - P2TR: bcrt1ppuw9wtl65avd026t9nkxux8whz3gyhkjh6n9cryp8a5xz9jacf9qlgxsch (cost: server 0, onchain 3850, offchain 0)\n"
]
}
},
{
"time_ms": 40341,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "13da0be986d8a1f095794f78c5e716150e93c92834e3be52f6eb2fbfc84b8eb0",
"id_bytes": "E9oL6YbYofCVeU94xecWFQ6TySg0475S9usvv8hLjrA=",
"type": "LOOP_OUT",
"state": "PREIMAGE_REVEALED",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769414430311530071",
"last_update_time": "1769414450735780318",
"htlc_address": "bcrt1ps8vvwey05ezrmhe4mpz60nre256ggl2uyazs7fayh83z7ptnclnstaxvu2",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1ps8vvwey05ezrmhe4mpz60nre256ggl2uyazs7fayh83z7ptnclnstaxvu2",
"cost_server": "0",
"cost_onchain": "0",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 40341,
"kind": "stdout",
"data": {
"lines": [
"2026-01-26T03:00:50-05:00 LOOP_OUT PREIMAGE_REVEALED 0.00500000 BTC - \n"
]
}
},
{
"time_ms": 44110,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/Monitor",
"event": "recv",
"message_type": "looprpc.SwapStatus",
"payload": {
"amt": "500000",
"id": "13da0be986d8a1f095794f78c5e716150e93c92834e3be52f6eb2fbfc84b8eb0",
"id_bytes": "E9oL6YbYofCVeU94xecWFQ6TySg0475S9usvv8hLjrA=",
"type": "LOOP_OUT",
"state": "SUCCESS",
"failure_reason": "FAILURE_REASON_NONE",
"initiation_time": "1769414430311530071",
"last_update_time": "1769414454503356191",
"htlc_address": "bcrt1ps8vvwey05ezrmhe4mpz60nre256ggl2uyazs7fayh83z7ptnclnstaxvu2",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1ps8vvwey05ezrmhe4mpz60nre256ggl2uyazs7fayh83z7ptnclnstaxvu2",
"cost_server": "3873",
"cost_onchain": "2774",
"cost_offchain": "0",
"last_hop": "",
"outgoing_chan_set": [],
"label": "",
"asset_info": null
}
}
},
{
"time_ms": 44110,
"kind": "stdout",
"data": {
"lines": [
"2026-01-26T03:00:54-05:00 LOOP_OUT SUCCESS 0.00500000 BTC - (cost: server 3873, onchain 2774, offchain 0)\n"
]
}
},
{
"time_ms": 58531,
"kind": "signal",
"data": {
"signal": "interrupt"
}
},
{
"time_ms": 58531,
"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": 58532,
"kind": "stderr",
"data": {
"lines": [
"[loop] recv: rpc error: code = Canceled desc = context canceled\n"
]
}
},
{
"time_ms": 58532,
"kind": "exit",
"data": {
"run_error": "recv: rpc error: code = Canceled desc = context canceled"
}
}
]
}

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

@ -0,0 +1,96 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"getinfo",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 200590737,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 4,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetInfo",
"event": "request",
"message_type": "looprpc.GetInfoRequest",
"payload": {}
}
},
{
"time_ms": 200,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetInfo",
"event": "response",
"message_type": "looprpc.GetInfoResponse",
"payload": {
"version": "0.31.7-beta",
"network": "regtest",
"rpc_listen": "localhost:11010",
"rest_listen": "localhost:8081",
"macaroon_path": "/home/user/.loop/regtest/loop.macaroon",
"tls_cert_path": "/home/user/.loop/regtest/tls.cert",
"loop_out_stats": {
"pending_count": "0",
"success_count": "0",
"fail_count": "0",
"sum_pending_amt": "0",
"sum_succeeded_amt": "0"
},
"loop_in_stats": {
"pending_count": "0",
"success_count": "0",
"fail_count": "0",
"sum_pending_amt": "0",
"sum_succeeded_amt": "0"
},
"commit_hash": "579ea31979e0cdb1ee6049ab97cc2768888eac40"
}
}
},
{
"time_ms": 200,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"commit_hash\": \"579ea31979e0cdb1ee6049ab97cc2768888eac40\",\n",
" \"loop_in_stats\": {\n",
" \"fail_count\": \"0\",\n",
" \"pending_count\": \"0\",\n",
" \"success_count\": \"0\",\n",
" \"sum_pending_amt\": \"0\",\n",
" \"sum_succeeded_amt\": \"0\"\n",
" },\n",
" \"loop_out_stats\": {\n",
" \"fail_count\": \"0\",\n",
" \"pending_count\": \"0\",\n",
" \"success_count\": \"0\",\n",
" \"sum_pending_amt\": \"0\",\n",
" \"sum_succeeded_amt\": \"0\"\n",
" },\n",
" \"macaroon_path\": \"/home/user/.loop/regtest/loop.macaroon\",\n",
" \"network\": \"regtest\",\n",
" \"rest_listen\": \"localhost:8081\",\n",
" \"rpc_listen\": \"localhost:11010\",\n",
" \"tls_cert_path\": \"/home/user/.loop/regtest/tls.cert\",\n",
" \"version\": \"0.31.7-beta\"\n",
"}\n"
]
}
},
{
"time_ms": 200,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,107 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"reservations",
"list",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 177945855,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "request",
"message_type": "looprpc.ListReservationsRequest",
"payload": {}
}
},
{
"time_ms": 177,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "response",
"message_type": "looprpc.ListReservationsResponse",
"payload": {
"reservations": [
{
"reservation_id": "s/RD6Lj/eXdmtFCtd/6AvrJQrP14h1YrblyZZMWITYc=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "8b3b5e235ad24be42bc3802c6694566964c394df247b4ae1f2f4fd785e5acd17",
"vout": 0,
"expiry": 1619
},
{
"reservation_id": "Wl+CTsY+V2zUvkK5bs9WIttkvKLrKM8A1kpZ5VZ/MHs=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "beb447976c6c1a93fdadee41280b912e62b305c7c30815dc172d58732b6c3836",
"vout": 0,
"expiry": 1623
},
{
"reservation_id": "Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "5f898ca1b3852c62889f1113e07c73d0a3d721fd98d0a5433c6b7122cc706952",
"vout": 0,
"expiry": 1626
}
]
}
}
},
{
"time_ms": 177,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"reservations\": [\n",
" {\n",
" \"amount\": \"800000\",\n",
" \"expiry\": 1619,\n",
" \"reservation_id\": \"b3f443e8b8ff797766b450ad77fe80beb250acfd7887562b6e5c9964c5884d87\",\n",
" \"state\": \"Confirmed\",\n",
" \"tx_id\": \"8b3b5e235ad24be42bc3802c6694566964c394df247b4ae1f2f4fd785e5acd17\",\n",
" \"vout\": 0\n",
" },\n",
" {\n",
" \"amount\": \"800000\",\n",
" \"expiry\": 1623,\n",
" \"reservation_id\": \"5a5f824ec63e576cd4be42b96ecf5622db64bca2eb28cf00d64a59e5567f307b\",\n",
" \"state\": \"Confirmed\",\n",
" \"tx_id\": \"beb447976c6c1a93fdadee41280b912e62b305c7c30815dc172d58732b6c3836\",\n",
" \"vout\": 0\n",
" },\n",
" {\n",
" \"amount\": \"800000\",\n",
" \"expiry\": 1626,\n",
" \"reservation_id\": \"32eeb97db85ac84b51ce8ba028b067a1e44df1ffad10cd7e3bd42e36f5087db2\",\n",
" \"state\": \"Confirmed\",\n",
" \"tx_id\": \"5f898ca1b3852c62889f1113e07c73d0a3d721fd98d0a5433c6b7122cc706952\",\n",
" \"vout\": 0\n",
" }\n",
" ]\n",
"}\n"
]
}
},
{
"time_ms": 177,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,190 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"instantout",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 1574538838,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 0,
"kind": "stdin",
"data": {
"text": "ALL\ny\n"
}
},
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "request",
"message_type": "looprpc.ListReservationsRequest",
"payload": {}
}
},
{
"time_ms": 152,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "response",
"message_type": "looprpc.ListReservationsResponse",
"payload": {
"reservations": [
{
"reservation_id": "s/RD6Lj/eXdmtFCtd/6AvrJQrP14h1YrblyZZMWITYc=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "8b3b5e235ad24be42bc3802c6694566964c394df247b4ae1f2f4fd785e5acd17",
"vout": 0,
"expiry": 1619
},
{
"reservation_id": "Wl+CTsY+V2zUvkK5bs9WIttkvKLrKM8A1kpZ5VZ/MHs=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "beb447976c6c1a93fdadee41280b912e62b305c7c30815dc172d58732b6c3836",
"vout": 0,
"expiry": 1623
},
{
"reservation_id": "Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "5f898ca1b3852c62889f1113e07c73d0a3d721fd98d0a5433c6b7122cc706952",
"vout": 0,
"expiry": 1626
}
]
}
}
},
{
"time_ms": 152,
"kind": "stdout",
"data": {
"lines": [
"Available reservations: \n",
"\n",
"Reservation 1: shortid b3f443, amt 800000, expiry height 1619 \n",
"Reservation 2: shortid 5a5f82, amt 800000, expiry height 1623 \n",
"Reservation 3: shortid 32eeb9, amt 800000, expiry height 1626 \n",
"\n",
"Max amount to instant out: 2400000\n",
"\n",
"Select reservations for instantout (e.g. '1,2,3')\n"
]
}
},
{
"time_ms": 152,
"kind": "stdout",
"data": {
"lines": [
"Type 'ALL' to use all available reservations.\n"
]
}
},
{
"time_ms": 152,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOutQuote",
"event": "request",
"message_type": "looprpc.InstantOutQuoteRequest",
"payload": {
"amt": "2400000",
"num_reservations": 0,
"reservation_ids": [
"s/RD6Lj/eXdmtFCtd/6AvrJQrP14h1YrblyZZMWITYc=",
"Wl+CTsY+V2zUvkK5bs9WIttkvKLrKM8A1kpZ5VZ/MHs=",
"Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI="
]
}
}
},
{
"time_ms": 177,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOutQuote",
"event": "response",
"message_type": "looprpc.InstantOutQuoteResponse",
"payload": {
"service_fee_sat": "4800",
"sweep_fee_sat": "5650"
}
}
},
{
"time_ms": 177,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOut",
"event": "request",
"message_type": "looprpc.InstantOutRequest",
"payload": {
"reservation_ids": [
"s/RD6Lj/eXdmtFCtd/6AvrJQrP14h1YrblyZZMWITYc=",
"Wl+CTsY+V2zUvkK5bs9WIttkvKLrKM8A1kpZ5VZ/MHs=",
"Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI="
],
"outgoing_chan_set": [],
"dest_addr": "",
"max_swap_fee_sat": "4800"
}
}
},
{
"time_ms": 177,
"kind": "stdout",
"data": {
"lines": [
"\n",
"Estimated on-chain fee: 5650 sat\n",
"Service fee: 4800 sat\n",
"\n",
"CONTINUE SWAP? (y/n): Starting instant swap out\n"
]
}
},
{
"time_ms": 1574,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOut",
"event": "response",
"message_type": "looprpc.InstantOutResponse",
"payload": {
"instant_out_hash": "uxbiybKoHjn4B4b+yHRFGI9e12jMMtTLEvNnF7wYriU=",
"sweep_tx_id": "a1650cb339118e58f317bbbbd8cff720e0cc02a9fe159c9286724d6f5722fef2",
"state": "PushPreimage"
}
}
},
{
"time_ms": 1574,
"kind": "stdout",
"data": {
"lines": [
"Instant out swap initiated with ID: bb16e2c9b2a81e39f80786fec87445188f5ed768cc32d4cb12f36717bc18ae25, State: PushPreimage \n",
"Sweepless sweep tx id: a1650cb339118e58f317bbbbd8cff720e0cc02a9fe159c9286724d6f5722fef2 \n"
]
}
},
{
"time_ms": 1574,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,80 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"listinstantouts",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 142800927,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListInstantOuts",
"event": "request",
"message_type": "looprpc.ListInstantOutsRequest",
"payload": {}
}
},
{
"time_ms": 142,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListInstantOuts",
"event": "response",
"message_type": "looprpc.ListInstantOutsResponse",
"payload": {
"swaps": [
{
"swap_hash": "uxbiybKoHjn4B4b+yHRFGI9e12jMMtTLEvNnF7wYriU=",
"state": "WaitForSweeplessSweepConfirmed",
"amount": "2400000",
"reservation_ids": [
"s/RD6Lj/eXdmtFCtd/6AvrJQrP14h1YrblyZZMWITYc=",
"Wl+CTsY+V2zUvkK5bs9WIttkvKLrKM8A1kpZ5VZ/MHs=",
"Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI="
],
"sweep_tx_id": "a1650cb339118e58f317bbbbd8cff720e0cc02a9fe159c9286724d6f5722fef2"
}
]
}
}
},
{
"time_ms": 142,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"swaps\": [\n",
" {\n",
" \"amount\": \"2400000\",\n",
" \"reservation_ids\": [\n",
" \"b3f443e8b8ff797766b450ad77fe80beb250acfd7887562b6e5c9964c5884d87\",\n",
" \"5a5f824ec63e576cd4be42b96ecf5622db64bca2eb28cf00d64a59e5567f307b\",\n",
" \"32eeb97db85ac84b51ce8ba028b067a1e44df1ffad10cd7e3bd42e36f5087db2\"\n",
" ],\n",
" \"state\": \"WaitForSweeplessSweepConfirmed\",\n",
" \"swap_hash\": \"bb16e2c9b2a81e39f80786fec87445188f5ed768cc32d4cb12f36717bc18ae25\",\n",
" \"sweep_tx_id\": \"a1650cb339118e58f317bbbbd8cff720e0cc02a9fe159c9286724d6f5722fef2\"\n",
" }\n",
" ]\n",
"}\n"
]
}
},
{
"time_ms": 142,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,94 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"instantout",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 271911644,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "stdin",
"data": {
"text": "ALL\nn\n"
}
},
{
"time_ms": 9,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "request",
"message_type": "looprpc.ListReservationsRequest",
"payload": {}
}
},
{
"time_ms": 270,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "response",
"message_type": "looprpc.ListReservationsResponse",
"payload": {
"reservations": [
{
"reservation_id": "s/RD6Lj/eXdmtFCtd/6AvrJQrP14h1YrblyZZMWITYc=",
"state": "Spent",
"amount": "800000",
"tx_id": "8b3b5e235ad24be42bc3802c6694566964c394df247b4ae1f2f4fd785e5acd17",
"vout": 0,
"expiry": 1619
},
{
"reservation_id": "Wl+CTsY+V2zUvkK5bs9WIttkvKLrKM8A1kpZ5VZ/MHs=",
"state": "Spent",
"amount": "800000",
"tx_id": "beb447976c6c1a93fdadee41280b912e62b305c7c30815dc172d58732b6c3836",
"vout": 0,
"expiry": 1623
},
{
"reservation_id": "Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI=",
"state": "Spent",
"amount": "800000",
"tx_id": "5f898ca1b3852c62889f1113e07c73d0a3d721fd98d0a5433c6b7122cc706952",
"vout": 0,
"expiry": 1626
},
{
"reservation_id": "/AslVrUe/D6Eum+CZsg1x+EalEVACi6ICfc8rVURH3U=",
"state": "WaitForConfirmation",
"amount": "800000",
"tx_id": "",
"vout": 0,
"expiry": 1632
}
]
}
}
},
{
"time_ms": 271,
"kind": "stdout",
"data": {
"lines": [
"No confirmed reservations found \n"
]
}
},
{
"time_ms": 271,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,155 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"instantout",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "swap canceled",
"duration": 229272887,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "stdin",
"data": {
"text": "ALL\nn\n"
}
},
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "request",
"message_type": "looprpc.ListReservationsRequest",
"payload": {}
}
},
{
"time_ms": 188,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "response",
"message_type": "looprpc.ListReservationsResponse",
"payload": {
"reservations": [
{
"reservation_id": "s/RD6Lj/eXdmtFCtd/6AvrJQrP14h1YrblyZZMWITYc=",
"state": "Spent",
"amount": "800000",
"tx_id": "8b3b5e235ad24be42bc3802c6694566964c394df247b4ae1f2f4fd785e5acd17",
"vout": 0,
"expiry": 1619
},
{
"reservation_id": "Wl+CTsY+V2zUvkK5bs9WIttkvKLrKM8A1kpZ5VZ/MHs=",
"state": "Spent",
"amount": "800000",
"tx_id": "beb447976c6c1a93fdadee41280b912e62b305c7c30815dc172d58732b6c3836",
"vout": 0,
"expiry": 1623
},
{
"reservation_id": "Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI=",
"state": "Spent",
"amount": "800000",
"tx_id": "5f898ca1b3852c62889f1113e07c73d0a3d721fd98d0a5433c6b7122cc706952",
"vout": 0,
"expiry": 1626
},
{
"reservation_id": "/AslVrUe/D6Eum+CZsg1x+EalEVACi6ICfc8rVURH3U=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "322d031e9ab55e0bcdac421c57d2d5d7705b80d01d3217a672aa1bf73edfc963",
"vout": 0,
"expiry": 1632
}
]
}
}
},
{
"time_ms": 188,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOutQuote",
"event": "request",
"message_type": "looprpc.InstantOutQuoteRequest",
"payload": {
"amt": "800000",
"num_reservations": 0,
"reservation_ids": [
"/AslVrUe/D6Eum+CZsg1x+EalEVACi6ICfc8rVURH3U="
]
}
}
},
{
"time_ms": 188,
"kind": "stdout",
"data": {
"lines": [
"Available reservations: \n",
"\n",
"Reservation 1: shortid fc0b25, amt 800000, expiry height 1632 \n",
"\n",
"Max amount to instant out: 800000\n",
"\n",
"Select reservations for instantout (e.g. '1,2,3')\n",
"Type 'ALL' to use all available reservations.\n"
]
}
},
{
"time_ms": 228,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOutQuote",
"event": "response",
"message_type": "looprpc.InstantOutQuoteResponse",
"payload": {
"service_fee_sat": "1600",
"sweep_fee_sat": "2773"
}
}
},
{
"time_ms": 228,
"kind": "stdout",
"data": {
"lines": [
"\n",
"Estimated on-chain fee: 2773 sat\n",
"Service fee: 1600 sat\n",
"\n",
"CONTINUE SWAP? (y/n): "
]
}
},
{
"time_ms": 229,
"kind": "stderr",
"data": {
"lines": [
"[loop] swap canceled\n"
]
}
},
{
"time_ms": 229,
"kind": "exit",
"data": {
"run_error": "swap canceled"
}
}
]
}

View file

@ -0,0 +1,113 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"instantout",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "invalid index 99",
"duration": 203873152,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 0,
"kind": "stdin",
"data": {
"text": "99\n"
}
},
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "request",
"message_type": "looprpc.ListReservationsRequest",
"payload": {}
}
},
{
"time_ms": 202,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "response",
"message_type": "looprpc.ListReservationsResponse",
"payload": {
"reservations": [
{
"reservation_id": "s/RD6Lj/eXdmtFCtd/6AvrJQrP14h1YrblyZZMWITYc=",
"state": "Spent",
"amount": "800000",
"tx_id": "8b3b5e235ad24be42bc3802c6694566964c394df247b4ae1f2f4fd785e5acd17",
"vout": 0,
"expiry": 1619
},
{
"reservation_id": "Wl+CTsY+V2zUvkK5bs9WIttkvKLrKM8A1kpZ5VZ/MHs=",
"state": "Spent",
"amount": "800000",
"tx_id": "beb447976c6c1a93fdadee41280b912e62b305c7c30815dc172d58732b6c3836",
"vout": 0,
"expiry": 1623
},
{
"reservation_id": "Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI=",
"state": "Spent",
"amount": "800000",
"tx_id": "5f898ca1b3852c62889f1113e07c73d0a3d721fd98d0a5433c6b7122cc706952",
"vout": 0,
"expiry": 1626
},
{
"reservation_id": "/AslVrUe/D6Eum+CZsg1x+EalEVACi6ICfc8rVURH3U=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "322d031e9ab55e0bcdac421c57d2d5d7705b80d01d3217a672aa1bf73edfc963",
"vout": 0,
"expiry": 1632
}
]
}
}
},
{
"time_ms": 202,
"kind": "stdout",
"data": {
"lines": [
"Available reservations: \n",
"\n",
"Reservation 1: shortid fc0b25, amt 800000, expiry height 1632 \n",
"\n",
"Max amount to instant out: 800000\n",
"\n",
"Select reservations for instantout (e.g. '1,2,3')\n",
"Type 'ALL' to use all available reservations.\n"
]
}
},
{
"time_ms": 203,
"kind": "stderr",
"data": {
"lines": [
"[loop] invalid index 99\n"
]
}
},
{
"time_ms": 203,
"kind": "exit",
"data": {
"run_error": "invalid index 99"
}
}
]
}

View file

@ -0,0 +1,207 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"instantout",
"--network",
"regtest",
"--channel",
"125344325763072"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 1716733605,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "stdin",
"data": {
"text": "ALL\ny\n"
}
},
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "request",
"message_type": "looprpc.ListReservationsRequest",
"payload": {}
}
},
{
"time_ms": 317,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "response",
"message_type": "looprpc.ListReservationsResponse",
"payload": {
"reservations": [
{
"reservation_id": "s/RD6Lj/eXdmtFCtd/6AvrJQrP14h1YrblyZZMWITYc=",
"state": "Spent",
"amount": "800000",
"tx_id": "8b3b5e235ad24be42bc3802c6694566964c394df247b4ae1f2f4fd785e5acd17",
"vout": 0,
"expiry": 1619
},
{
"reservation_id": "Wl+CTsY+V2zUvkK5bs9WIttkvKLrKM8A1kpZ5VZ/MHs=",
"state": "Spent",
"amount": "800000",
"tx_id": "beb447976c6c1a93fdadee41280b912e62b305c7c30815dc172d58732b6c3836",
"vout": 0,
"expiry": 1623
},
{
"reservation_id": "Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI=",
"state": "Spent",
"amount": "800000",
"tx_id": "5f898ca1b3852c62889f1113e07c73d0a3d721fd98d0a5433c6b7122cc706952",
"vout": 0,
"expiry": 1626
},
{
"reservation_id": "/AslVrUe/D6Eum+CZsg1x+EalEVACi6ICfc8rVURH3U=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "322d031e9ab55e0bcdac421c57d2d5d7705b80d01d3217a672aa1bf73edfc963",
"vout": 0,
"expiry": 1632
},
{
"reservation_id": "0JK1m4RmC5iKV9dR/SlixC0IEGmobPMTlVgmk13thx8=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "081eed1a78e87498ca29af380aaecc64e949c47aeb400375e662fdf630554ace",
"vout": 0,
"expiry": 1653
}
]
}
}
},
{
"time_ms": 317,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOutQuote",
"event": "request",
"message_type": "looprpc.InstantOutQuoteRequest",
"payload": {
"amt": "1600000",
"num_reservations": 0,
"reservation_ids": [
"/AslVrUe/D6Eum+CZsg1x+EalEVACi6ICfc8rVURH3U=",
"0JK1m4RmC5iKV9dR/SlixC0IEGmobPMTlVgmk13thx8="
]
}
}
},
{
"time_ms": 317,
"kind": "stdout",
"data": {
"lines": [
"Available reservations: \n",
"\n",
"Reservation 1: shortid fc0b25, amt 800000, expiry height 1632 \n",
"Reservation 2: shortid d092b5, amt 800000, expiry height 1653 \n",
"\n",
"Max amount to instant out: 1600000\n",
"\n",
"Select reservations for instantout (e.g. '1,2,3')\n",
"Type 'ALL' to use all available reservations.\n"
]
}
},
{
"time_ms": 359,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOutQuote",
"event": "response",
"message_type": "looprpc.InstantOutQuoteResponse",
"payload": {
"service_fee_sat": "3200",
"sweep_fee_sat": "4210"
}
}
},
{
"time_ms": 359,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOut",
"event": "request",
"message_type": "looprpc.InstantOutRequest",
"payload": {
"reservation_ids": [
"/AslVrUe/D6Eum+CZsg1x+EalEVACi6ICfc8rVURH3U=",
"0JK1m4RmC5iKV9dR/SlixC0IEGmobPMTlVgmk13thx8="
],
"outgoing_chan_set": [
"125344325763072"
],
"dest_addr": "",
"max_swap_fee_sat": "3200"
}
}
},
{
"time_ms": 360,
"kind": "stdout",
"data": {
"lines": [
"\n",
"Estimated on-chain fee: 4210 sat\n",
"Service fee: 3200 sat\n",
"\n",
"CONTINUE SWAP? (y/n): Starting instant swap out\n"
]
}
},
{
"time_ms": 1711,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOut",
"event": "response",
"message_type": "looprpc.InstantOutResponse",
"payload": {
"instant_out_hash": "RfjHjKyop9NqqQzGXnyF1mnv9mPeWJXyz+5seKnKZ4k=",
"sweep_tx_id": "6f6f29961d2b4ab9f893850a0250f1f55fb93b144966833a5546986b171255a4",
"state": "PushPreimage"
}
}
},
{
"time_ms": 1711,
"kind": "stdout",
"data": {
"lines": [
"Instant out swap initiated with ID: 45f8c78caca8a7d36aa90cc65e7c85d669eff663de5895f2cfee6c78a9ca6789, State: PushPreimage \n"
]
}
},
{
"time_ms": 1711,
"kind": "stdout",
"data": {
"lines": [
"Sweepless sweep tx id: 6f6f29961d2b4ab9f893850a0250f1f55fb93b144966833a5546986b171255a4 \n"
]
}
},
{
"time_ms": 1716,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,200 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"instantout",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 1377442432,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "stdin",
"data": {
"text": "1\ny\n"
}
},
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "request",
"message_type": "looprpc.ListReservationsRequest",
"payload": {}
}
},
{
"time_ms": 178,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListReservations",
"event": "response",
"message_type": "looprpc.ListReservationsResponse",
"payload": {
"reservations": [
{
"reservation_id": "s/RD6Lj/eXdmtFCtd/6AvrJQrP14h1YrblyZZMWITYc=",
"state": "Spent",
"amount": "800000",
"tx_id": "8b3b5e235ad24be42bc3802c6694566964c394df247b4ae1f2f4fd785e5acd17",
"vout": 0,
"expiry": 1619
},
{
"reservation_id": "Wl+CTsY+V2zUvkK5bs9WIttkvKLrKM8A1kpZ5VZ/MHs=",
"state": "Spent",
"amount": "800000",
"tx_id": "beb447976c6c1a93fdadee41280b912e62b305c7c30815dc172d58732b6c3836",
"vout": 0,
"expiry": 1623
},
{
"reservation_id": "Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI=",
"state": "Spent",
"amount": "800000",
"tx_id": "5f898ca1b3852c62889f1113e07c73d0a3d721fd98d0a5433c6b7122cc706952",
"vout": 0,
"expiry": 1626
},
{
"reservation_id": "/AslVrUe/D6Eum+CZsg1x+EalEVACi6ICfc8rVURH3U=",
"state": "Spent",
"amount": "800000",
"tx_id": "322d031e9ab55e0bcdac421c57d2d5d7705b80d01d3217a672aa1bf73edfc963",
"vout": 0,
"expiry": 1632
},
{
"reservation_id": "0JK1m4RmC5iKV9dR/SlixC0IEGmobPMTlVgmk13thx8=",
"state": "Spent",
"amount": "800000",
"tx_id": "081eed1a78e87498ca29af380aaecc64e949c47aeb400375e662fdf630554ace",
"vout": 0,
"expiry": 1653
},
{
"reservation_id": "cSfKVONNmsK9+p4Uc5nc3ZtE+37uOODHeq1vprhh/x4=",
"state": "Confirmed",
"amount": "800000",
"tx_id": "46808a670542c1f3bf20ade2af77c254e3535378aab6ccd32c58de9976b0cf26",
"vout": 0,
"expiry": 1671
}
]
}
}
},
{
"time_ms": 179,
"kind": "stdout",
"data": {
"lines": [
"Available reservations: \n",
"\n",
"Reservation 1: shortid 7127ca, amt 800000, expiry height 1671 \n",
"\n",
"Max amount to instant out: 800000\n",
"\n",
"Select reservations for instantout (e.g. '1,2,3')\n",
"Type 'ALL' to use all available reservations.\n"
]
}
},
{
"time_ms": 179,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOutQuote",
"event": "request",
"message_type": "looprpc.InstantOutQuoteRequest",
"payload": {
"amt": "800000",
"num_reservations": 0,
"reservation_ids": [
"cSfKVONNmsK9+p4Uc5nc3ZtE+37uOODHeq1vprhh/x4="
]
}
}
},
{
"time_ms": 186,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOutQuote",
"event": "response",
"message_type": "looprpc.InstantOutQuoteResponse",
"payload": {
"service_fee_sat": "1600",
"sweep_fee_sat": "2772"
}
}
},
{
"time_ms": 186,
"kind": "stdout",
"data": {
"lines": [
"\n",
"Estimated on-chain fee: 2772 sat\n",
"Service fee: 1600 sat\n",
"\n",
"CONTINUE SWAP? (y/n): Starting instant swap out\n"
]
}
},
{
"time_ms": 186,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOut",
"event": "request",
"message_type": "looprpc.InstantOutRequest",
"payload": {
"reservation_ids": [
"cSfKVONNmsK9+p4Uc5nc3ZtE+37uOODHeq1vprhh/x4="
],
"outgoing_chan_set": [],
"dest_addr": "",
"max_swap_fee_sat": "1600"
}
}
},
{
"time_ms": 1376,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/InstantOut",
"event": "response",
"message_type": "looprpc.InstantOutResponse",
"payload": {
"instant_out_hash": "0LvpOmXv+cqCnXgQ/lNOlbCvlI0AZkkkS1pFJk2n3us=",
"sweep_tx_id": "650e166a7d346f2b268c26c7fe075d8e2dc25655a747f546f5a039f01973d979",
"state": "WaitForSweeplessSweepConfirmed"
}
}
},
{
"time_ms": 1377,
"kind": "stdout",
"data": {
"lines": [
"Instant out swap initiated with ID: d0bbe93a65eff9ca829d7810fe534e95b0af948d006649244b5a45264da7deeb, State: WaitForSweeplessSweepConfirmed \n",
"Sweepless sweep tx id: 650e166a7d346f2b268c26c7fe075d8e2dc25655a747f546f5a039f01973d979 \n"
]
}
},
{
"time_ms": 1377,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,79 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"listauth",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 157631491,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetL402Tokens",
"event": "request",
"message_type": "looprpc.TokensRequest",
"payload": {}
}
},
{
"time_ms": 157,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetL402Tokens",
"event": "response",
"message_type": "looprpc.TokensResponse",
"payload": {
"tokens": [
{
"base_macaroon": "AgEEbHNhdAJCAAAkJPkWepsd1GgURnFywD7MZi7yoPZC/BjJHNrIs6en83L1T2GVJqpTIlrcw09RnFx5xHfjsNDjLRIekGAPVN/oAAIPc2VydmljZXM9bG9vcDowAAISbG9vcF9jYXBhYmlsaXRpZXM9AAAGIGIkKNbF9/LxQzqEKurnzP7lkGJbA2CsVbkwrKXyUTx/",
"payment_hash": "JCT5FnqbHdRoFEZxcsA+zGYu8qD2QvwYyRzayLOnp/M=",
"payment_preimage": "pSZZqEORw/4tPCPPS3sTDeh/2mq3EAzwqqWor/xwTmM=",
"amount_paid_msat": "1000000",
"routing_fee_paid_msat": "0",
"time_created": "1770076917",
"expired": false,
"storage_name": "/home/user/.loop/regtest/l402.token",
"id": "72f54f619526aa53225adcc34f519c5c79c477e3b0d0e32d121e90600f54dfe8"
}
]
}
}
},
{
"time_ms": 157,
"kind": "stdout",
"data": {
"lines": [
"[\n",
" {\n",
" \"amount_paid_msat\": 1000000,\n",
" \"base_macaroon\": \"0201046c736174024200002424f9167a9b1dd46814467172c03ecc662ef2a0f642fc18c91cdac8b3a7a7f372f54f619526aa53225adcc34f519c5c79c477e3b0d0e32d121e90600f54dfe800020f73657276696365733d6c6f6f703a300002126c6f6f705f6361706162696c69746965733d00000620622428d6c5f7f2f1433a842aeae7ccfee590625b0360ac55b930aca5f2513c7f\",\n",
" \"expired\": false,\n",
" \"file_name\": \"/home/user/.loop/regtest/l402.token\",\n",
" \"id\": \"72f54f619526aa53225adcc34f519c5c79c477e3b0d0e32d121e90600f54dfe8\",\n",
" \"payment_hash\": \"2424f9167a9b1dd46814467172c03ecc662ef2a0f642fc18c91cdac8b3a7a7f3\",\n",
" \"payment_preimage\": \"a52659a84391c3fe2d3c23cf4b7b130de87fda6ab7100cf0aaa5a8affc704e63\",\n",
" \"routing_fee_paid_msat\": 0,\n",
" \"time_created\": \"2026-02-02T19:01:57-05:00\",\n",
" \"valid_until\": \"\"\n",
" }\n",
"]\n"
]
}
},
{
"time_ms": 157,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,52 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"fetchl402",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 177710196,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/FetchL402Token",
"event": "request",
"message_type": "looprpc.FetchL402TokenRequest",
"payload": {}
}
},
{
"time_ms": 177,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/FetchL402Token",
"event": "response",
"message_type": "looprpc.FetchL402TokenResponse",
"payload": {}
}
},
{
"time_ms": 177,
"kind": "stdout",
"data": {
"lines": [
"{}\n"
]
}
},
{
"time_ms": 177,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,110 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"getparams",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 213058408,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 212,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 212,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"account\": \"\",\n",
" \"account_addr_type\": \"ADDRESS_TYPE_UNKNOWN\",\n",
" \"auto_max_in_flight\": \"1\",\n",
" \"autoloop\": false,\n",
" \"autoloop_budget_last_refresh\": \"1770076824\",\n",
" \"autoloop_budget_refresh_period_sec\": \"604800\",\n",
" \"autoloop_budget_sat\": \"335544\",\n",
" \"autoloop_budget_start_sec\": \"0\",\n",
" \"autoloop_dest_address\": \"\",\n",
" \"easy_asset_params\": {},\n",
" \"easy_autoloop\": false,\n",
" \"easy_autoloop_excluded_peers\": [],\n",
" \"easy_autoloop_local_target_sat\": \"0\",\n",
" \"failure_backoff_sec\": \"86400\",\n",
" \"fast_swap_publication\": true,\n",
" \"fee_ppm\": \"20000\",\n",
" \"htlc_conf_target\": 6,\n",
" \"loop_in_source\": \"LOOP_IN_SOURCE_WALLET\",\n",
" \"max_miner_fee_sat\": \"0\",\n",
" \"max_prepay_routing_fee_ppm\": \"0\",\n",
" \"max_prepay_sat\": \"0\",\n",
" \"max_routing_fee_ppm\": \"0\",\n",
" \"max_swap_amount\": \"0\",\n",
" \"max_swap_fee_ppm\": \"0\",\n",
" \"min_swap_amount\": \"0\",\n",
" \"rules\": [],\n",
" \"sweep_conf_target\": 100,\n",
" \"sweep_fee_rate_sat_per_vbyte\": \"0\"\n",
"}\n"
]
}
},
{
"time_ms": 213,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,93 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "at least one flag required to set params",
"duration": 150177446,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 5,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 149,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 150,
"kind": "stderr",
"data": {
"lines": [
"[loop] at least one flag required to set params\n"
]
}
},
{
"time_ms": 150,
"kind": "exit",
"data": {
"run_error": "at least one flag required to set params"
}
}
]
}

View file

@ -0,0 +1,125 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest",
"--feepercent",
"2"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 249608692,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 6,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 188,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 189,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 248,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "response",
"message_type": "looprpc.SetLiquidityParamsResponse",
"payload": {}
}
},
{
"time_ms": 249,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,97 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest",
"--feepercent",
"2",
"--sweeplimit",
"1"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "feepercent cannot be set with specific fee category flags",
"duration": 161206474,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 4,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 159,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 161,
"kind": "stderr",
"data": {
"lines": [
"[loop] feepercent cannot be set with specific fee category flags\n"
]
}
},
{
"time_ms": 161,
"kind": "exit",
"data": {
"run_error": "feepercent cannot be set with specific fee category flags"
}
}
]
}

View file

@ -0,0 +1,94 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setrule",
"--network",
"regtest",
"034fa8802fdc9f8c137e8f2cce2e4d30e590a1314e16bd060ac7d772dc698ff4bc"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "provide at least one flag to set rules or use the --clear flag to remove rules",
"duration": 143258905,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 142,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 143,
"kind": "stderr",
"data": {
"lines": [
"[loop] provide at least one flag to set rules or use the --clear flag to remove rules\n"
]
}
},
{
"time_ms": 143,
"kind": "exit",
"data": {
"run_error": "provide at least one flag to set rules or use the --clear flag to remove rules"
}
}
]
}

View file

@ -0,0 +1,35 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setrule",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "please set a channel id or peer pubkey for the rule update",
"duration": 1924009,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "stderr",
"data": {
"lines": [
"[loop] please set a channel id or peer pubkey for the rule update\n"
]
}
},
{
"time_ms": 1,
"kind": "exit",
"data": {
"run_error": "please set a channel id or peer pubkey for the rule update"
}
}
]
}

View file

@ -0,0 +1,58 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"suggestswaps",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "no rules set for autolooper, please set rules using the setrule command",
"duration": 178932401,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SuggestSwaps",
"event": "request",
"message_type": "looprpc.SuggestSwapsRequest",
"payload": {}
}
},
{
"time_ms": 178,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SuggestSwaps",
"event": "error",
"error": "rpc error: code = FailedPrecondition desc = no rules set for autoloop",
"status": {
"code": 9,
"message": "no rules set for autoloop"
}
}
},
{
"time_ms": 178,
"kind": "stderr",
"data": {
"lines": [
"[loop] no rules set for autolooper, please set rules using the setrule command\n"
]
}
},
{
"time_ms": 178,
"kind": "exit",
"data": {
"run_error": "no rules set for autolooper, please set rules using the setrule command"
}
}
]
}

View file

@ -0,0 +1,144 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setrule",
"--network",
"regtest",
"--incoming_threshold",
"10",
"034fa8802fdc9f8c137e8f2cce2e4d30e590a1314e16bd060ac7d772dc698ff4bc"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 130661787,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 124,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 124,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 130,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "response",
"message_type": "looprpc.SetLiquidityParamsResponse",
"payload": {}
}
},
{
"time_ms": 130,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,61 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"suggestswaps",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 137916583,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SuggestSwaps",
"event": "request",
"message_type": "looprpc.SuggestSwapsRequest",
"payload": {}
}
},
{
"time_ms": 137,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SuggestSwaps",
"event": "response",
"message_type": "looprpc.SuggestSwapsResponse",
"payload": {
"loop_out": [],
"loop_in": [],
"disqualified": []
}
}
},
{
"time_ms": 137,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"disqualified\": [],\n",
" \"loop_in\": [],\n",
" \"loop_out\": [],\n",
" \"static_loop_in\": []\n",
"}\n"
]
}
},
{
"time_ms": 137,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,152 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setrule",
"--network",
"regtest",
"--incoming_threshold",
"40",
"125344325763072"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 204743867,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 135,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 135,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
},
{
"channel_id": "125344325763072",
"swap_type": "LOOP_OUT",
"pubkey": "",
"type": "THRESHOLD",
"incoming_threshold": 40,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 204,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "response",
"message_type": "looprpc.SetLiquidityParamsResponse",
"payload": {}
}
},
{
"time_ms": 204,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,185 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setrule",
"--network",
"regtest",
"--type",
"in",
"--outgoing_threshold",
"30",
"124244814135296"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "rpc error: code = Unknown desc = channel level rules not supported for loop in swaps, only peer-level rules allowed",
"duration": 212373344,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 168,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "125344325763072",
"swap_type": "LOOP_OUT",
"pubkey": "",
"type": "THRESHOLD",
"incoming_threshold": 40,
"outgoing_threshold": 0
},
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 168,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "125344325763072",
"swap_type": "LOOP_OUT",
"pubkey": "",
"type": "THRESHOLD",
"incoming_threshold": 40,
"outgoing_threshold": 0
},
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
},
{
"channel_id": "124244814135296",
"swap_type": "LOOP_IN",
"pubkey": "",
"type": "THRESHOLD",
"incoming_threshold": 0,
"outgoing_threshold": 30
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 212,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "error",
"error": "rpc error: code = Unknown desc = channel level rules not supported for loop in swaps, only peer-level rules allowed",
"status": {
"code": 2,
"message": "channel level rules not supported for loop in swaps, only peer-level rules allowed"
}
}
},
{
"time_ms": 212,
"kind": "stderr",
"data": {
"lines": [
"[loop] rpc error: code = Unknown desc = channel level rules not supported for loop in swaps, only peer-level rules allowed\n"
]
}
},
{
"time_ms": 212,
"kind": "exit",
"data": {
"run_error": "rpc error: code = Unknown desc = channel level rules not supported for loop in swaps, only peer-level rules allowed"
}
}
]
}

View file

@ -0,0 +1,151 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setrule",
"--network",
"regtest",
"--clear",
"125344325763072"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 232507711,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 150,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "125344325763072",
"swap_type": "LOOP_OUT",
"pubkey": "",
"type": "THRESHOLD",
"incoming_threshold": 40,
"outgoing_threshold": 0
},
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 150,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 231,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "response",
"message_type": "looprpc.SetLiquidityParamsResponse",
"payload": {}
}
},
{
"time_ms": 232,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,191 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest",
"--maxswapfee",
"0.1",
"--maxroutingfee",
"0.1",
"--maxprepayfee",
"0.1",
"--maxprepay",
"1000",
"--maxminer",
"1000",
"--sweepconf",
"10",
"--failurebackoff",
"60",
"--autoloop",
"--autobudget",
"1000",
"--autobudgetrefreshperiod",
"1h",
"--autoinflight",
"2",
"--minamt",
"1000",
"--maxamt",
"200000",
"--htlc_conf",
"2",
"--easyautoloop",
"--localbalancesat",
"1000000",
"--easyautoloop_excludepeer",
"0271d6e29301159d9e1cc5d3983479a51f3b3c0c682eda7f16aa1f47dfe09b22f7",
"--fast"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "rpc error: code = Unknown desc = sweep fee rate limit must be > 1 sat/vByte",
"duration": 325173938,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 276,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 276,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "1000",
"max_swap_amount": "200000",
"htlc_conf_target": 2,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [
"AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3"
],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 324,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "error",
"error": "rpc error: code = Unknown desc = sweep fee rate limit must be > 1 sat/vByte",
"status": {
"code": 2,
"message": "sweep fee rate limit must be > 1 sat/vByte"
}
}
},
{
"time_ms": 325,
"kind": "stderr",
"data": {
"lines": [
"[loop] rpc error: code = Unknown desc = sweep fee rate limit must be > 1 sat/vByte\n"
]
}
},
{
"time_ms": 325,
"kind": "exit",
"data": {
"run_error": "rpc error: code = Unknown desc = sweep fee rate limit must be > 1 sat/vByte"
}
}
]
}

View file

@ -0,0 +1,193 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest",
"--sweeplimit",
"2",
"--maxswapfee",
"0.1",
"--maxroutingfee",
"0.1",
"--maxprepayfee",
"0.1",
"--maxprepay",
"1000",
"--maxminer",
"1000",
"--sweepconf",
"10",
"--failurebackoff",
"60",
"--autoloop",
"--autobudget",
"1000",
"--autobudgetrefreshperiod",
"1h",
"--autoinflight",
"2",
"--minamt",
"1000",
"--maxamt",
"200000",
"--htlc_conf",
"2",
"--easyautoloop",
"--localbalancesat",
"1000000",
"--easyautoloop_excludepeer",
"0271d6e29301159d9e1cc5d3983479a51f3b3c0c682eda7f16aa1f47dfe09b22f7",
"--fast"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "rpc error: code = Unknown desc = minimum swap amount is less than server minimum",
"duration": 273083406,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 191,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 191,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "2",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "1000",
"max_swap_amount": "200000",
"htlc_conf_target": 2,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [
"AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3"
],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 272,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "error",
"error": "rpc error: code = Unknown desc = minimum swap amount is less than server minimum",
"status": {
"code": 2,
"message": "minimum swap amount is less than server minimum"
}
}
},
{
"time_ms": 272,
"kind": "stderr",
"data": {
"lines": [
"[loop] rpc error: code = Unknown desc = minimum swap amount is less than server minimum\n"
]
}
},
{
"time_ms": 273,
"kind": "exit",
"data": {
"run_error": "rpc error: code = Unknown desc = minimum swap amount is less than server minimum"
}
}
]
}

View file

@ -0,0 +1,178 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest",
"--sweeplimit",
"2",
"--maxswapfee",
"0.1",
"--maxroutingfee",
"0.1",
"--maxprepayfee",
"0.1",
"--maxprepay",
"1000",
"--maxminer",
"1000",
"--sweepconf",
"10",
"--failurebackoff",
"60",
"--autoloop",
"--autobudget",
"1000",
"--autobudgetrefreshperiod",
"1h",
"--autoinflight",
"2",
"--minamt",
"500000",
"--maxamt",
"1000000",
"--htlc_conf",
"2",
"--easyautoloop",
"--localbalancesat",
"1000000",
"--easyautoloop_excludepeer",
"0271d6e29301159d9e1cc5d3983479a51f3b3c0c682eda7f16aa1f47dfe09b22f7",
"--fast"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 184771728,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 160,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 161,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "2",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "500000",
"max_swap_amount": "1000000",
"htlc_conf_target": 2,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [
"AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3"
],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 182,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "response",
"message_type": "looprpc.SetLiquidityParamsResponse",
"payload": {}
}
},
{
"time_ms": 184,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,147 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest",
"--destaddr",
"bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 235471909,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 223,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "2",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "500000",
"max_swap_amount": "1000000",
"htlc_conf_target": 2,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [
"AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3"
],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 224,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "2",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "500000",
"max_swap_amount": "1000000",
"htlc_conf_target": 2,
"autoloop_dest_address": "bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [
"AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3"
],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 234,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "response",
"message_type": "looprpc.SetLiquidityParamsResponse",
"payload": {}
}
},
{
"time_ms": 235,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,149 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest",
"--account",
"default",
"--account_addr_type",
"p2tr"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 264493138,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 249,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "2",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "500000",
"max_swap_amount": "1000000",
"htlc_conf_target": 2,
"autoloop_dest_address": "bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [
"AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3"
],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 250,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "2",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "500000",
"max_swap_amount": "1000000",
"htlc_conf_target": 2,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "default",
"account_addr_type": "TAPROOT_PUBKEY",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [
"AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3"
],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 264,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "response",
"message_type": "looprpc.SetLiquidityParamsResponse",
"payload": {}
}
},
{
"time_ms": 264,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,144 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest",
"--easyatutoloop_includeallpeers"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 304215261,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 263,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "2",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "500000",
"max_swap_amount": "1000000",
"htlc_conf_target": 2,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "default",
"account_addr_type": "TAPROOT_PUBKEY",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [
"AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3"
],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 271,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "2",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "500000",
"max_swap_amount": "1000000",
"htlc_conf_target": 2,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770076824",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "default",
"account_addr_type": "TAPROOT_PUBKEY",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 303,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "response",
"message_type": "looprpc.SetLiquidityParamsResponse",
"payload": {}
}
},
{
"time_ms": 304,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,154 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setrule",
"--network",
"regtest",
"--type",
"out",
"--incoming_threshold",
"20",
"125344325763072"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 195037901,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 137,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "2",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "500000",
"max_swap_amount": "1000000",
"htlc_conf_target": 2,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770092424",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "default",
"account_addr_type": "TAPROOT_PUBKEY",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 137,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_OUT",
"pubkey": "A0+ogC/cn4wTfo8szi5NMOWQoTFOFr0GCsfXctxpj/S8",
"type": "THRESHOLD",
"incoming_threshold": 10,
"outgoing_threshold": 0
},
{
"channel_id": "125344325763072",
"swap_type": "LOOP_OUT",
"pubkey": "",
"type": "THRESHOLD",
"incoming_threshold": 20,
"outgoing_threshold": 0
}
],
"fee_ppm": "0",
"sweep_fee_rate_sat_per_vbyte": "2",
"max_swap_fee_ppm": "1000",
"max_routing_fee_ppm": "1000",
"max_prepay_routing_fee_ppm": "1000",
"max_prepay_sat": "1000",
"max_miner_fee_sat": "1000",
"sweep_conf_target": 10,
"failure_backoff_sec": "60",
"autoloop": true,
"autoloop_budget_sat": "1000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "2",
"min_swap_amount": "500000",
"max_swap_amount": "1000000",
"htlc_conf_target": 2,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "3600",
"autoloop_budget_last_refresh": "1770092424",
"easy_autoloop": true,
"easy_autoloop_local_target_sat": "1000000",
"account": "default",
"account_addr_type": "TAPROOT_PUBKEY",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
}
},
{
"time_ms": 194,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "response",
"message_type": "looprpc.SetLiquidityParamsResponse",
"payload": {}
}
},
{
"time_ms": 195,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,36 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"in",
"--network",
"regtest",
"notanumber"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "invalid amt value \"notanumber\"",
"duration": 3964079,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 3,
"kind": "stderr",
"data": {
"lines": [
"[loop] invalid amt value \"notanumber\"\n"
]
}
},
{
"time_ms": 3,
"kind": "exit",
"data": {
"run_error": "invalid amt value \"notanumber\""
}
}
]
}

View file

@ -0,0 +1,39 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"in",
"--external",
"--conf_target",
"6",
"--network",
"regtest",
"10000"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "external and conf_target both set",
"duration": 13741983,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 13,
"kind": "stderr",
"data": {
"lines": [
"[loop] external and conf_target both set\n"
]
}
},
{
"time_ms": 13,
"kind": "exit",
"data": {
"run_error": "external and conf_target both set"
}
}
]
}

View file

@ -0,0 +1,39 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"in",
"--route_hints",
"{}",
"--private",
"--network",
"regtest",
"10000"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "private and route_hints both set",
"duration": 2834558,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "stderr",
"data": {
"lines": [
"[loop] private and route_hints both set\n"
]
}
},
{
"time_ms": 2,
"kind": "exit",
"data": {
"run_error": "private and route_hints both set"
}
}
]
}

View file

@ -0,0 +1,101 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"in",
"--network",
"regtest",
"--external",
"--amt",
"50000",
"--last_hop",
"0271d6e29301159d9e1cc5d3983479a51f3b3c0c682eda7f16aa1f47dfe09b22f7"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "swap canceled",
"duration": 539895736,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "stdin",
"data": {
"text": "n\n"
}
},
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "50000",
"conf_target": 0,
"external_htlc": true,
"swap_publication_deadline": "0",
"loop_in_last_hop": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 539,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "response",
"message_type": "looprpc.InQuoteResponse",
"payload": {
"swap_fee_sat": "1801",
"htlc_publish_fee_sat": "0",
"cltv_delta": 0,
"conf_target": 0,
"quoted_amt": "50000"
}
}
},
{
"time_ms": 539,
"kind": "stderr",
"data": {
"lines": [
"[loop] swap canceled\n"
]
}
},
{
"time_ms": 539,
"kind": "stdout",
"data": {
"lines": [
"On-chain fee for external loop in is not included.\n",
"Sufficient fees will need to be paid when constructing the transaction in the external wallet.\n",
"\n",
"Send on-chain: 50000 sat\n",
"Receive off-chain: 48199 sat\n",
"Loop service fee: 1801 sat\n",
"\n",
"CONTINUE SWAP? (y/n): "
]
}
},
{
"time_ms": 539,
"kind": "exit",
"data": {
"run_error": "swap canceled"
}
}
]
}

View file

@ -0,0 +1,104 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"in",
"--network",
"regtest",
"--external",
"--verbose",
"--amt",
"50000",
"--last_hop",
"0271d6e29301159d9e1cc5d3983479a51f3b3c0c682eda7f16aa1f47dfe09b22f7"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "swap canceled",
"duration": 164102130,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 0,
"kind": "stdin",
"data": {
"text": "n\n"
}
},
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "50000",
"conf_target": 0,
"external_htlc": true,
"swap_publication_deadline": "0",
"loop_in_last_hop": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 163,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "response",
"message_type": "looprpc.InQuoteResponse",
"payload": {
"swap_fee_sat": "1801",
"htlc_publish_fee_sat": "0",
"cltv_delta": 0,
"conf_target": 0,
"quoted_amt": "50000"
}
}
},
{
"time_ms": 163,
"kind": "stdout",
"data": {
"lines": [
"On-chain fee for external loop in is not included.\n",
"Sufficient fees will need to be paid when constructing the transaction in the external wallet.\n",
"\n",
"Send on-chain: 50000 sat\n",
"Receive off-chain: 48199 sat\n",
"Loop service fee: 1801 sat\n",
"\n",
"CLTV expiry delta: 0 block\n",
"\n",
"CONTINUE SWAP? (y/n): "
]
}
},
{
"time_ms": 164,
"kind": "stderr",
"data": {
"lines": [
"[loop] swap canceled\n"
]
}
},
{
"time_ms": 164,
"kind": "exit",
"data": {
"run_error": "swap canceled"
}
}
]
}

View file

@ -0,0 +1,116 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"in",
"--network",
"regtest",
"--external",
"--amt",
"500000",
"--force"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 1067066878,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "500000",
"conf_target": 0,
"external_htlc": true,
"swap_publication_deadline": "0",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 561,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "response",
"message_type": "looprpc.InQuoteResponse",
"payload": {
"swap_fee_sat": "1824",
"htlc_publish_fee_sat": "0",
"cltv_delta": 0,
"conf_target": 0,
"quoted_amt": "500000"
}
}
},
{
"time_ms": 561,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopIn",
"event": "request",
"message_type": "looprpc.LoopInRequest",
"payload": {
"amt": "500000",
"max_swap_fee": "1824",
"max_miner_fee": "0",
"last_hop": "",
"external_htlc": true,
"htlc_conf_target": 0,
"label": "",
"initiator": "loop-cli",
"route_hints": [],
"private": false
}
}
},
{
"time_ms": 1065,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopIn",
"event": "response",
"message_type": "looprpc.SwapResponse",
"payload": {
"id": "2f46a232e0f584fadf2abc7e7aaeefab0176f21e0fba5344a10e21838ec390fe",
"id_bytes": "L0aiMuD1hPrfKrx+eq7vqwF28h4PulNEoQ4hg47DkP4=",
"htlc_address": "bcrt1pcred99xc9l2cxphneej4ph6d6mczc0442d6ulyh3a9ad389z2aaq4n775y",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1pcred99xc9l2cxphneej4ph6d6mczc0442d6ulyh3a9ad389z2aaq4n775y",
"server_message": ""
}
}
},
{
"time_ms": 1066,
"kind": "stdout",
"data": {
"lines": [
"Swap initiated\n",
"ID: 2f46a232e0f584fadf2abc7e7aaeefab0176f21e0fba5344a10e21838ec390fe\n",
"HTLC address (P2TR): bcrt1pcred99xc9l2cxphneej4ph6d6mczc0442d6ulyh3a9ad389z2aaq4n775y\n",
"\n",
"Run `loop monitor` to monitor progress.\n"
]
}
},
{
"time_ms": 1067,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,127 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"out",
"--network",
"regtest",
"--force",
"--fast",
"500000"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 228730686,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "500000",
"conf_target": 9,
"external_htlc": false,
"swap_publication_deadline": "1769407086",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 139,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "response",
"message_type": "looprpc.OutQuoteResponse",
"payload": {
"swap_fee_sat": "3875",
"prepay_amt_sat": "1337",
"htlc_sweep_fee_sat": "3793",
"swap_payment_dest": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3",
"cltv_delta": 0,
"conf_target": 9,
"asset_rfq_info": null
}
}
},
{
"time_ms": 139,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "request",
"message_type": "looprpc.LoopOutRequest",
"payload": {
"amt": "500000",
"dest": "",
"max_swap_routing_fee": "10010",
"max_prepay_routing_fee": "36",
"max_swap_fee": "3875",
"max_prepay_amt": "1337",
"max_miner_fee": "948250",
"loop_out_channel": "0",
"outgoing_chan_set": [],
"sweep_conf_target": 9,
"htlc_confirmations": 1,
"swap_publication_deadline": "1769407086",
"label": "",
"initiator": "loop-cli",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"is_external_addr": false,
"reservation_ids": [],
"payment_timeout": 0,
"asset_info": null,
"asset_rfq_info": null
}
}
},
{
"time_ms": 228,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "response",
"message_type": "looprpc.SwapResponse",
"payload": {
"id": "46b8db64110a6008bd36b423439cbba1e8e76c0264e8a68f442fc22a3c2b4f6b",
"id_bytes": "RrjbZBEKYAi9NrQjQ5y7oejnbAJk6KaPRC/CKjwrT2s=",
"htlc_address": "bcrt1pj90ffd4j9hc2a58xpl3q77c6pf8ckhqhv9w9m2529kqsk77f6lksrca9pv",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1pj90ffd4j9hc2a58xpl3q77c6pf8ckhqhv9w9m2529kqsk77f6lksrca9pv",
"server_message": ""
}
}
},
{
"time_ms": 228,
"kind": "stdout",
"data": {
"lines": [
"Swap initiated\n",
"ID: 46b8db64110a6008bd36b423439cbba1e8e76c0264e8a68f442fc22a3c2b4f6b\n",
"\n",
"Run `loop monitor` to monitor progress.\n"
]
}
},
{
"time_ms": 228,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,36 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"out",
"--network",
"regtest",
"notanumber"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "invalid amt value \"notanumber\"",
"duration": 1215843,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "stderr",
"data": {
"lines": [
"[loop] invalid amt value \"notanumber\"\n"
]
}
},
{
"time_ms": 1,
"kind": "exit",
"data": {
"run_error": "invalid amt value \"notanumber\""
}
}
]
}

View file

@ -0,0 +1,40 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"out",
"--addr",
"bcrt1qtest",
"--account",
"testacct",
"--network",
"regtest",
"50000"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "cannot set --addr and --account at the same time. Please specify only one source for a new address to sweep the loop amount to",
"duration": 1276860,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "stderr",
"data": {
"lines": [
"[loop] cannot set --addr and --account at the same time. Please specify only one source for a new address to sweep the loop amount to\n"
]
}
},
{
"time_ms": 1,
"kind": "exit",
"data": {
"run_error": "cannot set --addr and --account at the same time. Please specify only one source for a new address to sweep the loop amount to"
}
}
]
}

View file

@ -0,0 +1,40 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"out",
"--account",
"testacct",
"--account_addr_type",
"foo",
"--network",
"regtest",
"50000"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"run_error": "unknown account address type",
"duration": 1107998,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "stderr",
"data": {
"lines": [
"[loop] unknown account address type\n"
]
}
},
{
"time_ms": 1,
"kind": "exit",
"data": {
"run_error": "unknown account address type"
}
}
]
}

View file

@ -0,0 +1,135 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"out",
"--network",
"regtest",
"--amt",
"500000",
"--channel",
"125344325763072",
"--max_swap_routing_fee",
"1000",
"--payment_timeout",
"30s",
"--force"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 288562226,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "500000",
"conf_target": 9,
"external_htlc": false,
"swap_publication_deadline": "1769408886",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 161,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "response",
"message_type": "looprpc.OutQuoteResponse",
"payload": {
"swap_fee_sat": "3873",
"prepay_amt_sat": "1337",
"htlc_sweep_fee_sat": "3791",
"swap_payment_dest": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3",
"cltv_delta": 0,
"conf_target": 9,
"asset_rfq_info": null
}
}
},
{
"time_ms": 161,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "request",
"message_type": "looprpc.LoopOutRequest",
"payload": {
"amt": "500000",
"dest": "",
"max_swap_routing_fee": "1000",
"max_prepay_routing_fee": "36",
"max_swap_fee": "3873",
"max_prepay_amt": "1337",
"max_miner_fee": "947750",
"loop_out_channel": "0",
"outgoing_chan_set": [
"125344325763072"
],
"sweep_conf_target": 9,
"htlc_confirmations": 1,
"swap_publication_deadline": "1769408886",
"label": "",
"initiator": "loop-cli",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"is_external_addr": false,
"reservation_ids": [],
"payment_timeout": 30,
"asset_info": null,
"asset_rfq_info": null
}
}
},
{
"time_ms": 288,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "response",
"message_type": "looprpc.SwapResponse",
"payload": {
"id": "a564cdea5bac2fa1e350da6e7096ddd3bc0256c157f9f4b9b71a1262742f4f0d",
"id_bytes": "pWTN6lusL6HjUNpucJbd07wCVsFX+fS5txoSYnQvTw0=",
"htlc_address": "bcrt1pg84j6m04s6ecvlgput74dh4kfppn0lxn7f9h5cyl7vylyu3l55ss2f9xyh",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1pg84j6m04s6ecvlgput74dh4kfppn0lxn7f9h5cyl7vylyu3l55ss2f9xyh",
"server_message": ""
}
}
},
{
"time_ms": 288,
"kind": "stdout",
"data": {
"lines": [
"Swap initiated\n",
"ID: a564cdea5bac2fa1e350da6e7096ddd3bc0256c157f9f4b9b71a1262742f4f0d\n",
"\n",
"Run `loop monitor` to monitor progress.\n"
]
}
},
{
"time_ms": 288,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,129 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"out",
"--network",
"regtest",
"--amt",
"500000",
"--addr",
"bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw",
"--force"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 299201197,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "500000",
"conf_target": 9,
"external_htlc": false,
"swap_publication_deadline": "1769408886",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 230,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "response",
"message_type": "looprpc.OutQuoteResponse",
"payload": {
"swap_fee_sat": "3873",
"prepay_amt_sat": "1337",
"htlc_sweep_fee_sat": "3791",
"swap_payment_dest": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3",
"cltv_delta": 0,
"conf_target": 9,
"asset_rfq_info": null
}
}
},
{
"time_ms": 230,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "request",
"message_type": "looprpc.LoopOutRequest",
"payload": {
"amt": "500000",
"dest": "bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw",
"max_swap_routing_fee": "10010",
"max_prepay_routing_fee": "36",
"max_swap_fee": "3873",
"max_prepay_amt": "1337",
"max_miner_fee": "947750",
"loop_out_channel": "0",
"outgoing_chan_set": [],
"sweep_conf_target": 9,
"htlc_confirmations": 1,
"swap_publication_deadline": "1769408886",
"label": "",
"initiator": "loop-cli",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"is_external_addr": true,
"reservation_ids": [],
"payment_timeout": 0,
"asset_info": null,
"asset_rfq_info": null
}
}
},
{
"time_ms": 298,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "response",
"message_type": "looprpc.SwapResponse",
"payload": {
"id": "67712e10d785636ec4b19c0c3d0f7b7612c1de5ccb2cb81df94e32ff13914fab",
"id_bytes": "Z3EuENeFY27EsZwMPQ97dhLB3lzLLLgd+U4y/xORT6s=",
"htlc_address": "bcrt1p0fn96tdw5ngu9cpww0w8q96tcps6w7jezaf0xnw0wa7e5d5gxv3st47amw",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1p0fn96tdw5ngu9cpww0w8q96tcps6w7jezaf0xnw0wa7e5d5gxv3st47amw",
"server_message": ""
}
}
},
{
"time_ms": 298,
"kind": "stdout",
"data": {
"lines": [
"Swap initiated\n",
"ID: 67712e10d785636ec4b19c0c3d0f7b7612c1de5ccb2cb81df94e32ff13914fab\n",
"\n",
"Run `loop monitor` to monitor progress.\n"
]
}
},
{
"time_ms": 299,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,128 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"out",
"--network",
"regtest",
"--amt",
"500000",
"--force",
"bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 290631176,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "500000",
"conf_target": 9,
"external_htlc": false,
"swap_publication_deadline": "1769408886",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 205,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "response",
"message_type": "looprpc.OutQuoteResponse",
"payload": {
"swap_fee_sat": "3873",
"prepay_amt_sat": "1337",
"htlc_sweep_fee_sat": "3791",
"swap_payment_dest": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3",
"cltv_delta": 0,
"conf_target": 9,
"asset_rfq_info": null
}
}
},
{
"time_ms": 205,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "request",
"message_type": "looprpc.LoopOutRequest",
"payload": {
"amt": "500000",
"dest": "",
"max_swap_routing_fee": "10010",
"max_prepay_routing_fee": "36",
"max_swap_fee": "3873",
"max_prepay_amt": "1337",
"max_miner_fee": "947750",
"loop_out_channel": "0",
"outgoing_chan_set": [],
"sweep_conf_target": 9,
"htlc_confirmations": 1,
"swap_publication_deadline": "1769408886",
"label": "",
"initiator": "loop-cli",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"is_external_addr": false,
"reservation_ids": [],
"payment_timeout": 0,
"asset_info": null,
"asset_rfq_info": null
}
}
},
{
"time_ms": 290,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "response",
"message_type": "looprpc.SwapResponse",
"payload": {
"id": "aa7d0c558627aaa8af70503cd81d4679400cc9771063cb474c32c625d5f2a8f3",
"id_bytes": "qn0MVYYnqqivcFA82B1GeUAMyXcQY8tHTDLGJdXyqPM=",
"htlc_address": "bcrt1p9jhv4amddjazuyzs8xr2u2fzzqdt2s5tx95ffhd43avxvq7d0xxsykf5r5",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1p9jhv4amddjazuyzs8xr2u2fzzqdt2s5tx95ffhd43avxvq7d0xxsykf5r5",
"server_message": ""
}
}
},
{
"time_ms": 290,
"kind": "stdout",
"data": {
"lines": [
"Swap initiated\n",
"ID: aa7d0c558627aaa8af70503cd81d4679400cc9771063cb474c32c625d5f2a8f3\n",
"\n",
"Run `loop monitor` to monitor progress.\n"
]
}
},
{
"time_ms": 290,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,131 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"out",
"--network",
"regtest",
"--amt",
"500000",
"--account",
"default",
"--account_addr_type",
"p2tr",
"--force"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 466934696,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "500000",
"conf_target": 9,
"external_htlc": false,
"swap_publication_deadline": "1769408886",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 360,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "response",
"message_type": "looprpc.OutQuoteResponse",
"payload": {
"swap_fee_sat": "3873",
"prepay_amt_sat": "1337",
"htlc_sweep_fee_sat": "3791",
"swap_payment_dest": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3",
"cltv_delta": 0,
"conf_target": 9,
"asset_rfq_info": null
}
}
},
{
"time_ms": 360,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "request",
"message_type": "looprpc.LoopOutRequest",
"payload": {
"amt": "500000",
"dest": "",
"max_swap_routing_fee": "10010",
"max_prepay_routing_fee": "36",
"max_swap_fee": "3873",
"max_prepay_amt": "1337",
"max_miner_fee": "947750",
"loop_out_channel": "0",
"outgoing_chan_set": [],
"sweep_conf_target": 9,
"htlc_confirmations": 1,
"swap_publication_deadline": "1769408886",
"label": "",
"initiator": "loop-cli",
"account": "default",
"account_addr_type": "TAPROOT_PUBKEY",
"is_external_addr": false,
"reservation_ids": [],
"payment_timeout": 0,
"asset_info": null,
"asset_rfq_info": null
}
}
},
{
"time_ms": 462,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOut",
"event": "response",
"message_type": "looprpc.SwapResponse",
"payload": {
"id": "04c1ef677f170f50ad3f221f49a779b99a464907bde2e0003a7b1dd6402d1419",
"id_bytes": "BMHvZ38XD1CtPyIfSad5uZpGSQe94uAAOnsd1kAtFBk=",
"htlc_address": "bcrt1p8lvg7l4zkdu0l4pe74h96240ndmp3demreml3zeqy28dvgq7j95q23zqcy",
"htlc_address_p2wsh": "",
"htlc_address_p2tr": "bcrt1p8lvg7l4zkdu0l4pe74h96240ndmp3demreml3zeqy28dvgq7j95q23zqcy",
"server_message": ""
}
}
},
{
"time_ms": 462,
"kind": "stdout",
"data": {
"lines": [
"Swap initiated\n",
"ID: 04c1ef677f170f50ad3f221f49a779b99a464907bde2e0003a7b1dd6402d1419\n",
"\n",
"Run `loop monitor` to monitor progress.\n"
]
}
},
{
"time_ms": 466,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,103 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"terms",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 199188467,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 7,
"kind": "stdout",
"data": {
"lines": [
"Loop Out\n",
"--------\n"
]
}
},
{
"time_ms": 7,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutTerms",
"event": "request",
"message_type": "looprpc.TermsRequest",
"payload": {}
}
},
{
"time_ms": 156,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutTerms",
"event": "response",
"message_type": "looprpc.OutTermsResponse",
"payload": {
"min_swap_amount": "50000",
"max_swap_amount": "1000000",
"min_cltv_delta": 50,
"max_cltv_delta": 250
}
}
},
{
"time_ms": 156,
"kind": "stdout",
"data": {
"lines": [
"Amount: 50000 - 1000000\n",
"Cltv delta: 50 - 250\n",
"\n",
"Loop In\n",
"------\n"
]
}
},
{
"time_ms": 156,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInTerms",
"event": "request",
"message_type": "looprpc.TermsRequest",
"payload": {}
}
},
{
"time_ms": 198,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInTerms",
"event": "response",
"message_type": "looprpc.InTermsResponse",
"payload": {
"min_swap_amount": "50000",
"max_swap_amount": "1000000"
}
}
},
{
"time_ms": 198,
"kind": "stdout",
"data": {
"lines": [
"Amount: 50000 - 1000000\n"
]
}
},
{
"time_ms": 199,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,76 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"quote",
"out",
"--network",
"regtest",
"500000"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 159166421,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "500000",
"conf_target": 9,
"external_htlc": false,
"swap_publication_deadline": "1769408886",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 158,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "response",
"message_type": "looprpc.OutQuoteResponse",
"payload": {
"swap_fee_sat": "3875",
"prepay_amt_sat": "1337",
"htlc_sweep_fee_sat": "3793",
"swap_payment_dest": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3",
"cltv_delta": 0,
"conf_target": 9,
"asset_rfq_info": null
}
}
},
{
"time_ms": 158,
"kind": "stdout",
"data": {
"lines": [
"Send off-chain: 500000 sat\n",
"Receive on-chain: 492332 sat\n",
"Estimated total fee: 7668 sat\n"
]
}
},
{
"time_ms": 159,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,92 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"quote",
"in",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 1248128,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "stdout",
"data": {
"lines": [
"NAME:\n",
" loop quote in - get a quote for the cost of a loop in swap\n",
"\n",
"USAGE:\n",
" loop quote in amt\n",
"\n",
"DESCRIPTION:\n",
" Allows to determine the cost of a swap up front.Either specify an amount or deposit outpoints.\n",
"\n",
"OPTIONS:\n"
]
}
},
{
"time_ms": 1,
"kind": "stdout",
"data": {
"lines": [
" --last_hop string the pubkey of the last hop to use for the quote\n",
" --conf_target uint the target number of blocks the on-chain htlc broadcast by the swap client should confirm within (default: 0)\n",
" --verbose, -v show expanded details (default: false)\n",
" --private generates and passes routehints. Should be used if the connected node is only reachable via private channels (default: false)\n",
" --route_hints string [ --route_hints string ] "
]
}
},
{
"time_ms": 1,
"kind": "stdout",
"data": {
"lines": [
" route hints that can each be individually used to assist in reaching the invoice's destination\n",
" --deposit_outpoint string [ --deposit_outpoint string ] one or more static address deposit outpoints to quote for. Deposit outpoints are not to be used in combination with an amount. Eachadditional outpoint can be added by specifying --deposit_outpoint tx_id:idx\n"
]
}
},
{
"time_ms": 1,
"kind": "stdout",
"data": {
"lines": [
" --help, -h "
]
}
},
{
"time_ms": 1,
"kind": "stdout",
"data": {
"lines": [
" "
]
}
},
{
"time_ms": 1,
"kind": "stdout",
"data": {
"lines": [
"show help\n"
]
}
},
{
"time_ms": 1,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,55 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"quote",
"out",
"--network",
"regtest"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 1687027,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "stdout",
"data": {
"lines": [
"NAME:"
]
}
},
{
"time_ms": 1,
"kind": "stdout",
"data": {
"lines": [
"\n",
" loop quote out - get a quote for the cost of a loop out swap\n",
"\n",
"USAGE:\n",
" loop quote out amt\n",
"\n",
"DESCRIPTION:\n",
" Allows to determine the cost of a swap up front\n",
"\n",
"OPTIONS:\n",
" --conf_target uint the number of blocks from the swap initiation height that the on-chain HTLC should be swept within in a Loop Out (default: 9)\n",
" --fast Indicate you want to swap immediately, paying potentially a higher fee. If not set the swap server might choose to wait up to 30 minutes before publishing the swap HTLC on-chain, to save on chain fees. Not setting this flag might result in a lower swap fee. (default: false)\n",
" --verbose, -v show expanded details (default: false)\n",
" --help, -h show help\n"
]
}
},
{
"time_ms": 1,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,114 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"quote",
"in",
"--network",
"regtest",
"--deposit_outpoint",
"edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 622954327,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "request",
"message_type": "looprpc.ListStaticAddressDepositsRequest",
"payload": {
"state_filter": "UNKNOWN_STATE",
"outpoints": [
"edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0"
]
}
}
},
{
"time_ms": 165,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "response",
"message_type": "looprpc.ListStaticAddressDepositsResponse",
"payload": {
"filtered_deposits": [
{
"id": "aCYqEEyewyXea+w3uOMb2HW70vXwuc4togzwvWNvxEg=",
"state": "DEPOSITED",
"outpoint": "edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0",
"value": "500000",
"confirmation_height": "125",
"blocks_until_expiry": "14394",
"swap_hash": ""
}
]
}
}
},
{
"time_ms": 165,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "0",
"conf_target": 0,
"external_htlc": false,
"swap_publication_deadline": "0",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [
"edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0"
],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 622,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "response",
"message_type": "looprpc.InQuoteResponse",
"payload": {
"swap_fee_sat": "1825",
"htlc_publish_fee_sat": "0",
"cltv_delta": 0,
"conf_target": 0,
"quoted_amt": "500000"
}
}
},
{
"time_ms": 622,
"kind": "stdout",
"data": {
"lines": [
"Previously deposited on-chain: 500000 sat\n",
"Receive off-chain: 498175 sat\n",
"Estimated total fee: 1825 sat\n"
]
}
},
{
"time_ms": 622,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,76 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"quote",
"in",
"--network",
"regtest",
"--last_hop",
"0271d6e29301159d9e1cc5d3983479a51f3b3c0c682eda7f16aa1f47dfe09b22f7",
"50000"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 226964878,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "50000",
"conf_target": 0,
"external_htlc": false,
"swap_publication_deadline": "0",
"loop_in_last_hop": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 226,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "response",
"message_type": "looprpc.InQuoteResponse",
"payload": {
"swap_fee_sat": "1801",
"htlc_publish_fee_sat": "3873",
"cltv_delta": 0,
"conf_target": 6,
"quoted_amt": "50000"
}
}
},
{
"time_ms": 226,
"kind": "stdout",
"data": {
"lines": [
"Send on-chain: 50000 sat\n",
"Receive off-chain: 44326 sat\n",
"Estimated total fee: 5674 sat\n"
]
}
},
{
"time_ms": 226,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,93 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"quote",
"out",
"--network",
"regtest",
"--verbose",
"50000"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 273326957,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "50000",
"conf_target": 9,
"external_htlc": false,
"swap_publication_deadline": "1769408886",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 272,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/LoopOutQuote",
"event": "response",
"message_type": "looprpc.OutQuoteResponse",
"payload": {
"swap_fee_sat": "3828",
"prepay_amt_sat": "1337",
"htlc_sweep_fee_sat": "3791",
"swap_payment_dest": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3",
"cltv_delta": 0,
"conf_target": 9,
"asset_rfq_info": null
}
}
},
{
"time_ms": 273,
"kind": "stdout",
"data": {
"lines": [
"Send off-chain: 50000 sat\n",
"Receive on-chain: 42381 sat\n",
"\n",
"Estimated on-chain fee: 3791 sat\n",
"Loop service fee: 3828 sat\n",
"Estimated total fee: 7619 sat\n",
"\n"
]
}
},
{
"time_ms": 273,
"kind": "stdout",
"data": {
"lines": [
"No show penalty (prepay): 1337 sat\n",
"Conf target: 9 block\n",
"CLTV expiry delta: 0 block\n",
"Publication deadline: 2026-01-26 01:28:06 -0500 EST\n"
]
}
},
{
"time_ms": 273,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,81 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"quote",
"in",
"--network",
"regtest",
"--verbose",
"50000"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 380680881,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "request",
"message_type": "looprpc.QuoteRequest",
"payload": {
"amt": "50000",
"conf_target": 0,
"external_htlc": false,
"swap_publication_deadline": "0",
"loop_in_last_hop": "",
"loop_in_route_hints": [],
"private": false,
"deposit_outpoints": [],
"asset_info": null,
"auto_select_deposits": false,
"fast": false
}
}
},
{
"time_ms": 379,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLoopInQuote",
"event": "response",
"message_type": "looprpc.InQuoteResponse",
"payload": {
"swap_fee_sat": "1800",
"htlc_publish_fee_sat": "3871",
"cltv_delta": 0,
"conf_target": 6,
"quoted_amt": "50000"
}
}
},
{
"time_ms": 380,
"kind": "stdout",
"data": {
"lines": [
"Send on-chain: 50000 sat\n",
"Receive off-chain: 44329 sat\n",
"\n",
"Estimated on-chain fee: 3871 sat\n",
"Loop service fee: 1800 sat\n",
"Estimated total fee: 5671 sat\n",
"\n",
"Conf target: 6 block\n",
"CLTV expiry delta: 0 block\n"
]
}
},
{
"time_ms": 380,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,151 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest",
"--loopinsource",
"static-address",
"--minamt",
"250000",
"--maxamt",
"1000000",
"--feepercent",
"5",
"--autoloop",
"--autobudget",
"50000",
"--autoinflight",
"1"
],
"version": "0.33.0-beta commit=v0.33.0-beta-74-g6a3c3e1df5f807b450d3ba663f4824f6588e1c13 commit_hash=6a3c3e1df5f807b450d3ba663f4824f6588e1c13",
"duration": 32231921,
"clock_start_unix": 1779406641
},
"events": [
{
"time_ms": 10,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 16,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_IN",
"pubkey": "AgN4nvpxLgk471Ol2/D2BGUJ9l/FOlv8dRHcZ/6+oaFl",
"type": "THRESHOLD",
"incoming_threshold": 0,
"outgoing_threshold": 60
}
],
"fee_ppm": "50000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": true,
"autoloop_budget_sat": "50000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "250000",
"max_swap_amount": "1000000",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1779405910",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_STATIC_ADDRESS"
}
}
},
{
"time_ms": 18,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_IN",
"pubkey": "AgN4nvpxLgk471Ol2/D2BGUJ9l/FOlv8dRHcZ/6+oaFl",
"type": "THRESHOLD",
"incoming_threshold": 0,
"outgoing_threshold": 60
}
],
"fee_ppm": "50000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": true,
"autoloop_budget_sat": "50000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "250000",
"max_swap_amount": "1000000",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1779405910",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_STATIC_ADDRESS"
}
}
}
},
{
"time_ms": 31,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "response",
"message_type": "looprpc.SetLiquidityParamsResponse",
"payload": {}
}
},
{
"time_ms": 32,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,125 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"getparams",
"--network",
"regtest"
],
"version": "0.33.0-beta commit=v0.33.0-beta-74-g6a3c3e1df5f807b450d3ba663f4824f6588e1c13 commit_hash=6a3c3e1df5f807b450d3ba663f4824f6588e1c13",
"duration": 12324887,
"clock_start_unix": 1779406647
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 5,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [
{
"channel_id": "0",
"swap_type": "LOOP_IN",
"pubkey": "AgN4nvpxLgk471Ol2/D2BGUJ9l/FOlv8dRHcZ/6+oaFl",
"type": "THRESHOLD",
"incoming_threshold": 0,
"outgoing_threshold": 60
}
],
"fee_ppm": "50000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": true,
"autoloop_budget_sat": "50000",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "250000",
"max_swap_amount": "1000000",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1779405910",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_STATIC_ADDRESS"
}
}
},
{
"time_ms": 12,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"account\": \"\",\n",
" \"account_addr_type\": \"ADDRESS_TYPE_UNKNOWN\",\n",
" \"auto_max_in_flight\": \"1\",\n",
" \"autoloop\": true,\n",
" \"autoloop_budget_last_refresh\": \"1779405910\",\n",
" \"autoloop_budget_refresh_period_sec\": \"604800\",\n",
" \"autoloop_budget_sat\": \"50000\",\n",
" \"autoloop_budget_start_sec\": \"0\",\n",
" \"autoloop_dest_address\": \"\",\n",
" \"easy_asset_params\": {},\n",
" \"easy_autoloop\": false,\n",
" \"easy_autoloop_excluded_peers\": [],\n",
" \"easy_autoloop_local_target_sat\": \"0\",\n",
" \"failure_backoff_sec\": \"86400\",\n",
" \"fast_swap_publication\": true,\n",
" \"fee_ppm\": \"50000\",\n",
" \"htlc_conf_target\": 6,\n",
" \"loop_in_source\": \"LOOP_IN_SOURCE_STATIC_ADDRESS\",\n",
" \"max_miner_fee_sat\": \"0\",\n",
" \"max_prepay_routing_fee_ppm\": \"0\",\n",
" \"max_prepay_sat\": \"0\",\n",
" \"max_routing_fee_ppm\": \"0\",\n",
" \"max_swap_amount\": \"1000000\",\n",
" \"max_swap_fee_ppm\": \"0\",\n",
" \"min_swap_amount\": \"250000\",\n",
" \"rules\": [\n",
" {\n",
" \"channel_id\": \"0\",\n",
" \"incoming_threshold\": 0,\n",
" \"outgoing_threshold\": 60,\n",
" \"pubkey\": \"0203789efa712e0938ef53a5dbf0f6046509f65fc53a5bfc7511dc67febea1a165\",\n",
" \"swap_type\": \"LOOP_IN\",\n",
" \"type\": \"THRESHOLD\"\n",
" }\n",
" ],\n",
" \"sweep_conf_target\": 100,\n",
" \"sweep_fee_rate_sat_per_vbyte\": \"0\"\n",
"}\n"
]
}
},
{
"time_ms": 12,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,101 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"suggestswaps",
"--network",
"regtest"
],
"version": "0.33.0-beta commit=v0.33.0-beta-74-g6a3c3e1df5f807b450d3ba663f4824f6588e1c13 commit_hash=6a3c3e1df5f807b450d3ba663f4824f6588e1c13",
"duration": 44264819,
"clock_start_unix": 1779406653
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SuggestSwaps",
"event": "request",
"message_type": "looprpc.SuggestSwapsRequest",
"payload": {}
}
},
{
"time_ms": 43,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SuggestSwaps",
"event": "response",
"message_type": "looprpc.SuggestSwapsResponse",
"payload": {
"loop_out": [],
"loop_in": [],
"static_loop_in": [
{
"outpoints": [
"9cfb0ec5ab2c851b4db5fe372cc3573f4074c436e50b13d2b7d214e45571def9:1"
],
"max_swap_fee_satoshis": "1828",
"last_hop": "AgN4nvpxLgk471Ol2/D2BGUJ9l/FOlv8dRHcZ/6+oaFl",
"label": "[reserved]: autoloop-in",
"initiator": "autoloop",
"route_hints": [],
"private": false,
"payment_timeout_seconds": 0,
"amount": "500000",
"fast": false
}
],
"disqualified": [
{
"channel_id": "127543348887552",
"pubkey": "",
"reason": "AUTO_REASON_CUSTOM_CHANNEL_DATA"
}
]
}
}
},
{
"time_ms": 43,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"disqualified\": [\n",
" {\n",
" \"channel_id\": \"127543348887552\",\n",
" \"pubkey\": \"\",\n",
" \"reason\": \"AUTO_REASON_CUSTOM_CHANNEL_DATA\"\n",
" }\n",
" ],\n",
" \"loop_in\": [],\n",
" \"loop_out\": [],\n",
" \"static_loop_in\": [\n",
" {\n",
" \"amount\": \"500000\",\n",
" \"fast\": false,\n",
" \"initiator\": \"autoloop\",\n",
" \"label\": \"[reserved]: autoloop-in\",\n",
" \"last_hop\": \"0203789efa712e0938ef53a5dbf0f6046509f65fc53a5bfc7511dc67febea1a165\",\n",
" \"max_swap_fee_satoshis\": \"1828\",\n",
" \"outpoints\": [\n",
" \"9cfb0ec5ab2c851b4db5fe372cc3573f4074c436e50b13d2b7d214e45571def9:1\"\n",
" ],\n",
" \"payment_timeout_seconds\": 0,\n",
" \"private\": false,\n",
" \"route_hints\": []\n",
" }\n",
" ]\n",
"}\n"
]
}
},
{
"time_ms": 44,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,137 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"setparams",
"--network",
"regtest",
"--loopinsource",
"static-address"
],
"version": "0.33.0-beta commit=v0.33.0-beta-74-g6a3c3e1df5f807b450d3ba663f4824f6588e1c13 commit_hash=6a3c3e1df5f807b450d3ba663f4824f6588e1c13",
"run_error": "rpc error: code = FailedPrecondition desc = static address autoloop requires --experimental",
"duration": 12501068,
"clock_start_unix": 1779406912
},
"events": [
{
"time_ms": 4,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "request",
"message_type": "looprpc.GetLiquidityParamsRequest",
"payload": {}
}
},
{
"time_ms": 11,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/GetLiquidityParams",
"event": "response",
"message_type": "looprpc.LiquidityParameters",
"payload": {
"rules": [],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1779406903",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_WALLET"
}
}
},
{
"time_ms": 11,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "request",
"message_type": "looprpc.SetLiquidityParamsRequest",
"payload": {
"parameters": {
"rules": [],
"fee_ppm": "20000",
"sweep_fee_rate_sat_per_vbyte": "0",
"max_swap_fee_ppm": "0",
"max_routing_fee_ppm": "0",
"max_prepay_routing_fee_ppm": "0",
"max_prepay_sat": "0",
"max_miner_fee_sat": "0",
"sweep_conf_target": 100,
"failure_backoff_sec": "86400",
"autoloop": false,
"autoloop_budget_sat": "335544",
"autoloop_budget_start_sec": "0",
"auto_max_in_flight": "1",
"min_swap_amount": "0",
"max_swap_amount": "0",
"htlc_conf_target": 6,
"autoloop_dest_address": "",
"autoloop_budget_refresh_period_sec": "604800",
"autoloop_budget_last_refresh": "1779406903",
"easy_autoloop": false,
"easy_autoloop_local_target_sat": "0",
"account": "",
"account_addr_type": "ADDRESS_TYPE_UNKNOWN",
"easy_asset_params": {},
"fast_swap_publication": true,
"easy_autoloop_excluded_peers": [],
"loop_in_source": "LOOP_IN_SOURCE_STATIC_ADDRESS"
}
}
}
},
{
"time_ms": 12,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/SetLiquidityParams",
"event": "error",
"error": "rpc error: code = FailedPrecondition desc = static address autoloop requires --experimental",
"status": {
"code": 9,
"message": "static address autoloop requires --experimental"
}
}
},
{
"time_ms": 12,
"kind": "stderr",
"data": {
"lines": [
"[loop] rpc error: code = FailedPrecondition desc = static address autoloop requires --experimental\n"
]
}
},
{
"time_ms": 12,
"kind": "exit",
"data": {
"run_error": "rpc error: code = FailedPrecondition desc = static address autoloop requires --experimental"
}
}
]
}

View file

@ -0,0 +1,62 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"static",
"listdeposits",
"--network",
"regtest",
"--filter",
"deposited"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 304503708,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "request",
"message_type": "looprpc.ListStaticAddressDepositsRequest",
"payload": {
"state_filter": "DEPOSITED",
"outpoints": []
}
}
},
{
"time_ms": 303,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "response",
"message_type": "looprpc.ListStaticAddressDepositsResponse",
"payload": {
"filtered_deposits": []
}
}
},
{
"time_ms": 304,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"filtered_deposits\": []\n",
"}\n"
]
}
},
{
"time_ms": 304,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,82 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"static",
"listdeposits",
"--network",
"regtest",
"--filter",
"withdrawing"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 210052149,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "request",
"message_type": "looprpc.ListStaticAddressDepositsRequest",
"payload": {
"state_filter": "WITHDRAWING",
"outpoints": []
}
}
},
{
"time_ms": 209,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "response",
"message_type": "looprpc.ListStaticAddressDepositsResponse",
"payload": {
"filtered_deposits": [
{
"id": "u38FDfC3w+H+YQEOEK1F4w3fes0wH6bgWi3bgltcLvs=",
"state": "WITHDRAWING",
"outpoint": "56cd081a3a6eadf25b7d3fe0b61207389352ed69a622d2ec28c5d669bf6a5313:0",
"value": "500000",
"confirmation_height": "160",
"blocks_until_expiry": "14395",
"swap_hash": ""
}
]
}
}
},
{
"time_ms": 209,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"filtered_deposits\": [\n",
" {\n",
" \"blocks_until_expiry\": \"14395\",\n",
" \"confirmation_height\": \"160\",\n",
" \"id\": \"bb7f050df0b7c3e1fe61010e10ad45e30ddf7acd301fa6e05a2ddb825b5c2efb\",\n",
" \"outpoint\": \"56cd081a3a6eadf25b7d3fe0b61207389352ed69a622d2ec28c5d669bf6a5313:0\",\n",
" \"state\": \"WITHDRAWING\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",
" ]\n",
"}\n"
]
}
},
{
"time_ms": 210,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,136 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"static",
"listdeposits",
"--network",
"regtest",
"--filter",
"withdrawn"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 271219039,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "request",
"message_type": "looprpc.ListStaticAddressDepositsRequest",
"payload": {
"state_filter": "WITHDRAWN",
"outpoints": []
}
}
},
{
"time_ms": 269,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "response",
"message_type": "looprpc.ListStaticAddressDepositsResponse",
"payload": {
"filtered_deposits": [
{
"id": "aCYqEEyewyXea+w3uOMb2HW70vXwuc4togzwvWNvxEg=",
"state": "WITHDRAWN",
"outpoint": "edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0",
"value": "500000",
"confirmation_height": "125",
"blocks_until_expiry": "14360",
"swap_hash": ""
},
{
"id": "hrXizflpTI5zmOQq/eEJdm180hQiA5Bbpj+9DrE3DvM=",
"state": "WITHDRAWN",
"outpoint": "bb358e4f73ae97c4e2d99c6d64e852bba7cf56e13105b05d1200b8ae1796665e:0",
"value": "500000",
"confirmation_height": "127",
"blocks_until_expiry": "14362",
"swap_hash": ""
},
{
"id": "bCkPdTbqUJeUav/6xqaZBqJtd1gj67rO3+by1pwHReQ=",
"state": "WITHDRAWN",
"outpoint": "5eaa7dd7a291665393eddf5dece91feef901f22665933cce7a0732a9b81c3001:0",
"value": "500000",
"confirmation_height": "128",
"blocks_until_expiry": "14363",
"swap_hash": ""
},
{
"id": "AYK02JWxxGcpCue1xsQv92sqQiWAepQhHJcxcNWog+s=",
"state": "WITHDRAWN",
"outpoint": "7e6360d6e6a394cfd096adf0bfe1275c5a83541eb573e90e463a78dc715f8894:0",
"value": "500000",
"confirmation_height": "142",
"blocks_until_expiry": "14377",
"swap_hash": ""
}
]
}
}
},
{
"time_ms": 269,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"filtered_deposits\": [\n",
" {\n",
" \"blocks_until_expiry\": \"14360\",\n",
" \"confirmation_height\": \"125\",\n",
" \"id\": \"68262a104c9ec325de6bec37b8e31bd875bbd2f5f0b9ce2da20cf0bd636fc448\",\n",
" \"outpoint\": \"edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
" {\n",
" \"blocks_until_expiry\": \"14362\",\n",
" \"confirmation_height\": \"127\",\n",
" \"id\": \"86b5e2cdf9694c8e7398e42afde109766d7cd2142203905ba63fbd0eb1370ef3\",\n",
" \"outpoint\": \"bb358e4f73ae97c4e2d99c6d64e852bba7cf56e13105b05d1200b8ae1796665e:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
" {\n",
" \"blocks_until_expiry\": \"14363\",\n",
" \"confirmation_height\": \"128\",\n",
" \"id\": \"6c290f7536ea5097946afffac6a69906a26d775823ebbacedfe6f2d69c0745e4\",\n",
" \"outpoint\": \"5eaa7dd7a291665393eddf5dece91feef901f22665933cce7a0732a9b81c3001:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
" {\n",
" \"blocks_until_expiry\": \"14377\",\n",
" \"confirmation_height\": \"142\",\n",
" \"id\": \"0182b4d895b1c467290ae7b5c6c42ff76b2a4225807a94211c973170d5a883eb\",\n",
" \"outpoint\": \"7e6360d6e6a394cfd096adf0bfe1275c5a83541eb573e90e463a78dc715f8894:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",
" ]\n",
"}\n"
]
}
},
{
"time_ms": 271,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,62 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"static",
"listdeposits",
"--network",
"regtest",
"--filter",
"looping_in"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 268850770,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "request",
"message_type": "looprpc.ListStaticAddressDepositsRequest",
"payload": {
"state_filter": "LOOPING_IN",
"outpoints": []
}
}
},
{
"time_ms": 268,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "response",
"message_type": "looprpc.ListStaticAddressDepositsResponse",
"payload": {
"filtered_deposits": []
}
}
},
{
"time_ms": 268,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"filtered_deposits\": []\n",
"}\n"
]
}
},
{
"time_ms": 268,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,82 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"static",
"listdeposits",
"--network",
"regtest",
"--filter",
"looped_in"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 169458759,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 3,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "request",
"message_type": "looprpc.ListStaticAddressDepositsRequest",
"payload": {
"state_filter": "LOOPED_IN",
"outpoints": []
}
}
},
{
"time_ms": 168,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "response",
"message_type": "looprpc.ListStaticAddressDepositsResponse",
"payload": {
"filtered_deposits": [
{
"id": "j71tovlF3ikFqn+pOGB0TZOH00ZEhDYOluRnpR3jvJ0=",
"state": "LOOPED_IN",
"outpoint": "9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0",
"value": "500000",
"confirmation_height": "148",
"blocks_until_expiry": "14383",
"swap_hash": "hDAjN0JANkGTlqt5ZN14uFsaSBqfHbc9tc3e5XwkQ+c="
}
]
}
}
},
{
"time_ms": 168,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"filtered_deposits\": [\n",
" {\n",
" \"blocks_until_expiry\": \"14383\",\n",
" \"confirmation_height\": \"148\",\n",
" \"id\": \"8fbd6da2f945de2905aa7fa93860744d9387d3464484360e96e467a51de3bc9d\",\n",
" \"outpoint\": \"9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0\",\n",
" \"state\": \"LOOPED_IN\",\n",
" \"swap_hash\": \"84302337424036419396ab7964dd78b85b1a481a9f1db73db5cddee57c2443e7\",\n",
" \"value\": \"500000\"\n",
" }\n",
" ]\n",
"}\n"
]
}
},
{
"time_ms": 169,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,62 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"static",
"listdeposits",
"--network",
"regtest",
"--filter",
"publish_expired_deposit"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 230057326,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 2,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "request",
"message_type": "looprpc.ListStaticAddressDepositsRequest",
"payload": {
"state_filter": "PUBLISH_EXPIRED",
"outpoints": []
}
}
},
{
"time_ms": 229,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "response",
"message_type": "looprpc.ListStaticAddressDepositsResponse",
"payload": {
"filtered_deposits": []
}
}
},
{
"time_ms": 230,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"filtered_deposits\": []\n",
"}\n"
]
}
},
{
"time_ms": 230,
"kind": "exit",
"data": {}
}
]
}

View file

@ -0,0 +1,62 @@
{
"metadata": {
"args": [
"/home/user/bin/loop",
"static",
"listdeposits",
"--network",
"regtest",
"--filter",
"sweep_htlc_timeout"
],
"env": {
"HOME": "/home/user"
},
"version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845",
"duration": 224559305,
"clock_start_unix": 1769407086
},
"events": [
{
"time_ms": 1,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "request",
"message_type": "looprpc.ListStaticAddressDepositsRequest",
"payload": {
"state_filter": "SWEEP_HTLC_TIMEOUT",
"outpoints": []
}
}
},
{
"time_ms": 223,
"kind": "grpc",
"data": {
"method": "/looprpc.SwapClient/ListStaticAddressDeposits",
"event": "response",
"message_type": "looprpc.ListStaticAddressDepositsResponse",
"payload": {
"filtered_deposits": []
}
}
},
{
"time_ms": 224,
"kind": "stdout",
"data": {
"lines": [
"{\n",
" \"filtered_deposits\": []\n",
"}\n"
]
}
},
{
"time_ms": 224,
"kind": "exit",
"data": {}
}
]
}

Some files were not shown because too many files have changed in this diff Show more