Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
269f45d
swap: reserve multi-address key families
hieblmi Jul 10, 2026
96ae662
loopdb: persist deposit address ownership
hieblmi Jul 10, 2026
3342e86
staticaddr/address: activate derived addresses
hieblmi Jul 10, 2026
d3e80a0
staticaddr/deposit: bind deposits to owning addresses
hieblmi Jul 10, 2026
e9f7720
staticaddr/deposit: detect replaced expiry sweeps
hieblmi Aug 27, 2026
9ebf8b9
staticaddr: create signing sessions from deposit keys
hieblmi Aug 27, 2026
7afd2fe
staticaddr/loopin: send per-deposit address proofs
hieblmi Aug 27, 2026
d02f93e
staticaddr/withdraw: send per-deposit address proofs
hieblmi Jul 10, 2026
f571b16
staticaddr/deposit: restore owning address parameters
hieblmi Aug 27, 2026
2481249
staticaddr/loopin: persist change addresses
hieblmi Aug 28, 2026
bdca3c2
staticaddr/loopin: use generated change addresses
hieblmi Aug 28, 2026
c6260a0
staticaddr/loopin: drop legacy address state
hieblmi Aug 26, 2026
06ad25f
staticaddr/withdraw: use generated change addresses
hieblmi Aug 27, 2026
396e86f
staticaddr: fund new addresses with sendcoins
hieblmi Aug 27, 2026
aeefe2b
staticaddr: expose addresses in deposit listings
hieblmi Aug 27, 2026
d351341
staticaddr: classify missing address RPC errors
hieblmi Aug 26, 2026
7d6b196
looprpc: deprecate singular static summary address
hieblmi Aug 26, 2026
72e513d
staticaddr/deposit: bind expiry confirmation to outpoint
hieblmi Aug 26, 2026
567aa8a
staticaddr: add multi-address integration coverage
hieblmi Aug 27, 2026
1179815
docs: document multi-address static deposits
hieblmi Aug 27, 2026
23c6330
staticaddr/withdraw: validate confirmed replacement spends
hieblmi Aug 27, 2026
eba1e93
staticaddr/withdraw: identify change outputs by script
hieblmi Aug 25, 2026
d403bd5
staticaddr/withdraw: reconcile partial confirmed spends
hieblmi Aug 25, 2026
b618043
staticaddr/withdraw: join withdrawal monitors
hieblmi Aug 26, 2026
790108b
staticaddr/withdraw: watch the actual replacement output
hieblmi Aug 26, 2026
c197185
staticaddr/withdraw: require confirmed replacement tx
hieblmi Aug 26, 2026
3aea2b1
staticaddr/withdraw: cover multi-address replacements
hieblmi Aug 27, 2026
042bc9b
docs: note withdrawal replacement hardening
hieblmi Aug 27, 2026
8bd46e4
staticaddr/loopin: persist confirmed HTLC outputs
hieblmi Aug 27, 2026
1f738ef
docs: note static loop-in HTLC recovery
hieblmi Aug 28, 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