Files
ark/client/utils/explorer.go
Pietralberto Mazza 72a7f29bab Add CreatePayment and CompletePayment (#229)
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>
2024-08-10 19:18:02 +02:00

359 lines
7.0 KiB
Go

package utils
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/wire"
"github.com/urfave/cli/v2"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
type Utxo struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount uint64 `json:"value"`
Asset string `json:"asset,omitempty"` // optional
Status struct {
Confirmed bool `json:"confirmed"`
Blocktime int64 `json:"block_time"`
} `json:"status"`
}
type Explorer interface {
GetTxHex(txid string) (string, error)
Broadcast(txHex string) (string, error)
GetUtxos(addr string) ([]Utxo, error)
GetBalance(addr, asset string) (uint64, error)
GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64,
) (uint64, map[int64]uint64, error)
GetTxBlocktime(txid string) (confirmed bool, blocktime int64, err error)
GetFeeRate() (float64, error)
}
type explorer struct {
cache map[string]string
baseUrl string
}
func NewExplorer(ctx *cli.Context) Explorer {
baseUrl, err := getBaseURL(ctx)
if err != nil {
panic(err)
}
return &explorer{
cache: make(map[string]string),
baseUrl: baseUrl,
}
}
func (e *explorer) GetFeeRate() (float64, error) {
endpoint, err := url.JoinPath(e.baseUrl, "fee-estimates")
if err != nil {
return 0, err
}
resp, err := http.Get(endpoint)
if err != nil {
return 0, err
}
defer resp.Body.Close()
var response map[string]float64
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return 0, err
}
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("error getting fee rate: %s", resp.Status)
}
if len(response) == 0 {
fmt.Println("empty fee-estimates response, default to 2 sat/vbyte")
return 2, nil
}
return response["1"], nil
}
func (e *explorer) GetTxHex(txid string) (string, error) {
if hex, ok := e.cache[txid]; ok {
return hex, nil
}
txHex, err := e.getTxHex(txid)
if err != nil {
return "", err
}
e.cache[txid] = txHex
return txHex, nil
}
func (e *explorer) Broadcast(txStr string) (string, error) {
clone := strings.Clone(txStr)
txStr, txid, err := parseLiquidTx(txStr)
if err != nil {
txStr, txid, err = parseBitcoinTx(clone)
if err != nil {
fmt.Println("error parsing tx hex")
return "", err
}
}
e.cache[txid] = txStr
txid, err = e.broadcast(txStr)
if err != nil {
if strings.Contains(
strings.ToLower(err.Error()), "transaction already in block chain",
) {
return txid, nil
}
return "", err
}
return txid, nil
}
func (e *explorer) GetUtxos(addr string) ([]Utxo, error) {
endpoint, err := url.JoinPath(e.baseUrl, "address", addr, "utxo")
if err != nil {
return nil, err
}
resp, err := http.Get(endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(string(body))
}
payload := []Utxo{}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
return payload, nil
}
func (e *explorer) GetBalance(addr, asset string) (uint64, error) {
payload, err := e.GetUtxos(addr)
if err != nil {
return 0, err
}
balance := uint64(0)
for _, p := range payload {
if len(asset) > 0 {
if p.Asset != asset {
continue
}
}
balance += p.Amount
}
return balance, nil
}
func (e *explorer) GetRedeemedVtxosBalance(
addr string, unilateralExitDelay int64,
) (spendableBalance uint64, lockedBalance map[int64]uint64, err error) {
utxos, err := e.GetUtxos(addr)
if err != nil {
return
}
lockedBalance = make(map[int64]uint64, 0)
now := time.Now()
for _, utxo := range utxos {
blocktime := now
if utxo.Status.Confirmed {
blocktime = time.Unix(utxo.Status.Blocktime, 0)
}
delay := time.Duration(unilateralExitDelay) * time.Second
availableAt := blocktime.Add(delay)
if availableAt.After(now) {
if _, ok := lockedBalance[availableAt.Unix()]; !ok {
lockedBalance[availableAt.Unix()] = 0
}
lockedBalance[availableAt.Unix()] += utxo.Amount
} else {
spendableBalance += utxo.Amount
}
}
return
}
func (e *explorer) GetTxBlocktime(txid string) (confirmed bool, blocktime int64, err error) {
endpoint, err := url.JoinPath(e.baseUrl, "tx", txid)
if err != nil {
return false, 0, err
}
resp, err := http.Get(endpoint)
if err != nil {
return false, 0, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, 0, err
}
if resp.StatusCode != http.StatusOK {
return false, 0, fmt.Errorf(string(body))
}
var tx struct {
Status struct {
Confirmed bool `json:"confirmed"`
Blocktime int64 `json:"block_time"`
} `json:"status"`
}
if err := json.Unmarshal(body, &tx); err != nil {
return false, 0, err
}
if !tx.Status.Confirmed {
return false, -1, nil
}
return true, tx.Status.Blocktime, nil
}
func (e *explorer) getTxHex(txid string) (string, error) {
endpoint, err := url.JoinPath(e.baseUrl, "tx", txid, "hex")
if err != nil {
return "", err
}
resp, err := http.Get(endpoint)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf(string(body))
}
hex := string(body)
e.cache[txid] = hex
return hex, nil
}
func (e *explorer) broadcast(txHex string) (string, error) {
body := bytes.NewBuffer([]byte(txHex))
endpoint, err := url.JoinPath(e.baseUrl, "tx")
if err != nil {
return "", err
}
resp, err := http.Post(endpoint, "text/plain", body)
if err != nil {
return "", err
}
defer resp.Body.Close()
bodyResponse, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf(string(bodyResponse))
}
return string(bodyResponse), nil
}
func parseLiquidTx(txStr string) (string, string, error) {
tx, err := transaction.NewTxFromHex(txStr)
if err != nil {
pset, err := psetv2.NewPsetFromBase64(txStr)
if err != nil {
return "", "", err
}
tx, err = psetv2.Extract(pset)
if err != nil {
return "", "", err
}
txhex, err := tx.ToHex()
if err != nil {
return "", "", err
}
txid := tx.TxHash().String()
return txhex, txid, nil
}
txhex, err := tx.ToHex()
if err != nil {
return "", "", err
}
txid := tx.TxHash().String()
return txhex, txid, nil
}
func parseBitcoinTx(txStr string) (string, string, error) {
var tx wire.MsgTx
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txStr))); err != nil {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(txStr), true)
if err != nil {
return "", "", err
}
txFromPartial, err := psbt.Extract(ptx)
if err != nil {
return "", "", err
}
tx = *txFromPartial
}
var txBuf bytes.Buffer
if err := tx.Serialize(&txBuf); err != nil {
return "", "", err
}
txhex := hex.EncodeToString(txBuf.Bytes())
txid := tx.TxHash().String()
return txhex, txid, nil
}