Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(btc): implement new btc rpc package #3349

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions zetaclient/chains/bitcoin/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Package client represents BTC RPC client.
package client

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"

"cosmossdk.io/errors"
types "github.com/btcsuite/btcd/btcjson"
chains "github.com/btcsuite/btcd/chaincfg"
"github.com/rs/zerolog"
"github.com/tendermint/btcd/btcjson"
"github.com/tendermint/btcd/chaincfg"

"github.com/zeta-chain/node/zetaclient/config"
"github.com/zeta-chain/node/zetaclient/logs"
"github.com/zeta-chain/node/zetaclient/metrics"
)

type Client struct {
hostURL string
client *http.Client
clientName string
config config.BTCConfig
params chains.Params
logger zerolog.Logger
}

type Opt func(c *Client)

type rawResponse struct {
Result json.RawMessage `json:"result"`
Error *btcjson.RPCError `json:"error"`
}

const (
// v1 means "no batch mode"
rpcVersion = types.RpcVersion1

// rpc command id. as we don't send batch requests, it's always 1
commandID = uint64(1)
)

func WithHTTP(httpClient *http.Client) Opt {
return func(c *Client) { c.client = httpClient }

Check warning on line 51 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L50-L51

Added lines #L50 - L51 were not covered by tests
}

// New Client constructor
func New(cfg config.BTCConfig, chainID int64, logger zerolog.Logger, opts ...Opt) (*Client, error) {
params, err := resolveParams(cfg.RPCParams)
if err != nil {
return nil, errors.Wrap(err, "unable to resolve chain params")
}

Check warning on line 59 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L55-L59

Added lines #L55 - L59 were not covered by tests

clientName := fmt.Sprintf("btc:%d", chainID)

c := &Client{
hostURL: normalizeHostURL(cfg.RPCHost, true),
client: defaultHTTPClient(),
params: params,
clientName: clientName,
logger: logger.With().
Str(logs.FieldModule, "btc_client").
Int64(logs.FieldChain, chainID).
Logger(),
}

for _, opt := range opts {
opt(c)
}

Check warning on line 76 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L61-L76

Added lines #L61 - L76 were not covered by tests

return c, nil

Check warning on line 78 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L78

Added line #L78 was not covered by tests
}

// send sends RPC command to the server via http post request
func (c *Client) sendCommand(ctx context.Context, cmd any) (json.RawMessage, error) {
method, reqBody, err := c.marshalCmd(cmd)
if err != nil {
return nil, errors.Wrap(err, "unable to marshal cmd")
}

Check warning on line 86 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L82-L86

Added lines #L82 - L86 were not covered by tests

// ps: we can add retry logic if needed

req, err := c.newRequest(ctx, reqBody)
if err != nil {
return nil, errors.Wrapf(err, "unable to create http request for %q", method)
}

Check warning on line 93 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L90-L93

Added lines #L90 - L93 were not covered by tests

out, err := c.sendRequest(req, method)
switch {
case err != nil:
return nil, errors.Wrapf(err, "%q failed", method)
case out.Error != nil:
return nil, errors.Wrapf(out.Error, "got rpc error for %q", method)

Check warning on line 100 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L95-L100

Added lines #L95 - L100 were not covered by tests
}

return out.Result, nil

Check warning on line 103 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L103

Added line #L103 was not covered by tests
}

func (c *Client) newRequest(ctx context.Context, body []byte) (*http.Request, error) {
payload := bytes.NewReader(body)

req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.hostURL, payload)
if err != nil {
return nil, err
}

Check warning on line 112 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L106-L112

Added lines #L106 - L112 were not covered by tests

req.Header.Set("Content-Type", "application/json")

if c.config.RPCPassword != "" || c.config.RPCUsername != "" {
req.SetBasicAuth(c.config.RPCUsername, c.config.RPCPassword)
}

Check warning on line 118 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L114-L118

Added lines #L114 - L118 were not covered by tests

return req, nil

Check warning on line 120 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L120

Added line #L120 was not covered by tests
}

