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
cbc2d24
staticaddr/address: activate derived addresses
hieblmi Jul 10, 2026
63fee86
staticaddr/deposit: bind deposits to owning addresses
hieblmi Jul 10, 2026
d1edf15
staticaddr/deposit: detect replaced expiry sweeps
hieblmi Aug 27, 2026
fd19587
staticaddr: create signing sessions from deposit keys
hieblmi Aug 27, 2026
101548a
staticaddr/loopin: send per-deposit address proofs
hieblmi Aug 27, 2026
8876800
staticaddr/withdraw: send per-deposit address proofs
hieblmi Jul 10, 2026
3a6cd7a
staticaddr/deposit: restore owning address parameters
hieblmi Aug 27, 2026
c46efba
staticaddr/loopin: persist change addresses
hieblmi Aug 28, 2026
4820689
staticaddr/loopin: use generated change addresses
hieblmi Aug 28, 2026
e30f4d2
staticaddr/loopin: drop legacy address state
hieblmi Aug 26, 2026
8fe8ef1
staticaddr/withdraw: use generated change addresses
hieblmi Aug 27, 2026
038349f
staticaddr: fund new addresses with sendcoins
hieblmi Aug 27, 2026
6ce4939
staticaddr: expose addresses in deposit listings
hieblmi Aug 27, 2026
137d3fe
staticaddr: classify missing address RPC errors
hieblmi Aug 26, 2026
7c4b410
looprpc: deprecate singular static summary address
hieblmi Aug 26, 2026
27fabd5
staticaddr/deposit: bind expiry confirmation to outpoint
hieblmi Aug 26, 2026
e1fdb83
staticaddr: add multi-address integration coverage
hieblmi Aug 27, 2026
f8b5012
staticaddr/loopin: require one prevout per sweep input
hieblmi Sep 1, 2026
5791453
docs: document multi-address static deposits
hieblmi Aug 27, 2026
c49a43e
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