mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
cmd/loop: warn for auto-selected low-conf deposits
Remove the old "no confirmed deposits available" error now that mempool deposits are listed immediately and can be selected for static loop-ins. Reproduce the server static-address deposit selection order in the CLI using the already-returned deposit metadata. This keeps the low-confirmation warning focused on the deposits auto-selection would actually choose, so users only see it when the swap payment may wait for the server confirmation-risk policy.
This commit is contained in:
parent
30a14ecc82
commit
cbe8eecb36
2 changed files with 402 additions and 5 deletions
|
|
@ -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"
|
||||
lndcommands "github.com/lightningnetwork/lnd/cmd/commands"
|
||||
"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"
|
||||
)
|
||||
|
|
@ -553,11 +556,14 @@ 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("no deposited outputs available")
|
||||
}
|
||||
|
||||
return errors.New(errString)
|
||||
summary, err := client.GetStaticAddressSummary(
|
||||
ctx, &looprpc.StaticAddressSummaryRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var depositOutpoints []string
|
||||
|
|
@ -614,6 +620,21 @@ 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.
|
||||
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 {
|
||||
|
|
@ -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.
|
||||
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 " +
|
||||
|
|
|
|||
220
cmd/loop/staticaddr_test.go
Normal file
220
cmd/loop/staticaddr_test.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue