Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1abe617
staticaddr/deposit: track unconfirmed deposits
hieblmi Jul 8, 2026
ac12d25
staticaddr/deposit: replay startup block after recovery
hieblmi Jul 8, 2026
e4bcc94
staticaddr/deposit: reconcile active deposits with wallet
hieblmi Jul 8, 2026
e8dd3aa
staticaddr: expose tracked deposit availability
hieblmi Jul 8, 2026
557ba99
staticaddr: require confirmed deposits for withdraws and channel opens
hieblmi Jul 8, 2026
0223caa
staticaddr/loopin: treat unconfirmed deposits as swappable
hieblmi Jul 8, 2026
1c89ff8
staticaddr/loopin: account for autoloop deposit expiry
hieblmi Jul 6, 2026
dc7da41
staticaddr: refresh deposits before spend selection
hieblmi Jul 8, 2026
1d935c6
loopd: filter static deposit views through active set
hieblmi Jul 8, 2026
ef78c85
staticaddr/loopin: cancel signing for unavailable deposits
hieblmi Jul 8, 2026
da96842
notifications: fan out static loop-in risk decisions
hieblmi Jul 8, 2026
b0f43bb
staticaddr/loopin: wait for risk decisions
hieblmi Jul 8, 2026
491fddb
loopdb: persist static loop-in risk decisions
hieblmi Jul 8, 2026
e53bb67
notifications: persist static loop-in risk decisions
hieblmi Jul 8, 2026
3e2b945
loopd: wire static risk decision persistence
hieblmi Jul 8, 2026
f01a1f0
staticaddr/loopin: clean up deposits after unpaid htlc timeout
hieblmi Jul 8, 2026
6593afc
staticaddr/loopin: recover risk-decision deadlines
hieblmi Jul 8, 2026
332aab7
staticaddr/loopin: add risk decision watcher
hieblmi Jul 9, 2026
6927c67
staticaddr/loopin: use risk decision watcher
hieblmi Jul 9, 2026
c718e9a
staticaddr/loopin: retry failed deposit state transitions
hieblmi Jul 15, 2026
43b16ff
cmd/loop: warn for low-confirmation static deposits
hieblmi Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 183 additions & 6 deletions cmd/loop/staticaddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import (
"context"
"errors"
"fmt"
"sort"
"strings"

"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/loopin"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli/v3"
)
Expand Down Expand Up @@ -553,11 +556,7 @@ func staticAddressLoopIn(ctx context.Context, cmd *cli.Command) error {
allDeposits := depositList.FilteredDeposits

if len(allDeposits) == 0 {
errString := fmt.Sprintf("no confirmed deposits available, "+
"deposits need at least %v confirmations",
deposit.MinConfs)

return errors.New(errString)
return errors.New("no deposited outputs available")
}

var depositOutpoints []string
Expand Down Expand Up @@ -614,6 +613,28 @@ func staticAddressLoopIn(ctx context.Context, cmd *cli.Command) error {
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")) {
err = displayInDetails(quoteReq, quote, cmd.Bool("verbose"))
if err != nil {
Expand Down Expand Up @@ -669,6 +690,162 @@ func depositsToOutpoints(deposits []*looprpc.Deposit) []string {
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.
Comment thread
hieblmi marked this conversation as resolved.
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 {
fmt.Printf("\nWARNING: Be aware that loosing your l402.token file in " +
".loop under your home directory will take your ability to " +
Expand Down
Loading
Loading