Update project structure and build process

This commit is contained in:
Juan Pablo Civile 2025-05-13 11:10:08 -03:00
parent 124e9fa1bc
commit d9f3e925a4
277 changed files with 15321 additions and 930 deletions

View File

@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [macos-12, macos-11]
os: [macos-13, macos-14, macos-15]
arch: [amd64, arm64]
runs-on: ${{ matrix.os }}
@ -19,32 +19,32 @@ jobs:
out: recovery-tool-${{ matrix.os }}-${{ matrix.arch }}
steps:
- name: Checkout
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Create output dir
run: |
mkdir -p bin
- name: Set up Go
uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5
with:
go-version: 1.18.1
go-version: '1.22.6'
- name: Build
run: |
CGO_ENABLED=1 \
GOOS=darwin \
GOARCH=${{ matrix.arch }} \
go build -mod=vendor -a -trimpath -o bin/${{ env.out }}
go build -mod=vendor -a -trimpath -o bin/${{ env.out }} ./recovery_tool
- name: Upload binary
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ${{ env.out }}
path: bin/${{ env.out }}
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
@ -53,9 +53,15 @@ jobs:
- os: "linux"
arch: "386"
out: "recovery-tool-linux32"
cc: "i686-linux-gnu-gcc-12"
- os: "linux"
arch: "amd64"
out: "recovery-tool-linux64"
cc: "x86_64-linux-gnu-gcc-12"
- os: "linux"
arch: "arm64"
out: "recovery-tool-linuxaarch64"
cc: "aarch64-linux-gnu-gcc-12"
- os: "windows"
arch: "386"
cc: "i686-w64-mingw32-gcc"
@ -67,21 +73,21 @@ jobs:
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@dc7b9719a96d48369863986a06765841d7ea23f6
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2
with:
buildkitd-flags: --debug
- name: Checkout
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Create output dir
run: |
mkdir -p bin
- name: Build
uses: docker/build-push-action@c84f38281176d4c9cdb1626ffafcd6b3911b5d94
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1
with:
file: Dockerfile
file: recovery_tool/Dockerfile
context: .
outputs: bin
cache-from: type=gha
@ -93,21 +99,25 @@ jobs:
out=${{ matrix.target.out }}
- name: Upload binary
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ${{ matrix.target.out }}
path: bin/${{ matrix.target.out }}
release:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs: [build-mac, build]
permissions:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
if: startsWith(github.ref, 'refs/tags/')
with:
path: artifacts
pattern: recovery-tool-*
- name: Compute SHA256 checksums
run: |
@ -117,7 +127,7 @@ jobs:
- name: Release
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
if: startsWith(github.ref, 'refs/tags/')
with:
body_path: sha_sum_table

View File

@ -7,7 +7,7 @@ To build the tool locally and run it, you must:
```
git clone https://github.com/muun/recovery
cd recovery
cd recovery/recovery_tool
```
3. Run the tool with:

4
go.work Normal file
View File

@ -0,0 +1,4 @@
go 1.22.1
use ./libwallet
use ./recovery_tool

View File

View File

View File

View File

View File

View File

321
libwallet/address_test.go Executable file
View File

@ -0,0 +1,321 @@
package libwallet
import (
"encoding/hex"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"google.golang.org/protobuf/proto"
)
const (
address = "2NDhvuRPCYXq4fB8SprminieZ2a1i3JFXyS"
amountURI = address + "?amount=1.2"
completeURI = amountURI + "&label=hola&message=mensaje%20con%20espacios"
uriWithSlashes = "bitcoin://" + amountURI
invalidAddress = "2NDhvuRPCYXq4fB8SprminieZ2a1i3JFXya"
randomText = "fooo"
bip70URL = "https://bitpay.com/i/KXCEAtJQssR9vG2BxdjFwx"
bip70NonRetroCompatAddress = bitcoinScheme + "?r=" + bip70URL
bip70RetroCompatAddress = bitcoinScheme + address + "?r=" + bip70URL
)
func TestGetPaymentURI(t *testing.T) {
const (
invoice = "lnbcrt1pwtpd4xpp55meuklpslk5jtxytyh7u2q490c2xhm68dm3a94486zntsg7ad4vsdqqcqzys763w70h39ze44ngzhdt2mag84wlkefqkphuy7ssg4la5gt9vcpmqts00fnapf8frs928mc5ujfutzyu8apkezhrfvydx82l40w0fckqqmerzjc"
invoiceHashHex = "a6f3cb7c30fda925988b25fdc502a57e146bef476ee3d2d6a7d0a6b823dd6d59"
invoiceDestinationHex = "028cfad4e092191a41f081bedfbe5a6e8f441603c78bf9001b8fb62ac0858f20edasd"
)
invoiceDestination, _ := hex.DecodeString(invoiceDestinationHex)
invoicePaymentHash := make([]byte, 32)
hex.Decode(invoicePaymentHash[:], []byte(invoiceHashHex))
type args struct {
address string
network Network
}
tests := []struct {
name string
args args
want *MuunPaymentURI
wantErr bool
}{
{
name: "validAddress",
args: args{
address: address,
network: *Regtest(),
},
want: &MuunPaymentURI{
Address: address,
Uri: bitcoinScheme + address,
},
},
{
name: "amountValidAddress",
args: args{
address: amountURI,
network: *Regtest(),
},
want: &MuunPaymentURI{
Address: address,
Amount: "1.2",
Uri: bitcoinScheme + amountURI,
},
},
{
name: "completeValidAddress",
args: args{
address: completeURI,
network: *Regtest(),
},
want: &MuunPaymentURI{
Address: address,
Amount: "1.2",
Label: "hola",
Message: "mensaje con espacios",
Uri: bitcoinScheme + completeURI,
},
},
{
name: "invalidAddress",
args: args{
address: invalidAddress,
network: *Regtest(),
},
wantErr: true,
},
{
name: "randomText",
args: args{
address: randomText,
network: *Regtest(),
},
wantErr: true,
},
{
name: "BIP70NonRetroCompatAddress",
args: args{
address: bip70NonRetroCompatAddress,
network: *Regtest(),
},
want: &MuunPaymentURI{
Uri: bip70NonRetroCompatAddress,
Bip70Url: bip70URL,
},
},
{
name: "BIP70RetroCompatAddress",
args: args{
address: bip70RetroCompatAddress,
network: *Regtest(),
},
want: &MuunPaymentURI{
Address: address,
Uri: bip70RetroCompatAddress,
Bip70Url: bip70URL,
},
},
{
name: "URL like address",
args: args{
address: uriWithSlashes,
network: *Regtest(),
},
want: &MuunPaymentURI{
Address: address,
Uri: uriWithSlashes,
Amount: "1.2",
},
},
{
name: "bad url",
args: args{
address: ":foo#%--",
network: *Regtest(),
},
wantErr: true,
},
{
name: "bad query",
args: args{
address: "bitcoin:123123?%&-=asd",
network: *Regtest(),
},
wantErr: true,
},
{
name: "network mismatch",
args: args{
address: amountURI,
network: *Mainnet(),
},
wantErr: true,
},
{
name: "BIP with lightning",
args: args{
address: "bitcoin:123123?lightning=" + invoice,
network: *network,
},
want: &MuunPaymentURI{Invoice: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
}},
},
{
name: "ALL CAPS",
args: args{
address: "BITCOIN:BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2",
network: *Mainnet(),
},
want: &MuunPaymentURI{
Address: strings.ToLower("BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2"),
Uri: "BITCOIN:BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2",
},
},
{
name: "MiXeD Case",
args: args{
address: "BiTcOiN:BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2",
network: *Mainnet(),
},
want: &MuunPaymentURI{
Address: strings.ToLower("BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2"),
Uri: "BiTcOiN:BC1QSQP0D3TY8AAA8N9J8R0D2PF3G40VN4AS9TPWY3J9R3GK5K64VX6QWPAXH2",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetPaymentURI(tt.args.address, &tt.args.network)
if (err != nil) != tt.wantErr {
t.Errorf("GetPaymentURI() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil && got.Invoice != nil {
// expiry is relative to now, so ignore it
got.Invoice.Expiry = 0
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetPaymentURI() = %+v, want %+v", got, tt.want)
}
})
}
}
func Test_normalizeAddress(t *testing.T) {
type args struct {
rawAddress string
targetScheme string
}
tests := []struct {
name string
args args
want string
}{
{
name: "normalAddress",
args: args{
rawAddress: address,
targetScheme: bitcoinScheme,
},
want: bitcoinScheme + address,
},
{
name: "bitcoinAddress",
args: args{
rawAddress: bitcoinScheme + address,
targetScheme: bitcoinScheme,
},
want: bitcoinScheme + address,
},
{
name: "muunAddress",
args: args{
rawAddress: muunScheme + address,
targetScheme: bitcoinScheme,
},
want: bitcoinScheme + address,
},
{
name: "muun to lightning",
args: args{
rawAddress: muunScheme + address,
targetScheme: lightningScheme,
},
want: lightningScheme + address,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, _ := buildUriFromString(tt.args.rawAddress, tt.args.targetScheme); got != tt.want {
t.Errorf("buildUriFromString() = %v, want %v", got, tt.want)
}
})
}
}
func TestDoPaymentRequestCall(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/payment-request/", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Accept") != "application/bitcoin-paymentrequest" {
t.Fatal("expected Accept header to be application/bitcoin-paymentrequest")
}
script, _ := hex.DecodeString("76a9146efcf883b4b6f9997be9a0600f6c095fe2bd2d9288ac")
serializedPaymentDetails, _ := proto.Marshal(&PaymentDetails{
Network: "test",
Outputs: []*Output{
{
Script: script,
Amount: 2500,
},
},
Time: 100000,
Expires: 102000,
Memo: "Hello World",
PaymentUrl: "http://localhost:8000/pay",
MerchantData: []byte(""),
})
payReq, _ := proto.Marshal(&PaymentRequest{SerializedPaymentDetails: serializedPaymentDetails})
w.Write(payReq)
})
server := httptest.NewServer(mux)
defer server.Close()
url := server.URL + "/payment-request/"
paymentURI, err := DoPaymentRequestCall(url, Testnet())
if err != nil {
t.Fatal(err)
}
expected := &MuunPaymentURI{
Address: "mqdofsXHpePPGBFXuwwypAqCcXi48Xhb2f",
Message: "Hello World",
Amount: "0.000025",
Bip70Url: url,
CreationTime: "100000",
ExpiresTime: "102000",
}
if !reflect.DeepEqual(paymentURI, expected) {
t.Fatalf("decoded URI struct does not match expected, %+v != %+v", paymentURI, expected)
}
}

View File

@ -0,0 +1,30 @@
package addresses
import (
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/muun/libwallet/hdpath"
)
func parseKey(s string) *hdkeychain.ExtendedKey {
key, err := hdkeychain.NewKeyFromString(s)
if err != nil {
panic(err)
}
return key
}
func derive(key *hdkeychain.ExtendedKey, fromPath, toPath string) *hdkeychain.ExtendedKey {
indexes := hdpath.MustParse(toPath).IndexesFrom(hdpath.MustParse(fromPath))
for _, index := range indexes {
var err error
var modifier uint32
if index.Hardened {
modifier = hdkeychain.HardenedKeyStart
}
key, err = key.Child(index.Index | modifier)
if err != nil {
panic(err)
}
}
return key
}

57
libwallet/addresses/v2_test.go Executable file
View File

@ -0,0 +1,57 @@
package addresses
import (
"reflect"
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
)
var network = &chaincfg.RegressionNetParams
func TestCreateAddressV2(t *testing.T) {
const (
addressPath = "m/schema:1'/recovery:1'/external:1/0"
originAddress = "2NDeWrsJEwvxwVnvtWzPjhDC5B2LYkFuX2s"
encodedMuunKey = "tpubDBYMnFoxYLdMBZThTk4uARTe4kGPeEYWdKcaEzaUxt1cesetnxtTqmAxVkzDRou51emWytommyLWcF91SdF5KecA6Ja8oHK1FF7d5U2hMxX"
encodedUserKey = "tprv8dfM4H5fYJirMai5Er3LguicgUAyxmcSQbFub5ens16amX1e1HAFiW4SXnFVw9nu9FedFQqTPGTTjPEmgfvvXMKww3UcRpFbbC4DFjbCcTb"
basePath = "m/schema:1'/recovery:1'"
v2EncodedScript = "5221029fa5af7a34c142c1ce348b360abeb7de01df25b1d50129e58a67a6b846c9303b21025714f6b3670d4a38f5e2d6e8f239c9fc072543ce33dca54fcb4f4886a5cb87a652ae"
)
baseMuunKey := parseKey(encodedMuunKey)
muunKey := derive(baseMuunKey, basePath, addressPath)
baseUserKey := parseKey(encodedUserKey)
userKey := derive(baseUserKey, basePath, addressPath)
type args struct {
userKey *hdkeychain.ExtendedKey
muunKey *hdkeychain.ExtendedKey
}
tests := []struct {
name string
args args
want *WalletAddress
wantErr bool
}{
{name: "gen address",
args: args{userKey: userKey, muunKey: muunKey},
want: &WalletAddress{address: originAddress, derivationPath: addressPath, version: V2}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CreateAddressV2(tt.args.userKey, tt.args.muunKey, addressPath, network)
if (err != nil) != tt.wantErr {
t.Errorf("CreateAddressV2() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("CreateAddressV2() = %v, want %v", got, tt.want)
}
})
}
}

54
libwallet/addresses/v3_test.go Executable file
View File

@ -0,0 +1,54 @@
package addresses
import (
"reflect"
"testing"
"github.com/btcsuite/btcutil/hdkeychain"
)
func TestCreateAddressV3(t *testing.T) {
const (
addressPath = "m/schema:1'/recovery:1'/external:1/0"
v3Address = "2MswEXmCLaHQq6pUTtnUVF8wVArfYSqUec5"
basePK = "tpubDAN21T1DFREQQS4FvpUktKRBzXXsj5ddenAa5u198hLXvErFFR4Lj8bt8xMG3xnZr6u8mx1vrFW9RwCDXQwQuYRCLq1j9Nr2VJUrENzteQH"
baseCosigningPK = "tpubDAsVhzq6otpasovieofhiaY38bSFGyJaBGvrJjBv9whhSnftUXfMTMVrq4BbTXT5A9b78CqqbPuM2j1ZGWdiggd7JHUTZAHh8GXDTt4Pkj9"
basePath = "m/schema:1'/recovery:1'"
v3EncodedScript = "0020e1fbfbd395aff8b4087fee3e4488815ef659b559b3cd0d6800b5a591efd99f38"
)
baseMuunKey := parseKey(baseCosigningPK)
muunKey := derive(baseMuunKey, basePath, addressPath)
baseUserKey := parseKey(basePK)
userKey := derive(baseUserKey, basePath, addressPath)
type args struct {
userKey *hdkeychain.ExtendedKey
muunKey *hdkeychain.ExtendedKey
}
tests := []struct {
name string
args args
want *WalletAddress
wantErr bool
}{
{name: "gen address",
args: args{userKey: userKey, muunKey: muunKey},
want: &WalletAddress{address: v3Address, derivationPath: addressPath, version: V3}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CreateAddressV3(tt.args.userKey, tt.args.muunKey, addressPath, network)
if (err != nil) != tt.wantErr {
t.Errorf("CreateAddressV3() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("CreateAddressV3() = %v, want %v", got, tt.want)
}
})
}
}

52
libwallet/addresses/v4_test.go Executable file
View File

