Update project structure and build process

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

6
recovery_tool/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.data
.idea
neutrino_test
/bin
recovery

79
recovery_tool/Dockerfile Normal file
View File

@@ -0,0 +1,79 @@
# Building this Dockerfile executes a full build of the Recovery Tool, cross-compiling inside
# the container. The resulting executable is copied to the output directory using Docker BuildKit.
# You need to pass 3 parameters via --build-arg:
# 1. `os` : the GOOS env var -- `linux`, `windows` or `darwin`.
# 2. `arch`: the GOARCH env var -- `386` or `amd64` (note that darwin/386 is not a thing).
# 3. `cc` : the CC env var -- a C compiler for CGO to use, empty to use the default.
# 4. `out` : the name of the resulting executable, placed in the output directory on the host.
# For example, to build a linux/386 binary into `bin/rt`:
# docker build . --output bin --build-arg os=linux --build-arg arch=386 --build-arg out=rt
# Note that the --output <dir> flag refers to the host, outside the container.
# --------------------------------------------------------------------------------------------------
# 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"
# 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-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
COPY . .
RUN /bin/bash
# --------------------------------------------------------------------------------------------------
FROM rtool-build-base AS rtool-build
ARG os
ARG arch
ARG cc
# Enable and configure C support:
ENV CGO_ENABLED=1
ENV GO386=softfloat
# Do the thing:
RUN env GOOS=${os} GOARCH=${arch} CC=${cc} go build -mod=vendor -a -trimpath -o /out ./recovery_tool/
# --------------------------------------------------------------------------------------------------
FROM scratch
ARG out
# Copy the resulting executable back to the host:
COPY --from=rtool-build /out ${out}

21
recovery_tool/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021 Muun Wallet, 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.

74
recovery_tool/Makefile Normal file
View File

@@ -0,0 +1,74 @@
# (Default) build the Recovery Tool to run on this system.
build:
mkdir -p bin
echo "Building recovery tool"
go build -mod=vendor -a -trimpath -o "bin/recovery-tool"
echo "Success! Built to bin/recovery-tool"
# Cross-compile and checksum the Recovery Tool for a range of OS/archs.
build-checksum-all: export DOCKER_BUILDKIT=1
build-checksum-all:
# Get vendor dependencies:
go work vendor -v
# Linux 32-bit:
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 -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 -f Dockerfile .. -o bin \
--build-arg os=windows \
--build-arg arch=386 \
--build-arg cc=i686-w64-mingw32-gcc \
--build-arg out=recovery-tool-windows32.exe
/bin/echo -n '✓ Windows 32-bit ' && sha256sum "bin/recovery-tool-windows32.exe"
# Windows 64-bit:
docker build -f Dockerfile .. -o bin \
--build-arg os=windows \
--build-arg arch=amd64 \
--build-arg cc=x86_64-w64-mingw32-gcc \
--build-arg out=recovery-tool-windows64.exe
/bin/echo -n '✓ Windows 64-bit ' && sha256sum "bin/recovery-tool-windows64.exe"
# NOTE:
# Darwin reproducible builds are disabled for now, since the inclusion of C code in the latest
# release made building the tool inside a Linux container extremely difficult. We'll be moving the
# process to GitHub actions, where we can build on MacOS.
# Darwin 64-bit:
# docker build . -o bin \
# --build-arg os=darwin \
# --build-arg arch=amd64 \
# --build-arg out=recovery-tool-macos64
# /bin/echo -n '✓ MacOS 64-bit ' && sha256sum "bin/recovery-tool-macos64"
.SILENT:

View File

@@ -0,0 +1,129 @@
package main
import (
"fmt"
"log"
"github.com/muun/libwallet"
"github.com/muun/recovery/utils"
)
type AddressGenerator struct {
addressCount int
userKey *libwallet.HDPrivateKey
muunKey *libwallet.HDPrivateKey
generateContacts bool
}
func NewAddressGenerator(userKey, muunKey *libwallet.HDPrivateKey, generateContacts bool) *AddressGenerator {
return &AddressGenerator{
addressCount: 0,
userKey: userKey,
muunKey: muunKey,
generateContacts: generateContacts,
}
}
// Stream returns a channel that emits all addresses generated.
func (g *AddressGenerator) Stream() chan libwallet.MuunAddress {
ch := make(chan libwallet.MuunAddress)
go func() {
g.generate(ch)
utils.NewLogger("ADDR").Printf("Addresses %v\n", g.addressCount)
close(ch)
}()
return ch
}
func (g *AddressGenerator) generate(consumer chan libwallet.MuunAddress) {
g.generateChangeAddrs(consumer)
g.generateExternalAddrs(consumer)
if g.generateContacts {
g.generateContactAddrs(consumer, 100)
}
}
func (g *AddressGenerator) generateChangeAddrs(consumer chan libwallet.MuunAddress) {
const changePath = "m/1'/1'/0"
changeUserKey, _ := g.userKey.DeriveTo(changePath)
changeMuunKey, _ := g.muunKey.DeriveTo(changePath)
g.deriveTree(consumer, changeUserKey, changeMuunKey, 2500, "change")
}
func (g *AddressGenerator) generateExternalAddrs(consumer chan libwallet.MuunAddress) {
const externalPath = "m/1'/1'/1"
externalUserKey, _ := g.userKey.DeriveTo(externalPath)
externalMuunKey, _ := g.muunKey.DeriveTo(externalPath)
g.deriveTree(consumer, externalUserKey, externalMuunKey, 2500, "external")
}
func (g *AddressGenerator) generateContactAddrs(consumer chan libwallet.MuunAddress, numContacts int64) {
const addressPath = "m/1'/1'/2"
contactUserKey, _ := g.userKey.DeriveTo(addressPath)
contactMuunKey, _ := g.muunKey.DeriveTo(addressPath)
for i := int64(0); i <= numContacts; i++ {
partialContactUserKey, _ := contactUserKey.DerivedAt(i, false)
partialMuunUserKey, _ := contactMuunKey.DerivedAt(i, false)
branch := fmt.Sprintf("contacts-%v", i)
g.deriveTree(consumer, partialContactUserKey, partialMuunUserKey, 200, branch)
}
}
func (g *AddressGenerator) deriveTree(
consumer chan libwallet.MuunAddress,
rootUserKey, rootMuunKey *libwallet.HDPrivateKey,
count int64,
name string,
) {
for i := int64(0); i <= count; i++ {
userKey, err := rootUserKey.DerivedAt(i, false)
if err != nil {
log.Printf("skipping child %v for %v due to %v", i, name, err)
continue
}
muunKey, err := rootMuunKey.DerivedAt(i, false)
if err != nil {
log.Printf("skipping child %v for %v due to %v", i, name, err)
continue
}
addrV2, err := libwallet.CreateAddressV2(userKey.PublicKey(), muunKey.PublicKey())
if err == nil {
consumer <- addrV2
g.addressCount++
} else {
log.Printf("failed to generate %v v2 for %v due to %v", name, i, err)
}
addrV3, err := libwallet.CreateAddressV3(userKey.PublicKey(), muunKey.PublicKey())
if err == nil {
consumer <- addrV3
g.addressCount++
} else {
log.Printf("failed to generate %v v3 for %v due to %v", name, i, err)
}
addrV4, err := libwallet.CreateAddressV4(userKey.PublicKey(), muunKey.PublicKey())
if err == nil {
consumer <- addrV4
g.addressCount++
} else {
log.Printf("failed to generate %v v4 for %v due to %v", name, i, err)
}
addrV5, err := libwallet.CreateAddressV5(userKey.PublicKey(), muunKey.PublicKey())
if err == nil {
consumer <- addrV5
g.addressCount++
} else {
log.Printf("failed to generate %v v5 for %v due to %v", name, i, err)
}
}
}

View File

@@ -0,0 +1,51 @@
package main
import (
"fmt"
"time"
"github.com/muun/recovery/electrum"
"github.com/muun/recovery/survey"
)
func main() {
config := &survey.Config{
InitialServers: electrum.PublicServers,
Workers: 30,
SpeedTestDuration: time.Second * 20,
SpeedTestBatchSize: 100,
}
survey := survey.NewSurvey(config)
results := survey.Run()
fmt.Println("\n\n// Worthy servers:")
for _, result := range results {
if result.IsWorthy {
fmt.Println(toCodeLine(result))
}
}
fmt.Println("\n\n// Unworthy servers:")
for _, result := range results {
if !result.IsWorthy {
fmt.Println(toCodeLine(result))
}
}
}
func toCodeLine(r *survey.Result) string {
if r.Err != nil {
return fmt.Sprintf("\"%s\", // %v", r.Server, r.Err)
}
return fmt.Sprintf(
"\"%s\", // impl: %s, batching: %v, ttc: %.2f, speed: %d, from: %s",
r.Server,
r.Impl,
r.BatchSupport,
r.TimeToConnect.Seconds(),
r.Speed,
r.FromPeer,
)
}

View File

