Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
41f5dcf
staticaddr/deposit: track unconfirmed deposits
hieblmi Jul 8, 2026
de17e49
staticaddr/deposit: replay startup block after recovery
hieblmi Jul 8, 2026
bce5897
staticaddr/deposit: reconcile active deposits with wallet
hieblmi Jul 8, 2026
8f7e1ed
staticaddr: expose tracked deposit availability
hieblmi Jul 8, 2026
b29e129
staticaddr: require confirmed deposits for withdraws and channel opens
hieblmi Jul 8, 2026
02a4bf7
staticaddr/loopin: treat unconfirmed deposits as swappable
hieblmi Jul 8, 2026
c4aa29a
staticaddr/loopin: account for autoloop deposit expiry
hieblmi Jul 6, 2026
04565b2
staticaddr: refresh deposits before spend selection
hieblmi Jul 8, 2026
f483434
loopd: filter static deposit views through active set
hieblmi Jul 8, 2026
32ac85a
staticaddr/loopin: cancel signing for unavailable deposits
hieblmi Jul 8, 2026
0b8017e
notifications: fan out static loop-in risk decisions
hieblmi Jul 8, 2026
4553de4
staticaddr/loopin: wait for risk decisions
hieblmi Jul 8, 2026
3650ae9
loopdb: persist static loop-in risk decisions
hieblmi Jul 8, 2026
819d3b7
notifications: persist static loop-in risk decisions
hieblmi Jul 8, 2026
f5ffb3e
loopd: wire static risk decision persistence
hieblmi Jul 8, 2026
a18fb9c
staticaddr/loopin: clean up deposits after unpaid htlc timeout
hieblmi Jul 8, 2026
14bb1d2
staticaddr/loopin: recover risk-decision deadlines
hieblmi Jul 8, 2026
ed91300
staticaddr/loopin: add risk decision watcher
hieblmi Jul 9, 2026
4fe2ee2
staticaddr/loopin: use risk decision watcher
hieblmi Jul 9, 2026
6c13d07
cmd/loop: warn for low-confirmation static deposits
hieblmi Jun 26, 2026
5b29bb1
cmd/loop: update static loop-in replay fixtures
hieblmi Jun 26, 2026
3a0e8f8
swap: reserve multi-address key families
hieblmi Jul 10, 2026
4117eaf
loopdb: persist deposit address ownership
hieblmi Jul 10, 2026
543f916
staticaddr/address: activate derived addresses
hieblmi Jul 10, 2026
020a62f
staticaddr/deposit: discover all active addresses
hieblmi Jul 10, 2026
c914ef2
staticaddr/deposit: sweep with owning address keys
hieblmi Jul 10, 2026
a4df9e1
staticaddr: sign with per-deposit address keys
hieblmi Jul 10, 2026
a53b305
swapserverrpc: add multi-address proofs
hieblmi May 6, 2026
f5ddd98
staticaddr/loopin: send per-deposit address proofs
hieblmi Jul 10, 2026
a0f0374
staticaddr/withdraw: send per-deposit address proofs
hieblmi Jul 10, 2026
4c9d6af
staticaddr/deposit: restore owning address parameters
hieblmi May 6, 2026
4235437
staticaddr/loopin: use generated change addresses
hieblmi Jul 10, 2026
07f7f0d
staticaddr/withdraw: use generated change addresses
hieblmi Jul 10, 2026
c3d7635
staticaddr: fund new addresses with sendcoins
hieblmi May 8, 2026
c1c691e
staticaddr: expose addresses in deposit listings
hieblmi May 11, 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
486 changes: 475 additions & 11 deletions cmd/loop/staticaddr.go

Large diffs are not rendered by default.

251 changes: 251 additions & 0 deletions cmd/loop/staticaddr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package main

import (
"context"
"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/stretchr/testify/require"
"github.com/urfave/cli/v3"
)

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

// TestLowConfDepositWarningConfirmedOnly verifies confirmed deposits below the
// conservative warning threshold are included in the warning text.
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")
}

// TestLowConfDepositWarningUnconfirmed verifies unconfirmed deposits get a
// warning that the swap may wait for confirmation-risk acceptance.
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,
"conservative 6-confirmation threshold",
),
)
require.NotContains(t, warning, "executed immediately")
}

// TestWarningDepositOutpointsAutoSelectPrefersConfirmed verifies automatic
// warning selection keeps the loop-in preference for confirmed outputs.
func TestWarningDepositOutpointsAutoSelectPrefersConfirmed(t *testing.T) {
t.Parallel()

const csvExpiry = 1100

deposits := []*looprpc.Deposit{
{
Outpoint: "mempool-large",
Value: 2_000_000,
ConfirmationHeight: 0,
BlocksUntilExpiry: csvExpiry,
},
{
Outpoint: "confirmed",
Value: 1_500_000,
ConfirmationHeight: 100,
BlocksUntilExpiry: csvExpiry - 5,
},
}

selected := warningDepositOutpoints(deposits, nil, true, 1_000_000)

require.Equal(t, []string{"confirmed"}, selected)
require.Empty(t, lowConfDepositWarning(deposits, selected, csvExpiry))
}