@ -0,0 +1,52 @@
package addresses
import (
"reflect"
"testing"
"github.com/btcsuite/btcutil/hdkeychain"
)
func TestCreateAddressV4(t *testing.T) {
const (
addressPath = "m/schema:1'/recovery:1'/external:1/2"
v4Address = "bcrt1qrs3vk4dzv70syck2qdz3g06tgckq4pftenuk5p77st9glnskpvtqe2tvvk"
basePK = "tpubDBf5wCeqg3KrLJiXaveDzD5JtFJ1ss9NVvFMx4RYS73SjwPEEawcAQ7V1B5DGM4gunWDeYNrnkc49sUaf7mS1wUKiJJQD6WEctExUQoLvrg"
baseCosigningPK = "tpubDB22PFkUaHoB7sgxh7exCivV5rAevVSzbB8WkFCCdbHq39r8xnYexiot4NGbi8PM6E1ySVeaHsoDeMYb6EMndpFrzVmuX8iQNExzwNpU61B"
basePath = "m/schema:1'/recovery:1'"
)
baseMuunKey := parseKey(baseCosigningPK)
muunKey := derive(baseMuunKey, basePath, addressPath)
baseUserKey := parseKey(basePK)
userKey := derive(baseUserKey, basePath, addressPath)
type args struct {
userKey *hdkeychain.ExtendedKey
muunKey *hdkeychain.ExtendedKey
}
tests := []struct {
name string
args args
want *WalletAddress
wantErr bool
}{
{name: "gen bech32 address",
args: args{userKey: userKey, muunKey: muunKey},
want: &WalletAddress{address: v4Address, derivationPath: addressPath, version: V4}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CreateAddressV4(tt.args.userKey, tt.args.muunKey, addressPath, network)
if (err != nil) != tt.wantErr {
t.Errorf("CreateAddressV4() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("CreateAddressV4() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,34 @@
package addresses
import (
"reflect"
"testing"
)
func TestCreateAddressV5(t *testing.T) {
const (
addressPath = "m/schema:1'/recovery:1'/external:1/17"
v5Address = "bcrt1pvqngr85tm8hmsv2hjyrejlpsy7u65f7vke8mmrxnyuj3aj3xsapqvh8yrf"
basePK = "tpubDBf5wCeqg3KrLJiXaveDzD5JtFJ1ss9NVvFMx4RYS73SjwPEEawcAQ7V1B5DGM4gunWDeYNrnkc49sUaf7mS1wUKiJJQD6WEctExUQoLvrg"
baseCosigningPK = "tpubDB22PFkUaHoB7sgxh7exCivV5rAevVSzbB8WkFCCdbHq39r8xnYexiot4NGbi8PM6E1ySVeaHsoDeMYb6EMndpFrzVmuX8iQNExzwNpU61B"
basePath = "m/schema:1'/recovery:1'"
)
baseMuunKey := parseKey(baseCosigningPK)
muunKey := derive(baseMuunKey, basePath, addressPath)
baseUserKey := parseKey(basePK)
userKey := derive(baseUserKey, basePath, addressPath)
expectedAddr := &WalletAddress{address: v5Address, derivationPath: addressPath, version: V5}
actualAddr, err := CreateAddressV5(userKey, muunKey, addressPath, network)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(actualAddr, expectedAddr) {
t.Errorf("Created v5 address %v, expected %v", actualAddr, expectedAddr)
}
}

View File

@ -0,0 +1,36 @@
package aescbc
import (
"bytes"
"crypto/rand"
"testing"
)
func TestEncryptionWithPkcs7Padding(t *testing.T) {
key := randomBytes(32)
iv := randomBytes(16)
plaintext := []byte("foobar")
ciphertext, err := EncryptPkcs7(key, iv, plaintext)
if err != nil {
t.Fatal(err)
}
decrypted, err := DecryptPkcs7(key, iv, ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(decrypted, plaintext) {
t.Fatalf("expected decrypted text to match plaintext")
}
}
func randomBytes(count int) []byte {
buf := make([]byte, count)
_, err := rand.Read(buf)
if err != nil {
panic("couldn't read random bytes")
}
return buf
}

View File

View File

@ -0,0 +1,66 @@
package bech32m
import (
"testing"
)
// The following test vectors were taken from BIP-350.
// We only test for valid/invalid (and not decoded data), since only the checksum changed.
var validBech32m = []string{
"A1LQFN3A",
"a1lqfn3a",
"an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6",
"abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx",
"11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8",
"split1checkupstagehandshakeupstreamerranterredcaperredlc445v",
"?1v759aa",
}
var invalidBech32m = []string{
"\x201xj0phk", // HRP character out of range
"\x7f1g6xzxy", // HRP character out of range
"\x801vctc34", // HRP character out of range
"an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4", // Overall max length exceeded
"qyrz8wqd2c9m", // No separator
"1qyrz8wqd2c9m", // Empty HRP
"16plkw9", // Empty HRP
"1p2gdwpf", // Empty HRP
"y1b0jsk6g", // Invalid data character
"lt1igcx5c0", // Invalid data character
"in1muywd", // Too short checksum
"mm1crxm3i", // Invalid character in checksum
"au1s5cgom", // Invalid character in checksum
"M1VUXWEZ", // checksum calculated with uppercase form of HRP
}
func TestDecodeValid(t *testing.T) {
for _, validBech := range validBech32m {
_, _, err := Decode(validBech)
if err != nil {
t.Fatalf("failed to decode valid bech32m %s: %v", validBech, err)
}
}
}
func TestDecodeInvalid(t *testing.T) {
for _, invalidBech := range invalidBech32m {
_, _, err := Decode(invalidBech)
if err == nil {
t.Fatalf("success decoding invalid string %s", invalidBech)
}
}
}
func TestNotCompat(t *testing.T) {
someBech32 := "bcrt1q77ayq0ldrwr3vg0rl0ss8u0ne0hajllz4h7yrqm8ldyy2v0860vs9xzmr4"
_, _, err := Decode(someBech32)
if err == nil {
t.Fatalf("success decoding bech32 with bech32m %s (expected checksum failure)", someBech32)
}
}

View File

@ -0,0 +1,139 @@
package txscriptw
import (
"bytes"
"encoding/binary"
"encoding/hex"
"testing"
_ "unsafe"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
)
// These test cases were taken from rust-bitcoin, which in turn took them from Bitcoin Core:
var sigHashTestCases = []sigHashTestCase{
{
tx: "020000000164eb050a5e3da0c2a65e4786f26d753b7bc69691fabccafb11f7acef36641f1846010000003101b2b404392a22000000000017a9147f2bde86fe78bf68a0544a4f290e12f0b7e0a08c87580200000000000017a91425d11723074ecfb96a0a83c3956bfaf362ae0c908758020000000000001600147e20f938993641de67bb0cdd71682aa34c4d29ad5802000000000000160014c64984dc8761acfa99418bd6bedc79b9287d652d72000000",
prevOuts: "01365724000000000023542156b39dab4f8f3508e0432cfb41fab110170acaa2d4c42539cb90a4dc7c093bc500",
index: 0,
hashType: txscript.SigHashOld,
// expectSigHash: "33ca0ebfb4a945eeee9569fc0f5040221275f88690b7f8592ada88ce3bdf6703",
expectError: true,
},
{
tx: "0200000002fff49be59befe7566050737910f6ccdc5e749c7f8860ddc140386463d88c5ad0f3000000002cf68eb4a3d67f9d4c079249f7e4f27b8854815cb1ed13842d4fbf395f9e217fd605ee24090100000065235d9203f458520000000000160014b6d48333bb13b4c644e57c43a9a26df3a44b785e58020000000000001976a914eea9461a9e1e3f765d3af3e726162e0229fe3eb688ac58020000000000001976a9143a8869c9f2b5ea1d4ff3aeeb6a8fb2fffb1ad5fe88ac0ad7125c",
prevOuts: "02591f220000000000225120f25ad35583ea31998d968871d7de1abd2a52f6fe4178b54ea158274806ff4ece48fb310000000000225120f25ad35583ea31998d968871d7de1abd2a52f6fe4178b54ea158274806ff4ece",
index: 1,
hashType: txscript.SigHashAll,
expectSigHash: "626ab955d58c9a8a600a0c580549d06dc7da4e802eb2a531f62a588e430967a8",
expectError: false,
},
{
tx: "0200000001350005f65aa830ced2079df348e2d8c2bdb4f10e2dde6a161d8a07b40d1ad87dae000000001611d0d603d9dc0e000000000017a914459b6d7d6bbb4d8837b4bf7e9a4556f952da2f5c8758020000000000001976a9141dd70e1299ffc2d5b51f6f87de9dfe9398c33cbb88ac58020000000000001976a9141dd70e1299ffc2d5b51f6f87de9dfe9398c33cbb88aca71c1f4f",
prevOuts: "01c4811000000000002251201bf9297d0a2968ae6693aadd0fa514717afefd218087a239afb7418e2d22e65c",
index: 0,
hashType: txscript.SigHashAll | txscript.SigHashAnyOneCanPay,
// expectSigHash: "dfa9437f9c9a1d1f9af271f79f2f5482f287cdb0d2e03fa92c8a9b216cc6061c",
expectError: true,
},
{
tx: "020000000185bed1a6da2bffbd60ec681a1bfb71c5111d6395b99b3f8b2bf90167111bcb18f5010000007c83ace802ded24a00000000001600142c4698f9f7a773866879755aa78c516fb332af8e5802000000000000160014d38639dfbac4259323b98a472405db0c461b31fa61073747",
prevOuts: "0144c84d0000000000225120e3f2107989c88e67296ab2faca930efa2e3a5bd3ff0904835a11c9e807458621",
index: 0,
hashType: txscript.SigHashNone,
// expectSigHash: "3129de36a5d05fff97ffca31eb75fcccbbbc27b3147a7a36a9e4b45d8b625067",
expectError: true,
},
{
tx: "eb93dbb901028c8515589dac980b6e7f8e4088b77ed866ca0d6d210a7218b6fd0f6b22dd6d7300000000eb4740a9047efc0e0000000000160014913da2128d8fcf292b3691db0e187414aa1783825802000000000000160014913da2128d8fcf292b3691db0e187414aa178382580200000000000017a9143dd27f01c6f7ef9bb9159937b17f17065ed01a0c875802000000000000160014d7630e19df70ada9905ede1722b800c0005f246641000000",
prevOuts: "013fed110000000000225120eb536ae8c33580290630fc495046e998086a64f8f33b93b07967d9029b265c55",
index: 0,
hashType: txscript.SigHashNone | txscript.SigHashAnyOneCanPay,
// expectSigHash: "2441e8b0e063a2083ee790f14f2045022f07258ddde5ee01de543c9e789d80ae",
expectError: true,
},
{
tx: "02000000017836b409a5fed32211407e44b971591f2032053f14701fb5b3a30c0ff382f2cc9c0100000061ac55f60288fb5600000000001976a9144ea02f6f182b082fb6ce47e36bbde390b6a41b5088ac58020000000000001976a9144ea02f6f182b082fb6ce47e36bbde390b6a41b5088ace4000000",
prevOuts: "01efa558000000000022512007071ea3dc7e331b0687d0193d1e6d6ed10e645ef36f10ef8831d5e522ac9e80",
index: 0,
hashType: txscript.SigHashSingle,
// expectSigHash: "30239345177cadd0e3ea413d49803580abb6cb27971b481b7788a78d35117a88",
expectError: true,
},
{
tx: "0100000001aa6deae89d5e0aaca58714fc76ef6f3c8284224888089232d4e663843ed3ab3eae010000008b6657a60450cb4c0000000000160014a3d42b5413ef0c0701c4702f3cd7d4df222c147058020000000000001976a91430b4ed8723a4ee8992aa2c8814cfe5c3ad0ab9d988ac5802000000000000160014365b1166a6ed0a5e8e9dff17a6d00bbb43454bc758020000000000001976a914bc98c51a84fe7fad5dc380eb8b39586eff47241688ac4f313247",
prevOuts: "0107af4e00000000002251202c36d243dfc06cb56a248e62df27ecba7417307511a81ae61aa41c597a929c69",
index: 0,
hashType: txscript.SigHashSingle | txscript.SigHashAnyOneCanPay,
// expectSigHash: "bf9c83f26c6dd16449e4921f813f551c4218e86f2ec906ca8611175b41b566df",
expectError: true,
},
}
func TestTaprootSigHash(t *testing.T) {
for i, testCase := range sigHashTestCases {
tx := testCase.ParseTx()
prevOuts := testCase.ParsePrevOuts()
sigHashes := NewTaprootSigHashes(tx, prevOuts)
sigHash, err := CalcTaprootSigHash(tx, sigHashes, testCase.index, testCase.hashType)
if (err != nil) != testCase.expectError {
t.Fatalf("case %d: expect error %v, actual error: %v", i, testCase.expectError, err)
}
if !bytes.Equal(sigHash, testCase.ParseExpectedSigHash()) {
t.Fatalf("case %d: sigHash does not match expected value", i)
}
}
}
type sigHashTestCase struct {
tx string
prevOuts string
index int
hashType txscript.SigHashType
expectSigHash string
expectError bool
}
func (c *sigHashTestCase) ParseTx() *wire.MsgTx {
b, _ := hex.DecodeString(c.tx)
r := bytes.NewReader(b)
tx := wire.NewMsgTx(0)
tx.BtcDecode(r, 0, wire.WitnessEncoding)
return tx
}
func (c *sigHashTestCase) ParsePrevOuts() []*wire.TxOut {
b, _ := hex.DecodeString(c.prevOuts)
r := bytes.NewReader(b)
prevOutCount, _ := wire.ReadVarInt(r, 0)
prevOuts := make([]*wire.TxOut, prevOutCount)
for i := 0; i < int(prevOutCount); i++ {
valueLe := make([]byte, 8)
r.Read(valueLe[:])
value := binary.LittleEndian.Uint64(valueLe)
pkScriptSize, _ := wire.ReadVarInt(r, 0)
pkScript := make([]byte, pkScriptSize)
r.Read(pkScript)
prevOuts[i] = &wire.TxOut{
Value: int64(value),
PkScript: pkScript,
}
}
return prevOuts
}
func (c *sigHashTestCase) ParseExpectedSigHash() []byte {
b, _ := hex.DecodeString(c.expectSigHash)
return b
}

160
libwallet/challenge_keys_test.go Executable file
View File

@ -0,0 +1,160 @@
package libwallet
import (
"reflect"
"testing"
"github.com/btcsuite/btcutil/base58"
)
func TestNewChallengePrivateKey(t *testing.T) {
type args struct {
input []byte
salt []byte
}
tests := []struct {
name string
args args
want *ChallengePrivateKey
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewChallengePrivateKey(tt.args.input, tt.args.salt); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewChallengePrivateKey() = %v, want %v", got, tt.want)
}
})
}
}
func TestChallengeKeySignSha(t *testing.T) {
input := randomBytes(32)
salt := randomBytes(32)
challengePrivKey := NewChallengePrivateKey(input, salt)
payload := []byte("foobar")
_, err := challengePrivKey.SignSha(payload)
if err != nil {
t.Fatal(err)
}
// TODO(federicobond): assert that signature verifies
}
func TestChallengeKeyCrypto(t *testing.T) {
const birthday = 376
network := Regtest()
salt := randomBytes(8)
privKey, _ := NewHDPrivateKey(randomBytes(32), network)
challengePrivKey := NewChallengePrivateKey([]byte("a very good password"), salt)
encryptedKey, err := challengePrivKey.PubKey().EncryptKey(privKey, salt, birthday)
if err != nil {
t.Fatal(err)
}
decryptedKey, err := challengePrivKey.DecryptRawKey(encryptedKey, network)
if err != nil {
t.Fatal(err)
}
if privKey.String() != decryptedKey.Key.String() {
t.Fatalf("keys dont match: orig %v vs decrypted %v", privKey.String(), decryptedKey.Key.String())
}
if birthday != decryptedKey.Birthday {
t.Fatalf("birthdays dont match: expected %v got %v", birthday, decryptedKey.Birthday)
}
}
func TestChallengeKeyCryptoV2(t *testing.T) {
const (
encodedKey = "tprv8ZgxMBicQKsPcxg1GFGZgL5zALjPwijrYNUqTi2s9JsVqDLzbpX55U9JH2PKAQKExtpdTyboZmV2ytaqr9pAHuxE1hX8k9bQgZAjq25E6P7"
encryptedKey = "4LbSKwcepbbx4dPetoxvTWszb6mLyJHFhumzmdPRVprbn8XZBvFa6Ffarm6R3WGKutFzdxxJgQDdSHuYdjhDp1EZfSNbj12gXMND1AgmNijSxEua3LwVURU3nzWsvV5b1AsWEjJca24CaFY6T3C"
password = "a very good password"
saltLength = 8
birthday = 376
)
extractSalt := func(rawKey string) []byte {
bytes := base58.Decode(rawKey)
return bytes[len(bytes)-saltLength:]
}
challengeKey := NewChallengePrivateKey([]byte(password), extractSalt(encryptedKey))
decryptedKey, err := challengeKey.DecryptRawKey(encryptedKey, Regtest())
if err != nil {
t.Fatal(err)
}
if decryptedKey.Birthday != birthday {
t.Fatalf("decrypted birthday %v differs from expected %v", decryptedKey.Birthday, birthday)
}
if decryptedKey.Key.String() != encodedKey {
t.Fatalf("key doesnt match\ngot %v\nexpected %v\n", decryptedKey.Key.String(), encodedKey)
}
_, err = challengeKey.PubKey().EncryptKey(decryptedKey.Key, extractSalt(encryptedKey), birthday)
if err != nil {
t.Fatal(err)
}
}
func TestDecodeKeyWithOrWithoutSalt(t *testing.T) {
const (
// The same encoded key, with one version missing the salt field:
saltedKey = "4LbSKwcepbbx4dPetoxvTWszb6mLyJHFhumzmdPRVprbn8XZBvFa6Ffarm6R3WGKutFzdxxJgQDdSHuYdjhDp1EZfSNbj12gXMND1AgmNijSxEua3LwVURU3nzWsvV5b1AsWEjJca24CaFY6T3C"
unsaltedKey = "5XEEts6mc9WV34krDWsqmpLcPCw2JkK8qJu3gFdZpP8ngkERuQEsaDvYrGkhXUpM6jQRtimTYm4XnBPujpo3MsdYBedsNVxvT3WC6uCCFuzNUZCoydVY39yJXbxva7naDxH5iTra"
)
expected := &EncryptedPrivateKeyInfo{
Version: 2,
Birthday: 376,
CipherText: "f6af1ecd17052a81b75902c1712567cf1c650329875feb7e24af3e27235f384054ea549025e99dc2659f95bb6447cf861aa2ec0407ea74baf5a9d6a885ae184b",
EphPublicKey: "020a8d322dda8ff685d80b16681d4e87c109664cdc246a9d3625adfe0de203e71e",
Salt: "e3305526d0cd675f",
}
// Verify the salted version:
actual, err := DecodeEncryptedPrivateKey(saltedKey)
if err != nil {
t.Fatal(err)
}
assertDecodedKeysEqual(t, actual, expected)
// Verify the unsalted version:
actual, err = DecodeEncryptedPrivateKey(unsaltedKey)
if err != nil {
t.Fatal(err)
}
expected.Salt = "0000000000000000" // unsalted key should decode with zeroed field
assertDecodedKeysEqual(t, actual, expected)
}
func assertDecodedKeysEqual(t *testing.T, actual, expected *EncryptedPrivateKeyInfo) {
if actual.Version != expected.Version {
t.Fatalf("version %v expected %v", actual.Version, expected.Version)
}
if actual.Birthday != expected.Birthday {
t.Fatalf("birthday %v, expected %v", actual.Birthday, expected.Birthday)
}
if actual.CipherText != expected.CipherText {
t.Fatalf("cipherText %x expected %x", actual.CipherText, expected.CipherText)
}
if actual.EphPublicKey != expected.EphPublicKey {
t.Fatalf("ephPublicKey %x expected %x", actual.EphPublicKey, expected.EphPublicKey)
}
if actual.Salt != expected.Salt {
t.Fatalf("salt %x expected %x", actual.Salt, expected.Salt)
}
}

15
libwallet/emergency_kit_test.go Executable file
View File

@ -0,0 +1,15 @@
package libwallet
import (
"testing"
)
func TestGenerateEmergencyKitHTML(t *testing.T) {
_, err := GenerateEmergencyKitHTML(&EKInput{
FirstEncryptedKey: "5zZPk5V7oJcXtQyFgdxrP6D5A4Xck2XMC2FG7rrxeDu89K4YuuMoAdZ2MeAGqMU28aR4Lsa5HRxB5mDXmajmYgLaZi6CivXeBRSzazJb8T4VizArrDA8NDH8TipEsHnwCyCd6eiNQYbedyRPw4B",
SecondEncryptedKey: "4RLVcRNPSdCcV5pdd6FsNuUzhGwp3h7piXhpDkHbF31PrHmNqsyMd9vRveXsBVsWPLXHvMkvhzk68yGw4Wwcxfz55yPeN5Jogqpmn7BQc7P1SNymwtgbatLiJfwqFLm1iqoLPobCmK6wH7MY9N7",
}, "es")
if err != nil {
t.Fatal(err)
}
}

View File

@ -0,0 +1,34 @@
package emergencykit
import "testing"
func TestChecksum(t *testing.T) {
// These descriptors are in https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md and
// their expected checksums obtained via the `getdescriptorinfo` RPC endpoint. Note that, to
// reproduce these results, you need a mainnet Bitcoin node (HD key parsing fails otherwise).
testChecksum(t, "gn28ywm7", "pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")
testChecksum(t, "8fhd9pwu", "pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)")
testChecksum(t, "8zl0zxma", "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)")
testChecksum(t, "qkrrc7je", "sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))")
testChecksum(t, "lq9sf04s", "combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")
testChecksum(t, "2wtr0ej5", "sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))")
testChecksum(t, "hzhjw406", "multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)")
testChecksum(t, "y9zthqta", "sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))")
testChecksum(t, "en3tu306", "wsh(multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a))")
testChecksum(t, "ks05yr6p", "sh(wsh(multi(1,03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8,03499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e4,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)))")
testChecksum(t, "qwx6n9lh", "sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))")
testChecksum(t, "axav5m0j", "pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)")
testChecksum(t, "kczqajcv", "pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1/2)")
testChecksum(t, "ml40v0wf", "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)")
testChecksum(t, "t2zpj2eu", "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))")
testChecksum(t, "v66cvalc", "wsh(sortedmulti(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))")
}
func testChecksum(t *testing.T, expectedChecksum string, descriptor string) {
actualChecksum := calculateChecksum(descriptor)
if actualChecksum != expectedChecksum {
t.Errorf("Descriptor %s checksum was %s expecting %s", descriptor, actualChecksum, expectedChecksum)
}
}

View File

@ -0,0 +1,105 @@
package emergencykit
import (
"strings"
"testing"
)
func TestGenerateHTML(t *testing.T) {
out, err := GenerateHTML(&Input{
FirstEncryptedKey: "MyFirstEncryptedKey",
SecondEncryptedKey: "MySecondEncryptedKey",
}, "en")
if err != nil {
t.Fatal(err)
}
if len(out.VerificationCode) != 6 {
t.Fatal("expected verification code to have length 6")
}
if !strings.Contains(out.HTML, out.VerificationCode) {
t.Fatal("expected output html to contain verification code")
}
if !strings.Contains(out.HTML, "MyFirstEncryptedKey") {
t.Fatal("expected output html to contain first encrypted key")
}
if !strings.Contains(out.HTML, "MySecondEncryptedKey") {
t.Fatal("expected output html to contain second encrypted key")
}
if !strings.Contains(out.HTML, `<ul class="descriptors">`) {
t.Fatal("expected output html to contain output descriptors")
}
if !strings.Contains(out.HTML, `<span class="f">wsh</span>`) {
t.Fatal("expected output html to contain output descriptor scripts")
}
}
func TestGenerateHTMLWithFingerprints(t *testing.T) {
data := &Input{
FirstEncryptedKey: "MyFirstEncryptedKey",
FirstFingerprint: "abababab",
SecondEncryptedKey: "MySecondEncryptedKey",
SecondFingerprint: "cdcdcdcd",
}
out, err := GenerateHTML(data, "en")
if err != nil {
t.Fatal(err)
}
if len(out.VerificationCode) != 6 {
t.Fatal("expected verification code to have length 6")
}
if !strings.Contains(out.HTML, out.VerificationCode) {
t.Fatal("expected output html to contain verification code")
}
if !strings.Contains(out.HTML, "MyFirstEncryptedKey") {
t.Fatal("expected output html to contain first encrypted key")
}
if !strings.Contains(out.HTML, "MySecondEncryptedKey") {
t.Fatal("expected output html to contain second encrypted key")
}
if !strings.Contains(out.HTML, `<ul class="descriptors">`) {
t.Fatal("expected output html to contain output descriptors")
}
if !strings.Contains(out.HTML, `<span class="f">wsh</span>`) {
t.Fatal("expected output html to contain output descriptor scripts")
}
if !strings.Contains(out.HTML, `<span class="f">musig</span>`) {
t.Fatal("expected output html to contain musig output descriptor scripts")
}
if !strings.Contains(out.HTML, data.FirstFingerprint) {
t.Fatal("expected output html to contain FirstFingerprint")
}
if !strings.Contains(out.HTML, data.SecondFingerprint) {
t.Fatal("expected output html to contain SecondFingerprint")
}
}
func TestGenerateDeterministicCode(t *testing.T) {
// Create a base Input, without version, which we'll set for each case below:
input := &Input{
FirstEncryptedKey: "foo",
SecondEncryptedKey: "bar",
}
// List our cases for each version:
versionExpectedCodes := []struct {
version int
expectedCode string
}{
{1, "190981"},
{2, "257250"},
{3, "494327"},
}
// Do the thing:
for _, testCase := range versionExpectedCodes {
input.Version = testCase.version
code := generateDeterministicCode(input)
if code != testCase.expectedCode {
t.Fatalf("expected code from %+v to be %s, not %s", input, testCase.expectedCode, code)
}
}
}

View File

@ -0,0 +1,121 @@
package emergencykit
import (
"encoding/hex"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
var someMetadata = Metadata{
Version: 1,
BirthdayBlock: 12345,
EncryptedKeys: []*MetadataKey{
&MetadataKey{
DhPubKey: "0338c52ecbb886ab45de31120c76888da73437e3d6e81510f56d3746399f0fef52",
EncryptedPrivKey: "d0a801c1923663295892e9a9a0bfc770abcb00c20e7cef28e2d743c96b441e677c875e8d6495afb8362aba886ae9ee346c62e82758f5b5ba9a70f61957529255",
Salt: "d579c14c61365bc0",
},
},
OutputDescriptors: []string{
"sh(wsh(multi(2, 89a1749c/1'/1'/0/*, 77e21d45/1'/1'/0/*)))#0wp4hp36",
},
}
func TestReadWriteMetadata(t *testing.T) {
// Create a temporary directory and pick some suitable paths for our input/output files:
tmpDir := createTmpDir(t)
srcFile := filepath.Join(tmpDir, "src.pdf")
dstFile := filepath.Join(tmpDir, "src.pdf")
defer os.RemoveAll(tmpDir)
// Save the sample PDF (included at the end of this file, for readability):
createPdfFile(t, srcFile)
// Write metadata:
mw := MetadataWriter{
SrcFile: srcFile,
DstFile: dstFile,
}
mw.WriteMetadata(&someMetadata)
// Read metadata:
mr := MetadataReader{
SrcFile: dstFile,
}
metadata, err := mr.ReadMetadata()
if err != nil {
t.Fatalf("Failed to read metadata from %s: %v", dstFile, err)
}
// Verify that we got the original metadata back:
if !reflect.DeepEqual(&someMetadata, metadata) {
t.Fatalf("Metadata objects don't match: %v (%v vs %v)", err, someMetadata, metadata)
}
}
func createTmpDir(t *testing.T) string {
tmpDir, err := ioutil.TempDir("", "pdf")
if err != nil {
t.Fatalf("Failed to create temporary directory %s: %v", tmpDir, err)
}
return tmpDir
}
func createPdfFile(t *testing.T, path string) {
content, err := hex.DecodeString(strings.Join(strings.Fields(verySmallPdf), ""))
if err != nil {
t.Fatalf("Failed to hex-decode the sample PDF data: %v", err)
}
err = ioutil.WriteFile(path, content, os.FileMode(0600))
if err != nil {
t.Fatalf("Failed to write PDF to %s: %v", path, err)
}
}
// A very small valid PDF obtained by printing `<html></html>` with Chromium:
const verySmallPdf = `
255044462d312e340a25d3ebe9e10a312030206f626a0a3c3c2f43726561
746f7220284d6f7a696c6c612f352e30205c284d6163696e746f73683b20
496e74656c204d6163204f5320582031305f31345f365c29204170706c65
5765624b69742f3533372e3336205c284b48544d4c2c206c696b65204765
636b6f5c29204368726f6d652f38372e302e343238302e38382053616661
72692f3533372e3336290a2f50726f64756365722028536b69612f504446
206d3837290a2f4372656174696f6e446174652028443a32303230313231
313136333033332b303027303027290a2f4d6f64446174652028443a3230
3230313231313136333033332b303027303027293e3e0a656e646f626a0a
332030206f626a0a3c3c2f636120310a2f424d202f4e6f726d616c3e3e0a
656e646f626a0a342030206f626a0a3c3c2f46696c746572202f466c6174
654465636f64650a2f4c656e6774682039353e3e2073747265616d0a789c
d33332b60403050320d4d543e29a5b1a2924e772157281648c4c4d0d148c
8d0d0c148a52b9c2b514f280e2c67a8646a6607d08165083a1020806b92b
401845e95cfaeec60ae9c560732c0ccd140c0d4ccd40c6a471050221009d
2a19fb0a656e6473747265616d0a656e646f626a0a322030206f626a0a3c
3c2f54797065202f506167650a2f5265736f7572636573203c3c2f50726f
63536574205b2f504446202f54657874202f496d61676542202f496d6167
6543202f496d616765495d0a2f457874475374617465203c3c2f47332033
203020523e3e3e3e0a2f4d65646961426f78205b30203020363132203739
325d0a2f436f6e74656e74732034203020520a2f53747275637450617265
6e747320300a2f506172656e742035203020523e3e0a656e646f626a0a35
2030206f626a0a3c3c2f54797065202f50616765730a2f436f756e742031
0a2f4b696473205b32203020525d3e3e0a656e646f626a0a362030206f62
6a0a3c3c2f54797065202f436174616c6f670a2f50616765732035203020
523e3e0a656e646f626a0a787265660a3020370a30303030303030303030
2036353533352066200a30303030303030303135203030303030206e200a
30303030303030343731203030303030206e200a30303030303030323730
203030303030206e200a30303030303030333037203030303030206e200a
30303030303030363539203030303030206e200a30303030303030373134
203030303030206e200a747261696c65720a3c3c2f53697a6520370a2f52
6f6f742036203020520a2f496e666f2031203020523e3e0a737461727478
7265660a3736310a2525454f46
`

46
libwallet/encodings_test.go Executable file
View File

@ -0,0 +1,46 @@
package libwallet
import (
"encoding/hex"
"math/big"
"reflect"
"testing"
)
func hexToBigInt(value string) *big.Int {
result := &big.Int{}
bytes, _ := hex.DecodeString(value)
result.SetBytes(bytes)
return result
}
func hexToBytes(value string) []byte {
bytes, _ := hex.DecodeString(value)
return bytes
}
func Test_paddedSerializeBigInt(t *testing.T) {
type args struct {
size uint
x *big.Int
}
tests := []struct {
name string
args args
want []byte
}{
{
name: "31 bytes key",
args: args{size: 32, x: hexToBigInt("0e815b7892396a2e28e09c0d50082931eedd7fec16ef2e06724fe48f877ea6")},
want: hexToBytes("000e815b7892396a2e28e09c0d50082931eedd7fec16ef2e06724fe48f877ea6"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := paddedSerializeBigInt(tt.args.size, tt.args.x); !reflect.DeepEqual(got, tt.want) {
t.Errorf("paddedSerializeBigInt() = %v, want %v", got, tt.want)
}
})
}
}

View File

252
libwallet/encrypt_test.go Executable file
View File

@ -0,0 +1,252 @@
package libwallet
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/hex"
"strings"
"testing"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil/base58"
)
func TestPublicKeyEncryption(t *testing.T) {
network := Mainnet()
senderKey, _ := NewHDPrivateKey(randomBytes(32), network)
receiverKey, _ := NewHDPrivateKey(randomBytes(32), network)
payload := randomBytes(178)
ciphertext, err := senderKey.EncrypterTo(receiverKey.PublicKey()).Encrypt(payload)
if err != nil {
t.Fatal(err)
}
ecKey, _ := senderKey.PublicKey().key.ECPubKey()
publicKey := &PublicKey{ecKey}
plaintext, err := receiverKey.DecrypterFrom(publicKey).Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, payload) {
t.Fatalf("decrypted payload differed from original\ndecrypted %v\noriginal %v",
hex.EncodeToString(plaintext),
hex.EncodeToString(payload))
}
// If we don't know who the sender was
plaintext, err = receiverKey.DecrypterFrom(nil).Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, payload) {
t.Fatalf("decrypted payload differed from original\ndecrypted %v\noriginal %v",
hex.EncodeToString(plaintext),
hex.EncodeToString(payload))
}
badKey, _ := NewHDPrivateKey(randomBytes(32), network)
badEcKey, _ := badKey.PublicKey().key.ECPubKey()
badPubKey := &PublicKey{badEcKey}
_, err = receiverKey.DecrypterFrom(badPubKey).Decrypt(ciphertext)
if err == nil {
t.Fatal("Expected decryption from bad sender key to fail")
}
derivedSenderKey, _ := senderKey.DerivedAt(10, false)
ciphertext, err = derivedSenderKey.Encrypter().Encrypt(payload)
if err != nil {
t.Fatal(err)
}
plaintext, err = senderKey.Decrypter().Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, payload) {
t.Fatalf("decrypted payload differed from original\ndecrypted %v\noriginal %v",
hex.EncodeToString(plaintext),
hex.EncodeToString(payload))
}
}
func TestPublicKeyEncryptionV1(t *testing.T) {
const (
priv = "xprv9s21ZrQH143K2DAjx7FiAo2GQAQ5g7GrPYkTB2RaCd2Ei5ZH7f9cbREHiZTCc1FPn9HKuviUHk8sf5cW3dhYjz6W6XPjXNHu5mLpT5oRH1j"
ciphertext = "AMWm2L3YjA7myBTQQgiZi9F5g1NzaaupkPq1y7csUkf7WLXwnPYjkmy5KjVkyTKjaSXPwjx2zmX9Augzwwh89AsWYTv7KfJTXTj3Lx2mNZgmxJ7eezaJyRHv4koQaEmRykSoVE4esjWK779Sac28kCstkqDMPDYeNud5H4ApetF4BvhvPJyMaVn4RHYSAGzBzMcBV7WxYoRveKHqU9LbAfhCndPtRSVZyTVXY8iE3EvQJFeZVyYdovPK67aHsXWRdi8QCinMQSG21TMmhs7GQAh6iB26X2ABcVFJRGeEKE2coAsfuAHzcAMZ3CdzGgVAm7rrQw13W3XpxwwjWVatH9Jm9H4TrnnnLxRCsBoSKDvA1hmH8a2UG9iMxkhsBVMPzNRMy4Bg4MHk8WyRo3bwCLSVJUFFEciQ3mUneHprezzbVZio"
plaintextHex = "ca4dabb05a47d3ab306c1fad895d97b06dc30564191e610f9b254b1a1d0a536b6eca2b83d0d17d67aaad2a958fe6a6557ad5b26f44e12e7662f47a4e4fd6f482b68a83cd140ad4ded43b90a2c2cf349af84d828b1f961901616b4c4cb01f761bd277ad0d3d90506065aef76b930a962fcb90f2f009898c0d55cd07b5e01c355a9067937185fa9237d03e5ed4243e1bf0f8a959c72a83cbb1729b679cbd660052dd2dd3096b0f19e9275ac459b94d02a95642"
)
privKey, _ := NewHDPrivateKeyFromString(priv, "m", Mainnet())
plaintext, _ := hex.DecodeString(plaintextHex)
decrypted, err := privKey.Decrypter().Decrypt(ciphertext)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(plaintext, decrypted) {
t.Fatalf("decrypted payload differed from original\ndecrypted %v\noriginal %v",
hex.EncodeToString(decrypted),
hex.EncodeToString(plaintext))
}
_, err = privKey.Encrypter().Encrypt(decrypted)
if err != nil {
t.Fatal(err)
}
// Since we use a random key every time, comparing cipher texts makes no sense
}
func TestPublicKeyDecryptV1(t *testing.T) {
const (
privHex = "xprv9s21ZrQH143K36uECEJcmTnxSXfHjT9jdb7FpMoUJpENDxeRgpscDF3g2w4ySH6G9uVsGKK7e6WgGp7Vc9VVnwC2oWdrr7a3taWiKW8jKnD"
path = "m"
pathLen = 1
)
payload := []byte("Asado Viernes")
privKey, _ := NewHDPrivateKeyFromString(privHex, path, Mainnet())
encrypted, _ := privKey.Encrypter().Encrypt(payload)
decrypted, err := privKey.Decrypter().Decrypt(encrypted)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(payload, decrypted) {
t.Fatalf("decrypted payload differed from original\ndecrypted %v\noriginal %v",
string(decrypted),
string(payload))
}
alterAndCheck := func(msg string, alter func(data []byte)) {
t.Run(msg, func(t *testing.T) {
encryptedData := base58.Decode(encrypted)
alter(encryptedData)
_, err = privKey.Decrypter().Decrypt(base58.Encode(encryptedData))
if err == nil {
t.Fatalf("Got nil error for altered payload: %v", msg)
}
t.Logf("Got error for altered payload %v: %v", msg, err)
})
}
alterAndCheck("big nonce size", func(data []byte) {
// Override the nonce size
data[1+serializedPublicKeyLength+2+pathLen] = 255
})
alterAndCheck("bigger nonce size", func(data []byte) {
// Override the nonce size
data[1+serializedPublicKeyLength+2+pathLen+1] = 14
})
alterAndCheck("smaller nonce size", func(data []byte) {
// Override the nonce size
data[1+serializedPublicKeyLength+2+pathLen+1] = 1
})
alterAndCheck("big derivation path len", func(data []byte) {
// Override derivation path length
data[1+serializedPublicKeyLength] = 255
})
alterAndCheck("bigger derivation path len", func(data []byte) {
// Override derivation path length
data[1+serializedPublicKeyLength+1] = 4
})
alterAndCheck("smaller derivation path len", func(data []byte) {
// Override derivation path length
data[1+serializedPublicKeyLength+1] = 0
})
alterAndCheck("nonce", func(data []byte) {
// Invert last byte of the nonce
data[1+serializedPublicKeyLength+2+pathLen+2+11] =
^data[1+serializedPublicKeyLength+2+pathLen+2+11]
})
alterAndCheck("tamper ciphertext", func(data []byte) {
// Invert last byte of the ciphertext
data[len(data)-1] = ^data[len(data)-1]
})
t.Run("tamperCiphertextWithAEAD", func(t *testing.T) {
data := base58.Decode(encrypted)
additionalData := data[0 : 1+serializedPublicKeyLength+2+pathLen+2]
nonce := data[len(data)-12:]
encryptionKey, _ := privKey.key.ECPrivKey()
secret, _ := recoverSharedEncryptionSecretForAES(encryptionKey, data[1:serializedPublicKeyLength+1])
block, _ := aes.NewCipher(secret)
gcm, _ := cipher.NewGCM(block)
fakeHdPrivKey, _ := NewHDPrivateKey(randomBytes(32), Mainnet())
fakePayload := []byte(strings.ToLower(string(payload)))
fakePrivKey, _ := fakeHdPrivKey.key.ECPrivKey()
hash := sha256.Sum256(fakePayload)
fakeSig, _ := btcec.SignCompact(btcec.S256(), fakePrivKey, hash[:], false)
plaintext := bytes.NewBuffer(nil)
addVariableBytes(plaintext, fakeSig)
plaintext.Write(fakePayload)
ciphertext := gcm.Seal(nil, nonce, plaintext.Bytes(), additionalData)
offset := len(additionalData)
for _, b := range ciphertext {
data[offset] = b
offset++
}
for _, b := range nonce {
data[offset] = b
offset++
}
_, err = privKey.Decrypter().Decrypt(base58.Encode(data))
if err == nil {
t.Errorf("Got nil error for altered payload: tamper chiphertex recalculating AEAD")
}
t.Logf("Got error for altered payload tamper chiphertex recalculating AEAD: %v", err)
})
}
func TestEncDecOps(t *testing.T) {
const (
privHex = "xprv9s21ZrQH143K36uECEJcmTnxSXfHjT9jdb7FpMoUJpENDxeRgpscDF3g2w4ySH6G9uVsGKK7e6WgGp7Vc9VVnwC2oWdrr7a3taWiKW8jKnD"
path = "m"
pathLen = 1
)
payload := []byte("Asado Viernes")
privKey, _ := NewHDPrivateKeyFromString(privHex, path, Mainnet())
encrypted, _ := NewEncryptOperation(privKey, payload).Encrypt()
decrypted, err := NewDecryptOperation(privKey, encrypted).Decrypt()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(payload, decrypted) {
t.Fatal("decrypt is bad")
}
}