@@ -0,0 +1,540 @@
package electrum
import (
"bufio"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"sort"
"strings"
"time"
"github.com/muun/recovery/utils"
)
const defaultLoggerTag = "Electrum/?"
const connectionTimeout = time.Second * 30
const callTimeout = time.Second * 30
const messageDelim = byte('\n')
const noTimeout = 0
var implsWithBatching = []string{"ElectrumX"}
// Client is a TLS client that implements a subset of the Electrum protocol.
//
// It includes a minimal implementation of a JSON-RPC client, since the one provided by the
// standard library doesn't support features such as batching.
//
// It is absolutely not thread-safe. Every Client should have a single owner.
type Client struct {
Server string
ServerImpl string
ProtoVersion string
nextRequestID int
conn net.Conn
log *utils.Logger
requireTls bool
}
// Request models the structure of all Electrum protocol requests.
type Request struct {
ID int `json:"id"`
Method string `json:"method"`
Params []Param `json:"params"`
}
// ErrorResponse models the structure of a generic error response.
type ErrorResponse struct {
ID int `json:"id"`
Error interface{} `json:"error"` // type varies among Electrum implementations.
}
// ServerVersionResponse models the structure of a `server.version` response.
type ServerVersionResponse struct {
ID int `json:"id"`
Result []string `json:"result"`
}
// ServerFeaturesResponse models the structure of a `server.features` response.
type ServerFeaturesResponse struct {
ID int `json:"id"`
Result ServerFeatures `json:"result"`
}
// ServerPeersResponse models the structure (or lack thereof) of a `server.peers.subscribe` response
type ServerPeersResponse struct {
ID int `json:"id"`
Result []interface{} `json:"result"`
}
// ListUnspentResponse models a `blockchain.scripthash.listunspent` response.
type ListUnspentResponse struct {
ID int `json:"id"`
Result []UnspentRef `json:"result"`
}
// GetTransactionResponse models the structure of a `blockchain.transaction.get` response.
type GetTransactionResponse struct {
ID int `json:"id"`
Result string `json:"result"`
}
// BroadcastResponse models the structure of a `blockchain.transaction.broadcast` response.
type BroadcastResponse struct {
ID int `json:"id"`
Result string `json:"result"`
}
// UnspentRef models an item in the `ListUnspentResponse` results.
type UnspentRef struct {
TxHash string `json:"tx_hash"`
TxPos int `json:"tx_pos"`
Value int64 `json:"value"`
Height int `json:"height"`
}
// ServerFeatures contains the relevant information from `ServerFeatures` results.
type ServerFeatures struct {
ID int `json:"id"`
GenesisHash string `json:"genesis_hash"`
HashFunction string `json:"hash_function"`
ServerVersion string `json:"server_version"`
ProcotolMin string `json:"protocol_min"`
ProtocolMax string `json:"protocol_max"`
Pruning int `json:"pruning"`
}
// Param is a convenience type that models an item in the `Params` array of an Request.
type Param = interface{}
// NewClient creates an initialized Client instance.
func NewClient(requireTls bool) *Client {
return &Client{
log: utils.NewLogger(defaultLoggerTag),
requireTls: requireTls,
}
}
// Connect establishes a TLS connection to an Electrum server.
func (c *Client) Connect(server string) error {
c.Disconnect()
c.log.SetTag("Electrum/" + server)
c.Server = server
c.log.Printf("Connecting")
err := c.establishConnection()
if err != nil {
c.Disconnect()
return c.log.Errorf("Connect failed: %w", err)
}
// Before calling it a day send a test request (trust me), and as we do identify the server:
err = c.identifyServer()
if err != nil {
c.Disconnect()
return c.log.Errorf("Identifying server failed: %w", err)
}
c.log.Printf("Identified as %s (%s)", c.ServerImpl, c.ProtoVersion)
return nil
}
// Disconnect cuts the connection (if connected) to the Electrum server.
func (c *Client) Disconnect() error {
if c.conn == nil {
return nil
}
c.log.Printf("Disconnecting")
err := c.conn.Close()
if err != nil {
return c.log.Errorf("Disconnect failed: %w", err)
}
c.conn = nil
return nil
}
// SupportsBatching returns whether this client can process batch requests.
func (c *Client) SupportsBatching() bool {
for _, implName := range implsWithBatching {
if strings.HasPrefix(c.ServerImpl, implName) {
return true
}
}
return false
}
// ServerVersion calls the `server.version` method and returns the [impl, protocol version] tuple.
func (c *Client) ServerVersion() ([]string, error) {
request := Request{
Method: "server.version",
Params: []Param{},
}
var response ServerVersionResponse
err := c.call(&request, &response, callTimeout)
if err != nil {
return nil, c.log.Errorf("ServerVersion failed: %w", err)
}
return response.Result, nil
}
// ServerFeatures calls the `server.features` method and returns the relevant part of the result.
func (c *Client) ServerFeatures() (*ServerFeatures, error) {
request := Request{
Method: "server.features",
Params: []Param{},
}
var response ServerFeaturesResponse
err := c.call(&request, &response, callTimeout)
if err != nil {
return nil, c.log.Errorf("ServerFeatures failed: %w", err)
}
return &response.Result, nil
}
// ServerPeers calls the `server.peers.subscribe` method and returns a list of server addresses.
func (c *Client) ServerPeers() ([]string, error) {
res, err := c.rawServerPeers()
if err != nil {
return nil, err // note that, besides I/O errors, some servers close the socket on this request
}
var peers []string
for _, entry := range res {
// Get ready for some hot casting action. Not for the faint of heart.
addr := entry.([]interface{})[1].(string)
port := entry.([]interface{})[2].([]interface{})[1].(string)[1:]
peers = append(peers, addr+":"+port)
}
return peers, nil
}
// rawServerPeers calls the `server.peers.subscribe` method and returns this monstrosity:
//
// [ "<ip>", "<domain>", ["<version>", "s<SSL port>", "t<TLS port>"] ]
//
// Ports can be in any order, or absent if the protocol is not supported
func (c *Client) rawServerPeers() ([]interface{}, error) {
request := Request{
Method: "server.peers.subscribe",
Params: []Param{},
}
var response ServerPeersResponse
err := c.call(&request, &response, callTimeout)
if err != nil {
return nil, c.log.Errorf("rawServerPeers failed: %w", err)
}
return response.Result, nil
}
// Broadcast calls the `blockchain.transaction.broadcast` endpoint and returns the transaction hash.
func (c *Client) Broadcast(rawTx string) (string, error) {
request := Request{
Method: "blockchain.transaction.broadcast",
Params: []Param{rawTx},
}
var response BroadcastResponse
err := c.call(&request, &response, callTimeout)
if err != nil {
return "", c.log.Errorf("Broadcast failed: %w", err)
}
return response.Result, nil
}
// GetTransaction calls the `blockchain.transaction.get` endpoint and returns the transaction hex.
func (c *Client) GetTransaction(txID string) (string, error) {
request := Request{
Method: "blockchain.transaction.get",
Params: []Param{txID},
}
var response GetTransactionResponse
err := c.call(&request, &response, callTimeout)
if err != nil {
return "", c.log.Errorf("GetTransaction failed: %w", err)
}
return response.Result, nil
}
// ListUnspent calls `blockchain.scripthash.listunspent` and returns the UTXO results.
func (c *Client) ListUnspent(indexHash string) ([]UnspentRef, error) {
request := Request{
Method: "blockchain.scripthash.listunspent",
Params: []Param{indexHash},
}
var response ListUnspentResponse
err := c.call(&request, &response, callTimeout)
if err != nil {
return nil, c.log.Errorf("ListUnspent failed: %w", err)
}
return response.Result, nil
}
// ListUnspentBatch is like `ListUnspent`, but using batching.
func (c *Client) ListUnspentBatch(indexHashes []string) ([][]UnspentRef, error) {
requests := make([]*Request, len(indexHashes))
method := "blockchain.scripthash.listunspent"
for i, indexHash := range indexHashes {
requests[i] = &Request{
Method: method,
Params: []Param{indexHash},
}
}
var responses []ListUnspentResponse
// Give it a little more time than non-batch calls
timeout := callTimeout * 2
err := c.callBatch(method, requests, &responses, timeout)
if err != nil {
return nil, fmt.Errorf("ListUnspentBatch failed: %w", err)
}
// Don't forget to sort responses:
sort.Slice(responses, func(i, j int) bool {
return responses[i].ID < responses[j].ID
})
// Now we can collect all results:
var unspentRefs [][]UnspentRef
for _, response := range responses {
unspentRefs = append(unspentRefs, response.Result)
}
return unspentRefs, nil
}
func (c *Client) establishConnection() error {
// We first try to connect over TCP+TLS
// If we fail and requireTls is false, we try over TCP
// TODO: check if insecure is necessary
config := &tls.Config{
InsecureSkipVerify: true,
}
dialer := &net.Dialer{
Timeout: connectionTimeout,
}
tlsConn, err := tls.DialWithDialer(dialer, "tcp", c.Server, config)
if err == nil {
c.conn = tlsConn
return nil
}
if c.requireTls {
return err
}
conn, err := net.DialTimeout("tcp", c.Server, connectionTimeout)
if err != nil {
return err
}
c.conn = conn
return nil
}
func (c *Client) identifyServer() error {
serverVersion, err := c.ServerVersion()
if err != nil {
return err
}
c.ServerImpl = serverVersion[0]
c.ProtoVersion = serverVersion[1]
c.log.Printf("Identified %s %s", c.ServerImpl, c.ProtoVersion)
return nil
}
// IsConnected returns whether this client is connected to a server.
// It does not guarantee the next request will succeed.
func (c *Client) IsConnected() bool {
return c.conn != nil
}
// call executes a request with JSON marshalling, and loads the response into a pointer.
func (c *Client) call(request *Request, response interface{}, timeout time.Duration) error {
// Assign a fresh request ID:
request.ID = c.incRequestID()
// Serialize the request:
requestBytes, err := json.Marshal(request)
if err != nil {
return c.log.Errorf("Marshal failed %v: %w", request, err)
}
// Make the call, obtain the serialized response:
responseBytes, err := c.callRaw(request.Method, requestBytes, timeout)
if err != nil {
return c.log.Errorf("Send failed %s: %w", request.Method, err)
}
// Deserialize into an error, to see if there's any:
var maybeErrorResponse ErrorResponse
err = json.Unmarshal(responseBytes, &maybeErrorResponse)
if err != nil {
return c.log.Errorf("Unmarshal of potential error failed: %s %w", request.Method, err)
}
if maybeErrorResponse.Error != nil {
return c.log.Errorf("Electrum error: %v", maybeErrorResponse.Error)
}
// Deserialize the response:
err = json.Unmarshal(responseBytes, response)
if err != nil {
return c.log.Errorf("Unmarshal failed %s: %w", string(responseBytes), err)
}
return nil
}
// call executes a batch request with JSON marshalling, and loads the response into a pointer.
// Response may not match request order, so callers MUST sort them by ID.
func (c *Client) callBatch(
method string, requests []*Request, response interface{}, timeout time.Duration,
) error {
// Assign fresh request IDs:
for _, request := range requests {
request.ID = c.incRequestID()
}
// Serialize the request:
requestBytes, err := json.Marshal(requests)
if err != nil {
return c.log.Errorf("Marshal failed %v: %w", requests, err)
}
// Make the call, obtain the serialized response:
responseBytes, err := c.callRaw(method, requestBytes, timeout)
if err != nil {
return c.log.Errorf("Send failed %s: %w", method, err)
}
// Deserialize into an array of errors, to see if there's any:
var maybeErrorResponses []ErrorResponse
err = json.Unmarshal(responseBytes, &maybeErrorResponses)
if err != nil {
return c.log.Errorf("Unmarshal of potential error failed: %s %w", string(responseBytes), err)
}
// Walk the responses, returning the first error found:
for _, maybeErrorResponse := range maybeErrorResponses {
if maybeErrorResponse.Error != nil {
return c.log.Errorf("Electrum error: %v", maybeErrorResponse.Error)
}
}
// Deserialize the response:
err = json.Unmarshal(responseBytes, response)
if err != nil {
return c.log.Errorf("Unmarshal failed %s: %w", string(responseBytes), err)
}
return nil
}
// callRaw sends a raw request in bytes, and returns a raw response (or an error).
func (c *Client) callRaw(method string, request []byte, timeout time.Duration) ([]byte, error) {
c.log.Printf("Sending %s request", method)
c.log.Tracef("Sending %s body: %s", method, string(request))
if !c.IsConnected() {
return nil, c.log.Errorf("Send failed %s: not connected", method)
}
request = append(request, messageDelim)
start := time.Now()
// SetDeadline is an absolute time based timeout. That is, we set an exact
// time we want it to fail.
var deadline time.Time
if timeout == noTimeout {
// This means no deadline
deadline = time.Time{}
} else {
deadline = start.Add(timeout)
}
err := c.conn.SetDeadline(deadline)
if err != nil {
return nil, c.log.Errorf("Send failed %s: SetDeadline failed", method)
}
_, err = c.conn.Write(request)
if err != nil {
duration := time.Now().Sub(start)
return nil, c.log.Errorf("Send failed %s after %vms: %w", method, duration.Milliseconds(), err)
}
reader := bufio.NewReader(c.conn)
response, err := reader.ReadBytes(messageDelim)
duration := time.Now().Sub(start)
if err != nil {
return nil, c.log.Errorf("Receive failed %s after %vms: %w", method, duration.Milliseconds(), err)
}
c.log.Printf("Received %s after %vms", method, duration.Milliseconds())
c.log.Tracef("Received %s: %s", method, string(response))
return response, nil
}
func (c *Client) incRequestID() int {
c.nextRequestID++
return c.nextRequestID
}
// GetIndexHash returns the script parameter to use with Electrum, given a Bitcoin address.
func GetIndexHash(script []byte) string {
indexHash := sha256.Sum256(script)
reverse(&indexHash)
return hex.EncodeToString(indexHash[:])
}
// reverse the order of the provided byte array, in place.
func reverse(a *[32]byte) {
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
}

