diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 7d70a1c..94a9a0d 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -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
diff --git a/BUILD.md b/BUILD.md
index 7922096..2c04bea 100644
--- a/BUILD.md
+++ b/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:
@@ -40,4 +40,4 @@ We use Docker for these builds to ensure they are reproducible.
For the 2.2 release, we had to disable reproducible builds for MacOS. The inclusion of C code for
the musig implementation made building the tool inside a Linux container extremely difficult. We'll
be moving the process to GitHub actions soon, which can be easily audited and can build natively on
-MacOS.
\ No newline at end of file
+MacOS.
diff --git a/go.work b/go.work
new file mode 100644
index 0000000..818fbfc
--- /dev/null
+++ b/go.work
@@ -0,0 +1,4 @@
+go 1.22.1
+
+use ./libwallet
+use ./recovery_tool
diff --git a/vendor/github.com/muun/libwallet/.gitignore b/libwallet/.gitignore
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/.gitignore
rename to libwallet/.gitignore
diff --git a/LICENSE b/libwallet/LICENSE
similarity index 100%
rename from LICENSE
rename to libwallet/LICENSE
diff --git a/vendor/github.com/muun/libwallet/README.md b/libwallet/README.md
similarity index 100%
rename from vendor/github.com/muun/libwallet/README.md
rename to libwallet/README.md
diff --git a/vendor/github.com/muun/libwallet/V1.go b/libwallet/V1.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/V1.go
rename to libwallet/V1.go
diff --git a/vendor/github.com/muun/libwallet/V2.go b/libwallet/V2.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/V2.go
rename to libwallet/V2.go
diff --git a/vendor/github.com/muun/libwallet/V3.go b/libwallet/V3.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/V3.go
rename to libwallet/V3.go
diff --git a/vendor/github.com/muun/libwallet/V4.go b/libwallet/V4.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/V4.go
rename to libwallet/V4.go
diff --git a/vendor/github.com/muun/libwallet/V5.go b/libwallet/V5.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/V5.go
rename to libwallet/V5.go
diff --git a/vendor/github.com/muun/libwallet/address.go b/libwallet/address.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/address.go
rename to libwallet/address.go
diff --git a/libwallet/address_test.go b/libwallet/address_test.go
new file mode 100755
index 0000000..49de037
--- /dev/null
+++ b/libwallet/address_test.go
@@ -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)
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/addresses/addresses.go b/libwallet/addresses/addresses.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/addresses/addresses.go
rename to libwallet/addresses/addresses.go
diff --git a/libwallet/addresses/addresses_test.go b/libwallet/addresses/addresses_test.go
new file mode 100644
index 0000000..f045050
--- /dev/null
+++ b/libwallet/addresses/addresses_test.go
@@ -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
+}
diff --git a/vendor/github.com/muun/libwallet/addresses/v1.go b/libwallet/addresses/v1.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/addresses/v1.go
rename to libwallet/addresses/v1.go
diff --git a/vendor/github.com/muun/libwallet/addresses/v2.go b/libwallet/addresses/v2.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/addresses/v2.go
rename to libwallet/addresses/v2.go
diff --git a/libwallet/addresses/v2_test.go b/libwallet/addresses/v2_test.go
new file mode 100755
index 0000000..3088884
--- /dev/null
+++ b/libwallet/addresses/v2_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/addresses/v3.go b/libwallet/addresses/v3.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/addresses/v3.go
rename to libwallet/addresses/v3.go
diff --git a/libwallet/addresses/v3_test.go b/libwallet/addresses/v3_test.go
new file mode 100755
index 0000000..5d5bd8b
--- /dev/null
+++ b/libwallet/addresses/v3_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/addresses/v4.go b/libwallet/addresses/v4.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/addresses/v4.go
rename to libwallet/addresses/v4.go
diff --git a/libwallet/addresses/v4_test.go b/libwallet/addresses/v4_test.go
new file mode 100755
index 0000000..b0293e3
--- /dev/null
+++ b/libwallet/addresses/v4_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/addresses/v5.go b/libwallet/addresses/v5.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/addresses/v5.go
rename to libwallet/addresses/v5.go
diff --git a/libwallet/addresses/v5_test.go b/libwallet/addresses/v5_test.go
new file mode 100644
index 0000000..0aefcb1
--- /dev/null
+++ b/libwallet/addresses/v5_test.go
@@ -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)
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/aescbc/aescbc.go b/libwallet/aescbc/aescbc.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/aescbc/aescbc.go
rename to libwallet/aescbc/aescbc.go
diff --git a/libwallet/aescbc/aescbc_test.go b/libwallet/aescbc/aescbc_test.go
new file mode 100644
index 0000000..5c2c5f5
--- /dev/null
+++ b/libwallet/aescbc/aescbc_test.go
@@ -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
+}
diff --git a/vendor/github.com/muun/libwallet/bip70.pb.go b/libwallet/bip70.pb.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/bip70.pb.go
rename to libwallet/bip70.pb.go
diff --git a/vendor/github.com/muun/libwallet/bip70.proto b/libwallet/bip70.proto
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/bip70.proto
rename to libwallet/bip70.proto
diff --git a/vendor/github.com/muun/libwallet/bridge.go b/libwallet/bridge.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/bridge.go
rename to libwallet/bridge.go
diff --git a/vendor/github.com/muun/libwallet/btcsuitew/bech32m/bech32m.go b/libwallet/btcsuitew/bech32m/bech32m.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/btcsuitew/bech32m/bech32m.go
rename to libwallet/btcsuitew/bech32m/bech32m.go
diff --git a/libwallet/btcsuitew/bech32m/bech32m_test.go b/libwallet/btcsuitew/bech32m/bech32m_test.go
new file mode 100644
index 0000000..d363551
--- /dev/null
+++ b/libwallet/btcsuitew/bech32m/bech32m_test.go
@@ -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)
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/btcsuitew/btcutilw/address.go b/libwallet/btcsuitew/btcutilw/address.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/btcsuitew/btcutilw/address.go
rename to libwallet/btcsuitew/btcutilw/address.go
diff --git a/vendor/github.com/muun/libwallet/btcsuitew/btcutilw/segwit.go b/libwallet/btcsuitew/btcutilw/segwit.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/btcsuitew/btcutilw/segwit.go
rename to libwallet/btcsuitew/btcutilw/segwit.go
diff --git a/vendor/github.com/muun/libwallet/btcsuitew/chainhashw/chainhashw.go b/libwallet/btcsuitew/chainhashw/chainhashw.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/btcsuitew/chainhashw/chainhashw.go
rename to libwallet/btcsuitew/chainhashw/chainhashw.go
diff --git a/vendor/github.com/muun/libwallet/btcsuitew/txscriptw/hashcache.go b/libwallet/btcsuitew/txscriptw/hashcache.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/btcsuitew/txscriptw/hashcache.go
rename to libwallet/btcsuitew/txscriptw/hashcache.go
diff --git a/vendor/github.com/muun/libwallet/btcsuitew/txscriptw/script.go b/libwallet/btcsuitew/txscriptw/script.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/btcsuitew/txscriptw/script.go
rename to libwallet/btcsuitew/txscriptw/script.go
diff --git a/libwallet/btcsuitew/txscriptw/script_test.go b/libwallet/btcsuitew/txscriptw/script_test.go
new file mode 100644
index 0000000..7a0fba1
--- /dev/null
+++ b/libwallet/btcsuitew/txscriptw/script_test.go
@@ -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
+}
diff --git a/vendor/github.com/muun/libwallet/btcsuitew/txscriptw/standard.go b/libwallet/btcsuitew/txscriptw/standard.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/btcsuitew/txscriptw/standard.go
rename to libwallet/btcsuitew/txscriptw/standard.go
diff --git a/vendor/github.com/muun/libwallet/challenge_keys.go b/libwallet/challenge_keys.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/challenge_keys.go
rename to libwallet/challenge_keys.go
diff --git a/libwallet/challenge_keys_test.go b/libwallet/challenge_keys_test.go
new file mode 100755
index 0000000..aa4fb7c
--- /dev/null
+++ b/libwallet/challenge_keys_test.go
@@ -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)
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/challenge_public_key.go b/libwallet/challenge_public_key.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/challenge_public_key.go
rename to libwallet/challenge_public_key.go
diff --git a/vendor/github.com/muun/libwallet/emergency_kit.go b/libwallet/emergency_kit.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/emergency_kit.go
rename to libwallet/emergency_kit.go
diff --git a/libwallet/emergency_kit_test.go b/libwallet/emergency_kit_test.go
new file mode 100755
index 0000000..3423426
--- /dev/null
+++ b/libwallet/emergency_kit_test.go
@@ -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)
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/emergencykit/content.go b/libwallet/emergencykit/content.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/emergencykit/content.go
rename to libwallet/emergencykit/content.go
diff --git a/vendor/github.com/muun/libwallet/emergencykit/css.go b/libwallet/emergencykit/css.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/emergencykit/css.go
rename to libwallet/emergencykit/css.go
diff --git a/vendor/github.com/muun/libwallet/emergencykit/descriptors.go b/libwallet/emergencykit/descriptors.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/emergencykit/descriptors.go
rename to libwallet/emergencykit/descriptors.go
diff --git a/libwallet/emergencykit/descriptors_test.go b/libwallet/emergencykit/descriptors_test.go
new file mode 100644
index 0000000..a69ab45
--- /dev/null
+++ b/libwallet/emergencykit/descriptors_test.go
@@ -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)
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/emergencykit/emergencykit.go b/libwallet/emergencykit/emergencykit.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/emergencykit/emergencykit.go
rename to libwallet/emergencykit/emergencykit.go
diff --git a/libwallet/emergencykit/emergencykit_test.go b/libwallet/emergencykit/emergencykit_test.go
new file mode 100644
index 0000000..23670fa
--- /dev/null
+++ b/libwallet/emergencykit/emergencykit_test.go
@@ -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, `
`) {
+ t.Fatal("expected output html to contain output descriptors")
+ }
+ if !strings.Contains(out.HTML, `wsh`) {
+ 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, ``) {
+ t.Fatal("expected output html to contain output descriptors")
+ }
+ if !strings.Contains(out.HTML, `wsh`) {
+ t.Fatal("expected output html to contain output descriptor scripts")
+ }
+ if !strings.Contains(out.HTML, `musig`) {
+ 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)
+ }
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/emergencykit/metadata.go b/libwallet/emergencykit/metadata.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/emergencykit/metadata.go
rename to libwallet/emergencykit/metadata.go
diff --git a/libwallet/emergencykit/metadata_test.go b/libwallet/emergencykit/metadata_test.go
new file mode 100644
index 0000000..92e24e4
--- /dev/null
+++ b/libwallet/emergencykit/metadata_test.go
@@ -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 `` 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
+`
diff --git a/vendor/github.com/muun/libwallet/encodings.go b/libwallet/encodings.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/encodings.go
rename to libwallet/encodings.go
diff --git a/libwallet/encodings_test.go b/libwallet/encodings_test.go
new file mode 100755
index 0000000..46d4c1d
--- /dev/null
+++ b/libwallet/encodings_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/encrypt.go b/libwallet/encrypt.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/encrypt.go
rename to libwallet/encrypt.go
diff --git a/libwallet/encrypt_test.go b/libwallet/encrypt_test.go
new file mode 100755
index 0000000..298b1de
--- /dev/null
+++ b/libwallet/encrypt_test.go
@@ -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")
+ }
+}
\ No newline at end of file
diff --git a/vendor/github.com/muun/libwallet/errors.go b/libwallet/errors.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/errors.go
rename to libwallet/errors.go
diff --git a/vendor/github.com/muun/libwallet/errors/errors.go b/libwallet/errors/errors.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/errors/errors.go
rename to libwallet/errors/errors.go
diff --git a/vendor/github.com/muun/libwallet/features.go b/libwallet/features.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/features.go
rename to libwallet/features.go
diff --git a/libwallet/features_test.go b/libwallet/features_test.go
new file mode 100644
index 0000000..a558585
--- /dev/null
+++ b/libwallet/features_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/fees.go b/libwallet/fees.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/fees.go
rename to libwallet/fees.go
diff --git a/vendor/github.com/muun/libwallet/fees/fees.go b/libwallet/fees/fees.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/fees/fees.go
rename to libwallet/fees/fees.go
diff --git a/libwallet/fees/fees_test.go b/libwallet/fees/fees_test.go
new file mode 100644
index 0000000..9244f4e
--- /dev/null
+++ b/libwallet/fees/fees_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/go.mod b/libwallet/go.mod
similarity index 100%
rename from vendor/github.com/muun/libwallet/go.mod
rename to libwallet/go.mod
diff --git a/vendor/github.com/muun/libwallet/go.sum b/libwallet/go.sum
similarity index 100%
rename from vendor/github.com/muun/libwallet/go.sum
rename to libwallet/go.sum
diff --git a/vendor/github.com/muun/libwallet/hashes.go b/libwallet/hashes.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/hashes.go
rename to libwallet/hashes.go
diff --git a/vendor/github.com/muun/libwallet/hdpath/hdpath.go b/libwallet/hdpath/hdpath.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/hdpath/hdpath.go
rename to libwallet/hdpath/hdpath.go
diff --git a/libwallet/hdpath/hdpath_test.go b/libwallet/hdpath/hdpath_test.go
new file mode 100644
index 0000000..5463cab
--- /dev/null
+++ b/libwallet/hdpath/hdpath_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/hdprivatekey.go b/libwallet/hdprivatekey.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/hdprivatekey.go
rename to libwallet/hdprivatekey.go
diff --git a/libwallet/hdprivatekey_test.go b/libwallet/hdprivatekey_test.go
new file mode 100755
index 0000000..255658a
--- /dev/null
+++ b/libwallet/hdprivatekey_test.go
@@ -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)
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/hdpublickey.go b/libwallet/hdpublickey.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/hdpublickey.go
rename to libwallet/hdpublickey.go
diff --git a/libwallet/hdpublickey_test.go b/libwallet/hdpublickey_test.go
new file mode 100755
index 0000000..8916a4d
--- /dev/null
+++ b/libwallet/hdpublickey_test.go
@@ -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)
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/incoming_swap.go b/libwallet/incoming_swap.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/incoming_swap.go
rename to libwallet/incoming_swap.go
diff --git a/libwallet/incoming_swap_test.go b/libwallet/incoming_swap_test.go
new file mode 100644
index 0000000..3b313ef
--- /dev/null
+++ b/libwallet/incoming_swap_test.go
@@ -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
+}
diff --git a/vendor/github.com/muun/libwallet/init.go b/libwallet/init.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/init.go
rename to libwallet/init.go
diff --git a/libwallet/init_test.go b/libwallet/init_test.go
new file mode 100644
index 0000000..a320116
--- /dev/null
+++ b/libwallet/init_test.go
@@ -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,
+ })
+}
diff --git a/vendor/github.com/muun/libwallet/invoice.go b/libwallet/invoice.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/invoice.go
rename to libwallet/invoice.go
diff --git a/libwallet/invoice_test.go b/libwallet/invoice_test.go
new file mode 100755
index 0000000..75a45b5
--- /dev/null
+++ b/libwallet/invoice_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/invoices.go b/libwallet/invoices.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/invoices.go
rename to libwallet/invoices.go
diff --git a/libwallet/invoices_test.go b/libwallet/invoices_test.go
new file mode 100644
index 0000000..401ec58
--- /dev/null
+++ b/libwallet/invoices_test.go
@@ -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")
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/keycrypt/keycrypt.go b/libwallet/keycrypt/keycrypt.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/keycrypt/keycrypt.go
rename to libwallet/keycrypt/keycrypt.go
diff --git a/libwallet/keycrypt/keycrypt_test.go b/libwallet/keycrypt/keycrypt_test.go
new file mode 100755
index 0000000..a3cf185
--- /dev/null
+++ b/libwallet/keycrypt/keycrypt_test.go
@@ -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())
+}
diff --git a/vendor/github.com/muun/libwallet/keycrypter.go b/libwallet/keycrypter.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/keycrypter.go
rename to libwallet/keycrypter.go
diff --git a/libwallet/keycrypter_test.go b/libwallet/keycrypter_test.go
new file mode 100755
index 0000000..2eaf4db
--- /dev/null
+++ b/libwallet/keycrypter_test.go
@@ -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")
+ }
+ })
+}
diff --git a/vendor/github.com/muun/libwallet/lnurl.go b/libwallet/lnurl.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/lnurl.go
rename to libwallet/lnurl.go
diff --git a/vendor/github.com/muun/libwallet/lnurl/lnurl.go b/libwallet/lnurl/lnurl.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/lnurl/lnurl.go
rename to libwallet/lnurl/lnurl.go
diff --git a/libwallet/lnurl/lnurl_test.go b/libwallet/lnurl/lnurl_test.go
new file mode 100644
index 0000000..89977dd
--- /dev/null
+++ b/libwallet/lnurl/lnurl_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/musig/README.md b/libwallet/musig/README.md
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/README.md
rename to libwallet/musig/README.md
diff --git a/vendor/github.com/muun/libwallet/musig/adaptor_impl.h b/libwallet/musig/adaptor_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/adaptor_impl.h
rename to libwallet/musig/adaptor_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/assumptions.h b/libwallet/musig/assumptions.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/assumptions.h
rename to libwallet/musig/assumptions.h
diff --git a/vendor/github.com/muun/libwallet/musig/basic-config.h b/libwallet/musig/basic-config.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/basic-config.h
rename to libwallet/musig/basic-config.h
diff --git a/vendor/github.com/muun/libwallet/musig/eccommit.h b/libwallet/musig/eccommit.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/eccommit.h
rename to libwallet/musig/eccommit.h
diff --git a/vendor/github.com/muun/libwallet/musig/eccommit_impl.h b/libwallet/musig/eccommit_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/eccommit_impl.h
rename to libwallet/musig/eccommit_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/ecdsa.h b/libwallet/musig/ecdsa.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/ecdsa.h
rename to libwallet/musig/ecdsa.h
diff --git a/vendor/github.com/muun/libwallet/musig/ecdsa_impl.h b/libwallet/musig/ecdsa_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/ecdsa_impl.h
rename to libwallet/musig/ecdsa_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/eckey.h b/libwallet/musig/eckey.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/eckey.h
rename to libwallet/musig/eckey.h
diff --git a/vendor/github.com/muun/libwallet/musig/eckey_impl.h b/libwallet/musig/eckey_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/eckey_impl.h
rename to libwallet/musig/eckey_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/ecmult.h b/libwallet/musig/ecmult.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/ecmult.h
rename to libwallet/musig/ecmult.h
diff --git a/vendor/github.com/muun/libwallet/musig/ecmult_const.h b/libwallet/musig/ecmult_const.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/ecmult_const.h
rename to libwallet/musig/ecmult_const.h
diff --git a/vendor/github.com/muun/libwallet/musig/ecmult_const_impl.h b/libwallet/musig/ecmult_const_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/ecmult_const_impl.h
rename to libwallet/musig/ecmult_const_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/ecmult_gen.h b/libwallet/musig/ecmult_gen.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/ecmult_gen.h
rename to libwallet/musig/ecmult_gen.h
diff --git a/vendor/github.com/muun/libwallet/musig/ecmult_gen_impl.h b/libwallet/musig/ecmult_gen_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/ecmult_gen_impl.h
rename to libwallet/musig/ecmult_gen_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/ecmult_impl.h b/libwallet/musig/ecmult_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/ecmult_impl.h
rename to libwallet/musig/ecmult_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/ecmult_static_context.h b/libwallet/musig/ecmult_static_context.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/ecmult_static_context.h
rename to libwallet/musig/ecmult_static_context.h
diff --git a/vendor/github.com/muun/libwallet/musig/extrakeys_main_impl.h b/libwallet/musig/extrakeys_main_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/extrakeys_main_impl.h
rename to libwallet/musig/extrakeys_main_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/field.h b/libwallet/musig/field.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/field.h
rename to libwallet/musig/field.h
diff --git a/vendor/github.com/muun/libwallet/musig/field_10x26.h b/libwallet/musig/field_10x26.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/field_10x26.h
rename to libwallet/musig/field_10x26.h
diff --git a/vendor/github.com/muun/libwallet/musig/field_10x26_impl.h b/libwallet/musig/field_10x26_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/field_10x26_impl.h
rename to libwallet/musig/field_10x26_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/field_5x52.h b/libwallet/musig/field_5x52.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/field_5x52.h
rename to libwallet/musig/field_5x52.h
diff --git a/vendor/github.com/muun/libwallet/musig/field_5x52_asm_impl.h b/libwallet/musig/field_5x52_asm_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/field_5x52_asm_impl.h
rename to libwallet/musig/field_5x52_asm_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/field_5x52_impl.h b/libwallet/musig/field_5x52_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/field_5x52_impl.h
rename to libwallet/musig/field_5x52_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/field_5x52_int128_impl.h b/libwallet/musig/field_5x52_int128_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/field_5x52_int128_impl.h
rename to libwallet/musig/field_5x52_int128_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/field_impl.h b/libwallet/musig/field_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/field_impl.h
rename to libwallet/musig/field_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/group.h b/libwallet/musig/group.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/group.h
rename to libwallet/musig/group.h
diff --git a/vendor/github.com/muun/libwallet/musig/group_impl.h b/libwallet/musig/group_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/group_impl.h
rename to libwallet/musig/group_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/hash.h b/libwallet/musig/hash.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/hash.h
rename to libwallet/musig/hash.h
diff --git a/vendor/github.com/muun/libwallet/musig/hash_impl.h b/libwallet/musig/hash_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/hash_impl.h
rename to libwallet/musig/hash_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/hsort.h b/libwallet/musig/hsort.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/hsort.h
rename to libwallet/musig/hsort.h
diff --git a/vendor/github.com/muun/libwallet/musig/hsort_impl.h b/libwallet/musig/hsort_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/hsort_impl.h
rename to libwallet/musig/hsort_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/import.sh b/libwallet/musig/import.sh
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/import.sh
rename to libwallet/musig/import.sh
diff --git a/vendor/github.com/muun/libwallet/musig/keyagg.h b/libwallet/musig/keyagg.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/keyagg.h
rename to libwallet/musig/keyagg.h
diff --git a/vendor/github.com/muun/libwallet/musig/keyagg_impl.h b/libwallet/musig/keyagg_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/keyagg_impl.h
rename to libwallet/musig/keyagg_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/libsecp256k1-config.h b/libwallet/musig/libsecp256k1-config.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/libsecp256k1-config.h
rename to libwallet/musig/libsecp256k1-config.h
diff --git a/vendor/github.com/muun/libwallet/musig/modinv32.h b/libwallet/musig/modinv32.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/modinv32.h
rename to libwallet/musig/modinv32.h
diff --git a/vendor/github.com/muun/libwallet/musig/modinv32_impl.h b/libwallet/musig/modinv32_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/modinv32_impl.h
rename to libwallet/musig/modinv32_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/modinv64.h b/libwallet/musig/modinv64.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/modinv64.h
rename to libwallet/musig/modinv64.h
diff --git a/vendor/github.com/muun/libwallet/musig/modinv64_impl.h b/libwallet/musig/modinv64_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/modinv64_impl.h
rename to libwallet/musig/modinv64_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/musig.go b/libwallet/musig/musig.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/musig.go
rename to libwallet/musig/musig.go
diff --git a/vendor/github.com/muun/libwallet/musig/musig_main_impl.h b/libwallet/musig/musig_main_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/musig_main_impl.h
rename to libwallet/musig/musig_main_impl.h
diff --git a/libwallet/musig/musig_test.go b/libwallet/musig/musig_test.go
new file mode 100644
index 0000000..2437f66
--- /dev/null
+++ b/libwallet/musig/musig_test.go
@@ -0,0 +1,253 @@
+package musig
+
+import (
+ "bytes"
+ "encoding/hex"
+ "testing"
+
+ "github.com/btcsuite/btcd/btcec"
+)
+
+func TestSigning(t *testing.T) {
+ toSign := [32]byte{1, 2, 3}
+
+ userPriv, _ := btcec.NewPrivateKey(btcec.S256())
+ muunPriv, _ := btcec.NewPrivateKey(btcec.S256())
+
+ combined, err := CombinePubKeysWithTweak(userPriv.PubKey(), muunPriv.PubKey(), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ userSessionId := RandomSessionId()
+ muunSessionId := RandomSessionId()
+
+ userPubNonces := GeneratePubNonce(userSessionId)
+ muunPubNonces := GeneratePubNonce(muunSessionId)
+
+ regeneratedUserNonces := GeneratePubNonce(userSessionId)
+ if !bytes.Equal(userPubNonces[:], regeneratedUserNonces[:]) {
+ t.Fatalf(
+ "Nonces do not match %v != %v",
+ hex.EncodeToString(userPubNonces[:]),
+ hex.EncodeToString(regeneratedUserNonces[:]),
+ )
+ }
+
+ muunSig, err := ComputeMuunPartialSignature(
+ toSign,
+ userPriv.PubKey(),
+ muunPriv,
+ userPubNonces,
+ muunSessionId,
+ nil,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fullSig, err := AddUserSignatureAndCombine(
+ toSign,
+ userPriv,
+ muunPriv.PubKey(),
+ muunSig,
+ muunPubNonces,
+ userSessionId,
+ nil,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !VerifySignature(toSign, fullSig, combined) {
+ t.Fatal("failed to verify sig")
+ }
+}
+
+func TestSigningWithCustomTweak(t *testing.T) {
+ someRandomKey, _ := btcec.NewPrivateKey(btcec.S256())
+ customTweak := someRandomKey.Serialize()
+
+ toSign := [32]byte{1, 2, 3}
+
+ userPriv, _ := btcec.NewPrivateKey(btcec.S256())
+ muunPriv, _ := btcec.NewPrivateKey(btcec.S256())
+
+ combined, err := CombinePubKeysWithTweak(userPriv.PubKey(), muunPriv.PubKey(), customTweak)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ userSessionId := RandomSessionId()
+ muunSessionId := RandomSessionId()
+
+ userPubNonces := GeneratePubNonce(userSessionId)
+ muunPubNonces := GeneratePubNonce(muunSessionId)
+
+ regeneratedUserNonces := GeneratePubNonce(userSessionId)
+ if !bytes.Equal(userPubNonces[:], regeneratedUserNonces[:]) {
+ t.Fatalf(
+ "Nonces do not match %v != %v",
+ hex.EncodeToString(userPubNonces[:]),
+ hex.EncodeToString(regeneratedUserNonces[:]),
+ )
+ }
+
+ muunSig, err := ComputeMuunPartialSignature(
+ toSign,
+ userPriv.PubKey(),
+ muunPriv,
+ userPubNonces,
+ muunSessionId,
+ customTweak,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fullSig, err := AddUserSignatureAndCombine(
+ toSign,
+ userPriv,
+ muunPriv.PubKey(),
+ muunSig,
+ muunPubNonces,
+ userSessionId,
+ customTweak,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !VerifySignature(toSign, fullSig, combined) {
+ t.Fatal("failed to verify sig")
+ }
+}
+
+func TestCrossWithJava(t *testing.T) {
+
+ decode32Bytes := func(str string) (result [32]byte) {
+ d, _ := hex.DecodeString(str)
+ copy(result[:], d)
+ return
+ }
+
+ rawUserPriv := decode32Bytes("6e39c6add6323a5ac5f65e50231fb815026476e734eb9f4f66dce3298fddf1dc")
+ rawMuunPriv := decode32Bytes("b876ecf97c19588cf4be95ddc0b06c0d9f623f2cf679276c25e4dfb512b19743")
+ userSessionId := decode32Bytes("52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c649")
+ muunSessionId := decode32Bytes("81855ad8681d0d86d1e91e00167939cb6694d2c422acd208a0072939487f6999")
+ toSign := decode32Bytes("0102030000000000000000000000000000000000000000000000000000000000")
+
+ expectedKey, _ := hex.DecodeString("027ca7eab04c2ad445418fa6a0ed2a331f121444aedd043adef94bdc00040ff96c")
+ expectedSig, _ := hex.DecodeString("773ad923eb5eef593095a6787b674675de2a558335dc3e44fe40cf7b3736637e66d508e4eaadca28dab02e2fdcf5707392b561fffa1837205d2fa77b74cbc82f")
+
+ userPriv, _ := btcec.PrivKeyFromBytes(btcec.S256(), rawUserPriv[:])
+ muunPriv, _ := btcec.PrivKeyFromBytes(btcec.S256(), rawMuunPriv[:])
+ combined, err := CombinePubKeysWithTweak(userPriv.PubKey(), muunPriv.PubKey(), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(combined.SerializeCompressed(), expectedKey) {
+ t.Fatal("Combined key doesn't match")
+ }
+
+ userPubNonces := GeneratePubNonce(userSessionId)
+ muunPubNonces := GeneratePubNonce(muunSessionId)
+
+ muunSig, err := ComputeMuunPartialSignature(
+ toSign,
+ userPriv.PubKey(),
+ muunPriv,
+ userPubNonces,
+ muunSessionId,
+ nil,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fullSig, err := AddUserSignatureAndCombine(
+ toSign,
+ userPriv,
+ muunPriv.PubKey(),
+ muunSig,
+ muunPubNonces,
+ userSessionId,
+ nil,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !VerifySignature(toSign, fullSig, combined) {
+ t.Fatal("failed to verify sig")
+ }
+ if !bytes.Equal(fullSig[:], expectedSig) {
+ t.Fatal("Signatures do no match")
+ }
+}
+
+func TestCrossWithJavaUsingTweak(t *testing.T) {
+
+ decode32Bytes := func(str string) (result [32]byte) {
+ d, _ := hex.DecodeString(str)
+ copy(result[:], d)
+ return
+ }
+
+ tweak := decode32Bytes("99f8135f02f85b24687a7b221f3cbc5c8641851b52c6b1a305fc32f6b7259fa0")
+ rawUserPriv := decode32Bytes("316f4ead37ad5ea564c9ca653a535cb8e5cad0fce249625372ba20453db01afe")
+ rawMuunPriv := decode32Bytes("cbd712e93a39b0eac8b4ae2922de2baa7f3765172ddecf3109e5fce4bff24560")
+ userSessionId := decode32Bytes("52fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c649")
+ muunSessionId := decode32Bytes("81855ad8681d0d86d1e91e00167939cb6694d2c422acd208a0072939487f6999")
+ toSign := decode32Bytes("0102030000000000000000000000000000000000000000000000000000000000")
+
+ expectedKey, _ := hex.DecodeString("0348131bb4b25e17ad573923b3a24f0ac780a9cf5cefaefe44f0b762c1e83bfc06")
+ expectedSig, _ := hex.DecodeString("1467fb39edcfc400f036ee97c0e5b0f10879e8f9c53de022c97fbd4e997b2ed8ad2a170747b3edbafd370e203c6284aad605486696887791c09d9e99195c2b85")
+
+ userPriv, _ := btcec.PrivKeyFromBytes(btcec.S256(), rawUserPriv[:])
+ muunPriv, _ := btcec.PrivKeyFromBytes(btcec.S256(), rawMuunPriv[:])
+ combined, err := CombinePubKeysWithTweak(userPriv.PubKey(), muunPriv.PubKey(), tweak[:])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(combined.SerializeCompressed(), expectedKey) {
+ t.Fatal("Combined key doesn't match")
+ }
+
+ userPubNonces := GeneratePubNonce(userSessionId)
+ muunPubNonces := GeneratePubNonce(muunSessionId)
+
+ muunSig, err := ComputeMuunPartialSignature(
+ toSign,
+ userPriv.PubKey(),
+ muunPriv,
+ userPubNonces,
+ muunSessionId,
+ tweak[:],
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fullSig, err := AddUserSignatureAndCombine(
+ toSign,
+ userPriv,
+ muunPriv.PubKey(),
+ muunSig,
+ muunPubNonces,
+ userSessionId,
+ tweak[:],
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !VerifySignature(toSign, fullSig, combined) {
+ t.Fatal("failed to verify sig")
+ }
+ if !bytes.Equal(fullSig[:], expectedSig) {
+ t.Fatal("Signatures do no match")
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/musig/scalar.h b/libwallet/musig/scalar.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/scalar.h
rename to libwallet/musig/scalar.h
diff --git a/vendor/github.com/muun/libwallet/musig/scalar_4x64.h b/libwallet/musig/scalar_4x64.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/scalar_4x64.h
rename to libwallet/musig/scalar_4x64.h
diff --git a/vendor/github.com/muun/libwallet/musig/scalar_4x64_impl.h b/libwallet/musig/scalar_4x64_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/scalar_4x64_impl.h
rename to libwallet/musig/scalar_4x64_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/scalar_8x32.h b/libwallet/musig/scalar_8x32.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/scalar_8x32.h
rename to libwallet/musig/scalar_8x32.h
diff --git a/vendor/github.com/muun/libwallet/musig/scalar_8x32_impl.h b/libwallet/musig/scalar_8x32_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/scalar_8x32_impl.h
rename to libwallet/musig/scalar_8x32_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/scalar_impl.h b/libwallet/musig/scalar_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/scalar_impl.h
rename to libwallet/musig/scalar_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/scalar_low.h b/libwallet/musig/scalar_low.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/scalar_low.h
rename to libwallet/musig/scalar_low.h
diff --git a/vendor/github.com/muun/libwallet/musig/scalar_low_impl.h b/libwallet/musig/scalar_low_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/scalar_low_impl.h
rename to libwallet/musig/scalar_low_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/schnorrsig_main_impl.h b/libwallet/musig/schnorrsig_main_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/schnorrsig_main_impl.h
rename to libwallet/musig/schnorrsig_main_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/scratch.h b/libwallet/musig/scratch.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/scratch.h
rename to libwallet/musig/scratch.h
diff --git a/vendor/github.com/muun/libwallet/musig/scratch_impl.h b/libwallet/musig/scratch_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/scratch_impl.h
rename to libwallet/musig/scratch_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/secp256k1.h b/libwallet/musig/secp256k1.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/secp256k1.h
rename to libwallet/musig/secp256k1.h
diff --git a/vendor/github.com/muun/libwallet/musig/secp256k1.k b/libwallet/musig/secp256k1.k
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/secp256k1.k
rename to libwallet/musig/secp256k1.k
diff --git a/vendor/github.com/muun/libwallet/musig/secp256k1_extrakeys.h b/libwallet/musig/secp256k1_extrakeys.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/secp256k1_extrakeys.h
rename to libwallet/musig/secp256k1_extrakeys.h
diff --git a/vendor/github.com/muun/libwallet/musig/secp256k1_musig.h b/libwallet/musig/secp256k1_musig.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/secp256k1_musig.h
rename to libwallet/musig/secp256k1_musig.h
diff --git a/vendor/github.com/muun/libwallet/musig/secp256k1_preallocated.h b/libwallet/musig/secp256k1_preallocated.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/secp256k1_preallocated.h
rename to libwallet/musig/secp256k1_preallocated.h
diff --git a/vendor/github.com/muun/libwallet/musig/secp256k1_schnorrsig.h b/libwallet/musig/secp256k1_schnorrsig.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/secp256k1_schnorrsig.h
rename to libwallet/musig/secp256k1_schnorrsig.h
diff --git a/vendor/github.com/muun/libwallet/musig/selftest.h b/libwallet/musig/selftest.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/selftest.h
rename to libwallet/musig/selftest.h
diff --git a/vendor/github.com/muun/libwallet/musig/session.h b/libwallet/musig/session.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/session.h
rename to libwallet/musig/session.h
diff --git a/vendor/github.com/muun/libwallet/musig/session_impl.h b/libwallet/musig/session_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/session_impl.h
rename to libwallet/musig/session_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/testrand.h b/libwallet/musig/testrand.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/testrand.h
rename to libwallet/musig/testrand.h
diff --git a/vendor/github.com/muun/libwallet/musig/testrand_impl.h b/libwallet/musig/testrand_impl.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/testrand_impl.h
rename to libwallet/musig/testrand_impl.h
diff --git a/vendor/github.com/muun/libwallet/musig/umbrella.c b/libwallet/musig/umbrella.c
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/umbrella.c
rename to libwallet/musig/umbrella.c
diff --git a/vendor/github.com/muun/libwallet/musig/umbrella.h b/libwallet/musig/umbrella.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/umbrella.h
rename to libwallet/musig/umbrella.h
diff --git a/vendor/github.com/muun/libwallet/musig/util.h b/libwallet/musig/util.h
similarity index 100%
rename from vendor/github.com/muun/libwallet/musig/util.h
rename to libwallet/musig/util.h
diff --git a/vendor/github.com/muun/libwallet/network.go b/libwallet/network.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/network.go
rename to libwallet/network.go
diff --git a/libwallet/network_test.go b/libwallet/network_test.go
new file mode 100755
index 0000000..eafef60
--- /dev/null
+++ b/libwallet/network_test.go
@@ -0,0 +1,75 @@
+package libwallet
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/btcsuite/btcd/chaincfg"
+)
+
+func TestMainnet(t *testing.T) {
+ tests := []struct {
+ name string
+ want *Network
+ }{
+ {name: "Get", want: &Network{network: &chaincfg.MainNetParams}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := Mainnet(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Mainnet() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestTestnet(t *testing.T) {
+ tests := []struct {
+ name string
+ want *Network
+ }{
+ {name: "Get", want: &Network{network: &chaincfg.TestNet3Params}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := Testnet(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Testnet() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestRegtest(t *testing.T) {
+ tests := []struct {
+ name string
+ want *Network
+ }{
+ {name: "Get", want: &Network{network: &chaincfg.RegressionNetParams}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := Regtest(); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Regtest() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestNetwork_Name(t *testing.T) {
+ tests := []struct {
+ name string
+ instance *Network
+ want string
+ }{
+ {name: "regtest", instance: Regtest(), want: "regtest"},
+ {name: "mainnet", instance: Mainnet(), want: "mainnet"},
+ {name: "testnet", instance: Testnet(), want: "testnet3"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := tt.instance.Name(); got != tt.want {
+ t.Errorf("Network.Name() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/libwallet/newop/context.go b/libwallet/newop/context.go
new file mode 100644
index 0000000..7f03666
--- /dev/null
+++ b/libwallet/newop/context.go
@@ -0,0 +1,36 @@
+package newop
+
+import "github.com/muun/libwallet/operation"
+
+// PaymentContext stores data required to analyze and validate an operation
+type PaymentContext struct {
+ FeeWindow *FeeWindow
+ NextTransactionSize *NextTransactionSize
+ ExchangeRateWindow *ExchangeRateWindow
+ PrimaryCurrency string
+ MinFeeRateInSatsPerVByte float64
+ SubmarineSwap *SubmarineSwap
+}
+
+func (c *PaymentContext) totalBalance() int64 {
+ return c.NextTransactionSize.toInternalType().TotalBalance()
+}
+
+func (c *PaymentContext) toBitcoinAmount(sats int64, inputCurrency string) *BitcoinAmount {
+ amount := c.ExchangeRateWindow.convert(
+ NewMonetaryAmountFromSatoshis(sats),
+ inputCurrency,
+ )
+ return &BitcoinAmount{
+ InSat: sats,
+ InInputCurrency: amount,
+ InPrimaryCurrency: c.ExchangeRateWindow.convert(amount, c.PrimaryCurrency),
+ }
+}
+
+func newPaymentAnalyzer(context *PaymentContext) *operation.PaymentAnalyzer {
+ return operation.NewPaymentAnalyzer(
+ context.FeeWindow.toInternalType(),
+ context.NextTransactionSize.toInternalType(),
+ )
+}
diff --git a/libwallet/newop/context_test.go b/libwallet/newop/context_test.go
new file mode 100644
index 0000000..6e2eb7c
--- /dev/null
+++ b/libwallet/newop/context_test.go
@@ -0,0 +1,65 @@
+package newop
+
+import (
+ "github.com/shopspring/decimal"
+ "testing"
+)
+
+var testPaymentContext = createTestPaymentContext()
+
+func createTestPaymentContext() *PaymentContext {
+ var context = &PaymentContext{
+ NextTransactionSize: &NextTransactionSize{
+ ExpectedDebtInSat: 10_000,
+ },
+ ExchangeRateWindow: &ExchangeRateWindow{
+ rates: make(map[string]float64),
+ },
+ FeeWindow: &FeeWindow{},
+ PrimaryCurrency: "BTC",
+ MinFeeRateInSatsPerVByte: 1.0,
+ }
+ context.NextTransactionSize.AddSizeForAmount(&SizeForAmount{
+ AmountInSat: 100_000_000,
+ SizeInVByte: 240,
+ })
+
+ context.ExchangeRateWindow.AddRate("BTC", 1)
+ context.ExchangeRateWindow.AddRate("USD", 32_000)
+
+ return context
+}
+
+func TestPaymentContextTotalBalance(t *testing.T) {
+
+ totalBalance := testPaymentContext.totalBalance()
+
+ if totalBalance != 99_990_000 {
+ t.Fatalf("expected totalBalance to be 90_000, got %v", totalBalance)
+ }
+}
+
+func TestPaymentContextToBitcoinAmount(t *testing.T) {
+
+ btcAmount := testPaymentContext.toBitcoinAmount(100_000, "USD")
+
+ if btcAmount.InSat != 100_000 {
+ t.Fatalf("expected bitcoin amount in sats to remain unchanged and be 100_000, got %v", btcAmount.InSat)
+ }
+
+ if btcAmount.InInputCurrency.Currency != "USD" {
+ t.Fatalf("expected bitcoin amount input currency to be USD, got %v", btcAmount.InInputCurrency.Currency)
+ }
+
+ if btcAmount.InInputCurrency.Value.Cmp(decimal.NewFromInt(32)) != 0 {
+ t.Fatalf("expected converted amount to be 32, got %v", btcAmount.InInputCurrency.Value)
+ }
+
+ if btcAmount.InPrimaryCurrency.Currency != "BTC" {
+ t.Fatalf("expected bitcoin amount primary currency to be BTC, got %v", btcAmount.InPrimaryCurrency.Currency)
+ }
+
+ if btcAmount.InPrimaryCurrency.Value.Cmp(decimal.NewFromFloat(0.001)) != 0 {
+ t.Fatalf("expected amount in primary currency to be 0.001, got %v", btcAmount.InPrimaryCurrency.Value)
+ }
+}
diff --git a/libwallet/newop/exchange_rates.go b/libwallet/newop/exchange_rates.go
new file mode 100644
index 0000000..1b9801b
--- /dev/null
+++ b/libwallet/newop/exchange_rates.go
@@ -0,0 +1,39 @@
+package newop
+
+import (
+ "github.com/muun/libwallet"
+ "github.com/shopspring/decimal"
+)
+
+// ExchangeRateWindow holds a map of exchange rates from BTC to every currency we handle
+type ExchangeRateWindow struct {
+ WindowId int
+ rates map[string]float64
+}
+
+func (w *ExchangeRateWindow) AddRate(currency string, rate float64) {
+ if w.rates == nil {
+ w.rates = make(map[string]float64)
+ }
+ w.rates[currency] = rate
+}
+
+func (w *ExchangeRateWindow) Rate(currency string) float64 {
+ return w.rates[currency]
+}
+
+func (s *ExchangeRateWindow) Currencies() *libwallet.StringList {
+ var currencies []string
+ for key := range s.rates {
+ currencies = append(currencies, key)
+ }
+ return libwallet.NewStringListWithElements(currencies)
+}
+
+func (w *ExchangeRateWindow) convert(amount *MonetaryAmount, currency string) *MonetaryAmount {
+ fromRate := decimal.NewFromFloat(w.Rate(amount.Currency))
+ toRate := decimal.NewFromFloat(w.Rate(currency))
+ value := amount.Value.Div(fromRate).Mul(toRate)
+
+ return &MonetaryAmount{Value: value, Currency: currency}
+}
diff --git a/libwallet/newop/exchange_rates_test.go b/libwallet/newop/exchange_rates_test.go
new file mode 100644
index 0000000..30a65a6
--- /dev/null
+++ b/libwallet/newop/exchange_rates_test.go
@@ -0,0 +1,27 @@
+package newop
+
+import (
+ "testing"
+
+ "github.com/shopspring/decimal"
+)
+
+func TestExchangeWindowConvert(t *testing.T) {
+
+ window := &ExchangeRateWindow{}
+ window.AddRate("BTC", 1)
+ window.AddRate("USD", 32_000)
+
+ amount := NewMonetaryAmountFromSatoshis(100_000_000)
+
+ converted := window.convert(amount, "USD")
+
+ if converted.Currency != "USD" {
+ t.Fatalf("expected converted currency to be USD, got %v", converted.Currency)
+ }
+
+ if converted.Value.Cmp(decimal.NewFromInt(32_000)) != 0 {
+ t.Fatalf("expected converted amount to be 32000, got %v", converted.Value)
+ }
+
+}
diff --git a/libwallet/newop/fee_state.go b/libwallet/newop/fee_state.go
new file mode 100644
index 0000000..013dd0b
--- /dev/null
+++ b/libwallet/newop/fee_state.go
@@ -0,0 +1,26 @@
+package newop
+
+const (
+ FeeStateFinalFee string = "FinalFee"
+ FeeStateNeedsChange string = "NeedsChange"
+ FeeStateNoPossibleFee string = "NoPossibleFee"
+)
+
+type FeeState struct {
+ State string
+ Amount *BitcoinAmount
+ RateInSatsPerVByte float64
+ TargetBlocks int64 // 0 if target not found
+}
+
+func (f *FeeState) IsFinal() bool {
+ return f.State == FeeStateFinalFee
+}
+
+func (f *FeeState) NeedsChange() bool {
+ return f.State == FeeStateNeedsChange
+}
+
+func (f *FeeState) IsNoPossibleFee() bool {
+ return f.State == FeeStateNoPossibleFee
+}
diff --git a/libwallet/newop/fee_window.go b/libwallet/newop/fee_window.go
new file mode 100644
index 0000000..78aff9e
--- /dev/null
+++ b/libwallet/newop/fee_window.go
@@ -0,0 +1,35 @@
+package newop
+
+import "github.com/muun/libwallet/operation"
+
+// FeeWindow holds a map of target block to fee rate for a given time
+type FeeWindow struct {
+ FastConfTarget int64
+ MediumConfTarget int64
+ SlowConfTarget int64
+ TargetedFees map[uint]float64
+}
+
+func (w *FeeWindow) PutTargetedFees(target int64, feeRateInSatsPerVByte float64) {
+ if w.TargetedFees == nil {
+ w.TargetedFees = make(map[uint]float64)
+ }
+ w.TargetedFees[uint(target)] = feeRateInSatsPerVByte
+}
+
+func (w *FeeWindow) GetTargetedFees(target int64) float64 {
+ if w.TargetedFees == nil {
+ return 0
+ }
+ return w.TargetedFees[uint(target)]
+}
+
+func (w *FeeWindow) nextHighestBlock(feeRate float64) int64 {
+ return int64(w.toInternalType().NextHighestBlock(feeRate))
+}
+
+func (w *FeeWindow) toInternalType() *operation.FeeWindow {
+ return &operation.FeeWindow{
+ TargetedFees: w.TargetedFees,
+ }
+}
diff --git a/libwallet/newop/money.go b/libwallet/newop/money.go
new file mode 100644
index 0000000..4895abc
--- /dev/null
+++ b/libwallet/newop/money.go
@@ -0,0 +1,86 @@
+package newop
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/btcsuite/btcutil"
+ "github.com/shopspring/decimal"
+)
+
+// MonetaryAmount holds an amount of money in a certain currency
+type MonetaryAmount struct {
+ Value decimal.Decimal
+ Currency string
+}
+
+func NewMonetaryAmountFromSatoshis(value int64) *MonetaryAmount {
+ v := decimal.NewFromInt(value).Div(decimal.NewFromInt(100_000_000))
+ return &MonetaryAmount{
+ Value: v,
+ Currency: "BTC",
+ }
+}
+
+func NewMonetaryAmountFromFiat(value string, currency string) *MonetaryAmount {
+ v, err := decimal.NewFromString(value)
+ if err != nil {
+ log.Printf("could not initialize monetary amount: %v", err)
+ return nil
+ }
+ return &MonetaryAmount{
+ Value: v,
+ Currency: currency,
+ }
+}
+
+func (m *MonetaryAmount) ValueAsString() string {
+ return m.Value.String()
+}
+
+func (m *MonetaryAmount) String() string {
+ return fmt.Sprintf("%v %v", m.Value, m.Currency) // TODO(newop): this is just a stub implementation
+}
+
+func (m *MonetaryAmount) toBtc(window *ExchangeRateWindow) btcutil.Amount {
+ rate := window.Rate(m.Currency)
+ v := m.Value.Div(decimal.NewFromFloat(rate)).Mul(decimal.NewFromInt(100_000_000))
+ v = v.RoundBank(0)
+ return btcutil.Amount(v.IntPart())
+}
+
+func (m *MonetaryAmount) add(n *MonetaryAmount) *MonetaryAmount {
+ if m.Currency != n.Currency {
+ panic("currencies do not match") // TODO(newop): replace panic and bubble up errors?
+ }
+ return &MonetaryAmount{
+ Value: m.Value.Add(n.Value),
+ Currency: m.Currency,
+ }
+}
+
+func (m *MonetaryAmount) toBitcoinAmount(window *ExchangeRateWindow, primaryCurrency string) *BitcoinAmount {
+ return &BitcoinAmount{
+ InSat: int64(m.toBtc(window)),
+ InInputCurrency: m,
+ InPrimaryCurrency: window.convert(m, primaryCurrency),
+ }
+}
+
+type BitcoinAmount struct {
+ InSat int64
+ InPrimaryCurrency *MonetaryAmount
+ InInputCurrency *MonetaryAmount
+}
+
+func (a *BitcoinAmount) toBtc() btcutil.Amount {
+ return btcutil.Amount(a.InSat)
+}
+
+func (a *BitcoinAmount) add(b *BitcoinAmount) *BitcoinAmount {
+ return &BitcoinAmount{
+ InSat: a.InSat + b.InSat,
+ InInputCurrency: a.InInputCurrency.add(b.InInputCurrency),
+ InPrimaryCurrency: a.InPrimaryCurrency.add(b.InPrimaryCurrency),
+ }
+}
diff --git a/libwallet/newop/money_test.go b/libwallet/newop/money_test.go
new file mode 100644
index 0000000..4f316e0
--- /dev/null
+++ b/libwallet/newop/money_test.go
@@ -0,0 +1,67 @@
+package newop
+
+import (
+ "testing"
+
+ "github.com/shopspring/decimal"
+)
+
+func TestMonetaryAmountString(t *testing.T) {
+
+ amount := NewMonetaryAmountFromFiat("10.54", "USD")
+ if amount.ValueAsString() != "10.54" {
+ t.Fatalf("expected value as string to match, got %v", amount.ValueAsString())
+ }
+
+ if amount.String() != "10.54 USD" {
+ t.Fatalf("expected value as string to match, got %v", amount.String())
+ }
+}
+
+func TestMonetaryAmountToBitcoinAmount(t *testing.T) {
+
+ amount := NewMonetaryAmountFromSatoshis(100_000_000)
+
+ window := &ExchangeRateWindow{}
+ window.AddRate("BTC", 1)
+ window.AddRate("USD", 32_000)
+
+ bitcoinAmount := amount.toBitcoinAmount(window, "USD")
+
+ if bitcoinAmount.InSat != 100_000_000 {
+ t.Fatalf("expected sats amount to be 100000000, got %v", bitcoinAmount.InSat)
+ }
+
+ if bitcoinAmount.InPrimaryCurrency.Currency != "USD" {
+ t.Fatalf("expected converted currency to be USD, got %v", bitcoinAmount.InPrimaryCurrency.Currency)
+ }
+
+ if bitcoinAmount.InPrimaryCurrency.Value.Cmp(decimal.NewFromInt(32_000)) != 0 {
+ t.Fatalf("expected converted amount to be 32000, got %v", bitcoinAmount.InInputCurrency.Value)
+ }
+
+ if bitcoinAmount.InInputCurrency.Currency != "BTC" {
+ t.Fatalf("expected intput currency to be BTC, got %v", bitcoinAmount.InInputCurrency.Currency)
+ }
+
+ if bitcoinAmount.InInputCurrency.Value.Cmp(decimal.NewFromInt(1)) != 0 {
+ t.Fatalf("expected converted amount to be 1, got %v", bitcoinAmount.InInputCurrency.Value)
+ }
+
+}
+
+func TestMonetaryAmountAdd(t *testing.T) {
+
+ a := NewMonetaryAmountFromFiat("10", "USD")
+ b := NewMonetaryAmountFromFiat("20", "USD")
+
+ sum := a.add(b)
+
+ if sum.Currency != "USD" {
+ t.Fatalf("expected converted currency to be USD, got %v", sum.Currency)
+ }
+
+ if sum.Value.Cmp(decimal.NewFromInt(30)) != 0 {
+ t.Fatalf("expected converted amount to be 30, got %v", sum.Value)
+ }
+}
diff --git a/libwallet/newop/nts.go b/libwallet/newop/nts.go
new file mode 100644
index 0000000..cb43dbf
--- /dev/null
+++ b/libwallet/newop/nts.go
@@ -0,0 +1,51 @@
+package newop
+
+import (
+ "strings"
+
+ "github.com/muun/libwallet/operation"
+)
+
+type SizeForAmount struct {
+ SizeInVByte int64
+ AmountInSat int64
+ Outpoint string
+}
+
+// NextTransactionSize is a struct used for calculating fees in terms of the
+// unspent outputs required to perform a transaction
+type NextTransactionSize struct {
+ SizeProgression []SizeForAmount
+ ValidAtOperationHid int64 // Just for debugging reasons
+ ExpectedDebtInSat int64
+}
+
+func (w *NextTransactionSize) AddSizeForAmount(item *SizeForAmount) {
+ w.SizeProgression = append(w.SizeProgression, *item)
+}
+
+func (w *NextTransactionSize) GetOutpoints() string {
+ var sb strings.Builder
+ for i, sizeForAmount := range w.SizeProgression {
+ sb.WriteString(sizeForAmount.Outpoint)
+ if i != len(w.SizeProgression)-1 { // avoid trailing \n at the end
+ sb.WriteRune('\n')
+ }
+
+ }
+ return sb.String()
+}
+
+func (w *NextTransactionSize) toInternalType() *operation.NextTransactionSize {
+ var sizeProgression []operation.SizeForAmount
+ for _, sizeForAmount := range w.SizeProgression {
+ sizeProgression = append(sizeProgression, operation.SizeForAmount{
+ SizeInVByte: sizeForAmount.SizeInVByte,
+ AmountInSat: sizeForAmount.AmountInSat,
+ })
+ }
+ return &operation.NextTransactionSize{
+ SizeProgression: sizeProgression,
+ ExpectedDebtInSat: w.ExpectedDebtInSat,
+ }
+}
diff --git a/libwallet/newop/state.go b/libwallet/newop/state.go
new file mode 100644
index 0000000..5e072d0
--- /dev/null
+++ b/libwallet/newop/state.go
@@ -0,0 +1,1106 @@
+package newop
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/btcsuite/btcutil"
+ "github.com/muun/libwallet"
+ "github.com/muun/libwallet/operation"
+)
+
+// Transitions that involve asynchronous work block, so apps should always fire in background and
+// observe results.
+//
+// Each State has a concrete type, that embeds a BaseState. The BaseState holds data shared by
+// all states, usually forwarded with every transition.
+
+// State is an interface implemented by every state type, containing flow-level properties.
+type State interface {
+ GetUpdate() string
+}
+
+// States are emitted in Transitions and contain a certain Update type.
+// UpdateAll is the default. Means the state is meant to update the UI entirely. This is currently
+// handled differently by clients. Falcon takes it as a built UI from scratch, while Apollo
+// only builds from scratch if it has to, otherwise it just updates values.
+// UpdateEmpty means the State machine is coming back to a previous State and UI needs no further
+// updating.
+// UpdateInPlace is a special update type for Falcon, it's used to just update values in place,
+// no building UI from scratch.
+const (
+ UpdateAll = ""
+ UpdateEmpty = "UpdateEmpty"
+ UpdateInPlace = "UpdateInPlace"
+)
+
+// TransitionListener allows app-level code to receive state updates asynchronously, preserving
+// concrete types across the bridge and enforcing type safety.
+type TransitionListener interface {
+ OnStart(nextState *StartState)
+ OnResolve(nextState *ResolveState)
+ OnEnterAmount(nextState *EnterAmountState)
+ OnEnterDescription(nextState *EnterDescriptionState)
+ OnValidate(nextState *ValidateState)
+ OnValidateLightning(nextState *ValidateLightningState)
+ OnConfirm(nextState *ConfirmState)
+ OnConfirmLightning(nextState *ConfirmLightningState)
+ OnEditFee(nextState *EditFeeState)
+ OnError(nextState *ErrorState)
+ OnBalanceError(nextState *BalanceErrorState)
+ OnAbort(nextState *AbortState)
+}
+
+// NewOperationFlow sets up the StartState, so transitions will be reported to `listener`
+func NewOperationFlow(listener TransitionListener) *StartState {
+ initial := &StartState{
+ BaseState: BaseState{
+ listener: listener,
+ },
+ }
+
+ return initial
+}
+
+// -------------------------------------------------------------------------------------------------
+
+// BaseState contains the shared structure among all states
+type BaseState struct {
+ listener TransitionListener
+ update string
+}
+
+func (s *BaseState) GetUpdate() string {
+ return s.update
+}
+
+type Resolved struct {
+ BaseState
+ PaymentIntent *PaymentIntent
+ PaymentContext *PaymentContext
+ PresetAmount *BitcoinAmount
+ PresetNote string
+}
+
+func (r Resolved) emitError(error string) error {
+ nextState := &ErrorState{
+ BaseState: r.BaseState,
+ PaymentIntent: r.PaymentIntent,
+ Error: error,
+ }
+ nextState.emit()
+
+ return nil
+}
+
+func (r Resolved) emitBalanceError(error string, analysis *operation.PaymentAnalysis, inputCurrency string) error {
+
+ toMonetaryAmount := func(sats int64) *MonetaryAmount {
+ window := r.PaymentContext.ExchangeRateWindow
+ return window.convert(NewMonetaryAmountFromSatoshis(sats), inputCurrency)
+ }
+
+ next := &BalanceErrorState{
+ BaseState: r.BaseState,
+ PaymentIntent: r.PaymentIntent,
+ TotalAmount: toMonetaryAmount(analysis.TotalInSat),
+ Balance: toMonetaryAmount(r.PaymentContext.totalBalance()),
+ Error: error,
+ }
+ next.emit()
+
+ return nil
+}
+
+type AmountInfo struct {
+ Amount *BitcoinAmount
+ TotalBalance *BitcoinAmount
+ TakeFeeFromAmount bool
+ FeeRateInSatsPerVByte float64
+}
+
+func (a *AmountInfo) mutating(f func(*AmountInfo)) *AmountInfo {
+ // Deref to make a copy before mutating. Otherwise we mutate the original.
+ mutated := *a
+ f(&mutated)
+ return &mutated
+}
+
+type Validated struct {
+ analysis *operation.PaymentAnalysis
+ Fee *BitcoinAmount
+ FeeNeedsChange bool
+ Total *BitcoinAmount
+ SwapInfo *SwapInfo
+}
+
+type SwapInfo struct {
+ IsOneConf bool
+ OnchainFee *BitcoinAmount
+ SwapFees *SwapFees
+}
+
+// -------------------------------------------------------------------------------------------------
+
+type StartState struct {
+ BaseState
+}
+
+func (s *StartState) Resolve(address string, network *libwallet.Network) error {
+ uri, err := libwallet.GetPaymentURI(address, network)
+ if err != nil {
+ return err
+ }
+
+ if len(uri.Bip70Url) > 0 {
+ // We resolve BIP70 here to avoid hoping about in the apps with the data
+ // However, this has the consequence of making the BIP70 resolution
+ // and loading of real time data sequential. While this is a bit worse
+ // BIP70 is rarely used, and we should usually have RTD up to date.
+
+ go s.resolveBip70(uri, network)
+ return nil
+ }
+
+ next := &ResolveState{
+ BaseState: s.BaseState,
+ PaymentIntent: &PaymentIntent{
+ URI: uri,
+ },
+ }
+
+ next.emit()
+ return nil
+}
+
+func (s *StartState) resolveBip70(uri *libwallet.MuunPaymentURI, network *libwallet.Network) {
+
+ intent := &PaymentIntent{
+ URI: uri,
+ }
+
+ bip70, err := libwallet.DoPaymentRequestCall(uri.Bip70Url, network)
+ if err != nil {
+
+ fmt.Printf("Error resolving bip70 uri: %s. Error: %v\n", uri.Bip70Url, err)
+
+ // If we failed to resolve the URI but we had an address set, use that!
+ if len(uri.Address) > 0 {
+ next := &ResolveState{
+ BaseState: s.BaseState,
+ PaymentIntent: intent,
+ }
+ next.emit()
+
+ // If the error contains the expired string, that means that the invoice has expired
+ } else if strings.Contains(err.Error(), "failed to unmarshal payment request") {
+ next := &ErrorState{
+ BaseState: s.BaseState,
+ PaymentIntent: intent,
+ Error: OperationErrorInvoiceExpired,
+ }
+ next.emit()
+
+ // In any other case we display the invalid address message
+ } else {
+ next := &ErrorState{
+ BaseState: s.BaseState,
+ PaymentIntent: intent,
+ Error: OperationErrorInvalidAddress,
+ }
+ next.emit()
+ }
+
+ } else {
+
+ fmt.Printf("Successfully resolved bip70 uri: %s\n", uri.Bip70Url)
+ next := &ResolveState{
+ BaseState: s.BaseState,
+ PaymentIntent: &PaymentIntent{
+ URI: bip70,
+ },
+ }
+ next.emit()
+ }
+}
+
+func (s *StartState) ResolveInvoice(invoice *libwallet.Invoice, network *libwallet.Network) error {
+ next := &ResolveState{
+ BaseState: s.BaseState,
+ PaymentIntent: &PaymentIntent{
+ URI: &libwallet.MuunPaymentURI{
+ Invoice: invoice,
+ },
+ },
+ }
+
+ next.emit()
+ return nil
+}
+
+// -------------------------------------------------------------------------------------------------
+
+// PaymentIntent contains the resolved payment intent and does not change during the flow
+type PaymentIntent struct {
+ URI *libwallet.MuunPaymentURI
+}
+
+func (p *PaymentIntent) Amount() *MonetaryAmount {
+ if p.URI.Invoice != nil && p.URI.Invoice.Sats != 0 {
+ return NewMonetaryAmountFromSatoshis(p.URI.Invoice.Sats)
+ }
+ if p.URI.Amount != "" {
+ return NewMonetaryAmountFromFiat(p.URI.Amount, "BTC")
+ }
+ return nil
+}
+
+type ResolveState struct {
+ BaseState
+ PaymentIntent *PaymentIntent
+}
+
+func (s *ResolveState) SetContext(context *PaymentContext) error {
+ return s.setContextWithTime(context, time.Now())
+}
+
+// setContextWithTime is meant only for testing, allows caller to use a fixed time to check invoice expiration
+func (s *ResolveState) setContextWithTime(context *PaymentContext, now time.Time) error {
+
+ // TODO(newop): add type to PaymentIntent to clarify lightning/onchain distinction
+ invoice := s.PaymentIntent.URI.Invoice
+ totalBalance := context.toBitcoinAmount(context.totalBalance(), context.PrimaryCurrency)
+
+ if invoice != nil {
+ // check expired and send to error state
+ if time.Unix(invoice.Expiry, 0).Before(now) {
+ return s.emitError(OperationErrorInvoiceExpired)
+ }
+
+ if invoice.Sats > 0 {
+ return s.emitValidateLightning(context, invoice, totalBalance)
+ }
+
+ return s.emitAmount(context, totalBalance)
+ }
+
+ amount := s.PaymentIntent.Amount()
+
+ if s.PaymentIntent.URI.Amount != "" && amount == nil {
+ return s.emitError(OperationErrorInvalidAddress)
+ }
+
+ if amount != nil {
+
+ if amount.toBtc(context.ExchangeRateWindow) < operation.DustThreshold {
+ return s.emitError(OperationErrorAmountTooSmall)
+ }
+
+ amount := amount.toBitcoinAmount(context.ExchangeRateWindow, context.PrimaryCurrency)
+ return s.emitValidate(&Resolved{
+ BaseState: s.BaseState,
+ PaymentIntent: s.PaymentIntent,
+ PaymentContext: context,
+ PresetAmount: amount,
+ }, amount, context)
+ }
+
+ return s.emitAmount(context, totalBalance)
+}
+
+func (s *ResolveState) emitAmount(context *PaymentContext, totalBalance *BitcoinAmount) error {
+
+ amount := NewMonetaryAmountFromFiat("0", context.PrimaryCurrency).
+ toBitcoinAmount(context.ExchangeRateWindow, context.PrimaryCurrency)
+
+ nextState := &EnterAmountState{
+ Resolved: &Resolved{
+ BaseState: s.BaseState,
+ PaymentIntent: s.PaymentIntent,
+ PaymentContext: context,
+ },
+ Amount: amount,
+ TotalBalance: totalBalance,
+ }
+ nextState.emit()
+ return nil
+}
+
+func (s *ResolveState) emitValidateLightning(context *PaymentContext, invoice *libwallet.Invoice, totalBalance *BitcoinAmount) error {
+ presetAmount := context.toBitcoinAmount(invoice.Sats, "BTC")
+ presetNote := s.PaymentIntent.URI.Invoice.Description
+ nextState := &ValidateLightningState{
+ Resolved: &Resolved{
+ BaseState: s.BaseState,
+ PaymentIntent: s.PaymentIntent,
+ PaymentContext: context,
+ PresetAmount: presetAmount,
+ PresetNote: presetNote,
+ },
+ AmountInfo: &AmountInfo{
+ Amount: presetAmount,
+ TotalBalance: totalBalance,
+ TakeFeeFromAmount: false,
+ },
+ Note: presetNote,
+ }
+ nextState.emit()
+
+ return nil
+}
+
+func (s *ResolveState) emitValidate(resolved *Resolved, amount *BitcoinAmount, context *PaymentContext) error {
+
+ nextState := &ValidateState{
+ Resolved: resolved,
+ AmountInfo: &AmountInfo{
+ TotalBalance: context.toBitcoinAmount(context.totalBalance(), "BTC"),
+ Amount: amount,
+ TakeFeeFromAmount: false,
+ FeeRateInSatsPerVByte: context.FeeWindow.toInternalType().FastestFeeRate(),
+ },
+ }
+ nextState.emit()
+
+ return nil
+}
+
+func (s *ResolveState) emitError(error string) error {
+ nextState := &ErrorState{
+ BaseState: s.BaseState,
+ PaymentIntent: s.PaymentIntent,
+ Error: error,
+ }
+ nextState.emit()
+
+ return nil
+}
+
+func (s *ResolveState) emit() {
+ s.listener.OnResolve(s)
+}
+
+// -------------------------------------------------------------------------------------------------
+
+type EnterAmountState struct {
+ *Resolved
+ Amount *BitcoinAmount
+ TotalBalance *BitcoinAmount
+}
+
+func (s *EnterAmountState) EnterAmount(amount *MonetaryAmount, takeFeeFromAmount bool) error {
+
+ // Let's enforce this business logic here. Our payment analyzer enforces
+ // IF tffa/useAllFunds THEN amount == balance
+ // But we also want the contraposition:
+ // IF amount == balance THEN tffa/useAllFunds
+ if amount.String() == s.TotalBalance.InInputCurrency.String() {
+ amount = s.TotalBalance.InInputCurrency
+ takeFeeFromAmount = true
+ }
+
+ feeWindow := s.PaymentContext.FeeWindow.toInternalType()
+ amountInfo := &AmountInfo{
+ Amount: amount.toBitcoinAmount(
+ s.PaymentContext.ExchangeRateWindow,
+ s.PaymentContext.PrimaryCurrency,
+ ),
+ TakeFeeFromAmount: takeFeeFromAmount,
+ TotalBalance: s.TotalBalance,
+ FeeRateInSatsPerVByte: feeWindow.FastestFeeRate(),
+ }
+
+ if s.Resolved.PaymentIntent.URI.Invoice != nil {
+
+ nextState := &ValidateLightningState{
+ Resolved: s.Resolved,
+ AmountInfo: amountInfo,
+ Note: s.PaymentIntent.URI.Invoice.Description,
+ }
+ nextState.emit()
+
+ } else {
+
+ nextState := &ValidateState{
+ Resolved: s.Resolved,
+ AmountInfo: amountInfo,
+ Note: s.Resolved.PaymentIntent.URI.Message,
+ }
+ nextState.emit()
+ }
+
+ return nil
+}
+
+func (s *EnterAmountState) PartialValidate(inputAmount *MonetaryAmount) (bool, error) {
+
+ amountInSat := int64(inputAmount.toBtc(s.PaymentContext.ExchangeRateWindow))
+ isSwap := s.PaymentContext.SubmarineSwap != nil
+
+ minAmount := int64(operation.DustThreshold)
+ if isSwap {
+ minAmount = 0
+ }
+
+ if amountInSat < minAmount || amountInSat > s.TotalBalance.InSat {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+// ChangeCurrency is deprecated. Prefer the newer, ChangeCurrencyWithAmount(currency, inputAmount)
+func (s *EnterAmountState) ChangeCurrency(currency string) error {
+ return s.ChangeCurrencyWithAmount(currency, s.Amount.InInputCurrency)
+}
+
+// ChangeCurrencyWithAmount respond to the user action of changing the current input currency to a new one,
+// while also updating the input amount, needed for performing the necessary conversion.
+// Note: this state machine doesn't receive partial updates for the input amount each time the
+// user types or deletes a digit, so ChangeCurrencyWithAmount needs to receive the updates input amount.
+func (s *EnterAmountState) ChangeCurrencyWithAmount(currency string, inputAmount *MonetaryAmount) error {
+ exchangeRateWindow := s.PaymentContext.ExchangeRateWindow
+
+ newTotalBalance := s.PaymentContext.toBitcoinAmount(
+ s.PaymentContext.totalBalance(),
+ currency,
+ )
+
+ var amount *BitcoinAmount
+ // IF amount == balance THEN tffa/useAllFunds
+ // See EnterAmount transition.
+ if inputAmount.String() == s.TotalBalance.InInputCurrency.String() {
+ amount = newTotalBalance
+ } else {
+ amount = &BitcoinAmount{
+ InSat: int64(inputAmount.toBtc(exchangeRateWindow)),
+ InInputCurrency: exchangeRateWindow.convert(inputAmount, currency),
+ InPrimaryCurrency: exchangeRateWindow.convert(inputAmount, s.PaymentContext.PrimaryCurrency),
+ }
+ }
+
+ nextState := &EnterAmountState{
+ Resolved: s.Resolved,
+ Amount: amount,
+ TotalBalance: newTotalBalance,
+ }
+ nextState.emitUpdate(UpdateInPlace)
+
+ return nil
+}
+
+func (s *EnterAmountState) Back() error {
+ nextState := &AbortState{
+ BaseState: BaseState{
+ listener: s.listener, // create new BaseState to avoid passing along update string
+ },
+ PreviousState: s,
+ }
+ nextState.emit()
+
+ return nil
+}
+
+func (s *EnterAmountState) emit() {
+ s.emitUpdate(UpdateAll)
+}
+
+func (s *EnterAmountState) emitUpdate(update string) {
+ s.update = update
+ s.listener.OnEnterAmount(s)
+}
+
+// -------------------------------------------------------------------------------------------------
+
+type EnterDescriptionState struct {
+ *Resolved
+ *AmountInfo
+ *Validated
+ Note string
+}
+
+func (s *EnterDescriptionState) EnterDescription(description string) error {
+
+ if s.PaymentIntent.URI.Invoice == nil {
+
+ nextState := &ConfirmState{
+ Resolved: s.Resolved,
+ AmountInfo: s.AmountInfo,
+ Validated: s.Validated,
+ Note: description,
+ }
+ nextState.emit()
+
+ } else {
+
+ nextState := &ConfirmLightningState{
+ Resolved: s.Resolved,
+ AmountInfo: s.AmountInfo,
+ Validated: s.Validated,
+ Note: description,
+ }
+
+ nextState.emit()
+ }
+
+ return nil
+}
+
+func (s *EnterDescriptionState) Back() error {
+
+ if s.PaymentIntent.Amount() != nil {
+
+ nextState := &AbortState{
+ BaseState: BaseState{
+ listener: s.listener, // create new BaseState to avoid passing along update string
+ },
+ PreviousState: s,
+ }
+ nextState.emit()
+
+ } else {
+
+ amount := s.Amount
+ if s.TakeFeeFromAmount {
+ amount = s.TotalBalance
+ }
+
+ nextState := &EnterAmountState{
+ Resolved: s.Resolved,
+ Amount: amount,
+ TotalBalance: s.TotalBalance,
+ }
+ nextState.emit()
+
+ }
+
+ return nil
+}
+
+func (s *EnterDescriptionState) emit() {
+ s.emitUpdate(UpdateAll)
+}
+
+func (s *EnterDescriptionState) emitUpdate(update string) {
+ s.update = update
+ s.listener.OnEnterDescription(s)
+}
+
+// -------------------------------------------------------------------------------------------------
+
+type ValidateState struct {
+ *Resolved
+ *AmountInfo
+ Note string
+}
+
+func (s *ValidateState) Continue() error {
+
+ amountInSat := s.Amount.toBtc()
+ if s.TakeFeeFromAmount {
+ amountInSat = btcutil.Amount(s.TotalBalance.InSat)
+ }
+
+ inputCurrency := s.Amount.InInputCurrency.Currency
+
+ analyzer := newPaymentAnalyzer(s.PaymentContext)
+
+ var analysis *operation.PaymentAnalysis
+ var err error
+
+ analysis, err = analyzer.ToAddress(&operation.PaymentToAddress{
+ TakeFeeFromAmount: s.TakeFeeFromAmount,
+ AmountInSat: int64(amountInSat),
+ FeeRateInSatsPerVByte: s.FeeRateInSatsPerVByte,
+ })
+ if err != nil {
+ return err
+ }
+
+ switch analysis.Status {
+ case operation.AnalysisStatusOk:
+ return s.emitAnalysisOk(analysis, false)
+
+ case operation.AnalysisStatusUnpayable:
+
+ // redo the analysis with min fee rate
+ minFeeAnalyzer := newPaymentAnalyzer(s.PaymentContext)
+ minFeeAnalysis, err := minFeeAnalyzer.ToAddress(&operation.PaymentToAddress{
+ TakeFeeFromAmount: s.TakeFeeFromAmount,
+ AmountInSat: int64(amountInSat),
+ FeeRateInSatsPerVByte: s.PaymentContext.MinFeeRateInSatsPerVByte,
+ })
+ if err != nil {
+ return err
+ }
+
+ switch minFeeAnalysis.Status {
+ case operation.AnalysisStatusOk:
+ // The UI expects to show the full unpayable fee, so we still
+ // use the failed analysis here.
+ return s.emitAnalysisOk(analysis, true)
+
+ // We couldn't even compute the minimum fee, amount must be above balance. We'll
+ // show the minimum fee for all funds (which is, of course, a lie):
+ case operation.AnalysisStatusUnpayable:
+
+ // We'll do a "best effort" guess of what minimum balance the user would
+ // need to pay for the requested amount, even though that's not real or
+ // accurate (its impossible to calculate the fee for an amount greater na
+ // than the sum of the utxos, because the fee depends on the number and
+ // the type of the utxos used). Our best guess is calculating the fee of a
+ // use all funds transaction and adding that to the requested amount.
+
+ return s.emitBalanceError(OperationErrorUnpayable, minFeeAnalysis, inputCurrency)
+ }
+
+ case operation.AnalysisStatusAmountGreaterThanBalance,
+ operation.AnalysisStatusAmountTooSmall:
+
+ switch analysis.Status {
+ case operation.AnalysisStatusUnpayable:
+ return s.emitBalanceError(OperationErrorUnpayable, analysis, inputCurrency)
+
+ case operation.AnalysisStatusAmountGreaterThanBalance:
+ return s.emitBalanceError(OperationErrorAmountGreaterThanBalance, analysis, inputCurrency)
+
+ case operation.AnalysisStatusAmountTooSmall:
+ return s.emitError(OperationErrorAmountTooSmall)
+ }
+
+ default:
+ return fmt.Errorf("unrecognized analysis status: %v", analysis.Status)
+ }
+
+ return nil
+}
+
+func (s *ValidateState) emitAnalysisOk(analysis *operation.PaymentAnalysis, feeNeedsChange bool) error {
+
+ amount := s.Amount
+ if s.TakeFeeFromAmount {
+ amount = s.PaymentContext.toBitcoinAmount(
+ analysis.AmountInSat,
+ s.Amount.InInputCurrency.Currency,
+ )
+ }
+ fee := s.PaymentContext.toBitcoinAmount(
+ analysis.FeeInSat,
+ s.Amount.InInputCurrency.Currency,
+ )
+ validated := &Validated{
+ analysis: analysis,
+ Fee: fee,
+ FeeNeedsChange: feeNeedsChange,
+ Total: amount.add(fee),
+ }
+
+ amountInfo := s.AmountInfo.mutating(func(info *AmountInfo) {
+ info.Amount = amount
+ })
+
+ if s.PaymentIntent.URI.Message != "" || s.Note != "" {
+
+ note := s.Note
+ if note == "" {
+ note = s.PaymentIntent.URI.Message
+ }
+
+ nextState := &ConfirmState{
+ Resolved: s.Resolved,
+ AmountInfo: amountInfo,
+ Validated: validated,
+ Note: note,
+ }
+ nextState.emitUpdate(s.update)
+
+ } else {
+
+ nextState := &EnterDescriptionState{
+ Resolved: s.Resolved,
+ AmountInfo: amountInfo,
+ Validated: validated,
+ }
+ nextState.emit()
+ }
+
+ return nil
+}
+
+func (s *ValidateState) emit() {
+ s.emitUpdate(UpdateAll)
+}
+
+func (s *ValidateState) emitUpdate(update string) {
+ s.update = update
+ s.listener.OnValidate(s)
+}
+
+// -------------------------------------------------------------------------------------------------
+
+type ValidateLightningState struct {
+ *Resolved
+ *AmountInfo
+ Note string
+}
+
+func (s *ValidateLightningState) Continue() error {
+
+ amountInSat := s.Amount.toBtc()
+ if s.TakeFeeFromAmount {
+ amountInSat = btcutil.Amount(s.TotalBalance.InSat)
+ }
+
+ inputCurrency := s.Amount.InInputCurrency.Currency
+
+ swap := s.PaymentContext.SubmarineSwap
+
+ analyzer := newPaymentAnalyzer(s.PaymentContext)
+ analysis, err := analyzer.ToInvoice(&operation.PaymentToInvoice{
+ TakeFeeFromAmount: s.TakeFeeFromAmount,
+ AmountInSat: int64(amountInSat),
+ SwapFees: swap.Fees.toInternalType(),
+ BestRouteFees: swap.toBestRouteFeesInternalType(),
+ FundingOutputPolicies: swap.FundingOutputPolicies.toInternalType(),
+ })
+ if err != nil {
+ return err
+ }
+
+ switch analysis.Status {
+ case operation.AnalysisStatusOk:
+
+ s.emitAnalysisOk(analysis)
+
+ case operation.AnalysisStatusUnpayable:
+
+ return s.emitBalanceError(OperationErrorUnpayable, analysis, inputCurrency)
+
+ case operation.AnalysisStatusAmountGreaterThanBalance:
+
+ return s.emitBalanceError(OperationErrorAmountGreaterThanBalance, analysis, inputCurrency)
+
+ case operation.AnalysisStatusAmountTooSmall:
+ return s.emitError(OperationErrorAmountTooSmall)
+
+ default:
+ return fmt.Errorf("unrecognized analysis status: %v", analysis.Status)
+ }
+
+ return nil
+}
+
+func (s *ValidateLightningState) emitAnalysisOk(analysis *operation.PaymentAnalysis) {
+ note := s.Note
+ if note != "" {
+ note = s.PaymentIntent.URI.Invoice.Description
+ }
+
+ amount := s.Amount
+ if s.TakeFeeFromAmount {
+ amount = s.PaymentContext.toBitcoinAmount(
+ analysis.AmountInSat,
+ s.Amount.InInputCurrency.Currency,
+ )
+ }
+
+ onchainFee := s.PaymentContext.toBitcoinAmount(
+ analysis.FeeInSat,
+ s.Amount.InInputCurrency.Currency,
+ )
+
+ swapFees := newSwapFeesFromInternal(analysis.SwapFees)
+ var offchainFee *BitcoinAmount
+ var totalFee *BitcoinAmount
+
+ if swapFees.DebtType == DebtTypeLend {
+ offchainFee = s.PaymentContext.toBitcoinAmount(
+ swapFees.RoutingFeeInSat,
+ s.Amount.InInputCurrency.Currency,
+ )
+ totalFee = offchainFee
+ } else {
+ offchainFee = s.PaymentContext.toBitcoinAmount(
+ swapFees.RoutingFeeInSat+swapFees.OutputPaddingInSat,
+ s.Amount.InInputCurrency.Currency,
+ )
+ totalFee = onchainFee.add(offchainFee)
+ }
+
+ isOneConf := analysis.SwapFees.ConfirmationsNeeded > 0
+
+ validated := &Validated{
+ Fee: totalFee,
+ Total: amount.add(totalFee),
+ analysis: analysis,
+ SwapInfo: &SwapInfo{
+ OnchainFee: onchainFee,
+ SwapFees: swapFees,
+ IsOneConf: isOneConf,
+ },
+ }
+
+ amountInfo := s.AmountInfo.mutating(func(info *AmountInfo) {
+ info.Amount = amount
+ })
+
+ if note != "" {
+
+ nextState := &ConfirmLightningState{
+ Resolved: s.Resolved,
+ AmountInfo: amountInfo,
+ Validated: validated,
+ Note: s.Note,
+ }
+ nextState.emit()
+
+ } else {
+
+ nextState := &EnterDescriptionState{
+ Resolved: s.Resolved,
+ AmountInfo: amountInfo,
+ Validated: validated,
+ }
+ nextState.emit()
+ }
+}
+
+func (s *ValidateLightningState) emit() {
+ s.emitUpdate(UpdateAll)
+}
+
+func (s *ValidateLightningState) emitUpdate(update string) {
+ s.update = update
+ s.listener.OnValidateLightning(s)
+}
+
+// -------------------------------------------------------------------------------------------------
+
+const (
+ OperationErrorUnpayable = "Unpayable"
+ OperationErrorAmountGreaterThanBalance = "AmountGreaterThanBalance"
+ OperationErrorAmountTooSmall = "AmountTooSmall"
+ OperationErrorInvalidAddress = "InvalidAddress"
+ OperationErrorInvoiceExpired = "InvoiceExpired"
+)
+
+type ErrorState struct {
+ BaseState
+ PaymentIntent *PaymentIntent
+ Error string
+}
+
+func (s *ErrorState) emit() {
+ s.listener.OnError(s)
+}
+
+type BalanceErrorState struct {
+ BaseState
+ PaymentIntent *PaymentIntent
+ TotalAmount *MonetaryAmount
+ Balance *MonetaryAmount
+ Error string
+}
+
+func (s *BalanceErrorState) emit() {
+ s.listener.OnBalanceError(s)
+}
+
+// -------------------------------------------------------------------------------------------------
+
+type ConfirmState struct {
+ *Resolved
+ *AmountInfo
+ *Validated
+ Note string
+}
+
+func (s *ConfirmState) OpenFeeEditor() error {
+
+ maxFeeRate := newPaymentAnalyzer(s.PaymentContext).MaxFeeRateToAddress(&operation.PaymentToAddress{
+ TakeFeeFromAmount: s.TakeFeeFromAmount,
+ AmountInSat: s.Amount.InSat,
+ FeeRateInSatsPerVByte: s.FeeRateInSatsPerVByte,
+ })
+
+ next := &EditFeeState{
+ Resolved: s.Resolved,
+ AmountInfo: s.AmountInfo,
+ Validated: s.Validated,
+ Note: s.Note,
+ MaxFeeRateInSatsPerVByte: maxFeeRate,
+ }
+
+ next.emit()
+
+ return nil
+}
+
+func (s *ConfirmState) Back() error {
+
+ nextState := &EnterDescriptionState{
+ Resolved: s.Resolved,
+ AmountInfo: s.AmountInfo,
+ Validated: s.Validated,
+ Note: s.Note,
+ }
+ nextState.emit()
+
+ return nil
+}
+
+func (s *ConfirmState) emit() {
+ s.emitUpdate(UpdateAll)
+}
+
+func (s *ConfirmState) emitUpdate(update string) {
+ s.update = update
+ s.listener.OnConfirm(s)
+}
+
+// -------------------------------------------------------------------------------------------------
+
+type EditFeeState struct {
+ *Resolved
+ *AmountInfo
+ *Validated
+ Note string
+ MaxFeeRateInSatsPerVByte float64
+}
+
+func (s *EditFeeState) MinFeeRateForTarget(target int) (float64, error) {
+ feeWindow := s.PaymentContext.FeeWindow.toInternalType()
+ return feeWindow.MinimumFeeRate(uint(target))
+}
+
+// TODO this currently ignores and forgets input currency, which is important for amount display
+// logic in Edit Fee screens
+func (s *EditFeeState) CalculateFee(rateInSatsPerVByte float64) (*FeeState, error) {
+ amountInSat := s.Amount.InSat
+ if s.TakeFeeFromAmount {
+ amountInSat = s.TotalBalance.InSat
+ }
+
+ analyzer := newPaymentAnalyzer(s.PaymentContext)
+ analysis, err := analyzer.ToAddress(&operation.PaymentToAddress{
+ TakeFeeFromAmount: s.TakeFeeFromAmount,
+ AmountInSat: amountInSat,
+ FeeRateInSatsPerVByte: rateInSatsPerVByte,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // TODO(newop): add targetblock to analysis result
+ switch analysis.Status {
+ // TODO: this should never happen, right? It should have been detected earlier
+ case operation.AnalysisStatusAmountGreaterThanBalance, operation.AnalysisStatusAmountTooSmall:
+ return &FeeState{
+ State: FeeStateNoPossibleFee,
+ }, nil
+ case operation.AnalysisStatusUnpayable:
+ return &FeeState{
+ State: FeeStateNeedsChange,
+ Amount: s.toBitcoinAmount(analysis.FeeInSat),
+ RateInSatsPerVByte: rateInSatsPerVByte,
+ TargetBlocks: s.PaymentContext.FeeWindow.nextHighestBlock(rateInSatsPerVByte),
+ }, nil
+ case operation.AnalysisStatusOk:
+ return &FeeState{
+ State: FeeStateFinalFee,
+ Amount: s.toBitcoinAmount(analysis.FeeInSat),
+ RateInSatsPerVByte: rateInSatsPerVByte,
+ TargetBlocks: s.PaymentContext.FeeWindow.nextHighestBlock(rateInSatsPerVByte),
+ }, nil
+ default:
+ return nil, fmt.Errorf("unrecognized analysis status: %v", analysis.Status)
+ }
+}
+
+func (s *EditFeeState) SetFeeRate(rateInSatsPerVByte float64) error {
+ // We deref to copy before mutating
+ amountInfo := s.AmountInfo.mutating(func(info *AmountInfo) {
+ info.FeeRateInSatsPerVByte = rateInSatsPerVByte
+ })
+
+ nextState := &ValidateState{
+ Resolved: s.Resolved,
+ AmountInfo: amountInfo,
+ Note: s.Note,
+ }
+ nextState.emitUpdate(UpdateInPlace)
+
+ return nil
+}
+
+func (s *EditFeeState) CloseEditor() error {
+ nextState := &ValidateState{
+ Resolved: s.Resolved,
+ AmountInfo: s.AmountInfo,
+ Note: s.Note,
+ }
+ nextState.emit()
+
+ return nil
+}
+
+func (s *EditFeeState) toBitcoinAmount(sats int64) *BitcoinAmount {
+ return NewMonetaryAmountFromSatoshis(sats).toBitcoinAmount(
+ s.PaymentContext.ExchangeRateWindow,
+ s.PaymentContext.PrimaryCurrency,
+ )
+}
+
+func (s *EditFeeState) emit() {
+ s.listener.OnEditFee(s)
+}
+
+// -------------------------------------------------------------------------------------------------
+
+type ConfirmLightningState struct {
+ *Resolved
+ *AmountInfo
+ *Validated
+ Note string
+}
+
+func (s *ConfirmLightningState) Back() error {
+
+ nextState := &EnterDescriptionState{
+ Resolved: s.Resolved,
+ AmountInfo: s.AmountInfo,
+ Validated: s.Validated,
+ Note: s.Note,
+ }
+ nextState.emit()
+
+ return nil
+}
+
+func (s *ConfirmLightningState) emit() {
+ s.emitUpdate(UpdateAll)
+}
+
+func (s *ConfirmLightningState) emitUpdate(update string) {
+ s.update = update
+ s.listener.OnConfirmLightning(s)
+}
+
+// -------------------------------------------------------------------------------------------------
+
+type AbortState struct {
+ BaseState
+ PreviousState interface {
+ emitUpdate(update string)
+ }
+}
+
+func (s *AbortState) emit() {
+ s.listener.OnAbort(s)
+}
+
+func (s *AbortState) Cancel() {
+ nextState := s.PreviousState
+ nextState.emitUpdate(UpdateEmpty)
+}
diff --git a/libwallet/newop/state_test.go b/libwallet/newop/state_test.go
new file mode 100644
index 0000000..d2f5c58
--- /dev/null
+++ b/libwallet/newop/state_test.go
@@ -0,0 +1,1398 @@
+package newop
+
+import (
+ "testing"
+ "time"
+
+ "github.com/muun/libwallet"
+ "github.com/shopspring/decimal"
+)
+
+type testListener struct {
+ ch chan State
+}
+
+func newTestListener() *testListener {
+ return &testListener{ch: make(chan State, 10)}
+}
+
+func (t *testListener) OnStart(nextState *StartState) { t.ch <- nextState }
+func (t *testListener) OnResolve(nextState *ResolveState) { t.ch <- nextState }
+func (t *testListener) OnEnterAmount(nextState *EnterAmountState) { t.ch <- nextState }
+func (t *testListener) OnEnterDescription(nextState *EnterDescriptionState) { t.ch <- nextState }
+func (t *testListener) OnValidate(nextState *ValidateState) { t.ch <- nextState }
+func (t *testListener) OnValidateLightning(nextState *ValidateLightningState) { t.ch <- nextState }
+func (t *testListener) OnConfirm(nextState *ConfirmState) { t.ch <- nextState }
+func (t *testListener) OnConfirmLightning(nextState *ConfirmLightningState) { t.ch <- nextState }
+func (t *testListener) OnEditFee(nextState *EditFeeState) { t.ch <- nextState }
+func (t *testListener) OnError(nextState *ErrorState) { t.ch <- nextState }
+func (t *testListener) OnBalanceError(nextState *BalanceErrorState) { t.ch <- nextState }
+func (t *testListener) OnAbort(nextState *AbortState) { t.ch <- nextState }
+
+func (t *testListener) next() State {
+ return <-t.ch
+}
+
+var _ TransitionListener = &testListener{}
+
+var testContext = createContext()
+
+func createContext() *PaymentContext {
+ var context = &PaymentContext{
+ NextTransactionSize: &NextTransactionSize{},
+ ExchangeRateWindow: &ExchangeRateWindow{
+ rates: make(map[string]float64),
+ },
+ FeeWindow: &FeeWindow{},
+ PrimaryCurrency: "BTC",
+ MinFeeRateInSatsPerVByte: 1.0,
+ }
+ context.NextTransactionSize.AddSizeForAmount(&SizeForAmount{
+ AmountInSat: 100_000_000,
+ SizeInVByte: 240,
+ })
+
+ context.ExchangeRateWindow.AddRate("BTC", 1)
+ context.ExchangeRateWindow.AddRate("USD", 32_000)
+ context.ExchangeRateWindow.AddRate("ARS", 9_074_813.98)
+
+ context.FeeWindow.PutTargetedFees(1, 400.0)
+ context.FeeWindow.PutTargetedFees(15, 120.0)
+ context.FeeWindow.PutTargetedFees(90, 8.0)
+
+ return context
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestBarebonesOnChainFixedAmountFixedFee(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.1&description=foo", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ resolveState.SetContext(testContext)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("bar")
+
+ confirmState := listener.next().(*ConfirmState)
+
+ if confirmState.Note != "bar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.1 BTC" {
+ t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.00096 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.10096 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestBarebonesOnChainFixedAmountFixedDescriptionFixedFee(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.1&message=foo", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ resolveState.SetContext(testContext)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ confirmState := listener.next().(*ConfirmState)
+
+ if confirmState.Note != "foo" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.1 BTC" {
+ t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.00096 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.10096 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestOnChainFixedAmountChangeFee(t *testing.T) {
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.1&description=foo", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ resolveState.SetContext(testContext)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("bar")
+
+ confirmState := listener.next().(*ConfirmState)
+
+ if confirmState.FeeRateInSatsPerVByte != 400 {
+ t.Fatalf("expected initial fee rate to be 400, got %v", confirmState.FeeRateInSatsPerVByte)
+ }
+
+ newFeeRate := 15.0
+
+ confirmState.OpenFeeEditor()
+
+ editFeeState := listener.next().(*EditFeeState)
+
+ feeState, err := editFeeState.CalculateFee(newFeeRate)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if feeState.State != FeeStateFinalFee {
+ t.Fatalf("expected fee state to be FinalFee, got %v", feeState.State)
+ }
+ if feeState.Amount.InSat != 3600 {
+ t.Fatalf("expected fee amount to be 3600, got %v", feeState.Amount.InSat)
+ }
+
+ editFeeState.SetFeeRate(newFeeRate)
+
+ validateState = listener.next().(*ValidateState)
+ validateState.Continue()
+
+ confirmState = listener.next().(*ConfirmState)
+
+ if confirmState.Note != "bar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.1 BTC" {
+ t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.000036 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.100036 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestOnChainFixedAmountFeeNeedsChange(t *testing.T) {
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.9999&description=foo", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ resolveState.SetContext(testContext)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("bar")
+
+ confirmState := listener.next().(*ConfirmState)
+
+ if confirmState.FeeRateInSatsPerVByte != 400 {
+ t.Fatalf("expected initial fee rate to be 400, got %v", confirmState.FeeRateInSatsPerVByte)
+ }
+
+ if !confirmState.FeeNeedsChange {
+ t.Fatalf("expected initial fee to be unpayable and need changing, got %v which is unpayable", confirmState.FeeRateInSatsPerVByte)
+ }
+
+ newFeeRate := 15.0
+
+ confirmState.OpenFeeEditor()
+
+ editFeeState := listener.next().(*EditFeeState)
+
+ feeState, err := editFeeState.CalculateFee(newFeeRate)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if feeState.State != FeeStateFinalFee {
+ t.Fatalf("expected fee state to be FinalFee, got %v", feeState.State)
+ }
+ if feeState.Amount.InSat != 3600 {
+ t.Fatalf("expected fee amount to be 3600, got %v", feeState.Amount.InSat)
+ }
+
+ editFeeState.SetFeeRate(newFeeRate)
+
+ validateState = listener.next().(*ValidateState)
+ validateState.Continue()
+
+ confirmState = listener.next().(*ConfirmState)
+
+ if confirmState.FeeNeedsChange {
+ t.Fatalf("expected fee to be payable, got %v which is not unpayable", confirmState.FeeRateInSatsPerVByte)
+ }
+
+ if confirmState.Note != "bar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.9999 BTC" {
+ t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.000036 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.999936 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestOnChainFixedAmountNoPossibleFee(t *testing.T) {
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.9999999&description=foo", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ resolveState.SetContext(testContext)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ balanceErrorState := listener.next().(*BalanceErrorState)
+
+ if balanceErrorState.Error != OperationErrorUnpayable {
+ t.Fatalf("expected initial fee to be unpayable but got %s", balanceErrorState.Error)
+ }
+
+ if balanceErrorState.TotalAmount.String() != "1.0000023 BTC" {
+ t.Fatalf("expected total to match, got %v", balanceErrorState.TotalAmount)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestOnChainFixedAmountTooSmall(t *testing.T) {
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=0.0000004&description=foo", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ resolveState.SetContext(testContext)
+
+ errorState := listener.next().(*ErrorState)
+ if errorState.Error != OperationErrorAmountTooSmall {
+ t.Fatalf("expected amount to be too small but got %s", errorState.Error)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestOnChainFixedAmountGreaterThanbalance(t *testing.T) {
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=2.0&description=foo", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ resolveState.SetContext(testContext)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ balanceErrorState := listener.next().(*BalanceErrorState)
+
+ if balanceErrorState.Error != OperationErrorAmountGreaterThanBalance {
+ t.Fatalf("expected amount to be too small but got %s", balanceErrorState.Error)
+ }
+
+ if balanceErrorState.TotalAmount.String() != "2 BTC" {
+ t.Fatalf("expected total to match, got %v", balanceErrorState.TotalAmount)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestOnChainSendZeroFundsWithZeroBalance(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu", libwallet.Regtest())
+
+ context := createContext()
+ context.NextTransactionSize = &NextTransactionSize{}
+
+ resolveState := listener.next().(*ResolveState)
+ resolveState.SetContext(context)
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(0), true)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ errorState := listener.next().(*ErrorState)
+
+ if errorState.Error != OperationErrorAmountTooSmall {
+ t.Fatalf("expected error to be amount too small but got %s", errorState.Error)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestOnChainTFFA(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+ resolveState.SetContext(testContext)
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(100_000_000), true)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("bar")
+ if enterDescriptionState.Amount.InInputCurrency.String() != "0.99904 BTC" {
+ t.Fatalf("expected amount to match input, got %v", enterDescriptionState.Amount.InInputCurrency)
+ }
+
+ confirmState := listener.next().(*ConfirmState)
+
+ if confirmState.Note != "bar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.99904 BTC" {
+ t.Fatalf("expected amount to match input, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.00096 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "1 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+
+ confirmState.Back()
+
+ // check we preserve values correctly
+ enterDescriptionState = listener.next().(*EnterDescriptionState)
+ if enterDescriptionState.Note != "bar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if enterDescriptionState.Amount.InInputCurrency.String() != "0.99904 BTC" {
+ t.Fatalf("expected amount to match input, got %v", enterDescriptionState.Amount.InInputCurrency)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestInvalidAmountEmitsInvalidAddress(t *testing.T) {
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu?amount=bananabanana", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ resolveState.SetContext(testContext)
+
+ errorState := listener.next().(*ErrorState)
+
+ if errorState.Error != OperationErrorInvalidAddress {
+ t.Fatalf("expected error to be invalid address but got %s", errorState.Error)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestOnChainBack(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+ resolveState.SetContext(testContext)
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(100_000_000), true)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("bar")
+
+ confirmState := listener.next().(*ConfirmState)
+ confirmState.Back()
+
+ enterDescriptionState = listener.next().(*EnterDescriptionState)
+ enterDescriptionState.Back()
+
+ enterAmountState = listener.next().(*EnterAmountState)
+ // TODO when deleting this method impl (deprecated) rm lines below up until the call to ChangeCurrencyWithAmount
+ enterAmountState.ChangeCurrency("USD")
+
+ enterAmountState = listener.next().(*EnterAmountState)
+ enterAmountState.Back()
+
+ abortState := listener.next().(*AbortState)
+ if abortState.update != UpdateAll {
+ t.Fatalf("expected normal/full update , got %v", abortState.update)
+ }
+ abortState.Cancel()
+
+ enterAmountState = listener.next().(*EnterAmountState)
+ if enterAmountState.update != UpdateEmpty {
+ t.Fatalf("expected empty update, got %v", enterAmountState.update)
+ }
+ enterAmountState.Back()
+
+ abortState = listener.next().(*AbortState)
+ if abortState.update != UpdateAll {
+ t.Fatalf("expected normal/full update , got %v", abortState.update)
+ }
+ abortState.Cancel()
+
+ enterAmountState = listener.next().(*EnterAmountState)
+ enterAmountState.ChangeCurrencyWithAmount("USD", NewMonetaryAmountFromSatoshis(1_000_000))
+
+ enterAmountState = listener.next().(*EnterAmountState)
+ enterAmountState.Back()
+
+ abortState = listener.next().(*AbortState)
+ if abortState.update != UpdateAll {
+ t.Fatalf("expected normal/full update , got %v", abortState.update)
+ }
+ abortState.Cancel()
+
+ enterAmountState = listener.next().(*EnterAmountState)
+ if enterAmountState.update != UpdateEmpty {
+ t.Fatalf("expected empty update, got %v", enterAmountState.update)
+ }
+ enterAmountState.Back()
+
+ abortState = listener.next().(*AbortState)
+ if abortState.update != UpdateAll {
+ t.Fatalf("expected normal/full update , got %v", abortState.update)
+ }
+ abortState.Cancel()
+
+ // One more, just for the giggles
+ enterAmountState = listener.next().(*EnterAmountState)
+ enterAmountState.Back()
+
+ _ = listener.next().(*AbortState)
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestOnChainChangeCurrency(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu", libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+ resolveState.SetContext(testContext)
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ inputAmountCurrency := enterAmountState.Amount.InInputCurrency.Currency
+ balanceCurrency := enterAmountState.TotalBalance.InInputCurrency.Currency
+ if inputAmountCurrency != balanceCurrency {
+ t.Fatalf("expected amount currency (%v) to match balance currency (%v)", inputAmountCurrency, balanceCurrency)
+ }
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(100_000_000), true)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ if enterDescriptionState.Amount.InInputCurrency.String() != "0.99904 BTC" {
+ t.Fatalf("expected amount to match input, got %v", enterDescriptionState.Amount.InInputCurrency)
+ }
+
+ enterDescriptionState.Back()
+
+ enterAmountState = listener.next().(*EnterAmountState)
+ // TODO when deleting this method impl (deprecated) rm lines below up until the call to ChangeCurrencyWithAmount
+ enterAmountState.ChangeCurrency("USD")
+
+ enterAmountState = listener.next().(*EnterAmountState)
+ if enterAmountState.update != UpdateInPlace {
+ t.Fatalf("expected UpdateInPlace, got '%v'", enterAmountState.update)
+ }
+ if enterAmountState.Amount.InSat != 100_000_000 {
+ t.Fatalf("expected amount to match 100_000_000, got '%v'", enterAmountState.Amount.InSat)
+ }
+ if enterAmountState.Amount.InInputCurrency.String() != "32000 USD" {
+ t.Fatalf("expected amount to match 32000 USD, got '%v'", enterAmountState.Amount.InInputCurrency.String())
+ }
+ if enterAmountState.Amount.InPrimaryCurrency.String() != "1 BTC" {
+ t.Fatalf("expected amount to match 1 BTC, got '%v'", enterAmountState.Amount.InPrimaryCurrency.String())
+ }
+
+ enterAmountState.ChangeCurrency("BTC")
+ enterAmountState = listener.next().(*EnterAmountState)
+ if enterAmountState.update != UpdateInPlace {
+ t.Fatalf("expected UpdateInPlace, got '%v'", enterAmountState.update)
+ }
+ if enterAmountState.Amount.InSat != 100_000_000 {
+ t.Fatalf("expected amount to match 100_000_000, got '%v'", enterAmountState.Amount.InSat)
+ }
+ if enterAmountState.Amount.InInputCurrency.String() != "1 BTC" {
+ t.Fatalf("expected amount to match 1 BTC, got '%v'", enterAmountState.Amount.InInputCurrency.String())
+ }
+ if enterAmountState.Amount.InPrimaryCurrency.String() != "1 BTC" {
+ t.Fatalf("expected amount to match 1 BTC, got '%v'", enterAmountState.Amount.InPrimaryCurrency.String())
+ }
+
+ enterAmountState.ChangeCurrencyWithAmount("USD", NewMonetaryAmountFromSatoshis(1_000_000))
+ enterAmountState = listener.next().(*EnterAmountState)
+ if enterAmountState.update != UpdateInPlace {
+ t.Fatalf("expected UpdateInPlace, got '%v'", enterAmountState.update)
+ }
+ if enterAmountState.Amount.InSat != 1_000_000 {
+ t.Fatalf("expected amount to match 1_000_000, got '%v'", enterAmountState.Amount.InSat)
+ }
+ if enterAmountState.Amount.InInputCurrency.String() != "320 USD" {
+ t.Fatalf("expected amount to match 320 USD, got '%v'", enterAmountState.Amount.InInputCurrency.String())
+ }
+ if enterAmountState.Amount.InPrimaryCurrency.String() != "0.01 BTC" {
+ t.Fatalf("expected amount to match 0.01 BTC, got '%v'", enterAmountState.Amount.InPrimaryCurrency.String())
+ }
+
+ enterAmountState.ChangeCurrencyWithAmount("BTC", enterAmountState.Amount.InInputCurrency)
+ enterAmountState = listener.next().(*EnterAmountState)
+ if enterAmountState.update != UpdateInPlace {
+ t.Fatalf("expected UpdateInPlace, got '%v'", enterAmountState.update)
+ }
+ if enterAmountState.Amount.InSat != 1_000_000 {
+ t.Fatalf("expected amount to match 1_000_000, got '%v'", enterAmountState.Amount.InSat)
+ }
+ if enterAmountState.Amount.InInputCurrency.String() != "0.01 BTC" {
+ t.Fatalf("expected amount to match 0.01 BTC, got '%v'", enterAmountState.Amount.InInputCurrency.String())
+ }
+ if enterAmountState.Amount.InPrimaryCurrency.String() != "0.01 BTC" {
+ t.Fatalf("expected amount to match 0.01 BTC, got '%v'", enterAmountState.Amount.InPrimaryCurrency.String())
+ }
+
+ enterDescriptionState.EnterDescription("bar")
+ confirmState := listener.next().(*ConfirmState)
+
+ if confirmState.Note != "bar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.99904 BTC" {
+ t.Fatalf("expected amount to match input, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.00096 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "1 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+
+ confirmState.Back()
+
+ // check we preserve values correctly
+ enterDescriptionState = listener.next().(*EnterDescriptionState)
+ if enterDescriptionState.Note != "bar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if enterDescriptionState.Amount.InInputCurrency.String() != "0.99904 BTC" {
+ t.Fatalf("expected amount to match input, got %v", enterDescriptionState.Amount.InInputCurrency)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestLightningSendZeroFunds(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.NextTransactionSize = &NextTransactionSize{}
+ context.SubmarineSwap = &SubmarineSwap{
+ BestRouteFees: []*BestRouteFees{
+ {
+ MaxCapacity: 100_000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 1_000,
+ },
+ },
+ FundingOutputPolicies: &FundingOutputPolicies{
+ MaximumDebtInSat: 0,
+ PotentialCollectInSat: 0,
+ MaxAmountInSatFor0Conf: 25_000,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(0), false)
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ errorState := listener.next().(*ErrorState)
+
+ if errorState.Error != OperationErrorAmountTooSmall {
+ t.Fatalf("expected error to be amount too small but got %s", errorState.Error)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestLightningSendZeroFundsTFFA(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.NextTransactionSize = &NextTransactionSize{}
+ context.SubmarineSwap = &SubmarineSwap{
+ BestRouteFees: []*BestRouteFees{
+ {
+ MaxCapacity: 100_000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 1_000,
+ },
+ },
+ FundingOutputPolicies: &FundingOutputPolicies{
+ MaximumDebtInSat: 0,
+ PotentialCollectInSat: 0,
+ MaxAmountInSatFor0Conf: 25_000,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(0), true)
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ errorState := listener.next().(*ErrorState)
+
+ if errorState.Error != OperationErrorAmountTooSmall {
+ t.Fatalf("expected error to be amount too small but got %s", errorState.Error)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestLightningSendNegativeFunds(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.NextTransactionSize = &NextTransactionSize{}
+ context.SubmarineSwap = &SubmarineSwap{
+ BestRouteFees: []*BestRouteFees{
+ {
+ MaxCapacity: 100_000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 1_000,
+ },
+ },
+ FundingOutputPolicies: &FundingOutputPolicies{
+ MaximumDebtInSat: 0,
+ PotentialCollectInSat: 0,
+ MaxAmountInSatFor0Conf: 25_000,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(-10), false)
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ errorState := listener.next().(*ErrorState)
+
+ if errorState.Error != OperationErrorAmountTooSmall {
+ t.Fatalf("expected error to be amount too small but got %s", errorState.Error)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestLightningSendNegativeFundsWithTFFA(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.NextTransactionSize = &NextTransactionSize{}
+ context.SubmarineSwap = &SubmarineSwap{
+ BestRouteFees: []*BestRouteFees{
+ {
+ MaxCapacity: 100_000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 1_000,
+ },
+ },
+ FundingOutputPolicies: &FundingOutputPolicies{
+ MaximumDebtInSat: 0,
+ PotentialCollectInSat: 0,
+ MaxAmountInSatFor0Conf: 25_000,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(-10), true)
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ errorState := listener.next().(*ErrorState)
+
+ if errorState.Error != OperationErrorAmountTooSmall {
+ t.Fatalf("expected error to be amount too small but got %s", errorState.Error)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestLightningExpiredInvoice(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt100u1ps3kdrgpp5klwrzs0u63sqnca8elqu86p98swxycw3fgjtmeddm2ljd7ymrlwsdqqcqzpgsp5zs02vngwrywtqhwygu44wp464lgjtqyc5h76vae073064p72znas9qyyssqz2263utx4n7r7n85s9wg3ma2zmg3xtg46nj3e6nnr6g67tnj6jwn7urvx5qukhqjzmcnuc4t7uqlxhftqwq4hxha3ests23fcmt5evqpazdmg2", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+ resolveState.SetContext(testContext)
+
+ errorState := listener.next().(*ErrorState)
+
+ if errorState.Error != OperationErrorInvoiceExpired {
+ t.Fatalf("expected error to match, got '%v'", errorState.Error)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestLightningInvoiceWithAmount(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt100u1ps3l5eepp5njeddrlmsg9cd2a4v508mqucz7tdge90vvp4f5n23gh7kthnjjdqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qsp52qtk90062t5mha837ulm77vf04ph4kaxerm8xugjdkp9gk6d8yqs9qyyssqw09stp3vy33dfjc6vcrdfmf58trg5pte6efph9pj9gwlg0w7anhz6aelv0p3r9qj6vrjjw9jyj6s9tjujec2fm9k8ag3yvgvwszswxsqhl6equ", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.SubmarineSwap = &SubmarineSwap{
+ Fees: &SwapFees{
+ RoutingFeeInSat: 0,
+ DebtType: DebtTypeNone,
+ DebtAmountInSat: 0,
+ OutputAmountInSat: 10000,
+ OutputPaddingInSat: 0,
+ ConfirmationsNeeded: 0,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629475653, 0))
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("foo")
+
+ confirmState := listener.next().(*ConfirmLightningState)
+
+ if confirmState.Note != "foo" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.0001 BTC" {
+ t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.0000192 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.0001192 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestLightningWithAmountBack(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt100u1ps3l5eepp5njeddrlmsg9cd2a4v508mqucz7tdge90vvp4f5n23gh7kthnjjdqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qsp52qtk90062t5mha837ulm77vf04ph4kaxerm8xugjdkp9gk6d8yqs9qyyssqw09stp3vy33dfjc6vcrdfmf58trg5pte6efph9pj9gwlg0w7anhz6aelv0p3r9qj6vrjjw9jyj6s9tjujec2fm9k8ag3yvgvwszswxsqhl6equ", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.SubmarineSwap = &SubmarineSwap{
+ Fees: &SwapFees{
+ RoutingFeeInSat: 0,
+ DebtType: DebtTypeNone,
+ DebtAmountInSat: 0,
+ OutputAmountInSat: 10000,
+ OutputPaddingInSat: 0,
+ ConfirmationsNeeded: 0,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629475653, 0))
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("foo")
+
+ confirmState := listener.next().(*ConfirmLightningState)
+
+ confirmState.Back()
+ enterDescriptionState = listener.next().(*EnterDescriptionState)
+ if enterDescriptionState.Note != "foo" {
+ t.Fatalf("expected note to match input, got '%v'", enterDescriptionState.Note)
+ }
+
+ enterDescriptionState.EnterDescription("bar")
+ confirmState = listener.next().(*ConfirmLightningState)
+
+ if confirmState.Note != "bar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.0001 BTC" {
+ t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.0000192 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.0001192 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestLightningInvoiceWithAmountAndDescription(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt100u1psjg8k6pp5n895ngj22v4dczwd8jrvvq76qvur2642y29m6x2faq0rgle2zwwsdq2vehk7cnpwgcqzpgsp5x9yeys2j294q402ewq2kfcas6wn63mk5q86ehe79plljzfwhr69s9qyyssq4exlg7ly068zc8dfh6ls5r69x0pmvdy9la70hw2vqwz9p2g4p5fyxr0hlkzrfnkmlx3kjrlecedatk96zuzs8a3cj48qg7vne6zp5ygpgzwd2x", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.SubmarineSwap = &SubmarineSwap{
+ Fees: &SwapFees{
+ RoutingFeeInSat: 0,
+ DebtType: DebtTypeNone,
+ DebtAmountInSat: 0,
+ OutputAmountInSat: 10000,
+ OutputPaddingInSat: 0,
+ ConfirmationsNeeded: 0,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629475653, 0))
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ confirmState := listener.next().(*ConfirmLightningState)
+
+ if confirmState.Note != "foobar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.0001 BTC" {
+ t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.0000192 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.0001192 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestLightningAmountlessInvoice(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.SubmarineSwap = &SubmarineSwap{
+ BestRouteFees: []*BestRouteFees{
+ {
+ MaxCapacity: 100_000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 1_000,
+ },
+ },
+ FundingOutputPolicies: &FundingOutputPolicies{
+ MaximumDebtInSat: 0,
+ PotentialCollectInSat: 0,
+ MaxAmountInSatFor0Conf: 25_000,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(10_000), false)
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("foo")
+
+ confirmState := listener.next().(*ConfirmLightningState)
+
+ if confirmState.Note != "foo" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.0001 BTC" {
+ t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.0000292 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.0001292 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+ if confirmState.SwapInfo.IsOneConf {
+ t.Fatalf("expected swap to be 0 conf")
+ }
+
+ // Test a bug where the one conf flag was set after back
+ confirmState.Back()
+ enterDescriptionState = listener.next().(*EnterDescriptionState)
+ if enterDescriptionState.SwapInfo.IsOneConf {
+ t.Fatalf("expected swap to be 0 conf")
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestInvoiceOneConf(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.SubmarineSwap = &SubmarineSwap{
+ BestRouteFees: []*BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 1000,
+ },
+ },
+ FundingOutputPolicies: &FundingOutputPolicies{
+ MaximumDebtInSat: 0,
+ PotentialCollectInSat: 0,
+ MaxAmountInSatFor0Conf: 0,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(10000), false)
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("foo")
+ if !enterDescriptionState.SwapInfo.IsOneConf {
+ t.Fatalf("expected swap to be 1 conf")
+ }
+
+ confirmState := listener.next().(*ConfirmLightningState)
+
+ if confirmState.Note != "foo" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.0001 BTC" {
+ t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.00097 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.00107 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+ if !confirmState.SwapInfo.IsOneConf {
+ t.Fatalf("expected swap to be 1 conf")
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestAmountConversion(t *testing.T) {
+
+ // This test repros a bug where we had:
+ // * Primary currency BTC
+ // * Fiat input
+ // Then, for amount/total the amount in sat and in primary currency differed
+ // in 1 sat.
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.SubmarineSwap = &SubmarineSwap{
+ BestRouteFees: []*BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 1000,
+ },
+ },
+ FundingOutputPolicies: &FundingOutputPolicies{
+ MaximumDebtInSat: 0,
+ PotentialCollectInSat: 0,
+ MaxAmountInSatFor0Conf: 0,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromFiat("2500", "ARS"), false)
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("foo")
+
+ confirmState := listener.next().(*ConfirmLightningState)
+ btcToSats := decimal.NewFromInt(100_000_000)
+
+ inPrimary := confirmState.Amount.InPrimaryCurrency.Value
+ // This emulates the conversions app make (and the Right Way™ to do it)
+ primaryInSats := inPrimary.Mul(btcToSats).RoundBank(0).IntPart()
+ if primaryInSats != confirmState.Amount.InSat {
+ t.Fatalf(
+ "expected amount in primary to match sats: %v != %v",
+ primaryInSats,
+ confirmState.Amount.InSat,
+ )
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestInvoiceUnpayable(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt1ps3l7zlpp5ngv7sl4wrjalma9navd0w9956pu0tcqwrltcnnzz83eeyk4rszxqdqqcqzpgrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqgqqqqqqqlgqqqqqqgq9qrzjq2eawnq2ywdmcpe56nk02tfamgfmsn0acp0zcn8z8cr0djkgpslr5qqpkgqqqqsqqqqqqqlgqqqqqqgq9qsp5luyagw4mtcq735je8ldukhlkg063cxzycjhpz2x2hjfq2mgk5xns9qyyssq7ydzdwyl7yr6ldpzqjjspmgrevw4lxt4jwfy3cxm7we20wveqq8p8khjxuq9u3v953e7t9r8ysfzx5r874vu3nd7w5yx5eqfxu0tevspgxr607", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.SubmarineSwap = &SubmarineSwap{
+ BestRouteFees: []*BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 1000,
+ },
+ },
+ FundingOutputPolicies: &FundingOutputPolicies{
+ MaximumDebtInSat: 0,
+ PotentialCollectInSat: 0,
+ MaxAmountInSatFor0Conf: 0,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629485164, 0))
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(99_999_999), false)
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ errorState := listener.next().(*BalanceErrorState)
+ if errorState.Error != OperationErrorUnpayable {
+ t.Fatalf("Expected error to be OperationErrorUnpayable: %v", errorState.Error)
+ }
+ if errorState.Balance.String() != "1 BTC" {
+ t.Fatalf("Expected balance to be 1 BTC: %v", errorState.Balance.String())
+ }
+ if errorState.TotalAmount.String() != "1.00097098 BTC" {
+ t.Fatalf("Expected total amount to be 1 BTC: %v", errorState.TotalAmount.String())
+ }
+
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestInvoiceLend(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ invoice, err := libwallet.ParseInvoice("lnbcrt1u1ps5ma8app59ujxjvtj8x34fyd7u7tghuq44dphjth8nqmkzeklg882y9ghjmvqdqqcqzpgsp5stzjqktxfh02dfz8tucfnh6rl3z87ctl2dumr40elhmrskhx5zlq9qyyssqnp4uhukcgxx6l0p5elppz5xc7a97n0hxvfm6lgr6ze06wqc2dnx95pet3vlalc9rqz20lu45y8sqg3n6fm6tqsftvzqp4l2zsrs0ndgpk2fyv5", libwallet.Regtest())
+ if err != nil {
+ panic(err)
+ }
+
+ startState.ResolveInvoice(invoice, libwallet.Regtest())
+
+ resolveState := listener.next().(*ResolveState)
+
+ context := createContext()
+ context.SubmarineSwap = &SubmarineSwap{
+ Fees: &SwapFees{
+ RoutingFeeInSat: 0,
+ DebtType: DebtTypeLend,
+ DebtAmountInSat: 100,
+ OutputAmountInSat: 546,
+ OutputPaddingInSat: 446,
+ ConfirmationsNeeded: 0,
+ },
+ }
+
+ resolveState.setContextWithTime(context, time.Unix(1629475653, 0))
+
+ validateState := listener.next().(*ValidateLightningState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("foo")
+
+ confirmState := listener.next().(*ConfirmLightningState)
+
+ if confirmState.Note != "foo" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.000001 BTC" {
+ t.Fatalf("expected amount to match resolved URI, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.000001 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+ if confirmState.SwapInfo.SwapFees.DebtType != DebtTypeLend {
+ t.Fatalf("Expected debt type to be lend: %v", confirmState.SwapInfo.SwapFees.DebtType)
+ }
+ if confirmState.SwapInfo.SwapFees.DebtAmountInSat != 100 {
+ t.Fatalf("Expected debt amount to be 100 sats: %v", confirmState.SwapInfo.SwapFees.DebtAmountInSat)
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestAmountInfo_Mutating(t *testing.T) {
+ amountInfo := &AmountInfo{
+ TakeFeeFromAmount: false,
+ FeeRateInSatsPerVByte: 0,
+ }
+
+ mutated := amountInfo.mutating(func(info *AmountInfo) {
+ info.TakeFeeFromAmount = true
+ })
+
+ if amountInfo.TakeFeeFromAmount {
+ t.Fatalf("Expected original to stay the same")
+ }
+ if !mutated.TakeFeeFromAmount {
+ t.Fatalf("Mutated should be mutated")
+ }
+}
+
+//goland:noinspection GoUnhandledErrorResult
+func TestOnChainTFFAWithDebtFeeNeedsChangeBecauseOutputAmountLowerThanDust(t *testing.T) {
+
+ listener := newTestListener()
+ startState := NewOperationFlow(listener)
+
+ startState.Resolve("bitcoin:bcrt1qj35fkq34xend9w0ssthn432vl9pxxsuy0epzlu", libwallet.Regtest())
+
+ context := createContext()
+
+ nts := &NextTransactionSize{}
+ nts.AddSizeForAmount(&SizeForAmount{
+ AmountInSat: 5338,
+ SizeInVByte: 172,
+ })
+ nts.ExpectedDebtInSat = 4353
+ context.NextTransactionSize = nts
+
+ context.FeeWindow.PutTargetedFees(100, 1.0)
+
+ resolveState := listener.next().(*ResolveState)
+ resolveState.SetContext(context)
+
+ enterAmountState := listener.next().(*EnterAmountState)
+ enterAmountState.EnterAmount(NewMonetaryAmountFromSatoshis(985), true)
+
+ validateState := listener.next().(*ValidateState)
+ validateState.Continue()
+
+ enterDescriptionState := listener.next().(*EnterDescriptionState)
+ enterDescriptionState.EnterDescription("bar")
+
+ // Amount is not payable with fastes/highest fee, but it is with min fee (1 sat/vbyte)
+ if enterDescriptionState.Amount.InInputCurrency.String() != "0 BTC" {
+ t.Fatalf("expected amount to match input, got %v", enterDescriptionState.Amount.InInputCurrency)
+ }
+
+ confirmState := listener.next().(*ConfirmState)
+
+ if confirmState.Note != "bar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0 BTC" {
+ t.Fatalf("expected amount to match input, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.000688 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.000688 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+ if confirmState.FeeNeedsChange != true {
+ t.Fatalf("expected feedsNeedsChange to be true, got %v", confirmState.FeeNeedsChange)
+ }
+
+ confirmState.OpenFeeEditor()
+ editFeeState := listener.next().(*EditFeeState)
+
+ editFeeState.SetFeeRate(1)
+
+ validateState = listener.next().(*ValidateState)
+ validateState.Continue()
+
+ confirmState = listener.next().(*ConfirmState)
+
+ if confirmState.Note != "bar" {
+ t.Fatalf("expected note to match input, got '%v'", confirmState.Note)
+ }
+ if confirmState.Amount.InInputCurrency.String() != "0.00000813 BTC" {
+ t.Fatalf("expected amount to match input, got %v", confirmState.Amount.InInputCurrency)
+ }
+ if confirmState.Fee.InInputCurrency.String() != "0.00000172 BTC" {
+ t.Fatalf("expected fee to match, got %v", confirmState.Fee.InInputCurrency)
+ }
+ if confirmState.Total.InInputCurrency.String() != "0.00000985 BTC" {
+ t.Fatalf("expected total to match, got %v", confirmState.Total.InInputCurrency)
+ }
+ if confirmState.FeeNeedsChange != false {
+ t.Fatalf("expected feedsNeedsChange to be false, got %v", confirmState.FeeNeedsChange)
+ }
+}
diff --git a/libwallet/newop/swaps.go b/libwallet/newop/swaps.go
new file mode 100644
index 0000000..2600406
--- /dev/null
+++ b/libwallet/newop/swaps.go
@@ -0,0 +1,105 @@
+package newop
+
+import (
+ "github.com/btcsuite/btcutil"
+ "github.com/muun/libwallet/fees"
+)
+
+type SubmarineSwapReceiver struct {
+ Alias string
+ NetworkAddresses string
+ PublicKey string
+}
+
+const (
+ DebtTypeNone = string(fees.DebtTypeNone)
+ DebtTypeCollect = string(fees.DebtTypeCollect)
+ DebtTypeLend = string(fees.DebtTypeLend)
+)
+
+type SwapFees struct {
+ RoutingFeeInSat int64
+ DebtType string
+ DebtAmountInSat int64
+ OutputAmountInSat int64
+ OutputPaddingInSat int64
+ ConfirmationsNeeded int64
+}
+
+func (f *SwapFees) toInternalType() *fees.SwapFees {
+ if f == nil {
+ return nil
+ }
+ return &fees.SwapFees{
+ RoutingFee: btcutil.Amount(f.RoutingFeeInSat),
+ DebtType: fees.DebtType(f.DebtType),
+ DebtAmount: btcutil.Amount(f.DebtAmountInSat),
+ OutputAmount: btcutil.Amount(f.OutputAmountInSat),
+ OutputPadding: btcutil.Amount(f.OutputPaddingInSat),
+ ConfirmationsNeeded: uint(f.ConfirmationsNeeded),
+ }
+}
+
+func newSwapFeesFromInternal(fees *fees.SwapFees) *SwapFees {
+ return &SwapFees{
+ RoutingFeeInSat: int64(fees.RoutingFee),
+ DebtType: string(fees.DebtType),
+ DebtAmountInSat: int64(fees.DebtAmount),
+ OutputAmountInSat: int64(fees.OutputAmount),
+ OutputPaddingInSat: int64(fees.OutputPadding),
+ ConfirmationsNeeded: int64(fees.ConfirmationsNeeded),
+ }
+}
+
+type BestRouteFees struct {
+ MaxCapacity int64
+ FeeProportionalMillionth int64
+ FeeBase int64
+}
+
+func (f *BestRouteFees) toInternalType() *fees.BestRouteFees {
+ if f == nil {
+ return nil
+ }
+ return &fees.BestRouteFees{
+ MaxCapacity: btcutil.Amount(f.MaxCapacity),
+ FeeProportionalMillionth: uint64(f.FeeProportionalMillionth),
+ FeeBase: btcutil.Amount(f.FeeBase),
+ }
+}
+
+type FundingOutputPolicies struct {
+ MaximumDebtInSat int64
+ PotentialCollectInSat int64
+ MaxAmountInSatFor0Conf int64
+}
+
+func (f *FundingOutputPolicies) toInternalType() *fees.FundingOutputPolicies {
+ if f == nil {
+ return nil
+ }
+ return &fees.FundingOutputPolicies{
+ MaximumDebt: btcutil.Amount(f.MaximumDebtInSat),
+ PotentialCollect: btcutil.Amount(f.PotentialCollectInSat),
+ MaxAmountFor0Conf: btcutil.Amount(f.MaxAmountInSatFor0Conf),
+ }
+}
+
+type SubmarineSwap struct {
+ Receiver *SubmarineSwapReceiver
+ Fees *SwapFees
+ FundingOutputPolicies *FundingOutputPolicies
+ BestRouteFees []*BestRouteFees
+}
+
+func (s *SubmarineSwap) AddBestRouteFees(bestRouteFees *BestRouteFees) {
+ s.BestRouteFees = append(s.BestRouteFees, bestRouteFees)
+}
+
+func (s *SubmarineSwap) toBestRouteFeesInternalType() []fees.BestRouteFees {
+ var l []fees.BestRouteFees
+ for _, bestRouteFee := range s.BestRouteFees {
+ l = append(l, *(bestRouteFee.toInternalType()))
+ }
+ return l
+}
diff --git a/libwallet/operation/fee_window.go b/libwallet/operation/fee_window.go
new file mode 100644
index 0000000..e0eaf99
--- /dev/null
+++ b/libwallet/operation/fee_window.go
@@ -0,0 +1,79 @@
+package operation
+
+import (
+ "fmt"
+ "math"
+)
+
+const swapV2ConfTarget = 250 // Approx 2 days
+
+type FeeWindow struct {
+ TargetedFees map[uint]float64
+}
+
+// SwapFeeRate gets the appropriate fee rate for a given swap (depends on confirmations needed).
+// Useful method for when swap doesn't have a fixed amount (e.g AmountLessInvoices + use all funds).
+func (f *FeeWindow) SwapFeeRate(confirmationsNeeded uint) (float64, error) {
+ if confirmationsNeeded == 0 {
+ return f.MinimumFeeRate(swapV2ConfTarget)
+ }
+
+ return f.FastestFeeRate(), nil
+}
+
+// MinimumFeeRate gets the minimum available fee rate that will hit a given confirmation target. We
+// make no guesses (no averages or interpolations), so we might overshoot the fee if data is too
+// sparse.
+// Note: the lower the confirmation target, the faster the tx will confirm, and greater the
+// fee(rate) will be.
+func (f *FeeWindow) MinimumFeeRate(confirmationTarget uint) (float64, error) {
+
+ if confirmationTarget <= 0 {
+ return 0, fmt.Errorf("can't get feeRate. Expected positive confirmation target, got %v", confirmationTarget)
+ }
+
+ // Walk the available targets backwards, finding the highest target below the given one:
+ for closestTarget := confirmationTarget; closestTarget > 0; closestTarget-- {
+ if feeRate, containsKey := f.TargetedFees[closestTarget]; containsKey {
+ // Found! This is the lowest fee rate that hits the given target.
+ return feeRate, nil
+ }
+ }
+
+ // No result? This is odd, but not illogical. It means *all* of our available targets
+ // are above the requested one. Let's use the fastest:
+ return f.FastestFeeRate(), nil
+}
+
+// FastestFeeRate gets the fastest fee rate, in satoshis per weight unit.
+func (f *FeeWindow) FastestFeeRate() float64 {
+
+ var lowestTarget uint = math.MaxUint32
+ for k := range f.TargetedFees {
+ if k < lowestTarget {
+ lowestTarget = k
+ }
+ }
+
+ return f.TargetedFees[lowestTarget]
+}
+
+// NextHighestBlock finds the next highest confirmation/block target for a certain feeRate. Let me
+// explain, we have a map that associates a conf-target with a fee rate. Now we want to know
+// associate a conf-target with a given fee rate. We want the NEXT conf-target as we usually want
+// this data for predictions or estimations and this makes the predictions for the fee rate to
+// "fall on the correct side" (e.g when estimating max time to confirmation for a given fee rate).
+// Note: code is not our best work of art. The target < next comparison is to account for our
+// TargetedFees map not necessarily being sorted.
+func (f *FeeWindow) NextHighestBlock(feeRate float64) uint {
+ next := uint(math.MaxUint32)
+ for target, rate := range f.TargetedFees {
+ if rate <= feeRate && target < next {
+ next = target
+ }
+ }
+ if next == math.MaxUint32 {
+ return 0 // 0 is a not valid targetBlock, we use it to signal target not found
+ }
+ return next
+}
diff --git a/libwallet/operation/fee_window_test.go b/libwallet/operation/fee_window_test.go
new file mode 100644
index 0000000..df61a8a
--- /dev/null
+++ b/libwallet/operation/fee_window_test.go
@@ -0,0 +1,172 @@
+package operation
+
+import "testing"
+
+var singleFeeWindow = &FeeWindow{
+ TargetedFees: func() map[uint]float64 {
+ fees := make(map[uint]float64)
+ fees[1] = 5.6
+ return fees
+ }(),
+}
+
+var someFeeWindow = &FeeWindow{
+ TargetedFees: func() map[uint]float64 {
+ fees := make(map[uint]float64)
+ fees[2] = 2.3
+ fees[5] = 7.2
+ fees[20] = 18.7
+ return fees
+ }(),
+}
+
+func TestFastestFeeRate(t *testing.T) {
+
+ testCases := []struct {
+ desc string
+ feewindow *FeeWindow
+ expectedFeeRate float64
+ }{
+ {
+ desc: "returns the fastest fee rate",
+ feewindow: someFeeWindow,
+ expectedFeeRate: 2.3,
+ },
+ {
+ desc: "returns the only fee rate as fastest",
+ feewindow: singleFeeWindow,
+ expectedFeeRate: 5.6,
+ },
+ }
+
+ for _, tC := range testCases {
+ t.Run(tC.desc, func(t *testing.T) {
+
+ feeRate := tC.feewindow.FastestFeeRate()
+
+ if feeRate != tC.expectedFeeRate {
+ t.Fatalf("expected feeRate = %v, got %v", tC.expectedFeeRate, feeRate)
+ }
+ })
+ }
+}
+
+func TestMinimumFeeRate(t *testing.T) {
+ testCases := []struct {
+ desc string
+ feewindow *FeeWindow
+ confTarget uint
+ expectedFeeRate float64
+ }{
+ {
+ desc: "returns the exact target as closest, if present (1)",
+ feewindow: someFeeWindow,
+ confTarget: 2,
+ expectedFeeRate: 2.3,
+ },
+ {
+ desc: "returns the exact target as closest, if present (2)",
+ feewindow: someFeeWindow,
+ confTarget: 5,
+ expectedFeeRate: 7.2,
+ },
+ {
+ desc: "returns the exact target as closest, if present (3)",
+ feewindow: someFeeWindow,
+ confTarget: 20,
+ expectedFeeRate: 18.7,
+ },
+ {
+ desc: "returns the closest lower target (1)",
+ feewindow: someFeeWindow,
+ confTarget: 4,
+ expectedFeeRate: 2.3,
+ },
+ {
+ desc: "returns the closest lower target (2)",
+ feewindow: someFeeWindow,
+ confTarget: 15,
+ expectedFeeRate: 7.2,
+ },
+ {
+ desc: "returns the closest lower target (3)",
+ feewindow: someFeeWindow,
+ confTarget: 22,
+ expectedFeeRate: 18.7,
+ },
+ {
+ desc: "returns the lowest target by default",
+ feewindow: someFeeWindow,
+ confTarget: 1,
+ expectedFeeRate: 2.3,
+ },
+ {
+ desc: "returns the only fee rate as closest (1)",
+ feewindow: singleFeeWindow,
+ confTarget: 1,
+ expectedFeeRate: 5.6,
+ },
+ {
+ desc: "returns the only fee rate as closest (2)",
+ feewindow: singleFeeWindow,
+ confTarget: 6,
+ expectedFeeRate: 5.6,
+ }, {
+ desc: "returns the only fee rate as closest (3)",
+ feewindow: singleFeeWindow,
+ confTarget: 18,
+ expectedFeeRate: 5.6,
+ }, {
+ desc: "returns the only fee rate as closest (4)",
+ feewindow: singleFeeWindow,
+ confTarget: 24,
+ expectedFeeRate: 5.6,
+ },
+ }
+
+ for _, tC := range testCases {
+ t.Run(tC.desc, func(t *testing.T) {
+
+ feeRate, err := tC.feewindow.MinimumFeeRate(tC.confTarget)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ if feeRate != tC.expectedFeeRate {
+ t.Fatalf("expected feeRate = %v, got %v", tC.expectedFeeRate, feeRate)
+ }
+ })
+ }
+}
+
+func TestInvalidConfirmationTargets(t *testing.T) {
+ testCases := []struct {
+ desc string
+ feewindow *FeeWindow
+ confTarget uint
+ }{
+ {
+ desc: "fails check when confirmation target is 0",
+ feewindow: someFeeWindow,
+ confTarget: 0,
+ },
+ }
+
+ for _, tC := range testCases {
+ t.Run(tC.desc, func(t *testing.T) {
+
+ _, err := tC.feewindow.MinimumFeeRate(tC.confTarget)
+
+ if err == nil {
+ t.Fatalf("expected test to error")
+ }
+ })
+ }
+}
+
+func TestNextHighestBlock(t *testing.T) {
+ block := someFeeWindow.NextHighestBlock(10.0)
+ if block != 2 {
+ t.Fatalf("expected block to be 2, got %v", block)
+ }
+}
diff --git a/libwallet/operation/fees.go b/libwallet/operation/fees.go
new file mode 100644
index 0000000..53edf80
--- /dev/null
+++ b/libwallet/operation/fees.go
@@ -0,0 +1,60 @@
+package operation
+
+import (
+ "math"
+)
+
+type feeCalculator struct {
+ NextTransactionSize *NextTransactionSize
+}
+
+// Fee DOES NOT return error when amount > balance. Instead we return the fee it would take to
+// spend all utxos and delegate to the caller the task of checking if that is spendable with the given
+// amount. This is to avoid using go error handling.
+// Consequences of this:
+// - we don't check balance whatsoever
+// - fee for COLLECT swap is exactly the same as normal case
+func (f *feeCalculator) Fee(amountInSat int64, feeRateInSatsPerVByte float64, takeFeeFromAmount bool) int64 {
+ if amountInSat == 0 {
+ return 0
+ }
+ if takeFeeFromAmount {
+ return f.feeFromAmount(amountInSat, feeRateInSatsPerVByte)
+ } else {
+ return f.feeFromRemainingBalance(amountInSat, feeRateInSatsPerVByte)
+ }
+}
+
+func (f *feeCalculator) feeFromAmount(amountInSat int64, feeRateInSatsPerVByte float64) int64 {
+ if f.NextTransactionSize == nil {
+ return 0
+ }
+
+ var fee int64
+ for _, sizeForAmount := range f.NextTransactionSize.SizeProgression {
+ fee = computeFee(sizeForAmount.SizeInVByte, feeRateInSatsPerVByte)
+ if sizeForAmount.AmountInSat >= amountInSat {
+ break // no more UTXOs needed
+ }
+ }
+ return fee
+}
+
+func (f *feeCalculator) feeFromRemainingBalance(amountInSat int64, feeRateInSatsPerVByte float64) int64 {
+ if f.NextTransactionSize == nil {
+ return 0
+ }
+
+ var fee int64
+ for _, sizeForAmount := range f.NextTransactionSize.SizeProgression {
+ fee = computeFee(sizeForAmount.SizeInVByte, feeRateInSatsPerVByte)
+ if sizeForAmount.AmountInSat >= amountInSat+fee {
+ break // no more UTXOs needed
+ }
+ }
+ return fee
+}
+
+func computeFee(sizeInVByte int64, feeRate float64) int64 {
+ return int64(math.Ceil(float64(sizeInVByte) * feeRate))
+}
diff --git a/libwallet/operation/fees_test.go b/libwallet/operation/fees_test.go
new file mode 100644
index 0000000..390f6af
--- /dev/null
+++ b/libwallet/operation/fees_test.go
@@ -0,0 +1,301 @@
+package operation
+
+import (
+ "testing"
+)
+
+var emptyNts = &NextTransactionSize{}
+
+var defaultNts = &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 103_456,
+ SizeInVByte: 110,
+ },
+ {
+ AmountInSat: 20_345_678,
+ SizeInVByte: 230,
+ },
+ {
+ AmountInSat: 303_456_789,
+ SizeInVByte: 340,
+ },
+ {
+ AmountInSat: 703_456_789,
+ SizeInVByte: 580,
+ },
+ },
+ ExpectedDebtInSat: 0,
+}
+
+var singleNts = &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 123_456,
+ SizeInVByte: 400,
+ },
+ },
+ ExpectedDebtInSat: 0,
+}
+
+// 2nd utxo is actually more expensive to spend that what its worth
+var negativeUtxoNts = &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 48_216,
+ SizeInVByte: 840,
+ },
+ {
+ AmountInSat: 48_880,
+ SizeInVByte: 1366,
+ },
+ },
+ ExpectedDebtInSat: 0,
+}
+
+// Utxo is actually more expensive to spend that what its worth
+var singleNegativeUtxoNts = &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 644,
+ SizeInVByte: 840,
+ },
+ },
+ ExpectedDebtInSat: 0,
+}
+
+func TestFeeCalculatorForAmountZero(t *testing.T) {
+ testCases := []struct {
+ desc string
+ feeRateInSatsPerVbyte float64
+ takeFeeFromAmount bool
+ expectedFeeInSat int64
+ }{
+ {
+ desc: "calculate for amount zero",
+ feeRateInSatsPerVbyte: 1,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: 0,
+ },
+ {
+ desc: "calculate for amount zero with TFFA",
+ feeRateInSatsPerVbyte: 1,
+ takeFeeFromAmount: true,
+ expectedFeeInSat: 0,
+ },
+ }
+
+ for _, tC := range testCases {
+ t.Run(tC.desc, func(t *testing.T) {
+
+ allNts := []NextTransactionSize{
+ *emptyNts,
+ *defaultNts,
+ *singleNts,
+ *negativeUtxoNts,
+ *singleNegativeUtxoNts,
+ }
+
+ for _, nts := range allNts {
+ calculator := feeCalculator{&nts}
+ feeInSat := calculator.Fee(0, tC.feeRateInSatsPerVbyte, tC.takeFeeFromAmount)
+
+ if feeInSat != tC.expectedFeeInSat {
+ t.Fatalf("expected fee = %v, got %v", tC.expectedFeeInSat, feeInSat)
+ }
+ }
+
+ calculator := feeCalculator{}
+ feeInSat := calculator.Fee(0, tC.feeRateInSatsPerVbyte, tC.takeFeeFromAmount)
+
+ if feeInSat != tC.expectedFeeInSat {
+ t.Fatalf("expected fee = %v, got %v", tC.expectedFeeInSat, feeInSat)
+ }
+ })
+ }
+}
+
+func TestFeeCalculator(t *testing.T) {
+
+ testCases := []struct {
+ desc string
+ amountInSat int64
+ feeCalculator *feeCalculator
+ feeRateInSatsPerVbyte float64
+ takeFeeFromAmount bool
+ expectedFeeInSat int64
+ }{
+ {
+ desc: "empty fee calculator",
+ amountInSat: 1000,
+ feeCalculator: &feeCalculator{},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: 0,
+ },
+ {
+ desc: "empty fee calculator with TFFA",
+ amountInSat: 1000,
+ feeCalculator: &feeCalculator{},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: 0,
+ },
+ {
+ desc: "non empty fee calculator",
+ amountInSat: 1000,
+ feeCalculator: &feeCalculator{&NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ }},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: 2400,
+ },
+ {
+ desc: "fails when balance is zero",
+ amountInSat: 1,
+ feeCalculator: &feeCalculator{emptyNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: 0,
+ },
+ {
+ desc: "fails when balance is zero with TFFA",
+ amountInSat: 1,
+ feeCalculator: &feeCalculator{emptyNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: true,
+ expectedFeeInSat: 0,
+ },
+ {
+ desc: "fails when amount greater than balance",
+ amountInSat: defaultNts.TotalBalance() + 1,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: 5800,
+ },
+ {
+ desc: "fails when amount greater than balance with TFFA",
+ amountInSat: defaultNts.TotalBalance() + 1,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: true,
+ expectedFeeInSat: 5800,
+ },
+ {
+ desc: "calculates when amount plus fee is greater than balance",
+ amountInSat: defaultNts.TotalBalance() - 1,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: 5800,
+ },
+ {
+ desc: "calculates reduced amount and fee with TFFA",
+ amountInSat: 10_345_678,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: true,
+ expectedFeeInSat: 2300,
+ },
+ {
+ // This case can't really happen since our PaymentAnalyzer enforces amount == totalBalance for TFFA
+ // We don't handle that precondition in FeeCalculator to keep its API simple (no error handling)
+ desc: "calculates when no amount is left after TFFA",
+ amountInSat: 10,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: true,
+ expectedFeeInSat: 1100,
+ },
+ {
+ desc: "calculates use-all-funds fee with TFFA",
+ amountInSat: defaultNTS.TotalBalance(),
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: true,
+ expectedFeeInSat: 2300,
+ },
+ {
+ desc: "calculates when paying fee does not require an additional UTXO (1)",
+ amountInSat: defaultNts.SizeProgression[0].AmountInSat / 2,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: defaultNts.SizeProgression[0].SizeInVByte * 10,
+ },
+ {
+ desc: "calculates when paying fee does not require an additional UTXO (2)",
+ amountInSat: defaultNts.SizeProgression[1].AmountInSat / 2,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: defaultNts.SizeProgression[1].SizeInVByte * 10,
+ },
+ {
+ desc: "calculates when paying fee does not require an additional UTXO (3)",
+ amountInSat: defaultNts.SizeProgression[2].AmountInSat / 2,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: defaultNts.SizeProgression[2].SizeInVByte * 10,
+ },
+ {
+ desc: "calculates when paying fee does not require an additional UTXO (4)",
+ amountInSat: defaultNts.SizeProgression[3].AmountInSat / 2,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: defaultNts.SizeProgression[3].SizeInVByte * 10,
+ },
+ {
+ desc: "calculates when paying fee requires an additional UTXO (1)",
+ amountInSat: defaultNts.SizeProgression[0].AmountInSat - 1,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: defaultNts.SizeProgression[1].SizeInVByte * 10,
+ },
+ {
+ desc: "calculates when paying fee requires an additional UTXO (2)",
+ amountInSat: defaultNts.SizeProgression[1].AmountInSat - 1,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: defaultNts.SizeProgression[2].SizeInVByte * 10,
+ },
+ {
+ desc: "calculates when paying fee requires an additional UTXO (3)",
+ amountInSat: defaultNts.SizeProgression[2].AmountInSat - 1,
+ feeCalculator: &feeCalculator{defaultNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: defaultNts.SizeProgression[3].SizeInVByte * 10,
+ },
+ {
+ desc: "calculates when negative UTXOs are larger than positive UTXOs",
+ amountInSat: 1,
+ feeCalculator: &feeCalculator{singleNegativeUtxoNts},
+ feeRateInSatsPerVbyte: 10,
+ takeFeeFromAmount: false,
+ expectedFeeInSat: 8400, // which is > 64, aka singleNegativeUtxoNts.TotalBalance()
+ },
+ }
+ for _, tC := range testCases {
+ t.Run(tC.desc, func(t *testing.T) {
+
+ feeInSat := tC.feeCalculator.Fee(tC.amountInSat, tC.feeRateInSatsPerVbyte, tC.takeFeeFromAmount)
+
+ if feeInSat != tC.expectedFeeInSat {
+ t.Fatalf("expected fee = %v, got %v", tC.expectedFeeInSat, feeInSat)
+ }
+ })
+ }
+}
diff --git a/libwallet/operation/payment_analyzer.go b/libwallet/operation/payment_analyzer.go
new file mode 100644
index 0000000..c646798
--- /dev/null
+++ b/libwallet/operation/payment_analyzer.go
@@ -0,0 +1,562 @@
+package operation
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/btcsuite/btcutil"
+ "github.com/muun/libwallet/fees"
+)
+
+// paymentAnalyzer is the component that decides whether a payment can be made or not, and what
+// are the amount and fees involved. Here a list of some important design decisions:
+//
+// - Delegate to the caller the need to make another call if she wants to analyze with minimumFee
+// - 2 entry points: toAddress for onchain payments, toInvoice for offchain payments
+// - PaymentAnalyzer return errors only on programmer errors, not for "payment errors" aka
+// situations that can happen like amount greater than balance
+// - Returned PaymentAnalysis contains a status that represent:
+// - Success → `StatusOk`
+// - Unrecoverable error -> `StatusAmountGreaterThanBalance`, `StatusAmountTooSmall`
+// - Maybe recoverable error → `StatusUnpayable`
+// - A status error is:
+// - Unrecoverable: if there's no way to pay the specified amount with the current state
+// - Recoverable: if there's a way to pay the specified amount with the current state
+// (e.g changing the fee, if it applies)
+// - `OutputAmount > uxtoBalance` IS NOT A `StatusAmountGreaterThanBalance`, because the problem
+// is not that the `amount > totalBalance` but that the swapFees (sweep + lightning) and/or
+// collectAmount make `OutputAmount > uxtoBalance`. Hence it is an `StatusUnpayable`.
+// - We don't allow TFFA for LEND swaps (don't want to lend money if you're taking it all away)
+// - FeeCalculator DOES NOT return error when amount > balance
+// - Instead we return the fee it would take to spend all utxos and delegate to the caller the
+// task of checking if that is spendable with the given amount
+// - This is to avoid using go error handling
+// - We finally renamed sweepFee to outputPadding since that's its only use. Here's how it works:
+// - Only makes sense for swaps
+// - If amount ≥ DUST ⇒ No need for padding (outputPadding = 0)
+// - If amount < DUST & debt < maxDebt ⇒ We use debt as padding (outputPadding = 0)
+// - If amount < DUST && debt >= maxDebt ⇒ outputPadding = DUST - amount
+// - If amount < DUST and TFFA
+// - Can't use debt as padding (don't lend money for TFFA)
+// - Can't use fee as we are using all funds and they are < DUST
+// - If there's need for padding ⇒ payment is UNPAYABLE
+// - Hence, outputPadding = 0 or payment is UNPAYABLE
+
+const DustThreshold = 546
+
+type PaymentToAddress struct {
+ TakeFeeFromAmount bool
+ AmountInSat int64
+ FeeRateInSatsPerVByte float64
+}
+
+type PaymentToInvoice struct {
+ TakeFeeFromAmount bool
+ AmountInSat int64
+ SwapFees *fees.SwapFees // Nullable before we know the paymentAmount for amountless invoice
+ BestRouteFees []fees.BestRouteFees // Nullable when we know the amount beforehand (invoice with amount)
+ FundingOutputPolicies *fees.FundingOutputPolicies // Nullable when we know the amount beforehand (invoice with amount)
+}
+
+type PaymentAnalyzer struct {
+ feeWindow *FeeWindow
+ nextTransactionSize *NextTransactionSize
+ feeCalculator *feeCalculator
+}
+
+type SizeForAmount struct {
+ AmountInSat int64
+ SizeInVByte int64
+}
+
+type NextTransactionSize struct {
+ SizeProgression []SizeForAmount
+ ExpectedDebtInSat int64
+}
+
+func (nts *NextTransactionSize) UtxoBalance() int64 {
+ if len(nts.SizeProgression) == 0 {
+ return 0
+ }
+ return nts.SizeProgression[len(nts.SizeProgression)-1].AmountInSat
+}
+
+func (nts *NextTransactionSize) TotalBalance() int64 {
+ return nts.UtxoBalance() - nts.ExpectedDebtInSat
+}
+
+type AnalysisStatus string
+
+const (
+ AnalysisStatusOk AnalysisStatus = "Ok"
+ AnalysisStatusAmountGreaterThanBalance AnalysisStatus = "GreaterThanBalance"
+ AnalysisStatusAmountTooSmall AnalysisStatus = "AmountTooSmall"
+ AnalysisStatusUnpayable AnalysisStatus = "Unpayable"
+)
+
+type PaymentAnalysis struct {
+ Status AnalysisStatus
+ AmountInSat int64
+ FeeInSat int64
+ SwapFees *fees.SwapFees
+ TotalInSat int64
+}
+
+func NewPaymentAnalyzer(feeWindow *FeeWindow, nts *NextTransactionSize) *PaymentAnalyzer {
+ return &PaymentAnalyzer{
+ feeWindow: feeWindow,
+ nextTransactionSize: nts,
+ feeCalculator: &feeCalculator{nts},
+ }
+}
+
+func (a *PaymentAnalyzer) totalBalance() int64 {
+ return a.nextTransactionSize.TotalBalance()
+}
+
+func (a *PaymentAnalyzer) utxoBalance() int64 {
+ return a.nextTransactionSize.UtxoBalance()
+}
+
+func (a *PaymentAnalyzer) ToAddress(payment *PaymentToAddress) (*PaymentAnalysis, error) {
+ if payment.AmountInSat < DustThreshold {
+ return &PaymentAnalysis{
+ Status: AnalysisStatusAmountTooSmall,
+ }, nil
+ }
+ if payment.AmountInSat > a.totalBalance() {
+ return &PaymentAnalysis{
+ Status: AnalysisStatusAmountGreaterThanBalance,
+ TotalInSat: payment.AmountInSat,
+ }, nil
+ }
+ if payment.TakeFeeFromAmount && payment.AmountInSat != a.totalBalance() {
+ return nil, fmt.Errorf("amount (%v) != userBalance (%v) for TFFA", payment.AmountInSat, a.totalBalance())
+ }
+
+ if payment.TakeFeeFromAmount {
+ return a.analyzeFeeFromAmount(payment)
+ }
+ return a.analyzeFeeFromRemainingBalance(payment)
+}
+
+func (a *PaymentAnalyzer) analyzeFeeFromAmount(payment *PaymentToAddress) (*PaymentAnalysis, error) {
+ fee := a.feeCalculator.Fee(payment.AmountInSat, payment.FeeRateInSatsPerVByte, true)
+
+ total := payment.AmountInSat
+ amount := total - fee
+
+ if amount <= DustThreshold {
+ // avoid returning a negative amount
+ if amount < 0 {
+ amount = 0
+ }
+ return &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: amount,
+ TotalInSat: payment.AmountInSat,
+ FeeInSat: fee,
+ }, nil
+ }
+
+ return &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: amount,
+ TotalInSat: payment.AmountInSat,
+ FeeInSat: fee,
+ }, nil
+}
+
+func (a *PaymentAnalyzer) analyzeFeeFromRemainingBalance(payment *PaymentToAddress) (*PaymentAnalysis, error) {
+ fee := a.feeCalculator.Fee(payment.AmountInSat, payment.FeeRateInSatsPerVByte, false)
+ total := payment.AmountInSat + fee
+
+ if total > a.totalBalance() {
+ return &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: payment.AmountInSat,
+ FeeInSat: fee,
+ TotalInSat: total,
+ }, nil
+ }
+
+ return &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: payment.AmountInSat,
+ TotalInSat: total,
+ FeeInSat: fee,
+ }, nil
+}
+
+func (a *PaymentAnalyzer) ToInvoice(payment *PaymentToInvoice) (*PaymentAnalysis, error) {
+ if payment.AmountInSat <= 0 {
+ return &PaymentAnalysis{
+ Status: AnalysisStatusAmountTooSmall,
+ }, nil
+ }
+ if payment.AmountInSat > a.totalBalance() {
+ return &PaymentAnalysis{
+ Status: AnalysisStatusAmountGreaterThanBalance,
+ TotalInSat: payment.AmountInSat,
+ }, nil
+ }
+ if payment.TakeFeeFromAmount {
+ if payment.BestRouteFees == nil {
+ return nil, errors.New("fixed amount swap can't be TFFA since that would change the amount")
+ }
+ if payment.AmountInSat != a.totalBalance() {
+ return nil, fmt.Errorf("amount (%v) != userBalance (%v) for TFFA", payment.AmountInSat, a.totalBalance())
+ }
+ }
+
+ if payment.BestRouteFees != nil {
+
+ // As users can enter newOp screen with 0 balance, we need to check for amount == 0
+ // because of our rule (if balance == amount then useAllFunds = true)
+ if !payment.TakeFeeFromAmount || payment.AmountInSat == 0 {
+ // User chose a specific amount
+ return a.analyzeFixedAmountSwap(payment, fees.ComputeSwapFees(
+ btcutil.Amount(payment.AmountInSat),
+ payment.BestRouteFees,
+ payment.FundingOutputPolicies,
+ payment.TakeFeeFromAmount,
+ ))
+
+ } else {
+ return a.analyzeTFFAAmountlessInvoiceSwap(payment)
+ }
+ }
+
+ if payment.SwapFees == nil {
+ return nil, fmt.Errorf("payment is missing required swap fees data")
+ }
+
+ return a.analyzeFixedAmountSwap(payment, payment.SwapFees)
+}
+
+func (a *PaymentAnalyzer) analyzeFixedAmountSwap(payment *PaymentToInvoice, swapFees *fees.SwapFees) (*PaymentAnalysis, error) {
+ switch swapFees.DebtType {
+ case fees.DebtTypeLend:
+ return a.analyzeLendSwap(payment, swapFees)
+ case fees.DebtTypeCollect:
+ fallthrough
+ case fees.DebtTypeNone:
+ return a.analyzeCollectSwap(payment, swapFees) // a non-debt swap is just a collect swap with debtAmount = 0
+ }
+ return nil, fmt.Errorf("unsupported debt type: %v", swapFees.DebtType)
+}
+
+func (a *PaymentAnalyzer) analyzeLendSwap(payment *PaymentToInvoice, swapFees *fees.SwapFees) (*PaymentAnalysis, error) {
+
+ amount := payment.AmountInSat
+ total := amount + int64(swapFees.RoutingFee)
+
+ if total > a.totalBalance() {
+ return &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: amount,
+ TotalInSat: total,
+ FeeInSat: 0,
+ SwapFees: swapFees,
+ }, nil
+ }
+
+ return &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: amount,
+ TotalInSat: total,
+ FeeInSat: 0,
+ SwapFees: swapFees,
+ }, nil
+}
+
+// Analyze non LEND swaps (e.g both COLLECT and NON-DEBT swaps), understanding that both cases warrant
+// the same analysis. A non-debt swap is just a collect swap with debtAmount = 0.
+func (a *PaymentAnalyzer) analyzeCollectSwap(payment *PaymentToInvoice, swapFees *fees.SwapFees) (*PaymentAnalysis, error) {
+
+ outputAmount := int64(swapFees.OutputAmount)
+ collectAmount := int64(swapFees.DebtAmount)
+
+ expectedOutputAmount := payment.AmountInSat +
+ int64(swapFees.RoutingFee) +
+ int64(swapFees.OutputPadding) +
+ collectAmount
+
+ if outputAmount != expectedOutputAmount {
+ return nil, fmt.Errorf(
+ "swap integrity check failed (outputAmount=%v, original_amount=%v, routing_fee=%v, output_padding=%v, collect_amount=%v)",
+ outputAmount,
+ payment.AmountInSat,
+ int64(swapFees.RoutingFee),
+ int64(swapFees.OutputPadding),
+ collectAmount,
+ )
+ }
+
+ feeRate, err := a.feeWindow.SwapFeeRate(swapFees.ConfirmationsNeeded)
+
+ if err != nil {
+ return nil, err
+ }
+
+ fee := a.feeCalculator.Fee(outputAmount, feeRate, false)
+ total := outputAmount + fee
+ totalForUser := total - collectAmount
+
+ if total > a.utxoBalance() || totalForUser > a.totalBalance() {
+ return &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: payment.AmountInSat,
+ FeeInSat: fee,
+ TotalInSat: totalForUser,
+ SwapFees: swapFees,
+ }, nil
+ }
+
+ return &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: payment.AmountInSat,
+ FeeInSat: fee,
+ TotalInSat: totalForUser,
+ SwapFees: swapFees,
+ }, nil
+}
+
+func (a *PaymentAnalyzer) analyzeTFFAAmountlessInvoiceSwap(payment *PaymentToInvoice) (*PaymentAnalysis, error) {
+ zeroConfFeeRate, err := a.feeWindow.SwapFeeRate(0)
+
+ if err != nil {
+ return nil, err
+ }
+
+ zeroConfFeeInSat := a.computeFeeForTFFASwap(payment, zeroConfFeeRate)
+ if zeroConfFeeInSat > a.totalBalance() {
+ // We can't even pay the onchain fee
+ return &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ FeeInSat: zeroConfFeeInSat,
+ TotalInSat: a.totalBalance() + int64(zeroConfFeeRate),
+ }, nil
+ }
+
+ params, err := a.computeParamsForTFFASwap(payment, 0)
+ if err != nil {
+ return &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ FeeInSat: zeroConfFeeInSat,
+ TotalInSat: a.totalBalance() + int64(zeroConfFeeInSat),
+ }, nil
+ }
+
+ confirmations := payment.FundingOutputPolicies.FundingConfirmations(params.Amount, params.RoutingFee)
+ if confirmations == 1 {
+ params, err = a.computeParamsForTFFASwap(payment, 1)
+ if err != nil {
+ // This LITERALLY can never happen, as only source of error for computeParamsForTFFASwap are:
+ // - negative conf target (we're using 1)
+ // - no route for amount (would be catched by previous call since amount with 1-conf fee would be smaller)
+ return &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ FeeInSat: zeroConfFeeInSat,
+ TotalInSat: a.totalBalance() + int64(zeroConfFeeInSat),
+ }, nil
+ }
+ }
+
+ amount := params.Amount
+ lightningFee := params.RoutingFee
+ onChainFee := params.OnChainFee
+
+ if amount <= 0 {
+ // We can't pay the combined fee
+ // This can be either cause we can't pay both fees summed or we had to bump to
+ // 1-conf and we can't pay that.
+ return &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ FeeInSat: zeroConfFeeInSat,
+ TotalInSat: a.totalBalance() + int64(zeroConfFeeInSat),
+ }, nil
+ }
+
+ swapFees := fees.ComputeSwapFees(amount, payment.BestRouteFees, payment.FundingOutputPolicies, true)
+
+ if swapFees.DebtType == fees.DebtTypeLend {
+ return nil, errors.New("TFFA swap should not be a lend operation")
+ }
+
+ // This assumes that debt amount is either 0 (DebtType = NONE) or positive (DebtType = COLLECT)
+ outputAmount := amount + lightningFee + swapFees.OutputPadding + swapFees.DebtAmount
+
+ if lightningFee != swapFees.RoutingFee {
+ return nil, fmt.Errorf(
+ "integrity error: inconsistent lightning fee calculated for TFFA swap (lightning_fee=%v, output_amount=%v, original_amount=%v, routing_fee=%v, output_padding=%v)",
+ int64(lightningFee),
+ int64(outputAmount),
+ payment.AmountInSat,
+ int64(swapFees.RoutingFee),
+ int64(swapFees.OutputPadding),
+ )
+ }
+
+ total := outputAmount + onChainFee
+ totalForDisplay := total - swapFees.DebtAmount // amount + lightningFee + outputPadding + onChainFee
+
+ // We need to ensure we can spend on chain and that we have enough UI visible balance too
+ // That is, the collect doesn't make us spend more than we really can and the amount + fee
+ // doesn't default any debt.
+ canPay := total <= btcutil.Amount(a.utxoBalance()) && totalForDisplay <= btcutil.Amount(a.totalBalance())
+
+ if !canPay {
+ return &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: int64(amount),
+ FeeInSat: int64(onChainFee),
+ TotalInSat: payment.AmountInSat,
+ SwapFees: swapFees,
+ }, nil
+ }
+
+ return &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: int64(amount),
+ FeeInSat: int64(onChainFee),
+ TotalInSat: payment.AmountInSat,
+ SwapFees: swapFees,
+ }, nil
+}
+
+type swapParams struct {
+ Amount btcutil.Amount
+ OnChainFee btcutil.Amount
+ RoutingFee btcutil.Amount
+}
+
+// computeParamsForTFFASwap takes care of the VERY COMPLEX tasks of calculating
+// the amount, routing fee and on-chain fee for a TFFA swap. Calculating the
+// on-chain fee is pretty straightforward but for the other two we use what
+// we call "the equation". Let's dive into how it works:
+//
+// Let:
+// - x be the payment amount
+// - l be the lightning/routing fee
+// - h be the onchain fee for amount x
+// - y the available user balance (utxoBalance - expectedDebt)
+//
+// Also, given a specific route, l is a linear function of x l(x) = a * x + b,
+// where a and b are known.
+//
+// For amountLessInvoices, we need to figure out x and l such that x + l = y - h
+//
+// Note:
+// - we don't care about debt (except to calculate user balance) since it
+// doesn't affect that payment/offchain amount and thus the routingFee. It may
+// affect the onchain fee though, which is already calculated considering it.
+// - we don't care about output padding, since it can either be:
+// - issued debt, in which case point above still holds
+// - taken by fee, which would mean we are in a TFFA for a sub-dust amount,
+// which is unpayable since we don't have balance to add as padding/fee.
+//
+// Suppose we have only one route.
+// Then, x + l(x) = y - h
+// Then, x + (a * x + b) = y - h
+// Then, x * (1 + a) = y - h - b
+// Then, x = (y - h - b) / (a + 1) (*1)
+//
+// BUT, we can have different routes for different amounts, aka:
+// l_1(x) = a_1 * x + b_1
+// l_2(x) = a_2 * x + b_2
+// l_3(x) = a_3 * x + b_3
+// etc...
+//
+// What we can do is try out each l(x) corresponding to each route and check
+// which one gives us a valid amount (e.g amount < route.maxCapacityInSat), in
+// other words, which route is the "best route" associated with that amount.
+// BestRouteFees guarantees that for each amount there's only one "best route"
+// and that there is a route for each amount.
+//
+// Final note, our final equation looks a little different since our param a
+// is given as FeeProportionalMillionth/1_000_000, so in order to not lose
+// precision we need to massage equation *1 bit more:
+//
+// x = (y - h - b) / (FeeProportionalMillionth/1_000_000 + 1)
+// x = (y - h - b) / (FeeProportionalMillionth + 1_000_000) / 1_000_000
+// x = ((y - h - b) * 1_000_000) / (FeeProportionalMillionth + 1_000_000)
+//
+func (a *PaymentAnalyzer) computeParamsForTFFASwap(payment *PaymentToInvoice, confs uint) (*swapParams, error) {
+ feeRate, err := a.feeWindow.SwapFeeRate(confs)
+
+ if err != nil {
+ return nil, err
+ }
+
+ onChainFeeInSat := a.computeFeeForTFFASwap(payment, feeRate)
+
+ for _, bestRouteFees := range payment.BestRouteFees {
+ amount := btcutil.Amount(
+ (a.totalBalance() - onChainFeeInSat - int64(bestRouteFees.FeeBase)) * 1_000_000 /
+ (int64(bestRouteFees.FeeProportionalMillionth) + 1_000_000))
+
+ lightningFee := bestRouteFees.ForAmount(amount)
+
+ // Warning, this is an ugly hack because we are not (yet) using millisats as unit of
+ // account thus we might an amount 1 sat smaller than the previous implementation.
+ // What this check does is to see if we can send just one sat more with the safe fee
+ // in sats. It probably has some edge cases in some cases but we have been ignoring
+ // them for now so we can live with it.
+ if bestRouteFees.ForAmount(amount+1) == lightningFee {
+ amount += 1
+ }
+
+ if amount+lightningFee <= bestRouteFees.MaxCapacity {
+
+ // There's a special comment to be made here for VERY edgy case where
+ // bestRouteFees.ForAmount(amount+1) == lightningFee+1.
+ // In this case adding 1 sat to the amount makes you need an extra sat for the
+ // routingFee, and these 2 extra sats make the total go over userBalance, but
+ // there's 1 sat available in our balance. What do we do it? Answer: nothing,
+ // it will be burn as on-chain fee.
+
+ return &swapParams{
+ Amount: amount,
+ RoutingFee: lightningFee,
+ OnChainFee: btcutil.Amount(onChainFeeInSat),
+ }, nil
+ }
+ }
+ return nil, errors.New("none of the best route fees have enough capacity")
+}
+
+func (a *PaymentAnalyzer) computeFeeForTFFASwap(payment *PaymentToInvoice, feeRate float64) int64 {
+ // Compute tha on-chain fee. As its TFFA, we want to calculate the fee for the total balance
+ // including any sats we want to collect.
+ onChainAmount := a.totalBalance() + int64(payment.FundingOutputPolicies.PotentialCollect)
+
+ return a.feeCalculator.Fee(onChainAmount, feeRate, true)
+}
+
+// MaxFeeRateToAddress computes the maximum fee rate that can be used when
+// paying a given amount. This does not imply that the payment _can be made_.
+// When given invalid parameters, it's likely to still obtain a value here and
+// the resulting analysis would be Unpayable. It's up to the caller to first
+// verify the amount is payable, and only then call this method.
+func (a *PaymentAnalyzer) MaxFeeRateToAddress(payment *PaymentToAddress) float64 {
+
+ if payment.AmountInSat > a.totalBalance() {
+ return 0
+ }
+
+ var restInSat int64
+ if payment.TakeFeeFromAmount {
+ restInSat = payment.AmountInSat
+ } else {
+ restInSat = a.totalBalance() - payment.AmountInSat
+ }
+
+ for _, sizeForAmount := range a.nextTransactionSize.SizeProgression {
+ if sizeForAmount.AmountInSat >= payment.AmountInSat {
+ return float64(restInSat) / float64(sizeForAmount.SizeInVByte)
+ }
+ }
+
+ return 0
+}
diff --git a/libwallet/operation/payment_analyzer_test.go b/libwallet/operation/payment_analyzer_test.go
new file mode 100644
index 0000000..419d429
--- /dev/null
+++ b/libwallet/operation/payment_analyzer_test.go
@@ -0,0 +1,2408 @@
+package operation
+
+import (
+ "math"
+ "reflect"
+ "testing"
+
+ "github.com/muun/libwallet/fees"
+)
+
+var defaultFeeWindow = &FeeWindow{
+ TargetedFees: func() map[uint]float64 {
+ fees := make(map[uint]float64)
+ fees[1] = 10.0
+ fees[2] = 5.0
+ fees[3] = 1.0
+ return fees
+ }(),
+}
+
+var feeWindowFailureTests = &FeeWindow{
+ TargetedFees: func() map[uint]float64 {
+ fees := make(map[uint]float64)
+ fees[1] = 100.0
+ fees[5] = 50.0
+ fees[10] = .25
+ return fees
+ }(),
+}
+
+var defaultNTS = &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 1_000_000,
+ SizeInVByte: 100,
+ },
+ },
+ ExpectedDebtInSat: 0,
+}
+
+func TestAnalyzeOnChain(t *testing.T) {
+ testCases := []struct {
+ desc string
+ nts *NextTransactionSize
+ payment *PaymentToAddress
+ expected *PaymentAnalysis
+ err bool
+ }{
+ {
+ desc: "success",
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 10000,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 10000,
+ FeeInSat: 100,
+ TotalInSat: 10100,
+ },
+ },
+ {
+ desc: "take fee from amount",
+ payment: &PaymentToAddress{
+ AmountInSat: 1_000_000,
+ TakeFeeFromAmount: true,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 999_900,
+ FeeInSat: 100,
+ TotalInSat: 1_000_000,
+ },
+ },
+ {
+ desc: "zero amount",
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 0,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountTooSmall,
+ },
+ },
+ {
+ desc: "zero amount using TFFA",
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 0,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountTooSmall,
+ },
+ },
+ {
+ desc: "sub dust amount",
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 100,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountTooSmall,
+ },
+ },
+ {
+ desc: "sub dust amount using TFFA",
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 100,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountTooSmall,
+ },
+ },
+ {
+ desc: "amount greater than balance",
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 1_000_000_000,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountGreaterThanBalance,
+ TotalInSat: 1_000_000_000,
+ },
+ },
+ {
+ desc: "valid amount plus selected fee",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 5000,
+ FeeRateInSatsPerVByte: 10,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 5000,
+ FeeInSat: 2400,
+ TotalInSat: 7400,
+ },
+ },
+ {
+ desc: "valid amount but unpayable because of fee",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 9000,
+ FeeRateInSatsPerVByte: 10,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 9000,
+ FeeInSat: 2400,
+ TotalInSat: 11400,
+ },
+ },
+ {
+ desc: "valid amount but unpayable with any fee",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 9999,
+ FeeRateInSatsPerVByte: 10,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 9999,
+ FeeInSat: 2400,
+ TotalInSat: 12399,
+ },
+ },
+ {
+ desc: "valid amount plus fee using TFFA",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 10_000,
+ FeeRateInSatsPerVByte: 10,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 7600,
+ FeeInSat: 2400,
+ TotalInSat: 10_000,
+ },
+ },
+ {
+ desc: "invalid amount using TFFA",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 9900,
+ FeeRateInSatsPerVByte: 10,
+ },
+ err: true,
+ },
+ {
+ desc: "valid amount but unpayable because of fee using TFFA",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 1_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 1000,
+ FeeRateInSatsPerVByte: 10,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 0,
+ FeeInSat: 2400,
+ TotalInSat: 1000,
+ },
+ },
+ {
+ desc: "valid amount but unpayable with any fee using TFFA",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 600,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 600,
+ FeeRateInSatsPerVByte: 10,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 0,
+ FeeInSat: 2400,
+ TotalInSat: 600,
+ },
+ },
+ {
+ desc: "success with debt > 0",
+ nts: &NextTransactionSize{
+ SizeProgression: defaultNTS.SizeProgression,
+ ExpectedDebtInSat: 10000,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 10000,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 10000,
+ FeeInSat: 100,
+ TotalInSat: 10100,
+ },
+ },
+ {
+ desc: "take fee from amount success with debt > 0",
+ nts: &NextTransactionSize{
+ SizeProgression: defaultNTS.SizeProgression,
+ ExpectedDebtInSat: 10000,
+ },
+ payment: &PaymentToAddress{
+ AmountInSat: 990_000,
+ TakeFeeFromAmount: true,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 989_900,
+ FeeInSat: 100,
+ TotalInSat: 990_000,
+ },
+ },
+ {
+ desc: "amount greater than balance because debt > 0",
+ nts: &NextTransactionSize{
+ SizeProgression: defaultNTS.SizeProgression,
+ ExpectedDebtInSat: 10000,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 999_900,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountGreaterThanBalance,
+ TotalInSat: 999_900,
+ },
+ },
+ {
+ desc: "unpayable because debt > 0",
+ nts: &NextTransactionSize{
+ SizeProgression: defaultNTS.SizeProgression,
+ ExpectedDebtInSat: 10000,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 989_500,
+ FeeRateInSatsPerVByte: 10,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 989_500,
+ FeeInSat: 1000,
+ TotalInSat: 990_500,
+ },
+ },
+ {
+ desc: "amount greater than balance using TFFA because debt > 0",
+ nts: &NextTransactionSize{
+ SizeProgression: defaultNTS.SizeProgression,
+ ExpectedDebtInSat: 10000,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 999_900,
+ FeeRateInSatsPerVByte: 1,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountGreaterThanBalance,
+ TotalInSat: 999_900,
+ },
+ },
+ {
+ desc: "unpayable using TFFA because debt > 0",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 8000,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 2000,
+ FeeRateInSatsPerVByte: 100,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 0,
+ FeeInSat: 24000,
+ TotalInSat: 2000,
+ },
+ },
+ {
+ desc: "unpayable using TFFA because amount < DUST",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 7400,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 2600,
+ FeeRateInSatsPerVByte: 10,
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 200,
+ FeeInSat: 2400,
+ TotalInSat: 2600,
+ },
+ },
+ }
+
+ for _, tC := range testCases {
+ t.Run(tC.desc, func(t *testing.T) {
+
+ var analyzer *PaymentAnalyzer
+ if tC.nts != nil {
+ analyzer = NewPaymentAnalyzer(defaultFeeWindow, tC.nts)
+ } else {
+ analyzer = NewPaymentAnalyzer(defaultFeeWindow, defaultNTS)
+ }
+
+ analysis, err := analyzer.ToAddress(tC.payment)
+
+ if err == nil && tC.err {
+ t.Fatal("expected analysis to error")
+ }
+ if err != nil && !tC.err {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(analysis, tC.expected) {
+ t.Fatalf("analysis does not match expected, got %+v, expected %+v", analysis, tC.expected)
+ }
+ })
+ }
+}
+
+func TestAnalyzeOnChainValidAmountButUnpayableWithAnyFee(t *testing.T) {
+ analyzer := NewPaymentAnalyzer(defaultFeeWindow, &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ })
+
+ analysis, err := analyzer.ToAddress(&PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 9999,
+ FeeRateInSatsPerVByte: 10,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if analysis.Status != AnalysisStatusUnpayable {
+ t.Fatal("expected analysis to be unpayable")
+ }
+ if analysis.FeeInSat != 2400 {
+ t.Fatalf("expected fee to be %v, but got %v", 2400, analysis.FeeInSat)
+ }
+ if analysis.TotalInSat != 12399 {
+ t.Fatalf("expected total to be %v, but got %v", 12399, analysis.TotalInSat)
+ }
+
+ analysis, err = analyzer.ToAddress(&PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 9999,
+ FeeRateInSatsPerVByte: 0.25,
+ })
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ if analysis.Status != AnalysisStatusUnpayable {
+ t.Fatal("expected analysis to be unpayable")
+ }
+ if analysis.FeeInSat != 60 {
+ t.Fatalf("expected fee to be %v, but got %v", 60, analysis.FeeInSat)
+ }
+ if analysis.TotalInSat != 10059 {
+ t.Fatalf("expected total to be %v, but got %v", 10059, analysis.TotalInSat)
+ }
+}
+
+func TestAnalyzeOnChainValidAmountButUnpayableWithAnyFeeUsingTFFA(t *testing.T) {
+ analyzer := NewPaymentAnalyzer(defaultFeeWindow, &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 600,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ })
+
+ analysis, err := analyzer.ToAddress(&PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 600,
+ FeeRateInSatsPerVByte: 10,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if analysis.Status != AnalysisStatusUnpayable {
+ t.Fatal("expected analysis to be unpayable")
+ }
+ if analysis.FeeInSat != 2400 {
+ t.Fatalf("expected fee to be %v, but got %v", 2400, analysis.FeeInSat)
+ }
+ if analysis.TotalInSat != 600 {
+ t.Fatalf("expected total to be %v, but got %v", 600, analysis.TotalInSat)
+ }
+
+ analysis, err = analyzer.ToAddress(&PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 600,
+ FeeRateInSatsPerVByte: 0.25,
+ })
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ if analysis.Status != AnalysisStatusUnpayable {
+ t.Fatal("expected analysis to be unpayable")
+ }
+ if analysis.FeeInSat != 60 {
+ t.Fatalf("expected fee to be %v, but got %v", 60, analysis.FeeInSat)
+ }
+ if analysis.AmountInSat != 540 {
+ t.Fatalf("expected amount to be %v, but got %v", 540, analysis.TotalInSat)
+ }
+ if analysis.TotalInSat != 600 {
+ t.Fatalf("expected total to be %v, but got %v", 600, analysis.TotalInSat)
+ }
+}
+
+func TestAnalyzeOffChain(t *testing.T) {
+ testCases := []struct {
+ desc string
+ feeWindow *FeeWindow
+ nts *NextTransactionSize
+ payment *PaymentToInvoice
+ expected *PaymentAnalysis
+ err bool
+ }{
+ {
+ desc: "swap with amount too small (zero funds)",
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 0,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 0,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountTooSmall,
+ },
+ },
+ {
+ desc: "swap with amount too small (negative funds)",
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: -10,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 0,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountTooSmall,
+ },
+ },
+ {
+ desc: "swap with amount greater than balance",
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 5_000_000,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 5_000_000,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountGreaterThanBalance,
+ TotalInSat: 5_000_000,
+ },
+ },
+ {
+ desc: "swap with integrity error",
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 500_000,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 500_001,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ err: true,
+ },
+ {
+ desc: "swap COLLECT with integrity error",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 5_000_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 1000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 500_000,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 501_001,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 1000,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ err: true,
+ },
+ {
+ desc: "swap with valid amount with no routing fee",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 100,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 1100,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 0,
+ OutputPadding: 1000,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 100,
+ FeeInSat: 240,
+ TotalInSat: 1340,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 1100,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 0,
+ OutputPadding: 1000,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap with valid amount",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 100,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 1101,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 1,
+ OutputPadding: 1000,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 100,
+ FeeInSat: 240,
+ TotalInSat: 1341,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 1101,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 1,
+ OutputPadding: 1000,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap with valid amount with 1-conf",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 100,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 1101,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 1,
+ OutputPadding: 1000,
+ ConfirmationsNeeded: 1,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 100,
+ FeeInSat: 2400,
+ TotalInSat: 3501,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 1101,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 1,
+ OutputPadding: 1000,
+ ConfirmationsNeeded: 1,
+ },
+ },
+ },
+ {
+ desc: "swap with valid FIXED amount using TFFA",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 10_000,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 10_000,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ err: true,
+ },
+ {
+ desc: "swap valid amount but unpayable because of fee",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 500,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 10000,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 1000,
+ OutputPadding: 8500,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 500,
+ FeeInSat: 240,
+ TotalInSat: 10240,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 10000,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 1000,
+ OutputPadding: 8500,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap valid amount but unpayable because of output padding",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 500,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 11000,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 1000,
+ OutputPadding: 9500,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 500,
+ FeeInSat: 240,
+ TotalInSat: 11240,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 11000,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 1000,
+ OutputPadding: 9500,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap LEND success",
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 100,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 0,
+ DebtType: fees.DebtTypeLend,
+ DebtAmount: 100,
+ RoutingFee: 1,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 100,
+ FeeInSat: 0,
+ TotalInSat: 101,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 0,
+ DebtType: fees.DebtTypeLend,
+ DebtAmount: 100,
+ RoutingFee: 1,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap LEND success with no routing fee",
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 100,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 0,
+ DebtType: fees.DebtTypeLend,
+ DebtAmount: 100,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 100,
+ FeeInSat: 0,
+ TotalInSat: 100,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 0,
+ DebtType: fees.DebtTypeLend,
+ DebtAmount: 100,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap LEND amount greater than balance",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 1_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 900,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 200,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 0,
+ DebtType: fees.DebtTypeLend,
+ DebtAmount: 200,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusAmountGreaterThanBalance,
+ TotalInSat: 200,
+ },
+ },
+ {
+ desc: "swap LEND unpayable",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 1_000,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 900,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 100,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 0,
+ DebtType: fees.DebtTypeLend,
+ DebtAmount: 100,
+ RoutingFee: 10,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 100,
+ FeeInSat: 0,
+ TotalInSat: 110,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 0,
+ DebtType: fees.DebtTypeLend,
+ DebtAmount: 100,
+ RoutingFee: 10,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap full COLLECT success with no routing fee",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 3000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 5000,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 8000,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 3000,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 5000,
+ FeeInSat: 240,
+ TotalInSat: 5240,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 8000,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 3000,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap full COLLECT success",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 3000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 5000,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 8001,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 3000,
+ RoutingFee: 1,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 5000,
+ FeeInSat: 240,
+ TotalInSat: 5241,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 8001,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 3000,
+ RoutingFee: 1,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap full COLLECT success with 1-conf",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 3000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 5000,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 8001,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 3000,
+ RoutingFee: 1,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 1,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 5000,
+ FeeInSat: 2400,
+ TotalInSat: 7401,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 8001,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 3000,
+ RoutingFee: 1,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 1,
+ },
+ },
+ },
+ {
+ desc: "swap COLLECT output amount greater than balance (aka unpayable)",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 3000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 7400,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 10500,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 3000,
+ RoutingFee: 100,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 7400,
+ FeeInSat: 240,
+ TotalInSat: 7740,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 10500,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 3000,
+ RoutingFee: 100,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap COLLECT amount plus fee greater than balance (aka unpayable)",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10500,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 3000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 7400,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 10500,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 3000,
+ RoutingFee: 100,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 7400,
+ FeeInSat: 240,
+ TotalInSat: 7740,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 10500,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 3000,
+ RoutingFee: 100,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap partial COLLECT success with no routing fee",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 20401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 8000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 12160,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 14160,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 2000,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 12160,
+ FeeInSat: 240,
+ TotalInSat: 12400,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 14160,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 2000,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap partial COLLECT success",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 20401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 8000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 12160,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 14161,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 2000,
+ RoutingFee: 1,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 12160,
+ FeeInSat: 240,
+ TotalInSat: 12401,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 14161,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 2000,
+ RoutingFee: 1,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap partial COLLECT for unpayable amount",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 20401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 8000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 12401,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 14401,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 2000,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 12401,
+ FeeInSat: 240,
+ TotalInSat: 12641,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 14401,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 2000,
+ RoutingFee: 0,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice as LEND swap success",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 20401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 3000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 12401,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 10,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 1000000,
+ PotentialCollect: 0,
+ MaxAmountFor0Conf: 1000000,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 12401,
+ FeeInSat: 0,
+ TotalInSat: 12411,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 0,
+ DebtType: fees.DebtTypeLend,
+ DebtAmount: 12411,
+ RoutingFee: 10,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice as COLLECT swap success",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 20401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 3000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 12401,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 10,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 100,
+ PotentialCollect: 1010,
+ MaxAmountFor0Conf: 1000000,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 12401,
+ FeeInSat: 240,
+ TotalInSat: 12651,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 13421,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 1010,
+ RoutingFee: 10,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice as NON DEBT swap success",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 20401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 3000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 12401,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 10,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 0,
+ PotentialCollect: 0,
+ MaxAmountFor0Conf: 1000000,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 12401,
+ FeeInSat: 240,
+ TotalInSat: 12651,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 12411,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 10,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice one-conf",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 20401,
+ SizeInVByte: 240,
+ },
+ },
+ ExpectedDebtInSat: 3000,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: false,
+ AmountInSat: 12401,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1,
+ FeeBase: 10,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 0,
+ PotentialCollect: 0,
+ MaxAmountFor0Conf: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 12401,
+ FeeInSat: 2400,
+ TotalInSat: 14811,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 12411,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ RoutingFee: 10,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 1,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA one-conf",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 369_310,
+ SizeInVByte: 253,
+ },
+ },
+ ExpectedDebtInSat: 26876,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 342434,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 1000000,
+ FeeProportionalMillionth: 1051,
+ FeeBase: 4,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 123124,
+ PotentialCollect: 20686,
+ MaxAmountFor0Conf: 0,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 316798,
+ FeeInSat: 25300,
+ TotalInSat: 342434,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 337820,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 20686,
+ RoutingFee: 336,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 1,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA with invalid amount",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 36_931,
+ SizeInVByte: 253,
+ },
+ },
+ ExpectedDebtInSat: 26876,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 10054,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1051,
+ FeeBase: 4,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 123124,
+ PotentialCollect: 20686,
+ MaxAmountFor0Conf: 193437,
+ },
+ },
+ err: true,
+ },
+ {
+ desc: "swap amountless invoice TFFA cant pay combined fee",
+ feeWindow: &FeeWindow{
+ TargetedFees: func() map[uint]float64 {
+ fees := make(map[uint]float64)
+ fees[1] = 100.0
+ fees[5] = 50.0
+ fees[10] = 3.1
+ return fees
+ }(),
+ },
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 800,
+ SizeInVByte: 253,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 800,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1051,
+ FeeBase: 40,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 123124,
+ PotentialCollect: 20686,
+ MaxAmountFor0Conf: 193437,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 0,
+ FeeInSat: 785,
+ TotalInSat: 1585,
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 1)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 36_931,
+ SizeInVByte: 253,
+ },
+ },
+ ExpectedDebtInSat: 26876,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 10055,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1051,
+ FeeBase: 4,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 123124,
+ PotentialCollect: 20686,
+ MaxAmountFor0Conf: 193437,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 9977,
+ FeeInSat: 64,
+ TotalInSat: 10055,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 30677,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 20686,
+ RoutingFee: 14,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 2)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 83880,
+ SizeInVByte: 495,
+ },
+ },
+ ExpectedDebtInSat: 73590,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 10290,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 297,
+ FeeBase: 4,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 76410,
+ PotentialCollect: 33292,
+ MaxAmountFor0Conf: 254235,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 10159,
+ FeeInSat: 124,
+ TotalInSat: 10290,
+ SwapFees: &fees.SwapFees{
+ OutputAmount: 43458,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 33292,
+ RoutingFee: 7,
+ OutputPadding: 0,
+ ConfirmationsNeeded: 0,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 3)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 1597,
+ SizeInVByte: 391,
+ },
+ },
+ ExpectedDebtInSat: 1511,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 86,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 875,
+ FeeBase: 2,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 148489,
+ PotentialCollect: 759,
+ MaxAmountFor0Conf: 163704,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ FeeInSat: 98,
+ TotalInSat: 86,
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 4)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 1016,
+ SizeInVByte: 409,
+ },
+ },
+ ExpectedDebtInSat: 776,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 240,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 468,
+ FeeBase: 4,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 149224,
+ PotentialCollect: 246,
+ MaxAmountFor0Conf: 243320,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 133,
+ FeeInSat: 103,
+ TotalInSat: 240,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 4,
+ OutputPadding: 163,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 246,
+ OutputAmount: 546,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 5)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 1843,
+ SizeInVByte: 475,
+ },
+ },
+ ExpectedDebtInSat: 458,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 1385,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1417,
+ FeeBase: 2,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 149542,
+ PotentialCollect: 222,
+ MaxAmountFor0Conf: 223421,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 1263,
+ FeeInSat: 119,
+ TotalInSat: 1385,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 3,
+ OutputPadding: 0,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 222,
+ OutputAmount: 1488,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 6)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 6658,
+ SizeInVByte: 262,
+ },
+ },
+ ExpectedDebtInSat: 6539,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 119,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1991,
+ FeeBase: 1,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 143461,
+ PotentialCollect: 6422,
+ MaxAmountFor0Conf: 254856,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 52,
+ FeeInSat: 66,
+ TotalInSat: 119,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 1,
+ OutputPadding: 0,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 6422,
+ OutputAmount: 6475,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 7)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 8169,
+ SizeInVByte: 301,
+ },
+ },
+ ExpectedDebtInSat: 5619,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 2550,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1009,
+ FeeBase: 2,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 144381,
+ PotentialCollect: 1140,
+ MaxAmountFor0Conf: 210617,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 2470,
+ FeeInSat: 76,
+ TotalInSat: 2550,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 4,
+ OutputPadding: 0,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 1140,
+ OutputAmount: 3614,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 8)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 46785,
+ SizeInVByte: 365,
+ },
+ {
+ AmountInSat: 99948,
+ SizeInVByte: 857,
+ },
+ },
+ ExpectedDebtInSat: 40471,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 59477,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 655,
+ FeeBase: 1,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 109529,
+ PotentialCollect: 13892,
+ MaxAmountFor0Conf: 258399,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 59223,
+ FeeInSat: 215,
+ TotalInSat: 59477,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 39,
+ OutputPadding: 0,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 13892,
+ OutputAmount: 73154,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 9)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10087,
+ SizeInVByte: 430,
+ },
+ {
+ AmountInSat: 156784,
+ SizeInVByte: 851,
+ },
+ },
+ ExpectedDebtInSat: 84117,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 72667,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1146,
+ FeeBase: 3,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 65883,
+ PotentialCollect: 77393,
+ MaxAmountFor0Conf: 240440,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 72369,
+ FeeInSat: 213,
+ TotalInSat: 72667,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 85,
+ OutputPadding: 0,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 77393,
+ OutputAmount: 149847,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 10)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 117197,
+ SizeInVByte: 333,
+ },
+ },
+ ExpectedDebtInSat: 18225,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 98972,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 890,
+ FeeBase: 1,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 131775,
+ PotentialCollect: 16109,
+ MaxAmountFor0Conf: 182743,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 98800,
+ FeeInSat: 84,
+ TotalInSat: 98972,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 88,
+ OutputPadding: 0,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 16109,
+ OutputAmount: 114997,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 11)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 3165,
+ SizeInVByte: 402,
+ },
+ },
+ ExpectedDebtInSat: 2812,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 353,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1461,
+ FeeBase: 3,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 147188,
+ PotentialCollect: 222,
+ MaxAmountFor0Conf: 164563,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 249,
+ FeeInSat: 101,
+ TotalInSat: 353,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 3,
+ OutputPadding: 72,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 222,
+ OutputAmount: 546,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 11 bis)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 3165,
+ SizeInVByte: 402,
+ },
+ },
+ ExpectedDebtInSat: 2740,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 425,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1461,
+ FeeBase: 3,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 147188,
+ PotentialCollect: 222,
+ MaxAmountFor0Conf: 164563,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 321,
+ FeeInSat: 101,
+ TotalInSat: 425,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 3,
+ OutputPadding: 0,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 222,
+ OutputAmount: 546,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 12)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 86343,
+ SizeInVByte: 358,
+ },
+ },
+ ExpectedDebtInSat: 32639,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 53704,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1457,
+ FeeBase: 2,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 117361,
+ PotentialCollect: 27161,
+ MaxAmountFor0Conf: 287685,
+ },
+ },
+ // In this scenario there's 1 sat that is lost and will be burn in fees to the miner
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusOk,
+ AmountInSat: 53534,
+ FeeInSat: 90,
+ TotalInSat: 53704,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 79,
+ OutputPadding: 0,
+ DebtType: fees.DebtTypeCollect,
+ DebtAmount: 27161,
+ OutputAmount: 80774,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA (failure 13)",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 500,
+ SizeInVByte: 209,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 500,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 100000,
+ FeeProportionalMillionth: 1000,
+ FeeBase: 1,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 0,
+ PotentialCollect: 0,
+ MaxAmountFor0Conf: math.MaxInt64,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ AmountInSat: 446,
+ FeeInSat: 53,
+ TotalInSat: 500,
+ SwapFees: &fees.SwapFees{
+ RoutingFee: 1,
+ OutputPadding: 99,
+ DebtType: fees.DebtTypeNone,
+ DebtAmount: 0,
+ OutputAmount: 546,
+ },
+ },
+ },
+ {
+ desc: "swap amountless invoice TFFA with no best route for amount",
+ feeWindow: feeWindowFailureTests,
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 117197,
+ SizeInVByte: 333,
+ },
+ },
+ ExpectedDebtInSat: 18225,
+ },
+ payment: &PaymentToInvoice{
+ TakeFeeFromAmount: true,
+ AmountInSat: 98972,
+ BestRouteFees: []fees.BestRouteFees{
+ {
+ MaxCapacity: 90000,
+ FeeProportionalMillionth: 890,
+ FeeBase: 1,
+ },
+ },
+ FundingOutputPolicies: &fees.FundingOutputPolicies{
+ MaximumDebt: 131775,
+ PotentialCollect: 16109,
+ MaxAmountFor0Conf: 182743,
+ },
+ },
+ expected: &PaymentAnalysis{
+ Status: AnalysisStatusUnpayable,
+ FeeInSat: 84,
+ TotalInSat: 99056,
+ },
+ },
+ }
+
+ for _, tC := range testCases {
+ t.Run(tC.desc, func(t *testing.T) {
+
+ nts := defaultNTS
+ if tC.nts != nil {
+ nts = tC.nts
+ }
+
+ feeWindow := defaultFeeWindow
+ if tC.feeWindow != nil {
+ feeWindow = tC.feeWindow
+ }
+
+ analyzer := NewPaymentAnalyzer(feeWindow, nts)
+ analysis, err := analyzer.ToInvoice(tC.payment)
+
+ if err == nil && tC.err {
+ t.Fatal("expected analysis to error")
+ }
+ if err != nil && !tC.err {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(analysis, tC.expected) {
+ t.Fatalf(
+ "analysis does not match expected\n analysis: got %+v, expected %+v\nswapfees: got %+v, expected %+v",
+ analysis,
+ tC.expected,
+ analysis.SwapFees,
+ tC.expected.SwapFees,
+ )
+ }
+ })
+ }
+}
+
+func TestMaxFeeRate(t *testing.T) {
+ testCases := []struct {
+ desc string
+ nts *NextTransactionSize
+ payment *PaymentToAddress
+ expected float64
+ }{
+ {
+ desc: "small amount with one coin",
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 10_000,
+ },
+ expected: 9_900,
+ },
+ {
+ desc: "take fee from amount one coin",
+ payment: &PaymentToAddress{
+ AmountInSat: 1_000_000,
+ TakeFeeFromAmount: true,
+ },
+ expected: 10_000,
+ },
+ {
+ desc: "zero amount",
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 0,
+ },
+ expected: 10_000,
+ },
+ {
+ desc: "zero amount using TFFA",
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 0,
+ },
+ expected: 0,
+ },
+ {
+ desc: "amount greater than balance",
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 1_000_000_000,
+ },
+ expected: 0,
+ },
+ {
+ desc: "small amount with one coin and debt > 0",
+ nts: &NextTransactionSize{
+ SizeProgression: defaultNTS.SizeProgression,
+ ExpectedDebtInSat: 10_000,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 10_000,
+ },
+ expected: 9_800,
+ },
+ {
+ desc: "take fee from amount success with debt > 0",
+ nts: &NextTransactionSize{
+ SizeProgression: defaultNTS.SizeProgression,
+ ExpectedDebtInSat: 10_000,
+ },
+ payment: &PaymentToAddress{
+ AmountInSat: 990_000,
+ TakeFeeFromAmount: true,
+ },
+ expected: 9_900,
+ },
+ {
+ desc: "amount greater than balance because debt > 0",
+ nts: &NextTransactionSize{
+ SizeProgression: defaultNTS.SizeProgression,
+ ExpectedDebtInSat: 10_000,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 999_900,
+ },
+ expected: 0,
+ },
+ {
+ desc: "needs 2 coins to spend",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ {
+ AmountInSat: 20_000,
+ SizeInVByte: 450,
+ },
+ },
+ ExpectedDebtInSat: 0,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 11_000,
+ },
+ expected: 20,
+ },
+ {
+ desc: "needs 2 coins to spend with debt",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ {
+ AmountInSat: 20_000,
+ SizeInVByte: 400,
+ },
+ },
+ ExpectedDebtInSat: 8_000,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: false,
+ AmountInSat: 11_000,
+ FeeRateInSatsPerVByte: 0,
+ },
+ expected: 2.5,
+ },
+ {
+ desc: "TFFA needs 2 coins to spend with debt",
+ nts: &NextTransactionSize{
+ SizeProgression: []SizeForAmount{
+ {
+ AmountInSat: 10_000,
+ SizeInVByte: 240,
+ },
+ {
+ AmountInSat: 20_000,
+ SizeInVByte: 400,
+ },
+ },
+ ExpectedDebtInSat: 8_000,
+ },
+ payment: &PaymentToAddress{
+ TakeFeeFromAmount: true,
+ AmountInSat: 12_000,
+ },
+ expected: 30,
+ },
+ }
+
+ for _, tC := range testCases {
+ t.Run(tC.desc, func(t *testing.T) {
+
+ var analyzer *PaymentAnalyzer
+ if tC.nts != nil {
+ analyzer = NewPaymentAnalyzer(defaultFeeWindow, tC.nts)
+ } else {
+ analyzer = NewPaymentAnalyzer(defaultFeeWindow, defaultNTS)
+ }
+
+ maxFeeRate := analyzer.MaxFeeRateToAddress(tC.payment)
+ if maxFeeRate != tC.expected {
+ t.Fatalf("Max fee rate %v != %v", maxFeeRate, tC.expected)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/partiallysignedtransaction.go b/libwallet/partiallysignedtransaction.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/partiallysignedtransaction.go
rename to libwallet/partiallysignedtransaction.go
diff --git a/libwallet/partiallysignedtransaction_test.go b/libwallet/partiallysignedtransaction_test.go
new file mode 100755
index 0000000..903e22f
--- /dev/null
+++ b/libwallet/partiallysignedtransaction_test.go
@@ -0,0 +1,830 @@
+package libwallet
+
+import (
+ "bytes"
+ "encoding/hex"
+ "testing"
+
+ "github.com/btcsuite/btcd/txscript"
+ "github.com/btcsuite/btcd/wire"
+
+ "github.com/muun/libwallet/addresses"
+ "github.com/muun/libwallet/walletdb"
+)
+
+const (
+ basePath = "m/schema:1'/recovery:1'"
+)
+
+type input struct {
+ outpoint outpoint
+ address MuunAddress
+ userSignature []byte
+ muunSignature []byte
+ submarineSwapV1 inputSubmarineSwapV1
+ submarineSwapV2 inputSubmarineSwapV2
+ incomingSwap inputIncomingSwap
+}
+
+func (i *input) OutPoint() Outpoint {
+ return &i.outpoint
+}
+
+func (i *input) Address() MuunAddress {
+ return i.address
+}
+
+func (i *input) UserSignature() []byte {
+ return i.userSignature
+}
+
+func (i *input) MuunSignature() []byte {
+ return i.muunSignature
+}
+
+func (i *input) SubmarineSwapV1() InputSubmarineSwapV1 {
+ return &i.submarineSwapV1
+}
+
+func (i *input) SubmarineSwapV2() InputSubmarineSwapV2 {
+ return &i.submarineSwapV2
+}
+
+func (i *input) IncomingSwap() InputIncomingSwap {
+ return &i.incomingSwap
+}
+
+func (i *input) MuunPublicNonce() []byte {
+ return nil
+}
+
+type outpoint struct {
+ txId []byte
+ index int
+ amount int64
+}
+
+func (o *outpoint) TxId() []byte {
+ return o.txId
+}
+
+func (o *outpoint) Index() int {
+ return o.index
+}
+
+func (o *outpoint) Amount() int64 {
+ return o.amount
+}
+
+type inputSubmarineSwapV1 struct {
+ refundAddress string
+ paymentHash256 []byte
+ serverPublicKey []byte
+ lockTime int64
+}
+
+func (i *inputSubmarineSwapV1) RefundAddress() string {
+ return i.refundAddress
+}
+
+func (i *inputSubmarineSwapV1) PaymentHash256() []byte {
+ return i.paymentHash256
+}
+
+func (i *inputSubmarineSwapV1) ServerPublicKey() []byte {
+ return i.serverPublicKey
+}
+
+func (i *inputSubmarineSwapV1) LockTime() int64 {
+ return i.lockTime
+}
+
+type inputSubmarineSwapV2 struct {
+ paymentHash256 []byte
+ serverPublicKey []byte
+ userPublicKey []byte
+ muunPublicKey []byte
+ blocksForExpiration int64
+ serverSignature []byte
+}
+
+func (i *inputSubmarineSwapV2) PaymentHash256() []byte {
+ return i.paymentHash256
+}
+
+func (i *inputSubmarineSwapV2) ServerPublicKey() []byte {
+ return i.serverPublicKey
+}
+
+func (i *inputSubmarineSwapV2) UserPublicKey() []byte {
+ return i.userPublicKey
+}
+
+func (i *inputSubmarineSwapV2) MuunPublicKey() []byte {
+ return i.muunPublicKey
+}
+
+func (i *inputSubmarineSwapV2) BlocksForExpiration() int64 {
+ return i.blocksForExpiration
+}
+
+func (i *inputSubmarineSwapV2) ServerSignature() []byte {
+ return i.serverSignature
+}
+
+type inputIncomingSwap struct {
+ sphinx []byte
+ htlcTx []byte
+ paymentHash []byte
+ swapServerPublicKey string
+ expirationHeight int64
+ collectInSats int64
+}
+
+func (i *inputIncomingSwap) Sphinx() []byte {
+ return i.sphinx
+}
+
+func (i *inputIncomingSwap) HtlcTx() []byte {
+ return i.htlcTx
+}
+
+func (i *inputIncomingSwap) PaymentHash256() []byte {
+ return i.paymentHash
+}
+
+func (i *inputIncomingSwap) SwapServerPublicKey() string {
+ return i.swapServerPublicKey
+}
+
+func (i *inputIncomingSwap) ExpirationHeight() int64 {
+ return i.expirationHeight
+}
+
+func (i *inputIncomingSwap) CollectInSats() int64 {
+ return i.collectInSats
+}
+
+func TestPartiallySignedTransaction_SignV1(t *testing.T) {
+ const (
+ hexTx = "0100000001706bcabdcdcfd519bdb4534f8ace9f8a3cd614e7b00f074cce0a58913eadfffb0100000000ffffffff022cf46905000000001976a914072b22dfb34153d4e084dce8c6655430d37f12d088aca4de8b00000000001976a914fded0987447ef3273cde87bf8b65a11d1fd9caca88ac00000000"
+ hexTxOut = "fbffad3e91580ace4c070fb0e714d63c8a9fce8a4f53b4bd19d5cfcdbdca6b70"
+ txIndex = 1
+ txAmount = 100000000
+
+ addressPath = "m/schema:1'/recovery:1'/external:1/1"
+ originAddress = "n4fbDDpmfZgyjHsp93C5z7rd68Wq5kS2tj"
+
+ encodedUserKey = "tprv8eJiUjHpVRyTUM1p4XDRUdRZPJLfud22swAv48my1MxaCZztUNRrWxmN6ycdd9a2xfJwLchq5jW9m2jkNpwruijwvygCv41e6YrsqUvw7hQ"
+ )
+
+ txOut1, _ := hex.DecodeString(hexTxOut)
+
+ inputs := []Input{
+ &input{
+ outpoint: outpoint{index: txIndex, amount: txAmount, txId: txOut1},
+ address: addresses.New(addresses.V1, addressPath, originAddress),
+ },
+ }
+
+ inputList := &InputList{inputs: inputs}
+ rawTx, _ := hex.DecodeString(hexTx)
+ partial, _ := NewPartiallySignedTransaction(inputList, rawTx, nil)
+
+ userKey, _ := NewHDPrivateKeyFromString(encodedUserKey, basePath, Regtest())
+ // We dont need to use the muunKey in V1
+ signedRawTx, err := partial.Sign(userKey, userKey.PublicKey())
+
+ if err != nil {
+ t.Fatalf("failed to sign tx due to %v", err)
+ }
+
+ signedTx := wire.NewMsgTx(0)
+ signedTx.Deserialize(bytes.NewReader(signedRawTx.Bytes))
+
+ verifyInput(t, signedTx, hexTx, txIndex, 0)
+
+}
+func TestPartiallySignedTransaction_SignV2(t *testing.T) {
+
+ const (
+ hexTx = "0100000004f3c15d23060a622bef5e0346ba3410ec118b959be0058c282a1e2045af511b720100000000ffffffffb8ac53a0702e45f7d0164cf6164b48fe66b56af23308e9478cb75e3a2627b74a0100000000ffffffff4e54dc96b07fb29f709c30007fc12abdcde6a20bcad73c8ec6124f34ce096f9b0000000000ffffffff4c11c4284a8e48baa4527fd26e7d0c3dda25ffb3a7f92aa2a248b5a76981d8a40000000000ffffffff01a9cbea0b0000000017a914dfca2abd2bb72cf911940a9d16de126cc1cd60368700000000"
+
+ txIndex1 = 1
+ txAmount1 = 50000000
+ hexTxOut1 = "721b51af45201e2a288c05e09b958b11ec1034ba46035eef2b620a06235dc1f3"
+ hexMuunSig1 = "3045022100d07028674c49d8dabc536db47f1371c2f61fc578cb2c8797a570e3176f5e91c902206a83db8ad5b63e88c48d0ae4e67646fcf6e33d0177a88996c15b280494885e7b01"
+ hexTx1 = "0200000001020678c852c6d943cf0d3a9b5102b1a4e2ebccdb4ca2eaae7731c8f59b81172a000000004847304402204a3958c1bd6abcd7b5ec2291bd43391dcfe757068ff0e340dd8f502cb25435b0022076e865730e49e4d126b94675d276545e35afa84feea2873bb5f923b842d90f4801feffffff0224bf45220000000017a914cb81f4e1ff68249e6f4f17a7995007b5a478705b8780f0fa020000000017a914dfca2abd2bb72cf911940a9d16de126cc1cd60368794020000"
+
+ txIndex2 = 1
+ txAmount2 = 50000000
+ hexTxOut2 = "4ab727263a5eb78c47e90833f26ab566fe484b16f64c16d0f7452e70a053acb8"
+ hexMuunSig2 = "304402201b0c35179a5fa8e6255115450979a77dbb97d89157e236783df0312a5d7bdb2c022064bae7ad0cdc72e4339421067cc65e0c3d03690a5c2d98c32a6ef67f883558a001"
+ hexTx2 = "0200000001ff3f3b16506ef957b9ea80287f276ee415380597a4ede7ae45fff6e18d3e13d8000000004847304402204dbe876d7f0761a72ecc2d0e0e45c1ab32d6bd69d5062068984e26af02c4b27102202f2bd18a17821bdce155b13ea2c379bb78c9157f7f44e2e6a8cef1a154ec68ac01feffffff0224bf45220000000017a914684830d4ef58c54b6b3db6b4a3eb7818d418ae258780f0fa020000000017a914dfca2abd2bb72cf911940a9d16de126cc1cd60368794020000"
+
+ txIndex3 = 0
+ txAmount3 = 50000000
+ hexTxOut3 = "9b6f09ce344f12c68e3cd7ca0ba2e6cdbd2ac17f00309c709fb27fb096dc544e"
+ hexMuunSig3 = "30440220076b14b1c906089546cb40ce05dab38f0388ca65d0bc5183d3c3f7dcb98be52c022001eea4635d56726d990daa92ac26c52c9030c96dddcc92e5d623546580aaaef401"
+ hexTx3 = "02000000019fdde3b7eb40584d103a04dd253ffa0ceb458776db56fbee6489aee0d34402d6000000004847304402206abfb750561acac1be3d6ec3eabc1c88ac7ce11f28f5c8162428ce78dabb4d8e0220753c03bf8b9af9c9bf592f52586d39d8aa10c1111f105fea0ce0cf5c82a4574101feffffff0280f0fa020000000017a914dfca2abd2bb72cf911940a9d16de126cc1cd60368724bf45220000000017a9148d7814264268f1f0f98870f95dc69017bd0cce708794020000"
+
+ txIndex4 = 0
+ txAmount4 = 50000000
+ hexTxOut4 = "a4d88169a7b548a2a22af9a7b3ff25da3d0c7d6ed27f52a4ba488e4a28c4114c"
+ hexMuunSig4 = "30440220145dcce0bf6cceda98b3a9635bd7611d92085ff3ad27690bcf471a6b39620e6c02205ca0a0bd93550e86468e236b291457a3ff84a3b5dedeb10067cc9d3233b5dafa01"
+ hexTx4 = "02000000019d657207178c19bb4fd45de6a5f83caadf86bd7519e1569c8daf078a46e565310000000048473044022033c864f4a6ab42ba29d09bb2dd110e55a3c4118fd0a68cbe5c461926cc64d3e9022029a5b57a2a6e24e6f66f4354b74d7ffc7affa6d43843797faa70c84ec47b7b8501feffffff0280f0fa020000000017a914dfca2abd2bb72cf911940a9d16de126cc1cd60368724bf45220000000017a914b392913e36a7017404c60424da4ebb48a53b5bb18794020000"
+
+ addressPath = "m/schema:1'/recovery:1'/external:1/0"
+ originAddress = "2NDeWrsJEwvxwVnvtWzPjhDC5B2LYkFuX2s"
+
+ encodedMuunKey = "tpubDBYMnFoxYLdMBZThTk4uARTe4kGPeEYWdKcaEzaUxt1cesetnxtTqmAxVkzDRou51emWytommyLWcF91SdF5KecA6Ja8oHK1FF7d5U2hMxX"
+ encodedUserKey = "tprv8dfM4H5fYJirMai5Er3LguicgUAyxmcSQbFub5ens16amX1e1HAFiW4SXnFVw9nu9FedFQqTPGTTjPEmgfvvXMKww3UcRpFbbC4DFjbCcTb"
+ basePath = "m/schema:1'/recovery:1'"
+ )
+
+ txOut1, _ := hex.DecodeString(hexTxOut1)
+ muunSig1, _ := hex.DecodeString(hexMuunSig1)
+ txOut2, _ := hex.DecodeString(hexTxOut2)
+ muunSig2, _ := hex.DecodeString(hexMuunSig2)
+ txOut3, _ := hex.DecodeString(hexTxOut3)
+ muunSig3, _ := hex.DecodeString(hexMuunSig3)
+ txOut4, _ := hex.DecodeString(hexTxOut4)
+ muunSig4, _ := hex.DecodeString(hexMuunSig4)
+
+ inputs := []Input{
+ &input{
+ outpoint: outpoint{index: txIndex1, amount: txAmount1, txId: txOut1},
+ address: addresses.New(addresses.V2, addressPath, originAddress),
+ muunSignature: muunSig1},
+ &input{
+ outpoint: outpoint{index: txIndex2, amount: txAmount2, txId: txOut2},
+ address: addresses.New(addresses.V2, addressPath, originAddress),
+ muunSignature: muunSig2},
+ &input{
+ outpoint: outpoint{index: txIndex3, amount: txAmount3, txId: txOut3},
+ address: addresses.New(addresses.V2, addressPath, originAddress),
+ muunSignature: muunSig3},
+ &input{
+ outpoint: outpoint{index: txIndex4, amount: txAmount4, txId: txOut4},
+ address: addresses.New(addresses.V2, addressPath, originAddress),
+ muunSignature: muunSig4},
+ }
+
+ inputList := &InputList{inputs: inputs}
+ rawTx, _ := hex.DecodeString(hexTx)
+ partial, _ := NewPartiallySignedTransaction(inputList, rawTx, nil)
+
+ muunKey, _ := NewHDPublicKeyFromString(encodedMuunKey, basePath, Regtest())
+ userKey, _ := NewHDPrivateKeyFromString(encodedUserKey, basePath, Regtest())
+ signedRawTx, err := partial.Sign(userKey, muunKey)
+
+ if err != nil {
+ t.Fatalf("failed to sign tx due to %v", err)
+ }
+
+ signedTx := wire.NewMsgTx(0)
+ signedTx.Deserialize(bytes.NewReader(signedRawTx.Bytes))
+
+ verifyInput(t, signedTx, hexTx1, txIndex1, 0)
+ verifyInput(t, signedTx, hexTx2, txIndex2, 0)
+ verifyInput(t, signedTx, hexTx3, txIndex3, 0)
+ verifyInput(t, signedTx, hexTx4, txIndex4, 0)
+
+}
+
+func TestPartiallySignedTransaction_SignV3(t *testing.T) {
+ const (
+ hexTx = "01000000014a4ca718419999e9bfb675dc9f7deff6b65512c11469a23d169038267cd097040100000000ffffffff02916067590000000017a91437a2fceeb0c454b22b427c34eb565d8b1dc953ed8797c400000000000017a9142b0cabe5d058bc3c58f8a656dec2601d117262538700000000"
+
+ txIndex1 = 1
+ txAmount1 = 1500000000
+ hexTxOut1 = "0497d07c263890163da26914c11255b6f6ef7d9fdc75b6bfe999994118a74c4a"
+ hexMuunSig1 = "3045022100d138caf8d3c19db84363b33e1ad002e1aee7907302ab5110edaf78d980c94e48022019e841da8759f63596fbcd81a3544219573288877206f8f651cae1023c397f0c01"
+ hexTx1 = "02000000014f1e7a952c72670bf03a040faa183687ec8c9e0fb7adf606d1ce13395fb663000000000017160014a89e2ded102b2dde96e8bc87219113c6d31a1fe4feffffff02240e5ea9cf00000017a9142773c1a1651ad774f4b867d955ae8b816ac806ad87002f68590000000017a9142b0cabe5d058bc3c58f8a656dec2601d117262538736010000"
+
+ encodedMuunKey = "tpubDABPYHYrYQHXY2pYFdcsFd41aE2uZmMQZpRRGiKfgz7G7nU7PoSwrzMKeHHnoMjmn9woC87coUanF2T911R8X5HpUtZRJRf56u4r51gTrqD"
+ encodedUserKey = "tprv8ezdJAiJTZz4BJo1VysKviVqto1f8CAS3d2M9LWZ5oygiMrtb6NYcPnkWTcdP8b2AuKVVegnWe3Czzo7geDqH2MzXvzDu1SiKucVAG6KFvE"
+
+ addressPath = "m/schema:1'/recovery:1'/external:1/0"
+ originAddress = "2MwArDxm83HCWKvoLKcKAg1Nv6ZG7fWYzMa"
+ )
+
+ txOut1, _ := hex.DecodeString(hexTxOut1)
+ muunSig1, _ := hex.DecodeString(hexMuunSig1)
+
+ inputs := []Input{
+ &input{
+ outpoint: outpoint{index: txIndex1, amount: txAmount1, txId: txOut1},
+ address: addresses.New(addresses.V3, addressPath, originAddress),
+ muunSignature: muunSig1},
+ }
+
+ inputList := &InputList{inputs: inputs}
+ rawTx, _ := hex.DecodeString(hexTx)
+ partial, _ := NewPartiallySignedTransaction(inputList, rawTx, nil)
+
+ muunKey, _ := NewHDPublicKeyFromString(encodedMuunKey, basePath, Regtest())
+ userKey, _ := NewHDPrivateKeyFromString(encodedUserKey, basePath, Regtest())
+ signedRawTx, err := partial.Sign(userKey, muunKey)
+
+ if err != nil {
+ t.Fatalf("failed to sign tx due to %v", err)
+ }
+
+ signedTx := wire.NewMsgTx(0)
+ signedTx.Deserialize(bytes.NewReader(signedRawTx.Bytes))
+
+ verifyInput(t, signedTx, hexTx1, txIndex1, 0)
+}
+
+func TestPartiallySignedTransaction_SignSubmarineSwapV1(t *testing.T) {
+ const (
+ hexTx = "01000000021a608c7d6e40586806c33b3b1036fbd305c37e9d38990d912cc02de7e7cec05e0000000000fffffffff18bce10875329410641316bf7c4d984e00780174b6983080e9225dc26e5bd8c0100000000feffffff01705bc0230000000017a91470fcbc29723c85fdbf9fb5189220f279e9be4508878f030000"
+
+ txIndex1 = 0
+ txAmount1 = 599817960
+ hexTxOut1 = "5ec0cee7e72dc02c910d99389d7ec305d3fb36103b3bc3066858406e7d8c601a"
+ hexTx1 = "0100000006f65ae1c782a5b37795a203a8820719100b1c82f59a4aa1cf3bbcc121442636a50000000023220020f1dcb100a8f4249af53e2ef831e2164545f329a5e8cda589210c033896cd1f12fffffffff21cc482a9359d2762f0a3621eb825e4e728b848588767aecdd8f906833e578e0100000023220020f1dcb100a8f4249af53e2ef831e2164545f329a5e8cda589210c033896cd1f12ffffffff68b507462f19a913b7a6a2a6956cd1c514e66b669d50b3f6228cc21935b78b7f00000000232200203ec9de492dfda91c6d7e84a14f478b1fd6c4b3432aeb4262482133975f94e8f2fffffffff18bce10875329410641316bf7c4d984e00780174b6983080e9225dc26e5bd8c00000000232200209f60ba93792ab212523ad6e6daaefb06d3d0c14ba02ddeaa38582031578bbbd3ffffffff741c42cabd1464b5752e4050acc9d9dfa7ccb296d3847a0e7da6d90effa0d80b0000000023220020d4cf5b8c1ddaa1e2788596655df089cbe10ad33bae149160e07dd76b54e2a1e3ffffffffa609573ae63856433d80793d44d05b077b2c5ef1cc04d820de0d107303ce831b0000000023220020b90f5d2eaf489a24ec6f6d93a47536145fbae13b745fbc7ef9fc5a16d1fa2408ffffffff01e87ec0230000000017a91417c1f13d6ba17a62d6f1f784927c0d45ba22f6fa8700000000"
+ txAddressPath1 = "m/schema:1'/recovery:1'/external:1/2"
+ txAddress1 = "2MuQqs3e42GpYteWDGEN16TqCQDC8oGCpiV"
+ txMuunSigHex1 = "3044022032b35746170883b2f46c2f14019eb95e2e7e4d800248e6a8b372e504dc48674b02202ff47b29abf8f1be8719e757cbd218a4111c214b0c1aa4bdfc7debaf1b46880f01"
+
+ txIndex2 = 1
+ txAmount2 = 18400
+ hexTxOut2 = "8cbde526dc25920e0883694b178007e084d9c4f76b3141064129538710ce8bf1"
+ hexTx2 = "0100000001c00ee241359fa47d45f4f08b67e37f7a31ebe996da59513dfc6c5af97a3959610100000023220020f1dcb100a8f4249af53e2ef831e2164545f329a5e8cda589210c033896cd1f12ffffffff02a064f5050000000017a914d2bf8b44779443e9a7571ab416c72cdee9e9d06e87e04700000000000017a9140c02072aee07d46ab06edb7d75d538c133ebd8c38700000000"
+ txAddressPath2 = "m/schema:1'/recovery:1'/change:0/7"
+ txAddress2 = "2MtLiXVbDBQdHKDAKwAL5AnsTo6LoCakjvg"
+ txPaymentHashHex2 = "0634be42f7a600c0457ace25f2502e9e473b7d5f0e50172dcce25044c8538936"
+ txServerPubKeyHex2 = "035560f6c13e630b4a4b58dac162d4cebd97eb7a96c7ba3636a0bece5c19c2c6dd"
+ txLockTime2 = 911
+ txRefundAddress2 = "n3yUtyw6xAnYNpfkbuVKPSqnGdbqsLNePr"
+
+ encodedMuunKey = "tpubDBZaivUL3Hv8r25JDupShPuWVkGcwM7NgbMBwkhQLfWu18iBbyQCbRdyg1wRMjoWdZN7Afg3F25zs4c8E6Q4VJrGqAw51DJeqacTFABV9u8"
+ encodedUserKey = "tprv8fFtghPy2BsdB8nrBZcrHSihQDb65yVJa5DfLcFdtjnRc8SQcV4d59hZAzn2auLdEom9KscWv5JAuxUG65gDYiBxwbGarcix7H2Vp8xXPnX"
+ )
+
+ txOut1, _ := hex.DecodeString(hexTxOut1)
+ txOut2, _ := hex.DecodeString(hexTxOut2)
+
+ muunSig1, _ := hex.DecodeString(txMuunSigHex1)
+ paymentHash2, _ := hex.DecodeString(txPaymentHashHex2)
+ serverPubKey2, _ := hex.DecodeString(txServerPubKeyHex2)
+
+ inputs := []Input{
+ &input{
+ outpoint: outpoint{index: txIndex1, amount: txAmount1, txId: txOut1},
+ address: addresses.New(addresses.V3, txAddressPath1, txAddress1),
+ muunSignature: muunSig1,
+ },
+ &input{
+ outpoint: outpoint{index: txIndex2, amount: txAmount2, txId: txOut2},
+ address: addresses.New(addresses.SubmarineSwapV1, txAddressPath2, txAddress2),
+ submarineSwapV1: inputSubmarineSwapV1{
+ refundAddress: txRefundAddress2,
+ paymentHash256: paymentHash2,
+ serverPublicKey: serverPubKey2,
+ lockTime: txLockTime2,
+ },
+ },
+ }
+
+ inputList := &InputList{inputs: inputs}
+ rawTx, _ := hex.DecodeString(hexTx)
+ partial, _ := NewPartiallySignedTransaction(inputList, rawTx, nil)
+
+ muunKey, _ := NewHDPublicKeyFromString(encodedMuunKey, basePath, Regtest())
+ userKey, _ := NewHDPrivateKeyFromString(encodedUserKey, basePath, Regtest())
+ signedRawTx, err := partial.Sign(userKey, muunKey)
+
+ if err != nil {
+ t.Fatalf("failed to sign tx due to %v", err)
+ }
+
+ signedTx := wire.NewMsgTx(0)
+ signedTx.Deserialize(bytes.NewReader(signedRawTx.Bytes))
+
+ verifyInput(t, signedTx, hexTx1, txIndex1, 0)
+ verifyInput(t, signedTx, hexTx2, txIndex2, 1)
+}
+
+func verifyInput(t *testing.T, signedTx *wire.MsgTx, hexPrevTx string, prevIndex, index int) {
+ t.Helper()
+
+ // Uncomment the next block if you need to see what the script engine outputs
+ txscript.DisableLog()
+ // logger := btclog.NewBackend(os.Stderr).Logger("test")
+ // logger.SetLevel(btclog.LevelTrace)
+ // txscript.UseLogger(logger)
+
+ prevTx := wire.NewMsgTx(0)
+
+ rawPrevTx, _ := hex.DecodeString(hexPrevTx)
+ prevTx.Deserialize(bytes.NewReader(rawPrevTx))
+
+ flags := txscript.ScriptBip16 | txscript.ScriptVerifyDERSignatures |
+ txscript.ScriptStrictMultiSig | txscript.ScriptDiscourageUpgradableNops |
+ txscript.ScriptVerifyStrictEncoding | txscript.ScriptVerifyLowS |
+ txscript.ScriptVerifyWitness | txscript.ScriptVerifyCheckLockTimeVerify
+
+ vm, err := txscript.NewEngine(prevTx.TxOut[prevIndex].PkScript, signedTx, index, flags, nil, nil, prevTx.TxOut[prevIndex].Value)
+ if err != nil {
+ t.Fatalf("failed to build script engine: %v", err)
+ }
+
+ if err := vm.Execute(); err != nil {
+ t.Fatalf("failed to verify script: %v", err)
+ }
+}
+
+func TestPartiallySignedTransaction_SignSubmarineSwapV2(t *testing.T) {
+ const (
+ hexTx = "010000000001010a1e9552f252c4f94dae951a3a2789263650d69de286ed4813333ac73179b4790000000023220020fc4ea5a79e0de596005a77df25fdc1d76a5bd2ca022b58260830b45dbf48005fffffffff0100000000000000001976a91476e6856729db9c3885fbd72c47bd225990eee4ad88ac03473044022038395a9846c02cc1b87655ea4679f3df127fa5f781c7db3598ee43acc65adab4022051f0f874a8c16544c4ab492b8a091b630703d742599ea17c61b2bfadb747f30e0147304402207bd5a91f032ed3d69a7999d170c696861f36991f6b54e24da4319eaf512ccac402203d3d14c42103261f605b3a870ab10b03ff8b84537575768067e41853d77d2b240187210310df0c435a58758d53821915501301581be8c18b63d5a0dab281aa7f98bcb6e67c210226048275203811ab30a61759f8271280cb754ede8c38b5c51fc662dec441511eac637c76a914f722e6b3c976eba035578a7b268de980682d60b1876375677cac6867029000b275ad76a9141528942b8aef6f523d8050ad6bab416d6199352288ac6800000000"
+
+ txIndex2 = 0
+ txAmount2 = 1000
+ hexTxOut2 = "79b47931c73a331348ed86e29dd650362689273a1a95ae4df9c452f252951e0a"
+ hexTx2 = "0100000001b9c3208b3cd1c687d73fec2022ac6ce057c00cf8ae060e5579107a8d99681a7f000000006a473044022042d2e34afb3b66b27641c774b467ce854cfa5d4f9a1eaa462174fa3c688208840220651fdeab3a8134c65431dba040b654d9d21f50343f82bc1870b5280eaff89fc101210209d4e395ce720f13439f4f73b0dac8433f2fa17f094c5fcdaa6965bf96ece088ffffffff02e80300000000000017a914fc7ee7c4ce68ca09559d9e8776f0455039ea18d58718ee052a010000001976a9143447bbd5107cb1572eeb8550f74e5d31a4bf5bd888ac00000000"
+ txAddressPath2 = "m"
+ txAddress2 = "2NGGJJARaFRcARRMDeSWQ46LwU46Z9oKNCZ"
+ txPaymentHashHex2 = "cdb14d5fcf498e8785caff18940bbd713b98b4d425ab0503adb92ab08c5850e3"
+ txServerPubKeyHex2 = "0226048275203811ab30a61759f8271280cb754ede8c38b5c51fc662dec441511e"
+ txBlockForExpiration2 = 144
+ txServerSignatureHex2 = "304402207bd5a91f032ed3d69a7999d170c696861f36991f6b54e24da4319eaf512ccac402203d3d14c42103261f605b3a870ab10b03ff8b84537575768067e41853d77d2b2401"
+
+ encodedMuunKey = "tpubD6NzVbkrYhZ4Yg872usw1wxNYrpCsUmiG4faYMaogSFwJFX9sz8MrR6GNKg4qUDjb3KUYcC9nrUL7tQYfK441qkFP9pwsw6fb8gTW7vJjXq"
+ encodedUserKey = "tprv8ZgxMBicQKsPdu1SiZiQbV4K2af648S6jf8Axu7RkgQborzWpQVRzrSvyoYWb5Rmy8VVyFBDjZobn7ZaK3Ax2hLvF9NxJ6gUWNLwgLxRav7"
+ )
+
+ txOut2, _ := hex.DecodeString(hexTxOut2)
+
+ paymentHash2, _ := hex.DecodeString(txPaymentHashHex2)
+ serverPubKey2, _ := hex.DecodeString(txServerPubKeyHex2)
+ serverSignature2, _ := hex.DecodeString(txServerSignatureHex2)
+
+ muunKey, _ := NewHDPublicKeyFromString(encodedMuunKey, "m", Regtest())
+ userKey, _ := NewHDPrivateKeyFromString(encodedUserKey, "m", Regtest())
+
+ inputs := []Input{
+ &input{
+ outpoint: outpoint{index: txIndex2, amount: txAmount2, txId: txOut2},
+ address: addresses.New(addresses.SubmarineSwapV2, txAddressPath2, txAddress2),
+ submarineSwapV2: inputSubmarineSwapV2{
+ paymentHash256: paymentHash2,
+ serverPublicKey: serverPubKey2,
+ userPublicKey: userKey.PublicKey().Raw(),
+ muunPublicKey: muunKey.Raw(),
+ blocksForExpiration: txBlockForExpiration2,
+ serverSignature: serverSignature2,
+ },
+ },
+ }
+
+ inputList := &InputList{inputs: inputs}
+ rawTx, _ := hex.DecodeString(hexTx)
+ partial, _ := NewPartiallySignedTransaction(inputList, rawTx, nil)
+
+ signedRawTx, err := partial.Sign(userKey, muunKey)
+
+ if err != nil {
+ t.Fatalf("failed to sign tx due to %v", err)
+ }
+
+ signedTx := wire.NewMsgTx(0)
+ signedTx.Deserialize(bytes.NewReader(signedRawTx.Bytes))
+
+ verifyInput(t, signedTx, hexTx2, txIndex2, 0)
+}
+
+func TestPartiallySignedTransaction_SignIncomingSwap(t *testing.T) {
+ const (
+ hexTx = "0100000001e3d55a5423fd70679839f47ed496d61bd4d0964acfa556172c945041eddf3d400000000000ffffffff02f875000000000000220020f411b28870bf089c41f703dbc1a428d60eb7cce61a9d4fa4a5c28ead872d8551963d000000000000220020eee7f6df991fac39aa2fd8054c83ef045c9569507fe4a224c8320162c028267600000000"
+
+ txIndex = 0
+ txAmount = 46200
+ hexTxOut = "403ddfed4150942c1756a5cf4a96d0d41bd696d47ef439986770fd23545ad5e3"
+ txAddressPath = "m/schema:1'/recovery:1'/invoices:4/1189547938/512484821/1"
+ txAddress = "bcrt1qk3mqxrvcdddyhvyywqhwc0vftfqdqt877gt0pxtzety54z73rxsse9hyt9"
+ paymentHashHex = "b0e74c22943fd1e2ee86b14fb6f6636c19649910705913f5bfc33014e0ca0fd4"
+ sphinxHex = "00035a24206be286645b5e2f81fe6d35bf26ceb70b15257f19e3b744c0ed855c3d8e60c5a8d7553d0a39dd162a50df5169f18129a737da3427095e1049c356e02bb71d9c70858bbf3936fb555c283d9015f4b85d629a24e84c61dc69d537545e4c0104a87a9ab6277083cf7cb21a56f10ed23e754adf357a638970fbcd38e985f42c44b69f1cfaac8dbf711a5b8edf56383d56ad4cafb297025fad5f9c3e79dad7d1342cabde86ea85950bb80237d95b676939461aed0447b88d0010023d653abf498780f7d8f9a1e5784638c893caefe95e23a85285b636ce2af87613c275e61da65255dba4f4bacde8d6efd1c29a4f8e3efb98a3881e280b8613d45dbe38b7b895621850322be927a6beb6aa183c9a11dbf29da8a3d2f6b0b6b8c7e1c62d4926f3dc1e06ead192daba315fbcd5edd2a7d08bcfa50d1b2cc799e98b3415202e7cd91ff54962e4e1d5716c339718ebea926db6e24aacf35ebc362aafcfde4d6264d56a6edf430ae4ade75cdf9c121c3708211407d5c7ad23e2bc8dfe0d71e588b01d2ada797830315ba616f6c79030481dde1d8aa1a37676aaa9a48aec1dcc535daf2547cff2d43c58acd7e09473c46cada1112e82b0502d057ce6a8a629836ea293be93c5d228169c46d0643378e20429ba09e0a236f8dc56a23e9d38509a72e3bb115dc7c959af913b7d561d17fd3df8d490e2d91c4ae16429a3ecfa45212dbd6ccd80d4ac5956ade21c46b4960c08570e0af69ee39d1c23b194f20bc4d5d5cb5ec0b1e3a376a51700d166dce2b09a6e0c2285af7d36c4d0178a1acad0bfd8d913c44506987df6406ad7f134927c5d46be261cc4025f6310e8cb8284f03ae75a75d4ca43ba1d578b1be69d76503370f95eb98a769eaf1e7c3032907f9ab50c450b7177a804b2e9cb8c5a6fe5bbeb07d0ad176961f817119989f090ad162cae302242651c0c69e7b52c36665be795538afc3aef77a1cd0a36a170d572f56eb79e07ee6544da446798b5e0a0ed92cd205288824335b0444e5eda4347d28be9b2a128d50f983c5b16ea2792eda5d352b609a08a15268e758e024dbde13ae42008c03c608c6bb1971c9eb7a1842129b056b9c0690a88c1aea43f9bc20d8e132575a1eae77ba2fe24ee780f42e6b73b7022049518f1c231fd4fe3e91ca443980e52507ebc97f8fc49036c6e141c0e74603ac02814aa0928381228f7aadd798dbaac3603099e94224dd0b51466d392d276f19b990e8b351b73d3e284fc24f1c1ee0bffad64d9415ae8ab358b01dbc7eedfadb181ab8080d0f9c151c445419ee670e8376a112a631c5ec4092aca077086299e406ab304c7f864da801147b0e09b9f8873c85e952550b62684ca9217d1c0763930b019871787b07cabe0ef8b541c2375bc7ceb4ec1153e6f8a48371f7f80c69dcd37a7d53053fde41f87231abdbed68f195ddc6082b9d0e55207fea2c4d0c8045d44bfe55de2fb71ed75f12c0105a2ea480678b73100a943c45b6d2d5556ece82f02bd12d8785f38ac96bba1167b27b40dbd4d1677cf0b96f9311382cc110f739ffba634fa5163c9e1bd6f0279356aeab301eca2398525ec136d9d3aab4634a1fe14b365c8ad4f98e217cdba327945dfe69f3a3b7f8cd932bd761b2b264a371b104559d0a69c8e7ac053512f3e7fd2cf64278f33f7288958042b3166ff0f05e174378c80ab8d01332e862f17e8cd5c74b3de9acb51e9526d8d3305fa51b447fcb289b26c96ff06e6d50a639514491077c9f70757b74c7e26800688a05274fdcf2697e69455742126dc0df95512e478417db81440a5d2f8c671df00d0bafca53a8ed4bf6c8ea0a4c8af39c7bb1103b828547b669b75f780d9d78ddd811dd1a639576b19805088c33e6e7855cc360827516f4de8f4788bf81feb45d6d31ba5277bca2c86d84f8a"
+ htlcTxHex = "020000000001042d54f0cbba265e7f2a0873ce9e03879b706b361d3ee18e8d4a29300c0948bee50000000017160014a411cc351bd7c5572a6ba5ba16e3f3f92106425bfeffffff330565534d05b1d0d4b8a1878f1a5c41d3f571f793c528a44d72af276a259bbd000000001716001443891b3727c96d1e1e91aec69b167123a429ed32feffffff4bb7cba13999b98510769270d161f2a8e0f80f1033c72579a78c105ab2b30ebf00000000171600141c9e61f3951956b00c6f215e75e7fb9a5d93988ffefffffffaa153f428dc6c0aa0be74fa56d6563556b4286d5f9b8a431d5acf0b08f00240000000001716001488ef3cc57c509e843387a7c74e55fdc27fe962f2feffffff0178b4000000000000220020b476030d986b5a4bb084702eec3d895a40d02cfef216f09962cac94a8bd119a102473044022059247039c8e3e95d2eb289e35f81b67e5811d5a757b2117cc9dab1cebf830b2e02204d15406ea075d3d4973344b9bf7c151bb39a7e2876551b8500057dce4591fb870121033d8377beee8caf5fccd958bfacfdd3b5dc1948dfad9fb09768d11e69abb76f3402473044022042e242e0c3adca4f8b3a19cbd8eefd80bf8a43debbe51c5759b2f607b7332bc6022020be1288b8dcddfdc1569c766490e8c0ea668ea1bc122d40b59cbe6589e6938701210243b39e7a2d42937b5aecf14d27b200f7486397d92cbb166ae9f3c11259caf9c602473044022024b16f6042b9a664a46ab0c8cfc3424f8a100309f4cb51e9969a62a257255e2b022053f09e518d2ba73f74d7f3feca98df2b38eb45870325a433457277caf1ce7e6a012103f0386dfb783fc1b55d50aa089326dc008fe2fa963b44d3851bc307bbc419d1c4024730440220346c877b9e983a20fbc52fca2132717e2647bba5489e4127e8032c263252c7d502202cea58c807f60c4b6e10262a2eb59daa8bf637465a4ad67db83c59768088bfb70121020dd5d5044667ebd71f0917a6182435afeb408b4142932c9f5d852b5623ca5d6800000000"
+
+ preimageHex = "D7EA6B6FE58119AA061CBA3A3C1B556DE966053EE0B8A455A2FA5BA6EAE978FA"
+ paymentSecretHex = "E06E5076678201F6B1324421315E16B093D7E24CC6F3D76F5A900D6D5DB6313A"
+ shortChanId = uint64(17665301721646554283)
+
+ encodedMuunKey = "tpubDBZaivUL3Hv8r25JDupShPuWVkGcwM7NgbMBwkhQLfWu18iBbyQCbRdyg1wRMjoWdZN7Afg3F25zs4c8E6Q4VJrGqAw51DJeqacTFABV9u8"
+ encodedUserKey = "tprv8deMke4d4jbc5wVYMaDpoqsXYuEPvwLPN43iRRwdZqVJCr9Wc9xh5194mMJeLTkLfQHS5CgkuXbZ9uwK9Eogcx2t7JoscYtrFirGsc3kgCr"
+
+ muunSigHex = "3045022100c4bef5d32c5ed3530cd258df645dfb0298744dee7820095aca1a188a3b2138c102201669e21db8ee4d2b090cbb18e3e52bce40fc5e07be73c1be4d26c9f13c02e69701"
+ )
+
+ txOut, _ := hex.DecodeString(hexTxOut)
+
+ paymentHash, _ := hex.DecodeString(paymentHashHex)
+ sphinx, _ := hex.DecodeString(sphinxHex)
+ htlcTx, _ := hex.DecodeString(htlcTxHex)
+
+ preimage, _ := hex.DecodeString(preimageHex)
+ paymentSecret, _ := hex.DecodeString(paymentSecretHex)
+
+ muunKey, _ := NewHDPublicKeyFromString(encodedMuunKey, "m/schema:1'/recovery:1'", Regtest())
+ userKey, _ := NewHDPrivateKeyFromString(encodedUserKey, "m/schema:1'/recovery:1'", Regtest())
+
+ muunSig, _ := hex.DecodeString(muunSigHex)
+
+ inputs := []Input{
+ &input{
+ outpoint: outpoint{index: txIndex, amount: txAmount, txId: txOut},
+ address: addresses.New(addresses.IncomingSwap, txAddressPath, txAddress),
+ muunSignature: muunSig,
+ incomingSwap: inputIncomingSwap{
+ sphinx: sphinx,
+ htlcTx: htlcTx,
+ paymentHash: paymentHash,
+ swapServerPublicKey: "03912b4cfbd725133cbc319b444c1dad96a8bb0fcf840adc28c8e05e84ecbaa89b",
+ expirationHeight: 5528,
+ collectInSats: 0,
+ },
+ },
+ }
+
+ inputList := &InputList{inputs: inputs}
+ rawTx, _ := hex.DecodeString(hexTx)
+
+ setup()
+
+ db, _ := openDB()
+ db.CreateInvoice(&walletdb.Invoice{
+ Preimage: preimage,
+ PaymentHash: paymentHash,
+ PaymentSecret: paymentSecret,
+ KeyPath: "m/schema:1'/recovery:1'/invoices:4/1189547938/512484821",
+ ShortChanId: shortChanId,
+ AmountSat: txAmount,
+ State: walletdb.InvoiceStateUsed,
+ })
+
+ partial, _ := NewPartiallySignedTransaction(inputList, rawTx, nil)
+
+ signedRawTx, err := partial.Sign(userKey, muunKey)
+
+ if err != nil {
+ t.Fatalf("failed to sign tx due to %v", err)
+ }
+
+ signedTx := wire.NewMsgTx(0)
+ signedTx.Deserialize(bytes.NewReader(signedRawTx.Bytes))
+
+ verifyInput(t, signedTx, htlcTxHex, txIndex, 0)
+}
+
+func TestPartiallySignedTransaction_Verify(t *testing.T) {
+
+ const (
+ hexTx1 = "0100000002a51cc04ab631dee48c989a7cd55c4abc451aa958b09d4579cc9852c52baa57ae0100000000ffffffffdf39591fa749826f87a3d7e5fd5f0468d338c3d81dd3b2c953534b0210f98c560000000000ffffffff02a8d6c20400000000220020452f4ae303ec79acd2bce8f7ddb6469f1060d9146003ea34887e5bbdf021c787000e2707000000002200202ccf0ca2c9b5077ce8345785af26a39277003886fb358877e4083a3fcc5cd66700000000"
+
+ txIndex1 = 1
+ txAmount1 = 100000000
+ txIdHex1 = "ae57aa2bc55298cc79459db058a91a45bc4a5cd57c9a988ce4de31b64ac01ca5"
+ txAddressPath1 = "m/schema:1'/recovery:1'/external:1/0"
+ txAddress1 = "bcrt1q9n8segkfk5rhe6p527z67f4rjfmsqwyxlv6csalypqarlnzu6ens8cm8ye"
+ txAddressVersion1 = addresses.V4
+
+ txIndex2 = 0
+ txAmount2 = 100000000
+ txIdHex2 = "568cf910024b5353c9b2d31dd8c338d368045ffde5d7a3876f8249a71f5939df"
+ txAddressPath2 = "m/schema:1'/recovery:1'/external:1/0"
+ txAddress2 = "bcrt1q9n8segkfk5rhe6p527z67f4rjfmsqwyxlv6csalypqarlnzu6ens8cm8ye"
+ txAddressVersion2 = addresses.V4
+
+ changeAddress1 = "bcrt1qg5h54ccra3u6e54uarmamdjxnugxpkg5vqp75dyg0edmmuppc7rsdfcvcp"
+ changePath1 = "m/schema:1'/recovery:1'/change:0/1"
+ changeVersion1 = addresses.V4
+
+ hexTx2 = "01000000010ead2fa0d6866d0414aba97fd8f1b242fdc3d4c8e7771e40969402319b6e876b0000000000ffffffff02922988040000000017a914d1ac5d61107d2bef187d1aef5cfd3536f4fd5dbe87d6b2050100000000220020bac6de765432ee16e10ce268341062f8f5a417b15a7f6ee8fe903e6d7470f0f700000000"
+
+ txIndex3 = 0
+ txAmount3 = 93266680
+ txIdHex3 = "6b876e9b31029496401e77e7c8d4c3fd42b2f1d87fa9ab14046d86d6a02fad0e"
+ txAddressPath3 = "m/schema:1'/recovery:1'/change:0/8"
+ txAddress3 = "bcrt1q9yzsghvmmn7wv3esylrvn3c469s4ce4thk7qmxdly4tzk4f8vvjsqv0crh"
+ txAddressVersion3 = addresses.V4
+
+ hexTx4 = "0100000002a51cc04ab631dee48c989a7cd55c4abc451aa958b09d4579cc9852c52baa57ae0100000000ffffffffdf39591fa749826f87a3d7e5fd5f0468d338c3d81dd3b2c953534b0210f98c560000000000ffffffff01000e2707000000002200202ccf0ca2c9b5077ce8345785af26a39277003886fb358877e4083a3fcc5cd66700000000"
+
+ changeAddress2 = "bcrt1qg5h54ccra3u6e54uarmamdjxnugxpkg5vqp75dyg0edmmuppc7rsdfcvcp"
+ changePath2 = "m/schema:1'/recovery:1'/change:0/1"
+ changeVersion2 = addresses.V4
+
+ encodedUserKey = "tpubDAKxNPypXDF3GNCpXFUh6sCdxz7DY9eKMgFxYBgyRSiYWXrBLgdtkPuMbQQzrsYLVyPPSHmNcduLRRd9TSMaYrGLryp8KNkkYBm6eka1Bem"
+ encodedMuunKey = "tpubDBZaivUL3Hv8r25JDupShPuWVkGcwM7NgbMBwkhQLfWu18iBbyQCbRdyg1wRMjoWdZN7Afg3F25zs4c8E6Q4VJrGqAw51DJeqacTFABV9u8"
+
+ basePath = "m/schema:1'/recovery:1'"
+ )
+
+ txId1, _ := hex.DecodeString(txIdHex1)
+ txId2, _ := hex.DecodeString(txIdHex2)
+ txId3, _ := hex.DecodeString(txIdHex3)
+
+ userPublicKey, _ := NewHDPublicKeyFromString(
+ encodedUserKey,
+ basePath,
+ Regtest())
+
+ muunPublicKey, _ := NewHDPublicKeyFromString(
+ encodedMuunKey,
+ basePath,
+ Regtest())
+
+ type fields struct {
+ tx string
+ inputs []Input
+ }
+ type args struct {
+ expectations *SigningExpectations
+ userPublicKey *HDPublicKey
+ muunPublickKey *HDPublicKey
+ }
+ firstInput := input{
+ outpoint: outpoint{index: txIndex1, amount: txAmount1, txId: txId1},
+ address: addresses.New(txAddressVersion1, txAddressPath1, txAddress1),
+ }
+ secondInput := input{
+ outpoint: outpoint{index: txIndex2, amount: txAmount2, txId: txId2},
+ address: addresses.New(txAddressVersion2, txAddressPath2, txAddress2),
+ }
+ secondInputGeneratingDust := input{
+ outpoint: outpoint{index: txIndex2, amount: 120000000 - txAmount1 + 122200 + 100 /* dust */, txId: txId2},
+ address: addresses.New(txAddressVersion2, txAddressPath2, txAddress2),
+ }
+ thirdInput := input{
+ outpoint: outpoint{index: txIndex3, amount: txAmount3, txId: txId3},
+ address: addresses.New(txAddressVersion3, txAddressPath3, txAddress3),
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ wantErr bool
+ }{
+ {
+ name: "2 inputs, one change",
+ fields: fields{
+ tx: hexTx1,
+ inputs: []Input{&firstInput, &secondInput},
+ },
+ args: args{
+ expectations: &SigningExpectations{
+ destination: "bcrt1q9n8segkfk5rhe6p527z67f4rjfmsqwyxlv6csalypqarlnzu6ens8cm8ye",
+ amount: 120000000,
+ change: addresses.New(changeVersion1, changePath1, changeAddress1),
+ fee: 122200,
+ },
+ userPublicKey: userPublicKey,
+ muunPublickKey: muunPublicKey,
+ },
+ },
+ {
+ name: "lied about destination amount",
+ fields: fields{
+ tx: hexTx1,
+ inputs: []Input{&firstInput, &secondInput},
+ },
+ args: args{
+ expectations: &SigningExpectations{
+ destination: "bcrt1q9n8segkfk5rhe6p527z67f4rjfmsqwyxlv6csalypqarlnzu6ens8cm8ye",
+ amount: 110000000,
+ change: addresses.New(changeVersion1, changePath1, changeAddress1),
+ fee: 122200,
+ },
+ userPublicKey: userPublicKey,
+ muunPublickKey: muunPublicKey,
+ },
+ wantErr: true,
+ },
+ {
+ name: "lied about change",
+ fields: fields{
+ tx: hexTx1,
+ inputs: []Input{&firstInput, &secondInput},
+ },
+ args: args{
+ expectations: &SigningExpectations{
+ destination: "bcrt1q9n8segkfk5rhe6p527z67f4rjfmsqwyxlv6csalypqarlnzu6ens8cm8ye",
+ amount: 120000000,
+ change: addresses.New(changeVersion1, basePath+"/123", changeAddress1),
+ fee: 122200,
+ },
+ userPublicKey: userPublicKey,
+ muunPublickKey: muunPublicKey,
+ },
+ wantErr: true,
+ },
+ {
+ name: "lied about destination",
+ fields: fields{
+ tx: hexTx1,
+ inputs: []Input{&firstInput, &secondInput},
+ },
+ args: args{
+ expectations: &SigningExpectations{
+ destination: "2N2giv9tsN3pV7Rkm89SReRBgdqKNBESVBk",
+ amount: 120000000,
+ change: addresses.New(changeVersion1, changePath1, changeAddress1),
+ fee: 122200,
+ },
+ userPublicKey: userPublicKey,
+ muunPublickKey: muunPublicKey,
+ },
+ wantErr: true,
+ },
+ {
+ name: "lied about fee",
+ fields: fields{
+ tx: hexTx1,
+ inputs: []Input{&firstInput, &secondInput},
+ },
+ args: args{
+ expectations: &SigningExpectations{
+ destination: "bcrt1q9n8segkfk5rhe6p527z67f4rjfmsqwyxlv6csalypqarlnzu6ens8cm8ye",
+ amount: 120000000,
+ change: addresses.New(changeVersion1, changePath1, changeAddress1),
+ fee: 12200,
+ },
+ userPublicKey: userPublicKey,
+ muunPublickKey: muunPublicKey,
+ },
+ wantErr: true,
+ },
+ {
+ name: "wasnt expecting change",
+ fields: fields{
+ tx: hexTx1,
+ inputs: []Input{&firstInput, &secondInput},
+ },
+ args: args{
+ expectations: &SigningExpectations{
+ destination: "bcrt1q9n8segkfk5rhe6p527z67f4rjfmsqwyxlv6csalypqarlnzu6ens8cm8ye",
+ amount: 120000000,
+ change: nil,
+ fee: 122200,
+ },
+ userPublicKey: userPublicKey,
+ muunPublickKey: muunPublicKey,
+ },
+ wantErr: true,
+ },
+ {
+ name: "lying change",
+ fields: fields{
+ tx: hexTx2,
+ inputs: []Input{&thirdInput},
+ },
+ args: args{
+ expectations: &SigningExpectations{
+ destination: "bcrt1qhtrduaj5xthpdcgvuf5rgyrzlr66g9a3tflka687jqlx6ars7rms0flpmy",
+ amount: 17150678,
+ change: addresses.New(changeVersion2, changePath2, changeAddress2),
+ fee: 83600,
+ },
+ userPublicKey: userPublicKey,
+ muunPublickKey: muunPublicKey,
+ },
+ wantErr: true,
+ },
+ {
+ name: "inputs generating dust",
+ fields: fields{
+ tx: hexTx4,
+ inputs: []Input{&firstInput, &secondInputGeneratingDust},
+ },
+ args: args{
+ expectations: &SigningExpectations{
+ destination: "bcrt1q9n8segkfk5rhe6p527z67f4rjfmsqwyxlv6csalypqarlnzu6ens8cm8ye",
+ amount: 120000000,
+ change: nil,
+ fee: 122200,
+ },
+ userPublicKey: userPublicKey,
+ muunPublickKey: muunPublicKey,
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ inputList := &InputList{inputs: tt.fields.inputs}
+ rawTx, _ := hex.DecodeString(tt.fields.tx)
+ p, err := NewPartiallySignedTransaction(inputList, rawTx, nil)
+ if err != nil {
+ panic(err)
+ }
+ err = p.Verify(tt.args.expectations, tt.args.userPublicKey, tt.args.muunPublickKey)
+ t.Logf("test %v returned %v", tt.name, err)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/publickey.go b/libwallet/publickey.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/publickey.go
rename to libwallet/publickey.go
diff --git a/vendor/github.com/muun/libwallet/recoverycode.go b/libwallet/recoverycode.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/recoverycode.go
rename to libwallet/recoverycode.go
diff --git a/vendor/github.com/muun/libwallet/recoverycode/recoverycode.go b/libwallet/recoverycode/recoverycode.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/recoverycode/recoverycode.go
rename to libwallet/recoverycode/recoverycode.go
diff --git a/libwallet/recoverycode/recoverycode_test.go b/libwallet/recoverycode/recoverycode_test.go
new file mode 100644
index 0000000..9e31e75
--- /dev/null
+++ b/libwallet/recoverycode/recoverycode_test.go
@@ -0,0 +1,86 @@
+package recoverycode
+
+import (
+ "encoding/hex"
+ "testing"
+)
+
+func TestGenerate(t *testing.T) {
+ code := Generate()
+ if err := Validate(code); err != nil {
+ t.Fatalf("expected generated recovery code to be valid, got error: %v", err)
+ }
+}
+
+func TestValidate(t *testing.T) {
+ testCases := []struct {
+ desc string
+ input string
+ expectErr bool
+ }{
+ {
+ desc: "empty string",
+ input: "",
+ expectErr: true,
+ },
+ {
+ desc: "invalid version",
+ input: "LB2Q-48Z3-25JR-S5JB-5SUS-HXHJ-RCMM-8YUA",
+ expectErr: true,
+ },
+ {
+ desc: "invalid characters",
+ input: "LA2Q-48Z3-25JR-S51B-5SUS-HXHJ-RCMM-8YUA",
+ expectErr: true,
+ },
+ {
+ desc: "invalid length",
+ input: "LA2Q-48Z3-25JR-SB5S-USHX-HJRC-MM8YUA",
+ expectErr: true,
+ },
+ {
+ desc: "valid",
+ input: "LA2Q-48Z3-25JR-S5JB-5SUS-HXHJ-RCMM-8YUA",
+ expectErr: false,
+ },
+ }
+ for _, tt := range testCases {
+ t.Run(tt.desc, func(t *testing.T) {
+ err := Validate(tt.input)
+ if (err != nil) != tt.expectErr {
+ t.Errorf("unexpected result: %v", err)
+ }
+ })
+ }
+}
+
+func TestConvertToKey(t *testing.T) {
+ testCases := []struct {
+ desc string
+ input string
+ expected string
+ }{
+ {
+ desc: "legacy recovery code",
+ input: "R52Q-48Z3-25JR-S5JB-5SUS-HXHJ-RCMM-8YUA",
+ expected: "ade3fe99c608fd04484bce1ccf2889a5096f68f4b6b459e7f9ee9f0ada0a2782",
+ },
+ {
+ desc: "version 2 recovery code",
+ input: "LA2Q-48Z3-25JR-S5JB-5SUS-HXHJ-RCMM-8YUA",
+ expected: "0e1446153d4cafb073110739608fdd76b8712221476ec198cf35e1d74d274e83",
+ },
+ }
+ for _, tt := range testCases {
+ t.Run(tt.desc, func(t *testing.T) {
+ key, err := ConvertToKey(tt.input, "FFFFFFFF")
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ got := hex.EncodeToString(key.Serialize())
+ if got != tt.expected {
+ t.Errorf("expected %v but got %v", tt.expected, got)
+ }
+ })
+ }
+}
diff --git a/libwallet/recoverycode_test.go b/libwallet/recoverycode_test.go
new file mode 100644
index 0000000..7873143
--- /dev/null
+++ b/libwallet/recoverycode_test.go
@@ -0,0 +1,41 @@
+package libwallet
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestRecoveryCodeToKey(t *testing.T) {
+ type args struct {
+ code string
+ salt string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ wantErr bool
+ }{
+ {
+ name: "boop",
+ args: args{
+ code: "3V4N-R9EC-V3TQ-NRB3-Q7NY-9HXP-CSDC-B5BC",
+ salt: "63f701fda4fc0b0c",
+ },
+ want: "0220af974bdfdf4274d21c061bbf21e67d3e2687f5b43073b4e9aac5bae53a3602",
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := RecoveryCodeToKey(tt.args.code, tt.args.salt)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("RecoveryCodeToKey() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got.PubKeyHex(), tt.want) {
+ t.Errorf("RecoveryCodeToKey() got = %v, want %v", got.PubKeyHex(), tt.want)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/ripemd160.go b/libwallet/ripemd160.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/ripemd160.go
rename to libwallet/ripemd160.go
diff --git a/libwallet/ripemd160_test.go b/libwallet/ripemd160_test.go
new file mode 100755
index 0000000..27f1cd8
--- /dev/null
+++ b/libwallet/ripemd160_test.go
@@ -0,0 +1,42 @@
+package libwallet
+
+import (
+ "reflect"
+ "testing"
+)
+
+func Test_ripemd160(t *testing.T) {
+ type args struct {
+ data []byte
+ }
+ tests := []struct {
+ name string
+ args args
+ want []byte
+ }{
+ {name: "simple",
+ args: args{data: []byte{1, 2, 3}},
+ want: []byte{121, 249, 1, 218, 38, 9, 240, 32, 173, 173, 191, 46, 95, 104, 161, 108, 140, 63, 125, 87}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := ripemd160(tt.args.data); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("ripemd160() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+
+ t.Run("random input", func(t *testing.T) {
+ small := randomBytes(8)
+ smallRes := ripemd160(small)
+ if smallRes == nil || len(smallRes) != 20 {
+ t.Errorf("result is not of expected size for input (%v, %v)", smallRes, small)
+ }
+
+ big := randomBytes(120)
+ bigRes := ripemd160(big)
+ if bigRes == nil || len(bigRes) != 20 {
+ t.Errorf("result is not of expected size for input (%v, %v)", bigRes, big)
+ }
+ })
+}
diff --git a/vendor/github.com/muun/libwallet/scrypt.go b/libwallet/scrypt.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/scrypt.go
rename to libwallet/scrypt.go
diff --git a/vendor/github.com/muun/libwallet/segwit.go b/libwallet/segwit.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/segwit.go
rename to libwallet/segwit.go
diff --git a/vendor/github.com/muun/libwallet/sphinx/sphinx.go b/libwallet/sphinx/sphinx.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/sphinx/sphinx.go
rename to libwallet/sphinx/sphinx.go
diff --git a/vendor/github.com/muun/libwallet/submarineSwap.go b/libwallet/submarineSwap.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/submarineSwap.go
rename to libwallet/submarineSwap.go
diff --git a/vendor/github.com/muun/libwallet/submarineSwapV1.go b/libwallet/submarineSwapV1.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/submarineSwapV1.go
rename to libwallet/submarineSwapV1.go
diff --git a/vendor/github.com/muun/libwallet/submarineSwapV2.go b/libwallet/submarineSwapV2.go
old mode 100644
new mode 100755
similarity index 100%
rename from vendor/github.com/muun/libwallet/submarineSwapV2.go
rename to libwallet/submarineSwapV2.go
diff --git a/vendor/github.com/muun/libwallet/swaps/swaps.go b/libwallet/swaps/swaps.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/swaps/swaps.go
rename to libwallet/swaps/swaps.go
diff --git a/vendor/github.com/muun/libwallet/swaps/v1.go b/libwallet/swaps/v1.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/swaps/v1.go
rename to libwallet/swaps/v1.go
diff --git a/libwallet/swaps/v1_test.go b/libwallet/swaps/v1_test.go
new file mode 100755
index 0000000..cfae33e
--- /dev/null
+++ b/libwallet/swaps/v1_test.go
@@ -0,0 +1,75 @@
+package swaps
+
+import (
+ "testing"
+
+ "github.com/btcsuite/btcd/chaincfg"
+ "github.com/btcsuite/btcutil/hdkeychain"
+ "github.com/muun/libwallet/addresses"
+)
+
+func TestValidateSubmarineSwapV1(t *testing.T) {
+ type args struct {
+ rawInvoice string
+ userPublicKey *KeyDescriptor
+ muunPublicKey *KeyDescriptor
+ swap *SubmarineSwap
+ network *chaincfg.Params
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ }{
+ {
+ name: "successful",
+ args: args{
+ rawInvoice: "lnbcrt1p033394pp5sfcfh0ukkjfcvcg2vwk2hudue9d48lawqkacdan4msxne66w4krqdqqcqzpgsp5jelulm6a7q38j6jffa9qet3scz4qvcs08x6hfsyn0lfg34p2584q9qy9qsqjcq059jh8qeslj7qwl69ln69znalrxykhaj4kl0g0kfstwsa3warsyxx2d0rqs24tx896lz895wffqaj7l82zs896ec7r5arnw0cwtqpvzt8yd",
+ userPublicKey: &KeyDescriptor{
+ Key: decodeKey("tpubD6NzVbkrYhZ4Y3iy9soFSA9zoYbpyhUFu3eAH1sDWyERxH2yJVZUhPUX5QsxD6bZfMWRKzxw28ohD5n6AZWmvZbDpZzgxSVxUnMevqzTXQk"),
+ Path: "m",
+ },
+ muunPublicKey: &KeyDescriptor{
+ Key: decodeKey("tpubD6NzVbkrYhZ4XbhomyY2axxKe3KB1FK2Wq2z7XYyDF3T4QCuEDZFBUyGfjfHChvEbsbP9RpaYA8cwxkZpQjEcNdaPfuj3cKGqCiHC5YeRTo"),
+ Path: "m",
+ },
+ swap: &SubmarineSwap{
+ FundingOutput: SubmarineSwapFundingOutput{
+ OutputAddress: "2MvW8nGkzFXnLWUca6ZGUh3yqEq5MKEyAxb",
+ ExpirationInBlocks: 10,
+ ServerPaymentHashInHex: "82709bbf96b49386610a63acabf1bcc95b53ffae05bb86f675dc0d3ceb4ead86",
+ UserRefundAddress: addresses.New(addresses.V4, "m", "bcrt1q553urspdhwr49xavd67fvl35pzacz4853l4u09vntr8z06djnw7s95fgat"),
+ UserPublicKey: decodeKey("tpubD6NzVbkrYhZ4Y3iy9soFSA9zoYbpyhUFu3eAH1sDWyERxH2yJVZUhPUX5QsxD6bZfMWRKzxw28ohD5n6AZWmvZbDpZzgxSVxUnMevqzTXQk"),
+ MuunPublicKey: decodeKey("tpubD6NzVbkrYhZ4XbhomyY2axxKe3KB1FK2Wq2z7XYyDF3T4QCuEDZFBUyGfjfHChvEbsbP9RpaYA8cwxkZpQjEcNdaPfuj3cKGqCiHC5YeRTo"),
+ },
+ Receiver: SubmarineSwapReceiver{
+ PublicKey: "02c9a35bdbeab0b93ee9542d85c38beab7d1e72ea1d9639e5b00b1d5feb64bcfdd",
+ },
+ },
+ network: &chaincfg.RegressionNetParams,
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := tt.args.swap.validateV1(
+ tt.args.rawInvoice,
+ tt.args.userPublicKey,
+ tt.args.muunPublicKey,
+ tt.args.network,
+ )
+ if (err != nil) != tt.wantErr {
+ t.Errorf("validateV1() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func decodeKey(s string) *hdkeychain.ExtendedKey {
+ key, err := hdkeychain.NewKeyFromString(s)
+ if err != nil {
+ panic(err)
+ }
+ return key
+}
diff --git a/vendor/github.com/muun/libwallet/swaps/v2.go b/libwallet/swaps/v2.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/swaps/v2.go
rename to libwallet/swaps/v2.go
diff --git a/libwallet/swaps/v2_test.go b/libwallet/swaps/v2_test.go
new file mode 100644
index 0000000..07a6a40
--- /dev/null
+++ b/libwallet/swaps/v2_test.go
@@ -0,0 +1,133 @@
+package swaps
+
+import (
+ "testing"
+
+ "github.com/btcsuite/btcd/chaincfg"
+)
+
+func TestValidateSubmarineSwapV2(t *testing.T) {
+ type args struct {
+ rawInvoice string
+ userPublicKey *KeyDescriptor
+ muunPublicKey *KeyDescriptor
+ swap *SubmarineSwap
+ originalExpirationInBlocks int64
+ network *chaincfg.Params
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ }{
+ {
+ name: "invalid invoice",
+ args: args{
+ swap: &SubmarineSwap{
+ FundingOutput: SubmarineSwapFundingOutput{},
+ },
+ rawInvoice: "invalid",
+ userPublicKey: &KeyDescriptor{
+ Key: decodeKey("tpubD6NzVbkrYhZ4Y3iy9soFSA9zoYbpyhUFu3eAH1sDWyERxH2yJVZUhPUX5QsxD6bZfMWRKzxw28ohD5n6AZWmvZbDpZzgxSVxUnMevqzTXQk"),
+ Path: "m",
+ },
+ muunPublicKey: &KeyDescriptor{
+ Key: decodeKey("tpubD6NzVbkrYhZ4XbhomyY2axxKe3KB1FK2Wq2z7XYyDF3T4QCuEDZFBUyGfjfHChvEbsbP9RpaYA8cwxkZpQjEcNdaPfuj3cKGqCiHC5YeRTo"),
+ Path: "m",
+ },
+ originalExpirationInBlocks: 0,
+ network: &chaincfg.RegressionNetParams,
+ },
+ wantErr: true,
+ },
+ {
+ name: "payment hash from server is invalid hex",
+ args: args{
+ swap: &SubmarineSwap{
+ FundingOutput: SubmarineSwapFundingOutput{
+ ServerPaymentHashInHex: "invalid hex",
+ },
+ },
+ rawInvoice: "lnbcrt1p033394pp5sfcfh0ukkjfcvcg2vwk2hudue9d48lawqkacdan4msxne66w4krqdqqcqzpgsp5jelulm6a7q38j6jffa9qet3scz4qvcs08x6hfsyn0lfg34p2584q9qy9qsqjcq059jh8qeslj7qwl69ln69znalrxykhaj4kl0g0kfstwsa3warsyxx2d0rqs24tx896lz895wffqaj7l82zs896ec7r5arnw0cwtqpvzt8yd",
+ userPublicKey: &KeyDescriptor{
+ Key: decodeKey("tpubD6NzVbkrYhZ4Y3iy9soFSA9zoYbpyhUFu3eAH1sDWyERxH2yJVZUhPUX5QsxD6bZfMWRKzxw28ohD5n6AZWmvZbDpZzgxSVxUnMevqzTXQk"),
+ Path: "m",
+ },
+ muunPublicKey: &KeyDescriptor{
+ Key: decodeKey("tpubD6NzVbkrYhZ4XbhomyY2axxKe3KB1FK2Wq2z7XYyDF3T4QCuEDZFBUyGfjfHChvEbsbP9RpaYA8cwxkZpQjEcNdaPfuj3cKGqCiHC5YeRTo"),
+ Path: "m",
+ },
+ originalExpirationInBlocks: 0,
+ network: &chaincfg.RegressionNetParams,
+ },
+ wantErr: true,
+ },
+ {
+ name: "payment hash from server does not match invoice",
+ args: args{
+ swap: &SubmarineSwap{
+ FundingOutput: SubmarineSwapFundingOutput{
+ ServerPaymentHashInHex: "112233445566778899",
+ },
+ },
+ rawInvoice: "lnbcrt1p033394pp5sfcfh0ukkjfcvcg2vwk2hudue9d48lawqkacdan4msxne66w4krqdqqcqzpgsp5jelulm6a7q38j6jffa9qet3scz4qvcs08x6hfsyn0lfg34p2584q9qy9qsqjcq059jh8qeslj7qwl69ln69znalrxykhaj4kl0g0kfstwsa3warsyxx2d0rqs24tx896lz895wffqaj7l82zs896ec7r5arnw0cwtqpvzt8yd",
+ userPublicKey: &KeyDescriptor{
+ Key: decodeKey("tpubD6NzVbkrYhZ4Y3iy9soFSA9zoYbpyhUFu3eAH1sDWyERxH2yJVZUhPUX5QsxD6bZfMWRKzxw28ohD5n6AZWmvZbDpZzgxSVxUnMevqzTXQk"),
+ Path: "m",
+ },
+ muunPublicKey: &KeyDescriptor{
+ Key: decodeKey("tpubD6NzVbkrYhZ4XbhomyY2axxKe3KB1FK2Wq2z7XYyDF3T4QCuEDZFBUyGfjfHChvEbsbP9RpaYA8cwxkZpQjEcNdaPfuj3cKGqCiHC5YeRTo"),
+ Path: "m",
+ },
+ originalExpirationInBlocks: 0,
+ network: &chaincfg.RegressionNetParams,
+ },
+ wantErr: true,
+ },
+ // TODO: add more test cases for the different error conditions
+ {
+ name: "successful",
+ args: args{
+ swap: &SubmarineSwap{
+ FundingOutput: SubmarineSwapFundingOutput{
+ OutputAddress: "bcrt1qk956axjf2pzmf6esd4jfrppkhmegn8eez2gl2zdkzje0w8lt2tmqvqvrut",
+ ExpirationInBlocks: 10,
+ ServerPaymentHashInHex: "82709bbf96b49386610a63acabf1bcc95b53ffae05bb86f675dc0d3ceb4ead86",
+ UserPublicKey: decodeKey("tpubD6NzVbkrYhZ4Y3iy9soFSA9zoYbpyhUFu3eAH1sDWyERxH2yJVZUhPUX5QsxD6bZfMWRKzxw28ohD5n6AZWmvZbDpZzgxSVxUnMevqzTXQk"),
+ MuunPublicKey: decodeKey("tpubD6NzVbkrYhZ4XbhomyY2axxKe3KB1FK2Wq2z7XYyDF3T4QCuEDZFBUyGfjfHChvEbsbP9RpaYA8cwxkZpQjEcNdaPfuj3cKGqCiHC5YeRTo"),
+ KeyPath: "m",
+ },
+ Receiver: SubmarineSwapReceiver{
+ PublicKey: "02c9a35bdbeab0b93ee9542d85c38beab7d1e72ea1d9639e5b00b1d5feb64bcfdd",
+ },
+ },
+ rawInvoice: "lnbcrt1p033394pp5sfcfh0ukkjfcvcg2vwk2hudue9d48lawqkacdan4msxne66w4krqdqqcqzpgsp5jelulm6a7q38j6jffa9qet3scz4qvcs08x6hfsyn0lfg34p2584q9qy9qsqjcq059jh8qeslj7qwl69ln69znalrxykhaj4kl0g0kfstwsa3warsyxx2d0rqs24tx896lz895wffqaj7l82zs896ec7r5arnw0cwtqpvzt8yd",
+ userPublicKey: &KeyDescriptor{
+ Key: decodeKey("tpubD6NzVbkrYhZ4Y3iy9soFSA9zoYbpyhUFu3eAH1sDWyERxH2yJVZUhPUX5QsxD6bZfMWRKzxw28ohD5n6AZWmvZbDpZzgxSVxUnMevqzTXQk"),
+ Path: "m",
+ },
+ muunPublicKey: &KeyDescriptor{
+ Key: decodeKey("tpubD6NzVbkrYhZ4XbhomyY2axxKe3KB1FK2Wq2z7XYyDF3T4QCuEDZFBUyGfjfHChvEbsbP9RpaYA8cwxkZpQjEcNdaPfuj3cKGqCiHC5YeRTo"),
+ Path: "m",
+ },
+ originalExpirationInBlocks: 10,
+ network: &chaincfg.RegressionNetParams,
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := tt.args.swap.validateV2(
+ tt.args.rawInvoice,
+ tt.args.userPublicKey,
+ tt.args.muunPublicKey,
+ tt.args.originalExpirationInBlocks,
+ tt.args.network,
+ )
+ if (err != nil) != tt.wantErr {
+ t.Errorf("validateV2() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/muun/libwallet/walletdb/walletdb.go b/libwallet/walletdb/walletdb.go
similarity index 100%
rename from vendor/github.com/muun/libwallet/walletdb/walletdb.go
rename to libwallet/walletdb/walletdb.go
diff --git a/libwallet/walletdb/walletdb_test.go b/libwallet/walletdb/walletdb_test.go
new file mode 100644
index 0000000..2028330
--- /dev/null
+++ b/libwallet/walletdb/walletdb_test.go
@@ -0,0 +1,95 @@
+package walletdb
+
+import (
+ "bytes"
+ "crypto/rand"
+ "io/ioutil"
+ "math"
+ "path"
+ "testing"
+)
+
+func TestOpen(t *testing.T) {
+ dir, err := ioutil.TempDir("", "libwallet")
+ if err != nil {
+ panic(err)
+ }
+
+ db, err := Open(path.Join(dir, "test.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Close()
+}
+
+func TestInvoices(t *testing.T) {
+ dir, err := ioutil.TempDir("", "libwallet")
+ if err != nil {
+ panic(err)
+ }
+
+ db, err := Open(path.Join(dir, "test.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Close()
+
+ shortChanId := uint64((math.MaxInt64 - 5) | (1 << 63))
+ paymentHash := randomBytes(32)
+
+ err = db.CreateInvoice(&Invoice{
+ Preimage: randomBytes(32),
+ PaymentHash: paymentHash,
+ PaymentSecret: randomBytes(32),
+ KeyPath: "34/56",
+ ShortChanId: shortChanId,
+ State: InvoiceStateRegistered,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ count, err := db.CountUnusedInvoices()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if count != 1 {
+ t.Fatalf("expected to find 1 unused invoice, got %d", count)
+ }
+
+ inv, err := db.FindByPaymentHash(paymentHash)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(inv.PaymentHash, paymentHash) {
+ t.Fatal("expected invoice payment hash does not match")
+ }
+
+ inv, err = db.FindFirstUnusedInvoice()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(inv.PaymentHash, paymentHash) {
+ t.Fatal("expected invoice payment hash does not match")
+ }
+ if inv.ShortChanId != shortChanId {
+ t.Fatal("expected invoice short channel id does not match")
+ }
+
+ err = db.SaveInvoice(inv)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if inv.ShortChanId != shortChanId {
+ t.Fatal("expected invoice short channel id does not match")
+ }
+}
+
+func randomBytes(count int) []byte {
+ buf := make([]byte, count)
+ _, err := rand.Read(buf)
+ if err != nil {
+ panic(err)
+ }
+ return buf
+}
diff --git a/.gitignore b/recovery_tool/.gitignore
similarity index 100%
rename from .gitignore
rename to recovery_tool/.gitignore
diff --git a/Dockerfile b/recovery_tool/Dockerfile
similarity index 61%
rename from Dockerfile
rename to recovery_tool/Dockerfile
index 7112a5b..af18258 100644
--- a/Dockerfile
+++ b/recovery_tool/Dockerfile
@@ -14,7 +14,13 @@
# --------------------------------------------------------------------------------------------------
-FROM ubuntu:22.04 AS rtool-build-base
+# We have to different base images: one for arm and one for amd.
+# Turns out that debian gcc packages assume the current is always the "gcc" package, but
+# for other archs it's "gcc-multilib-${arch}-linux-gnu". This makes it impossible to build
+# one command that downloads all the archs we want for both amd64 and arm64 runners.
+# The solution is to have one base layer per arch.
+
+FROM golang:1.22.6-bookworm AS rtool-build-base-arm64
# Avoid prompts during package installation:
ENV DEBIAN_FRONTEND="noninteractive"
@@ -24,10 +30,25 @@ RUN apt-get update
# Install the various compilers we're going to use, with specific versions:
RUN apt-get install -y \
- golang-1.18-go=1.18.1-1ubuntu1 \
- gcc-mingw-w64=10.3.0-14ubuntu1+24.3 \
- gcc-multilib=4:11.2.0-1ubuntu1 \
- git=1:2.34.1-1ubuntu1
+ gcc-mingw-w64 \
+ gcc-12-multilib-i686-linux-gnu \
+ gcc-12-multilib-x86-64-linux-gnu
+
+FROM golang:1.22.6-bookworm AS rtool-build-base-amd64
+
+# Avoid prompts during package installation:
+ENV DEBIAN_FRONTEND="noninteractive"
+
+# Upgrade indices:
+RUN apt-get update
+
+# Install the various compilers we're going to use, with specific versions:
+RUN apt-get install -y \
+ gcc-mingw-w64 \
+ gcc-12-multilib-i686-linux-gnu \
+ gcc-12-aarch64-linux-gnu
+
+FROM rtool-build-base-${TARGETARCH} AS rtool-build-base
# Copy the source code into the container:
WORKDIR /src
@@ -47,7 +68,7 @@ ENV CGO_ENABLED=1
ENV GO386=softfloat
# Do the thing:
-RUN env GOOS=${os} GOARCH=${arch} CC=${cc} /usr/lib/go-1.18/bin/go build -mod=vendor -a -trimpath -o /out .
+RUN env GOOS=${os} GOARCH=${arch} CC=${cc} go build -mod=vendor -a -trimpath -o /out ./recovery_tool/
# --------------------------------------------------------------------------------------------------
diff --git a/vendor/github.com/muun/libwallet/LICENSE b/recovery_tool/LICENSE
similarity index 100%
rename from vendor/github.com/muun/libwallet/LICENSE
rename to recovery_tool/LICENSE
diff --git a/Makefile b/recovery_tool/Makefile
similarity index 73%
rename from Makefile
rename to recovery_tool/Makefile
index 9417378..5827fee 100644
--- a/Makefile
+++ b/recovery_tool/Makefile
@@ -3,7 +3,7 @@ build:
mkdir -p bin
echo "Building recovery tool"
- go build -a -trimpath -o "bin/recovery-tool"
+ go build -mod=vendor -a -trimpath -o "bin/recovery-tool"
echo "Success! Built to bin/recovery-tool"
@@ -11,26 +11,37 @@ build:
build-checksum-all: export DOCKER_BUILDKIT=1
build-checksum-all:
# Get vendor dependencies:
- go mod vendor -v
+ go work vendor -v
# Linux 32-bit:
- docker build . -o bin \
+ docker build -f Dockerfile .. -o bin \
--build-arg os=linux \
--build-arg arch=386 \
+ --build-arg cc=i686-linux-gnu-gcc-12 \
--build-arg out=recovery-tool-linux32
/bin/echo -n '✓ Linux 32-bit ' && sha256sum "bin/recovery-tool-linux32"
# Linux 64-bit:
- docker build . -o bin \
+ docker build -f Dockerfile .. -o bin \
--build-arg os=linux \
--build-arg arch=amd64 \
+ --build-arg cc=x86_64-linux-gnu-gcc-12 \
--build-arg out=recovery-tool-linux64
/bin/echo -n '✓ Linux 64-bit ' && sha256sum "bin/recovery-tool-linux64"
+ # Linux arm64:
+ docker build -f Dockerfile .. -o bin \
+ --build-arg os=linux \
+ --build-arg arch=arm64 \
+ --build-arg cc=aarch64-linux-gnu-gcc-12 \
+ --build-arg out=recovery-tool-linuxaarch64
+
+ /bin/echo -n '✓ Linux arm64' && sha256sum "bin/recovery-tool-linuxaarch64"
+
# Windows 32-bit:
- docker build . -o bin \
+ docker build -f Dockerfile .. -o bin \
--build-arg os=windows \
--build-arg arch=386 \
--build-arg cc=i686-w64-mingw32-gcc \
@@ -39,7 +50,7 @@ build-checksum-all:
/bin/echo -n '✓ Windows 32-bit ' && sha256sum "bin/recovery-tool-windows32.exe"
# Windows 64-bit:
- docker build . -o bin \
+ docker build -f Dockerfile .. -o bin \
--build-arg os=windows \
--build-arg arch=amd64 \
--build-arg cc=x86_64-w64-mingw32-gcc \
diff --git a/address_generator.go b/recovery_tool/address_generator.go
similarity index 100%
rename from address_generator.go
rename to recovery_tool/address_generator.go
diff --git a/cmd/survey/main.go b/recovery_tool/cmd/survey/main.go
similarity index 100%
rename from cmd/survey/main.go
rename to recovery_tool/cmd/survey/main.go
diff --git a/electrum/client.go b/recovery_tool/electrum/client.go
similarity index 100%
rename from electrum/client.go
rename to recovery_tool/electrum/client.go
diff --git a/electrum/pool.go b/recovery_tool/electrum/pool.go
similarity index 100%
rename from electrum/pool.go
rename to recovery_tool/electrum/pool.go
diff --git a/electrum/servers.go b/recovery_tool/electrum/servers.go
similarity index 100%
rename from electrum/servers.go
rename to recovery_tool/electrum/servers.go
diff --git a/go.mod b/recovery_tool/go.mod
similarity index 85%
rename from go.mod
rename to recovery_tool/go.mod
index 8d191f5..af3bf7c 100644
--- a/go.mod
+++ b/recovery_tool/go.mod
@@ -9,4 +9,6 @@ require (
github.com/muun/libwallet v0.11.0
)
+replace github.com/muun/libwallet => ../libwallet
+
replace github.com/lightninglabs/neutrino => github.com/muun/neutrino v0.0.0-20190914162326-7082af0fa257
diff --git a/go.sum b/recovery_tool/go.sum
similarity index 100%
rename from go.sum
rename to recovery_tool/go.sum
diff --git a/keys_generator.go b/recovery_tool/keys_generator.go
similarity index 100%
rename from keys_generator.go
rename to recovery_tool/keys_generator.go
diff --git a/main.go b/recovery_tool/main.go
similarity index 100%
rename from main.go
rename to recovery_tool/main.go
diff --git a/readme/demo.gif b/recovery_tool/readme/demo.gif
similarity index 100%
rename from readme/demo.gif
rename to recovery_tool/readme/demo.gif
diff --git a/readme/demo.yml b/recovery_tool/readme/demo.yml
similarity index 100%
rename from readme/demo.yml
rename to recovery_tool/readme/demo.yml
diff --git a/recovery-tool b/recovery_tool/recovery-tool
similarity index 100%
rename from recovery-tool
rename to recovery_tool/recovery-tool
diff --git a/recovery_tool.go b/recovery_tool/recovery_tool.go
similarity index 100%
rename from recovery_tool.go
rename to recovery_tool/recovery_tool.go
diff --git a/scanner/scanner.go b/recovery_tool/scanner/scanner.go
similarity index 100%
rename from scanner/scanner.go
rename to recovery_tool/scanner/scanner.go
diff --git a/scanner/task.go b/recovery_tool/scanner/task.go
similarity index 100%
rename from scanner/task.go
rename to recovery_tool/scanner/task.go
diff --git a/survey/survey.go b/recovery_tool/survey/survey.go
similarity index 100%
rename from survey/survey.go
rename to recovery_tool/survey/survey.go
diff --git a/sweeper.go b/recovery_tool/sweeper.go
similarity index 100%
rename from sweeper.go
rename to recovery_tool/sweeper.go
diff --git a/utils/logger.go b/recovery_tool/utils/logger.go
similarity index 100%
rename from utils/logger.go
rename to recovery_tool/utils/logger.go
diff --git a/version.go b/recovery_tool/version.go
similarity index 100%
rename from version.go
rename to recovery_tool/version.go
diff --git a/vendor/github.com/btcsuite/btcutil/go.mod b/vendor/github.com/btcsuite/btcutil/go.mod
deleted file mode 100644
index d455377..0000000
--- a/vendor/github.com/btcsuite/btcutil/go.mod
+++ /dev/null
@@ -1,11 +0,0 @@
-module github.com/btcsuite/btcutil
-
-go 1.13
-
-require (
- github.com/aead/siphash v1.0.1
- github.com/btcsuite/btcd v0.20.1-beta
- github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495
- github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23
- golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d
-)
diff --git a/vendor/github.com/btcsuite/btcutil/go.sum b/vendor/github.com/btcsuite/btcutil/go.sum
deleted file mode 100644
index 088c1a3..0000000
--- a/vendor/github.com/btcsuite/btcutil/go.sum
+++ /dev/null
@@ -1,54 +0,0 @@
-github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
-github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
-github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw=
-github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd h1:qdGvebPBDuYDPGi1WCPjy1tGyMpmDK8IEapSsszn7HE=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723 h1:ZA/jbKoGcVAnER6pCHPEkGdZOV7U1oLUedErBHCUMs0=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
-github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495 h1:6IyqGr3fnd0tM3YxipK27TUskaOVUjU2nG45yzwcQKY=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI=
-golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/btcsuite/btcutil/psbt/go.mod b/vendor/github.com/btcsuite/btcutil/psbt/go.mod
deleted file mode 100644
index d47c286..0000000
--- a/vendor/github.com/btcsuite/btcutil/psbt/go.mod
+++ /dev/null
@@ -1,9 +0,0 @@
-module github.com/btcsuite/btcutil/psbt
-
-go 1.13
-
-require (
- github.com/btcsuite/btcd v0.20.1-beta
- github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
- github.com/davecgh/go-spew v1.1.1
-)
diff --git a/vendor/github.com/btcsuite/btcutil/psbt/go.sum b/vendor/github.com/btcsuite/btcutil/psbt/go.sum
deleted file mode 100644
index 4267a6f..0000000
--- a/vendor/github.com/btcsuite/btcutil/psbt/go.sum
+++ /dev/null
@@ -1,36 +0,0 @@
-github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
-github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw=
-github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/btcutil v0.0.0-20191219182022-e17c9730c422 h1:EqnrgSSg0SFWRlEZLExgjtuUR/IPnuQ6qw6nwRda4Uk=
-github.com/btcsuite/btcutil v0.0.0-20191219182022-e17c9730c422/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
-github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/btcsuite/btcwallet/wallet/txauthor/go.mod b/vendor/github.com/btcsuite/btcwallet/wallet/txauthor/go.mod
deleted file mode 100644
index f153014..0000000
--- a/vendor/github.com/btcsuite/btcwallet/wallet/txauthor/go.mod
+++ /dev/null
@@ -1,14 +0,0 @@
-module github.com/btcsuite/btcwallet/wallet/txauthor
-
-go 1.12
-
-require (
- github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3
- github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
- github.com/btcsuite/btcwallet/wallet/txrules v1.0.0
- github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0
-)
-
-replace github.com/btcsuite/btcwallet/wallet/txrules => ../txrules
-
-replace github.com/btcsuite/btcwallet/wallet/txsizes => ../txsizes
diff --git a/vendor/github.com/btcsuite/btcwallet/wallet/txauthor/go.sum b/vendor/github.com/btcsuite/btcwallet/wallet/txauthor/go.sum
deleted file mode 100644
index a96257f..0000000
--- a/vendor/github.com/btcsuite/btcwallet/wallet/txauthor/go.sum
+++ /dev/null
@@ -1,66 +0,0 @@
-github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
-github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3 h1:A/EVblehb75cUgXA5njHPn0kLAsykn6mJGz7rnmW5W0=
-github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
-github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4=
-github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE=
-github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
-github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
-golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80=
-golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/btcsuite/btcwallet/wallet/txrules/go.mod b/vendor/github.com/btcsuite/btcwallet/wallet/txrules/go.mod
deleted file mode 100644
index d50f7fd..0000000
--- a/vendor/github.com/btcsuite/btcwallet/wallet/txrules/go.mod
+++ /dev/null
@@ -1,8 +0,0 @@
-module github.com/btcsuite/btcwallet/wallet/txrules
-
-go 1.12
-
-require (
- github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3
- github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
-)
diff --git a/vendor/github.com/btcsuite/btcwallet/wallet/txrules/go.sum b/vendor/github.com/btcsuite/btcwallet/wallet/txrules/go.sum
deleted file mode 100644
index a12f032..0000000
--- a/vendor/github.com/btcsuite/btcwallet/wallet/txrules/go.sum
+++ /dev/null
@@ -1,33 +0,0 @@
-github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
-github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3 h1:A/EVblehb75cUgXA5njHPn0kLAsykn6mJGz7rnmW5W0=
-github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
-github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495 h1:6IyqGr3fnd0tM3YxipK27TUskaOVUjU2nG45yzwcQKY=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/btcsuite/btcwallet/wallet/txsizes/go.mod b/vendor/github.com/btcsuite/btcwallet/wallet/txsizes/go.mod
deleted file mode 100644
index 1a2893b..0000000
--- a/vendor/github.com/btcsuite/btcwallet/wallet/txsizes/go.mod
+++ /dev/null
@@ -1,17 +0,0 @@
-module github.com/btcsuite/btcwallet/wallet/txsizes
-
-go 1.12
-
-require (
- github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3
- github.com/btcsuite/goleveldb v1.0.0 // indirect
- github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/kr/pretty v0.1.0 // indirect
- golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 // indirect
- golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 // indirect
- golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
- golang.org/x/sys v0.0.0-20190904154756-749cb33beabd // indirect
- golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 // indirect
- gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
- gopkg.in/yaml.v2 v2.2.2 // indirect
-)
diff --git a/vendor/github.com/btcsuite/btcwallet/wallet/txsizes/go.sum b/vendor/github.com/btcsuite/btcwallet/wallet/txsizes/go.sum
deleted file mode 100644
index 08cf615..0000000
--- a/vendor/github.com/btcsuite/btcwallet/wallet/txsizes/go.sum
+++ /dev/null
@@ -1,65 +0,0 @@
-github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
-github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3 h1:A/EVblehb75cUgXA5njHPn0kLAsykn6mJGz7rnmW5W0=
-github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
-github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4=
-github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE=
-github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
-github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
-golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80=
-golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/btcsuite/btcwallet/walletdb/go.mod b/vendor/github.com/btcsuite/btcwallet/walletdb/go.mod
deleted file mode 100644
index f5ca20b..0000000
--- a/vendor/github.com/btcsuite/btcwallet/walletdb/go.mod
+++ /dev/null
@@ -1,9 +0,0 @@
-module github.com/btcsuite/btcwallet/walletdb
-
-go 1.12
-
-require (
- github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
- github.com/davecgh/go-spew v1.1.1
- go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50
-)
diff --git a/vendor/github.com/btcsuite/btcwallet/walletdb/go.sum b/vendor/github.com/btcsuite/btcwallet/walletdb/go.sum
deleted file mode 100644
index 28549da..0000000
--- a/vendor/github.com/btcsuite/btcwallet/walletdb/go.sum
+++ /dev/null
@@ -1,8 +0,0 @@
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50 h1:ASw9n1EHMftwnP3Az4XW6e308+gNsrHzmdhd0Olz9Hs=
-go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/vendor/github.com/btcsuite/btcwallet/wtxmgr/go.mod b/vendor/github.com/btcsuite/btcwallet/wtxmgr/go.mod
deleted file mode 100644
index 4ea06c5..0000000
--- a/vendor/github.com/btcsuite/btcwallet/wtxmgr/go.mod
+++ /dev/null
@@ -1,12 +0,0 @@
-module github.com/btcsuite/btcwallet/wtxmgr
-
-go 1.12
-
-require (
- github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3
- github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
- github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
- github.com/btcsuite/btcwallet/walletdb v1.3.2
- github.com/lightningnetwork/lnd/clock v1.0.1
- github.com/stretchr/testify v1.5.1 // indirect
-)
diff --git a/vendor/github.com/btcsuite/btcwallet/wtxmgr/go.sum b/vendor/github.com/btcsuite/btcwallet/wtxmgr/go.sum
deleted file mode 100644
index 551a11d..0000000
--- a/vendor/github.com/btcsuite/btcwallet/wtxmgr/go.sum
+++ /dev/null
@@ -1,66 +0,0 @@
-github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
-github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3 h1:A/EVblehb75cUgXA5njHPn0kLAsykn6mJGz7rnmW5W0=
-github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/btcwallet/walletdb v1.3.2 h1:nFnMBVgkqoVOx08Z756oDwNc9sdVgYR52T1ONSXs90w=
-github.com/btcsuite/btcwallet/walletdb v1.3.2/go.mod h1:GZCMPNpUu5KE3ASoVd+k06p/1OW8OwNGCCaNWRto2cQ=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd h1:qdGvebPBDuYDPGi1WCPjy1tGyMpmDK8IEapSsszn7HE=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723 h1:ZA/jbKoGcVAnER6pCHPEkGdZOV7U1oLUedErBHCUMs0=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
-github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
-github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
-github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
-github.com/lightningnetwork/lnd/clock v1.0.1 h1:QQod8+m3KgqHdvVMV+2DRNNZS1GRFir8mHZYA+Z2hFo=
-github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50 h1:ASw9n1EHMftwnP3Az4XW6e308+gNsrHzmdhd0Olz9Hs=
-go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/decred/dcrd/lru/go.mod b/vendor/github.com/decred/dcrd/lru/go.mod
deleted file mode 100644
index 6a13da4..0000000
--- a/vendor/github.com/decred/dcrd/lru/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module github.com/decred/dcrd/lru
-
-go 1.11
diff --git a/vendor/github.com/fiatjaf/go-lnurl/go.mod b/vendor/github.com/fiatjaf/go-lnurl/go.mod
deleted file mode 100644
index 20227ae..0000000
--- a/vendor/github.com/fiatjaf/go-lnurl/go.mod
+++ /dev/null
@@ -1,8 +0,0 @@
-module github.com/fiatjaf/go-lnurl
-
-go 1.14
-
-require (
- github.com/btcsuite/btcd v0.20.1-beta
- github.com/tidwall/gjson v1.6.0
-)
diff --git a/vendor/github.com/fiatjaf/go-lnurl/go.sum b/vendor/github.com/fiatjaf/go-lnurl/go.sum
deleted file mode 100644
index 406459f..0000000
--- a/vendor/github.com/fiatjaf/go-lnurl/go.sum
+++ /dev/null
@@ -1,35 +0,0 @@
-github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
-github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw=
-github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
-github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
-github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
-github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
-github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
-github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
-github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/gookit/color/go.mod b/vendor/github.com/gookit/color/go.mod
deleted file mode 100644
index cd94efc..0000000
--- a/vendor/github.com/gookit/color/go.mod
+++ /dev/null
@@ -1,9 +0,0 @@
-module github.com/gookit/color
-
-go 1.12
-
-require (
- github.com/stretchr/testify v1.6.1
- github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
- golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44
-)
diff --git a/vendor/github.com/gookit/color/go.sum b/vendor/github.com/gookit/color/go.sum
deleted file mode 100644
index 2d67cba..0000000
--- a/vendor/github.com/gookit/color/go.sum
+++ /dev/null
@@ -1,15 +0,0 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
-github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/vendor/github.com/hhrutter/lzw/go.mod b/vendor/github.com/hhrutter/lzw/go.mod
deleted file mode 100644
index eb3ac3c..0000000
--- a/vendor/github.com/hhrutter/lzw/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module github.com/hhrutter/lzw
-
-go 1.12
diff --git a/vendor/github.com/hhrutter/tiff/go.mod b/vendor/github.com/hhrutter/tiff/go.mod
deleted file mode 100644
index a2367a4..0000000
--- a/vendor/github.com/hhrutter/tiff/go.mod
+++ /dev/null
@@ -1,8 +0,0 @@
-module github.com/hhrutter/tiff
-
-go 1.12
-
-require (
- github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc
- golang.org/x/image v0.0.0-20190823064033-3a9bac650e44
-)
diff --git a/vendor/github.com/hhrutter/tiff/go.sum b/vendor/github.com/hhrutter/tiff/go.sum
deleted file mode 100644
index 33a5b7f..0000000
--- a/vendor/github.com/hhrutter/tiff/go.sum
+++ /dev/null
@@ -1,6 +0,0 @@
-github.com/hhrutter/lzw v0.0.0-20190826233241-e4e67a6cc9b8 h1:U1DNFAgO5OSS70hFTvB7PN/Ex0mhqC7cZZ4FUaNJ8F0=
-github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc h1:crd+cScoxEqSOqClzjkNMNQNdMCF3SGXhPdDWBQfNZE=
-github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
-golang.org/x/image v0.0.0-20190823064033-3a9bac650e44 h1:1/e6LjNi7iqpDTz8tCLSKoR5dqrX4C3ub4H31JJZM4U=
-golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/vendor/github.com/jinzhu/gorm/go.mod b/vendor/github.com/jinzhu/gorm/go.mod
deleted file mode 100644
index 64a42a4..0000000
--- a/vendor/github.com/jinzhu/gorm/go.mod
+++ /dev/null
@@ -1,14 +0,0 @@
-module github.com/jinzhu/gorm
-
-go 1.12
-
-require (
- github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd
- github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5
- github.com/go-sql-driver/mysql v1.5.0
- github.com/jinzhu/inflection v1.0.0
- github.com/jinzhu/now v1.0.1
- github.com/lib/pq v1.1.1
- github.com/mattn/go-sqlite3 v1.14.0
- golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd // indirect
-)
diff --git a/vendor/github.com/jinzhu/gorm/go.sum b/vendor/github.com/jinzhu/gorm/go.sum
deleted file mode 100644
index d30630b..0000000
--- a/vendor/github.com/jinzhu/gorm/go.sum
+++ /dev/null
@@ -1,33 +0,0 @@
-github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
-github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
-github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
-github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
-github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
-github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
-github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
-github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
-github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
-github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
-github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
-github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
-github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
-github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
-github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
-github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
-github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
-golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM=
-golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/vendor/github.com/jinzhu/inflection/go.mod b/vendor/github.com/jinzhu/inflection/go.mod
deleted file mode 100644
index 2fb7690..0000000
--- a/vendor/github.com/jinzhu/inflection/go.mod
+++ /dev/null
@@ -1 +0,0 @@
-module github.com/jinzhu/inflection
diff --git a/vendor/github.com/lightninglabs/gozmq/go.mod b/vendor/github.com/lightninglabs/gozmq/go.mod
deleted file mode 100644
index abd616b..0000000
--- a/vendor/github.com/lightninglabs/gozmq/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module github.com/lightninglabs/gozmq
-
-go 1.12
diff --git a/vendor/github.com/lightninglabs/neutrino/go.mod b/vendor/github.com/lightninglabs/neutrino/go.mod
deleted file mode 100644
index cb9e95a..0000000
--- a/vendor/github.com/lightninglabs/neutrino/go.mod
+++ /dev/null
@@ -1,14 +0,0 @@
-module github.com/lightninglabs/neutrino
-
-require (
- github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3
- github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
- github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
- github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0
- github.com/btcsuite/btcwallet/walletdb v1.0.0
- github.com/btcsuite/btcwallet/wtxmgr v1.0.0
- github.com/davecgh/go-spew v1.1.1
- github.com/lightningnetwork/lnd/queue v1.0.1
-)
-
-go 1.13
diff --git a/vendor/github.com/lightninglabs/neutrino/go.sum b/vendor/github.com/lightninglabs/neutrino/go.sum
deleted file mode 100644
index 75e86a0..0000000
--- a/vendor/github.com/lightninglabs/neutrino/go.sum
+++ /dev/null
@@ -1,94 +0,0 @@
-github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
-github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
-github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3 h1:A/EVblehb75cUgXA5njHPn0kLAsykn6mJGz7rnmW5W0=
-github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 h1:KGHMW5sd7yDdDMkCZ/JpP0KltolFsQcB973brBnfj4c=
-github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU=
-github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 h1:2VsfS0sBedcM5KmDzRMT3+b6xobqWveZGvjb+jFez5w=
-github.com/btcsuite/btcwallet/wallet/txrules v1.0.0/go.mod h1:UwQE78yCerZ313EXZwEiu3jNAtfXj2n2+c8RWiE/WNA=
-github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0 h1:6DxkcoMnCPY4E9cUDPB5tbuuf40SmmMkSQkoE8vCT+s=
-github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs=
-github.com/btcsuite/btcwallet/walletdb v1.0.0 h1:mheT7vCWK5EP6rZzhxsQ7ms9+yX4VE8bwiJctECBeNw=
-github.com/btcsuite/btcwallet/walletdb v1.0.0/go.mod h1:bZTy9RyYZh9fLnSua+/CD48TJtYJSHjjYcSaszuxCCk=
-github.com/btcsuite/btcwallet/wtxmgr v1.0.0 h1:aIHgViEmZmZfe0tQQqF1xyd2qBqFWxX5vZXkkbjtbeA=
-github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd h1:qdGvebPBDuYDPGi1WCPjy1tGyMpmDK8IEapSsszn7HE=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
-github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4=
-github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723 h1:ZA/jbKoGcVAnER6pCHPEkGdZOV7U1oLUedErBHCUMs0=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE=
-github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
-github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
-github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
-github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI=
-github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0=
-github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms=
-github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU=
-github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
-go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
-golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80=
-golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/lightningnetwork/lightning-onion/go.mod b/vendor/github.com/lightningnetwork/lightning-onion/go.mod
deleted file mode 100644
index 4d19c54..0000000
--- a/vendor/github.com/lightningnetwork/lightning-onion/go.mod
+++ /dev/null
@@ -1,13 +0,0 @@
-module github.com/lightningnetwork/lightning-onion
-
-require (
- github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da
- github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8
- github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
- github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
- github.com/davecgh/go-spew v1.1.1
- golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67
- golang.org/x/sys v0.0.0-20190209173611-3b5209105503 // indirect
-)
-
-go 1.13
diff --git a/vendor/github.com/lightningnetwork/lightning-onion/go.sum b/vendor/github.com/lightningnetwork/lightning-onion/go.sum
deleted file mode 100644
index ca83343..0000000
--- a/vendor/github.com/lightningnetwork/lightning-onion/go.sum
+++ /dev/null
@@ -1,42 +0,0 @@
-github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
-github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
-github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
-github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
-github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8 h1:mOg8/RgDSHTQ1R0IR+LMDuW4TDShPv+JzYHuR4GLoNA=
-github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
-github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng=
-github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
-github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
-github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
-github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
-github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
-github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
-github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
-github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
-golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190209173611-3b5209105503 h1:5SvYFrOM3W8Mexn9/oA44Ji7vhXAZQ9hiP+1Q/DMrWg=
-golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/lightningnetwork/lnd/clock/go.mod b/vendor/github.com/lightningnetwork/lnd/clock/go.mod
deleted file mode 100644
index 104130b..0000000
--- a/vendor/github.com/lightningnetwork/lnd/clock/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module github.com/lightningnetwork/lnd/clock
-
-go 1.13
diff --git a/vendor/github.com/lightningnetwork/lnd/queue/go.mod b/vendor/github.com/lightningnetwork/lnd/queue/go.mod
deleted file mode 100644
index a5731bf..0000000
--- a/vendor/github.com/lightningnetwork/lnd/queue/go.mod
+++ /dev/null
@@ -1,7 +0,0 @@
-module github.com/lightningnetwork/lnd/queue
-
-require github.com/lightningnetwork/lnd/ticker v1.0.0
-
-replace github.com/lightningnetwork/lnd/ticker v1.0.0 => ../ticker
-
-go 1.12
diff --git a/vendor/github.com/lightningnetwork/lnd/queue/go.sum b/vendor/github.com/lightningnetwork/lnd/queue/go.sum
deleted file mode 100644
index e69de29..0000000
diff --git a/vendor/github.com/lightningnetwork/lnd/ticker/go.mod b/vendor/github.com/lightningnetwork/lnd/ticker/go.mod
deleted file mode 100644
index fdab508..0000000
--- a/vendor/github.com/lightningnetwork/lnd/ticker/go.mod
+++ /dev/null
@@ -1 +0,0 @@
-module github.com/lightningnetwork/lnd/ticker
diff --git a/vendor/github.com/mattn/go-sqlite3/go.mod b/vendor/github.com/mattn/go-sqlite3/go.mod
deleted file mode 100644
index d3a1571..0000000
--- a/vendor/github.com/mattn/go-sqlite3/go.mod
+++ /dev/null
@@ -1,8 +0,0 @@
-module github.com/mattn/go-sqlite3
-
-go 1.10
-
-require (
- github.com/PuerkitoBio/goquery v1.5.1
- golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
-)
diff --git a/vendor/github.com/mattn/go-sqlite3/go.sum b/vendor/github.com/mattn/go-sqlite3/go.sum
deleted file mode 100644
index d2963a7..0000000
--- a/vendor/github.com/mattn/go-sqlite3/go.sum
+++ /dev/null
@@ -1,13 +0,0 @@
-github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
-github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
-github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
-github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/vendor/github.com/miekg/dns/go.mod b/vendor/github.com/miekg/dns/go.mod
deleted file mode 100644
index 6003d05..0000000
--- a/vendor/github.com/miekg/dns/go.mod
+++ /dev/null
@@ -1,11 +0,0 @@
-module github.com/miekg/dns
-
-go 1.12
-
-require (
- golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
- golang.org/x/net v0.0.0-20190923162816-aa69164e4478
- golang.org/x/sync v0.0.0-20190423024810-112230192c58
- golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe
- golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 // indirect
-)
diff --git a/vendor/github.com/miekg/dns/go.sum b/vendor/github.com/miekg/dns/go.sum
deleted file mode 100644
index 96bda3a..0000000
--- a/vendor/github.com/miekg/dns/go.sum
+++ /dev/null
@@ -1,39 +0,0 @@
-golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 h1:Vk3wNqEZwyGyei9yq5ekj7frek2u7HUfffJ1/opblzc=
-golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 h1:Gv7RPwsi3eZ2Fgewe3CBsuOebPwO27PoXzRpJPsvSSM=
-golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
-golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3 h1:dgd4x4kJt7G4k4m93AYLzM8Ni6h2qLTfh9n9vXJT3/0=
-golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
-golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
-golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 h1:O33LKL7WyJgjN9CvxfTIomjIClbd/Kq86/iipowHQU0=
-golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
-golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
-golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/vendor/github.com/shopspring/decimal/.gitignore b/vendor/github.com/shopspring/decimal/.gitignore
new file mode 100644
index 0000000..8a43ce9
--- /dev/null
+++ b/vendor/github.com/shopspring/decimal/.gitignore
@@ -0,0 +1,6 @@
+.git
+*.swp
+
+# IntelliJ
+.idea/
+*.iml
diff --git a/vendor/github.com/shopspring/decimal/.travis.yml b/vendor/github.com/shopspring/decimal/.travis.yml
new file mode 100644
index 0000000..55d42b2
--- /dev/null
+++ b/vendor/github.com/shopspring/decimal/.travis.yml
@@ -0,0 +1,13 @@
+language: go
+
+go:
+ - 1.7.x
+ - 1.12.x
+ - 1.13.x
+ - tip
+
+install:
+ - go build .
+
+script:
+ - go test -v
diff --git a/vendor/github.com/shopspring/decimal/CHANGELOG.md b/vendor/github.com/shopspring/decimal/CHANGELOG.md
new file mode 100644
index 0000000..01ba02f
--- /dev/null
+++ b/vendor/github.com/shopspring/decimal/CHANGELOG.md
@@ -0,0 +1,19 @@
+## Decimal v1.2.0
+
+#### BREAKING
+- Drop support for Go version older than 1.7 [#172](https://github.com/shopspring/decimal/pull/172)
+
+#### FEATURES
+- Add NewFromInt and NewFromInt32 initializers [#72](https://github.com/shopspring/decimal/pull/72)
+- Add support for Go modules [#157](https://github.com/shopspring/decimal/pull/157)
+- Add BigInt, BigFloat helper methods [#171](https://github.com/shopspring/decimal/pull/171)
+
+#### ENHANCEMENTS
+- Memory usage optimization [#160](https://github.com/shopspring/decimal/pull/160)
+- Updated travis CI golang versions [#156](https://github.com/shopspring/decimal/pull/156)
+- Update documentation [#173](https://github.com/shopspring/decimal/pull/173)
+- Improve code quality [#174](https://github.com/shopspring/decimal/pull/174)
+
+#### BUGFIXES
+- Revert remove insignificant digits [#159](https://github.com/shopspring/decimal/pull/159)
+- Remove 15 interval for RoundCash [#166](https://github.com/shopspring/decimal/pull/166)
diff --git a/vendor/github.com/shopspring/decimal/LICENSE b/vendor/github.com/shopspring/decimal/LICENSE
new file mode 100644
index 0000000..ad2148a
--- /dev/null
+++ b/vendor/github.com/shopspring/decimal/LICENSE
@@ -0,0 +1,45 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Spring, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+- Based on https://github.com/oguzbilgic/fpd, which has the following license:
+"""
+The MIT License (MIT)
+
+Copyright (c) 2013 Oguz Bilgic
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
diff --git a/vendor/github.com/shopspring/decimal/README.md b/vendor/github.com/shopspring/decimal/README.md
new file mode 100644
index 0000000..b70f901
--- /dev/null
+++ b/vendor/github.com/shopspring/decimal/README.md
@@ -0,0 +1,130 @@
+# decimal
+
+[](https://travis-ci.org/shopspring/decimal) [](https://godoc.org/github.com/shopspring/decimal) [](https://goreportcard.com/report/github.com/shopspring/decimal)
+
+Arbitrary-precision fixed-point decimal numbers in go.
+
+_Note:_ Decimal library can "only" represent numbers with a maximum of 2^31 digits after the decimal point.
+
+## Features
+
+ * The zero-value is 0, and is safe to use without initialization
+ * Addition, subtraction, multiplication with no loss of precision
+ * Division with specified precision
+ * Database/sql serialization/deserialization
+ * JSON and XML serialization/deserialization
+
+## Install
+
+Run `go get github.com/shopspring/decimal`
+
+## Requirements
+
+Decimal library requires Go version `>=1.7`
+
+## Usage
+
+```go
+package main
+
+import (
+ "fmt"
+ "github.com/shopspring/decimal"
+)
+
+func main() {
+ price, err := decimal.NewFromString("136.02")
+ if err != nil {
+ panic(err)
+ }
+
+ quantity := decimal.NewFromInt(3)
+
+ fee, _ := decimal.NewFromString(".035")
+ taxRate, _ := decimal.NewFromString(".08875")
+
+ subtotal := price.Mul(quantity)
+
+ preTax := subtotal.Mul(fee.Add(decimal.NewFromFloat(1)))
+
+ total := preTax.Mul(taxRate.Add(decimal.NewFromFloat(1)))
+
+ fmt.Println("Subtotal:", subtotal) // Subtotal: 408.06
+ fmt.Println("Pre-tax:", preTax) // Pre-tax: 422.3421
+ fmt.Println("Taxes:", total.Sub(preTax)) // Taxes: 37.482861375
+ fmt.Println("Total:", total) // Total: 459.824961375
+ fmt.Println("Tax rate:", total.Sub(preTax).Div(preTax)) // Tax rate: 0.08875
+}
+```
+
+## Documentation
+
+http://godoc.org/github.com/shopspring/decimal
+
+## Production Usage
+
+* [Spring](https://shopspring.com/), since August 14, 2014.
+* If you are using this in production, please let us know!
+
+## FAQ
+
+#### Why don't you just use float64?
+
+Because float64 (or any binary floating point type, actually) can't represent
+numbers such as `0.1` exactly.
+
+Consider this code: http://play.golang.org/p/TQBd4yJe6B You might expect that
+it prints out `10`, but it actually prints `9.999999999999831`. Over time,
+these small errors can really add up!
+
+#### Why don't you just use big.Rat?
+
+big.Rat is fine for representing rational numbers, but Decimal is better for
+representing money. Why? Here's a (contrived) example:
+
+Let's say you use big.Rat, and you have two numbers, x and y, both
+representing 1/3, and you have `z = 1 - x - y = 1/3`. If you print each one
+out, the string output has to stop somewhere (let's say it stops at 3 decimal
+digits, for simplicity), so you'll get 0.333, 0.333, and 0.333. But where did
+the other 0.001 go?
+
+Here's the above example as code: http://play.golang.org/p/lCZZs0w9KE
+
+With Decimal, the strings being printed out represent the number exactly. So,
+if you have `x = y = 1/3` (with precision 3), they will actually be equal to
+0.333, and when you do `z = 1 - x - y`, `z` will be equal to .334. No money is
+unaccounted for!
+
+You still have to be careful. If you want to split a number `N` 3 ways, you
+can't just send `N/3` to three different people. You have to pick one to send
+`N - (2/3*N)` to. That person will receive the fraction of a penny remainder.
+
+But, it is much easier to be careful with Decimal than with big.Rat.
+
+#### Why isn't the API similar to big.Int's?
+
+big.Int's API is built to reduce the number of memory allocations for maximal
+performance. This makes sense for its use-case, but the trade-off is that the
+API is awkward and easy to misuse.
+
+For example, to add two big.Ints, you do: `z := new(big.Int).Add(x, y)`. A
+developer unfamiliar with this API might try to do `z := a.Add(a, b)`. This
+modifies `a` and sets `z` as an alias for `a`, which they might not expect. It
+also modifies any other aliases to `a`.
+
+Here's an example of the subtle bugs you can introduce with big.Int's API:
+https://play.golang.org/p/x2R_78pa8r
+
+In contrast, it's difficult to make such mistakes with decimal. Decimals
+behave like other go numbers types: even though `a = b` will not deep copy
+`b` into `a`, it is impossible to modify a Decimal, since all Decimal methods
+return new Decimals and do not modify the originals. The downside is that
+this causes extra allocations, so Decimal is less performant. My assumption
+is that if you're using Decimals, you probably care more about correctness
+than performance.
+
+## License
+
+The MIT License (MIT)
+
+This is a heavily modified fork of [fpd.Decimal](https://github.com/oguzbilgic/fpd), which was also released under the MIT License.
diff --git a/vendor/github.com/shopspring/decimal/decimal-go.go b/vendor/github.com/shopspring/decimal/decimal-go.go
new file mode 100644
index 0000000..9958d69
--- /dev/null
+++ b/vendor/github.com/shopspring/decimal/decimal-go.go
@@ -0,0 +1,415 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Multiprecision decimal numbers.
+// For floating-point formatting only; not general purpose.
+// Only operations are assign and (binary) left/right shift.
+// Can do binary floating point in multiprecision decimal precisely
+// because 2 divides 10; cannot do decimal floating point
+// in multiprecision binary precisely.
+
+package decimal
+
+type decimal struct {
+ d [800]byte // digits, big-endian representation
+ nd int // number of digits used
+ dp int // decimal point
+ neg bool // negative flag
+ trunc bool // discarded nonzero digits beyond d[:nd]
+}
+
+func (a *decimal) String() string {
+ n := 10 + a.nd
+ if a.dp > 0 {
+ n += a.dp
+ }
+ if a.dp < 0 {
+ n += -a.dp
+ }
+
+ buf := make([]byte, n)
+ w := 0
+ switch {
+ case a.nd == 0:
+ return "0"
+
+ case a.dp <= 0:
+ // zeros fill space between decimal point and digits
+ buf[w] = '0'
+ w++
+ buf[w] = '.'
+ w++
+ w += digitZero(buf[w : w+-a.dp])
+ w += copy(buf[w:], a.d[0:a.nd])
+
+ case a.dp < a.nd:
+ // decimal point in middle of digits
+ w += copy(buf[w:], a.d[0:a.dp])
+ buf[w] = '.'
+ w++
+ w += copy(buf[w:], a.d[a.dp:a.nd])
+
+ default:
+ // zeros fill space between digits and decimal point
+ w += copy(buf[w:], a.d[0:a.nd])
+ w += digitZero(buf[w : w+a.dp-a.nd])
+ }
+ return string(buf[0:w])
+}
+
+func digitZero(dst []byte) int {
+ for i := range dst {
+ dst[i] = '0'
+ }
+ return len(dst)
+}
+
+// trim trailing zeros from number.
+// (They are meaningless; the decimal point is tracked
+// independent of the number of digits.)
+func trim(a *decimal) {
+ for a.nd > 0 && a.d[a.nd-1] == '0' {
+ a.nd--
+ }
+ if a.nd == 0 {
+ a.dp = 0
+ }
+}
+
+// Assign v to a.
+func (a *decimal) Assign(v uint64) {
+ var buf [24]byte
+
+ // Write reversed decimal in buf.
+ n := 0
+ for v > 0 {
+ v1 := v / 10
+ v -= 10 * v1
+ buf[n] = byte(v + '0')
+ n++
+ v = v1
+ }
+
+ // Reverse again to produce forward decimal in a.d.
+ a.nd = 0
+ for n--; n >= 0; n-- {
+ a.d[a.nd] = buf[n]
+ a.nd++
+ }
+ a.dp = a.nd
+ trim(a)
+}
+
+// Maximum shift that we can do in one pass without overflow.
+// A uint has 32 or 64 bits, and we have to be able to accommodate 9<> 63)
+const maxShift = uintSize - 4
+
+// Binary shift right (/ 2) by k bits. k <= maxShift to avoid overflow.
+func rightShift(a *decimal, k uint) {
+ r := 0 // read pointer
+ w := 0 // write pointer
+
+ // Pick up enough leading digits to cover first shift.
+ var n uint
+ for ; n>>k == 0; r++ {
+ if r >= a.nd {
+ if n == 0 {
+ // a == 0; shouldn't get here, but handle anyway.
+ a.nd = 0
+ return
+ }
+ for n>>k == 0 {
+ n = n * 10
+ r++
+ }
+ break
+ }
+ c := uint(a.d[r])
+ n = n*10 + c - '0'
+ }
+ a.dp -= r - 1
+
+ var mask uint = (1 << k) - 1
+
+ // Pick up a digit, put down a digit.
+ for ; r < a.nd; r++ {
+ c := uint(a.d[r])
+ dig := n >> k
+ n &= mask
+ a.d[w] = byte(dig + '0')
+ w++
+ n = n*10 + c - '0'
+ }
+
+ // Put down extra digits.
+ for n > 0 {
+ dig := n >> k
+ n &= mask
+ if w < len(a.d) {
+ a.d[w] = byte(dig + '0')
+ w++
+ } else if dig > 0 {
+ a.trunc = true
+ }
+ n = n * 10
+ }
+
+ a.nd = w
+ trim(a)
+}
+
+// Cheat sheet for left shift: table indexed by shift count giving
+// number of new digits that will be introduced by that shift.
+//
+// For example, leftcheats[4] = {2, "625"}. That means that
+// if we are shifting by 4 (multiplying by 16), it will add 2 digits
+// when the string prefix is "625" through "999", and one fewer digit
+// if the string prefix is "000" through "624".
+//
+// Credit for this trick goes to Ken.
+
+type leftCheat struct {
+ delta int // number of new digits
+ cutoff string // minus one digit if original < a.
+}
+
+var leftcheats = []leftCheat{
+ // Leading digits of 1/2^i = 5^i.
+ // 5^23 is not an exact 64-bit floating point number,
+ // so have to use bc for the math.
+ // Go up to 60 to be large enough for 32bit and 64bit platforms.
+ /*
+ seq 60 | sed 's/^/5^/' | bc |
+ awk 'BEGIN{ print "\t{ 0, \"\" }," }
+ {
+ log2 = log(2)/log(10)
+ printf("\t{ %d, \"%s\" },\t// * %d\n",
+ int(log2*NR+1), $0, 2**NR)
+ }'
+ */
+ {0, ""},
+ {1, "5"}, // * 2
+ {1, "25"}, // * 4
+ {1, "125"}, // * 8
+ {2, "625"}, // * 16
+ {2, "3125"}, // * 32
+ {2, "15625"}, // * 64
+ {3, "78125"}, // * 128
+ {3, "390625"}, // * 256
+ {3, "1953125"}, // * 512
+ {4, "9765625"}, // * 1024
+ {4, "48828125"}, // * 2048
+ {4, "244140625"}, // * 4096
+ {4, "1220703125"}, // * 8192
+ {5, "6103515625"}, // * 16384
+ {5, "30517578125"}, // * 32768
+ {5, "152587890625"}, // * 65536
+ {6, "762939453125"}, // * 131072
+ {6, "3814697265625"}, // * 262144
+ {6, "19073486328125"}, // * 524288
+ {7, "95367431640625"}, // * 1048576
+ {7, "476837158203125"}, // * 2097152
+ {7, "2384185791015625"}, // * 4194304
+ {7, "11920928955078125"}, // * 8388608
+ {8, "59604644775390625"}, // * 16777216
+ {8, "298023223876953125"}, // * 33554432
+ {8, "1490116119384765625"}, // * 67108864
+ {9, "7450580596923828125"}, // * 134217728
+ {9, "37252902984619140625"}, // * 268435456
+ {9, "186264514923095703125"}, // * 536870912
+ {10, "931322574615478515625"}, // * 1073741824
+ {10, "4656612873077392578125"}, // * 2147483648
+ {10, "23283064365386962890625"}, // * 4294967296
+ {10, "116415321826934814453125"}, // * 8589934592
+ {11, "582076609134674072265625"}, // * 17179869184
+ {11, "2910383045673370361328125"}, // * 34359738368
+ {11, "14551915228366851806640625"}, // * 68719476736
+ {12, "72759576141834259033203125"}, // * 137438953472
+ {12, "363797880709171295166015625"}, // * 274877906944
+ {12, "1818989403545856475830078125"}, // * 549755813888
+ {13, "9094947017729282379150390625"}, // * 1099511627776
+ {13, "45474735088646411895751953125"}, // * 2199023255552
+ {13, "227373675443232059478759765625"}, // * 4398046511104
+ {13, "1136868377216160297393798828125"}, // * 8796093022208
+ {14, "5684341886080801486968994140625"}, // * 17592186044416
+ {14, "28421709430404007434844970703125"}, // * 35184372088832
+ {14, "142108547152020037174224853515625"}, // * 70368744177664
+ {15, "710542735760100185871124267578125"}, // * 140737488355328
+ {15, "3552713678800500929355621337890625"}, // * 281474976710656
+ {15, "17763568394002504646778106689453125"}, // * 562949953421312
+ {16, "88817841970012523233890533447265625"}, // * 1125899906842624
+ {16, "444089209850062616169452667236328125"}, // * 2251799813685248
+ {16, "2220446049250313080847263336181640625"}, // * 4503599627370496
+ {16, "11102230246251565404236316680908203125"}, // * 9007199254740992
+ {17, "55511151231257827021181583404541015625"}, // * 18014398509481984
+ {17, "277555756156289135105907917022705078125"}, // * 36028797018963968
+ {17, "1387778780781445675529539585113525390625"}, // * 72057594037927936
+ {18, "6938893903907228377647697925567626953125"}, // * 144115188075855872
+ {18, "34694469519536141888238489627838134765625"}, // * 288230376151711744
+ {18, "173472347597680709441192448139190673828125"}, // * 576460752303423488
+ {19, "867361737988403547205962240695953369140625"}, // * 1152921504606846976
+}
+
+// Is the leading prefix of b lexicographically less than s?
+func prefixIsLessThan(b []byte, s string) bool {
+ for i := 0; i < len(s); i++ {
+ if i >= len(b) {
+ return true
+ }
+ if b[i] != s[i] {
+ return b[i] < s[i]
+ }
+ }
+ return false
+}
+
+// Binary shift left (* 2) by k bits. k <= maxShift to avoid overflow.
+func leftShift(a *decimal, k uint) {
+ delta := leftcheats[k].delta
+ if prefixIsLessThan(a.d[0:a.nd], leftcheats[k].cutoff) {
+ delta--
+ }
+
+ r := a.nd // read index
+ w := a.nd + delta // write index
+
+ // Pick up a digit, put down a digit.
+ var n uint
+ for r--; r >= 0; r-- {
+ n += (uint(a.d[r]) - '0') << k
+ quo := n / 10
+ rem := n - 10*quo
+ w--
+ if w < len(a.d) {
+ a.d[w] = byte(rem + '0')
+ } else if rem != 0 {
+ a.trunc = true
+ }
+ n = quo
+ }
+
+ // Put down extra digits.
+ for n > 0 {
+ quo := n / 10
+ rem := n - 10*quo
+ w--
+ if w < len(a.d) {
+ a.d[w] = byte(rem + '0')
+ } else if rem != 0 {
+ a.trunc = true
+ }
+ n = quo
+ }
+
+ a.nd += delta
+ if a.nd >= len(a.d) {
+ a.nd = len(a.d)
+ }
+ a.dp += delta
+ trim(a)
+}
+
+// Binary shift left (k > 0) or right (k < 0).
+func (a *decimal) Shift(k int) {
+ switch {
+ case a.nd == 0:
+ // nothing to do: a == 0
+ case k > 0:
+ for k > maxShift {
+ leftShift(a, maxShift)
+ k -= maxShift
+ }
+ leftShift(a, uint(k))
+ case k < 0:
+ for k < -maxShift {
+ rightShift(a, maxShift)
+ k += maxShift
+ }
+ rightShift(a, uint(-k))
+ }
+}
+
+// If we chop a at nd digits, should we round up?
+func shouldRoundUp(a *decimal, nd int) bool {
+ if nd < 0 || nd >= a.nd {
+ return false
+ }
+ if a.d[nd] == '5' && nd+1 == a.nd { // exactly halfway - round to even
+ // if we truncated, a little higher than what's recorded - always round up
+ if a.trunc {
+ return true
+ }
+ return nd > 0 && (a.d[nd-1]-'0')%2 != 0
+ }
+ // not halfway - digit tells all
+ return a.d[nd] >= '5'
+}
+
+// Round a to nd digits (or fewer).
+// If nd is zero, it means we're rounding
+// just to the left of the digits, as in
+// 0.09 -> 0.1.
+func (a *decimal) Round(nd int) {
+ if nd < 0 || nd >= a.nd {
+ return
+ }
+ if shouldRoundUp(a, nd) {
+ a.RoundUp(nd)
+ } else {
+ a.RoundDown(nd)
+ }
+}
+
+// Round a down to nd digits (or fewer).
+func (a *decimal) RoundDown(nd int) {
+ if nd < 0 || nd >= a.nd {
+ return
+ }
+ a.nd = nd
+ trim(a)
+}
+
+// Round a up to nd digits (or fewer).
+func (a *decimal) RoundUp(nd int) {
+ if nd < 0 || nd >= a.nd {
+ return
+ }
+
+ // round up
+ for i := nd - 1; i >= 0; i-- {
+ c := a.d[i]
+ if c < '9' { // can stop after this digit
+ a.d[i]++
+ a.nd = i + 1
+ return
+ }
+ }
+
+ // Number is all 9s.
+ // Change to single 1 with adjusted decimal point.
+ a.d[0] = '1'
+ a.nd = 1
+ a.dp++
+}
+
+// Extract integer part, rounded appropriately.
+// No guarantees about overflow.
+func (a *decimal) RoundedInteger() uint64 {
+ if a.dp > 20 {
+ return 0xFFFFFFFFFFFFFFFF
+ }
+ var i int
+ n := uint64(0)
+ for i = 0; i < a.dp && i < a.nd; i++ {
+ n = n*10 + uint64(a.d[i]-'0')
+ }
+ for ; i < a.dp; i++ {
+ n *= 10
+ }
+ if shouldRoundUp(a, a.dp) {
+ n++
+ }
+ return n
+}
diff --git a/vendor/github.com/shopspring/decimal/decimal.go b/vendor/github.com/shopspring/decimal/decimal.go
new file mode 100644
index 0000000..801c1a0
--- /dev/null
+++ b/vendor/github.com/shopspring/decimal/decimal.go
@@ -0,0 +1,1477 @@
+// Package decimal implements an arbitrary precision fixed-point decimal.
+//
+// The zero-value of a Decimal is 0, as you would expect.
+//
+// The best way to create a new Decimal is to use decimal.NewFromString, ex:
+//
+// n, err := decimal.NewFromString("-123.4567")
+// n.String() // output: "-123.4567"
+//
+// To use Decimal as part of a struct:
+//
+// type Struct struct {
+// Number Decimal
+// }
+//
+// Note: This can "only" represent numbers with a maximum of 2^31 digits after the decimal point.
+package decimal
+
+import (
+ "database/sql/driver"
+ "encoding/binary"
+ "fmt"
+ "math"
+ "math/big"
+ "strconv"
+ "strings"
+)
+
+// DivisionPrecision is the number of decimal places in the result when it
+// doesn't divide exactly.
+//
+// Example:
+//
+// d1 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(3))
+// d1.String() // output: "0.6666666666666667"
+// d2 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(30000))
+// d2.String() // output: "0.0000666666666667"
+// d3 := decimal.NewFromFloat(20000).Div(decimal.NewFromFloat(3))
+// d3.String() // output: "6666.6666666666666667"
+// decimal.DivisionPrecision = 3
+// d4 := decimal.NewFromFloat(2).Div(decimal.NewFromFloat(3))
+// d4.String() // output: "0.667"
+//
+var DivisionPrecision = 16
+
+// MarshalJSONWithoutQuotes should be set to true if you want the decimal to
+// be JSON marshaled as a number, instead of as a string.
+// WARNING: this is dangerous for decimals with many digits, since many JSON
+// unmarshallers (ex: Javascript's) will unmarshal JSON numbers to IEEE 754
+// double-precision floating point numbers, which means you can potentially
+// silently lose precision.
+var MarshalJSONWithoutQuotes = false
+
+// Zero constant, to make computations faster.
+// Zero should never be compared with == or != directly, please use decimal.Equal or decimal.Cmp instead.
+var Zero = New(0, 1)
+
+var zeroInt = big.NewInt(0)
+var oneInt = big.NewInt(1)
+var twoInt = big.NewInt(2)
+var fourInt = big.NewInt(4)
+var fiveInt = big.NewInt(5)
+var tenInt = big.NewInt(10)
+var twentyInt = big.NewInt(20)
+
+// Decimal represents a fixed-point decimal. It is immutable.
+// number = value * 10 ^ exp
+type Decimal struct {
+ value *big.Int
+
+ // NOTE(vadim): this must be an int32, because we cast it to float64 during
+ // calculations. If exp is 64 bit, we might lose precision.
+ // If we cared about being able to represent every possible decimal, we
+ // could make exp a *big.Int but it would hurt performance and numbers
+ // like that are unrealistic.
+ exp int32
+}
+
+// New returns a new fixed-point decimal, value * 10 ^ exp.
+func New(value int64, exp int32) Decimal {
+ return Decimal{
+ value: big.NewInt(value),
+ exp: exp,
+ }
+}
+
+// NewFromInt converts a int64 to Decimal.
+//
+// Example:
+//
+// NewFromInt(123).String() // output: "123"
+// NewFromInt(-10).String() // output: "-10"
+func NewFromInt(value int64) Decimal {
+ return Decimal{
+ value: big.NewInt(value),
+ exp: 0,
+ }
+}
+
+// NewFromInt32 converts a int32 to Decimal.
+//
+// Example:
+//
+// NewFromInt(123).String() // output: "123"
+// NewFromInt(-10).String() // output: "-10"
+func NewFromInt32(value int32) Decimal {
+ return Decimal{
+ value: big.NewInt(int64(value)),
+ exp: 0,
+ }
+}
+
+// NewFromBigInt returns a new Decimal from a big.Int, value * 10 ^ exp
+func NewFromBigInt(value *big.Int, exp int32) Decimal {
+ return Decimal{
+ value: big.NewInt(0).Set(value),
+ exp: exp,
+ }
+}
+
+// NewFromString returns a new Decimal from a string representation.
+// Trailing zeroes are not trimmed.
+//
+// Example:
+//
+// d, err := NewFromString("-123.45")
+// d2, err := NewFromString(".0001")
+// d3, err := NewFromString("1.47000")
+//
+func NewFromString(value string) (Decimal, error) {
+ originalInput := value
+ var intString string
+ var exp int64
+
+ // Check if number is using scientific notation
+ eIndex := strings.IndexAny(value, "Ee")
+ if eIndex != -1 {
+ expInt, err := strconv.ParseInt(value[eIndex+1:], 10, 32)
+ if err != nil {
+ if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
+ return Decimal{}, fmt.Errorf("can't convert %s to decimal: fractional part too long", value)
+ }
+ return Decimal{}, fmt.Errorf("can't convert %s to decimal: exponent is not numeric", value)
+ }
+ value = value[:eIndex]
+ exp = expInt
+ }
+
+ parts := strings.Split(value, ".")
+ if len(parts) == 1 {
+ // There is no decimal point, we can just parse the original string as
+ // an int
+ intString = value
+ } else if len(parts) == 2 {
+ intString = parts[0] + parts[1]
+ expInt := -len(parts[1])
+ exp += int64(expInt)
+ } else {
+ return Decimal{}, fmt.Errorf("can't convert %s to decimal: too many .s", value)
+ }
+
+ dValue := new(big.Int)
+ _, ok := dValue.SetString(intString, 10)
+ if !ok {
+ return Decimal{}, fmt.Errorf("can't convert %s to decimal", value)
+ }
+
+ if exp < math.MinInt32 || exp > math.MaxInt32 {
+ // NOTE(vadim): I doubt a string could realistically be this long
+ return Decimal{}, fmt.Errorf("can't convert %s to decimal: fractional part too long", originalInput)
+ }
+
+ return Decimal{
+ value: dValue,
+ exp: int32(exp),
+ }, nil
+}
+
+// RequireFromString returns a new Decimal from a string representation
+// or panics if NewFromString would have returned an error.
+//
+// Example:
+//
+// d := RequireFromString("-123.45")
+// d2 := RequireFromString(".0001")
+//
+func RequireFromString(value string) Decimal {
+ dec, err := NewFromString(value)
+ if err != nil {
+ panic(err)
+ }
+ return dec
+}
+
+// NewFromFloat converts a float64 to Decimal.
+//
+// The converted number will contain the number of significant digits that can be
+// represented in a float with reliable roundtrip.
+// This is typically 15 digits, but may be more in some cases.
+// See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more information.
+//
+// For slightly faster conversion, use NewFromFloatWithExponent where you can specify the precision in absolute terms.
+//
+// NOTE: this will panic on NaN, +/-inf
+func NewFromFloat(value float64) Decimal {
+ if value == 0 {
+ return New(0, 0)
+ }
+ return newFromFloat(value, math.Float64bits(value), &float64info)
+}
+
+// NewFromFloat32 converts a float32 to Decimal.
+//
+// The converted number will contain the number of significant digits that can be
+// represented in a float with reliable roundtrip.
+// This is typically 6-8 digits depending on the input.
+// See https://www.exploringbinary.com/decimal-precision-of-binary-floating-point-numbers/ for more information.
+//
+// For slightly faster conversion, use NewFromFloatWithExponent where you can specify the precision in absolute terms.
+//
+// NOTE: this will panic on NaN, +/-inf
+func NewFromFloat32(value float32) Decimal {
+ if value == 0 {
+ return New(0, 0)
+ }
+ // XOR is workaround for https://github.com/golang/go/issues/26285
+ a := math.Float32bits(value) ^ 0x80808080
+ return newFromFloat(float64(value), uint64(a)^0x80808080, &float32info)
+}
+
+func newFromFloat(val float64, bits uint64, flt *floatInfo) Decimal {
+ if math.IsNaN(val) || math.IsInf(val, 0) {
+ panic(fmt.Sprintf("Cannot create a Decimal from %v", val))
+ }
+ exp := int(bits>>flt.mantbits) & (1<>(flt.expbits+flt.mantbits) != 0
+
+ roundShortest(&d, mant, exp, flt)
+ // If less than 19 digits, we can do calculation in an int64.
+ if d.nd < 19 {
+ tmp := int64(0)
+ m := int64(1)
+ for i := d.nd - 1; i >= 0; i-- {
+ tmp += m * int64(d.d[i]-'0')
+ m *= 10
+ }
+ if d.neg {
+ tmp *= -1
+ }
+ return Decimal{value: big.NewInt(tmp), exp: int32(d.dp) - int32(d.nd)}
+ }
+ dValue := new(big.Int)
+ dValue, ok := dValue.SetString(string(d.d[:d.nd]), 10)
+ if ok {
+ return Decimal{value: dValue, exp: int32(d.dp) - int32(d.nd)}
+ }
+
+ return NewFromFloatWithExponent(val, int32(d.dp)-int32(d.nd))
+}
+
+// NewFromFloatWithExponent converts a float64 to Decimal, with an arbitrary
+// number of fractional digits.
+//
+// Example:
+//
+// NewFromFloatWithExponent(123.456, -2).String() // output: "123.46"
+//
+func NewFromFloatWithExponent(value float64, exp int32) Decimal {
+ if math.IsNaN(value) || math.IsInf(value, 0) {
+ panic(fmt.Sprintf("Cannot create a Decimal from %v", value))
+ }
+
+ bits := math.Float64bits(value)
+ mant := bits & (1<<52 - 1)
+ exp2 := int32((bits >> 52) & (1<<11 - 1))
+ sign := bits >> 63
+
+ if exp2 == 0 {
+ // specials
+ if mant == 0 {
+ return Decimal{}
+ }
+ // subnormal
+ exp2++
+ } else {
+ // normal
+ mant |= 1 << 52
+ }
+
+ exp2 -= 1023 + 52
+
+ // normalizing base-2 values
+ for mant&1 == 0 {
+ mant = mant >> 1
+ exp2++
+ }
+
+ // maximum number of fractional base-10 digits to represent 2^N exactly cannot be more than -N if N<0
+ if exp < 0 && exp < exp2 {
+ if exp2 < 0 {
+ exp = exp2
+ } else {
+ exp = 0
+ }
+ }
+
+ // representing 10^M * 2^N as 5^M * 2^(M+N)
+ exp2 -= exp
+
+ temp := big.NewInt(1)
+ dMant := big.NewInt(int64(mant))
+
+ // applying 5^M
+ if exp > 0 {
+ temp = temp.SetInt64(int64(exp))
+ temp = temp.Exp(fiveInt, temp, nil)
+ } else if exp < 0 {
+ temp = temp.SetInt64(-int64(exp))
+ temp = temp.Exp(fiveInt, temp, nil)
+ dMant = dMant.Mul(dMant, temp)
+ temp = temp.SetUint64(1)
+ }
+
+ // applying 2^(M+N)
+ if exp2 > 0 {
+ dMant = dMant.Lsh(dMant, uint(exp2))
+ } else if exp2 < 0 {
+ temp = temp.Lsh(temp, uint(-exp2))
+ }
+
+ // rounding and downscaling
+ if exp > 0 || exp2 < 0 {
+ halfDown := new(big.Int).Rsh(temp, 1)
+ dMant = dMant.Add(dMant, halfDown)
+ dMant = dMant.Quo(dMant, temp)
+ }
+
+ if sign == 1 {
+ dMant = dMant.Neg(dMant)
+ }
+
+ return Decimal{
+ value: dMant,
+ exp: exp,
+ }
+}
+
+// rescale returns a rescaled version of the decimal. Returned
+// decimal may be less precise if the given exponent is bigger
+// than the initial exponent of the Decimal.
+// NOTE: this will truncate, NOT round
+//
+// Example:
+//
+// d := New(12345, -4)
+// d2 := d.rescale(-1)
+// d3 := d2.rescale(-4)
+// println(d1)
+// println(d2)
+// println(d3)
+//
+// Output:
+//
+// 1.2345
+// 1.2
+// 1.2000
+//
+func (d Decimal) rescale(exp int32) Decimal {
+ d.ensureInitialized()
+
+ if d.exp == exp {
+ return Decimal{
+ new(big.Int).Set(d.value),
+ d.exp,
+ }
+ }
+
+ // NOTE(vadim): must convert exps to float64 before - to prevent overflow
+ diff := math.Abs(float64(exp) - float64(d.exp))
+ value := new(big.Int).Set(d.value)
+
+ expScale := new(big.Int).Exp(tenInt, big.NewInt(int64(diff)), nil)
+ if exp > d.exp {
+ value = value.Quo(value, expScale)
+ } else if exp < d.exp {
+ value = value.Mul(value, expScale)
+ }
+
+ return Decimal{
+ value: value,
+ exp: exp,
+ }
+}
+
+// Abs returns the absolute value of the decimal.
+func (d Decimal) Abs() Decimal {
+ d.ensureInitialized()
+ d2Value := new(big.Int).Abs(d.value)
+ return Decimal{
+ value: d2Value,
+ exp: d.exp,
+ }
+}
+
+// Add returns d + d2.
+func (d Decimal) Add(d2 Decimal) Decimal {
+ rd, rd2 := RescalePair(d, d2)
+
+ d3Value := new(big.Int).Add(rd.value, rd2.value)
+ return Decimal{
+ value: d3Value,
+ exp: rd.exp,
+ }
+}
+
+// Sub returns d - d2.
+func (d Decimal) Sub(d2 Decimal) Decimal {
+ rd, rd2 := RescalePair(d, d2)
+
+ d3Value := new(big.Int).Sub(rd.value, rd2.value)
+ return Decimal{
+ value: d3Value,
+ exp: rd.exp,
+ }
+}
+
+// Neg returns -d.
+func (d Decimal) Neg() Decimal {
+ d.ensureInitialized()
+ val := new(big.Int).Neg(d.value)
+ return Decimal{
+ value: val,
+ exp: d.exp,
+ }
+}
+
+// Mul returns d * d2.
+func (d Decimal) Mul(d2 Decimal) Decimal {
+ d.ensureInitialized()
+ d2.ensureInitialized()
+
+ expInt64 := int64(d.exp) + int64(d2.exp)
+ if expInt64 > math.MaxInt32 || expInt64 < math.MinInt32 {
+ // NOTE(vadim): better to panic than give incorrect results, as
+ // Decimals are usually used for money
+ panic(fmt.Sprintf("exponent %v overflows an int32!", expInt64))
+ }
+
+ d3Value := new(big.Int).Mul(d.value, d2.value)
+ return Decimal{
+ value: d3Value,
+ exp: int32(expInt64),
+ }
+}
+
+// Shift shifts the decimal in base 10.
+// It shifts left when shift is positive and right if shift is negative.
+// In simpler terms, the given value for shift is added to the exponent
+// of the decimal.
+func (d Decimal) Shift(shift int32) Decimal {
+ d.ensureInitialized()
+ return Decimal{
+ value: new(big.Int).Set(d.value),
+ exp: d.exp + shift,
+ }
+}
+
+// Div returns d / d2. If it doesn't divide exactly, the result will have
+// DivisionPrecision digits after the decimal point.
+func (d Decimal) Div(d2 Decimal) Decimal {
+ return d.DivRound(d2, int32(DivisionPrecision))
+}
+
+// QuoRem does divsion with remainder
+// d.QuoRem(d2,precision) returns quotient q and remainder r such that
+// d = d2 * q + r, q an integer multiple of 10^(-precision)
+// 0 <= r < abs(d2) * 10 ^(-precision) if d>=0
+// 0 >= r > -abs(d2) * 10 ^(-precision) if d<0
+// Note that precision<0 is allowed as input.
+func (d Decimal) QuoRem(d2 Decimal, precision int32) (Decimal, Decimal) {
+ d.ensureInitialized()
+ d2.ensureInitialized()
+ if d2.value.Sign() == 0 {
+ panic("decimal division by 0")
+ }
+ scale := -precision
+ e := int64(d.exp - d2.exp - scale)
+ if e > math.MaxInt32 || e < math.MinInt32 {
+ panic("overflow in decimal QuoRem")
+ }
+ var aa, bb, expo big.Int
+ var scalerest int32
+ // d = a 10^ea
+ // d2 = b 10^eb
+ if e < 0 {
+ aa = *d.value
+ expo.SetInt64(-e)
+ bb.Exp(tenInt, &expo, nil)
+ bb.Mul(d2.value, &bb)
+ scalerest = d.exp
+ // now aa = a
+ // bb = b 10^(scale + eb - ea)
+ } else {
+ expo.SetInt64(e)
+ aa.Exp(tenInt, &expo, nil)
+ aa.Mul(d.value, &aa)
+ bb = *d2.value
+ scalerest = scale + d2.exp
+ // now aa = a ^ (ea - eb - scale)
+ // bb = b
+ }
+ var q, r big.Int
+ q.QuoRem(&aa, &bb, &r)
+ dq := Decimal{value: &q, exp: scale}
+ dr := Decimal{value: &r, exp: scalerest}
+ return dq, dr
+}
+
+// DivRound divides and rounds to a given precision
+// i.e. to an integer multiple of 10^(-precision)
+// for a positive quotient digit 5 is rounded up, away from 0
+// if the quotient is negative then digit 5 is rounded down, away from 0
+// Note that precision<0 is allowed as input.
+func (d Decimal) DivRound(d2 Decimal, precision int32) Decimal {
+ // QuoRem already checks initialization
+ q, r := d.QuoRem(d2, precision)
+ // the actual rounding decision is based on comparing r*10^precision and d2/2
+ // instead compare 2 r 10 ^precision and d2
+ var rv2 big.Int
+ rv2.Abs(r.value)
+ rv2.Lsh(&rv2, 1)
+ // now rv2 = abs(r.value) * 2
+ r2 := Decimal{value: &rv2, exp: r.exp + precision}
+ // r2 is now 2 * r * 10 ^ precision
+ var c = r2.Cmp(d2.Abs())
+
+ if c < 0 {
+ return q
+ }
+
+ if d.value.Sign()*d2.value.Sign() < 0 {
+ return q.Sub(New(1, -precision))
+ }
+
+ return q.Add(New(1, -precision))
+}
+
+// Mod returns d % d2.
+func (d Decimal) Mod(d2 Decimal) Decimal {
+ quo := d.Div(d2).Truncate(0)
+ return d.Sub(d2.Mul(quo))
+}
+
+// Pow returns d to the power d2
+func (d Decimal) Pow(d2 Decimal) Decimal {
+ var temp Decimal
+ if d2.IntPart() == 0 {
+ return NewFromFloat(1)
+ }
+ temp = d.Pow(d2.Div(NewFromFloat(2)))
+ if d2.IntPart()%2 == 0 {
+ return temp.Mul(temp)
+ }
+ if d2.IntPart() > 0 {
+ return temp.Mul(temp).Mul(d)
+ }
+ return temp.Mul(temp).Div(d)
+}
+
+// Cmp compares the numbers represented by d and d2 and returns:
+//
+// -1 if d < d2
+// 0 if d == d2
+// +1 if d > d2
+//
+func (d Decimal) Cmp(d2 Decimal) int {
+ d.ensureInitialized()
+ d2.ensureInitialized()
+
+ if d.exp == d2.exp {
+ return d.value.Cmp(d2.value)
+ }
+
+ rd, rd2 := RescalePair(d, d2)
+
+ return rd.value.Cmp(rd2.value)
+}
+
+// Equal returns whether the numbers represented by d and d2 are equal.
+func (d Decimal) Equal(d2 Decimal) bool {
+ return d.Cmp(d2) == 0
+}
+
+// Equals is deprecated, please use Equal method instead
+func (d Decimal) Equals(d2 Decimal) bool {
+ return d.Equal(d2)
+}
+
+// GreaterThan (GT) returns true when d is greater than d2.
+func (d Decimal) GreaterThan(d2 Decimal) bool {
+ return d.Cmp(d2) == 1
+}
+
+// GreaterThanOrEqual (GTE) returns true when d is greater than or equal to d2.
+func (d Decimal) GreaterThanOrEqual(d2 Decimal) bool {
+ cmp := d.Cmp(d2)
+ return cmp == 1 || cmp == 0
+}
+
+// LessThan (LT) returns true when d is less than d2.
+func (d Decimal) LessThan(d2 Decimal) bool {
+ return d.Cmp(d2) == -1
+}
+
+// LessThanOrEqual (LTE) returns true when d is less than or equal to d2.
+func (d Decimal) LessThanOrEqual(d2 Decimal) bool {
+ cmp := d.Cmp(d2)
+ return cmp == -1 || cmp == 0
+}
+
+// Sign returns:
+//
+// -1 if d < 0
+// 0 if d == 0
+// +1 if d > 0
+//
+func (d Decimal) Sign() int {
+ if d.value == nil {
+ return 0
+ }
+ return d.value.Sign()
+}
+
+// IsPositive return
+//
+// true if d > 0
+// false if d == 0
+// false if d < 0
+func (d Decimal) IsPositive() bool {
+ return d.Sign() == 1
+}
+
+// IsNegative return
+//
+// true if d < 0
+// false if d == 0
+// false if d > 0
+func (d Decimal) IsNegative() bool {
+ return d.Sign() == -1
+}
+
+// IsZero return
+//
+// true if d == 0
+// false if d > 0
+// false if d < 0
+func (d Decimal) IsZero() bool {
+ return d.Sign() == 0
+}
+
+// Exponent returns the exponent, or scale component of the decimal.
+func (d Decimal) Exponent() int32 {
+ return d.exp
+}
+
+// Coefficient returns the coefficient of the decimal. It is scaled by 10^Exponent()
+func (d Decimal) Coefficient() *big.Int {
+ d.ensureInitialized()
+ // we copy the coefficient so that mutating the result does not mutate the
+ // Decimal.
+ return big.NewInt(0).Set(d.value)
+}
+
+// IntPart returns the integer component of the decimal.
+func (d Decimal) IntPart() int64 {
+ scaledD := d.rescale(0)
+ return scaledD.value.Int64()
+}
+
+// BigInt returns integer component of the decimal as a BigInt.
+func (d Decimal) BigInt() *big.Int {
+ scaledD := d.rescale(0)
+ i := &big.Int{}
+ i.SetString(scaledD.String(), 10)
+ return i
+}
+
+// BigFloat returns decimal as BigFloat.
+// Be aware that casting decimal to BigFloat might cause a loss of precision.
+func (d Decimal) BigFloat() *big.Float {
+ f := &big.Float{}
+ f.SetString(d.String())
+ return f
+}
+
+// Rat returns a rational number representation of the decimal.
+func (d Decimal) Rat() *big.Rat {
+ d.ensureInitialized()
+ if d.exp <= 0 {
+ // NOTE(vadim): must negate after casting to prevent int32 overflow
+ denom := new(big.Int).Exp(tenInt, big.NewInt(-int64(d.exp)), nil)
+ return new(big.Rat).SetFrac(d.value, denom)
+ }
+
+ mul := new(big.Int).Exp(tenInt, big.NewInt(int64(d.exp)), nil)
+ num := new(big.Int).Mul(d.value, mul)
+ return new(big.Rat).SetFrac(num, oneInt)
+}
+
+// Float64 returns the nearest float64 value for d and a bool indicating
+// whether f represents d exactly.
+// For more details, see the documentation for big.Rat.Float64
+func (d Decimal) Float64() (f float64, exact bool) {
+ return d.Rat().Float64()
+}
+
+// String returns the string representation of the decimal
+// with the fixed point.
+//
+// Example:
+//
+// d := New(-12345, -3)
+// println(d.String())
+//
+// Output:
+//
+// -12.345
+//
+func (d Decimal) String() string {
+ return d.string(true)
+}
+
+// StringFixed returns a rounded fixed-point string with places digits after
+// the decimal point.
+//
+// Example:
+//
+// NewFromFloat(0).StringFixed(2) // output: "0.00"
+// NewFromFloat(0).StringFixed(0) // output: "0"
+// NewFromFloat(5.45).StringFixed(0) // output: "5"
+// NewFromFloat(5.45).StringFixed(1) // output: "5.5"
+// NewFromFloat(5.45).StringFixed(2) // output: "5.45"
+// NewFromFloat(5.45).StringFixed(3) // output: "5.450"
+// NewFromFloat(545).StringFixed(-1) // output: "550"
+//
+func (d Decimal) StringFixed(places int32) string {
+ rounded := d.Round(places)
+ return rounded.string(false)
+}
+
+// StringFixedBank returns a banker rounded fixed-point string with places digits
+// after the decimal point.
+//
+// Example:
+//
+// NewFromFloat(0).StringFixedBank(2) // output: "0.00"
+// NewFromFloat(0).StringFixedBank(0) // output: "0"
+// NewFromFloat(5.45).StringFixedBank(0) // output: "5"
+// NewFromFloat(5.45).StringFixedBank(1) // output: "5.4"
+// NewFromFloat(5.45).StringFixedBank(2) // output: "5.45"
+// NewFromFloat(5.45).StringFixedBank(3) // output: "5.450"
+// NewFromFloat(545).StringFixedBank(-1) // output: "540"
+//
+func (d Decimal) StringFixedBank(places int32) string {
+ rounded := d.RoundBank(places)
+ return rounded.string(false)
+}
+
+// StringFixedCash returns a Swedish/Cash rounded fixed-point string. For
+// more details see the documentation at function RoundCash.
+func (d Decimal) StringFixedCash(interval uint8) string {
+ rounded := d.RoundCash(interval)
+ return rounded.string(false)
+}
+
+// Round rounds the decimal to places decimal places.
+// If places < 0, it will round the integer part to the nearest 10^(-places).
+//
+// Example:
+//
+// NewFromFloat(5.45).Round(1).String() // output: "5.5"
+// NewFromFloat(545).Round(-1).String() // output: "550"
+//
+func (d Decimal) Round(places int32) Decimal {
+ // truncate to places + 1
+ ret := d.rescale(-places - 1)
+
+ // add sign(d) * 0.5
+ if ret.value.Sign() < 0 {
+ ret.value.Sub(ret.value, fiveInt)
+ } else {
+ ret.value.Add(ret.value, fiveInt)
+ }
+
+ // floor for positive numbers, ceil for negative numbers
+ _, m := ret.value.DivMod(ret.value, tenInt, new(big.Int))
+ ret.exp++
+ if ret.value.Sign() < 0 && m.Cmp(zeroInt) != 0 {
+ ret.value.Add(ret.value, oneInt)
+ }
+
+ return ret
+}
+
+// RoundBank rounds the decimal to places decimal places.
+// If the final digit to round is equidistant from the nearest two integers the
+// rounded value is taken as the even number
+//
+// If places < 0, it will round the integer part to the nearest 10^(-places).
+//
+// Examples:
+//
+// NewFromFloat(5.45).Round(1).String() // output: "5.4"
+// NewFromFloat(545).Round(-1).String() // output: "540"
+// NewFromFloat(5.46).Round(1).String() // output: "5.5"
+// NewFromFloat(546).Round(-1).String() // output: "550"
+// NewFromFloat(5.55).Round(1).String() // output: "5.6"
+// NewFromFloat(555).Round(-1).String() // output: "560"
+//
+func (d Decimal) RoundBank(places int32) Decimal {
+
+ round := d.Round(places)
+ remainder := d.Sub(round).Abs()
+
+ half := New(5, -places-1)
+ if remainder.Cmp(half) == 0 && round.value.Bit(0) != 0 {
+ if round.value.Sign() < 0 {
+ round.value.Add(round.value, oneInt)
+ } else {
+ round.value.Sub(round.value, oneInt)
+ }
+ }
+
+ return round
+}
+
+// RoundCash aka Cash/Penny/öre rounding rounds decimal to a specific
+// interval. The amount payable for a cash transaction is rounded to the nearest
+// multiple of the minimum currency unit available. The following intervals are
+// available: 5, 10, 25, 50 and 100; any other number throws a panic.
+// 5: 5 cent rounding 3.43 => 3.45
+// 10: 10 cent rounding 3.45 => 3.50 (5 gets rounded up)
+// 25: 25 cent rounding 3.41 => 3.50
+// 50: 50 cent rounding 3.75 => 4.00
+// 100: 100 cent rounding 3.50 => 4.00
+// For more details: https://en.wikipedia.org/wiki/Cash_rounding
+func (d Decimal) RoundCash(interval uint8) Decimal {
+ var iVal *big.Int
+ switch interval {
+ case 5:
+ iVal = twentyInt
+ case 10:
+ iVal = tenInt
+ case 25:
+ iVal = fourInt
+ case 50:
+ iVal = twoInt
+ case 100:
+ iVal = oneInt
+ default:
+ panic(fmt.Sprintf("Decimal does not support this Cash rounding interval `%d`. Supported: 5, 10, 25, 50, 100", interval))
+ }
+ dVal := Decimal{
+ value: iVal,
+ }
+
+ // TODO: optimize those calculations to reduce the high allocations (~29 allocs).
+ return d.Mul(dVal).Round(0).Div(dVal).Truncate(2)
+}
+
+// Floor returns the nearest integer value less than or equal to d.
+func (d Decimal) Floor() Decimal {
+ d.ensureInitialized()
+
+ if d.exp >= 0 {
+ return d
+ }
+
+ exp := big.NewInt(10)
+
+ // NOTE(vadim): must negate after casting to prevent int32 overflow
+ exp.Exp(exp, big.NewInt(-int64(d.exp)), nil)
+
+ z := new(big.Int).Div(d.value, exp)
+ return Decimal{value: z, exp: 0}
+}
+
+// Ceil returns the nearest integer value greater than or equal to d.
+func (d Decimal) Ceil() Decimal {
+ d.ensureInitialized()
+
+ if d.exp >= 0 {
+ return d
+ }
+
+ exp := big.NewInt(10)
+
+ // NOTE(vadim): must negate after casting to prevent int32 overflow
+ exp.Exp(exp, big.NewInt(-int64(d.exp)), nil)
+
+ z, m := new(big.Int).DivMod(d.value, exp, new(big.Int))
+ if m.Cmp(zeroInt) != 0 {
+ z.Add(z, oneInt)
+ }
+ return Decimal{value: z, exp: 0}
+}
+
+// Truncate truncates off digits from the number, without rounding.
+//
+// NOTE: precision is the last digit that will not be truncated (must be >= 0).
+//
+// Example:
+//
+// decimal.NewFromString("123.456").Truncate(2).String() // "123.45"
+//
+func (d Decimal) Truncate(precision int32) Decimal {
+ d.ensureInitialized()
+ if precision >= 0 && -precision > d.exp {
+ return d.rescale(-precision)
+ }
+ return d
+}
+
+// UnmarshalJSON implements the json.Unmarshaler interface.
+func (d *Decimal) UnmarshalJSON(decimalBytes []byte) error {
+ if string(decimalBytes) == "null" {
+ return nil
+ }
+
+ str, err := unquoteIfQuoted(decimalBytes)
+ if err != nil {
+ return fmt.Errorf("error decoding string '%s': %s", decimalBytes, err)
+ }
+
+ decimal, err := NewFromString(str)
+ *d = decimal
+ if err != nil {
+ return fmt.Errorf("error decoding string '%s': %s", str, err)
+ }
+ return nil
+}
+
+// MarshalJSON implements the json.Marshaler interface.
+func (d Decimal) MarshalJSON() ([]byte, error) {
+ var str string
+ if MarshalJSONWithoutQuotes {
+ str = d.String()
+ } else {
+ str = "\"" + d.String() + "\""
+ }
+ return []byte(str), nil
+}
+
+// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. As a string representation
+// is already used when encoding to text, this method stores that string as []byte
+func (d *Decimal) UnmarshalBinary(data []byte) error {
+ // Extract the exponent
+ d.exp = int32(binary.BigEndian.Uint32(data[:4]))
+
+ // Extract the value
+ d.value = new(big.Int)
+ return d.value.GobDecode(data[4:])
+}
+
+// MarshalBinary implements the encoding.BinaryMarshaler interface.
+func (d Decimal) MarshalBinary() (data []byte, err error) {
+ // Write the exponent first since it's a fixed size
+ v1 := make([]byte, 4)
+ binary.BigEndian.PutUint32(v1, uint32(d.exp))
+
+ // Add the value
+ var v2 []byte
+ if v2, err = d.value.GobEncode(); err != nil {
+ return
+ }
+
+ // Return the byte array
+ data = append(v1, v2...)
+ return
+}
+
+// Scan implements the sql.Scanner interface for database deserialization.
+func (d *Decimal) Scan(value interface{}) error {
+ // first try to see if the data is stored in database as a Numeric datatype
+ switch v := value.(type) {
+
+ case float32:
+ *d = NewFromFloat(float64(v))
+ return nil
+
+ case float64:
+ // numeric in sqlite3 sends us float64
+ *d = NewFromFloat(v)
+ return nil
+
+ case int64:
+ // at least in sqlite3 when the value is 0 in db, the data is sent
+ // to us as an int64 instead of a float64 ...
+ *d = New(v, 0)
+ return nil
+
+ default:
+ // default is trying to interpret value stored as string
+ str, err := unquoteIfQuoted(v)
+ if err != nil {
+ return err
+ }
+ *d, err = NewFromString(str)
+ return err
+ }
+}
+
+// Value implements the driver.Valuer interface for database serialization.
+func (d Decimal) Value() (driver.Value, error) {
+ return d.String(), nil
+}
+
+// UnmarshalText implements the encoding.TextUnmarshaler interface for XML
+// deserialization.
+func (d *Decimal) UnmarshalText(text []byte) error {
+ str := string(text)
+
+ dec, err := NewFromString(str)
+ *d = dec
+ if err != nil {
+ return fmt.Errorf("error decoding string '%s': %s", str, err)
+ }
+
+ return nil
+}
+
+// MarshalText implements the encoding.TextMarshaler interface for XML
+// serialization.
+func (d Decimal) MarshalText() (text []byte, err error) {
+ return []byte(d.String()), nil
+}
+
+// GobEncode implements the gob.GobEncoder interface for gob serialization.
+func (d Decimal) GobEncode() ([]byte, error) {
+ return d.MarshalBinary()
+}
+
+// GobDecode implements the gob.GobDecoder interface for gob serialization.
+func (d *Decimal) GobDecode(data []byte) error {
+ return d.UnmarshalBinary(data)
+}
+
+// StringScaled first scales the decimal then calls .String() on it.
+// NOTE: buggy, unintuitive, and DEPRECATED! Use StringFixed instead.
+func (d Decimal) StringScaled(exp int32) string {
+ return d.rescale(exp).String()
+}
+
+func (d Decimal) string(trimTrailingZeros bool) string {
+ if d.exp >= 0 {
+ return d.rescale(0).value.String()
+ }
+
+ abs := new(big.Int).Abs(d.value)
+ str := abs.String()
+
+ var intPart, fractionalPart string
+
+ // NOTE(vadim): this cast to int will cause bugs if d.exp == INT_MIN
+ // and you are on a 32-bit machine. Won't fix this super-edge case.
+ dExpInt := int(d.exp)
+ if len(str) > -dExpInt {
+ intPart = str[:len(str)+dExpInt]
+ fractionalPart = str[len(str)+dExpInt:]
+ } else {
+ intPart = "0"
+
+ num0s := -dExpInt - len(str)
+ fractionalPart = strings.Repeat("0", num0s) + str
+ }
+
+ if trimTrailingZeros {
+ i := len(fractionalPart) - 1
+ for ; i >= 0; i-- {
+ if fractionalPart[i] != '0' {
+ break
+ }
+ }
+ fractionalPart = fractionalPart[:i+1]
+ }
+
+ number := intPart
+ if len(fractionalPart) > 0 {
+ number += "." + fractionalPart
+ }
+
+ if d.value.Sign() < 0 {
+ return "-" + number
+ }
+
+ return number
+}
+
+func (d *Decimal) ensureInitialized() {
+ if d.value == nil {
+ d.value = new(big.Int)
+ }
+}
+
+// Min returns the smallest Decimal that was passed in the arguments.
+//
+// To call this function with an array, you must do:
+//
+// Min(arr[0], arr[1:]...)
+//
+// This makes it harder to accidentally call Min with 0 arguments.
+func Min(first Decimal, rest ...Decimal) Decimal {
+ ans := first
+ for _, item := range rest {
+ if item.Cmp(ans) < 0 {
+ ans = item
+ }
+ }
+ return ans
+}
+
+// Max returns the largest Decimal that was passed in the arguments.
+//
+// To call this function with an array, you must do:
+//
+// Max(arr[0], arr[1:]...)
+//
+// This makes it harder to accidentally call Max with 0 arguments.
+func Max(first Decimal, rest ...Decimal) Decimal {
+ ans := first
+ for _, item := range rest {
+ if item.Cmp(ans) > 0 {
+ ans = item
+ }
+ }
+ return ans
+}
+
+// Sum returns the combined total of the provided first and rest Decimals
+func Sum(first Decimal, rest ...Decimal) Decimal {
+ total := first
+ for _, item := range rest {
+ total = total.Add(item)
+ }
+
+ return total
+}
+
+// Avg returns the average value of the provided first and rest Decimals
+func Avg(first Decimal, rest ...Decimal) Decimal {
+ count := New(int64(len(rest)+1), 0)
+ sum := Sum(first, rest...)
+ return sum.Div(count)
+}
+
+// RescalePair rescales two decimals to common exponential value (minimal exp of both decimals)
+func RescalePair(d1 Decimal, d2 Decimal) (Decimal, Decimal) {
+ d1.ensureInitialized()
+ d2.ensureInitialized()
+
+ if d1.exp == d2.exp {
+ return d1, d2
+ }
+
+ baseScale := min(d1.exp, d2.exp)
+ if baseScale != d1.exp {
+ return d1.rescale(baseScale), d2
+ }
+ return d1, d2.rescale(baseScale)
+}
+
+func min(x, y int32) int32 {
+ if x >= y {
+ return y
+ }
+ return x
+}
+
+func unquoteIfQuoted(value interface{}) (string, error) {
+ var bytes []byte
+
+ switch v := value.(type) {
+ case string:
+ bytes = []byte(v)
+ case []byte:
+ bytes = v
+ default:
+ return "", fmt.Errorf("could not convert value '%+v' to byte array of type '%T'",
+ value, value)
+ }
+
+ // If the amount is quoted, strip the quotes
+ if len(bytes) > 2 && bytes[0] == '"' && bytes[len(bytes)-1] == '"' {
+ bytes = bytes[1 : len(bytes)-1]
+ }
+ return string(bytes), nil
+}
+
+// NullDecimal represents a nullable decimal with compatibility for
+// scanning null values from the database.
+type NullDecimal struct {
+ Decimal Decimal
+ Valid bool
+}
+
+// Scan implements the sql.Scanner interface for database deserialization.
+func (d *NullDecimal) Scan(value interface{}) error {
+ if value == nil {
+ d.Valid = false
+ return nil
+ }
+ d.Valid = true
+ return d.Decimal.Scan(value)
+}
+
+// Value implements the driver.Valuer interface for database serialization.
+func (d NullDecimal) Value() (driver.Value, error) {
+ if !d.Valid {
+ return nil, nil
+ }
+ return d.Decimal.Value()
+}
+
+// UnmarshalJSON implements the json.Unmarshaler interface.
+func (d *NullDecimal) UnmarshalJSON(decimalBytes []byte) error {
+ if string(decimalBytes) == "null" {
+ d.Valid = false
+ return nil
+ }
+ d.Valid = true
+ return d.Decimal.UnmarshalJSON(decimalBytes)
+}
+
+// MarshalJSON implements the json.Marshaler interface.
+func (d NullDecimal) MarshalJSON() ([]byte, error) {
+ if !d.Valid {
+ return []byte("null"), nil
+ }
+ return d.Decimal.MarshalJSON()
+}
+
+// Trig functions
+
+// Atan returns the arctangent, in radians, of x.
+func (d Decimal) Atan() Decimal {
+ if d.Equal(NewFromFloat(0.0)) {
+ return d
+ }
+ if d.GreaterThan(NewFromFloat(0.0)) {
+ return d.satan()
+ }
+ return d.Neg().satan().Neg()
+}
+
+func (d Decimal) xatan() Decimal {
+ P0 := NewFromFloat(-8.750608600031904122785e-01)
+ P1 := NewFromFloat(-1.615753718733365076637e+01)
+ P2 := NewFromFloat(-7.500855792314704667340e+01)
+ P3 := NewFromFloat(-1.228866684490136173410e+02)
+ P4 := NewFromFloat(-6.485021904942025371773e+01)
+ Q0 := NewFromFloat(2.485846490142306297962e+01)
+ Q1 := NewFromFloat(1.650270098316988542046e+02)
+ Q2 := NewFromFloat(4.328810604912902668951e+02)
+ Q3 := NewFromFloat(4.853903996359136964868e+02)
+ Q4 := NewFromFloat(1.945506571482613964425e+02)
+ z := d.Mul(d)
+ b1 := P0.Mul(z).Add(P1).Mul(z).Add(P2).Mul(z).Add(P3).Mul(z).Add(P4).Mul(z)
+ b2 := z.Add(Q0).Mul(z).Add(Q1).Mul(z).Add(Q2).Mul(z).Add(Q3).Mul(z).Add(Q4)
+ z = b1.Div(b2)
+ z = d.Mul(z).Add(d)
+ return z
+}
+
+// satan reduces its argument (known to be positive)
+// to the range [0, 0.66] and calls xatan.
+func (d Decimal) satan() Decimal {
+ Morebits := NewFromFloat(6.123233995736765886130e-17) // pi/2 = PIO2 + Morebits
+ Tan3pio8 := NewFromFloat(2.41421356237309504880) // tan(3*pi/8)
+ pi := NewFromFloat(3.14159265358979323846264338327950288419716939937510582097494459)
+
+ if d.LessThanOrEqual(NewFromFloat(0.66)) {
+ return d.xatan()
+ }
+ if d.GreaterThan(Tan3pio8) {
+ return pi.Div(NewFromFloat(2.0)).Sub(NewFromFloat(1.0).Div(d).xatan()).Add(Morebits)
+ }
+ return pi.Div(NewFromFloat(4.0)).Add((d.Sub(NewFromFloat(1.0)).Div(d.Add(NewFromFloat(1.0)))).xatan()).Add(NewFromFloat(0.5).Mul(Morebits))
+}
+
+// sin coefficients
+var _sin = [...]Decimal{
+ NewFromFloat(1.58962301576546568060e-10), // 0x3de5d8fd1fd19ccd
+ NewFromFloat(-2.50507477628578072866e-8), // 0xbe5ae5e5a9291f5d
+ NewFromFloat(2.75573136213857245213e-6), // 0x3ec71de3567d48a1
+ NewFromFloat(-1.98412698295895385996e-4), // 0xbf2a01a019bfdf03
+ NewFromFloat(8.33333333332211858878e-3), // 0x3f8111111110f7d0
+ NewFromFloat(-1.66666666666666307295e-1), // 0xbfc5555555555548
+}
+
+// Sin returns the sine of the radian argument x.
+func (d Decimal) Sin() Decimal {
+ PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts
+ PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000,
+ PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170,
+ M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi
+
+ if d.Equal(NewFromFloat(0.0)) {
+ return d
+ }
+ // make argument positive but save the sign
+ sign := false
+ if d.LessThan(NewFromFloat(0.0)) {
+ d = d.Neg()
+ sign = true
+ }
+
+ j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle
+ y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float
+
+ // map zeros to origin
+ if j&1 == 1 {
+ j++
+ y = y.Add(NewFromFloat(1.0))
+ }
+ j &= 7 // octant modulo 2Pi radians (360 degrees)
+ // reflect in x axis
+ if j > 3 {
+ sign = !sign
+ j -= 4
+ }
+ z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic
+ zz := z.Mul(z)
+
+ if j == 1 || j == 2 {
+ w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5]))
+ y = NewFromFloat(1.0).Sub(NewFromFloat(0.5).Mul(zz)).Add(w)
+ } else {
+ y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5])))
+ }
+ if sign {
+ y = y.Neg()
+ }
+ return y
+}
+
+// cos coefficients
+var _cos = [...]Decimal{
+ NewFromFloat(-1.13585365213876817300e-11), // 0xbda8fa49a0861a9b
+ NewFromFloat(2.08757008419747316778e-9), // 0x3e21ee9d7b4e3f05
+ NewFromFloat(-2.75573141792967388112e-7), // 0xbe927e4f7eac4bc6
+ NewFromFloat(2.48015872888517045348e-5), // 0x3efa01a019c844f5
+ NewFromFloat(-1.38888888888730564116e-3), // 0xbf56c16c16c14f91
+ NewFromFloat(4.16666666666665929218e-2), // 0x3fa555555555554b
+}
+
+// Cos returns the cosine of the radian argument x.
+func (d Decimal) Cos() Decimal {
+
+ PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts
+ PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000,
+ PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170,
+ M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi
+
+ // make argument positive
+ sign := false
+ if d.LessThan(NewFromFloat(0.0)) {
+ d = d.Neg()
+ }
+
+ j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle
+ y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float
+
+ // map zeros to origin
+ if j&1 == 1 {
+ j++
+ y = y.Add(NewFromFloat(1.0))
+ }
+ j &= 7 // octant modulo 2Pi radians (360 degrees)
+ // reflect in x axis
+ if j > 3 {
+ sign = !sign
+ j -= 4
+ }
+ if j > 1 {
+ sign = !sign
+ }
+
+ z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic
+ zz := z.Mul(z)
+
+ if j == 1 || j == 2 {
+ y = z.Add(z.Mul(zz).Mul(_sin[0].Mul(zz).Add(_sin[1]).Mul(zz).Add(_sin[2]).Mul(zz).Add(_sin[3]).Mul(zz).Add(_sin[4]).Mul(zz).Add(_sin[5])))
+ } else {
+ w := zz.Mul(zz).Mul(_cos[0].Mul(zz).Add(_cos[1]).Mul(zz).Add(_cos[2]).Mul(zz).Add(_cos[3]).Mul(zz).Add(_cos[4]).Mul(zz).Add(_cos[5]))
+ y = NewFromFloat(1.0).Sub(NewFromFloat(0.5).Mul(zz)).Add(w)
+ }
+ if sign {
+ y = y.Neg()
+ }
+ return y
+}
+
+var _tanP = [...]Decimal{
+ NewFromFloat(-1.30936939181383777646e+4), // 0xc0c992d8d24f3f38
+ NewFromFloat(1.15351664838587416140e+6), // 0x413199eca5fc9ddd
+ NewFromFloat(-1.79565251976484877988e+7), // 0xc1711fead3299176
+}
+var _tanQ = [...]Decimal{
+ NewFromFloat(1.00000000000000000000e+0),
+ NewFromFloat(1.36812963470692954678e+4), //0x40cab8a5eeb36572
+ NewFromFloat(-1.32089234440210967447e+6), //0xc13427bc582abc96
+ NewFromFloat(2.50083801823357915839e+7), //0x4177d98fc2ead8ef
+ NewFromFloat(-5.38695755929454629881e+7), //0xc189afe03cbe5a31
+}
+
+// Tan returns the tangent of the radian argument x.
+func (d Decimal) Tan() Decimal {
+
+ PI4A := NewFromFloat(7.85398125648498535156e-1) // 0x3fe921fb40000000, Pi/4 split into three parts
+ PI4B := NewFromFloat(3.77489470793079817668e-8) // 0x3e64442d00000000,
+ PI4C := NewFromFloat(2.69515142907905952645e-15) // 0x3ce8469898cc5170,
+ M4PI := NewFromFloat(1.273239544735162542821171882678754627704620361328125) // 4/pi
+
+ if d.Equal(NewFromFloat(0.0)) {
+ return d
+ }
+
+ // make argument positive but save the sign
+ sign := false
+ if d.LessThan(NewFromFloat(0.0)) {
+ d = d.Neg()
+ sign = true
+ }
+
+ j := d.Mul(M4PI).IntPart() // integer part of x/(Pi/4), as integer for tests on the phase angle
+ y := NewFromFloat(float64(j)) // integer part of x/(Pi/4), as float
+
+ // map zeros to origin
+ if j&1 == 1 {
+ j++
+ y = y.Add(NewFromFloat(1.0))
+ }
+
+ z := d.Sub(y.Mul(PI4A)).Sub(y.Mul(PI4B)).Sub(y.Mul(PI4C)) // Extended precision modular arithmetic
+ zz := z.Mul(z)
+
+ if zz.GreaterThan(NewFromFloat(1e-14)) {
+ w := zz.Mul(_tanP[0].Mul(zz).Add(_tanP[1]).Mul(zz).Add(_tanP[2]))
+ x := zz.Add(_tanQ[1]).Mul(zz).Add(_tanQ[2]).Mul(zz).Add(_tanQ[3]).Mul(zz).Add(_tanQ[4])
+ y = z.Add(z.Mul(w.Div(x)))
+ } else {
+ y = z
+ }
+ if j&2 == 2 {
+ y = NewFromFloat(-1.0).Div(y)
+ }
+ if sign {
+ y = y.Neg()
+ }
+ return y
+}
diff --git a/vendor/github.com/shopspring/decimal/rounding.go b/vendor/github.com/shopspring/decimal/rounding.go
new file mode 100644
index 0000000..8008f55
--- /dev/null
+++ b/vendor/github.com/shopspring/decimal/rounding.go
@@ -0,0 +1,119 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Multiprecision decimal numbers.
+// For floating-point formatting only; not general purpose.
+// Only operations are assign and (binary) left/right shift.
+// Can do binary floating point in multiprecision decimal precisely
+// because 2 divides 10; cannot do decimal floating point
+// in multiprecision binary precisely.
+
+package decimal
+
+type floatInfo struct {
+ mantbits uint
+ expbits uint
+ bias int
+}
+
+var float32info = floatInfo{23, 8, -127}
+var float64info = floatInfo{52, 11, -1023}
+
+// roundShortest rounds d (= mant * 2^exp) to the shortest number of digits
+// that will let the original floating point value be precisely reconstructed.
+func roundShortest(d *decimal, mant uint64, exp int, flt *floatInfo) {
+ // If mantissa is zero, the number is zero; stop now.
+ if mant == 0 {
+ d.nd = 0
+ return
+ }
+
+ // Compute upper and lower such that any decimal number
+ // between upper and lower (possibly inclusive)
+ // will round to the original floating point number.
+
+ // We may see at once that the number is already shortest.
+ //
+ // Suppose d is not denormal, so that 2^exp <= d < 10^dp.
+ // The closest shorter number is at least 10^(dp-nd) away.
+ // The lower/upper bounds computed below are at distance
+ // at most 2^(exp-mantbits).
+ //
+ // So the number is already shortest if 10^(dp-nd) > 2^(exp-mantbits),
+ // or equivalently log2(10)*(dp-nd) > exp-mantbits.
+ // It is true if 332/100*(dp-nd) >= exp-mantbits (log2(10) > 3.32).
+ minexp := flt.bias + 1 // minimum possible exponent
+ if exp > minexp && 332*(d.dp-d.nd) >= 100*(exp-int(flt.mantbits)) {
+ // The number is already shortest.
+ return
+ }
+
+ // d = mant << (exp - mantbits)
+ // Next highest floating point number is mant+1 << exp-mantbits.
+ // Our upper bound is halfway between, mant*2+1 << exp-mantbits-1.
+ upper := new(decimal)
+ upper.Assign(mant*2 + 1)
+ upper.Shift(exp - int(flt.mantbits) - 1)
+
+ // d = mant << (exp - mantbits)
+ // Next lowest floating point number is mant-1 << exp-mantbits,
+ // unless mant-1 drops the significant bit and exp is not the minimum exp,
+ // in which case the next lowest is mant*2-1 << exp-mantbits-1.
+ // Either way, call it mantlo << explo-mantbits.
+ // Our lower bound is halfway between, mantlo*2+1 << explo-mantbits-1.
+ var mantlo uint64
+ var explo int
+ if mant > 1< github.com/muun/neutrino v0.0.0-20190914162326-7082af0fa257
+## go 1.13
github.com/lightninglabs/neutrino
github.com/lightninglabs/neutrino/banman
github.com/lightninglabs/neutrino/blockntfns
@@ -91,8 +111,10 @@ github.com/lightninglabs/neutrino/headerfs
github.com/lightninglabs/neutrino/headerlist
github.com/lightninglabs/neutrino/pushtx
# github.com/lightningnetwork/lightning-onion v1.0.1
+## explicit; go 1.13
github.com/lightningnetwork/lightning-onion
# github.com/lightningnetwork/lnd v0.10.4-beta
+## explicit; go 1.12
github.com/lightningnetwork/lnd/build
github.com/lightningnetwork/lnd/chainntnfs
github.com/lightningnetwork/lnd/channeldb
@@ -117,8 +139,10 @@ github.com/lightningnetwork/lnd/tlv
github.com/lightningnetwork/lnd/tor
github.com/lightningnetwork/lnd/zpay32
# github.com/lightningnetwork/lnd/clock v1.0.1
+## go 1.13
github.com/lightningnetwork/lnd/clock
# github.com/lightningnetwork/lnd/queue v1.0.4
+## go 1.12
github.com/lightningnetwork/lnd/queue
# github.com/lightningnetwork/lnd/ticker v1.0.0
github.com/lightningnetwork/lnd/ticker
@@ -127,29 +151,15 @@ github.com/ltcsuite/ltcd/chaincfg
github.com/ltcsuite/ltcd/chaincfg/chainhash
github.com/ltcsuite/ltcd/wire
# github.com/mattn/go-sqlite3 v1.14.0
+## go 1.10
github.com/mattn/go-sqlite3
# github.com/miekg/dns v1.1.29
+## explicit; go 1.12
github.com/miekg/dns
-# github.com/muun/libwallet v0.11.0
-github.com/muun/libwallet
-github.com/muun/libwallet/addresses
-github.com/muun/libwallet/aescbc
-github.com/muun/libwallet/btcsuitew/bech32m
-github.com/muun/libwallet/btcsuitew/btcutilw
-github.com/muun/libwallet/btcsuitew/chainhashw
-github.com/muun/libwallet/btcsuitew/txscriptw
-github.com/muun/libwallet/emergencykit
-github.com/muun/libwallet/errors
-github.com/muun/libwallet/fees
-github.com/muun/libwallet/hdpath
-github.com/muun/libwallet/keycrypt
-github.com/muun/libwallet/lnurl
-github.com/muun/libwallet/musig
-github.com/muun/libwallet/recoverycode
-github.com/muun/libwallet/sphinx
-github.com/muun/libwallet/swaps
-github.com/muun/libwallet/walletdb
+# github.com/muun/libwallet v0.11.0 => ./libwallet
+## explicit; go 1.14
# github.com/pdfcpu/pdfcpu v0.3.11
+## explicit; go 1.15
github.com/pdfcpu/pdfcpu/internal/config
github.com/pdfcpu/pdfcpu/internal/corefont/metrics
github.com/pdfcpu/pdfcpu/pkg/api
@@ -160,18 +170,26 @@ github.com/pdfcpu/pdfcpu/pkg/pdfcpu
github.com/pdfcpu/pdfcpu/pkg/pdfcpu/validate
github.com/pdfcpu/pdfcpu/pkg/types
# github.com/pkg/errors v0.9.1
+## explicit
github.com/pkg/errors
+# github.com/shopspring/decimal v1.2.0
+## explicit; go 1.13
+github.com/shopspring/decimal
# github.com/tidwall/gjson v1.6.0
+## go 1.12
github.com/tidwall/gjson
# github.com/tidwall/match v1.0.1
github.com/tidwall/match
# github.com/tidwall/pretty v1.0.0
github.com/tidwall/pretty
# github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
+## go 1.15
github.com/xo/terminfo
# go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50
+## go 1.12
go.etcd.io/bbolt
# golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
+## explicit; go 1.17
golang.org/x/crypto/ed25519
golang.org/x/crypto/ed25519/internal/edwards25519
golang.org/x/crypto/internal/poly1305
@@ -183,12 +201,14 @@ golang.org/x/crypto/salsa20/salsa
golang.org/x/crypto/scrypt
golang.org/x/crypto/ssh/terminal
# golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
+## go 1.12
golang.org/x/image/ccitt
golang.org/x/image/riff
golang.org/x/image/vp8
golang.org/x/image/vp8l
golang.org/x/image/webp
# golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f
+## explicit; go 1.17
golang.org/x/net/bpf
golang.org/x/net/internal/iana
golang.org/x/net/internal/socket
@@ -196,18 +216,26 @@ golang.org/x/net/internal/socks
golang.org/x/net/ipv4
golang.org/x/net/ipv6
golang.org/x/net/proxy
+# golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
+## explicit
# golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6
+## explicit; go 1.17
golang.org/x/sys/cpu
golang.org/x/sys/internal/unsafeheader
golang.org/x/sys/plan9
golang.org/x/sys/unix
golang.org/x/sys/windows
# golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
+## go 1.11
golang.org/x/term
# golang.org/x/text v0.3.7
+## explicit; go 1.17
golang.org/x/text/transform
golang.org/x/text/unicode/norm
+# golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
+## explicit; go 1.11
# google.golang.org/protobuf v1.25.0
+## explicit; go 1.9
google.golang.org/protobuf/encoding/prototext
google.golang.org/protobuf/encoding/protowire
google.golang.org/protobuf/internal/descfmt
@@ -235,6 +263,10 @@ google.golang.org/protobuf/reflect/protoregistry
google.golang.org/protobuf/runtime/protoiface
google.golang.org/protobuf/runtime/protoimpl
# gopkg.in/gormigrate.v1 v1.6.0
+## explicit
gopkg.in/gormigrate.v1
# gopkg.in/yaml.v2 v2.4.0
+## go 1.15
gopkg.in/yaml.v2
+# golang.org/x/mobile => github.com/muun/mobile v0.0.0-20220913162405-8cc629edd37b
+# github.com/lightninglabs/neutrino => github.com/muun/neutrino v0.0.0-20190914162326-7082af0fa257