211
libwallet/features_test.go Normal file
View File

@ -0,0 +1,211 @@
package libwallet
import (
"testing"
)
func Test_DetermineUserActivatedFeatureStatus(t *testing.T) {
backendFeaturesWithTaproot := NewStringListWithElements([]string{
BackendFeatureTaproot, BackendFeatureTaprootPreactivation,
})
backendFeaturesWithTaprootPreactivation := NewStringListWithElements([]string{
BackendFeatureTaprootPreactivation,
})
backendFeaturesWithoutTaproot := NewStringListWithElements([]string{})
neverExportedKit := newIntList([]int{})
exportedPreviousKit := newIntList([]int{EKVersionDescriptors})
exportedOnlyLatest := newIntList([]int{EKVersionMusig})
exportedBoth := newIntList([]int{EKVersionDescriptors, EKVersionMusig})
type args struct {
feature UserActivatedFeature
height int
kitVersion *IntList
backendFeatures *StringList
network *Network
}
const (
postTaprootActivationHeight = 709_650
preTaprootActivationHeight = 709_620
)
tests := []struct {
name string
args args
want string
}{
{
"taproot scheduled in regtest",
args{
feature: UserActivatedFeatureTaproot,
height: 99,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithTaproot,
network: Regtest(),
},
UserActivatedFeatureStatusScheduledActivation,
},
{
"taproot off in regtest",
args{
feature: UserActivatedFeatureTaproot,
height: 100,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithoutTaproot,
network: Regtest(),
},
UserActivatedFeatureStatusOff,
},
{
"taproot live in mainnet with no kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusActive,
},
{
"taproot live in mainnet with new kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedBoth,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusActive,
},
{
"taproot preactivated in mainnet with new kit",
args{
feature: UserActivatedFeatureTaproot,
height: preTaprootActivationHeight,
kitVersion: exportedBoth,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusPreactivated,
},
{
"taproot needs activation in mainnet",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedPreviousKit,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusCanActivate,
},
{
"taproot can preactivate in mainnet",
args{
feature: UserActivatedFeatureTaproot,
height: preTaprootActivationHeight,
kitVersion: exportedPreviousKit,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusCanPreactivate,
},
{
"taproot scheduled in mainnet",
args{
feature: UserActivatedFeatureTaproot,
height: preTaprootActivationHeight,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusScheduledActivation,
},
{
"scheduled activation in mainnet",
args{
feature: UserActivatedFeatureTaproot,
height: preTaprootActivationHeight,
kitVersion: exportedOnlyLatest,
backendFeatures: backendFeaturesWithTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusScheduledActivation,
},
{
"backend only preactivation for pre-activated user",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedBoth,
backendFeatures: backendFeaturesWithTaprootPreactivation,
network: Mainnet(),
},
UserActivatedFeatureStatusPreactivated,
},
{
"backend only preactivation for scheduled activated user with no kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithTaprootPreactivation,
network: Mainnet(),
},
UserActivatedFeatureStatusOff,
},
{
"backend only preactivation for scheduled activated user with latest kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedOnlyLatest,
backendFeatures: backendFeaturesWithTaprootPreactivation,
network: Mainnet(),
},
UserActivatedFeatureStatusScheduledActivation,
},
{
"backend turned off for pre-activated user",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedBoth,
backendFeatures: backendFeaturesWithoutTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusOff,
},
{
"backend turned off for scheduled activated user with no kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: neverExportedKit,
backendFeatures: backendFeaturesWithoutTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusOff,
},
{
"backend turned off for scheduled activated user with latest kit",
args{
feature: UserActivatedFeatureTaproot,
height: postTaprootActivationHeight,
kitVersion: exportedOnlyLatest,
backendFeatures: backendFeaturesWithoutTaproot,
network: Mainnet(),
},
UserActivatedFeatureStatusOff,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := DetermineUserActivatedFeatureStatus(tt.args.feature, tt.args.height, tt.args.kitVersion, tt.args.backendFeatures, tt.args.network); got != tt.want {
t.Errorf("DetermineUserActivatedFeatureStatus() = %v, want %v", got, tt.want)
}
})
}
}