View File

@@ -0,0 +1,28 @@
package electrum
// Pool provides a shared pool of Clients that callers can acquire and release, limiting
// the amount of concurrent Clients in active use.
type Pool struct {
nextClient chan *Client
}
// NewPool creates an initialized Pool with a `size` number of clients.
func NewPool(size int, requireTls bool) *Pool {
nextClient := make(chan *Client, size)
for i := 0; i < size; i++ {
nextClient <- NewClient(requireTls)
}
return &Pool{nextClient}
}
// Acquire obtains an unused Client, blocking until one is released.
func (p *Pool) Acquire() <-chan *Client {
return p.nextClient
}
// Release returns a Client to the pool, unblocking the next caller trying to `Acquire()`.
func (p *Pool) Release(client *Client) {
p.nextClient <- client
}

View File

@@ -0,0 +1,105 @@
package electrum
import "sync/atomic"
// ServerProvider manages a rotating server list, from which callers can pull server addresses.
type ServerProvider struct {
nextIndex int32
servers []string
}
// NewServerProvider returns an initialized ServerProvider.
func NewServerProvider(servers []string) *ServerProvider {
return &ServerProvider{
nextIndex: -1,
servers: servers,
}
}
// NextServer returns an address from the rotating list. It's thread-safe.
func (p *ServerProvider) NextServer() string {
index := int(atomic.AddInt32(&p.nextIndex, 1))
return p.servers[index%len(p.servers)]
}
// PublicServers list.
//
// This list was taken from Electrum repositories, keeping TLS servers and excluding onion URIs.
// It was then sorted into sections using the `cmd/survey` program, to prioritize the more reliable
// servers with batch support.
//
// See https://github.com/spesmilo/electrum/blob/master/electrum/servers.json
// See https://github.com/kyuupichan/electrumx/blob/master/electrumx/lib/coins.py
// See `cmd/survey/main.go`
var PublicServers = []string{
// Fast servers with batching
"electrum.coinext.com.br:50002", // impl: ElectrumX 1.14.0, batching: true, ttc: 0.15, speed: 113, from:
"fulcrum.sethforprivacy.com:50002", // impl: Fulcrum 1.9.0, batching: true, ttc: 0.55, speed: 96, from:
"mainnet.foundationdevices.com:50002", // impl: Fulcrum 1.8.2, batching: true, ttc: 0.54, speed: 88, from:
"btc.lastingcoin.net:50002", // impl: Fulcrum 1.7.0, batching: true, ttc: 0.74, speed: 73, from:
"vmd71287.contaboserver.net:50002", // impl: Fulcrum 1.9.0, batching: true, ttc: 0.60, speed: 70, from:
"de.poiuty.com:50002", // impl: Fulcrum 1.8.2, batching: true, ttc: 0.87, speed: 70, from:
"electrum.jochen-hoenicke.de:50006", // impl: Fulcrum 1.8.2, batching: true, ttc: 0.83, speed: 69, from:
"btc.cr.ypto.tech:50002", // impl: Fulcrum 1.9.0, batching: true, ttc: 0.81, speed: 65, from:
"e.keff.org:50002", // impl: Fulcrum 1.8.2, batching: true, ttc: 0.82, speed: 65, from:
"vmd104014.contaboserver.net:50002", // impl: Fulcrum 1.9.0, batching: true, ttc: 0.58, speed: 64, from:
"e2.keff.org:50002", // impl: Fulcrum 1.8.2, batching: true, ttc: 0.83, speed: 64, from:
"fulcrum.grey.pw:51002", // impl: Fulcrum 1.9.0, batching: true, ttc: 0.81, speed: 63, from:
"fortress.qtornado.com:443", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.84, speed: 62, from:
"f.keff.org:50002", // impl: Fulcrum 1.8.2, batching: true, ttc: 0.89, speed: 62, from:
"2ex.digitaleveryware.com:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.71, speed: 61, from:
"electrum.petrkr.net:50002", // impl: Fulcrum 1.9.0, batching: true, ttc: 0.84, speed: 58, from:
"electrum.stippy.com:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.80, speed: 57, from:
"electrum0.snel.it:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.80, speed: 56, from:
"ru.poiuty.com:50002", // impl: Fulcrum 1.8.2, batching: true, ttc: 0.99, speed: 56, from:
"electrum.privateservers.network:50002", // impl: ElectrumX 1.15.0, batching: true, ttc: 0.85, speed: 49, from:
"btc.electroncash.dk:60002", // impl: Fulcrum 1.9.0, batching: true, ttc: 0.92, speed: 48, from:
"bitcoin.aranguren.org:50002", // impl: Fulcrum 1.8.2, batching: true, ttc: 1.19, speed: 48, from:
"electrum.bitcoinserver.nl:50514", // impl: Fulcrum 1.8.1, batching: true, ttc: 0.85, speed: 44, from:
"btc.prompt.cash:61002", // impl: Fulcrum 1.8.1, batching: true, ttc: 1.22, speed: 44, from:
"fulc.bot.nu:50002", // impl: Fulcrum 1.7.0, batching: true, ttc: 1.04, speed: 35, from:
"bolt.schulzemic.net:50002", // impl: Fulcrum 1.8.2, batching: true, ttc: 0.96, speed: 33, from:
"node1.btccuracao.com:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.90, speed: 25, from:
// Other servers
"xtrum.com:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.91, speed: 19, from: fulcrum.sethforprivacy.com:50002
"electrum.bitaroo.net:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.04, speed: 19, from:
"btce.iiiiiii.biz:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.07, speed: 19, from: electrum.coinext.com.br:50002
"electrum.emzy.de:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.17, speed: 19, from:
"alviss.coinjoined.com:50002", // impl: ElectrumX 1.15.0, batching: true, ttc: 1.15, speed: 17, from:
"2AZZARITA.hopto.org:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.81, speed: 16, from: electrum.coinext.com.br:50002
"vmd104012.contaboserver.net:50002", // impl: Fulcrum 1.9.0, batching: true, ttc: 1.25, speed: 16, from:
"electrum.bitcoinlizard.net:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 4.99, speed: 14, from:
"btc.ocf.sh:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.19, speed: 12, from:
"bitcoins.sk:56002", // impl: ElectrumX 1.14.0, batching: true, ttc: 0.80, speed: 11, from:
"electrum-btc.leblancnet.us:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.04, speed: 11, from:
"helicarrier.bauerj.eu:50002", // impl: ElectrumX 1.10.0, batching: true, ttc: 0.96, speed: 10, from:
"electrum.neocrypto.io:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.69, speed: 7, from:
"caleb.vegas:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.71, speed: 7, from:
"smmalis37.ddns.net:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.77, speed: 7, from:
"2azzarita.hopto.org:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.78, speed: 7, from: fulcrum.sethforprivacy.com:50002
"electrum.kendigisland.xyz:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.79, speed: 7, from:
"electrum.hsmiths.com:50002", // impl: ElectrumX 1.10.0, batching: true, ttc: 0.90, speed: 7, from:
"vmd63185.contaboserver.net:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.92, speed: 7, from:
"blkhub.net:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.94, speed: 7, from:
"electrum.mmitech.info:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.95, speed: 7, from:
"elx.bitske.com:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.17, speed: 7, from:
"bitcoin.lu.ke:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.21, speed: 7, from:
"ex05.axalgo.com:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.21, speed: 7, from:
"walle.dedyn.io:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.23, speed: 7, from:
"eai.coincited.net:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.34, speed: 6, from:
"2electrumx.hopto.me:56022", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.93, speed: 6, from:
"hodlers.beer:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.03, speed: 6, from:
"kareoke.qoppa.org:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.13, speed: 6, from:
"ASSUREDLY.not.fyi:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.85, speed: 4, from:
"electrumx.alexridevski.net:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.87, speed: 4, from:
"assuredly.not.fyi:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 0.90, speed: 4, from:
"ragtor.duckdns.org:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.07, speed: 4, from:
"surely.not.fyi:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.20, speed: 4, from:
"btc.electrum.bitbitnet.net:50002", // impl: ElectrumX 1.15.0, batching: true, ttc: 0.71, speed: 3, from:
"gods-of-rock.screaminglemur.net:50002", // impl: ElectrumX 1.15.0, batching: true, ttc: 0.92, speed: 3, from:
"SURELY.not.fyi:50002", // impl: ElectrumX 1.16.0, batching: true, ttc: 1.35, speed: 3, from:
"horsey.cryptocowboys.net:50002", // impl: ElectrumX 1.15.0, batching: true, ttc: 0.61, speed: 2, from:
"electrumx-btc.cryptonermal.net:50002", // impl: ElectrumX 1.15.0, batching: true, ttc: 0.83, speed: 1, from:
"electrum.coineuskal.com:50002", // impl: ElectrumX 1.15.0, batching: true, ttc: 1.73, speed: 0, from: electrum.coinext.com.br:50002
}

