Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f3d6d44
swap: reserve multi-address key families
hieblmi Jul 10, 2026
c9a5873
loopdb: persist deposit address ownership
hieblmi Jul 10, 2026
d76d13a
staticaddr/address: activate derived addresses
hieblmi Jul 10, 2026
dd946c8
staticaddr/deposit: bind deposits to owning addresses
hieblmi Jul 10, 2026
13780f6
staticaddr/deposit: detect replaced expiry sweeps
hieblmi Aug 27, 2026
0b83dfa
staticaddr: create signing sessions from deposit keys
hieblmi Aug 27, 2026
b57e0b5
staticaddr/loopin: send per-deposit address proofs
hieblmi Aug 27, 2026
89281ce
staticaddr/withdraw: send per-deposit address proofs
hieblmi Jul 10, 2026
96f7f2e
staticaddr/deposit: restore owning address parameters
hieblmi Aug 27, 2026
a237e00
staticaddr/loopin: persist change addresses
hieblmi Aug 28, 2026
ac1be16
staticaddr/loopin: use generated change addresses
hieblmi Aug 28, 2026
0a102ce
staticaddr/loopin: drop legacy address state
hieblmi Aug 26, 2026
d96110c
staticaddr/withdraw: use generated change addresses
hieblmi Aug 27, 2026
ae27449
staticaddr: fund new addresses with sendcoins
hieblmi Aug 27, 2026
526b386
staticaddr: expose addresses in deposit listings
hieblmi Aug 27, 2026
9591165
staticaddr: classify missing address RPC errors
hieblmi Aug 26, 2026
fcac8b9
looprpc: deprecate singular static summary address
hieblmi Aug 26, 2026
3d99cc1
staticaddr/deposit: bind expiry confirmation to outpoint
hieblmi Aug 26, 2026
53c03d0
staticaddr: add multi-address integration coverage
hieblmi Aug 27, 2026
03012c2
staticaddr/loopin: require one prevout per sweep input
hieblmi Sep 1, 2026
77e738e
docs: document multi-address static deposits
hieblmi Aug 27, 2026
56816b7
staticaddr: validate per-deposit quote expiry
hieblmi Sep 2, 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
379 changes: 364 additions & 15 deletions cmd/loop/staticaddr.go

Large diffs are not rendered by default.

242 changes: 240 additions & 2 deletions cmd/loop/staticaddr_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,254 @@
package main

import (
"bytes"
"context"
"errors"
"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/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/loopin"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type staticAddressSummaryErrorClient struct {
looprpc.SwapClientClient

err error
newAddressRequest *looprpc.NewStaticAddressRequest
newAddressResponse *looprpc.NewStaticAddressResponse
newAddressCalls int
}

func (c *staticAddressSummaryErrorClient) GetStaticAddressSummary(
context.Context, *looprpc.StaticAddressSummaryRequest,
...grpc.CallOption) (*looprpc.StaticAddressSummaryResponse, error) {

return nil, c.err
}

func (c *staticAddressSummaryErrorClient) NewStaticAddress(
_ context.Context, req *looprpc.NewStaticAddressRequest,
_ ...grpc.CallOption) (*looprpc.NewStaticAddressResponse, error) {

c.newAddressCalls++
c.newAddressRequest = req

return c.newAddressResponse, nil
}

func TestMaybeDisplayNewAddressWarningReturnsUnexpectedError(t *testing.T) {
t.Parallel()

expectedErr := errors.New("permission denied")
err := maybeDisplayNewAddressWarning(
context.Background(), &staticAddressSummaryErrorClient{
err: expectedErr,
},
false, strings.NewReader(""), &bytes.Buffer{},
)
require.ErrorIs(t, err, expectedErr)
}

func TestStaticAddressDepositRequiresInteractiveConfirmation(t *testing.T) {
client := &staticAddressSummaryErrorClient{}
cmd := &cli.Command{
Name: "deposit",
Flags: depositStaticAddressCommand.Flags,
Action: func(ctx context.Context, cmd *cli.Command) error {
_, err := executeStaticAddressDeposit(
ctx, cmd, client, strings.NewReader(""),
&bytes.Buffer{}, false,
)

return err
},
}

err := cmd.Run(t.Context(), []string{
"deposit", "--amt", "100000",
})
require.ErrorContains(t, err, "requires an interactive terminal")
require.Zero(t, client.newAddressCalls)
}

func TestStaticAddressDepositForceFirstUseNonInteractive(t *testing.T) {
client := &staticAddressSummaryErrorClient{
err: address.ErrNoStaticAddress,
newAddressResponse: &looprpc.NewStaticAddressResponse{
Address: "bcrt1ptestaddress",
},
}

var (
resp *looprpc.NewStaticAddressResponse
output bytes.Buffer
)
cmd := &cli.Command{
Name: "deposit",
Flags: depositStaticAddressCommand.Flags,
Action: func(ctx context.Context, cmd *cli.Command) error {
var err error
resp, err = executeStaticAddressDeposit(
ctx, cmd, client, strings.NewReader(""), &output,
false,
)

return err
},
}

err := cmd.Run(t.Context(), []string{
"deposit", "--amt", "100000", "--force",
})
require.NoError(t, err)
require.Same(t, client.newAddressResponse, resp)
require.Equal(t, 1, client.newAddressCalls)
require.EqualValues(
t, 100_000, client.newAddressRequest.GetSendCoinsRequest().Amount,
)
require.Empty(t, client.newAddressRequest.GetSendCoinsRequest().Addr)
require.Contains(t, output.String(), "WARNING")
require.NotContains(t, output.String(), "CONTINUE WITH NEW ADDRESS")
}

// TestIsNoStaticAddressSummaryError verifies that the CLI recognizes the
// durable status returned by current loopd versions while keeping only the
// exact legacy Unknown status for compatibility with older versions.
func TestIsNoStaticAddressSummaryError(t *testing.T) {
t.Parallel()

tests := []struct {
name string
err error
expected bool
}{
{
name: "not found",
err: status.Error(codes.NotFound, "not initialized"),
expected: true,
},
{
name: "legacy unknown",
err: status.Error(
codes.Unknown, address.ErrNoStaticAddress.Error(),
),
expected: true,
},
{
name: "wrapped legacy message",
err: status.Error(
codes.Unknown, "lookup failed: "+
address.ErrNoStaticAddress.Error(),
),
},
{
name: "wrong status",
err: status.Error(
codes.Internal, address.ErrNoStaticAddress.Error(),
),
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

require.Equal(
t, test.expected,
isNoStaticAddressSummaryError(test.err),
)
})
}
}