363
libwallet/fees/fees_test.go Normal file
View File

@ -0,0 +1,363 @@
package fees
import (
"reflect"
"testing"
"github.com/btcsuite/btcutil"
)
func TestComputeSwapFees(t *testing.T) {
testCases := []struct {
desc string
amount btcutil.Amount
bestRouteFees []BestRouteFees
policies *FundingOutputPolicies
takeFeeFromAmount bool
expected *SwapFees
}{
{
desc: "smoke test",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
takeFeeFromAmount: false,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1010,
ConfirmationsNeeded: 1,
},
},
{
desc: "qualifies for 0-conf",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 1000000,
},
takeFeeFromAmount: false,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
ConfirmationsNeeded: 0,
OutputAmount: 1010,
},
},
{
desc: "qualifies for debt lend",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
takeFeeFromAmount: false,
policies: &FundingOutputPolicies{
MaximumDebt: 1000000,
PotentialCollect: 0,
MaxAmountFor0Conf: 1000000,
},
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeLend,
DebtAmount: 1010,
OutputAmount: 0,
ConfirmationsNeeded: 0,
},
},
{
desc: "debt collect",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 100,
PotentialCollect: 1010,
MaxAmountFor0Conf: 1000000,
},
takeFeeFromAmount: false,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeCollect,
DebtAmount: 1010,
OutputAmount: 2020,
ConfirmationsNeeded: 0,
},
},
{
desc: "dust threshold",
amount: 50,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
takeFeeFromAmount: false,
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 486,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 546,
ConfirmationsNeeded: 1,
},
},
{
desc: "sub-dust lend",
amount: 50,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
takeFeeFromAmount: false,
policies: &FundingOutputPolicies{
MaximumDebt: 1000000,
PotentialCollect: 0,
MaxAmountFor0Conf: 1000000,
},
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 486,
DebtType: DebtTypeLend,
DebtAmount: 60,
OutputAmount: 0,
ConfirmationsNeeded: 0,
},
},
{
desc: "uses last route if route with enough capacity",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 900,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
{
MaxCapacity: 900,
FeeProportionalMillionth: 1,
FeeBase: 20,
},
},
takeFeeFromAmount: false,
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
expected: &SwapFees{
RoutingFee: 20,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1020,
ConfirmationsNeeded: 1,
},
},
{
desc: "smoke test TFFA",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
takeFeeFromAmount: true,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1010,
ConfirmationsNeeded: 1,
},
},
{
desc: "qualifies for 0-conf TFFA",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
takeFeeFromAmount: true,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1010,
ConfirmationsNeeded: 1,
},
},
{
desc: "qualifies for debt lend TFFA",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
takeFeeFromAmount: true,
policies: &FundingOutputPolicies{
MaximumDebt: 1000000,
PotentialCollect: 0,
MaxAmountFor0Conf: 1000000,
},
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1010,
ConfirmationsNeeded: 0,
},
},
{
desc: "debt collect TFFA",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
policies: &FundingOutputPolicies{
MaximumDebt: 100,
PotentialCollect: 1010,
MaxAmountFor0Conf: 1000000,
},
takeFeeFromAmount: true,
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 0,
DebtType: DebtTypeCollect,
DebtAmount: 1010,
OutputAmount: 2020,
ConfirmationsNeeded: 0,
},
},
{
desc: "dust threshold TFFA",
amount: 50,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
takeFeeFromAmount: true,
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
expected: &SwapFees{
RoutingFee: 10,
OutputPadding: 486,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 546,
ConfirmationsNeeded: 1,
},
},
{
desc: "uses last route if route with enough capacity",
amount: 1000,
bestRouteFees: []BestRouteFees{
{
MaxCapacity: 900,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
{
MaxCapacity: 900,
FeeProportionalMillionth: 1,
FeeBase: 20,
},
},
takeFeeFromAmount: true,
policies: &FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
expected: &SwapFees{
RoutingFee: 20,
OutputPadding: 0,
DebtType: DebtTypeNone,
DebtAmount: 0,
OutputAmount: 1020,
ConfirmationsNeeded: 1,
},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
fees := ComputeSwapFees(tC.amount, tC.bestRouteFees, tC.policies, tC.takeFeeFromAmount)
if !reflect.DeepEqual(fees, tC.expected) {
t.Errorf("fees do not equal expected fees (%+v != %+v)", fees, tC.expected)
}
})
}
}

View File

