Skip to content

Commit 61e1f72

Browse files
committed
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.
1 parent 8374730 commit 61e1f72

2 files changed

Lines changed: 403 additions & 6 deletions

File tree

cmd/loop/staticaddr.go

Lines changed: 183 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"sort"
8+
"strings"
79

810
"github.com/lightninglabs/loop/labels"
911
"github.com/lightninglabs/loop/looprpc"
10-
"github.com/lightninglabs/loop/staticaddr/deposit"
1112
"github.com/lightninglabs/loop/staticaddr/loopin"
1213
"github.com/lightninglabs/loop/swapserverrpc"
1314
"github.com/lightningnetwork/lnd"
15+
"github.com/lightningnetwork/lnd/input"
1416
"github.com/lightningnetwork/lnd/lnrpc"
17+
"github.com/lightningnetwork/lnd/lnwallet"
1518
"github.com/lightningnetwork/lnd/routing/route"
1619
"github.com/urfave/cli/v3"
1720
)
@@ -553,11 +556,7 @@ func staticAddressLoopIn(ctx context.Context, cmd *cli.Command) error {
553556
allDeposits := depositList.FilteredDeposits
554557

555558
if len(allDeposits) == 0 {
556-
errString := fmt.Sprintf("no confirmed deposits available, "+
557-
"deposits need at least %v confirmations",
558-
deposit.MinConfs)
559-
560-
return errors.New(errString)
559+
return errors.New("no deposited outputs available")
561560
}
562561

563562
var depositOutpoints []string
@@ -614,6 +613,28 @@ func staticAddressLoopIn(ctx context.Context, cmd *cli.Command) error {
614613
return err
615614
}
616615

616+
// Warn the user if any selected deposits have fewer than 6
617+
// confirmations, as the swap payment won't be received immediately
618+
// for those.
619+
summary, err := client.GetStaticAddressSummary(
620+
ctx, &looprpc.StaticAddressSummaryRequest{},
621+
)
622+
if err != nil {
623+
return err
624+
}
625+
626+
depositsToCheck := warningDepositOutpoints(
627+
allDeposits, depositOutpoints, autoSelectDepositsForQuote,
628+
quoteReq.Amt,
629+
)
630+
warning := lowConfDepositWarning(
631+
allDeposits, depositsToCheck,
632+
int64(summary.RelativeExpiryBlocks),
633+
)
634+
if warning != "" {
635+
fmt.Println(warning)
636+
}
637+
617638
if !(cmd.Bool("force") || cmd.Bool("f")) {
618639
err = displayInDetails(quoteReq, quote, cmd.Bool("verbose"))
619640
if err != nil {
@@ -669,6 +690,162 @@ func depositsToOutpoints(deposits []*looprpc.Deposit) []string {
669690
return outpoints
670691
}
671692

693+
var warningSelectionDustLimit = int64(lnwallet.DustLimitForSize(input.P2TRSize))
694+
695+
// warningDepositOutpoints returns the deposit outpoints to check for
696+
// low-confirmation warnings.
697+
func warningDepositOutpoints(allDeposits []*looprpc.Deposit,
698+
selectedOutpoints []string, autoSelect bool, targetAmount int64) []string {
699+
700+
if !autoSelect {
701+
return selectedOutpoints
702+
}
703+
704+
return autoSelectedWarningOutpoints(allDeposits, targetAmount)
705+
}
706+
707+
// autoSelectedWarningOutpoints returns the outpoints selected by the same
708+
// ordering used for automatic static loop-in deposit selection.
709+
func autoSelectedWarningOutpoints(allDeposits []*looprpc.Deposit,
710+
targetAmount int64) []string {
711+
712+
if targetAmount <= 0 {
713+
return nil
714+
}
715+
716+
// KEEP IN SYNC with staticaddr/loopin.SelectDeposits.
717+
deposits := filterSwappableWarningDeposits(allDeposits)
718+
sort.Slice(deposits, func(i, j int) bool {
719+
iConfirmed := deposits[i].ConfirmationHeight > 0
720+
jConfirmed := deposits[j].ConfirmationHeight > 0
721+
if iConfirmed != jConfirmed {
722+
return iConfirmed
723+
}
724+
725+
if deposits[i].Value == deposits[j].Value {
726+
return deposits[i].BlocksUntilExpiry <
727+
deposits[j].BlocksUntilExpiry
728+
}
729+
730+
return deposits[i].Value > deposits[j].Value
731+
})
732+
733+
selectedOutpoints := make([]string, 0, len(deposits))
734+
var selectedAmount int64
735+
for _, deposit := range deposits {
736+
selectedOutpoints = append(selectedOutpoints, deposit.Outpoint)
737+
selectedAmount += deposit.Value
738+
if selectedAmount == targetAmount {
739+
return selectedOutpoints
740+
}
741+
742+
if selectedAmount > targetAmount &&
743+
selectedAmount-targetAmount >= warningSelectionDustLimit {
744+
745+
return selectedOutpoints
746+
}
747+
}
748+
749+
return nil
750+
}
751+
752+
// filterSwappableWarningDeposits filters deposits for CLI warning selection.
753+
func filterSwappableWarningDeposits(
754+
allDeposits []*looprpc.Deposit) []*looprpc.Deposit {
755+
756+
swappable := make([]*looprpc.Deposit, 0, len(allDeposits))
757+
minBlocksUntilExpiry := int64(
758+
loopin.DefaultLoopInOnChainCltvDelta + loopin.DepositHtlcDelta,
759+
)
760+
for _, deposit := range allDeposits {
761+
// Unconfirmed deposits remain swappable because their CSV timeout has
762+
// not started yet. This mirrors loopin.IsSwappable.
763+
if deposit.ConfirmationHeight > 0 &&
764+
deposit.BlocksUntilExpiry < minBlocksUntilExpiry {
765+
766+
continue
767+
}
768+
769+
swappable = append(swappable, deposit)
770+
}
771+
772+
return swappable
773+
}
774+
775+
// conservativeWarningConfs is the highest default confirmation tier used by
776+
// the server's dynamic confirmation-risk policy.
777+
//
778+
// The CLI does not currently know the server's exact policy, so we use this
779+
// conservative threshold for warnings without promising immediate execution.
780+
const conservativeWarningConfs = 6
781+
782+
// lowConfDepositWarning checks the selected deposits against a conservative
783+
// confirmation threshold and returns a warning string if any are found.
784+
func lowConfDepositWarning(allDeposits []*looprpc.Deposit,
785+
selectedOutpoints []string, csvExpiry int64) string {
786+
787+
depositMap := make(map[string]*looprpc.Deposit, len(allDeposits))
788+
for _, d := range allDeposits {
789+
depositMap[d.Outpoint] = d
790+
}
791+
792+
var lowConfEntries []string
793+
for _, op := range selectedOutpoints {
794+
d, ok := depositMap[op]
795+
if !ok {
796+
continue
797+
}
798+
799+
var confs int64
800+
switch {
801+
case d.ConfirmationHeight <= 0:
802+
confs = 0
803+
804+
case csvExpiry > 0:
805+
// For confirmed deposits we can compute
806+
// confirmations as CSVExpiry - BlocksUntilExpiry + 1.
807+
confs = csvExpiry - d.BlocksUntilExpiry + 1
808+
809+
default:
810+
// Can't determine confirmations without the CSV expiry.
811+
continue
812+
}
813+
814+
if confs >= conservativeWarningConfs {
815+
continue
816+
}
817+
818+
if confs == 0 {
819+
lowConfEntries = append(
820+
lowConfEntries,
821+
fmt.Sprintf(" - %s (unconfirmed)", op),
822+
)
823+
} else {
824+
lowConfEntries = append(
825+
lowConfEntries,
826+
fmt.Sprintf(
827+
" - %s (%d confirmations)", op,
828+
confs,
829+
),
830+
)
831+
}
832+
}
833+
834+
if len(lowConfEntries) == 0 {
835+
return ""
836+
}
837+
838+
return fmt.Sprintf(
839+
"\nWARNING: The following deposits are below the "+
840+
"conservative %d-confirmation threshold:\n%s\n"+
841+
"The swap payment for these deposits may wait for "+
842+
"more confirmations depending on the server's "+
843+
"confirmation-risk policy.\n",
844+
conservativeWarningConfs,
845+
strings.Join(lowConfEntries, "\n"),
846+
)
847+
}
848+
672849
func displayNewAddressWarning() error {
673850
fmt.Printf("\nWARNING: Be aware that loosing your l402.token file in " +
674851
".loop under your home directory will take your ability to " +

0 commit comments

Comments
 (0)