mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 20:24:21 +01:00
Co-authored-by: Marco Argentieri <tiero@users.noreply.github.com> * Add claim command * Persist pending data in sqlite repo * Remove debug log * Return pending data at interface level * Fix unlocking btc wallet after restart * Lint & Fix whitelist permissions * Fix send command for covenant * Update client/covenantless/claim.go Signed-off-by: Marco Argentieri <3596602+tiero@users.noreply.github.com> * Fix * Pay for min relay fee instead of estimating fees for redeem and unconf forfeit txs * Add support for pending payments (coventanless) * Fixes * Fixes * Improve verbosity * Fix coin selection * Fix --------- Signed-off-by: Marco Argentieri <3596602+tiero@users.noreply.github.com> Co-authored-by: louisinger <louis@vulpem.com> Co-authored-by: Marco Argentieri <tiero@users.noreply.github.com> Co-authored-by: Marco Argentieri <3596602+tiero@users.noreply.github.com>
448 lines
9.3 KiB
Go
448 lines
9.3 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/urfave/cli/v2"
|
|
"gopkg.in/macaroon.v2"
|
|
)
|
|
|
|
// flags
|
|
var (
|
|
passwordFlag = &cli.StringFlag{
|
|
Name: "password",
|
|
Usage: "wallet password",
|
|
Required: true,
|
|
}
|
|
mnemonicFlag = &cli.StringFlag{
|
|
Name: "mnemonic",
|
|
Usage: "mnemonic from which restore the wallet",
|
|
}
|
|
gapLimitFlag = &cli.Uint64Flag{
|
|
Name: "addr-gap-limit",
|
|
Usage: "address gap limit for wallet restoration",
|
|
Value: 100,
|
|
}
|
|
)
|
|
|
|
// commands
|
|
var (
|
|
walletCmd = &cli.Command{
|
|
Name: "wallet",
|
|
Usage: "Manage the Ark Server wallet",
|
|
Subcommands: append(
|
|
cli.Commands{},
|
|
walletStatusCmd,
|
|
walletCreateOrRestoreCmd,
|
|
walletUnlockCmd,
|
|
walletAddressCmd,
|
|
walletBalanceCmd,
|
|
),
|
|
}
|
|
walletStatusCmd = &cli.Command{
|
|
Name: "status",
|
|
Usage: "Get info about the status of the wallet",
|
|
Action: walletStatusAction,
|
|
}
|
|
walletCreateOrRestoreCmd = &cli.Command{
|
|
Name: "create",
|
|
Usage: "Create or restore the wallet",
|
|
Action: walletCreateOrRestoreAction,
|
|
Flags: []cli.Flag{passwordFlag, mnemonicFlag, gapLimitFlag},
|
|
}
|
|
walletUnlockCmd = &cli.Command{
|
|
Name: "unlock",
|
|
Usage: "Unlock the wallet",
|
|
Action: walletUnlockAction,
|
|
Flags: []cli.Flag{passwordFlag},
|
|
}
|
|
walletAddressCmd = &cli.Command{
|
|
Name: "address",
|
|
Usage: "Generate a receiving address",
|
|
Action: walletAddressAction,
|
|
}
|
|
walletBalanceCmd = &cli.Command{
|
|
Name: "balance",
|
|
Usage: "Get the wallet balance",
|
|
Action: walletBalanceAction,
|
|
}
|
|
)
|
|
|
|
func walletStatusAction(ctx *cli.Context) error {
|
|
baseURL := ctx.String("url")
|
|
tlsCertPath := ctx.String("tls-cert-path")
|
|
if strings.Contains(baseURL, "http://") {
|
|
tlsCertPath = ""
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/v1/admin/wallet/status", baseURL)
|
|
status, err := getStatus(url, tlsCertPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println(status)
|
|
return nil
|
|
}
|
|
|
|
func walletCreateOrRestoreAction(ctx *cli.Context) error {
|
|
baseURL := ctx.String("url")
|
|
password := ctx.String("password")
|
|
mnemonic := ctx.String("mnemonic")
|
|
gapLimit := ctx.Uint64("addr-gap-limit")
|
|
tlsCertPath := ctx.String("tls-cert-path")
|
|
if strings.Contains(baseURL, "http://") {
|
|
tlsCertPath = ""
|
|
}
|
|
|
|
if len(mnemonic) > 0 {
|
|
url := fmt.Sprintf("%s/v1/admin/wallet/restore", baseURL)
|
|
body := fmt.Sprintf(
|
|
`{"seed": "%s", "password": "%s", "gap_limit": %d}`,
|
|
mnemonic, password, gapLimit,
|
|
)
|
|
if _, err := post[struct{}](url, body, "", "", tlsCertPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("wallet restored")
|
|
return nil
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/v1/admin/wallet/seed", baseURL)
|
|
seed, err := get[string](url, "seed", "", tlsCertPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
url = fmt.Sprintf("%s/v1/admin/wallet/create", baseURL)
|
|
body := fmt.Sprintf(
|
|
`{"seed": "%s", "password": "%s"}`, seed, password,
|
|
)
|
|
if _, err := post[struct{}](url, body, "", "", tlsCertPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println(seed)
|
|
return nil
|
|
}
|
|
|
|
func walletUnlockAction(ctx *cli.Context) error {
|
|
baseURL := ctx.String("url")
|
|
password := ctx.String("password")
|
|
url := fmt.Sprintf("%s/v1/admin/wallet/unlock", baseURL)
|
|
body := fmt.Sprintf(`{"password": "%s"}`, password)
|
|
tlsCertPath := ctx.String("tls-cert-path")
|
|
if strings.Contains(baseURL, "http://") {
|
|
tlsCertPath = ""
|
|
}
|
|
|
|
if _, err := post[struct{}](url, body, "", "", tlsCertPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("wallet unlocked")
|
|
return nil
|
|
}
|
|
|
|
func walletAddressAction(ctx *cli.Context) error {
|
|
baseURL := ctx.String("url")
|
|
var macaroon string
|
|
if !ctx.Bool("no-macaroon") {
|
|
macaroonPath := ctx.String("macaroon-path")
|
|
mac, err := getMacaroon(macaroonPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
macaroon = mac
|
|
}
|
|
tlsCertPath := ctx.String("tls-cert-path")
|
|
if strings.Contains(baseURL, "http://") {
|
|
tlsCertPath = ""
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/v1/admin/wallet/address", baseURL)
|
|
addr, err := get[string](url, "address", macaroon, tlsCertPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println(addr)
|
|
return nil
|
|
}
|
|
|
|
func walletBalanceAction(ctx *cli.Context) error {
|
|
baseURL := ctx.String("url")
|
|
var macaroon string
|
|
if !ctx.Bool("no-macaroon") {
|
|
macaroonPath := ctx.String("macaroon-path")
|
|
mac, err := getMacaroon(macaroonPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
macaroon = mac
|
|
}
|
|
tlsCertPath := ctx.String("tls-cert-path")
|
|
if strings.Contains(baseURL, "http://") {
|
|
tlsCertPath = ""
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/v1/admin/wallet/balance", baseURL)
|
|
balance, err := getBalance(url, macaroon, tlsCertPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println(balance)
|
|
return nil
|
|
}
|
|
|
|
func post[T any](url, body, key, macaroon, tlsCert string) (result T, err error) {
|
|
tlsConfig, err := getTLSConfig(tlsCert)
|
|
if err != nil {
|
|
return
|
|
}
|
|
req, err := http.NewRequest("POST", url, strings.NewReader(body))
|
|
if err != nil {
|
|
return
|
|
}
|
|
req.Header.Add("Content-Type", "application/json")
|
|
if len(macaroon) > 0 {
|
|
req.Header.Add("X-Macaroon", macaroon)
|
|
}
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: tlsConfig,
|
|
},
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
buf, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
err = fmt.Errorf(string(buf))
|
|
return
|
|
}
|
|
if key == "" {
|
|
return
|
|
}
|
|
res := make(map[string]T)
|
|
if err = json.Unmarshal(buf, &res); err != nil {
|
|
return
|
|
}
|
|
|
|
result = res[key]
|
|
return
|
|
}
|
|
|
|
func get[T any](url, key, macaroon, tlsCert string) (result T, err error) {
|
|
tlsConfig, err := getTLSConfig(tlsCert)
|
|
if err != nil {
|
|
return
|
|
}
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
req.Header.Add("Content-Type", "application/json")
|
|
if len(macaroon) > 0 {
|
|
req.Header.Add("X-Macaroon", macaroon)
|
|
}
|
|
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: tlsConfig,
|
|
},
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
buf, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
err = fmt.Errorf(string(buf))
|
|
return
|
|
}
|
|
|
|
res := make(map[string]T)
|
|
if err = json.Unmarshal(buf, &res); err != nil {
|
|
return
|
|
}
|
|
|
|
result = res[key]
|
|
return
|
|
}
|
|
|
|
type accountBalance struct {
|
|
Available string `json:"available"`
|
|
Locked string `json:"locked"`
|
|
}
|
|
|
|
func (b accountBalance) String() string {
|
|
return fmt.Sprintf(" available: %s\n locked: %s", b.Available, b.Locked)
|
|
}
|
|
|
|
type balance struct {
|
|
MainAccount accountBalance `json:"mainAccount"`
|
|
ConnectorsAccount accountBalance `json:"connectorsAccount"`
|
|
}
|
|
|
|
func (b balance) String() string {
|
|
return fmt.Sprintf(
|
|
"main account\n%s\nconnectors account\n%s",
|
|
b.MainAccount, b.ConnectorsAccount,
|
|
)
|
|
}
|
|
|
|
func getBalance(url, macaroon, tlsCert string) (*balance, error) {
|
|
tlsConfig, err := getTLSConfig(tlsCert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Add("Content-Type", "application/json")
|
|
if len(macaroon) > 0 {
|
|
req.Header.Add("X-Macaroon", macaroon)
|
|
}
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: tlsConfig,
|
|
},
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
buf, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
err = fmt.Errorf(string(buf))
|
|
return nil, err
|
|
}
|
|
|
|
result := &balance{}
|
|
if err := json.Unmarshal(buf, result); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
type status struct {
|
|
Initialized bool `json:"initialized"`
|
|
Unlocked bool `json:"unlocked"`
|
|
Synced bool `json:"synced"`
|
|
}
|
|
|
|
func (s status) String() string {
|
|
return fmt.Sprintf(
|
|
"initialized: %t\nunlocked: %t\nsynced: %t",
|
|
s.Initialized, s.Unlocked, s.Synced,
|
|
)
|
|
}
|
|
|
|
func getStatus(url, tlsCert string) (*status, error) {
|
|
tlsConfig, err := getTLSConfig(tlsCert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Add("Content-Type", "application/json")
|
|
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: tlsConfig,
|
|
},
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
buf, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
err = fmt.Errorf(string(buf))
|
|
return nil, err
|
|
}
|
|
|
|
result := &status{}
|
|
if err := json.Unmarshal(buf, result); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func getMacaroon(path string) (string, error) {
|
|
macBytes, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read macaroon %s: %s", path, err)
|
|
}
|
|
mac := &macaroon.Macaroon{}
|
|
if err := mac.UnmarshalBinary(macBytes); err != nil {
|
|
return "", fmt.Errorf("failed to parse macaroon %s: %s", path, err)
|
|
}
|
|
|
|
return hex.EncodeToString(macBytes), nil
|
|
}
|
|
|
|
func getTLSConfig(path string) (*tls.Config, error) {
|
|
if len(path) <= 0 {
|
|
return nil, nil
|
|
}
|
|
var buf []byte
|
|
buf, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
caCertPool := x509.NewCertPool()
|
|
if ok := caCertPool.AppendCertsFromPEM(buf); !ok {
|
|
return nil, fmt.Errorf("failed to parse tls cert")
|
|
}
|
|
|
|
return &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
RootCAs: caCertPool,
|
|
}, nil
|
|
}
|