mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
Verify the invoice returned when creating a swap out before storing and paying it: - the invoice payment hash must match the payment hash of the locally generated preimage - the invoice amount must not exceed the requested amount plus the quoted service and miner fees (with a small rounding tolerance) - the lockup address is checked against the swap tree, matching the checks already performed for swap in and refunds - the invoice is verified again directly before it is paid Also renames AlbySwapServiceFee to AlbySwapServiceFeePercentage for clarity. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
109 lines
3.4 KiB
Go
109 lines
3.4 KiB
Go
package swaps
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func makeTestInvoice(t *testing.T, paymentHash [32]byte, amountMsat uint64) string {
|
|
t.Helper()
|
|
|
|
privKey, err := btcec.NewPrivateKey()
|
|
require.NoError(t, err)
|
|
|
|
invoice, err := zpay32.NewInvoice(
|
|
&chaincfg.MainNetParams,
|
|
paymentHash,
|
|
time.Now(),
|
|
zpay32.Amount(lnwire.MilliSatoshi(amountMsat)),
|
|
zpay32.Description("test swap invoice"),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
encoded, err := invoice.Encode(zpay32.MessageSigner{
|
|
SignCompact: func(msg []byte) ([]byte, error) {
|
|
return ecdsa.SignCompact(privKey, chainhash.HashB(msg), true), nil
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return encoded
|
|
}
|
|
|
|
func makeTestPaymentHash(t *testing.T) ([32]byte, string) {
|
|
t.Helper()
|
|
|
|
preimage := make([]byte, 32)
|
|
_, err := rand.Read(preimage)
|
|
require.NoError(t, err)
|
|
paymentHash := sha256.Sum256(preimage)
|
|
return paymentHash, hex.EncodeToString(paymentHash[:])
|
|
}
|
|
|
|
func TestVerifySwapOutInvoice(t *testing.T) {
|
|
paymentHash, paymentHashHex := makeTestPaymentHash(t)
|
|
|
|
t.Run("accepts invoice with matching payment hash and amount", func(t *testing.T) {
|
|
invoice := makeTestInvoice(t, paymentHash, 100_000_000)
|
|
|
|
sendAmountSat, err := verifySwapOutInvoice(invoice, paymentHashHex, 100_000)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(100_000), sendAmountSat)
|
|
})
|
|
|
|
t.Run("rejects invoice with different payment hash", func(t *testing.T) {
|
|
otherPaymentHash, _ := makeTestPaymentHash(t)
|
|
invoice := makeTestInvoice(t, otherPaymentHash, 100_000_000)
|
|
|
|
_, err := verifySwapOutInvoice(invoice, paymentHashHex, 100_000)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "does not match swap payment hash")
|
|
})
|
|
|
|
t.Run("rejects invoice exceeding maximum amount", func(t *testing.T) {
|
|
invoice := makeTestInvoice(t, paymentHash, 100_001_000)
|
|
|
|
_, err := verifySwapOutInvoice(invoice, paymentHashHex, 100_000)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "exceeds maximum expected amount")
|
|
})
|
|
|
|
t.Run("rejects invoice without an amount", func(t *testing.T) {
|
|
invoice := makeTestInvoice(t, paymentHash, 0)
|
|
|
|
_, err := verifySwapOutInvoice(invoice, paymentHashHex, 100_000)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "does not have an amount")
|
|
})
|
|
|
|
t.Run("rejects unparseable invoice", func(t *testing.T) {
|
|
_, err := verifySwapOutInvoice("lnbc1notaninvoice", paymentHashHex, 100_000)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestCalculateMaxSwapOutSendAmountSat(t *testing.T) {
|
|
// 100_000 requested + 300 claim fee + 500 lockup fee = 100_800,
|
|
// marked up by 0.5% boltz + 1% alby fee on the invoice amount:
|
|
// ceil(100_800 / 0.985) = 102_336, plus 10 sat tolerance
|
|
assert.Equal(t, uint64(102_346), calculateMaxSwapOutSendAmountSat(100_000, 0.5, 500, 300))
|
|
|
|
// with a 0% boltz fee only the alby fee percentage applies:
|
|
// ceil(100_800 / 0.99) = 101_819, plus 10 sat tolerance
|
|
assert.Equal(t, uint64(101_829), calculateMaxSwapOutSendAmountSat(100_000, 0, 500, 300))
|
|
|
|
// invalid fee rates of 100% or more are never accepted
|
|
assert.Equal(t, uint64(0), calculateMaxSwapOutSendAmountSat(100_000, 100, 500, 300))
|
|
}
|