mirror of
https://github.com/muun/recovery.git
synced 2025-07-20 22:11:16 -04:00
Update project structure and build process
This commit is contained in:
parent
124e9fa1bc
commit
d9f3e925a4
40
.github/workflows/pr.yml
vendored
40
.github/workflows/pr.yml
vendored
@ -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
|
||||
|
2
BUILD.md
2
BUILD.md
@ -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:
|
||||
|
0
vendor/github.com/muun/libwallet/.gitignore → libwallet/.gitignore
vendored
Normal file → Executable file
0
vendor/github.com/muun/libwallet/.gitignore → libwallet/.gitignore
vendored
Normal file → Executable file
0
vendor/github.com/muun/libwallet/V1.go → libwallet/V1.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/V1.go → libwallet/V1.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/V2.go → libwallet/V2.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/V2.go → libwallet/V2.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/V3.go → libwallet/V3.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/V3.go → libwallet/V3.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/V4.go → libwallet/V4.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/V4.go → libwallet/V4.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/address.go → libwallet/address.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/address.go → libwallet/address.go
Normal file → Executable file
321
libwallet/address_test.go
Executable file
321
libwallet/address_test.go
Executable 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)
|
||||
}
|
||||
}
|
30
libwallet/addresses/addresses_test.go
Normal file
30
libwallet/addresses/addresses_test.go
Normal 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
57
libwallet/addresses/v2_test.go
Executable 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
54
libwallet/addresses/v3_test.go
Executable 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
52
libwallet/addresses/v4_test.go
Executable 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
34
libwallet/addresses/v5_test.go
Normal file
34
libwallet/addresses/v5_test.go
Normal 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)
|
||||
}
|
||||
}
|
0
vendor/github.com/muun/libwallet/aescbc/aescbc.go → libwallet/aescbc/aescbc.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/aescbc/aescbc.go → libwallet/aescbc/aescbc.go
Normal file → Executable file
36
libwallet/aescbc/aescbc_test.go
Normal file
36
libwallet/aescbc/aescbc_test.go
Normal 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
|
||||
}
|
0
vendor/github.com/muun/libwallet/bip70.proto → libwallet/bip70.proto
Normal file → Executable file
0
vendor/github.com/muun/libwallet/bip70.proto → libwallet/bip70.proto
Normal file → Executable file
66
libwallet/btcsuitew/bech32m/bech32m_test.go
Normal file
66
libwallet/btcsuitew/bech32m/bech32m_test.go
Normal 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)
|
||||
}
|
||||
}
|
139
libwallet/btcsuitew/txscriptw/script_test.go
Normal file
139
libwallet/btcsuitew/txscriptw/script_test.go
Normal 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
|
||||
}
|
0
vendor/github.com/muun/libwallet/challenge_keys.go → libwallet/challenge_keys.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/challenge_keys.go → libwallet/challenge_keys.go
Normal file → Executable file
160
libwallet/challenge_keys_test.go
Executable file
160
libwallet/challenge_keys_test.go
Executable 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)
|
||||
}
|
||||
}
|
0
vendor/github.com/muun/libwallet/challenge_public_key.go → libwallet/challenge_public_key.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/challenge_public_key.go → libwallet/challenge_public_key.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/emergency_kit.go → libwallet/emergency_kit.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/emergency_kit.go → libwallet/emergency_kit.go
Normal file → Executable file
15
libwallet/emergency_kit_test.go
Executable file
15
libwallet/emergency_kit_test.go
Executable 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)
|
||||
}
|
||||
}
|
34
libwallet/emergencykit/descriptors_test.go
Normal file
34
libwallet/emergencykit/descriptors_test.go
Normal 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)
|
||||
}
|
||||
}
|
105
libwallet/emergencykit/emergencykit_test.go
Normal file
105
libwallet/emergencykit/emergencykit_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
121
libwallet/emergencykit/metadata_test.go
Normal file
121
libwallet/emergencykit/metadata_test.go
Normal 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
|
||||
`
|
0
vendor/github.com/muun/libwallet/encodings.go → libwallet/encodings.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/encodings.go → libwallet/encodings.go
Normal file → Executable file
46
libwallet/encodings_test.go
Executable file
46
libwallet/encodings_test.go
Executable 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
0
vendor/github.com/muun/libwallet/encrypt.go → libwallet/encrypt.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/encrypt.go → libwallet/encrypt.go
Normal file → Executable file
252
libwallet/encrypt_test.go
Executable file
252
libwallet/encrypt_test.go
Executable 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
211
libwallet/features_test.go
Normal 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
363
libwallet/fees/fees_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
82
libwallet/hdpath/hdpath_test.go
Normal file
82
libwallet/hdpath/hdpath_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
0
vendor/github.com/muun/libwallet/hdprivatekey.go → libwallet/hdprivatekey.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/hdprivatekey.go → libwallet/hdprivatekey.go
Normal file → Executable file
294
libwallet/hdprivatekey_test.go
Executable file
294
libwallet/hdprivatekey_test.go
Executable 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)
|
||||
}
|
||||
}
|
0
vendor/github.com/muun/libwallet/hdpublickey.go → libwallet/hdpublickey.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/hdpublickey.go → libwallet/hdpublickey.go
Normal file → Executable file
36
libwallet/hdpublickey_test.go
Executable file
36
libwallet/hdpublickey_test.go
Executable 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)
|
||||
}
|
||||
}
|
791
libwallet/incoming_swap_test.go
Normal file
791
libwallet/incoming_swap_test.go
Normal 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
14
libwallet/init_test.go
Normal 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,
|
||||
})
|
||||
}
|
0
vendor/github.com/muun/libwallet/invoice.go → libwallet/invoice.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/invoice.go → libwallet/invoice.go
Normal file → Executable file
215
libwallet/invoice_test.go
Executable file
215
libwallet/invoice_test.go
Executable 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
276
libwallet/invoices_test.go
Normal 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")
|
||||
}
|
||||
}
|
0
vendor/github.com/muun/libwallet/keycrypt/keycrypt.go → libwallet/keycrypt/keycrypt.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/keycrypt/keycrypt.go → libwallet/keycrypt/keycrypt.go
Normal file → Executable file
87
libwallet/keycrypt/keycrypt_test.go
Executable file
87
libwallet/keycrypt/keycrypt_test.go
Executable 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())
|
||||
}
|
0
vendor/github.com/muun/libwallet/keycrypter.go → libwallet/keycrypter.go
Normal file → Executable file
0
vendor/github.com/muun/libwallet/keycrypter.go → libwallet/keycrypter.go
Normal file → Executable file
54
libwallet/keycrypter_test.go
Executable file
54
libwallet/keycrypter_test.go
Executable 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")
|
||||
}
|
||||
})
|
||||
}
|
771
libwallet/lnurl/lnurl_test.go
Normal file
771
libwallet/lnurl/lnurl_test.go
Normal 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
Loading…
x
Reference in New Issue
Block a user