func TestStaticAddressDepositRequestAllowsNoUtxos(t *testing.T) {
t.Parallel()

var req *looprpc.NewStaticAddressRequest
cmd := &cli.Command{
Name: "deposit",
Flags: depositStaticAddressCommand.Flags,
Action: func(_ context.Context, cmd *cli.Command) error {
var err error
req, err = staticAddressDepositRequest(
cmd, "bcrt1ptestaddress",
)

return err
},
}

err := cmd.Run(context.Background(), []string{
"deposit", "--amt", "1000000",
})
require.NoError(t, err)
require.Equal(t, "bcrt1ptestaddress", req.GetSendCoinsRequest().Addr)
require.EqualValues(t, 1_000_000, req.GetSendCoinsRequest().Amount)
require.Empty(t, req.GetSendCoinsRequest().Outpoints)
}

func TestStaticAddressDepositForceAlias(t *testing.T) {
var forceFlag *cli.BoolFlag
for _, flag := range depositStaticAddressCommand.Flags {
boolFlag, ok := flag.(*cli.BoolFlag)
if ok && boolFlag.Name == "force" {
forceFlag = boolFlag
break
}
}
require.NotNil(t, forceFlag)

for _, flag := range []string{"--force", "-f"} {
t.Run(flag, func(t *testing.T) {
flagCopy := *forceFlag
var forced bool
cmd := &cli.Command{
Name: "deposit",
Flags: []cli.Flag{&flagCopy},
Action: func(_ context.Context,
cmd *cli.Command) error {

forced = cmd.Bool("force")
return nil
},
}

err := cmd.Run(t.Context(), []string{"deposit", flag})
require.NoError(t, err)
require.True(t, forced)
})
}
}

func TestConfirmStaticAddressDeposit(t *testing.T) {
t.Parallel()

req := &looprpc.NewStaticAddressRequest{
SendCoinsRequest: &lnrpc.SendCoinsRequest{Amount: 10_000},
}

var output bytes.Buffer
confirmed, err := confirmStaticAddressDeposit(
req, strings.NewReader("yes\n"), &output,
)
require.NoError(t, err)
require.True(t, confirmed)
require.Contains(t, output.String(), "Amount: 10000")
require.Contains(t, output.String(), "newly derived static address")
}

// TestLowConfDepositWarningConfirmedOnly verifies confirmed deposits below the
// conservative warning threshold are included in the warning text.
func TestLowConfDepositWarningConfirmedOnly(t *testing.T) {
Expand Down Expand Up @@ -196,6 +432,9 @@ func TestWarningDepositSelectionMatchesLoopInSelection(t *testing.T) {
OutPoint: outpoint,
Value: btcutil.Amount(fixture.value),
ConfirmationHeight: fixture.confirmationHeight,
AddressParams: &address.Parameters{
Expiry: csvExpiry,
},
})
}

Expand All @@ -204,8 +443,7 @@ func TestWarningDepositSelectionMatchesLoopInSelection(t *testing.T) {
)