@ -0,0 +1,82 @@
package hdpath
import (
"reflect"
"testing"
)
var (
rootPath = make([]PathIndex, 0)
shortPath = []PathIndex{
PathIndex{Index: 0, Hardened: true},
}
longPath = []PathIndex{
PathIndex{Index: 44, Hardened: true},
PathIndex{Index: 1, Hardened: true},
PathIndex{Index: 2, Hardened: false},
}
shortMuunPath = []PathIndex{
PathIndex{Index: 1, Hardened: true, Name: "schema"},
}
longMuunPath = []PathIndex{
PathIndex{Index: 1, Hardened: true, Name: "schema"},
PathIndex{Index: 1, Hardened: true, Name: "recovery"},
}
)
func TestBuild(t *testing.T) {
p, err := Parse("m/1/1")
if err != nil {
t.Fatal(err)
}
p = p.Child(0)
p = p.NamedChild("foo", 1)
if p.String() != "m/1/1/0/foo:1" {
t.Fatalf("expected path to be m/1/1/0/foo:1, got %s instead", p.String())
}
}
func TestParsingAndValidation(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want []PathIndex
wantErr bool
}{
{name: "root1", args: args{path: ""}, want: rootPath},
{name: "root2", args: args{path: "m"}, want: rootPath},
{name: "root3", args: args{path: "/"}, want: rootPath},
{name: "short1", args: args{path: "m/0'"}, want: shortPath},
{name: "short2", args: args{path: "0'"}, want: shortPath},
{name: "short3", args: args{path: "/0'"}, want: shortPath},
{name: "long1", args: args{path: "m/44'/1'/2"}, want: longPath},
{name: "long2", args: args{path: "/44'/1'/2"}, want: longPath},
{name: "long3", args: args{path: "44'/1'/2"}, want: longPath},
{name: "shortMuun", args: args{path: "m/schema:1'"}, want: shortMuunPath},
{name: "longMuun", args: args{path: "m/schema:1'/recovery:1'"}, want: longMuunPath},
{name: "has spaces", args: args{path: "m / 0 / 1"}, wantErr: true},
{name: "has no indexes", args: args{path: "m/b/c"}, wantErr: true},
{name: "has weird chars", args: args{path: "m/1.2^3"}, wantErr: true},
{name: "has several :", args: args{path: "m/recovery:1:1"}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path, err := Parse(tt.args.path)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
return
}
indexes := path.Indexes()
if !reflect.DeepEqual(indexes, tt.want) {
t.Errorf("Indexes() = %v, want %v", indexes, tt.want)
}
})
}
}

294
libwallet/hdprivatekey_test.go Executable file
View File

@ -0,0 +1,294 @@
package libwallet
import (
"crypto/sha256"
"testing"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
)
const (
// m
vector1PrivKey = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
vector1PubKey = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
vector1FirstPath = "m/0'/1"
vector1FirstPriv = "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"
vector1FirstPub = "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"
vector1SecondPath = "m/0'/1/2'/2"
vector1SecondPriv = "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334"
vector1SecondPub = "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"
vector2PrivKey = "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"
vector2PubKey = "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
vector2FirstPath = "m/0"
vector2FirstPriv = "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt"
vector2FirstPub = "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"
vector2SecondPath = "m/0/2147483647'/1"
vector2SecondPriv = "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef"
vector2SecondPub = "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"
vector2ThirdPath = "m/0/2147483647'/1/2147483646'"
vector2ThirdPriv = "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc"
vector2ThirdPub = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
vector3PrivKey = "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6"
vector3PubKey = "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13"
vector3FirstPath = "m/0'"
vector3FirstPriv = "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L"
vector3FirstPub = "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y"
symmetricPrivKey = "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"
symmetricPubKey = "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"
symmetricFirstPath = "m/0"
symmetricFirstPriv = "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt"
symmetricFirstPub = "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"
symmetricSecondPath = "m/0/2147483647'/1"
symmetricSecondPriv = "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef"
symmetricSecondPub = "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"
symmetricThirdPath = "m/0/2147483647'/1/2147483646'"
symmetricThirdPriv = "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc"
symmetricThirdPub = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"
)
var (
network = Regtest()
)
func TestNewHDPrivateKeySerialization(t *testing.T) {
t.Run("bad key generation", func(t *testing.T) {
badKey, err := NewHDPrivateKey(randomBytes(1), network)
if badKey != nil || err == nil {
t.Errorf("keys with only 1 byte should return error, got %v, %v", badKey, err)
}
})
t.Run("invalid key deserialization", func(t *testing.T) {
badKey, err := NewHDPrivateKeyFromString("fooo", "m", Regtest())
if badKey != nil || err == nil {
t.Errorf("bad key should only return error returned %v, %v", badKey, err)
}
badKey, err = NewHDPrivateKeyFromString(vector1FirstPub, "m", Regtest())
if badKey != nil || err == nil {
t.Errorf("expected failure when parsing pub key as priv key, got %v, %v", badKey, err)
}
badPubKey, err := NewHDPublicKeyFromString(vector1FirstPriv, "m", Regtest())
if badPubKey != nil || err == nil {
t.Errorf("expected failure when parsing priv key as pub key, got %v, %v", badPubKey, err)
}
})
t.Run("test regtest address serialization", func(t *testing.T) {
// Create a new key and set regtest as chain
randomKey, _ := NewHDPrivateKey(randomBytes(16), network)
randomKey.key.SetNet(&chaincfg.RegressionNetParams)
// Parsing it should fail since we check we know the chain
key, err := NewHDPrivateKeyFromString(randomKey.String(), "m", Regtest())
if key == nil || err != nil {
t.Errorf("failed to parse regtest key, got err %v", err)
}
})
t.Run("Random key serialization", func(t *testing.T) {
seed := randomBytes(16)
randomKey, err := NewHDPrivateKey(seed, network)
if err != nil {
t.Fatalf("couldn't generate priv key")
}
serialized := randomKey.String()
deserialized, err := NewHDPrivateKeyFromString(serialized, "m", Regtest())
if err != nil {
t.Fatalf("failed to deserialize key")
}
if serialized != deserialized.String() {
t.Errorf("keys are different")
}
})
t.Run("Child key serialization", func(t *testing.T) {
root, err := NewHDPrivateKeyFromString(
"tprv8ZgxMBicQKsPdGCzsJ31BsQnFL1TSQ82dfsZYTtsWJ1T8g7xTfnV19gf8nYPqzkzk6yLL9kzDYshmUrYyXt7uXsGbk9eN7juRxg9sjaxSjn", "m", Regtest())
if err != nil {
t.Fatalf("failed to parse root key")
}
key1, _ := root.DerivedAt(1, true)
key2, _ := key1.DerivedAt(1, true)
const encodedKey = "tprv8e8vMhwEcLr1ZfZETKTQSpxJ6KfZuczALe8KrRCDLpSbXPwp7PY1ZVHtqUkFsYZETPRcfjVSCv8DiYP9RyAZrFhnLE8aYdaSaZEWyT5c8Ji"
if key2.String() != encodedKey {
t.Fatalf("derived key doesn't match serialized")
}
decodedKey, _ := NewHDPrivateKeyFromString(encodedKey, "m", Regtest())
if decodedKey.String() != encodedKey {
t.Fatalf("decoded key doesn't match encoded string")
}
})
}
// These tests are based on the test vectors in BIP 32
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
func TestKeyDerivation(t *testing.T) {
testPath := func(t *testing.T, privKey *HDPrivateKey, path, priv, pub string) {
key, _ := privKey.DeriveTo(path)
if key.String() != priv {
t.Errorf("%v priv doesn't match", path)
}
if key.PublicKey().String() != pub {
t.Errorf("%v pub doesn't match", path)
}
}
t.Run("vector1", func(t *testing.T) {
privKey, _ := NewHDPrivateKeyFromString(vector1PrivKey, "m", Regtest())
if privKey.PublicKey().String() != vector1PubKey {
t.Errorf("pub key doesnt match")
}
testPath(t, privKey, vector1FirstPath, vector1FirstPriv, vector1FirstPub)
testPath(t, privKey, vector1SecondPath, vector1SecondPriv, vector1SecondPub)
})
t.Run("vector2", func(t *testing.T) {
privKey, _ := NewHDPrivateKeyFromString(vector2PrivKey, "m", Regtest())
if privKey.PublicKey().String() != vector2PubKey {
t.Errorf("pub key doesnt match")
}
testPath(t, privKey, vector2FirstPath, vector2FirstPriv, vector2FirstPub)
testPath(t, privKey, vector2SecondPath, vector2SecondPriv, vector2SecondPub)
testPath(t, privKey, vector2ThirdPath, vector2ThirdPriv, vector2ThirdPub)
})
t.Run("vector3", func(t *testing.T) {
privKey, _ := NewHDPrivateKeyFromString(vector3PrivKey, "m", Regtest())
if privKey.PublicKey().String() != vector3PubKey {
t.Errorf("pub key doesnt match")
}
testPath(t, privKey, vector3FirstPath, vector3FirstPriv, vector3FirstPub)
})
}
func TestSymmetricDerivation(t *testing.T) {
privKey, _ := NewHDPrivateKeyFromString(symmetricPrivKey, "m", Regtest())
pubKey := privKey.PublicKey()
t.Run("basic check", func(t *testing.T) {
if pubKey.String() != symmetricPubKey {
t.Fatalf("pub key doesn't match")
}
})
t.Run("first path", func(t *testing.T) {
newPriv, _ := privKey.DeriveTo(symmetricFirstPath)
if newPriv.String() != symmetricFirstPriv {
t.Errorf("priv key doesn't match")
}
if newPriv.PublicKey().String() != symmetricFirstPub {
t.Errorf("pub key doesn't match")
}
newPub, _ := pubKey.DeriveTo(symmetricFirstPath)
if newPub.String() != newPriv.PublicKey().String() {
t.Errorf("extracted and derived pub key don't match")
}
})
t.Run("second path", func(t *testing.T) {
newPriv, _ := privKey.DeriveTo(symmetricSecondPath)
if newPriv.String() != symmetricSecondPriv {
t.Errorf("priv key doesn't match")
}
if newPriv.PublicKey().String() != symmetricSecondPub {
t.Errorf("pub key doesn't match")
}
hPriv, _ := privKey.DeriveTo("m/0/2147483647'")
newPub, _ := hPriv.PublicKey().DeriveTo(symmetricSecondPath)
if newPub.String() != newPriv.PublicKey().String() {
t.Errorf("extracted and derived pub key don't match")
}
})
t.Run("third path", func(t *testing.T) {
newPriv, _ := privKey.DeriveTo(symmetricThirdPath)
if newPriv.String() != symmetricThirdPriv {
t.Errorf("priv key doesn't match")
}
if newPriv.PublicKey().String() != symmetricThirdPub {
t.Errorf("pub key doesn't match")
}
})
testBadDerivation := func(t *testing.T, path string) {
privKey, _ := NewHDPrivateKeyFromString(vector1PrivKey, "m/123", Regtest())
pubKey := privKey.PublicKey()
badKey, err := privKey.DeriveTo(path)
if badKey != nil || err == nil {
t.Errorf("derivation should fail got %v, %v", badKey, err)
}
badPubKey, err := pubKey.DeriveTo(path)
if badPubKey != nil || err == nil {
t.Errorf("derivation should fail got %v, %v", badPubKey, err)
}
}
t.Run("new path is not prefix of old path", func(t *testing.T) {
testBadDerivation(t, "m/45")
})
t.Run("derivation path is invalid", func(t *testing.T) {
testBadDerivation(t, "m/123/asd45")
})
}
func TestHDPrivateKeySign(t *testing.T) {
seed := randomBytes(32)
privKey, err := NewHDPrivateKey(seed, Regtest())
if err != nil {
t.Fatal(err)
}
data := []byte("foobar")
sigBytes, err := privKey.Sign(data)
if err != nil {
t.Fatal(err)
}
sig, err := btcec.ParseSignature(sigBytes, btcec.S256())
if err != nil {
t.Fatal(err)
}
pubKey, err := privKey.key.ECPubKey()
if err != nil {
t.Fatal(err)
}
hash := sha256.Sum256(data)
if ok := sig.Verify(hash[:], pubKey); !ok {
t.Fatal(err)
}
}

36
libwallet/hdpublickey_test.go Executable file
View File

@ -0,0 +1,36 @@
package libwallet
import (
"bytes"
"math"
"testing"
"github.com/btcsuite/btcutil/hdkeychain"
)
func TestHDPublicKey_DerivedAt(t *testing.T) {
priv, _ := NewHDPrivateKey(randomBytes(32), Mainnet())
_, err := priv.PublicKey().DerivedAt(math.MaxUint32)
if err == nil {
t.Errorf("derived a hardened pub key")
}
_, err = priv.PublicKey().DerivedAt(math.MaxUint32 ^ hdkeychain.HardenedKeyStart)
if err != nil {
t.Errorf("failed to derive unhardened pub key due to %v", err)
}
}
func TestHDPublicKey_Fingerprint(t *testing.T) {
pubKey, _ := NewHDPublicKeyFromString(
"xpub661MyMwAqRbcF3YgLe8xTTTrDHf5bmEQuj5XfQP3bvwHqBpYvt99tcMSXXzroWJoQM4eMDNZNzNYZEJfTqxq5S82J644buASmW4Y7VnwUeJ",
"m/schema:1'/recovery:1'",
Mainnet(),
)
fingerprint := pubKey.Fingerprint()
if !bytes.Equal(fingerprint, []byte{207, 227, 7, 97}) {
t.Fatalf("fingerprint does not match, got %x", fingerprint)
}
}

View File

