@@ -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+ "\n WARNING: 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+
672849func displayNewAddressWarning () error {
673850 fmt .Printf ("\n WARNING: Be aware that loosing your l402.token file in " +
674851 ".loop under your home directory will take your ability to " +
0 commit comments