Skip to content
102 changes: 97 additions & 5 deletions cmd/loop/staticaddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"context"
"errors"
"fmt"
"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"
Expand Down Expand Up @@ -553,11 +553,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
Expand Down Expand Up @@ -614,6 +617,22 @@ 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 := depositOutpoints
if autoSelectDepositsForQuote {
// When auto-selecting, any deposit could be chosen.
depositsToCheck = depositsToOutpoints(allDeposits)
}
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 +688,79 @@ func depositsToOutpoints(deposits []*looprpc.Deposit) []string {
return outpoints
}

// minImmediateConfs is the minimum number of confirmations a deposit needs
// for the swap payment to be executed immediately.
const minImmediateConfs = 6

// lowConfDepositWarning checks the selected deposits for fewer than 6
// confirmations and returns a warning string if any are found. The swap
// payment for such deposits won't be received immediately.
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 >= minImmediateConfs {
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 have fewer than %d "+
"confirmations:\n%s\n"+
"The swap payment for these deposits may not be "+
"received immediately.\nOnly deposits with %d or "+
"more confirmations are executed immediately.\n",
minImmediateConfs,
strings.Join(lowConfEntries, "\n"),
minImmediateConfs,
)
}

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
50 changes: 50 additions & 0 deletions cmd/loop/staticaddr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"strings"
"testing"

"github.com/lightninglabs/loop/looprpc"
"github.com/stretchr/testify/require"
)

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")
}

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, "fewer than 6 confirmations"))
}
105 changes: 59 additions & 46 deletions loopd/swapclient_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1662,54 +1662,43 @@ func (s *swapClientServer) ListUnspentDeposits(ctx context.Context,
// not spendable because they already have been used but not yet spent
// by the server. We filter out such deposits here.
var (
outpoints []string
isUnspent = make(map[wire.OutPoint]struct{})
outpoints []string
isUnspent = make(map[wire.OutPoint]struct{})
knownUtxos = make(map[wire.OutPoint]struct{})
)

// Keep track of confirmed outpoints that we need to check against our
// database.
confirmedToCheck := make(map[wire.OutPoint]struct{})

for _, utxo := range utxos {
if utxo.Confirmations < deposit.MinConfs {
// Unconfirmed deposits are always available.
isUnspent[utxo.OutPoint] = struct{}{}
} else {
// Confirmed deposits need to be checked.
outpoints = append(outpoints, utxo.OutPoint.String())
confirmedToCheck[utxo.OutPoint] = struct{}{}
}
outpoints = append(outpoints, utxo.OutPoint.String())
knownUtxos[utxo.OutPoint] = struct{}{}
}

// Check the spent status of the deposits by looking at their states.
ignoreUnknownOutpoints := false
ignoreUnknownOutpoints := true
deposits, err := s.depositManager.DepositsForOutpoints(
ctx, outpoints, ignoreUnknownOutpoints,
)
if err != nil {
return nil, err
}

knownDeposits := make(map[wire.OutPoint]struct{}, len(deposits))
for _, d := range deposits {
// A nil deposit means we don't have a record for it. We'll
// handle this case after the loop.
if d == nil {
continue
}

// If the deposit is in the "Deposited" state, it's available.
knownDeposits[d.OutPoint] = struct{}{}
if d.IsInState(deposit.Deposited) {
isUnspent[d.OutPoint] = struct{}{}
}

// We have a record for this deposit, so we no longer need to
// check it.
delete(confirmedToCheck, d.OutPoint)
}

// Any remaining outpoints in confirmedToCheck are ones that lnd knows
// about but we don't. These are new, unspent deposits.
for op := range confirmedToCheck {
isUnspent[op] = struct{}{}
// Any wallet outpoints that are unknown to the deposit store are new
// deposits and therefore still available.
for op := range knownUtxos {
if _, ok := knownDeposits[op]; !ok {
isUnspent[op] = struct{}{}
}
}

// Prepare the list of unspent deposits for the rpc response.
Expand Down Expand Up @@ -1756,7 +1745,7 @@ func (s *swapClientServer) WithdrawDeposits(ctx context.Context,
return nil, err
}

for _, d := range deposits {
for _, d := range confirmedDeposits(deposits) {
outpoints = append(outpoints, d.OutPoint)
}

Expand All @@ -1780,6 +1769,22 @@ func (s *swapClientServer) WithdrawDeposits(ctx context.Context,
}, err
}

// confirmedDeposits filters the given deposits and returns only those that have
// a positive confirmation height, i.e. deposits that have been confirmed
// on-chain.
func confirmedDeposits(deposits []*deposit.Deposit) []*deposit.Deposit {
confirmed := make([]*deposit.Deposit, 0, len(deposits))
for _, d := range deposits {
if d.ConfirmationHeight <= 0 {
continue
}

confirmed = append(confirmed, d)
}

return confirmed
}

// ListStaticAddressDeposits returns a list of all sufficiently confirmed
// deposits behind the static address and displays properties like value,
// state or blocks til expiry.
Expand Down Expand Up @@ -1948,9 +1953,10 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context,
protoDeposits = make([]*looprpc.Deposit, 0, len(ds))
for _, d := range ds {
state := toClientDepositState(d.GetState())
blocksUntilExpiry := d.ConfirmationHeight +
int64(addrParams.Expiry) -
int64(lndInfo.BlockHeight)
blocksUntilExpiry := depositBlocksUntilExpiry(
d.ConfirmationHeight, addrParams.Expiry,
int64(lndInfo.BlockHeight),
)

pd := &looprpc.Deposit{
Id: d.ID[:],
Expand Down Expand Up @@ -2011,23 +2017,16 @@ func (s *swapClientServer) GetStaticAddressSummary(ctx context.Context,
htlcTimeoutSwept int64
)

// Value unconfirmed.
utxos, err := s.staticAddressManager.ListUnspent(
ctx, 0, deposit.MinConfs-1,
)
if err != nil {
return nil, err
}
for _, u := range utxos {
valueUnconfirmed += int64(u.Value)
}

// Confirmed total values by category.
// Total values by category.
for _, d := range allDeposits {
value := int64(d.Value)
switch d.GetState() {
case deposit.Deposited:
valueDeposited += value
if d.ConfirmationHeight <= 0 {
valueUnconfirmed += value
} else {
valueDeposited += value
}

case deposit.Expired:
valueExpired += value
Expand Down Expand Up @@ -2170,13 +2169,27 @@ func (s *swapClientServer) populateBlocksUntilExpiry(ctx context.Context,
return err
}
for i := range len(deposits) {
deposits[i].BlocksUntilExpiry =
deposits[i].ConfirmationHeight +
int64(params.Expiry) - bestBlockHeight
deposits[i].BlocksUntilExpiry = depositBlocksUntilExpiry(
deposits[i].ConfirmationHeight, params.Expiry,
bestBlockHeight,
)
}
return nil
}

// depositBlocksUntilExpiry returns the remaining blocks until a deposit
// expires. Unconfirmed deposits return the full CSV value because the timeout
// has not started yet.
func depositBlocksUntilExpiry(confirmationHeight int64, expiry uint32,
bestBlockHeight int64) int64 {

if confirmationHeight <= 0 {
return int64(expiry)
}

return confirmationHeight + int64(expiry) - bestBlockHeight
}

// StaticOpenChannel initiates an open channel request using static address
// deposits.
func (s *swapClientServer) StaticOpenChannel(ctx context.Context,
Expand Down
Loading
Loading