@ -0,0 +1,791 @@
package libwallet
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"testing"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/tlv"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/muun/libwallet/hdpath"
)
func TestFulfillHtlc(t *testing.T) {
setup()
network := Regtest()
userKey, _ := NewHDPrivateKey(randomBytes(32), network)
userKey.Path = "m/schema:1'/recovery:1'"
muunKey, _ := NewHDPrivateKey(randomBytes(32), network)
muunKey.Path = "m/schema:1'/recovery:1'"
secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
panic(err)
}
err = PersistInvoiceSecrets(secrets)
if err != nil {
panic(err)
}
// stub
swapServerPublicKey := randomBytes(32)
invoice := secrets.Get(0)
paymentHash := invoice.PaymentHash
amt := int64(10000)
lockTime := int64(1000)
htlcKeyPath := hdpath.MustParse(invoice.keyPath).Child(htlcKeyChildIndex)
userHtlcKey, err := userKey.DeriveTo(htlcKeyPath.String())
if err != nil {
panic(err)
}
muunHtlcKey, err := muunKey.DeriveTo(htlcKeyPath.String())
if err != nil {
panic(err)
}
htlcScript, err := createHtlcScript(
userHtlcKey.PublicKey().Raw(),
muunHtlcKey.PublicKey().Raw(),
swapServerPublicKey,
lockTime,
paymentHash,
)
if err != nil {
panic(err)
}
witnessHash := sha256.Sum256(htlcScript)
address, err := btcutil.NewAddressWitnessScriptHash(witnessHash[:], Regtest().network)
if err != nil {
t.Fatal(err)
}
pkScript, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatal(err)
}
prevOutHash, err := chainhash.NewHash(randomBytes(32))
if err != nil {
panic(err)
}
htlcTx := wire.NewMsgTx(1)
htlcTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *prevOutHash,
},
})
htlcTx.AddTxOut(&wire.TxOut{
PkScript: pkScript,
Value: amt,
})
nodePublicKey, err := invoice.IdentityKey.key.ECPubKey()
if err != nil {
panic(err)
}
fulfillmentTx := wire.NewMsgTx(1)
fulfillmentTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: htlcTx.TxHash(),
Index: 0,
},
})
outputPath := "m/schema:1'/recovery:1'/34/56"
addr := newAddressAt(userKey, muunKey, outputPath, network)
fulfillmentTx.AddTxOut(&wire.TxOut{
PkScript: addr.ScriptAddress(),
Value: amt,
})
muunSignKey, err := muunHtlcKey.key.ECPrivKey()
if err != nil {
panic(err)
}
sigHashes := txscript.NewTxSigHashes(fulfillmentTx)
muunSignature, err := txscript.RawTxInWitnessSignature(
fulfillmentTx,
sigHashes,
0,
amt,
htlcScript,
txscript.SigHashAll,
muunSignKey,
)
if err != nil {
panic(err)
}
swap := &IncomingSwap{
SphinxPacket: createSphinxPacket(nodePublicKey, paymentHash, invoice.paymentSecret, amt, lockTime),
PaymentHash: paymentHash,
Htlc: &IncomingSwapHtlc{
HtlcTx: serializeTx(htlcTx),
ExpirationHeight: lockTime,
SwapServerPublicKey: swapServerPublicKey,
},
}
data := &IncomingSwapFulfillmentData{
FulfillmentTx: serializeTx(fulfillmentTx),
MuunSignature: muunSignature,
MerkleTree: nil,
HtlcBlock: nil,
ConfirmationTarget: 1,
}
result, err := swap.Fulfill(data, userKey, muunKey.PublicKey(), network)
if err != nil {
t.Fatal(err)
}
signedTx := wire.NewMsgTx(2)
signedTx.Deserialize(bytes.NewReader(result.FulfillmentTx))
verifyInput(t, signedTx, hex.EncodeToString(swap.Htlc.HtlcTx), 0, 0)
}
func TestFulfillHtlcWithCollect(t *testing.T) {
setup()
network := Regtest()
userKey, _ := NewHDPrivateKey(randomBytes(32), network)
userKey.Path = "m/schema:1'/recovery:1'"
muunKey, _ := NewHDPrivateKey(randomBytes(32), network)
muunKey.Path = "m/schema:1'/recovery:1'"
secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
panic(err)
}
err = PersistInvoiceSecrets(secrets)
if err != nil {
panic(err)
}
// stub
swapServerPublicKey := randomBytes(32)
invoiceSecrets := secrets.Get(0)
paymentHash := invoiceSecrets.PaymentHash
amt := int64(10000)
lockTime := int64(1000)
collected := int64(1000)
outputAmount := amt - collected
htlcKeyPath := hdpath.MustParse(invoiceSecrets.keyPath).Child(htlcKeyChildIndex)
userHtlcKey, err := userKey.DeriveTo(htlcKeyPath.String())
if err != nil {
panic(err)
}
muunHtlcKey, err := muunKey.DeriveTo(htlcKeyPath.String())
if err != nil {
panic(err)
}
htlcScript, err := createHtlcScript(
userHtlcKey.PublicKey().Raw(),
muunHtlcKey.PublicKey().Raw(),
swapServerPublicKey,
lockTime,
paymentHash,
)
if err != nil {
panic(err)
}
witnessHash := sha256.Sum256(htlcScript)
address, err := btcutil.NewAddressWitnessScriptHash(witnessHash[:], Regtest().network)
if err != nil {
t.Fatal(err)
}
pkScript, err := txscript.PayToAddrScript(address)
if err != nil {
t.Fatal(err)
}
prevOutHash, err := chainhash.NewHash(randomBytes(32))
if err != nil {
panic(err)
}
htlcTx := wire.NewMsgTx(1)
htlcTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *prevOutHash,
},
})
htlcTx.AddTxOut(&wire.TxOut{
PkScript: pkScript,
Value: amt,
})
nodePublicKey, err := invoiceSecrets.IdentityKey.key.ECPubKey()
if err != nil {
panic(err)
}
fulfillmentTx := wire.NewMsgTx(1)
fulfillmentTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: htlcTx.TxHash(),
Index: 0,
},
})
outputPath := "m/schema:1'/recovery:1'/34/56"
addr := newAddressAt(userKey, muunKey, outputPath, network)
fulfillmentTx.AddTxOut(&wire.TxOut{
PkScript: addr.ScriptAddress(),
Value: outputAmount,
})
muunSignKey, err := muunHtlcKey.key.ECPrivKey()
if err != nil {
panic(err)
}
sigHashes := txscript.NewTxSigHashes(fulfillmentTx)
muunSignature, err := txscript.RawTxInWitnessSignature(
fulfillmentTx,
sigHashes,
0,
amt,
htlcScript,
txscript.SigHashAll,
muunSignKey,
)
if err != nil {
panic(err)
}
swap := &IncomingSwap{
SphinxPacket: createSphinxPacket(nodePublicKey, paymentHash, invoiceSecrets.paymentSecret, amt, lockTime),
PaymentHash: paymentHash,
Htlc: &IncomingSwapHtlc{
HtlcTx: serializeTx(htlcTx),
ExpirationHeight: lockTime,
SwapServerPublicKey: swapServerPublicKey,
},
CollectSat: collected,
}
data := &IncomingSwapFulfillmentData{
FulfillmentTx: serializeTx(fulfillmentTx),
MuunSignature: muunSignature,
OutputVersion: 4,
OutputPath: outputPath,
MerkleTree: nil,
HtlcBlock: nil,
ConfirmationTarget: 1,
}
result, err := swap.Fulfill(data, userKey, muunKey.PublicKey(), network)
if err != nil {
t.Fatal(err)
}
swap.CollectSat = 0
_, err = swap.Fulfill(data, userKey, muunKey.PublicKey(), network)
if err == nil {
t.Fatal("expected 0 collect to fail")
}
signedTx := wire.NewMsgTx(2)
signedTx.Deserialize(bytes.NewReader(result.FulfillmentTx))
verifyInput(t, signedTx, hex.EncodeToString(swap.Htlc.HtlcTx), 0, 0)
}
func TestVerifyFulfillable(t *testing.T) {
setup()
network := Regtest()
userKey, _ := NewHDPrivateKey(randomBytes(32), network)
userKey.Path = "m/schema:1'/recovery:1'"
muunKey, _ := NewHDPrivateKey(randomBytes(32), network)
muunKey.Path = "m/schema:1'/recovery:1'"
generateAndPersistInvoiceSecrets := func() {
secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
panic(err)
}
err = PersistInvoiceSecrets(secrets)
if err != nil {
panic(err)
}
}
createInvoice := func(amountSat int64) string {
builder := InvoiceBuilder{}
builder.Network(network)
builder.UserKey(userKey)
builder.AddRouteHints(&RouteHints{
Pubkey: "03c48d1ff96fa32e2776f71bba02102ffc2a1b91e2136586418607d32e762869fd",
FeeBaseMsat: 1000,
FeeProportionalMillionths: 1000,
CltvExpiryDelta: 8,
})
if amountSat != 0 {
builder.AmountSat(amountSat)
}
retry:
invoice, err := builder.Build()
if err != nil {
panic(err)
}
if invoice == "" {
generateAndPersistInvoiceSecrets()
goto retry
}
return invoice
}
t.Run("single part payment", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err != nil {
t.Fatal(err)
}
})
t.Run("multi part payment fails", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createMppSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected failure to fulfill mpp payment")
}
})
t.Run("non existant invoice", func(t *testing.T) {
swap := &IncomingSwap{
PaymentHash: randomBytes(32),
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected failure to fulfill non existant invoice")
}
})
t.Run("invalid payment secret", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, _, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, randomBytes(32), amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected error with random payment secret")
}
})
t.Run("muun 2 muun with no blob", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, _, _ := getInvoiceSecrets(invoice, userKey)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: nil,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err != nil {
t.Fatal(err)
}
})
t.Run("invalid amount from server", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt - 1,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected error with invalid amount")
}
})
t.Run("validates amount from server", func(t *testing.T) {
invoice := createInvoice(0)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err != nil {
t.Fatal(err)
}
})
t.Run("validates invoice amount", func(t *testing.T) {
invoice := createInvoice(20000)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected error with amount not matching invoice amount")
}
})
t.Run("validates invoice amount for muun 2 muun", func(t *testing.T) {
invoice := createInvoice(20000)
paymentHash, _, _ := getInvoiceSecrets(invoice, userKey)
amt := int64(10000)
swap := &IncomingSwap{
PaymentHash: paymentHash,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err == nil {
t.Fatal("expected error with amount not matching invoice amount")
}
})
t.Run("invoice with amount", func(t *testing.T) {
invoice := createInvoice(20000)
paymentHash, paymentSecret, nodePublicKey := getInvoiceSecrets(invoice, userKey)
amt := int64(20000)
lockTime := int64(1000)
onion := createSphinxPacket(nodePublicKey, paymentHash, paymentSecret, amt, lockTime)
swap := &IncomingSwap{
PaymentHash: paymentHash,
SphinxPacket: onion,
PaymentAmountSat: amt,
// ignore the rest of the parameters
}
if err := swap.VerifyFulfillable(userKey, network); err != nil {
t.Fatal(err)
}
})
}
func TestFulfillWithHardwiredData(t *testing.T) {
setup()
d := func(s string) []byte {
b, _ := hex.DecodeString(s)
return b
}
network := Regtest()
swap := &IncomingSwap{
SphinxPacket: d("0002dc29e8562cbd4961bbe76ebc847641fba878b5dda04a31d17c5c4648c4e8f614380397b83978e2f12161c7a010d494f16ca5dc96a06369a19ccfadf9ee3ec0ecdcac9479b25459d01670c629175e8cc1110f328ec6d0e21ca81c5a7f3b71023b10ca287985695fc4c757ea25c9d49bd6b4e43bb85abe043fbcb2ef473bfd1830dbdad7c3e6de26d3a703bd307cba5a33ba56d8398e22c87034b6794ecd4c2d4157a90520b78171b1860c69c302b25f7a10edf9ac3ad87d10cf7cbe8525ac4b3ebc6544787b1b010e61ab73ee86ae8752f44687753af3b31678a7fe1e85c57c6e1de33878f43ccbba1478fbd8c055a5e2c55cadcae05537f6478ba13391343c7f1063138ba9c38803ac8fd6b9eb5b5114559df1746593df1d4d9a6883f835758dc583bb9dea72ad3079df653e73efa915c629ba8056d945cf63dc316ffd118aa7e8d20430de12ac9beaf9f472b68bdf278dccd6a84f2b6c37c25ddb3abc3583094613a07f277ed80840a33ae34d62e3dd17d21e2faf82221375914444460e38ebe5ef67d9fac02de507d7964a2191b0de43c0c7115840e863f1ca03e0a5b05dedb90826b79b1b1ce5aa7666c37bae08bbe8032a82ed1d9c15df4195e408be16844dc2b5e5868a38bd560e87d629d1c6ec11e3dbb112dc1d2692ad4b7c28b5904bf49c1efcb87562f48ec5e7177f2034dadd2c08c4a02d718ffa16585738489d89f01d350123e621e4bd8927879bd3c4cccf1fe44f7b4daf4466a60b7197dbb14c5ffd23e477343fa79a8d8818804280757b1f98439749927de21545d1a9434c59c1d0e093ab3c1936b4db3b4c67dd9cae55cf2ee55066490a602a74cf88382d35db442b7e57b869fd43360ca0c9ef03bc89784e340450fcae81fb2080c97f9852124900a71bf68921e5a6e690a5ee73c266df2344106aec8de601f8a14254c97ee96dd3f858df1cb727ee51bc8ebeb6dea5253841bd2a13aeba1bc3846c9cc45d7124f9f9aa61a6c3a7b15424c5dfadfb7644392bf0843f643d97b2e08c1a3d6ebfcb7aafcd78cd2d904645cf043e1a42b60390647f24d6663fc74dc77d06bb691d12b09bb4afc3b55427f5bac76748b73b6debb17ca6bb890f2005f39e714aa0e7a584e57a41a78f1d3f4981ce4e22a49caa389360eabc9f623b923c864eb74a2a860a061d6ecbe6f4c55596907ba342836c7607117f405e098af1f73b8ae2542a59d30c58fca8ee37c6482bd87069b142e692f54a04fd6d3a5e22595eb2de31c830cea4395b085b7c8725971df657c5af5501fa8cc9cefda4f1ae8862b6229ed74b045e17587f68ab55c9176c256c69564274502d0ec6e5e3be8ea93e14428d328963ca4671ee2f629ae8f2c2ff8f2b2145f218d8a3707715bdfa5b2bb5211b9cd8775e33ce5546f618bc998b5953c5d2a2f7932873fd248be3a504ce7f7d4b731bfb4fea363e5e281ff3c314b997d8c89d90c8bf15d983da26e75bf52e98b92d108e6f4aee0b25561d0ce8f22a8400b2085e713d909c20b2c84d5ba36dbe94f324690ab207070bfb7247510e78263989dc04669ea273ca44d2de31aa8a950bc120fcec0c627ad78b59f635ddd657d97d56fcc9ebef32b3ee1051e003c0b617a1196d6c387f014fd47e7f1c64b27d43cadfaf25a7849a77392a63470665e5e3bb0c28b66b9de938c805fab01de62cd63b0d200f97156236fcd412f1eadc125371bd09726e65da8ee8e77e7fa0070bb4f6090a2afd7a33e3d37aff7a5dac62830a7f79aa28f6bce305fc6eb96dd53cd2448b618bdadfc79dcee815d6dd6935d9cece06f810df6cbd529b01361d97f3c50d749739d9598edd53c9bd984a5348a5345c25c13fc7c6d48b7412f4ab6de74e6b7fd4945f710562c312a2903680c387a7364920e435db7777fe66b60a49adb656cdd12f"),
PaymentHash: d("31b35302d3e842a363f8992e423910bfb655b9cd6325b67f5c469fa8f2c4e55b"),
Htlc: &IncomingSwapHtlc{
HtlcTx: d("02000000000101896c8b88d8219cc7dae111558626c952da6fc2a542f7db970e8af745c4678bdb0000000000feffffff02d006032a01000000160014b710e26258f27a99807e2a09bf39b5d3588c561b089d0000000000002200208fb1ed3841bee4385ba4efe1a8aff0943b3b1eeadada45e4784f54e2efa1f30a0247304402205e6a82391804b8bc483f6d9d44bdcd7afb477f66c4c794872735447f1dd883480220626fc746386f8afed04a43776d661bab1d610cdebcb5d03c7d594b0edd3612ed0121037d4c78fdce4b13788efb012a68834da3a75f6ac153f55edf22fadc09e6d4f67700000000"),
ExpirationHeight: 401,
SwapServerPublicKey: d("028b7c740b590012eaffef072675baaa95aee39508fd049ed1cd698ee26ce33f02"),
},
}
data := &IncomingSwapFulfillmentData{
FulfillmentTx: d("0100000001a2b209d88daaa2b9fedc8217904b75934d280f889cd64db243c530dbd72a9b670100000000ffffffff0110270000000000002200209c58b43eff77533a3a056046ee4cb5044bb0eeb74635ebb8cc03048b3720716b00000000"),
MuunSignature: d("30450221008c40c9ef1613cfa500c52531b9fd0b7212f562e425dcdc4358cc3a6de25e11940220717ab86c13cb645dd2e694c3b4e5fd0e81e84f00ed8380570ab33a19fed0547201"),
OutputVersion: 4,
OutputPath: "m/schema:1\\'/recovery:1\\'/change:0/3",
MerkleTree: d(""),
HtlcBlock: d(""),
ConfirmationTarget: 0,
}
userKey, _ := NewHDPrivateKeyFromString("tprv8eNitriyeyGgaAe7teh17j8mvqN3MuzkFy5TzdfS4KUATjgdP29jN7w9A8iQ5PDUZMqsb2aiJjEgjuPGCRjoDbJsCZ5iFGpb4uJCXkksjXM", "m/schema:1'/recovery:1'", network)
muunKey, _ := NewHDPublicKeyFromString("tpubDBYMnFoxYLdMBZThTk4uARTe4kGPeEYWdKcaEzaUxt1cesetnxtTqmAxVkzDRou51emWytommyLWcF91SdF5KecA6Ja8oHK1FF7d5U2hMxX", "m/schema:1'/recovery:1'", network)
invoice := &InvoiceSecrets{
preimage: d("52441108d7144b82ed13a18b7572fa78fa6f6a3f85fdbf4752dcce985430e43c"),
paymentSecret: d("79d011595d443897b46c2811bd4e5aa7f3fa225b880249edb64b52601aa7f963"),
keyPath: "m/schema:1'/recovery:1'/invoices:4/1159744029/738246992",
PaymentHash: d("31b35302d3e842a363f8992e423910bfb655b9cd6325b67f5c469fa8f2c4e55b"),
IdentityKey: nil,
UserHtlcKey: nil,
MuunHtlcKey: nil,
ShortChanId: 123,
}
PersistInvoiceSecrets(&InvoiceSecretsList{secrets: []*InvoiceSecrets{invoice}})
result, err := swap.Fulfill(data, userKey, muunKey, network)
if err != nil {
t.Fatal(err)
}
htlcTx := wire.NewMsgTx(2)
htlcTx.Deserialize(bytes.NewReader(swap.Htlc.HtlcTx))
signedTx := wire.NewMsgTx(2)
signedTx.Deserialize(bytes.NewReader(result.FulfillmentTx))
verifyInput(t, signedTx, hex.EncodeToString(swap.Htlc.HtlcTx), 1, 0)
}
func TestFulfillFullDebt(t *testing.T) {
setup()
network := Regtest()
userKey, _ := NewHDPrivateKey(randomBytes(32), network)
userKey.Path = "m/schema:1'/recovery:1'"
muunKey, _ := NewHDPrivateKey(randomBytes(32), network)
muunKey.Path = "m/schema:1'/recovery:1'"
secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
panic(err)
}
err = PersistInvoiceSecrets(secrets)
if err != nil {
panic(err)
}
invoice := secrets.Get(0)
swap := &IncomingSwap{
PaymentHash: invoice.PaymentHash,
}
result, err := swap.FulfillFullDebt()
if err != nil {
t.Fatal(err)
}
if result.FulfillmentTx != nil {
t.Fatal("expected FulfillmentTx to be nil")
}
if result.Preimage == nil {
t.Fatal("expected preimage to be non-nil")
}
}
func createSphinxPacket(nodePublicKey *btcec.PublicKey, paymentHash, paymentSecret []byte, amt, lockTime int64) []byte {
var paymentPath sphinx.PaymentPath
paymentPath[0].NodePub = *nodePublicKey
var secret [32]byte
copy(secret[:], paymentSecret)
uintAmount := uint64(amt * 1000) // msat are expected
uintLocktime := uint32(lockTime)
tlvRecords := []tlv.Record{
record.NewAmtToFwdRecord(&uintAmount),
record.NewLockTimeRecord(&uintLocktime),
record.NewMPP(lnwire.MilliSatoshi(uintAmount), secret).Record(),
}
b := &bytes.Buffer{}
tlv.MustNewStream(tlvRecords...).Encode(b)
hopPayload, err := sphinx.NewHopPayload(nil, b.Bytes())
if err != nil {
panic(err)
}
paymentPath[0].HopPayload = hopPayload
ephemeralKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
panic(err)
}
pkt, err := sphinx.NewOnionPacket(
&paymentPath, ephemeralKey, paymentHash, sphinx.BlankPacketFiller)
if err != nil {
panic(err)
}
var buf bytes.Buffer
err = pkt.Encode(&buf)
if err != nil {
panic(err)
}
return buf.Bytes()
}
func createMppSphinxPacket(
nodePublicKey *btcec.PublicKey,
paymentHash, paymentSecret []byte,
amt, lockTime int64,
) []byte {
var paymentPath sphinx.PaymentPath
paymentPath[0].NodePub = *nodePublicKey
var secret [32]byte
copy(secret[:], paymentSecret)
uintAmount := uint64(amt * 1000) // msat are expected
uintLocktime := uint32(lockTime)
uintFwdAmount := uintAmount / 2
tlvRecords := []tlv.Record{
record.NewAmtToFwdRecord(&uintFwdAmount),
record.NewLockTimeRecord(&uintLocktime),
record.NewMPP(lnwire.MilliSatoshi(uintAmount), secret).Record(),
}
b := &bytes.Buffer{}
tlv.MustNewStream(tlvRecords...).Encode(b)
hopPayload, err := sphinx.NewHopPayload(nil, b.Bytes())
if err != nil {
panic(err)
}
paymentPath[0].HopPayload = hopPayload
ephemeralKey, err := btcec.NewPrivateKey(btcec.S256())
if err != nil {
panic(err)
}
pkt, err := sphinx.NewOnionPacket(
&paymentPath, ephemeralKey, paymentHash, sphinx.BlankPacketFiller)
if err != nil {
panic(err)
}
var buf bytes.Buffer
err = pkt.Encode(&buf)
if err != nil {
panic(err)
}
return buf.Bytes()
}
func newAddressAt(userKey, muunKey *HDPrivateKey, keyPath string, network *Network) btcutil.Address {
userPublicKey, err := userKey.PublicKey().DeriveTo(keyPath)
if err != nil {
panic(err)
}
muunPublicKey, err := muunKey.PublicKey().DeriveTo(keyPath)
if err != nil {
panic(err)
}
muunAddr, err := CreateAddressV4(userPublicKey, muunPublicKey)
if err != nil {
panic(err)
}
addr, err := btcutil.DecodeAddress(muunAddr.Address(), network.network)
if err != nil {
panic(err)
}
return addr
}
func serializeTx(tx *wire.MsgTx) []byte {
var buf bytes.Buffer
err := tx.Serialize(&buf)
if err != nil {
panic(err)
}
return buf.Bytes()
}
func getInvoiceSecrets(invoice string, userKey *HDPrivateKey) (paymentHash []byte, paymentSecret []byte, identityKey *btcec.PublicKey) {
db, err := openDB()
if err != nil {
panic(err)
}
defer db.Close()
payReq, err := zpay32.Decode(invoice, network.network)
if err != nil {
panic(err)
}
dbInvoice, err := db.FindByPaymentHash(payReq.PaymentHash[:])
if err != nil {
panic(err)
}
paymentHash = payReq.PaymentHash[:]
paymentSecret = dbInvoice.PaymentSecret
keyPath := hdpath.MustParse(dbInvoice.KeyPath).Child(identityKeyChildIndex)
key, err := userKey.DeriveTo(keyPath.String())
if err != nil {
panic(err)
}
identityKey, err = key.key.ECPubKey()
if err != nil {
panic(err)
}
return
}

