mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
* fix: limit LSP opening fees for JIT channel invoices JIT channel invoices are now created with a maximum LSP opening fee instead of no limit: the fee the LSP advertises in its LSPS2 opening fee menu for the payment size, bounded by an absolute ceiling of 5000 sats or 10% of the payment, whichever is greater. Invoice creation fails if the LSP quotes a fee above this limit. The minimum JIT payment size calculation now uses the same ceiling so the advertised receivable range matches what invoice creation accepts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: validate invoice expiry range and guard LSPS2 cache reads Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
203 lines
6.6 KiB
Go
203 lines
6.6 KiB
Go
package ldk
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/getAlby/ldk-node-go/ldk_node"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/getAlby/hub/tests"
|
|
)
|
|
|
|
func TestGetVssNodeIdentifier(t *testing.T) {
|
|
mnemonic := "thought turkey ask pottery head say catalog desk pledge elbow naive mimic"
|
|
expectedVssNodeIdentifier := "751636"
|
|
|
|
svc, err := tests.CreateTestServiceWithMnemonic(t, mnemonic, "123")
|
|
require.NoError(t, err)
|
|
defer svc.Remove()
|
|
|
|
vssNodeIdentifier, err := GetVssNodeIdentifier(svc.Keys)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, expectedVssNodeIdentifier, vssNodeIdentifier)
|
|
}
|
|
func TestGetVssNodeIdentifier2(t *testing.T) {
|
|
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
|
expectedVssNodeIdentifier := "770256"
|
|
|
|
svc, err := tests.CreateTestServiceWithMnemonic(t, mnemonic, "123")
|
|
require.NoError(t, err)
|
|
defer svc.Remove()
|
|
|
|
vssNodeIdentifier, err := GetVssNodeIdentifier(svc.Keys)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, expectedVssNodeIdentifier, vssNodeIdentifier)
|
|
}
|
|
|
|
func makeLsps2OpeningFeeParams(minFeeMsat uint64, proportional uint32, minPaymentSizeMsat uint64, maxPaymentSizeMsat uint64) ldk_node.Lsps2OpeningFeeParams {
|
|
return ldk_node.Lsps2OpeningFeeParams{
|
|
MinFeeMsat: minFeeMsat,
|
|
Proportional: proportional,
|
|
ValidUntil: "2035-01-01T00:00:00Z",
|
|
MinLifetime: 4032,
|
|
MaxClientToSelfDelay: 2016,
|
|
MinPaymentSizeMsat: minPaymentSizeMsat,
|
|
MaxPaymentSizeMsat: maxPaymentSizeMsat,
|
|
Promise: "promise",
|
|
}
|
|
}
|
|
|
|
func TestComputeLsps2MaxTotalOpeningFeeMsat(t *testing.T) {
|
|
t.Run("proportional fee above minimum fee", func(t *testing.T) {
|
|
menu := []ldk_node.Lsps2OpeningFeeParams{
|
|
// 0.5% of 10M msat = 50k msat > 10k msat minimum
|
|
makeLsps2OpeningFeeParams(10_000, 5_000, 1_000_000, 100_000_000),
|
|
}
|
|
assert.Equal(t, uint64(50_000), computeLsps2MaxTotalOpeningFeeMsat(10_000_000, menu))
|
|
})
|
|
|
|
t.Run("minimum fee above proportional fee", func(t *testing.T) {
|
|
menu := []ldk_node.Lsps2OpeningFeeParams{
|
|
// 0.5% of 1M msat = 5k msat < 10k msat minimum
|
|
makeLsps2OpeningFeeParams(10_000, 5_000, 1_000_000, 100_000_000),
|
|
}
|
|
assert.Equal(t, uint64(10_000), computeLsps2MaxTotalOpeningFeeMsat(1_000_000, menu))
|
|
})
|
|
|
|
t.Run("highest fee across menu entries", func(t *testing.T) {
|
|
menu := []ldk_node.Lsps2OpeningFeeParams{
|
|
makeLsps2OpeningFeeParams(10_000, 5_000, 1_000_000, 100_000_000),
|
|
makeLsps2OpeningFeeParams(10_000, 20_000, 1_000_000, 100_000_000),
|
|
}
|
|
assert.Equal(t, uint64(200_000), computeLsps2MaxTotalOpeningFeeMsat(10_000_000, menu))
|
|
})
|
|
|
|
t.Run("entries not covering the payment size are skipped", func(t *testing.T) {
|
|
menu := []ldk_node.Lsps2OpeningFeeParams{
|
|
makeLsps2OpeningFeeParams(10_000, 5_000, 1_000_000, 100_000_000),
|
|
// covers larger payments only, would otherwise win with 2%
|
|
makeLsps2OpeningFeeParams(10_000, 20_000, 20_000_000, 100_000_000),
|
|
}
|
|
assert.Equal(t, uint64(50_000), computeLsps2MaxTotalOpeningFeeMsat(10_000_000, menu))
|
|
})
|
|
|
|
t.Run("menu fee above ceiling is clamped to ceiling", func(t *testing.T) {
|
|
menu := []ldk_node.Lsps2OpeningFeeParams{
|
|
// 20% of 100M msat = 20M msat, above the 10% / 10M msat ceiling
|
|
makeLsps2OpeningFeeParams(5_000_000, 200_000, 1_000_000, 1_000_000_000),
|
|
}
|
|
assert.Equal(t, uint64(10_000_000), computeLsps2MaxTotalOpeningFeeMsat(100_000_000, menu))
|
|
})
|
|
|
|
t.Run("base ceiling applies when no entry covers the payment size", func(t *testing.T) {
|
|
menu := []ldk_node.Lsps2OpeningFeeParams{
|
|
makeLsps2OpeningFeeParams(10_000, 5_000, 1_000_000, 100_000_000),
|
|
}
|
|
// 10% of 200M msat = 20M msat
|
|
assert.Equal(t, uint64(20_000_000), computeLsps2MaxTotalOpeningFeeMsat(200_000_000, menu))
|
|
})
|
|
|
|
t.Run("base ceiling applies on empty menu", func(t *testing.T) {
|
|
assert.Equal(t, uint64(5_000_000), computeLsps2MaxTotalOpeningFeeMsat(10_000_000, nil))
|
|
})
|
|
}
|
|
|
|
func TestComputeLsps2MinPaymentSizeMsat(t *testing.T) {
|
|
t.Run("minimum fee below ceiling base", func(t *testing.T) {
|
|
// 1000 sat minimum fee: smallest usable payment nets 1 sat above the fee
|
|
params := makeLsps2OpeningFeeParams(1_000_000, 10_000, 1_000, 100_000_000_000)
|
|
minPaymentSizeMsat, ok := computeLsps2MinPaymentSizeMsat(params)
|
|
require.True(t, ok)
|
|
assert.Equal(t, uint64(1_001_000), minPaymentSizeMsat)
|
|
})
|
|
|
|
t.Run("minimum fee above ceiling base", func(t *testing.T) {
|
|
// 8000 sat minimum fee exceeds the 5000 sat ceiling base, so the
|
|
// smallest payment is where the fee equals 10% of the payment
|
|
params := makeLsps2OpeningFeeParams(8_000_000, 10_000, 1_000, 100_000_000_000)
|
|
minPaymentSizeMsat, ok := computeLsps2MinPaymentSizeMsat(params)
|
|
require.True(t, ok)
|
|
assert.Equal(t, uint64(80_000_000), minPaymentSizeMsat)
|
|
})
|
|
|
|
t.Run("proportional fee above ceiling percentage never fits", func(t *testing.T) {
|
|
// 30% proportional fee with a minimum fee above the ceiling base can
|
|
// never satisfy the 10% ceiling
|
|
params := makeLsps2OpeningFeeParams(6_000_000, 300_000, 1_000, 1_000_000_000)
|
|
_, ok := computeLsps2MinPaymentSizeMsat(params)
|
|
assert.False(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestSanitizeChainEndpointForBitcoind(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
endpoint string
|
|
rpcPort string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "adds configured port to host",
|
|
endpoint: "127.0.0.1",
|
|
rpcPort: "8332",
|
|
expected: "127.0.0.1:8332",
|
|
},
|
|
{
|
|
name: "preserves endpoint port",
|
|
endpoint: "127.0.0.1:18443",
|
|
rpcPort: "8332",
|
|
expected: "127.0.0.1:18443",
|
|
},
|
|
{
|
|
name: "formats ipv6 host",
|
|
endpoint: "[2001:db8::1]",
|
|
rpcPort: "8332",
|
|
expected: "[2001:db8::1]:8332",
|
|
},
|
|
{
|
|
name: "strips credentials from url-shaped input",
|
|
endpoint: "user:pass@[2001:db8::1]:18443",
|
|
rpcPort: "8332",
|
|
expected: "[2001:db8::1]:18443",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, sanitizeChainEndpoint(tt.endpoint, tt.rpcPort))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSanitizeChainEndpointForURLs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
endpoint string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "keeps bare electrum endpoint unchanged",
|
|
endpoint: "electrum.example.com:50002",
|
|
expected: "electrum.example.com:50002",
|
|
},
|
|
{
|
|
name: "strips url credentials",
|
|
endpoint: "ssl://user:pass@electrum.example.com:50002",
|
|
expected: "ssl://electrum.example.com:50002",
|
|
},
|
|
{
|
|
name: "strips esplora credentials",
|
|
endpoint: "https://user:pass@esplora.example.com/api",
|
|
expected: "https://esplora.example.com/api",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, sanitizeChainEndpoint(tt.endpoint, ""))
|
|
})
|
|
}
|
|
}
|