func (c *Client) sendRequest(req *http.Request, method string) (out rawResponse, err error) {
c.logger.Debug().Str("rpc.method", method).Msg("Sending request")
start := time.Now()

defer func() {
c.recordMetrics(method, start, out, err)
c.logger.Debug().Err(err).
Str("rpc.method", method).Dur("rpc.duration", time.Since(start)).
Msg("Sent request")
}()

Check warning on line 132 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L123-L132

Added lines #L123 - L132 were not covered by tests

res, err := c.client.Do(req)
if err != nil {
return rawResponse{}, errors.Wrap(err, "unable to send the request")
}

Check warning on line 137 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L134-L137

Added lines #L134 - L137 were not covered by tests
Comment on lines +134 to +137
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be a response code is 200 check here too?


defer res.Body.Close()

resBody, err := io.ReadAll(res.Body)
if err != nil {
return rawResponse{}, errors.Wrap(err, "unable to read response body")
}

Check warning on line 144 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L139-L144

Added lines #L139 - L144 were not covered by tests

if err = json.Unmarshal(resBody, &out); err != nil {
return rawResponse{}, errors.Wrapf(err, "unable to unmarshal rpc response (%s)", resBody)
}

Check warning on line 148 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L146-L148

Added lines #L146 - L148 were not covered by tests

return out, nil

Check warning on line 150 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L150

Added line #L150 was not covered by tests
}

func (c *Client) recordMetrics(method string, start time.Time, out rawResponse, err error) {
dur := time.Since(start).Seconds()

status := "ok"
if err != nil || out.Error != nil {
status = "failed"
}

Check warning on line 159 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L153-L159

Added lines #L153 - L159 were not covered by tests

metrics.RPCClientCounter.WithLabelValues(status, c.clientName, method).Inc()
metrics.RPCClientDuration.WithLabelValues(status, c.clientName, method).Observe(dur)

Check warning on line 162 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L161-L162

Added lines #L161 - L162 were not covered by tests
}

func (c *Client) marshalCmd(cmd any) (string, []byte, error) {
methodName, err := types.CmdMethod(cmd)
if err != nil {
return "", nil, errors.Wrap(err, "unable to resolve method")
}

Check warning on line 169 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L165-L169

Added lines #L165 - L169 were not covered by tests

body, err := types.MarshalCmd(rpcVersion, commandID, cmd)
if err != nil {
return "", nil, errors.Wrapf(err, "unable to marshal cmd %q", methodName)
}

Check warning on line 174 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L171-L174

Added lines #L171 - L174 were not covered by tests

return methodName, body, nil

Check warning on line 176 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L176

Added line #L176 was not covered by tests
}

func unmarshal[T any](raw json.RawMessage) (T, error) {
var tt T

if err := json.Unmarshal(raw, &tt); err != nil {
return tt, errors.Wrapf(err, "unable to unmarshal to '%T' (%s)", tt, raw)
}

Check warning on line 184 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L179-L184

Added lines #L179 - L184 were not covered by tests

return tt, nil

Check warning on line 186 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L186

Added line #L186 was not covered by tests
}

func unmarshalPtr[T any](raw json.RawMessage) (*T, error) {
tt, err := unmarshal[T](raw)
if err != nil {
return nil, err
}

Check warning on line 193 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L189-L193

Added lines #L189 - L193 were not covered by tests

return &tt, nil

Check warning on line 195 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L195

Added line #L195 was not covered by tests
}

func resolveParams(name string) (chains.Params, error) {
switch name {
case chains.MainNetParams.Name:
return chains.MainNetParams, nil
case chains.TestNet3Params.Name:
return chains.TestNet3Params, nil
case chaincfg.RegressionNetParams.Name:
return chains.RegressionNetParams, nil
case chaincfg.SimNetParams.Name:
return chains.SimNetParams, nil
default:
return chains.Params{}, fmt.Errorf("unknown chain params %q", name)

Check warning on line 209 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L198-L209

Added lines #L198 - L209 were not covered by tests
}
}

func normalizeHostURL(host string, disableHTTPS bool) string {
if strings.HasPrefix(host, "http://") || strings.HasPrefix(host, "https://") {
return host
}

Check warning on line 216 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L213-L216

Added lines #L213 - L216 were not covered by tests

protocol := "http"
if !disableHTTPS {
protocol = "https"
}

Check warning on line 221 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L218-L221

Added lines #L218 - L221 were not covered by tests

return fmt.Sprintf("%s://%s", protocol, host)

Check warning on line 223 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L223

Added line #L223 was not covered by tests
}

func defaultHTTPClient() *http.Client {
return &http.Client{
Transport: http.DefaultTransport,
Timeout: 10 * time.Second,
}

Check warning on line 230 in zetaclient/chains/bitcoin/client/client.go

View check run for this annotation

Codecov / codecov/patch

zetaclient/chains/bitcoin/client/client.go#L226-L230

Added lines #L226 - L230 were not covered by tests
}
Loading
Loading