14
libwallet/init_test.go Normal file
View File

@ -0,0 +1,14 @@
package libwallet
import "io/ioutil"
func setup() {
dir, err := ioutil.TempDir("", "libwallet")
if err != nil {
panic(err)
}
Init(&Config{
DataDir: dir,
})
}

View File

215
libwallet/invoice_test.go Executable file
View File

@ -0,0 +1,215 @@
package libwallet
import (
"encoding/hex"
"reflect"
"testing"
)
func TestParseInvoice(t *testing.T) {
const (
invoice = "lnbcrt1pwtpd4xpp55meuklpslk5jtxytyh7u2q490c2xhm68dm3a94486zntsg7ad4vsdqqcqzys763w70h39ze44ngzhdt2mag84wlkefqkphuy7ssg4la5gt9vcpmqts00fnapf8frs928mc5ujfutzyu8apkezhrfvydx82l40w0fckqqmerzjc"
invoiceWithAmount = "lnbcrt10u1pwtpd4jpp5lh0p9amq02xel0gduna95ta5ve9q5dwyk8tglvpa258yzzvcgynsdqqcqzysrukfteknjzcqpu8kfnm76dhdtnkmyr3j42xrl89axhqxmpgusyqhn28u2uaave3nr8sk3mg5nug6t8hcnj2aw8t2l5wtksh6w0yyntgqjrrgqk"
invoiceWithDescription = "lnbcrt1pwtpdh7pp5celcayxvuw9pm9f8420n2dyd3css8ahzlr4nl69uczhf2sf99ydqdqswe5hvcfqwpjhymmwcqzysx7gwcf9a559rxrah9yp0u7dnk4vuvq2ywy6dyqtwzna9c92q058qppmv9p094vq9g6nv46d3sc7jd8faglzjj2h0w7j06wcu2h3e27cqc5zm4d"
invoiceWithFallbackAdrr = "lnbcrt1pwtpduxpp57xglq4thtrerzzxt8wzg4wresfclewh8pk8xghahwq8kgek3qslqdqqcqzysfppqhv0a0uhrt2crdehgfge8e8e6texw3q4hpmge888yuu6076utcrhgc97wu7vydmudyagkz25ahuyp4fqrc9e945ff248cpa3krn7vvgcqq6spyuqltd245sjvwh23gz220cegadspkn3lx0"
invoiceHashHex = "a6f3cb7c30fda925988b25fdc502a57e146bef476ee3d2d6a7d0a6b823dd6d59"
invoiceWithAmountHashHex = "fdde12f7607a8d9fbd0de4fa5a2fb4664a0a35c4b1d68fb03d550e4109984127"
invoiceWithDescriptionHashHex = "c67f8e90cce38a1d9527aa9f35348d8e2103f6e2f8eb3fe8bcc0ae954125291a"
invoiceWithFallbackAddrHashHex = "f191f0557758f23108cb3b848ab8798271fcbae70d8e645fb7700f6466d1043e"
invoiceDestinationHex = "028cfad4e092191a41f081bedfbe5a6e8f441603c78bf9001b8fb62ac0858f20edasd"
)
invoiceDestination, _ := hex.DecodeString(invoiceDestinationHex)
invoicePaymentHash := make([]byte, 32)
hex.Decode(invoicePaymentHash[:], []byte(invoiceHashHex))
invoiceWithAmountPaymentHash := make([]byte, 32)
hex.Decode(invoiceWithAmountPaymentHash[:], []byte(invoiceWithAmountHashHex))
invoiceWithDescriptionPaymentHash := make([]byte, 32)
hex.Decode(invoiceWithDescriptionPaymentHash[:], []byte(invoiceWithDescriptionHashHex))
invoiceWithFallbackAddrPaymentHash := make([]byte, 32)
hex.Decode(invoiceWithFallbackAddrPaymentHash[:], []byte(invoiceWithFallbackAddrHashHex))
fallbackAddr, _ := GetPaymentURI("bcrt1qhv0a0uhrt2crdehgfge8e8e6texw3q4has8jh7", network)
type args struct {
invoice string
network *Network
}
tests := []struct {
name string
args args
want *Invoice
wantErr bool
}{
{
name: "simple invoice",
args: args{
invoice: invoice,
network: network,
},
want: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
},
},
{
name: "simple invoice with scheme",
args: args{
invoice: lightningScheme + invoice,
network: network,
},
want: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
},
},
{
name: "simple invoice with uppercased scheme",
args: args{
invoice: "LIGHTNING:" + invoice,
network: network,
},
want: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
},
},
{
// -amt 1000
name: "invoice with amount",
args: args{
invoice: invoiceWithAmount,
network: network,
},
want: &Invoice{
RawInvoice: invoiceWithAmount,
FallbackAddress: nil,
Network: network,
MilliSat: "1000000",
Sats: 1000,
Destination: invoiceDestination,
PaymentHash: invoiceWithAmountPaymentHash,
Description: "",
},
},
{
// "viva peron"
name: "invoice with description",
args: args{
invoice: invoiceWithDescription,
network: network,
},
want: &Invoice{
RawInvoice: invoiceWithDescription,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoiceWithDescriptionPaymentHash,
Description: "viva peron",
},
},
{
// addr bcrt1qhv0a0uhrt2crdehgfge8e8e6texw3q4has8jh7
name: "invoice with fallback address",
args: args{
invoice: invoiceWithFallbackAdrr,
network: network,
},
want: &Invoice{
RawInvoice: invoiceWithFallbackAdrr,
FallbackAddress: fallbackAddr,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoiceWithFallbackAddrPaymentHash,
Description: "",
},
},
{
name: "invoice with invalid fallback address",
args: args{
invoice: "lnbcrt1pwtpduxpp57xglq4thtrerzzxt8wzg4wresfclewh8pk8xghahwq8kgek3qslqdqqcqzysfppqhv0a0uhrt2crdehgfge8e8e6texw3q4hpmge888yuu6076utcrhgc97wu7vydmudyagkz25ahuyp4fqrc9e945ff248cpa3krn7vvgcqq6spyuqltd245sjvwh23gz220cegadspkn3lx0",
network: Mainnet(),
},
wantErr: true,
},
{
name: "malformed invoice",
args: args{
invoice: "asdasd",
network: network,
},
wantErr: true,
},
{
name: "simple invoice with muun scheme",
args: args{
invoice: muunScheme + invoice,
network: network,
},
want: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
},
},
{
name: "simple invoice with muun:// scheme",
args: args{
invoice: muunScheme + "//" + invoice,
network: network,
},
want: &Invoice{
RawInvoice: invoice,
FallbackAddress: nil,
Network: network,
MilliSat: "",
Destination: invoiceDestination,
PaymentHash: invoicePaymentHash,
Description: "",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseInvoice(tt.args.invoice, tt.args.network)
if (err != nil) != tt.wantErr {
t.Errorf("ParseInvoice() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil {
// expiry is relative to now, so ignore it
got.Expiry = 0
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseInvoice() = %v, want %v", got, tt.want)
}
})
}
}

276
libwallet/invoices_test.go Normal file
View File

@ -0,0 +1,276 @@
package libwallet
import (
"bytes"
"encoding/hex"
"encoding/json"
"testing"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32"
)
func TestInvoiceSecrets(t *testing.T) {
setup()
network := Regtest()
userKey, _ := NewHDPrivateKey(randomBytes(32), network)
userKey.Path = "m/schema:1'/recovery:1'"
muunKey, _ := NewHDPrivateKey(randomBytes(32), network)
muunKey.Path = "m/schema:1'/recovery:1'"
routeHints := &RouteHints{
Pubkey: "03c48d1ff96fa32e2776f71bba02102ffc2a1b91e2136586418607d32e762869fd",
FeeBaseMsat: 1000,
FeeProportionalMillionths: 1000,
CltvExpiryDelta: 8,
}
secrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
t.Fatal(err)
}
if secrets.Length() != 5 {
t.Fatalf("expected 5 new secrets, got %d", secrets.Length())
}
err = PersistInvoiceSecrets(secrets)
if err != nil {
t.Fatal(err)
}
t.Run("generating more invoices", func(t *testing.T) {
// Make sure the secrets list is already topped up
_, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
t.Fatal(err)
}
// try to generate more secrets
moreSecrets, err := GenerateInvoiceSecrets(userKey.PublicKey(), muunKey.PublicKey())
if err != nil {
t.Fatal(err)
}
if moreSecrets.Length() != 0 {
t.Fatal("expected no new secrets to be created")
}
})
t.Run("create an invoice", func(t *testing.T) {
builder := &InvoiceBuilder{}
builder.Network(network)
builder.UserKey(userKey)
builder.AddRouteHints(routeHints)
builder.AmountSat(1000)
builder.Description("hello world")
invoice, err := builder.Build()
if err != nil {
t.Fatal(err)
}
if invoice == "" {
t.Fatal("expected non-empty invoice string")
}
payreq, err := zpay32.Decode(invoice, network.network)
if err != nil {
t.Fatal(err)
}
if !payreq.Features.HasFeature(lnwire.TLVOnionPayloadOptional) {
t.Fatal("expected invoice to have var onion optin feature")
}
if !payreq.Features.HasFeature(lnwire.PaymentAddrOptional) {
t.Fatal("expected invoice to have payment secret feature")
}
if payreq.MilliSat.ToSatoshis() != btcutil.Amount(1000) {
t.Fatalf("expected invoice amount to be 1000 sats, got %v", payreq.MilliSat)
}
if payreq.Description == nil || *payreq.Description != "hello world" {
t.Fatalf("expected payment description to match, got %v", payreq.Description)
}
if payreq.MinFinalCLTVExpiry() != 72 {
t.Fatalf("expected min final CLTV expiry to be 72, got %v", payreq.MinFinalCLTVExpiry())
}
if payreq.PaymentAddr == nil {
t.Fatalf("expected payment addr to be non-nil")
}
if len(payreq.RouteHints) == 0 {
t.Fatalf("expected invoice to contain route hints")
}
hopHints := payreq.RouteHints[0]
if len(hopHints) != 1 {
t.Fatalf("expected invoice route hints to contain exactly 1 hop hint")
}
if hopHints[0].ChannelID&(1<<63) == 0 {
t.Fatal("invalid short channel id in hophints")
}
if hopHints[0].FeeBaseMSat != 1000 {
t.Fatalf("expected fee base to be 1000 msat, got %v instead", hopHints[0].FeeBaseMSat)
}
if hopHints[0].FeeProportionalMillionths != 1000 {
t.Fatalf("expected fee proportional millionths to be 1000, got %v instead", hopHints[0].FeeProportionalMillionths)
}
if hopHints[0].CLTVExpiryDelta != 8 {
t.Fatalf("expected CLTV expiry delta to be 8, got %v instead", hopHints[0].CLTVExpiryDelta)
}
metadata, err := GetInvoiceMetadata(payreq.PaymentHash[:])
if err != nil {
t.Fatalf("expected invoice to contain metadata, got error: %v", err)
}
decryptedMetadata, err := userKey.Decrypter().Decrypt(metadata)
if err != nil {
t.Fatalf("expected metadata to decrypt correctly, got error: %v", err)
}
var jsonMetadata OperationMetadata
err = json.NewDecoder(bytes.NewReader(decryptedMetadata)).Decode(&jsonMetadata)
if err != nil {
t.Fatal("expected metadata to parse correctly")
}
if jsonMetadata.Invoice == "" {
t.Fatal("expected metadata to contain a non-empty invoice")
}
})
t.Run("creating a 2nd invoice returns a different payment hash", func(t *testing.T) {
builder := &InvoiceBuilder{}
builder.Network(network)
builder.UserKey(userKey)
builder.AddRouteHints(routeHints)
invoice1, err := builder.Build()
if err != nil {
t.Fatal(err)
}
payreq1, err := zpay32.Decode(invoice1, network.network)
if err != nil {
t.Fatal(err)
}
invoice2, err := builder.Build()
if err != nil {
t.Fatal(err)
}
payreq2, err := zpay32.Decode(invoice2, network.network)
if err != nil {
t.Fatal(err)
}
if payreq1.PaymentHash == payreq2.PaymentHash {
t.Fatal("successive invoice payment hashes should be different")
}
})
t.Run("amountMsat gets stored", func(t *testing.T) {
builder := &InvoiceBuilder{}
builder.Network(network)
builder.UserKey(userKey)
builder.AddRouteHints(routeHints)
builder.AmountMSat(1001)
invoice3, err := builder.Build()
if err != nil {
t.Fatal(err)
}
payreq3, err := zpay32.Decode(invoice3, network.network)
if err != nil {
t.Fatal(err)
}
db, err := openDB()
if err != nil {
t.Fatal(err)
}
defer db.Close()
invoiceMetadata, err := db.FindByPaymentHash(payreq3.PaymentHash[:])
if err != nil {
t.Fatal(err)
}
// Note that we sent 1001 msats
if invoiceMetadata.AmountSat != 1 {
t.Fatalf("Expected persisted amount to 1 found %v", invoiceMetadata.AmountSat)
}
})
t.Run("two route hints are encoded", func(t *testing.T) {
builder := &InvoiceBuilder{}
invoice, err := builder.
Network(network).
UserKey(userKey).
AddRouteHints(routeHints).
AddRouteHints(&RouteHints{
Pubkey: "03c48d1ff96fa32e2776f71bba02102ffc2a1b91e2136586418607d32e762869ff",
FeeBaseMsat: 123,
FeeProportionalMillionths: 1,
CltvExpiryDelta: 23,
}).
Build()
if err != nil {
t.Fatal(err)
}
payreq, err := zpay32.Decode(invoice, network.network)
if err != nil {
t.Fatal(err)
}
if len(payreq.RouteHints) != 2 {
t.Fatalf("Expected there to be 2 route hints, found %v", len(payreq.RouteHints))
}
for i, hops := range payreq.RouteHints {
if len(hops) != 1 {
t.Fatalf(
"Expected hops for hint %v to be 1, found %v",
i,
len(hops),
)
}
hint := hops[0]
var expectedFeeBase, expectedProportional uint32
var expectedPubKey string
if hint.CLTVExpiryDelta == 23 {
// Second hint
expectedFeeBase = 123
expectedProportional = 1
expectedPubKey = "03c48d1ff96fa32e2776f71bba02102ffc2a1b91e2136586418607d32e762869ff"
} else if hint.CLTVExpiryDelta == uint16(routeHints.CltvExpiryDelta) {
// First hint
expectedFeeBase = uint32(routeHints.FeeBaseMsat)
expectedProportional = uint32(routeHints.FeeProportionalMillionths)
expectedPubKey = routeHints.Pubkey
} else {
t.Fatalf("Failed to match route hint %v: %v", i, hops)
}
if hint.ChannelID&(1<<63) == 0 {
t.Fatal("invalid short channel id in hophints")
}
if hint.FeeProportionalMillionths != expectedProportional {
t.Fatalf("Route hint %v proportional fee %v != %v", i, hint.FeeProportionalMillionths, expectedProportional)
}
if hint.FeeBaseMSat != expectedFeeBase {
t.Fatalf("Route hint %v base fee %v != %v", i, hint.FeeBaseMSat, expectedFeeBase)
}
pubKey := hex.EncodeToString(hint.NodeID.SerializeCompressed())
if pubKey != expectedPubKey {
t.Fatalf("Route hint %v pub key %v != %v", i, pubKey, expectedPubKey)
}
}
})
}
func TestGetInvoiceMetadataMissingHash(t *testing.T) {
setup()
_, err := GetInvoiceMetadata(randomBytes(32))
if err == nil {
t.Fatal("expected GetInvoiceMetadata to fail")
}
}

