Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0d50b7a
swap: reserve multi-address key families
hieblmi Jul 10, 2026
e5e37b0
loopdb: persist deposit address ownership
hieblmi Jul 10, 2026
98899ff
staticaddr/address: activate derived addresses
hieblmi Jul 10, 2026
68c0824
staticaddr/deposit: bind deposits to owning addresses
hieblmi Jul 10, 2026
57de9df
staticaddr/deposit: detect replaced expiry sweeps
hieblmi Aug 27, 2026
4ef6c00
staticaddr: create signing sessions from deposit keys
hieblmi Aug 27, 2026
9e3db58
staticaddr/loopin: send per-deposit address proofs
hieblmi Aug 27, 2026
24a326d
staticaddr/withdraw: send per-deposit address proofs
hieblmi Jul 10, 2026
d1bc4d2
staticaddr/deposit: restore owning address parameters
hieblmi Aug 27, 2026
d0594e7
staticaddr/loopin: persist change addresses
hieblmi Aug 28, 2026
0199d2b
staticaddr/loopin: use generated change addresses
hieblmi Aug 28, 2026
a3f3e31
staticaddr/loopin: drop legacy address state
hieblmi Aug 26, 2026
30d314a
staticaddr/withdraw: use generated change addresses
hieblmi Aug 27, 2026
812ae2e
staticaddr: fund new addresses with sendcoins
hieblmi Aug 27, 2026
6dcb502
staticaddr: expose addresses in deposit listings
hieblmi Aug 27, 2026
bf5d42f
staticaddr: classify missing address RPC errors
hieblmi Aug 26, 2026
b70bc74
looprpc: deprecate singular static summary address
hieblmi Aug 26, 2026
6d361ec
staticaddr/deposit: bind expiry confirmation to outpoint
hieblmi Aug 26, 2026
97f4ac7
staticaddr: add multi-address integration coverage
hieblmi Aug 27, 2026
3dfba8b
docs: document multi-address static deposits
hieblmi Aug 27, 2026
e220314
staticaddr/withdraw: validate confirmed replacement spends
hieblmi Aug 27, 2026
05dfc97
staticaddr/withdraw: reconcile partial confirmed spends
hieblmi Aug 25, 2026
1d4e07b
staticaddr/withdraw: join withdrawal monitors
hieblmi Aug 26, 2026
251cfa2
staticaddr/withdraw: watch the actual replacement output
hieblmi Aug 26, 2026
26836b8
staticaddr/withdraw: require confirmed replacement tx
hieblmi Aug 26, 2026
7ac0b92
staticaddr/withdraw: cover multi-address replacements
hieblmi Aug 27, 2026
d2f73b7
docs: note withdrawal replacement hardening
hieblmi Aug 27, 2026
e501056
staticaddr/withdraw: persist confirmations before finalizing
hieblmi Aug 31, 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