loopInSelected, err := loopin.SelectDeposits(
btcutil.Amount(targetAmount), loopInDeposits, csvExpiry,
blockHeight,
btcutil.Amount(targetAmount), loopInDeposits, blockHeight,
)
require.NoError(t, err)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
" \"id\": \"bb7f050df0b7c3e1fe61010e10ad45e30ddf7acd301fa6e05a2ddb825b5c2efb\",\n",
" \"outpoint\": \"56cd081a3a6eadf25b7d3fe0b61207389352ed69a622d2ec28c5d669bf6a5313:0\",\n",
" \"state\": \"WITHDRAWING\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
" \"id\": \"68262a104c9ec325de6bec37b8e31bd875bbd2f5f0b9ce2da20cf0bd636fc448\",\n",
" \"outpoint\": \"edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
Expand All @@ -101,6 +102,7 @@
" \"id\": \"86b5e2cdf9694c8e7398e42afde109766d7cd2142203905ba63fbd0eb1370ef3\",\n",
" \"outpoint\": \"bb358e4f73ae97c4e2d99c6d64e852bba7cf56e13105b05d1200b8ae1796665e:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
Expand All @@ -110,6 +112,7 @@
" \"id\": \"6c290f7536ea5097946afffac6a69906a26d775823ebbacedfe6f2d69c0745e4\",\n",
" \"outpoint\": \"5eaa7dd7a291665393eddf5dece91feef901f22665933cce7a0732a9b81c3001:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
Expand All @@ -119,6 +122,7 @@
" \"id\": \"0182b4d895b1c467290ae7b5c6c42ff76b2a4225807a94211c973170d5a883eb\",\n",
" \"outpoint\": \"7e6360d6e6a394cfd096adf0bfe1275c5a83541eb573e90e463a78dc715f8894:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
" \"id\": \"8fbd6da2f945de2905aa7fa93860744d9387d3464484360e96e467a51de3bc9d\",\n",
" \"outpoint\": \"9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0\",\n",
" \"state\": \"LOOPED_IN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"84302337424036419396ab7964dd78b85b1a481a9f1db73db5cddee57c2443e7\",\n",
" \"value\": \"500000\"\n",
" }\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
" \"id\": \"68262a104c9ec325de6bec37b8e31bd875bbd2f5f0b9ce2da20cf0bd636fc448\",\n",
" \"outpoint\": \"edcdab8f0b1138d853a453b8b7a5ac3c694bd53ad38b7ccf062e45f99440e6e6:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
Expand All @@ -119,6 +120,7 @@
" \"id\": \"86b5e2cdf9694c8e7398e42afde109766d7cd2142203905ba63fbd0eb1370ef3\",\n",
" \"outpoint\": \"bb358e4f73ae97c4e2d99c6d64e852bba7cf56e13105b05d1200b8ae1796665e:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
Expand All @@ -128,6 +130,7 @@
" \"id\": \"6c290f7536ea5097946afffac6a69906a26d775823ebbacedfe6f2d69c0745e4\",\n",
" \"outpoint\": \"5eaa7dd7a291665393eddf5dece91feef901f22665933cce7a0732a9b81c3001:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
Expand All @@ -137,6 +140,7 @@
" \"id\": \"0182b4d895b1c467290ae7b5c6c42ff76b2a4225807a94211c973170d5a883eb\",\n",
" \"outpoint\": \"7e6360d6e6a394cfd096adf0bfe1275c5a83541eb573e90e463a78dc715f8894:0\",\n",
" \"state\": \"WITHDRAWN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
Expand All @@ -146,6 +150,7 @@
" \"id\": \"8fbd6da2f945de2905aa7fa93860744d9387d3464484360e96e467a51de3bc9d\",\n",
" \"outpoint\": \"9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0\",\n",
" \"state\": \"LOOPED_IN\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"84302337424036419396ab7964dd78b85b1a481a9f1db73db5cddee57c2443e7\",\n",
" \"value\": \"500000\"\n",
" },\n",
Expand All @@ -155,6 +160,7 @@
" \"id\": \"bb7f050df0b7c3e1fe61010e10ad45e30ddf7acd301fa6e05a2ddb825b5c2efb\",\n",
" \"outpoint\": \"56cd081a3a6eadf25b7d3fe0b61207389352ed69a622d2ec28c5d669bf6a5313:0\",\n",
" \"state\": \"WITHDRAWING\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" }\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
" \"id\": \"7a7cbe9b90f23d47aa92eb10a9d323f7ace6e9eaab5b77379c63422c15da19c8\",\n",
" \"outpoint\": \"0e70673c1da3343648c26f779555346f30d235314838b1160826d0d5c29b4fba:1\",\n",
" \"state\": \"CHANNEL_PUBLISHED\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"500000\"\n",
" },\n",
Expand All @@ -86,6 +87,7 @@
" \"id\": \"ff9a43b2082f906a2e2758934220c4ce32393eb2823b292517ae081e16daded9\",\n",
" \"outpoint\": \"d2d6e50f157f0d31b8688a4af4f064edf3454714e92369b2c8c4d82477edbaca:0\",\n",
" \"state\": \"CHANNEL_PUBLISHED\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"1000000\"\n",
" }\n",
Expand Down
Loading
Loading