// TestWarningDepositOutpointsAutoSelectIncludesNeededUnconfirmed verifies the
// warning path includes mempool deposits when they are needed for the target.
func TestWarningDepositOutpointsAutoSelectIncludesNeededUnconfirmed(t *testing.T) {
t.Parallel()

const csvExpiry = 1100

deposits := []*looprpc.Deposit{
{
Outpoint: "confirmed-small",
Value: 500_000,
ConfirmationHeight: 100,
BlocksUntilExpiry: csvExpiry - 5,
},
{
Outpoint: "mempool-large",
Value: 2_000_000,
ConfirmationHeight: 0,
BlocksUntilExpiry: csvExpiry,
},
}

selected := warningDepositOutpoints(deposits, nil, true, 1_000_000)

require.Equal(
t, []string{"confirmed-small", "mempool-large"}, selected,
)

warning := lowConfDepositWarning(deposits, selected, csvExpiry)
require.Contains(t, warning, "mempool-large (unconfirmed)")
require.NotContains(t, warning, "confirmed-small")
}

// TestWarningDepositSelectionMatchesLoopInSelection verifies CLI warning
// selection matches the loop-in selector.
func TestWarningDepositSelectionMatchesLoopInSelection(t *testing.T) {
t.Parallel()

const (
blockHeight = uint32(10_000)
csvExpiry = uint32(1_200)
targetAmount = int64(2_500_000)
)

type fixture struct {
name string
value int64
confirmationHeight int64
}

fixtures := []fixture{
{
name: "mempool-huge",
value: 3_000_000,
confirmationHeight: 0,
},
{
name: "confirmed-later-expiry",
value: 2_000_000,
confirmationHeight: 9_900,
},
{
name: "confirmed-earlier-expiry",
value: 2_000_000,
confirmationHeight: 9_890,
},
{
name: "confirmed-small",
value: 600_000,
confirmationHeight: 9_900,
},
{
name: "confirmed-too-close-to-expiry",
value: 5_000_000,
confirmationHeight: 9_849,
},
}

rpcDeposits := make([]*looprpc.Deposit, 0, len(fixtures))
loopInDeposits := make([]*deposit.Deposit, 0, len(fixtures))
for idx, fixture := range fixtures {
hash := chainhash.Hash{byte(idx + 1)}
outpoint := wire.OutPoint{
Hash: hash,
Index: uint32(idx),
}

blocksUntilExpiry := int64(0)
if fixture.confirmationHeight > 0 {
blocksUntilExpiry = fixture.confirmationHeight +
int64(csvExpiry) - int64(blockHeight)
}

rpcDeposits = append(rpcDeposits, &looprpc.Deposit{
Outpoint: outpoint.String(),
Value: fixture.value,
ConfirmationHeight: fixture.confirmationHeight,
BlocksUntilExpiry: blocksUntilExpiry,
})
loopInDeposits = append(loopInDeposits, &deposit.Deposit{
OutPoint: outpoint,
Value: btcutil.Amount(fixture.value),
ConfirmationHeight: fixture.confirmationHeight,
AddressParams: &address.Parameters{
Expiry: csvExpiry,
},
})
}

cliSelected := autoSelectedWarningOutpoints(
rpcDeposits, targetAmount,
)

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

loopInSelectedOutpoints := make([]string, 0, len(loopInSelected))
for _, selected := range loopInSelected {
loopInSelectedOutpoints = append(
loopInSelectedOutpoints, selected.OutPoint.String(),
)
}

require.Equal(t, loopInSelectedOutpoints, cliSelected)
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"event": "request",
"message_type": "looprpc.NewStaticAddressRequest",
"payload": {
"client_key": ""
"client_key": "",
"send_coins_request": null
}
}
},
Expand All @@ -64,7 +65,8 @@
"lines": [
"{\n",
" \"address\": \"bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw\",\n",
" \"expiry\": 14400\n",
" \"expiry\": 14400,\n",
" \"send_coins_response\": null\n",
"}\n"
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"\n",
"COMMANDS:\n",
" new, n Create a new static loop in address.\n",
" deposit Create and fund a new static loop in address.\n",
" listunspent, l List unspent static address outputs.\n",
" listdeposits Displays static address deposits. A filter can be applied to only show deposits in a specific state.\n",
" listwithdrawals Display a summary of past withdrawals.\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
" \"id\": \"ea6abbf0571c0ba82117ae9f2086614eacea8b2913dc0544b70c00de78353e71\",\n",
" \"outpoint\": \"188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0\",\n",
" \"state\": \"DEPOSITED\",\n",
" \"static_address\": \"\",\n",
" \"swap_hash\": \"\",\n",
" \"value\": \"2500000\"\n",
" }\n",
Expand Down
Loading
Loading