View File

@ -0,0 +1,87 @@
package keycrypt
import (
"bytes"
"os"
"testing"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
)
var (
key *hdkeychain.ExtendedKey // set by TestMain
path = "m/123'/1"
passphrase = "asdasdasd"
)
func TestEncrypt(t *testing.T) {
_, err := Encrypt(key, path, passphrase)
if err != nil {
t.Errorf("Encrypt() error = %v", err)
return
}
}
func TestEncryptDecrypt(t *testing.T) {
encrypted, err := Encrypt(key, path, passphrase)
if err != nil {
t.Fatalf("Encrypt() error = %v", err)
}
decryptedKey, decryptedPath, err := Decrypt(encrypted, passphrase)
if err != nil {
t.Fatalf("Encrypt() error = %v", err)
}
if decryptedKey.String() != key.String() {
t.Errorf("Encrypt() expected key %v got %v", key.String(), decryptedKey.String())
}
if decryptedPath != path {
t.Errorf("Encrypt() expected path %v got %v", path, decryptedPath)
}
}
func TestBadPassphrase(t *testing.T) {
encrypted, err := Encrypt(key, path, passphrase)
if err != nil {
t.Fatalf("Encrypt() error = %v", err)
}
_, _, err = Decrypt(encrypted, passphrase+"foo")
if err == nil {
t.Fatalf("expected decryption error")
}
}
func TestEncodeUTF16(t *testing.T) {
tests := []struct {
name string
input string
want []byte
}{
{name: "no data", input: "", want: nil},
{name: "one char", input: "a", want: []byte{0, 97}},
{name: "multi byte char", input: "€", want: []byte{0x20, 0xAC}},
{name: "complex string", input: "€aह", want: []byte{0x20, 0xAC, 0, 97, 0x09, 0x39}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := encodeUTF16(tt.input); !bytes.Equal(got, tt.want) {
t.Errorf("EncodeString() = %v, want %v", got, tt.want)
}
})
}
}
func TestMain(m *testing.M) {
var err error
key, err = hdkeychain.NewMaster(randomBytes(32), &chaincfg.MainNetParams)
if err != nil {
panic(err)
}
os.Exit(m.Run())
}

54
libwallet/keycrypter_test.go Executable file
View File

@ -0,0 +1,54 @@
package libwallet
import (
"testing"
)
func TestKeyCrypter(t *testing.T) {
key, _ := NewHDPrivateKey(randomBytes(16), Regtest())
key, _ = key.DeriveTo("m/123'/1")
testPassphrase := "asdasdasd"
t.Run("simple encrypt", func(t *testing.T) {
_, err := KeyEncrypt(key, testPassphrase)
if err != nil {
t.Errorf("KeyEncrypt() error = %v", err)
return
}
})
t.Run("encrypt & decrypt", func(t *testing.T) {
encrypted, err := KeyEncrypt(key, testPassphrase)
if err != nil {
t.Fatalf("KeyEncrypt() error = %v", err)
}
decrypted, err := KeyDecrypt(encrypted, testPassphrase, Regtest())
if err != nil {
t.Fatalf("KeyEncrypt() error = %v", err)
}
if decrypted.Key.String() != key.String() {
t.Errorf("KeyEncrypt() expected key %v got %v", key, decrypted.Key)
}
if decrypted.Path != key.Path {
t.Errorf("KeyEncrypt() expected path %v got %v", key.Path, decrypted.Path)
}
})
t.Run("bad passphrase", func(t *testing.T) {
encrypted, err := KeyEncrypt(key, testPassphrase)
if err != nil {
t.Fatalf("KeyEncrypt() error = %v", err)
}
_, err = KeyDecrypt(encrypted, testPassphrase+"foo", Regtest())
if err == nil {
t.Fatalf("expected decryption error")
}
})
}

View File

@ -0,0 +1,771 @@
package lnurl
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/fiatjaf/go-lnurl"
"github.com/lightningnetwork/lnd/lnwire"
)
func TestWithdraw(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestWithdrawWithCompatibilityTag(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdraw",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestDecodeError(t *testing.T) {
qr := "lightning:abcde"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrDecode {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusContacting {
t.Fatal("should not contact server")
}
})
}
func TestWrongTagError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/channelRequest", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
Tag: "channelRequest",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/channelRequest", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrWrongTag {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not create invoice")
}
})
}
func TestUnreachableError(t *testing.T) {
originalTimeout := httpClient.Timeout
httpClient.Timeout = 1 * time.Second
defer func() {
httpClient.Timeout = originalTimeout
}()
// LNURL QR pointing to a non-responding domain
qr := "LIGHTNING:LNURL1DP68GURN8GHJ7ARGD9EJUER0D4SKJM3WV3HK2UEWDEHHGTN90P5HXAPWV4UXZMTSD3JJUCM0D5LHXETRWFJHG0F3XGENGDGQ8EH52"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrUnreachable {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not create invoice")
}
})
}
func TestServiceError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusError,
Reason: "something something",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusReceiving {
t.Fatal("should not reach receiving status")
}
})
}
func TestInvalidResponseError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("foobar"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrInvalidResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func TestUnsafeURLError(t *testing.T) {
qr, _ := encode("http://localhost/withdraw")
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, false, func(e *Event) {
if e.Code < 100 && e.Code != ErrUnsafeURL {
t.Fatalf("unexpected error code: %v", e.Code)
}
})
}
func TestWrongTagInQR(t *testing.T) {
// LNURL QR with a `login` tag value in its query params
qr := "lightning:lnurl1dp68gurn8ghj7mrww4exctt5dahkccn00qhxget8wfjk2um0veax2un09e3k7mf0w5lhgct884kx7emfdcnxkvfa8qexxc35vymnxcf5xumkxvfsv4snxwph8qunzv3hxesnyv3jvv6nyv3e8yuxzvnpv4skvepnxg6rwv34xqck2c3sxcerzdpnv56r2dss2vt96"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrWrongTag {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusContacting {
t.Fatal("should not contact server")
}
})
}
func TestOnionLinkNotSupported(t *testing.T) {
qr := "LNURL1DP68GUP69UHKVMM0VFSHYTN0DE5K7MSHXU8YD"
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrTorNotSupported {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusContacting {
t.Fatal("should not contact server")
}
})
}
func TestExpiredCheck(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: "ERROR",
Reason: "something something Expired blabla",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrRequestExpired {
t.Fatalf("unexpected error code: %v", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not create invoice")
}
})
}
func TestNoAvailableBalance(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 0,
MinWithdrawable: 0,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrNoAvailableBalance {
t.Fatalf("unexpected error code: %d", e.Code)
}
if e.Code == StatusInvoiceCreated {
t.Fatalf("should not create invoice")
}
})
}
func TestNoRouteCheck(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusError,
Reason: "Unable to pay LN Invoice: FAILURE_REASON_NO_ROUTE",
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
return "12345", nil
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 && e.Code != ErrNoRoute {
t.Fatalf("unexpected error code: %d", e.Code)
}
})
}
func TestExtraQueryParams(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete?foo=bar",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("foo") != "bar" {
t.Fatalf("Expected foo=bar in query params. Got URL: %v", r.URL.String())
}
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestStringlyTypedNumberFields(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&struct {
Response
Tag string `json:"tag"`
K1 string `json:"k1"`
Callback string `json:"callback"`
MaxWithdrawable string `json:"maxWithdrawable"`
MinWithdrawable string `json:"minWithdrawable"`
DefaultDescription string `json:"defaultDescription"`
}{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: "1000000",
MinWithdrawable: "0",
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&Response{
Status: StatusOK,
})
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
if amt != 1_000_000 {
t.Fatalf("unexpected invoice amount: %v", amt)
}
if desc != "Withdraw from Lapp" {
t.Fatalf("unexpected invoice description: %v", desc)
}
if host != "127.0.0.1" {
t.Fatalf("unexpected host: %v", host)
}
return "12345", nil
}
var err string
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
err = e.Message
}
})
if err != "" {
t.Fatalf("expected withdraw to succeed, got: %v", err)
}
}
func TestErrorContainsResponseBody(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400)
w.Write([]byte("this is a custom error response"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrInvalidResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if !strings.Contains(e.Message, "this is a custom error response") {
t.Fatalf("expected error message to contain response, got `%s`", e.Message)
}
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func TestErrorContainsResponseBodyForFinishRequest(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&WithdrawResponse{
K1: "foobar",
Callback: "http://" + r.Host + "/withdraw/complete",
MaxWithdrawable: 1_000_000,
DefaultDescription: "Withdraw from Lapp",
Tag: "withdrawRequest",
})
})
mux.HandleFunc("/withdraw/complete", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400)
w.Write([]byte("this is a custom error response"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
return "12345", nil
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrInvalidResponse {
t.Fatalf("unexpected error code: %v", e.Code)
}
if !strings.Contains(e.Message, "this is a custom error response") {
t.Fatalf("expected error message to contain response, got `%s`", e.Message)
}
}
if e.Code == StatusReceiving {
t.Fatal("should not reach receiving status")
}
})
}
func TestForbidden(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(403)
w.Write([]byte("Forbidden"))
})
server := httptest.NewServer(mux)
defer server.Close()
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrForbidden {
t.Fatalf("unexpected error code: %v", e.Code)
}
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func TestZebedee403MapsToCountryNotSupported(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/withdraw/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(403)
w.Write([]byte("Forbidden"))
})
server := httptest.NewServer(mux)
defer server.Close()
// Super ugly hack to emulate that local endpoint is zebedee
zebedeeHost = "127.0.0.1"
t.Cleanup(func() {
zebedeeHost = zebedeeHostConst // after test reset to its original value
})
qr, _ := encode(fmt.Sprintf("%s/withdraw", server.URL))
createInvoiceFunc := func(amt lnwire.MilliSatoshi, desc string, host string) (string, error) {
panic("should not reach here")
}
Withdraw(qr, createInvoiceFunc, true, func(e *Event) {
if e.Code < 100 {
if e.Code != ErrCountryNotSupported {
t.Fatalf("unexpected error code: %v", e.Code)
}
}
if e.Code == StatusInvoiceCreated {
t.Fatal("should not reach invoice creation")
}
})
}
func encode(url string) (string, error) {
return lnurl.LNURLEncode(url)
}
func TestWithdrawResponse_Validate(t *testing.T) {
type fields struct {
Response Response
Tag string
K1 string
Callback string
MaxWithdrawable stringOrNumber
MinWithdrawable stringOrNumber
DefaultDescription string
}
errorResponse := func(reason string) fields {
return fields{
Response: Response{
Status: StatusError,
Reason: reason,
},
}
}
tests := []struct {
name string
fields fields
want int
}{
{
"invalid tag",
fields{Tag: "blebidy"},
ErrWrongTag,
},
{
"negative withdraw",
fields{MaxWithdrawable: -1, Tag: "withdraw"},
ErrNoAvailableBalance,
},
{
"valid",
fields{
Response: Response{
Status: StatusOK,
},
Tag: "withdraw",
MaxWithdrawable: 10,
},
ErrNone,
},
{
"already being processed",
errorResponse("This Withdrawal Request is already being processed by another wallet"),
ErrAlreadyUsed,
},
{
"can only be processed only once",
errorResponse("This Withdrawal Request can only be processed once"),
ErrAlreadyUsed,
},
{
"withdraw is spent",
errorResponse("Withdraw is spent"),
ErrAlreadyUsed,
},
{
"withdraw link is empty",
errorResponse("Withdraw link is empty"),
ErrAlreadyUsed,
},
{
"has already been used",
errorResponse("This LNURL has already been used"),
ErrAlreadyUsed,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wr := &WithdrawResponse{
Response: tt.fields.Response,
Tag: tt.fields.Tag,
K1: tt.fields.K1,
Callback: tt.fields.Callback,
MaxWithdrawable: tt.fields.MaxWithdrawable,
MinWithdrawable: tt.fields.MinWithdrawable,
DefaultDescription: tt.fields.DefaultDescription,
}
got, _ := wr.Validate()
if got != tt.want {
t.Errorf("Validate() got = %v, want %v", got, tt.want)
}
})
}
}
func TestValidate(t *testing.T) {
type args struct {
qr string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "plain",
args: args{"LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "lightning scheme",
args: args{"lightning:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "HTTP fallback scheme",
args: args{"https://example.com/?lightning=LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun scheme",
args: args{"muun:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun scheme with double slashes",
args: args{"muun://LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "lightning scheme with double slashes",
args: args{"lightning://LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun + lightning schemes",
args: args{"muun:lightning:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
{
name: "muun + lightning schemes with double slashes",
args: args{"muun://lightning:LNURL1DP68GUP69UHKCMMRV9KXSMMNWSARWVPCXQHKCMN4WFKZ7AMFW35XGUNPWULHXETRWFJHG0F3XGENGDGK59DKV"},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Validate(tt.args.qr); got != tt.want {
t.Errorf("Validate() = %v, want %v", got, tt.want)
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More