14
recovery_tool/go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/muun/recovery
go 1.12
require (
github.com/btcsuite/btcd v0.21.0-beta
github.com/btcsuite/btcutil v1.0.2
github.com/gookit/color v1.4.2
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

388
recovery_tool/go.sum Normal file
View File

@@ -0,0 +1,388 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
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/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46/go.mod h1:Yktc19YNjh/Iz2//CX0vfRTS4IJKM/RKO5YZ9Fn+Pgo=
github.com/btcsuite/btcd v0.21.0-beta h1:At9hIZdJW0s9E/fAz28nrz6AmcNlSVucCH796ZteX1M=
github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94=
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/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts=
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
github.com/btcsuite/btcutil/psbt v1.0.2 h1:gCVY3KxdoEVU7Q6TjusPO+GANIwVgr9yTLqM+a6CZr8=
github.com/btcsuite/btcutil/psbt v1.0.2/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ=
github.com/btcsuite/btcwallet v0.11.1-0.20200612012534-48addcd5591a h1:AZ1Mf0gd9mgJqrTTIFUc17ep9EKUbQusVAIzJ6X+x3Q=
github.com/btcsuite/btcwallet v0.11.1-0.20200612012534-48addcd5591a/go.mod h1:9+AH3V5mcTtNXTKe+fe63fDLKGOwQbZqmvOVUef+JFE=
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/go.mod h1:bZTy9RyYZh9fLnSua+/CD48TJtYJSHjjYcSaszuxCCk=
github.com/btcsuite/btcwallet/walletdb v1.3.1/go.mod h1:9cwc1Yyg4uvd4ZdfdoMnALji+V9gfWSMfxEdLdR5Vwc=
github.com/btcsuite/btcwallet/walletdb v1.3.2/go.mod h1:GZCMPNpUu5KE3ASoVd+k06p/1OW8OwNGCCaNWRto2cQ=
github.com/btcsuite/btcwallet/walletdb v1.3.3 h1:u6e7vRIKBF++cJy+hOHaMGg+88ZTwvpaY27AFvtB668=
github.com/btcsuite/btcwallet/walletdb v1.3.3/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU=
github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY=
github.com/btcsuite/btcwallet/wtxmgr v1.2.0 h1:ZUYPsSv8GjF9KK7lboB2OVHF0uYEcHxgrCfFWqPd9NA=
github.com/btcsuite/btcwallet/wtxmgr v1.2.0/go.mod h1:h8hkcKUE3X7lMPzTUoGnNiw5g7VhGrKEW3KpR2r0VnY=
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/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc=
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 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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
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/decred/dcrd/lru v1.0.0 h1:Kbsb1SFDsIlaupWPwsPp+dkxiBY1frcS07PCPgotKz8=
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
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/fiatjaf/go-lnurl v1.3.1 h1:9Qn4n1ZyzTMW/YuVX2Wr9cE+LEAzpE1hrCbxVK/yBKE=
github.com/fiatjaf/go-lnurl v1.3.1/go.mod h1:BqA8WXAOzntF7Z3EkVO7DfP4y5rhWUmJ/Bu9KBke+rs=
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
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/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.8.6/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 h1:1yY/RQWNSBjJe2GDCIYoLmpWVidrooriUr4QS/zaATQ=
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 h1:o1wMw7uTNyA58IlEdDpxIrtFHTgnvYzA8sCQz8luv94=
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7/go.mod h1:WkUxfS2JUu3qPo6tRld7ISb8HiC0gVSU91kooBMDVok=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
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 v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
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/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
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/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
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/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
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/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc=
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d/go.mod h1:KDb67YMzoh4eudnzClmvs2FbiLG9vxISmLApUkCa4uI=
github.com/lightningnetwork/lightning-onion v1.0.1 h1:qChGgS5+aPxFeR6JiUsGvanei1bn6WJpYbvosw/1604=
github.com/lightningnetwork/lightning-onion v1.0.1/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4=
github.com/lightningnetwork/lnd v0.10.4-beta h1:Af2zOCPePeaU8Tkl8IqtTjr4BP3zYfi+hAtQYcCMM58=
github.com/lightningnetwork/lnd v0.10.4-beta/go.mod h1:4d02pduRVtZwgTJ+EimKJTsEAY0jDwi0SPE9h5aRneM=
github.com/lightningnetwork/lnd/cert v1.0.2/go.mod h1:fmtemlSMf5t4hsQmcprSoOykypAPp+9c+0d0iqTScMo=
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/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms=
github.com/lightningnetwork/lnd/queue v1.0.4 h1:8Dq3vxAFSACPy+pKN88oPFhuCpCoAAChPBwa4BJxH4k=
github.com/lightningnetwork/lnd/queue v1.0.4/go.mod h1:YTkTVZCxz8tAYreH27EO3s8572ODumWrNdYW2E/YKxg=
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/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw=
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY=
github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/muun/libwallet v0.11.0 h1:P3WdPhoUhO5T/jNu+2DWZPJoaKjCR3MZLahYjnKxnWM=
github.com/muun/libwallet v0.11.0/go.mod h1:zZDt5CpHETILSCYrRsb3r7Fl278hoU4gdR96jFyqyR8=
github.com/muun/neutrino v0.0.0-20190914162326-7082af0fa257 h1:NW17wq2gZlEFeW3/Zx3wSmqlD0wKGf7YvhpP+CNCsbE=
github.com/muun/neutrino v0.0.0-20190914162326-7082af0fa257/go.mod h1:awTrhbCWjWNH4yVwZ4IE7nZbvpQ27e7OyD+jao7wRxA=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
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=
github.com/pdfcpu/pdfcpu v0.3.11 h1:T5XLD5blrB61tBjkSrQnwikrQO4gmwQm61fsyGZa04w=
github.com/pdfcpu/pdfcpu v0.3.11/go.mod h1:SZ51teSs9l709Xim2VEuOYGf+uf7RdH2eY0LrXvz7n8=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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/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=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY=
github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
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=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
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=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw=
gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA=
gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
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.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -0,0 +1,64 @@
package main
import (
"fmt"
"github.com/muun/libwallet"
"github.com/muun/libwallet/emergencykit"
)
var defaultNetwork = libwallet.Mainnet()
func decodeKeysFromInput(rawKey1 string, rawKey2 string) ([]*libwallet.EncryptedPrivateKeyInfo, error) {
key1, err := libwallet.DecodeEncryptedPrivateKey(rawKey1)
if err != nil {
return nil, fmt.Errorf("failed to decode first key: %w", err)
}
key2, err := libwallet.DecodeEncryptedPrivateKey(rawKey2)
if err != nil {
return nil, fmt.Errorf("failed to decode second key: %w", err)
}
return []*libwallet.EncryptedPrivateKeyInfo{key1, key2}, nil
}
func decodeKeysFromMetadata(meta *emergencykit.Metadata) ([]*libwallet.EncryptedPrivateKeyInfo, error) {
decodedKeys := make([]*libwallet.EncryptedPrivateKeyInfo, len(meta.EncryptedKeys))
for i, metaKey := range meta.EncryptedKeys {
decodedKeys[i] = &libwallet.EncryptedPrivateKeyInfo{
Version: meta.Version,
Birthday: meta.BirthdayBlock,
EphPublicKey: metaKey.DhPubKey,
CipherText: metaKey.EncryptedPrivKey,
Salt: metaKey.Salt,
}
}
return decodedKeys, nil
}
func decryptKeys(encryptedKeys []*libwallet.EncryptedPrivateKeyInfo, recoveryCode string) ([]*libwallet.DecryptedPrivateKey, error) {
// Always take the salt from the second key (the same salt was used for all keys, but our legacy
// key format did not include it in the first key):
salt := encryptedKeys[1].Salt
decryptionKey, err := libwallet.RecoveryCodeToKey(recoveryCode, salt)
if err != nil {
return nil, fmt.Errorf("failed to process recovery code: %w", err)
}
decryptedKeys := make([]*libwallet.DecryptedPrivateKey, len(encryptedKeys))
for i, encryptedKey := range encryptedKeys {
decryptedKey, err := decryptionKey.DecryptKey(encryptedKey, defaultNetwork)
if err != nil {
return nil, fmt.Errorf("failed to decrypt key %d: %w", i, err)
}
decryptedKeys[i] = decryptedKey
}
return decryptedKeys, nil
}

537
recovery_tool/main.go Normal file
View File

@@ -0,0 +1,537 @@
package main
import (
"bytes"
"flag"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"github.com/btcsuite/btcutil"
"github.com/gookit/color"
"github.com/muun/libwallet"
"github.com/muun/libwallet/btcsuitew/btcutilw"
"github.com/muun/libwallet/emergencykit"
"github.com/muun/recovery/electrum"
"github.com/muun/recovery/scanner"
"github.com/muun/recovery/utils"
)
const electrumPoolSize = 6
var debugOutputStream = bytes.NewBuffer(nil)
type config struct {
generateContacts bool
providedElectrum string
usesProvidedElectrum bool
onlyScan bool
}
func main() {
utils.SetOutputStream(debugOutputStream)
var config config
// Pick up command-line arguments:
flag.BoolVar(&config.generateContacts, "generate-contacts", false, "Generate contact addresses")
flag.StringVar(&config.providedElectrum, "electrum-server", "", "Connect to this electrum server to find funds")
flag.BoolVar(&config.onlyScan, "only-scan", false, "Only scan for UTXOs without generating a transaction")
flag.Usage = printUsage
flag.Parse()
args := flag.Args()
// Ensure correct form:
if len(args) > 1 {
printUsage()
os.Exit(0)
}
// Welcome!
printWelcomeMessage()
config.usesProvidedElectrum = len(strings.TrimSpace(config.providedElectrum)) > 0
if config.usesProvidedElectrum {
validateProvidedElectrum(config.providedElectrum)
}
// We're going to need a few things to move forward with the recovery process. Let's make a list
// so we keep them in mind:
var recoveryCode string
var encryptedKeys []*libwallet.EncryptedPrivateKeyInfo
var destinationAddress btcutil.Address
// First on our list is the Recovery Code. This is the time to go looking for that piece of paper:
recoveryCode = readRecoveryCode()
// Good! Now, on to those keys. We need to read them and decrypt them:
encryptedKeys, err := readBackupFromInputOrPDF(flag.Arg(0))
if err != nil {
exitWithError(err)
}
decryptedKeys, err := decryptKeys(encryptedKeys, recoveryCode)
if err != nil {
exitWithError(err)
}
decryptedKeys[0].Key.Path = "m/1'/1'" // a little adjustment for legacy users.
if !config.onlyScan {
// Finally, we need the destination address to sweep the funds:
destinationAddress = readAddress()
}
sayBlock(`
Starting scan of all possible addresses. This will take a few minutes.
`)
doRecovery(decryptedKeys, destinationAddress, config)
sayBlock("We appreciate all kinds of feedback. If you have any, send it to {blue contact@muun.com}\n")
}
// doRecovery runs the scan & sweep process, and returns the ID of the broadcasted transaction.
func doRecovery(
decryptedKeys []*libwallet.DecryptedPrivateKey,
destinationAddress btcutil.Address,
config config,
) {
addrGen := NewAddressGenerator(decryptedKeys[0].Key, decryptedKeys[1].Key, config.generateContacts)
var electrumProvider *electrum.ServerProvider
if config.usesProvidedElectrum {
electrumProvider = electrum.NewServerProvider([]string{
config.providedElectrum,
})
} else {
electrumProvider = electrum.NewServerProvider(electrum.PublicServers)
}
connectionPool := electrum.NewPool(electrumPoolSize, !config.usesProvidedElectrum)
utxoScanner := scanner.NewScanner(connectionPool, electrumProvider)
addresses := addrGen.Stream()
sweeper := Sweeper{
UserKey: decryptedKeys[0].Key,
MuunKey: decryptedKeys[1].Key,
Birthday: decryptedKeys[1].Birthday,
SweepAddress: destinationAddress,
}
reports := utxoScanner.Scan(addresses)
say("► {white Finding servers...}")
var lastReport *scanner.Report
for lastReport = range reports {
printReport(lastReport)
}
fmt.Println()
fmt.Println()
if lastReport.Err != nil {
exitWithError(fmt.Errorf("error while scanning addresses: %w", lastReport.Err))
}
say("{green ✓ Scan complete}\n")
utxos := lastReport.UtxosFound
if len(utxos) == 0 {
sayBlock("No funds were discovered\n\n")
return
}
var total int64
for _, utxo := range utxos {
total += utxo.Amount
say("• {white %d} sats in %s\n", utxo.Amount, utxo.Address.Address())
}
say("\n— {white %d} sats total\n", total)
if config.onlyScan {
return
}
txOutputAmount, txWeightInBytes, err := sweeper.GetSweepTxAmountAndWeightInBytes(utxos)
if err != nil {
exitWithError(err)
}
fee := readFee(txOutputAmount, txWeightInBytes)
// Then we re-build the sweep tx with the actual fee
sweepTx, err := sweeper.BuildSweepTx(utxos, fee)
if err != nil {
exitWithError(err)
}
sayBlock("Sending transaction...")
err = sweeper.BroadcastTx(sweepTx)
if err != nil {
exitWithError(err)
}
sayBlock(`
Transaction sent! You can check the status here: https://mempool.space/tx/%v
(it will appear in mempool.space after a short delay)
`, sweepTx.TxHash().String())
}
func validateProvidedElectrum(providedElectrum string) {
client := electrum.NewClient(false)
err := client.Connect(providedElectrum)
defer func(client *electrum.Client) {
_ = client.Disconnect()
}(client)
if err != nil {
sayBlock(`
{red Error!}
The Recovery Tool couldn't connect to the provided Electrum server %v.
If the problem persists, contact {blue support@muun.com}.
――― {white error report} ―――
%v
――――――――――――――――――――
We're always there to help.
`, providedElectrum, err)
os.Exit(2)
}
}
func exitWithError(err error) {
sayBlock(`
{red Error!}
The Recovery Tool encountered a problem. Please, try again.
If the problem persists, contact {blue support@muun.com} and include the file
called error_log you can find in the same folder as this tool.
――― {white error report} ―――
%v
――――――――――――――――――――
We're always there to help.
`, err)
// Ensure we always log the error in the file
_ = utils.NewLogger("").Errorf("exited with error: %s", err.Error())
_ = os.WriteFile("error_log", debugOutputStream.Bytes(), 0600)
os.Exit(1)
}
func printWelcomeMessage() {
say(`
{blue Muun Recovery Tool v%s}
To recover your funds, you will need:
1. {yellow Your Recovery Code}, which you wrote down during your security setup
2. {yellow Your Emergency Kit PDF}, which you exported from the app
3. {yellow Your destination bitcoin address}, where all your funds will be sent
If you have any questions, we'll be happy to answer them. Contact us at {blue support@muun.com}
`, version)
}
func printUsage() {
fmt.Println("Usage: recovery-tool [optional: path to Emergency Kit PDF]")
flag.PrintDefaults()
}
func printReport(report *scanner.Report) {
if utils.DebugMode {
return // don't print reports while debugging, there's richer information in the logs
}
var total int64
for _, utxo := range report.UtxosFound {
total += utxo.Amount
}
say("\r► {white Scanned addresses}: %d | {white Sats found}: %d", report.ScannedAddresses, total)
}
func readRecoveryCode() string {
sayBlock(`
{yellow Enter your Recovery Code}
(it looks like this: 'ABCD-1234-POW2-R561-P120-JK26-12RW-45TT')
`)
var userInput string
ask(&userInput)
userInput = strings.TrimSpace(userInput)
finalRC := strings.ToUpper(userInput)
if strings.Count(finalRC, "-") != 7 {
say(`
Invalid recovery code. Did you add the '-' separator between each 4-characters segment?
Please, try again
`)
return readRecoveryCode()
}
if len(finalRC) != 39 {
say(`
Your recovery code must have 39 characters
Please, try again
`)
return readRecoveryCode()
}
return finalRC
}
func readBackupFromInputOrPDF(optionalPDF string) ([]*libwallet.EncryptedPrivateKeyInfo, error) {
// Here we have two possible flows, depending on whether the PDF was provided (pick up the
// encrypted backup automatically) or not (manual input). If we try for the automatic flow and fail,
// we can fall back to the manual one.
// Read metadata from the PDF, if given:
if optionalPDF != "" {
encryptedKeys, err := readBackupFromPDF(optionalPDF)
if err == nil {
return encryptedKeys, nil
}
// Hmm. Okay, we'll confess and fall back to manual input.
say(`
Couldn't read the PDF automatically: %v
Please, enter your data manually
`, err)
}
// Ask for manual input, if we have no PDF or couldn't read it:
encryptedKeys, err := readBackupFromInput()
if err != nil {
return nil, err
}
return encryptedKeys, nil
}
func readBackupFromInput() ([]*libwallet.EncryptedPrivateKeyInfo, error) {
firstRawKey := readKey("first encrypted private key")
secondRawKey := readKey("second encrypted private key")
decodedKeys, err := decodeKeysFromInput(firstRawKey, secondRawKey)
if err != nil {
return nil, err
}
return decodedKeys, nil
}
func readBackupFromPDF(path string) ([]*libwallet.EncryptedPrivateKeyInfo, error) {
reader := &emergencykit.MetadataReader{SrcFile: path}
metadata, err := reader.ReadMetadata()
if err != nil {
return nil, err
}
decodedKeys, err := decodeKeysFromMetadata(metadata)
if err != nil {
return nil, err
}
return decodedKeys, nil
}
func readKey(keyType string) string {
sayBlock(`
{yellow Enter your %v}
(it looks like this: '9xzpc7y6sNtRvh8Fh...')
`, keyType)
// NOTE:
// Users will most likely copy and paste their keys from the Emergency Kit PDF. In this case,
// input will come suddenly in multiple lines, so a simple scan & retry (let's say 3 lines
// were pasted) will attempt to parse a key and fail 2 times in a row, with leftover characters
// until the user presses enter to fail for a 3rd time.
// Given the line lengths actually found in our Emergency Kits, we have a simple solution for now:
// scan a minimum length of characters. Pasing from current versions of the Emergency Kit will
// only go past a minimum length when the key being entered is complete, in all cases.
userInput := askMultiline(libwallet.EncodedKeyLengthLegacy)
if len(userInput) < libwallet.EncodedKeyLengthLegacy {
// This is obviously invalid. Other problems will be detected later on, during the actual
// decoding and decryption stage.
say(`
The key you entered doesn't look valid
Please, try again
`)
return readKey(keyType)
}
return userInput
}
func readAddress() btcutil.Address {
sayBlock(`
{yellow Enter your destination bitcoin address}
`)
var userInput string
ask(&userInput)
userInput = strings.TrimSpace(userInput)
addr, err := btcutilw.DecodeAddress(userInput, &chainParams)
if err != nil {
say(`
This is not a valid bitcoin address
Please, try again
`)
return readAddress()
}
return addr
}
func readFee(totalBalance, weight int64) int64 {
sayBlock(`
{yellow Enter the fee rate (sats/byte)}
Your transaction weighs %v bytes. You can get suggestions in https://mempool.space/ under "Transaction fees".
`, weight)
var userInput string
ask(&userInput)
feeInSatsPerByte, err := strconv.ParseInt(userInput, 10, 64)
if err != nil || feeInSatsPerByte <= 0 {
say(`
The fee must be a whole number
Please, try again
`)
return readFee(totalBalance, weight)
}
totalFee := feeInSatsPerByte * weight
if totalBalance-totalFee < 546 {
say(`
The fee is too high. The remaining amount after deducting is too low to send.
Please, try again
`)
return readFee(totalBalance, weight)
}
return totalFee
}
func readConfirmation(value, fee int64, address string) {
sayBlock(`
{whiteUnderline Summary}
{white Amount}: %v sats
{white Fee}: %v sats
{white Destination}: %v
{yellow Confirm?} (y/n)
`, value, fee, address)
var userInput string
ask(&userInput)
if userInput == "y" || userInput == "Y" {
return
}
if userInput == "n" || userInput == "N" {
sayBlock(`
Recovery tool stopped
You can try again or contact us at {blue support@muun.com}
`)
os.Exit(1)
}
say(`You can only enter 'y' to confirm or 'n' to cancel`)
fmt.Print("\n\n")
readConfirmation(value, fee, address)
}
var leadingIndentRe = regexp.MustCompile("^[ \t]+")
var colorRe = regexp.MustCompile(`\{(\w+?) ([^\}]+?)\}`)
func say(message string, v ...interface{}) {
noEmptyLine := strings.TrimLeft(message, " \n")
firstIndent := leadingIndentRe.FindString(noEmptyLine)
noIndent := strings.ReplaceAll(noEmptyLine, firstIndent, "")
noTrailingSpace := strings.TrimRight(noIndent, " \t")
withColors := colorRe.ReplaceAllStringFunc(noTrailingSpace, func(match string) string {
groups := colorRe.FindStringSubmatch(match)
return applyColor(groups[1], groups[2])
})
fmt.Printf(withColors, v...)
}
func sayBlock(message string, v ...interface{}) {
fmt.Println()
say(message, v...)
}
func applyColor(colorName string, text string) string {
switch colorName {
case "red":
return color.New(color.FgRed, color.BgDefault, color.OpBold).Sprint(text)
case "blue":
return color.New(color.FgBlue, color.BgDefault, color.OpBold).Sprint(text)
case "yellow":
return color.New(color.FgYellow, color.BgDefault, color.OpBold).Sprint(text)
case "green":
return color.New(color.FgGreen, color.BgDefault, color.OpBold).Sprint(text)
case "white":
return color.New(color.FgWhite, color.BgDefault, color.OpBold).Sprint(text)
case "whiteUnderline":
return color.New(color.FgWhite, color.BgDefault, color.OpBold, color.OpUnderscore).Sprint(text)
}
panic("No such color: " + colorName)
}
func askMultiline(minChars int) string {
fmt.Print("➜ ")
var result strings.Builder
for result.Len() < minChars {
var line string
fmt.Scan(&line)
result.WriteString(strings.TrimSpace(line))
}
return result.String()
}
func ask(result *string) {
fmt.Print("➜ ")
fmt.Scan(result)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -0,0 +1,505 @@
# This is a Terminalizer (https://github.com/faressoft/terminalizer) configuration file.
# Render with:
# $ terminalizer render demo.yml -o demo-unoptimized.gif
# Make background transparent with:
# https://onlinegiftools.com/create-transparent-gif (use white, 1%)
# Optimize with:
# $ gifsicle --optimize=3 --colors=32 -i demo-unoptimized.gif -o demo.gif
config:
env:
recording: true
rows: 24
cols: 100
repeat: 0
quality: 100
frameDelay: auto
maxIdleTime: auto
frameBox:
type: solid
title: null
style:
border: 0px black solid
boxShadow: none
watermark:
imagePath: null
style:
position: absolute
right: 15px
bottom: 15px
opacity: 0.9
cursorStyle: block
fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace"
fontSize: 12
lineHeight: 1
letterSpacing: 0
theme:
background: "transparent"
foreground: "#afafaf"
cursor: "#c7c7c7"
black: "#232628"
red: "#fc4384"
green: "#b3e33b"
yellow: "#ffa727"
blue: "#75dff2"
magenta: "#ae89fe"
cyan: "#708387"
white: "#d5d5d0"
brightBlack: "#626566"
brightRed: "#ff7fac"
brightGreen: "#c8ed71"
brightYellow: "#ebdf86"
brightBlue: "#75dff2"
brightMagenta: "#ae89fe"
brightCyan: "#b1c6ca"
brightWhite: "#f9f9f4"
records:
- delay: 100
content: "\e[1;32mrecovery\e[00m$ "
- delay: "600"
content: "."
- delay: "20"
content: "/"
- delay: "20"
content: "r"
- delay: "20"
content: "e"
- delay: "20"
content: "c"
- delay: "20"
content: "o"
- delay: "20"
content: "v"
- delay: "20"
content: "e"
- delay: "20"
content: "r"
- delay: "20"
content: "y"
- delay: "20"
content: "-"
- delay: "20"
content: "t"
- delay: "20"
content: "o"
- delay: "20"
content: "o"
- delay: "20"
content: "l"
- delay: "20"
content: " "
- delay: "500"
content: "\e[1;37m~"
- delay: "20"
content: "/"
- delay: "20"
content: "d"
- delay: "20"
content: "o"
- delay: "20"
content: "w"
- delay: "20"
content: "n"
- delay: "20"
content: "l"
- delay: "20"
content: "o"
- delay: "20"
content: "a"
- delay: "20"
content: "d"
- delay: "20"
content: "s"
- delay: "20"
content: "/"
- delay: "20"
content: "M"
- delay: "20"
content: "u"
- delay: "20"
content: "u"
- delay: "20"
content: "n"
- delay: "20"
content: "-"
- delay: "20"
content: "E"
- delay: "20"
content: "m"
- delay: "20"
content: "e"
- delay: "20"
content: "r"
- delay: "20"
content: "g"
- delay: "20"
content: "e"
- delay: "20"
content: "n"
- delay: "20"
content: "c"
- delay: "20"
content: "y"
- delay: "20"
content: "-"
- delay: "20"
content: "K"
- delay: "20"
content: "i"
- delay: "20"
content: "t"
- delay: "20"
content: "."
- delay: "20"
content: "p"
- delay: "20"
content: "d"
- delay: "20"
content: "f"
- delay: 1200
content: "\e[0m\r\n\n"
- delay: 500
content: "\e[1;34mMuun Recovery Tool v2.1.0\e[0m\r\n\r\nTo recover your funds, you will need:\r\n\r\n1. \e[1;33mYour Recovery Code\e[0m, which you wrote down during your security setup\r\n2. \e[1;33mYour Emergency Kit PDF\e[0m, which you exported from the app\r\n3. \e[1;33mYour destination bitcoin address\e[0m, where all your funds will be sent\r\n\r\nIf you have any questions, we'll be happy to answer them. Contact us at \e[1;34msupport@muun.com\e[0m\r\n\r\n\e[1;33mEnter your Recovery Code\e[0m\r\n(it looks like this: 'ABCD-1234-POW2-R561-P120-JK26-12RW-45TT')\r\n➜ "
- delay: 2500
content: "L"
- delay: "20"
content: "A"
- delay: "20"
content: "8"
- delay: "20"
content: "H"
- delay: "20"
content: "-"
- delay: "20"
content: "D"
- delay: "20"
content: "B"
- delay: "20"
content: "9"
- delay: "20"
content: "M"
- delay: "20"
content: "-"
- delay: "20"
content: "E"
- delay: "20"
content: "J"
- delay: "20"
content: "R"
- delay: "20"
content: "P"
- delay: "20"
content: "-"
- delay: "20"
content: "Y"
- delay: "20"
content: "J"
- delay: "20"
content: "R"
- delay: "20"
content: "Q"
- delay: "20"
content: "-"
- delay: "20"
content: "B"
- delay: "20"
content: "W"
- delay: "20"
content: "T"
- delay: "20"
content: "4"
- delay: "20"
content: "-"
- delay: "20"
content: "L"
- delay: "20"
content: "J"
- delay: "20"
content: "9"
- delay: "20"
content: "B"
- delay: "20"
content: "-"
- delay: "20"
content: "F"
- delay: "20"
content: "C"
- delay: "20"
content: "P"
- delay: "20"
content: "Z"
- delay: "20"
content: "-"
- delay: "20"
content: "R"
- delay: "20"
content: "8"
- delay: "20"
content: "V"
- delay: "20"
content: "R"
- delay: 405
content: "\r\n"
- delay: 6
content: "\r\n\e[1;33mEnter your destination bitcoin address\e[0m\r\n➜ "
- delay: 1500
content: bc1qepja3llne3xp27car3gvvy7kzg7nfsacqvfe5l5jx4fa2f2v3wlskh9le7
- delay: 700
content: "\r\n\r\nStarting scan of all possible addresses. This will take a few minutes.\r\n► \e[1;37mFinding servers...\e[0m"
- delay: 2500
content: "\r► \e[1;37mScanned addresses\e[0m: 100 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 900 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 1200 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 1500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 2000 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 3400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 3500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 5400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 5500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 6200 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 6500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 6900 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 7400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 7800 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 8100 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 8400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 8900 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 9300 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 9700 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 10100 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 11100 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 11200 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 11400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 11600 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 12100 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 12400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 13100 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 13300 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 13400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 13500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 13800 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 14100 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 14500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 14900 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 15000 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 15500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 15800 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 16000 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 16200 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 16300 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 16900 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 17200 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 17400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 17500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 17600 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 17700 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 17800 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 18200 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 18300 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 18800 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 19100 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 19500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 19900 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 20000 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 20300 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 20600 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 20700 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 21400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 21800 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 21900 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 22100 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 22400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 24100 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 24500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 24600 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 25000 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 25400 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 25500 | \e[1;37mSats found\e[0m: 0"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 26000 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 26100 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 26300 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 26600 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 26900 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 27400 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 28000 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 28600 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 28800 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 29400 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 29600 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 30900 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 31200 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 31400 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 31900 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 32300 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 33000 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 33400 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 33800 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 34100 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 34200 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 34600 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 34700 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 35000 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 35100 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 35700 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 36000 | \e[1;37mSats found\e[0m: 11155"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 37400 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 37700 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 38000 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 38300 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 38400 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 38500 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 39000 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 39700 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 40100 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 40400 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 41600 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 42100 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 43000 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 43600 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 43900 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 44000 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 44400 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 44900 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 45900 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 47500 | \e[1;37mSats found\e[0m: 56307"
- delay: 10
content: "\r► \e[1;37mScanned addresses\e[0m: 48200 | \e[1;37mSats found\e[0m: 56307"
- delay: 71
content: "\r\n\r\n\e[1;32m✓ Scan complete\e[0m\r\n• \e[1;37m11155\e[0m sats in bc1qefj50ll0a6pf0jyd7x96w5ejzkelc6fxxypyu0fl7uspdtr8efvqfcykst\r\n• \e[1;37m45152\e[0m sats in bc1ql4hjt3newq69a9ty9ymrpjwxex4msuk9jd6z7d8570zkh995v6cs576m62\r\n— \e[1;37m56307\e[0m sats total\r\n\r\n\e[1;33mEnter the fee rate (sats/byte)\e[0m\r\nYour transaction weighs 315 bytes. You can get suggestions in https://bitcoinfees.earn.com/#fees\r\n➜ "
- delay: 2500
content: '5'
- delay: 1000
content: "\r\n\r\n\e[1;4;37mSummary\e[0m\r\n \e[1;37mAmount\e[0m: 54732 sats\r\n \e[1;37mFee\e[0m: 1575 sats\r\n \e[1;37mDestination\e[0m: bc1qepja3llne3xp27car3gvvy7kzg7nfsacqvfe5l5jx4fa2f2v3wlskh9le7\r\n\r\n\e[1;33mConfirm?\e[0m (y/n)\r\n➜ "
- delay: 2500
content: 'y'
- delay: 1200
content: "\r\n\r\nSending transaction..."
- delay: 2000
content: "\r\n\nTransaction sent! You can check the status here:\r\n\e[4mhttps://blockstream.info/tx/7316aae315f2c6280a4a0199ea233f84d2d5af12f0d2588b92cbcc8c0e441da3\e[0m\r\n(it will appear in Blockstream after a short delay)\r\n\r\nWe appreciate all kinds of feedback. If you have any, send it to \e[1;34mcontact@muun.com\e[0m\r\n"
- delay: 6000
content: "\r"

7
recovery_tool/recovery-tool Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
# Move to the repository root:
cd "$(dirname "${BASH_SOURCE[0]}")" || exit
# Go!
go run -mod=vendor . -- "$@"

View File

@@ -0,0 +1,144 @@
package main
import (
"bytes"
"encoding/hex"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/muun/libwallet"
"github.com/muun/libwallet/btcsuitew/txscriptw"
"github.com/muun/recovery/scanner"
)
func buildSweepTx(utxos []*scanner.Utxo, sweepAddress btcutil.Address, fee int64) ([]byte, error) {
tx := wire.NewMsgTx(2)
value := int64(0)
for _, utxo := range utxos {
chainHash, err := chainhash.NewHashFromStr(utxo.TxID)
if err != nil {
return nil, err
}
outpoint := wire.OutPoint{
Hash: *chainHash,
Index: uint32(utxo.OutputIndex),
}
tx.AddTxIn(wire.NewTxIn(&outpoint, []byte{}, [][]byte{}))
value += utxo.Amount
}
value -= fee
script, err := txscriptw.PayToAddrScript(sweepAddress)
if err != nil {
return nil, err
}
tx.AddTxOut(wire.NewTxOut(value, script))
writer := &bytes.Buffer{}
err = tx.Serialize(writer)
if err != nil {
return nil, err
}
if fee != 0 {
readConfirmation(value, fee, sweepAddress.String())
}
return writer.Bytes(), nil
}
func buildSignedTx(utxos []*scanner.Utxo, sweepTx []byte, userKey *libwallet.HDPrivateKey,
muunKey *libwallet.HDPrivateKey) (*wire.MsgTx, error) {
inputList := &libwallet.InputList{}
for _, utxo := range utxos {
inputList.Add(&input{
utxo,
[]byte{},
})
}
nonces := libwallet.GenerateMusigNonces(len(utxos))
pstx, err := libwallet.NewPartiallySignedTransaction(inputList, sweepTx, nonces)
if err != nil {
return nil, err
}
signedTx, err := pstx.FullySign(userKey, muunKey)
if err != nil {
return nil, err
}
wireTx := wire.NewMsgTx(0)
wireTx.BtcDecode(bytes.NewReader(signedTx.Bytes), 0, wire.WitnessEncoding)
return wireTx, nil
}
// input is a minimal type that implements libwallet.Input
type input struct {
utxo *scanner.Utxo
muunSignature []byte
}
func (i *input) OutPoint() libwallet.Outpoint {
return &outpoint{utxo: i.utxo}
}
func (i *input) Address() libwallet.MuunAddress {
return i.utxo.Address
}
func (i *input) UserSignature() []byte {
return []byte{}
}
func (i *input) MuunSignature() []byte {
return i.muunSignature
}
func (i *input) SubmarineSwapV1() libwallet.InputSubmarineSwapV1 {
return nil
}
func (i *input) SubmarineSwapV2() libwallet.InputSubmarineSwapV2 {
return nil
}
func (i *input) IncomingSwap() libwallet.InputIncomingSwap {
return nil
}
func (i *input) MuunPublicNonce() []byte {
// Will always be nil in the context of the tool
// Look at coinV5.signFirstWith for the reason why.
return nil
}
// outpoint is a minimal type that implements libwallet.Outpoint
type outpoint struct {
utxo *scanner.Utxo
}
func (o *outpoint) TxId() []byte {
raw, err := hex.DecodeString(o.utxo.TxID)
if err != nil {
panic(err) // we wrote this hex value ourselves, no input from anywhere else
}
return raw
}
func (o *outpoint) Index() int {
return o.utxo.OutputIndex
}
func (o *outpoint) Amount() int64 {
return o.utxo.Amount
}

View File

@@ -0,0 +1,217 @@
package scanner
import (
"sync"
"time"
"github.com/muun/libwallet"
"github.com/muun/recovery/electrum"
"github.com/muun/recovery/utils"
)
const taskTimeout = 15 * time.Minute
const batchSize = 100
// Scanner finds unspent outputs and their transactions when given a map of addresses.
//
// It implements multi-server support, batching feature detection and use, concurrency control,
// timeouts and cancellations, and provides a channel-based interface.
//
// Servers are provided by a ServerProvider instance, and rotated when unreachable or faulty. We
// trust ServerProvider to prioritize good targets.
//
// Batching is leveraged when supported by a particular server, falling back to sequential requests
// for single addresses (which is much slower, but can get us out of trouble when better servers are
// not available).
//
// Timeouts and cancellations are an internal affair, not configurable by callers. See taskTimeout
// declared above.
//
// Concurrency control works by using an electrum.Pool, limiting access to clients, and not an
// internal worker pool. This is the Go way (limiting access to resources rather than having a fixed
// number of parallel goroutines), and (more to the point) semantically correct. We don't care
// about the number of concurrent workers, what we want to avoid is too many connections to
// Electrum servers.
type Scanner struct {
pool *electrum.Pool
servers *electrum.ServerProvider
log *utils.Logger
}
// Report contains information about an ongoing scan.
type Report struct {
ScannedAddresses int
UtxosFound []*Utxo
Err error
}
// Utxo references a transaction output, plus the associated MuunAddress and script.
type Utxo struct {
TxID string
OutputIndex int
Amount int64
Address libwallet.MuunAddress
Script []byte
}
// scanContext contains the synchronization objects for a single Scanner round, to manage Tasks.
type scanContext struct {
// Task management:
addresses chan libwallet.MuunAddress
results chan *scanTaskResult
stopScan chan struct{}
stopCollect chan struct{}
wg *sync.WaitGroup
// Progress reporting:
reports chan *Report
reportCache *Report
}
// NewScanner creates an initialized Scanner.
func NewScanner(connectionPool *electrum.Pool, electrumProvider *electrum.ServerProvider) *Scanner {
return &Scanner{
pool: connectionPool,
servers: electrumProvider,
log: utils.NewLogger("Scanner"),
}
}
// Scan an address space and return all relevant transactions for a sweep.
func (s *Scanner) Scan(addresses chan libwallet.MuunAddress) <-chan *Report {
var waitGroup sync.WaitGroup
// Create the Context that goroutines will share:
ctx := &scanContext{
addresses: addresses,
results: make(chan *scanTaskResult),
stopScan: make(chan struct{}),
stopCollect: make(chan struct{}),
wg: &waitGroup,
reports: make(chan *Report),
reportCache: &Report{
ScannedAddresses: 0,
UtxosFound: []*Utxo{},
},
}
// Start the scan in background:
go s.startCollect(ctx)
go s.startScan(ctx)
return ctx.reports
}
func (s *Scanner) startCollect(ctx *scanContext) {
// Collect all results until the done signal, or abort on the first error:
for {
select {
case result := <-ctx.results:
s.log.Printf("Scanned %d, found %d (err %v)", len(result.Task.addresses), len(result.Utxos), result.Err)
newReport := *ctx.reportCache // create a new private copy
ctx.reportCache = &newReport
if result.Err != nil {
ctx.reportCache.Err = s.log.Errorf("Scan failed: %w", result.Err)
ctx.reports <- ctx.reportCache
close(ctx.stopScan) // failed after several retries, we give up and terminate all tasks
close(ctx.reports) // close the report channel to let callers know we're done
return
}
ctx.reportCache.ScannedAddresses += len(result.Task.addresses)
ctx.reportCache.UtxosFound = append(ctx.reportCache.UtxosFound, result.Utxos...)
ctx.reports <- ctx.reportCache
case <-ctx.stopCollect:
close(ctx.reports) // close the report channel to let callers know we're done
return
}
}
}
func (s *Scanner) startScan(ctx *scanContext) {
s.log.Printf("Scan started")
batches := streamBatches(ctx.addresses)
var client *electrum.Client
for batch := range batches {
// Stop the loop until a client becomes available, or the scan is canceled:
select {
case <-ctx.stopScan:
return
case client = <-s.pool.Acquire():
}
// Start scanning this address in background:
ctx.wg.Add(1)
go func(batch []libwallet.MuunAddress) {
defer s.pool.Release(client)
defer ctx.wg.Done()
s.scanBatch(ctx, client, batch)
}(batch)
}
// Wait for all tasks that are still executing to complete:
ctx.wg.Wait()
s.log.Printf("Scan complete")
// Signal to the collector that this Context has no more pending work:
close(ctx.stopCollect)
}
func (s *Scanner) scanBatch(ctx *scanContext, client *electrum.Client, batch []libwallet.MuunAddress) {
// NOTE:
// We begin by building the task, passing our selected Client. Since we're choosing the instance,
// it's our job to control acquisition and release of Clients to prevent sharing (remember,
// clients are single-user). The task won't enforce this safety measure (it can't), it's fully
// up to us.
task := &scanTask{
servers: s.servers,
client: client,
addresses: batch,
timeout: taskTimeout,
exit: ctx.stopCollect,
}
// Do the thing and send back the result:
ctx.results <- task.Execute()
}
func streamBatches(addresses chan libwallet.MuunAddress) chan []libwallet.MuunAddress {
batches := make(chan []libwallet.MuunAddress)
go func() {
var nextBatch []libwallet.MuunAddress
for address := range addresses {
// Add items to the batch until we reach the limit:
nextBatch = append(nextBatch, address)
if len(nextBatch) < batchSize {
continue
}
// Send back the batch and start over:
batches <- nextBatch
nextBatch = []libwallet.MuunAddress{}
}
// Send back an incomplete batch with any remaining addresses:
if len(nextBatch) > 0 {
batches <- nextBatch
}
close(batches)
}()
return batches
}

View File

@@ -0,0 +1,197 @@
package scanner
import (
"fmt"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/muun/libwallet"
"github.com/muun/libwallet/btcsuitew/btcutilw"
"github.com/muun/libwallet/btcsuitew/txscriptw"
"github.com/muun/recovery/electrum"
)
// scanTask encapsulates a parallelizable Scanner unit of work.
type scanTask struct {
servers *electrum.ServerProvider
client *electrum.Client
addresses []libwallet.MuunAddress
timeout time.Duration
exit chan struct{}
}
// scanTaskResult contains a summary of the execution of a task.
type scanTaskResult struct {
Task *scanTask
Utxos []*Utxo
Err error
}
// Execute obtains the Utxo set for the Task address, implementing a retry strategy.
func (t *scanTask) Execute() *scanTaskResult {
results := make(chan *scanTaskResult)
timeout := time.After(t.timeout)
// Keep the last error around, in case we reach the timeout and want to know the reason:
var lastError error
for {
// Attempt to run the task:
go t.tryExecuteAsync(results)
// Wait until a result is sent, the timeout is reached or the task canceled, capturing errors
// errors along the way:
select {
case <-t.exit:
return t.exitResult() // stop retrying when we get the done signal
case result := <-results:
if result.Err == nil {
return result // we're done! nice work everyone.
}
lastError = result.Err // keep retrying when an attempt fails
case <-timeout:
return t.errorResult(fmt.Errorf("Task timed out. Last error: %w", lastError)) // stop on timeout
}
}
}
func (t *scanTask) tryExecuteAsync(results chan *scanTaskResult) {
// Errors will almost certainly arise from Electrum server failures, which are extremely
// common. Unreachable IPs, dropped connections, sudden EOFs, etc. We'll run this task, assuming
// the servers are at fault when something fails, disconnecting and cycling them as we retry.
result := t.tryExecute()
if result.Err != nil {
t.client.Disconnect()
}
results <- result
}
func (t *scanTask) tryExecute() *scanTaskResult {
// If our client is not connected, make an attempt to connect to a server:
if !t.client.IsConnected() {
err := t.client.Connect(t.servers.NextServer())
if err != nil {
return t.errorResult(err)
}
}
// Prepare the output scripts for all given addresses:
outputScripts, err := getOutputScripts(t.addresses)
if err != nil {
return t.errorResult(err)
}
// Prepare the index hashes that Electrum requires to list outputs:
indexHashes, err := getIndexHashes(outputScripts)
if err != nil {
return t.errorResult(err)
}
// Call Electrum to get the unspent output list, grouped by index for each address:
var unspentRefGroups [][]electrum.UnspentRef
if t.client.SupportsBatching() {
unspentRefGroups, err = t.listUnspentWithBatching(indexHashes)
} else {
unspentRefGroups, err = t.listUnspentWithoutBatching(indexHashes)
}
if err != nil {
return t.errorResult(err)
}
// Compile the results into a list of `Utxos`:
var utxos []*Utxo
for i, unspentRefGroup := range unspentRefGroups {
for _, unspentRef := range unspentRefGroup {
newUtxo := &Utxo{
TxID: unspentRef.TxHash,
OutputIndex: unspentRef.TxPos,
Amount: unspentRef.Value,
Script: outputScripts[i],
Address: t.addresses[i],
}
utxos = append(utxos, newUtxo)
}
}
return t.successResult(utxos)
}
func (t *scanTask) listUnspentWithBatching(indexHashes []string) ([][]electrum.UnspentRef, error) {
unspentRefGroups, err := t.client.ListUnspentBatch(indexHashes)
if err != nil {
return nil, fmt.Errorf("Listing with batching failed: %w", err)
}
return unspentRefGroups, nil
}
func (t *scanTask) listUnspentWithoutBatching(indexHashes []string) ([][]electrum.UnspentRef, error) {
var unspentRefGroups [][]electrum.UnspentRef
for _, indexHash := range indexHashes {
newGroup, err := t.client.ListUnspent(indexHash)
if err != nil {
return nil, fmt.Errorf("Listing without batching failed: %w", err)
}
unspentRefGroups = append(unspentRefGroups, newGroup)
}
return unspentRefGroups, nil
}
func (t *scanTask) errorResult(err error) *scanTaskResult {
return &scanTaskResult{Task: t, Err: err}
}
func (t *scanTask) successResult(utxos []*Utxo) *scanTaskResult {
return &scanTaskResult{Task: t, Utxos: utxos}
}
func (t *scanTask) exitResult() *scanTaskResult {
return &scanTaskResult{Task: t}
}
// getIndexHashes calculates all the Electrum index hashes for a list of output scripts.
func getIndexHashes(outputScripts [][]byte) ([]string, error) {
indexHashes := make([]string, len(outputScripts))
for i, outputScript := range outputScripts {
indexHashes[i] = electrum.GetIndexHash(outputScript)
}
return indexHashes, nil
}
// getOutputScripts creates all the scripts that send to an list of Bitcoin address.
func getOutputScripts(addresses []libwallet.MuunAddress) ([][]byte, error) {
outputScripts := make([][]byte, len(addresses))
for i, address := range addresses {
rawAddress := address.Address()
decodedAddress, err := btcutilw.DecodeAddress(rawAddress, &chaincfg.MainNetParams)
if err != nil {
return nil, fmt.Errorf("Failed to decode address %s: %w", rawAddress, err)
}
outputScript, err := txscriptw.PayToAddrScript(decodedAddress)
if err != nil {
return nil, fmt.Errorf("Failed to craft script for %s: %w", rawAddress, err)
}
outputScripts[i] = outputScript
}
return outputScripts, nil
}

View File

@@ -0,0 +1,341 @@
package survey
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"sort"
"strings"
"sync"
"time"
"github.com/muun/recovery/electrum"
)
type Survey struct {
config *Config
tasks chan *surveyTask
taskWg sync.WaitGroup
results chan *Result
visited map[string]bool
}
type Config struct {
InitialServers []string
Workers int
SpeedTestDuration time.Duration
SpeedTestBatchSize int
}
type Result struct {
Server string
FromPeer string
IsWorthy bool
Err error
Impl string
Version string
TimeToConnect time.Duration
Speed int
BatchSupport bool
peers []string
}
type surveyTask struct {
server string
fromPeer string
}
// Values to check whether we're in the same chain (in a previous version, SV servers snuck in)
var mainnetSomeTx = "985eb411473fa1bbd73efa5e3685edc00366c86b8d4d3f5b969ad59c23f4d959"
var mainnetGenesisHash = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
func NewSurvey(config *Config) *Survey {
return &Survey{
config: config,
tasks: make(chan *surveyTask),
results: make(chan *Result),
visited: make(map[string]bool),
}
}
func (s *Survey) Run() []*Result {
// Add initial tasks:
for _, server := range s.config.InitialServers {
s.addTask(server, "")
}
// Start collecting results in background:
results := []*Result{}
go s.startCollect(&results)
// Launch workers to process tasks and send back results:
for i := 0; i < s.config.Workers; i++ {
go s.startWorker()
}
// Wait until there's no tasks left, and signal everyone to stop:
s.taskWg.Wait()
close(s.tasks)
close(s.results)
sort.Slice(results, func(i, j int) bool {
return results[i].IsBetterThan(results[j])
})
return results
}
func (s *Survey) addTask(server string, fromPeer string) {
task := &surveyTask{server, fromPeer}
if _, ok := s.visited[task.server]; ok {
return
}
s.visited[task.server] = true
s.taskWg.Add(1)
go func() { s.tasks <- task }() // scheduling tasks is non-blocking for users of the type
}
func (s *Survey) notifyResult(result *Result) {
s.results <- result
s.taskWg.Done()
}
func (s *Survey) startCollect(resultsRef *[]*Result) {
for result := range s.results {
*resultsRef = append(*resultsRef, result)
}
}
func (s *Survey) startWorker() {
for task := range s.tasks {
log("• %s\n", task.server)
result := s.processTask(task)
if result.Err != nil {
log("✕ %s\n", task.server)
} else {
log("✓ %s\n", task.server)
}
s.notifyResult(result)
}
}
func (s *Survey) processTask(task *surveyTask) *Result {
// We're going to perform a number of tests an measurements:
//
// 1. How much time does it take to establish a connection?
// 2. Does the server support batching?
// 3. Is the server willing to share its peers? If so, crawl.
// 4. How many requests can the server handle in a given time interval?
// 5. Did the server fail at any point during testing?
//
// Each test can result in a closed socket (since Electrum communicates errors by slapping you
// in the face with no explanation), so we'll be connecting separately for each attempt.
//
// When a testing method returns an error, it means the server failed completely and we couldn't
// obtain meaningful results (while some internal errors in a test are expected and handled).
impl, version, timeToConnect, err := testConnection(task)
if err != nil {
return &Result{Server: task.server, Err: err}
}
isBitcoinMainnet, err := testBitcoinMainnet(task)
if err != nil || !isBitcoinMainnet {
return &Result{Server: task.server, Err: fmt.Errorf("not on Bitcoin mainnet: %w", err)}
}
batchSupport, err := testBatchSupport(task)
if err != nil {
return &Result{Server: task.server, Err: err}
}
speed, err := s.measureSpeed(task)
if err != nil {
return &Result{Server: task.server, Err: err}
}
peers, err := getPeers(task)
if err != nil {
return &Result{Server: task.server, Err: err}
}
for _, peer := range peers {
if strings.Contains(peer, ".onion:") {
continue
}
s.addTask(peer, task.server)
}
isWorthy := err == nil &&
batchSupport &&
timeToConnect.Seconds() < 5.0 &&
speed >= int(s.config.SpeedTestDuration.Seconds())
return &Result{
IsWorthy: isWorthy,
Server: task.server,
FromPeer: task.fromPeer,
Impl: impl,
Version: version,
TimeToConnect: timeToConnect,
BatchSupport: batchSupport,
Speed: speed,
peers: peers,
}
}
// testConnection returns the server implementation, protocol version and time to connect
func testConnection(task *surveyTask) (string, string, time.Duration, error) {
client := electrum.NewClient(true)
start := time.Now()
err := client.Connect(task.server)
if err != nil {
return "", "", 0, err
}
return client.ServerImpl, client.ProtoVersion, time.Since(start), nil
}
// testsBlockchain returns whether this server is operating on Bitcoin mainnet
func testBitcoinMainnet(task *surveyTask) (bool, error) {
client := electrum.NewClient(true)
err := client.Connect(task.server)
if err != nil {
return false, err
}
features, err := client.ServerFeatures()
if err != nil || features.GenesisHash != mainnetGenesisHash {
return false, err
}
_, err = client.GetTransaction(mainnetSomeTx)
if err != nil {
return false, err
}
return true, nil
}
// testBatchSupport returns whether the server successfully responded to a batched request
func testBatchSupport(task *surveyTask) (bool, error) {
client := electrum.NewClient(true)
err := client.Connect(task.server)
if err != nil {
return false, err
}
_, err = client.ListUnspentBatch(createFakeHashes(2))
if err != nil {
return false, nil // an error here suggests lack of support for this call
}
return true, nil
}
// measureSpeed returns the amount of successful ListUnspentBatch calls in SPEED_TEST_DURATION
// seconds. It assumes batch support was verified beforehand.
func (s *Survey) measureSpeed(task *surveyTask) (int, error) {
client := electrum.NewClient(true)
err := client.Connect(task.server)
if err != nil {
return 0, err
}
start := time.Now()
responseCount := 0
for time.Since(start) < s.config.SpeedTestDuration {
fakeHashes := createFakeHashes(s.config.SpeedTestBatchSize)
_, err := client.ListUnspentBatch(fakeHashes) // TODO: is the faking affecting the result?
if err != nil {
return 0, err
}
responseCount++
}
return responseCount - 1, nil // the last one was over the time limit
}
// getPeers returns the list of peers from a server, or empty if it doesn't responds to the request
func getPeers(task *surveyTask) ([]string, error) {
client := electrum.NewClient(true)
err := client.Connect(task.server)
if err != nil {
return nil, err
}
peers, err := client.ServerPeers()
if err != nil {
return []string{}, nil // an error here suggests lack of support for this call
}
return peers, nil
}
func (r *Result) IsBetterThan(other *Result) bool {
if r.Err != nil {
return false
}
if other.Err != nil {
return true
}
if r.IsWorthy != other.IsWorthy {
return r.IsWorthy
}
if r.BatchSupport != other.BatchSupport {
return r.BatchSupport
}
if r.Speed != other.Speed {
return (r.Speed > other.Speed)
}
return (r.TimeToConnect < other.TimeToConnect)
}
func (r *Result) String() string {
return fmt.Sprintf(
"%s, %s, %s, %v, %v, %d, %v",
r.Server,
r.Impl,
r.Version,
r.BatchSupport,
r.TimeToConnect.Seconds(),
r.Speed,
r.Err,
)
}
func createFakeHashes(count int) []string {
randomBuffer := make([]byte, 32)
fakeHashes := make([]string, count)
for i := 0; i < count; i++ {
rand.Read(randomBuffer)
fakeHashes[i] = hex.EncodeToString(randomBuffer)
}
return fakeHashes
}
func log(msg string, args ...interface{}) {
fmt.Fprintf(os.Stderr, msg, args...)
}

81
recovery_tool/sweeper.go Normal file
View File

@@ -0,0 +1,81 @@
package main
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/muun/recovery/electrum"
"github.com/muun/recovery/scanner"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/muun/libwallet"
)
var (
chainParams = chaincfg.MainNetParams
)
type Sweeper struct {
UserKey *libwallet.HDPrivateKey
MuunKey *libwallet.HDPrivateKey
Birthday int
SweepAddress btcutil.Address
}
func (s *Sweeper) GetSweepTxAmountAndWeightInBytes(utxos []*scanner.Utxo) (outputAmount int64, weightInBytes int64, err error) {
// we build a sweep tx with 0 fee with the only purpose of checking its signed size
zeroFeeSweepTx, err := s.BuildSweepTx(utxos, 0)
if err != nil {
return 0, 0, err
}
outputAmount = zeroFeeSweepTx.TxOut[0].Value
weightInBytes = int64(zeroFeeSweepTx.SerializeSize())
return outputAmount, weightInBytes, nil
}
func (s *Sweeper) BuildSweepTx(utxos []*scanner.Utxo, fee int64) (*wire.MsgTx, error) {
derivedMuunKey, err := s.MuunKey.DeriveTo("m/1'/1'")
if err != nil {
return nil, err
}
sweepTx, err := buildSweepTx(utxos, s.SweepAddress, fee)
if err != nil {
return nil, err
}
return buildSignedTx(utxos, sweepTx, s.UserKey, derivedMuunKey)
}
func (s *Sweeper) BroadcastTx(tx *wire.MsgTx) error {
// Connect to an Electurm server using a fresh client and provider pair:
sp := electrum.NewServerProvider(electrum.PublicServers) // TODO create servers module, for provider and pool
client := electrum.NewClient(true)
for !client.IsConnected() {
client.Connect(sp.NextServer())
}
// Encode the transaction for broadcast:
txBytes := new(bytes.Buffer)
err := tx.BtcEncode(txBytes, wire.ProtocolVersion, wire.WitnessEncoding)
if err != nil {
return fmt.Errorf("error while encoding tx: %w", err)
}
txHex := hex.EncodeToString(txBytes.Bytes())
// Do the thing!
_, err = client.Broadcast(txHex)
if err != nil {
return fmt.Errorf("error while broadcasting: %w", err)
}
return nil
}

View File

@@ -0,0 +1,78 @@
package utils
import (
"fmt"
"io"
"os"
"strings"
"time"
)
// DebugMode is true when the `DEBUG` environment variable is set to "true".
var DebugMode bool = os.Getenv("DEBUG") == "true"
var outputStream io.Writer = io.Discard
// SetOutputStream set a writer to record all logs (except Tracef).
func SetOutputStream(s io.Writer) {
outputStream = s
}
// Logger provides logging methods. Logs are written to the stream set by
// SetOutputStream. If `DebugMode` is true, we also print to stdout/stderr.
// This allows callers to log detailed information without displaying it to
// users during normal execution.
type Logger struct {
tag string
}
// NewLogger returns an initialized Logger instance.
func NewLogger(tag string) *Logger {
return &Logger{tag}
}
// SetTag updates the tag of this Logger.
func (l *Logger) SetTag(newTag string) {
l.tag = newTag
}
// Tracef works like fmt.Printf, but only prints when `DebugMode` is true. These logs
// are *not* recorded to the output stream.
func (l *Logger) Tracef(format string, v ...interface{}) {
if !DebugMode {
return
}
message := strings.TrimSpace(fmt.Sprintf(format, v...))
fmt.Printf("%s %s %s\n", time.Now().Format(time.RFC3339Nano), l.getPrefix(), message)
}
// Printf works like fmt.Printf, but only prints when `DebugMode` is true. These logs
// are recorded to the output stream, so they should not include sensitive information.
func (l *Logger) Printf(format string, v ...interface{}) {
message := strings.TrimSpace(fmt.Sprintf(format, v...))
log := fmt.Sprintf("%s %s %s\n", time.Now().Format(time.RFC3339Nano), l.getPrefix(), message)
_, _ = outputStream.Write([]byte(log))
if DebugMode {
print(log)
}
}
// Errorf works like fmt.Errorf, but prints the error to the console if `DebugMode` is true.
// These logs are recorded to the output stream, so they should not include sensitive information.
func (l *Logger) Errorf(format string, v ...interface{}) error {
err := fmt.Errorf(format, v...)
log := fmt.Sprintf("ERROR: %s %s %v\n", time.Now().Format(time.RFC3339Nano), l.getPrefix(), err)
_, _ = outputStream.Write([]byte(log))
if DebugMode {
print(log)
}
return err
}
func (l *Logger) getPrefix() string {
return fmt.Sprintf("[%s]", l.tag)
}

6
recovery_tool/version.go Normal file
View File

@@ -0,0 +1,6 @@
package main
// The OSS tool depends on this file with this content. If you need to change
// it, change the tool too.
const version = "2.2.4"