diff --git a/changelog.md b/changelog.md index 4cc4454097..f94914aa1f 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ ### Refactor * [3332](https://github.com/zeta-chain/node/pull/3332) - implement orchestrator V2. Move BTC observer-signer to V2 +* [3349](https://github.com/zeta-chain/node/pull/3349) - implement new bitcoin rpc in zetaclient with improved performance and observability ## v25.0.0 diff --git a/cmd/zetaclientd/inbound.go b/cmd/zetaclientd/inbound.go index ee1ac98a05..8ec61b1666 100644 --- a/cmd/zetaclientd/inbound.go +++ b/cmd/zetaclientd/inbound.go @@ -15,8 +15,8 @@ import ( "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/zetaclient/chains/base" + btcclient "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" - btcrpc "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" evmobserver "github.com/zeta-chain/node/zetaclient/chains/evm/observer" "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" @@ -162,7 +162,7 @@ func InboundGetBallot(_ *cobra.Command, args []string) error { return fmt.Errorf("unable to find btc config") } - rpcClient, err := btcrpc.NewRPCClient(bitcoinConfig) + rpcClient, err := btcclient.New(bitcoinConfig, chain.ID(), zerolog.Nop()) if err != nil { return errors.Wrap(err, "unable to create rpc client") } diff --git a/cmd/zetae2e/config/clients.go b/cmd/zetae2e/config/clients.go index 51c0ff617a..1a3309e1e9 100644 --- a/cmd/zetae2e/config/clients.go +++ b/cmd/zetae2e/config/clients.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "github.com/btcsuite/btcd/rpcclient" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/ethclient" "github.com/gagliardetto/solana-go/rpc" + "github.com/rs/zerolog" ton "github.com/tonkeeper/tongo/liteapi" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -15,9 +15,12 @@ import ( "github.com/zeta-chain/node/e2e/config" "github.com/zeta-chain/node/e2e/runner" tonrunner "github.com/zeta-chain/node/e2e/runner/ton" + "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/retry" zetacore_rpc "github.com/zeta-chain/node/pkg/rpc" + btcclient "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" tonconfig "github.com/zeta-chain/node/zetaclient/chains/ton" + zetaclientconfig "github.com/zeta-chain/node/zetaclient/config" ) // getClientsFromConfig get clients from config @@ -72,27 +75,27 @@ func getClientsFromConfig(ctx context.Context, conf config.Config, account confi } // getBtcClient get btc client -func getBtcClient(rpcConf config.BitcoinRPC) (*rpcclient.Client, error) { - var param string - switch rpcConf.Params { +func getBtcClient(e2eConfig config.BitcoinRPC) (*btcclient.Client, error) { + cfg := zetaclientconfig.BTCConfig{ + RPCUsername: e2eConfig.User, + RPCPassword: e2eConfig.Pass, + RPCHost: e2eConfig.Host, + RPCParams: string(e2eConfig.Params), + } + + var chain chains.Chain + switch e2eConfig.Params { case config.Regnet: + chain = chains.BitcoinRegtest case config.Testnet3: - param = "testnet3" + chain = chains.BitcoinTestnet case config.Mainnet: - param = "mainnet" + chain = chains.BitcoinMainnet default: - return nil, fmt.Errorf("invalid bitcoin params %s", rpcConf.Params) + return nil, fmt.Errorf("invalid bitcoin params %s", e2eConfig.Params) } - connCfg := &rpcclient.ConnConfig{ - Host: rpcConf.Host, - User: rpcConf.User, - Pass: rpcConf.Pass, - HTTPPostMode: rpcConf.HTTPPostMode, - DisableTLS: rpcConf.DisableTLS, - Params: param, - } - return rpcclient.New(connCfg, nil) + return btcclient.New(cfg, chain.ChainId, zerolog.Nop()) } // getEVMClient get evm client diff --git a/e2e/config/config.go b/e2e/config/config.go index e208ecb04c..6bb69c22b5 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -106,12 +106,10 @@ type RPCs struct { // BitcoinRPC contains the configuration for the Bitcoin RPC endpoint type BitcoinRPC struct { - User string `yaml:"user"` - Pass string `yaml:"pass"` - Host string `yaml:"host"` - HTTPPostMode bool `yaml:"http_post_mode"` - DisableTLS bool `yaml:"disable_tls"` - Params BitcoinNetworkType `yaml:"params"` + User string `yaml:"user"` + Pass string `yaml:"pass"` + Host string `yaml:"host"` + Params BitcoinNetworkType `yaml:"params"` } // Contracts contains the addresses of predeployed contracts @@ -166,12 +164,10 @@ func DefaultConfig() Config { Zevm: "http://zetacore0:8545", EVM: "http://eth:8545", Bitcoin: BitcoinRPC{ - Host: "bitcoin:18443", - User: "smoketest", - Pass: "123", - HTTPPostMode: true, - DisableTLS: true, - Params: Regnet, + Host: "bitcoin:18443", + User: "smoketest", + Pass: "123", + Params: Regnet, }, ZetaCoreGRPC: "zetacore0:9090", ZetaCoreRPC: "http://zetacore0:26657", diff --git a/e2e/e2etests/helpers.go b/e2e/e2etests/helpers.go index a06149139e..d1820491a8 100644 --- a/e2e/e2etests/helpers.go +++ b/e2e/e2etests/helpers.go @@ -59,7 +59,7 @@ func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) hash, err := chainhash.NewHashFromStr(outTxHash) require.NoError(r, err) - rawTx, err := r.BtcRPCClient.GetRawTransactionVerbose(hash) + rawTx, err := r.BtcRPCClient.GetRawTransactionVerbose(r.Ctx, hash) require.NoError(r, err) r.Logger.Info("raw tx:") diff --git a/e2e/e2etests/test_crosschain_swap.go b/e2e/e2etests/test_crosschain_swap.go index d13e8d9057..466525b95d 100644 --- a/e2e/e2etests/test_crosschain_swap.go +++ b/e2e/e2etests/test_crosschain_swap.go @@ -159,7 +159,7 @@ func TestCrosschainSwap(r *runner.E2ERunner, _ []string) { outboundHash, err := chainhash.NewHashFromStr(cctx.GetCurrentOutboundParam().Hash) require.NoError(r, err) - txraw, err := r.BtcRPCClient.GetRawTransactionVerbose(outboundHash) + txraw, err := r.BtcRPCClient.GetRawTransactionVerbose(r.Ctx, outboundHash) require.NoError(r, err) r.Logger.Info("out txid %s", txraw.Txid) diff --git a/e2e/runner/accounting.go b/e2e/runner/accounting.go index 48472af445..be54db2d17 100644 --- a/e2e/runner/accounting.go +++ b/e2e/runner/accounting.go @@ -99,7 +99,7 @@ func (r *E2ERunner) CheckBTCTSSBalance() error { if err != nil { continue } - utxos, err := r.BtcRPCClient.ListUnspent() + utxos, err := r.BtcRPCClient.ListUnspent(r.Ctx) if err != nil { continue } diff --git a/e2e/runner/balances.go b/e2e/runner/balances.go index 5de1e9b9ff..cb2d8b3c05 100644 --- a/e2e/runner/balances.go +++ b/e2e/runner/balances.go @@ -162,7 +162,7 @@ func (r *E2ERunner) GetBitcoinBalance() (string, error) { // GetBitcoinBalanceByAddress get btc balance by address. func (r *E2ERunner) GetBitcoinBalanceByAddress(address btcutil.Address) (btcutil.Amount, error) { - unspentList, err := r.BtcRPCClient.ListUnspentMinMaxAddresses(1, 9999999, []btcutil.Address{address}) + unspentList, err := r.BtcRPCClient.ListUnspentMinMaxAddresses(r.Ctx, 1, 9999999, []btcutil.Address{address}) if err != nil { return 0, errors.Wrap(err, "failed to list unspent") } diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index 619844a30c..8e61312bbb 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -32,6 +32,7 @@ import ( func (r *E2ERunner) ListDeployerUTXOs() ([]btcjson.ListUnspentResult, error) { // query UTXOs from node utxos, err := r.BtcRPCClient.ListUnspentMinMaxAddresses( + r.Ctx, 1, 9999999, []btcutil.Address{r.BTCDeployerAddress}, @@ -59,6 +60,7 @@ func (r *E2ERunner) ListDeployerUTXOs() ([]btcjson.ListUnspentResult, error) { func (r *E2ERunner) GetTop20UTXOsForTssAddress() ([]btcjson.ListUnspentResult, error) { // query UTXOs from node utxos, err := r.BtcRPCClient.ListUnspentMinMaxAddresses( + r.Ctx, 0, 9999999, []btcutil.Address{r.BTCTSSAddress}, @@ -256,7 +258,7 @@ func (r *E2ERunner) sendToAddrFromDeployerWithMemo( // create raw r.Logger.Info("ADDRESS: %s, %s", btcDeployerAddress.EncodeAddress(), to.EncodeAddress()) - tx, err := btcRPC.CreateRawTransaction(inputs, amountMap, nil) + tx, err := btcRPC.CreateRawTransaction(r.Ctx, inputs, amountMap, nil) require.NoError(r, err) // this adds a OP_RETURN + single BYTE len prefix to the data @@ -293,22 +295,23 @@ func (r *E2ERunner) sendToAddrFromDeployerWithMemo( } } - stx, signed, err := btcRPC.SignRawTransactionWithWallet2(tx, inputsForSign) + stx, signed, err := btcRPC.SignRawTransactionWithWallet2(r.Ctx, tx, inputsForSign) require.NoError(r, err) require.True(r, signed, "btc transaction is not signed") - txid, err := btcRPC.SendRawTransaction(stx, true) + txid, err := btcRPC.SendRawTransaction(r.Ctx, stx, true) require.NoError(r, err) r.Logger.Info("txid: %+v", txid) _, err = r.GenerateToAddressIfLocalBitcoin(6, btcDeployerAddress) require.NoError(r, err) - gtx, err := btcRPC.GetTransaction(txid) + gtx, err := btcRPC.GetTransaction(r.Ctx, txid) require.NoError(r, err) r.Logger.Info("rawtx confirmation: %d", gtx.BlockIndex) - rawtx, err := btcRPC.GetRawTransactionVerbose(txid) + rawtx, err := btcRPC.GetRawTransactionVerbose(r.Ctx, txid) require.NoError(r, err) events, err := btcobserver.FilterAndParseIncomingTx( + r.Ctx, btcRPC, []btcjson.TxRawResult{*rawtx}, 0, @@ -367,7 +370,7 @@ func (r *E2ERunner) InscribeToTSSFromDeployerWithMemo( require.NoError(r, err) // submit the reveal transaction - txid, err := r.BtcRPCClient.SendRawTransaction(revealTx, true) + txid, err := r.BtcRPCClient.SendRawTransaction(r.Ctx, revealTx, true) require.NoError(r, err) r.Logger.Info("reveal txid: %s", txid.String()) @@ -394,7 +397,7 @@ func (r *E2ERunner) GenerateToAddressIfLocalBitcoin( ) ([]*chainhash.Hash, error) { // if not local bitcoin network, do nothing if r.IsLocalBitcoin() { - return r.BtcRPCClient.GenerateToAddress(numBlocks, address, nil) + return r.BtcRPCClient.GenerateToAddress(r.Ctx, numBlocks, address, nil) } return nil, nil } @@ -405,7 +408,7 @@ func (r *E2ERunner) QueryOutboundReceiverAndAmount(txid string) (string, int64) require.NoError(r, err) // query outbound raw transaction - revertTx, err := r.BtcRPCClient.GetRawTransaction(txHash) + revertTx, err := r.BtcRPCClient.GetRawTransaction(r.Ctx, txHash) require.NoError(r, err, revertTx) require.True(r, len(revertTx.MsgTx().TxOut) >= 2, "bitcoin outbound must have at least two outputs") diff --git a/e2e/runner/clients.go b/e2e/runner/clients.go index 0c1508949b..a524d0fc13 100644 --- a/e2e/runner/clients.go +++ b/e2e/runner/clients.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" - "github.com/btcsuite/btcd/rpcclient" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/ethclient" "github.com/gagliardetto/solana-go/rpc" @@ -13,6 +12,7 @@ import ( tonrunner "github.com/zeta-chain/node/e2e/runner/ton" zetacore_rpc "github.com/zeta-chain/node/pkg/rpc" + btcclient "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" ) // Clients contains all the RPC clients and gRPC clients for E2E tests @@ -20,7 +20,7 @@ type Clients struct { Zetacore zetacore_rpc.Clients // the RPC clients for external chains in the localnet - BtcRPC *rpcclient.Client + BtcRPC *btcclient.Client Solana *rpc.Client Evm *ethclient.Client EvmAuth *bind.TransactOpts diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index e7c1e67a59..6f32778952 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -8,7 +8,6 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/rpcclient" "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -48,6 +47,7 @@ import ( fungibletypes "github.com/zeta-chain/node/x/fungible/types" lightclienttypes "github.com/zeta-chain/node/x/lightclient/types" observertypes "github.com/zeta-chain/node/x/observer/types" + btcclient "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" ) type E2ERunnerOption func(*E2ERunner) @@ -86,7 +86,7 @@ type E2ERunner struct { // rpc clients ZEVMClient *ethclient.Client EVMClient *ethclient.Client - BtcRPCClient *rpcclient.Client + BtcRPCClient *btcclient.Client SolanaClient *rpc.Client // zetacored grpc clients diff --git a/e2e/runner/setup_bitcoin.go b/e2e/runner/setup_bitcoin.go index b349e14639..a3eca1e116 100644 --- a/e2e/runner/setup_bitcoin.go +++ b/e2e/runner/setup_bitcoin.go @@ -18,7 +18,7 @@ func (r *E2ERunner) AddTSSToNode() { }() // import the TSS address - err := r.BtcRPCClient.ImportAddress(r.BTCTSSAddress.EncodeAddress()) + err := r.BtcRPCClient.ImportAddress(r.Ctx, r.BTCTSSAddress.EncodeAddress()) require.NoError(r, err) // mine some blocks to get some BTC into the deployer address @@ -38,12 +38,12 @@ func (r *E2ERunner) SetupBitcoinAccounts(createWallet bool) { r.SetupBtcAddress(createWallet) // import the TSS address to index TSS utxos and transactions - err := r.BtcRPCClient.ImportAddress(r.BTCTSSAddress.EncodeAddress()) + err := r.BtcRPCClient.ImportAddress(r.Ctx, r.BTCTSSAddress.EncodeAddress()) require.NoError(r, err) r.Logger.Info("⚙️ imported BTC TSSAddress: %s", r.BTCTSSAddress.EncodeAddress()) // import deployer address to index deployer utxos and transactions - err = r.BtcRPCClient.ImportAddress(r.BTCDeployerAddress.EncodeAddress()) + err = r.BtcRPCClient.ImportAddress(r.Ctx, r.BTCDeployerAddress.EncodeAddress()) require.NoError(r, err) r.Logger.Info("⚙️ imported BTCDeployerAddress: %s", r.BTCDeployerAddress.EncodeAddress()) } @@ -98,12 +98,12 @@ func (r *E2ERunner) SetupBtcAddress(createWallet bool) { require.NoError(r, err) argsRawMsg = append(argsRawMsg, encodedArg) } - _, err := r.BtcRPCClient.RawRequest("createwallet", argsRawMsg) + _, err := r.BtcRPCClient.RawRequest(r.Ctx, "createwallet", argsRawMsg) if err != nil { require.ErrorContains(r, err, "Database already exists") } - err = r.BtcRPCClient.ImportPrivKeyRescan(privkeyWIF, r.Name, true) + err = r.BtcRPCClient.ImportPrivKeyRescan(r.Ctx, privkeyWIF, r.Name, true) require.NoError(r, err, "failed to execute ImportPrivKeyRescan") } } diff --git a/zetaclient/chains/bitcoin/client/client.go b/zetaclient/chains/bitcoin/client/client.go new file mode 100644 index 0000000000..c501cd5cdb --- /dev/null +++ b/zetaclient/chains/bitcoin/client/client.go @@ -0,0 +1,268 @@ +// Package client implements a Bitcoin RPC with support for context, logging, and metrics. +// +// Portions of this package in `./commands.go` are derived from or inspired by btcd, +// which is licensed under the ISC License. +// +// # ISC License +// +// Copyright (c) 2013-2024 The btcsuite developers +// Copyright (c) 2015-2016 The Decred developers +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +package client + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + types "github.com/btcsuite/btcd/btcjson" + chains "github.com/btcsuite/btcd/chaincfg" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "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" +) + +// Client Bitcoin RPC client +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 *types.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) +) + +var _ client = (*Client)(nil) + +func WithHTTP(httpClient *http.Client) Opt { + return func(c *Client) { c.client = httpClient } +} + +// 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") + } + + clientName := fmt.Sprintf("btc:%d", chainID) + + c := &Client{ + hostURL: normalizeHostURL(cfg.RPCHost, true), + client: defaultHTTPClient(), + config: cfg, + params: params, + clientName: clientName, + logger: logger.With(). + Str(logs.FieldModule, "btc_client"). + Int64(logs.FieldChain, chainID). + Logger(), + } + + for _, opt := range opts { + opt(c) + } + + return c, nil +} + +// 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") + } + + req, err := c.newRequest(ctx, reqBody) + if err != nil { + return nil, errors.Wrapf(err, "unable to create http request for %q", method) + } + + 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) + } + + return out.Result, nil +} + +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 + } + + req.Header.Set("Content-Type", "application/json") + + if c.config.RPCPassword != "" || c.config.RPCUsername != "" { + req.SetBasicAuth(c.config.RPCUsername, c.config.RPCPassword) + } + + return req, nil +} + +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") + }() + + res, err := c.client.Do(req) + if err != nil { + return rawResponse{}, errors.Wrap(err, "unable to send the request") + } + + defer res.Body.Close() + + resBody, err := io.ReadAll(res.Body) + if err != nil { + return rawResponse{}, errors.Wrap(err, "unable to read response body") + } + + if res.StatusCode != http.StatusOK { + return rawResponse{}, errors.Errorf("unexpected status code %d (%s)", res.StatusCode, resBody) + } + + if err = json.Unmarshal(resBody, &out); err != nil { + return rawResponse{}, errors.Wrapf(err, "unable to unmarshal rpc response (%s)", resBody) + } + + return out, nil +} + +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" + } + + metrics.RPCClientCounter.WithLabelValues(status, c.clientName, method).Inc() + metrics.RPCClientDuration.WithLabelValues(c.clientName).Observe(dur) +} + +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") + } + + body, err := types.MarshalCmd(rpcVersion, commandID, cmd) + if err != nil { + return "", nil, errors.Wrapf(err, "unable to marshal cmd %q", methodName) + } + + return methodName, body, nil +} + +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) + } + + return tt, nil +} + +func unmarshalPtr[T any](raw json.RawMessage) (*T, error) { + tt, err := unmarshal[T](raw) + if err != nil { + return nil, err + } + + return &tt, nil +} + +func unmarshalHex(raw json.RawMessage) ([]byte, error) { + str, err := unmarshal[string](raw) + if err != nil { + return nil, err + } + + return hex.DecodeString(str) +} + +func resolveParams(name string) (chains.Params, error) { + const regNetAlias = "regnet" + + switch name { + case chains.MainNetParams.Name: + return chains.MainNetParams, nil + case chains.TestNet3Params.Name: + return chains.TestNet3Params, nil + case chaincfg.RegressionNetParams.Name, regNetAlias: + return chains.RegressionNetParams, nil + case chaincfg.SimNetParams.Name: + return chains.SimNetParams, nil + default: + return chains.Params{}, fmt.Errorf("unknown chain params %q", name) + } +} + +func normalizeHostURL(host string, disableHTTPS bool) string { + if strings.HasPrefix(host, "http://") || strings.HasPrefix(host, "https://") { + return host + } + + protocol := "http" + if !disableHTTPS { + protocol = "https" + } + + return fmt.Sprintf("%s://%s", protocol, host) +} + +func defaultHTTPClient() *http.Client { + return &http.Client{ + Transport: http.DefaultTransport, + Timeout: 10 * time.Second, + } +} diff --git a/zetaclient/chains/bitcoin/client/client_test.go b/zetaclient/chains/bitcoin/client/client_test.go new file mode 100644 index 0000000000..d4b4ffd140 --- /dev/null +++ b/zetaclient/chains/bitcoin/client/client_test.go @@ -0,0 +1,591 @@ +package client_test + +import ( + "context" + "encoding/hex" + "fmt" + "math/big" + "os" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcd/blockchain" + types "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" + btc "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/node/zetaclient/common" + "github.com/zeta-chain/node/zetaclient/config" + "github.com/zeta-chain/node/zetaclient/testutils" + "github.com/zeta-chain/node/zetaclient/testutils/testlog" +) + +// TestClientLive runs tests on a real note. +// Note that t.Parallel() is avoided due to potential rate limiting. +// +// You can get a free btc mainnet & testnet @ nownodes.io +// - mainnet: "https://btc.nownodes.io/ +// - testnet: "https://btc-testnet.nownodes.io/ +func TestClientLive(t *testing.T) { + if !common.LiveTestEnabled() { + t.Skip("skipping live test") + } + + mainnetConfig := config.BTCConfig{ + RPCHost: os.Getenv(common.EnvBtcRPCMainnet), + RPCParams: "mainnet", + } + + testnetConfig := config.BTCConfig{ + RPCHost: os.Getenv(common.EnvBtcRPCTestnet), + RPCParams: "testnet3", + } + + t.Run("Healthcheck", func(t *testing.T) { + t.Skip("most rpc won't allow private methods e.g. listUnspentMinMaxAddresses") + + // ARRANGE + ts := newTestSuite(t, mainnetConfig) + + tssAddress, err := chains.DecodeBtcAddress(testutils.TSSAddressBTCMainnet, ts.chain.ChainId) + require.NoError(t, err) + + // ACT + _, err = ts.Healthcheck(ts.ctx, tssAddress) + + // ASSERT + require.NoError(t, err) + }) + + t.Run("GetBlockCount", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, mainnetConfig) + + // ACT + bn, err := ts.GetBlockCount(ts.ctx) + + // ASSERT + require.NoError(t, err) + require.True(t, bn > 879_088) + }) + + t.Run("GetBlockHash", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, mainnetConfig) + + // ACT + hash, err := ts.GetBlockHash(ts.ctx, 879088) + + // ASSERT + require.NoError(t, err) + require.NotEmpty(t, hash) + + // ACT #2 + block, err := ts.GetBlockHeader(ts.ctx, hash) + + // ASSERT #2 + require.NoError(t, err) + require.NotEmpty(t, block) + }) + + t.Run("GetBlockHeightByStr", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, mainnetConfig) + + // the block hashes to test + expectedHeight := int64(835053) + hash := "00000000000000000000994a5d12976ec5bda078a7b9c27981f0a4e7a6d46d23" + invalidHash := "invalidhash" + + // ACT #1 + // get block by invalid has + _, err := ts.GetBlockHeightByStr(ts.ctx, invalidHash) + require.ErrorContains(t, err, "unable to create btc hash from string") + + // ACT #2 + // get block height by block hash + height, err := ts.GetBlockHeightByStr(ts.ctx, hash) + require.NoError(t, err) + require.Equal(t, expectedHeight, height) + }) + + t.Run("FilterAndParseIncomingTx", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, testnetConfig) + + // get the block that contains the incoming tx + hashStr := "0000000000000032cb372f5d5d99c1ebf4430a3059b67c47a54dd626550fb50d" + + block, err := ts.GetBlockVerboseByStr(ts.ctx, hashStr) + require.NoError(t, err) + + inbounds, err := observer.FilterAndParseIncomingTx( + ts.ctx, + ts.Client, + block.Tx, + uint64(block.Height), + "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", + ts.Logger, + &chaincfg.TestNet3Params, + ) + + require.NoError(t, err) + require.Len(t, inbounds, 1) + require.Equal(t, inbounds[0].Value, 0.0001) + require.Equal(t, inbounds[0].ToAddress, "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2") + + // the text memo is base64 std encoded string:DSRR1RmDCwWmxqY201/TMtsJdmA= + // see https://blockstream.info/testnet/tx/889bfa69eaff80a826286d42ec3f725fd97c3338357ddc3a1f543c2d6266f797 + memo, err := hex.DecodeString("4453525231526d444377576d7871593230312f544d74734a646d413d") + require.NoError(t, err) + require.Equal(t, inbounds[0].MemoBytes, memo) + require.Equal(t, inbounds[0].FromAddress, "tb1qyslx2s8evalx67n88wf42yv7236303ezj3tm2l") + require.Equal(t, inbounds[0].BlockNumber, uint64(2406185)) + require.Equal(t, inbounds[0].TxHash, "889bfa69eaff80a826286d42ec3f725fd97c3338357ddc3a1f543c2d6266f797") + }) + + t.Run("FilterAndParseIncomingTxNoop", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, testnetConfig) + + // get a block that contains no incoming tx + hashStr := "000000000000002fd8136dbf91708898da9d6ae61d7c354065a052568e2f2888" + + block, err := ts.GetBlockVerboseByStr(ts.ctx, hashStr) + require.NoError(t, err) + + // filter incoming tx + inbounds, err := observer.FilterAndParseIncomingTx( + ts.ctx, + ts.Client, + block.Tx, + uint64(block.Height), + "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", + ts.Logger, + &chaincfg.TestNet3Params, + ) + + require.NoError(t, err) + require.Empty(t, inbounds) + }) + + t.Run("GetRecentFeeRate", func(t *testing.T) { + // ARRANGE + // setup Bitcoin testnet client + ts := newTestSuite(t, testnetConfig) + + // ACT + // get fee rate from recent blocks + feeRate, err := btc.GetRecentFeeRate(ts.ctx, ts.Client, &chaincfg.TestNet3Params) + + // ASSERT + require.NoError(t, err) + require.Greater(t, feeRate, uint64(0)) + }) + + // LiveTestBitcoinFeeRate query Bitcoin mainnet fee rate every 5 minutes + // and compares Conservative and Economical fee rates for different block targets (1 and 2) + t.Run("BitcoinFeeRate", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, mainnetConfig) + bn, err := ts.GetBlockCount(ts.ctx) + require.NoError(t, err) + + // get fee rate for 1 block target + feeRateConservative1, errCon1 := ts.getFeeRate(1, &types.EstimateModeConservative) + if errCon1 != nil { + t.Error(errCon1) + } + + feeRateEconomical1, errEco1 := ts.getFeeRate(1, &types.EstimateModeEconomical) + if errEco1 != nil { + t.Error(errEco1) + } + + // get fee rate for 2 block target + feeRateConservative2, errCon2 := ts.getFeeRate(2, &types.EstimateModeConservative) + if errCon2 != nil { + t.Error(errCon2) + } + + feeRateEconomical2, errEco2 := ts.getFeeRate(2, &types.EstimateModeEconomical) + if errEco2 != nil { + t.Error(errEco2) + } + + fmt.Printf( + "Block: %d, Conservative-1 fee rate: %d, Economical-1 fee rate: %d\n", + bn, + feeRateConservative1.Uint64(), + feeRateEconomical1.Uint64(), + ) + fmt.Printf( + "Block: %d, Conservative-2 fee rate: %d, Economical-2 fee rate: %d\n", + bn, + feeRateConservative2.Uint64(), + feeRateEconomical2.Uint64(), + ) + + // monitor fee rate every 5 minutes, adjust the iteration count as needed + for i := 0; i < 1; i++ { + // please uncomment this interval for long running test + //time.Sleep(time.Duration(5) * time.Minute) + + bn, err = ts.GetBlockCount(ts.ctx) + feeRateConservative1, errCon1 = ts.getFeeRate(1, &types.EstimateModeConservative) + feeRateEconomical1, errEco1 = ts.getFeeRate(1, &types.EstimateModeEconomical) + feeRateConservative2, errCon2 = ts.getFeeRate(2, &types.EstimateModeConservative) + feeRateEconomical2, errEco2 = ts.getFeeRate(2, &types.EstimateModeEconomical) + if err != nil || errCon1 != nil || errEco1 != nil || errCon2 != nil || errEco2 != nil { + continue + } + require.True(t, feeRateConservative1.Uint64() >= feeRateEconomical1.Uint64()) + require.True(t, feeRateConservative2.Uint64() >= feeRateEconomical2.Uint64()) + require.True(t, feeRateConservative1.Uint64() >= feeRateConservative2.Uint64()) + require.True(t, feeRateEconomical1.Uint64() >= feeRateEconomical2.Uint64()) + fmt.Printf( + "Block: %d, Conservative-1 fee rate: %d, Economical-1 fee rate: %d\n", + bn, + feeRateConservative1.Uint64(), + feeRateEconomical1.Uint64(), + ) + fmt.Printf( + "Block: %d, Conservative-2 fee rate: %d, Economical-2 fee rate: %d\n", + bn, + feeRateConservative2.Uint64(), + feeRateEconomical2.Uint64(), + ) + } + }) + + t.Run("GetSenderByVin", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, mainnetConfig) + + // net params + net, err := chains.GetBTCChainParams(ts.chain.ChainId) + require.NoError(t, err) + + // calculates block range to test + startBlock, err := ts.GetBlockCount(ts.ctx) + require.NoError(t, err) + + // go back to whatever block as needed + endBlock := startBlock - 1 + + // loop through mempool.space blocks backwards + BLOCKLOOP: + for bn := startBlock; bn >= endBlock; { + // get mempool.space txs for the block + _, mempoolTxs, err := ts.getMemPoolSpaceTxsByBlock(bn, false) + if err != nil { + time.Sleep(3 * time.Second) + continue + } + + // loop through each tx in the block + for i, mptx := range mempoolTxs { + // sample 10 txs per block + if i >= 10 { + break + } + for _, mpvin := range mptx.Vin { + // skip coinbase tx + if mpvin.IsCoinbase { + continue + } + // get sender address for each vin + vin := types.Vin{ + Txid: mpvin.TxID, + Vout: mpvin.Vout, + } + senderAddr, err := observer.GetSenderAddressByVin(ts.ctx, ts.Client, vin, net) + if err != nil { + fmt.Printf("error GetSenderAddressByVin for block %d, tx %s vout %d: %s\n", bn, vin.Txid, vin.Vout, err) + time.Sleep(3 * time.Second) + continue BLOCKLOOP // retry the block + } + if senderAddr != mpvin.Prevout.ScriptpubkeyAddress { + t.Errorf("block %d, tx %s, vout %d: want %s, got %s\n", bn, vin.Txid, vin.Vout, mpvin.Prevout.ScriptpubkeyAddress, senderAddr) + } else { + fmt.Printf("block: %d sender address type: %s\n", bn, mpvin.Prevout.ScriptpubkeyType) + } + } + } + bn-- + time.Sleep(100 * time.Millisecond) + } + }) + + t.Run("GetTransactionFeeAndRate", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, testnetConfig) + + // calculates block range to test + startBlock, err := ts.GetBlockCount(ts.ctx) + require.NoError(t, err) + + // go back whatever blocks as needed + endBlock := startBlock - 100 + + // loop through mempool.space blocks backwards + for bn := startBlock; bn >= endBlock; { + // get mempool.space txs for the block + blkHash, mempoolTxs, err := ts.getMemPoolSpaceTxsByBlock(bn, false) + if err != nil { + time.Sleep(3 * time.Second) + continue + } + + // get the block from rpc client + block, err := ts.GetBlockVerbose(ts.ctx, blkHash) + if err != nil { + time.Sleep(3 * time.Second) + continue + } + + // loop through each tx in the block (skip coinbase tx) + for i := 1; i < len(block.Tx); { + // sample 20 txs per block + if i >= 20 { + break + } + + // the two txs from two different sources + tx := block.Tx[i] + mpTx := mempoolTxs[i] + require.Equal(t, tx.Txid, mpTx.TxID) + + // get transaction fee rate for the raw result + fee, feeRate, err := ts.GetTransactionFeeAndRate(ts.ctx, &tx) + if err != nil { + t.Logf("error GetTransactionFeeRate %s: %s\n", mpTx.TxID, err) + continue + } + require.EqualValues(t, mpTx.Fee, fee) + require.EqualValues(t, mpTx.Weight, tx.Weight) + + // calculate mempool.space fee rate + vBytes := mpTx.Weight / blockchain.WitnessScaleFactor + mpFeeRate := int64(mpTx.Fee / vBytes) + + // compare our fee rate with mempool.space fee rate + var diff int64 + var diffPercent float64 + if feeRate == mpFeeRate { + fmt.Printf("tx %s: [our rate] %5d == %5d [mempool.space]", mpTx.TxID, feeRate, mpFeeRate) + } else if feeRate > mpFeeRate { + diff = feeRate - mpFeeRate + fmt.Printf("tx %s: [our rate] %5d > %5d [mempool.space]", mpTx.TxID, feeRate, mpFeeRate) + } else { + diff = mpFeeRate - feeRate + fmt.Printf("tx %s: [our rate] %5d < %5d [mempool.space]", mpTx.TxID, feeRate, mpFeeRate) + } + + // print the diff percentage + diffPercent = float64(diff) / float64(mpFeeRate) * 100 + if diff > 0 { + fmt.Printf(", diff: %f%%\n", diffPercent) + } else { + fmt.Printf("\n") + } + + // the expected diff percentage should be within 5% + if mpFeeRate >= 20 { + require.LessOrEqual(t, diffPercent, 5.0) + } else { + // for small fee rate, the absolute diff should be within 1 satoshi/vByte + require.LessOrEqual(t, diff, int64(1)) + } + + // next tx + i++ + } + + bn-- + time.Sleep(100 * time.Millisecond) + } + }) + + t.Run("AvgFeeRateMainnetMempoolSpace", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, mainnetConfig) + + // test against mempool.space API for 10000 blocks + // startBlock := 210000 * 3 // 3rd halving + startBlock := 829596 + endBlock := startBlock - 1 // go back to whatever block as needed + + // ACT + ts.compareAvgFeeRate(startBlock, endBlock, false) + }) + + t.Run("AvgFeeRateTestnetMempoolSpace", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, testnetConfig) + + // test against mempool.space API for 10000 blocks + //startBlock := 210000 * 12 // 12th halving + startBlock := 2577600 + endBlock := startBlock - 1 // go back to whatever block as needed + + // ACT + ts.compareAvgFeeRate(startBlock, endBlock, true) + }) + + t.Run("CalcDepositorFee", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t, mainnetConfig) + + // test tx hash + // https://mempool.space/tx/8dc0d51f83810cec7fcb5b194caebfc5fc64b10f9fe21845dfecc621d2a28538 + hash, err := chainhash.NewHashFromStr("8dc0d51f83810cec7fcb5b194caebfc5fc64b10f9fe21845dfecc621d2a28538") + require.NoError(t, err) + + // get the raw transaction result + rawResult, err := ts.GetRawTransactionVerbose(ts.ctx, hash) + require.NoError(t, err) + + t.Run("should return default depositor fee", func(t *testing.T) { + depositorFee, err := btc.CalcDepositorFee(ts.ctx, ts.Client, rawResult, &chaincfg.RegressionNetParams) + require.NoError(t, err) + require.Equal(t, btc.DefaultDepositorFee, depositorFee) + }) + + t.Run("should return correct depositor fee for a given tx", func(t *testing.T) { + depositorFee, err := btc.CalcDepositorFee(ts.ctx, ts.Client, rawResult, &chaincfg.MainNetParams) + require.NoError(t, err) + + // the actual fee rate is 860 sat/vByte + // #nosec G115 always in range + expectedRate := int64(float64(860) * common.BTCOutboundGasPriceMultiplier) + expectedFee := btc.DepositorFee(expectedRate) + require.Equal(t, expectedFee, depositorFee) + }) + }) +} + +type testSuite struct { + t *testing.T + *testlog.Log + *client.Client + ctx context.Context + chain chains.Chain +} + +func newTestSuite(t *testing.T, cfg config.BTCConfig) *testSuite { + logger := testlog.New(t) + + require.True(t, cfg.RPCParams == "mainnet" || cfg.RPCParams == "testnet3") + + chain := chains.BitcoinMainnet + if cfg.RPCParams == "testnet3" { + chain = chains.BitcoinTestnet + } + + c, err := client.New(cfg, chain.ChainId, logger.Logger) + require.NoError(t, err) + + return &testSuite{ + t: t, + Log: logger, + Client: c, + ctx: context.Background(), + chain: chain, + } +} + +// getMemPoolSpaceTxsByBlock gets mempool.space txs for a given block +func (ts *testSuite) getMemPoolSpaceTxsByBlock( + blkNumber int64, + testnet bool, +) (*chainhash.Hash, []testutils.MempoolTx, error) { + blkHash, err := ts.GetBlockHash(ts.ctx, blkNumber) + if err != nil { + return nil, nil, err + } + + // get mempool.space txs for the block + mempoolTxs, err := testutils.GetBlockTxs(ts.ctx, blkHash.String(), testnet) + if err != nil { + return nil, nil, err + } + + return blkHash, mempoolTxs, nil +} + +func (ts *testSuite) getFeeRate(confTarget int64, estimateMode *types.EstimateSmartFeeMode) (*big.Int, error) { + feeResult, err := ts.EstimateSmartFee(ts.ctx, confTarget, estimateMode) + if err != nil { + return nil, err + } + + if feeResult.Errors != nil { + return nil, errors.New(strings.Join(feeResult.Errors, ", ")) + } + + if feeResult.FeeRate == nil { + return nil, errors.New("fee rate is nil") + } + + return new(big.Int).SetInt64(int64(*feeResult.FeeRate * 1e8)), nil +} + +func (ts *testSuite) compareAvgFeeRate(startBlock int, endBlock int, testnet bool) { + // mempool.space return 15 blocks [bn-14, bn] per request + for bn := startBlock; bn >= endBlock; { + // get mempool.space return blocks in descending order [bn, bn-14] + mempoolBlocks, err := testutils.GetBlocks(context.Background(), bn, testnet) + if err != nil { + fmt.Printf("error GetBlocks %d: %s\n", bn, err) + time.Sleep(10 * time.Second) + continue + } + + // calculate gas rate for each block + for _, mb := range mempoolBlocks { + // stop on end block + if mb.Height < endBlock { + break + } + bn = int(mb.Height) - 1 + + // get block hash + blkHash, err := ts.GetBlockHash(ts.ctx, int64(mb.Height)) + if err != nil { + fmt.Printf("error: %s\n", err) + continue + } + // get block + blockVb, err := ts.GetBlockVerbose(ts.ctx, blkHash) + if err != nil { + fmt.Printf("error: %s\n", err) + continue + } + // calculate gas rate + netParams := &chaincfg.MainNetParams + if testnet { + netParams = &chaincfg.TestNet3Params + } + gasRate, err := btc.CalcBlockAvgFeeRate(blockVb, netParams) + require.NoError(ts.t, err) + + // compare with mempool.space + if int(gasRate) == mb.Extras.AvgFeeRate { + fmt.Printf("block %d: gas rate %d == mempool.space gas rate\n", mb.Height, gasRate) + } else if int(gasRate) > mb.Extras.AvgFeeRate { + fmt.Printf("block %d: gas rate %d > mempool.space gas rate %d, diff: %f percent\n", + mb.Height, gasRate, mb.Extras.AvgFeeRate, float64(int(gasRate)-mb.Extras.AvgFeeRate)/float64(mb.Extras.AvgFeeRate)*100) + } else { + fmt.Printf("block %d: gas rate %d < mempool.space gas rate %d, diff: %f percent\n", + mb.Height, gasRate, mb.Extras.AvgFeeRate, float64(mb.Extras.AvgFeeRate-int(gasRate))/float64(mb.Extras.AvgFeeRate)*100) + } + } + } +} diff --git a/zetaclient/chains/bitcoin/client/commands.go b/zetaclient/chains/bitcoin/client/commands.go new file mode 100644 index 0000000000..1a7954d6f1 --- /dev/null +++ b/zetaclient/chains/bitcoin/client/commands.go @@ -0,0 +1,431 @@ +package client + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + + types "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/wire" + "github.com/pkg/errors" +) + +const ( + // github.com/btcsuite/btcd@v0.24.2/rpcclient/rawtransactions.go:22 + defaultMaxFeeRate types.BTCPerkvB = 0.1 +) + +func (c *Client) Ping(ctx context.Context) error { + _, err := c.GetBlockCount(ctx) + return errors.Wrap(err, "ping failed") +} + +func (c *Client) GetNetworkInfo(ctx context.Context) (*types.GetNetworkInfoResult, error) { + out, err := c.sendCommand(ctx, types.NewGetNetworkInfoCmd()) + if err != nil { + return nil, errors.Wrap(err, "unable to get network info") + } + + return unmarshalPtr[types.GetNetworkInfoResult](out) +} + +func (c *Client) GetBlockCount(ctx context.Context) (int64, error) { + out, err := c.sendCommand(ctx, types.NewGetBlockCountCmd()) + if err != nil { + return 0, errors.Wrap(err, "unable to get block count") + } + + return unmarshal[int64](out) +} + +func (c *Client) GetBlockHash(ctx context.Context, blockHeight int64) (*chainhash.Hash, error) { + out, err := c.sendCommand(ctx, types.NewGetBlockHashCmd(blockHeight)) + if err != nil { + return nil, errors.Wrapf(err, "unable to get block hash for %d", blockHeight) + } + + str, err := unmarshal[string](out) + if err != nil { + return nil, errors.Wrap(err, "unable to unmarshal block hash") + } + + return chainhash.NewHashFromStr(str) +} + +func (c *Client) GetBlockHeader(ctx context.Context, hash *chainhash.Hash) (*wire.BlockHeader, error) { + cmd := types.NewGetBlockHeaderCmd(hash.String(), types.Bool(false)) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrapf(err, "unable to get block header for %s", hash.String()) + } + + serializedBH, err := unmarshalHex(out) + if err != nil { + return nil, errors.Wrap(err, "unable to decode hex") + } + + var bh wire.BlockHeader + if err = bh.Deserialize(bytes.NewReader(serializedBH)); err != nil { + return nil, errors.Wrap(err, "unable to deserialize block header") + } + + return &bh, nil +} + +func (c *Client) GetBlockVerbose(ctx context.Context, hash *chainhash.Hash) (*types.GetBlockVerboseTxResult, error) { + cmd := types.NewGetBlockCmd(hash.String(), types.Int(2)) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to get block hash verbose") + } + + return unmarshalPtr[types.GetBlockVerboseTxResult](out) +} + +func (c *Client) GetTransaction(ctx context.Context, hash *chainhash.Hash) (*types.GetTransactionResult, error) { + out, err := c.sendCommand(ctx, types.NewGetTransactionCmd(hash.String(), nil)) + if err != nil { + return nil, errors.Wrap(err, "unable to get transaction") + } + + return unmarshalPtr[types.GetTransactionResult](out) +} + +func (c *Client) GetRawTransaction(ctx context.Context, hash *chainhash.Hash) (*btcutil.Tx, error) { + cmd := types.NewGetRawTransactionCmd(hash.String(), types.Int(0)) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to get raw tx") + } + + // Decode the serialized transaction hex to raw bytes. + serializedTx, err := unmarshalHex(out) + if err != nil { + return nil, errors.Wrap(err, "unable to decode raw tx") + } + + // Deserialize the transaction and return it. + var msgTx wire.MsgTx + if err = msgTx.Deserialize(bytes.NewReader(serializedTx)); err != nil { + return nil, errors.Wrap(err, "unable to deserialize raw tx") + } + + return btcutil.NewTx(&msgTx), nil +} + +func (c *Client) GetRawTransactionVerbose(ctx context.Context, hash *chainhash.Hash) (*types.TxRawResult, error) { + cmd := types.NewGetRawTransactionCmd(hash.String(), types.Int(1)) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to get raw tx verbose") + } + + return unmarshalPtr[types.TxRawResult](out) +} + +// SendRawTransaction github.com/btcsuite/btcd@v0.24.2/rpcclient/rawtransactions.go +func (c *Client) SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { + if tx == nil { + return nil, errors.New("tx is nil") + } + + // Serialize the transaction and convert to hex string. + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + if err := tx.Serialize(buf); err != nil { + return nil, errors.Wrap(err, "unable to serialize tx") + } + + txHex := hex.EncodeToString(buf.Bytes()) + + // Using a 0 MaxFeeRate is interpreted as a maximum fee rate not + // being enforced by bitcoind. + var maxFeeRate types.BTCPerkvB + if !allowHighFees { + maxFeeRate = defaultMaxFeeRate + } + + cmd := types.NewBitcoindSendRawTransactionCmd(txHex, maxFeeRate) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to send raw tx") + } + + txHashStr, err := unmarshal[string](out) + if err != nil { + return nil, errors.Wrap(err, "unable to unmarshal tx hash") + } + + return chainhash.NewHashFromStr(txHashStr) +} + +func (c *Client) EstimateSmartFee( + ctx context.Context, + confTarget int64, + mode *types.EstimateSmartFeeMode, +) (*types.EstimateSmartFeeResult, error) { + cmd := types.NewEstimateSmartFeeCmd(confTarget, mode) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to estimate smart fee") + } + + return unmarshalPtr[types.EstimateSmartFeeResult](out) +} + +func (c *Client) ListUnspent(ctx context.Context) ([]types.ListUnspentResult, error) { + cmd := types.NewListUnspentCmd(nil, nil, nil) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to list unspent") + } + + return unmarshal[[]types.ListUnspentResult](out) +} + +func (c *Client) ListUnspentMinMaxAddresses( + ctx context.Context, + minConf, maxConf int, + addresses []btcutil.Address, +) ([]types.ListUnspentResult, error) { + stringAddresses := make([]string, 0, len(addresses)) + for _, a := range addresses { + stringAddresses = append(stringAddresses, a.EncodeAddress()) + } + + cmd := types.NewListUnspentCmd(&minConf, &maxConf, &stringAddresses) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to list unspent") + } + + return unmarshal[[]types.ListUnspentResult](out) +} + +func (c *Client) CreateWallet( + ctx context.Context, + name string, + opts ...rpcclient.CreateWalletOpt, +) (*types.CreateWalletResult, error) { + cmd := types.NewCreateWalletCmd(name, nil, nil, nil, nil) + for _, opt := range opts { + opt(cmd) + } + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to create wallet") + } + + return unmarshalPtr[types.CreateWalletResult](out) +} + +func (c *Client) GetBalance(ctx context.Context, account string) (btcutil.Amount, error) { + cmd := types.NewGetBalanceCmd(&account, nil) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return 0, errors.Wrap(err, "unable to get balance") + } + + balanceRaw, err := unmarshal[float64](out) + if err != nil { + return 0, errors.Wrap(err, "unable to unmarshal balance") + } + + return btcutil.NewAmount(balanceRaw) +} + +func (c *Client) GetNewAddress(ctx context.Context, account string) (btcutil.Address, error) { + cmd := types.NewGetNewAddressCmd(&account, nil) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to get new address") + } + + addr, err := unmarshal[string](out) + if err != nil { + return nil, err + } + + return btcutil.DecodeAddress(addr, &c.params) +} + +func (c *Client) GenerateToAddress( + ctx context.Context, + numBlocks int64, + address btcutil.Address, + maxTries *int64, +) ([]*chainhash.Hash, error) { + cmd := types.NewGenerateToAddressCmd(numBlocks, address.EncodeAddress(), maxTries) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to generate to address") + } + + result, err := unmarshal[[]string](out) + if err != nil { + return nil, errors.Wrap(err, "unable to unmarshal to strings") + } + + convertedResult := make([]*chainhash.Hash, len(result)) + for i, hashString := range result { + convertedResult[i], err = chainhash.NewHashFromStr(hashString) + if err != nil { + return nil, err + } + } + + return convertedResult, nil +} + +func (c *Client) CreateRawTransaction( + ctx context.Context, + inputs []types.TransactionInput, + amounts map[btcutil.Address]btcutil.Amount, + lockTime *int64, +) (*wire.MsgTx, error) { + convertedAmounts := make(map[string]float64, len(amounts)) + for addr, amount := range amounts { + convertedAmounts[addr.String()] = amount.ToBTC() + } + + cmd := types.NewCreateRawTransactionCmd(inputs, convertedAmounts, lockTime) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, errors.Wrap(err, "unable to create raw tx") + } + + // Decode the serialized transaction hex to raw bytes. + serializedTx, err := unmarshalHex(out) + if err != nil { + return nil, err + } + + // Deserialize the transaction and return it. + var msgTx wire.MsgTx + + if err = msgTx.Deserialize(bytes.NewReader(serializedTx)); err != nil { + return nil, err + } + + return &msgTx, nil +} + +func (c *Client) SignRawTransactionWithWallet2( + ctx context.Context, + tx *wire.MsgTx, + inputs []types.RawTxWitnessInput, +) (*wire.MsgTx, bool, error) { + if tx == nil { + return nil, false, errors.New("tx is nil") + } + + // Serialize the transaction and convert to hex string. + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + if err := tx.Serialize(buf); err != nil { + return nil, false, errors.Wrap(err, "unable to serialize tx") + } + + txHex := hex.EncodeToString(buf.Bytes()) + + cmd := types.NewSignRawTransactionWithWalletCmd(txHex, &inputs, nil) + + out, err := c.sendCommand(ctx, cmd) + if err != nil { + return nil, false, errors.Wrap(err, "unable to sign raw tx") + } + + result, err := unmarshalPtr[types.SignRawTransactionWithWalletResult](out) + if err != nil { + return nil, false, errors.Wrap(err, "unable to unmarshal sign raw tx result") + } + + // Decode the serialized transaction hex to raw bytes. + serializedTx, err := hex.DecodeString(result.Hex) + if err != nil { + return nil, false, err + } + + // Deserialize the transaction and return it. + var msgTx wire.MsgTx + if err = msgTx.Deserialize(bytes.NewReader(serializedTx)); err != nil { + return nil, false, err + } + + return &msgTx, result.Complete, nil +} + +func (c *Client) ImportAddress(ctx context.Context, address string) error { + cmd := types.NewImportAddressCmd(address, "", nil) + + _, err := c.sendCommand(ctx, cmd) + return err +} + +func (c *Client) ImportPrivKeyRescan(ctx context.Context, privKeyWIF *btcutil.WIF, label string, rescan bool) error { + wif := "" + if privKeyWIF != nil { + wif = privKeyWIF.String() + } + + cmd := types.NewImportPrivKeyCmd(wif, &label, &rescan) + + _, err := c.sendCommand(ctx, cmd) + return err +} + +func (c *Client) RawRequest(ctx context.Context, method string, params []json.RawMessage) (json.RawMessage, error) { + switch { + case method == "": + return nil, errors.New("no method") + case params == nil: + params = []json.RawMessage{} + } + + payload := struct { + Version string `json:"jsonrpc"` + ID uint64 `json:"id"` + Method string `json:"method"` + Params []json.RawMessage `json:"params"` + }{ + Version: string(rpcVersion), + ID: commandID, + Method: method, + Params: params, + } + + body, err := json.Marshal(payload) + if err != nil { + return nil, errors.Wrap(err, "unable to marshal body") + } + + req, err := c.newRequest(ctx, body) + if err != nil { + return nil, errors.Wrap(err, "unable to create request") + } + + res, err := c.sendRequest(req, method) + switch { + case err != nil: + return nil, errors.Wrapf(err, "%q failed", method) + case res.Error != nil: + return nil, errors.Wrapf(res.Error, "got rpc error for %q", method) + } + + return res.Result, nil +} diff --git a/zetaclient/chains/bitcoin/client/helpers.go b/zetaclient/chains/bitcoin/client/helpers.go new file mode 100644 index 0000000000..d33050337b --- /dev/null +++ b/zetaclient/chains/bitcoin/client/helpers.go @@ -0,0 +1,195 @@ +package client + +import ( + "context" + "fmt" + "time" + + types "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/pkg/errors" +) + +// GetBlockVerboseByStr alias for GetBlockVerbose +func (c *Client) GetBlockVerboseByStr(ctx context.Context, blockHash string) (*types.GetBlockVerboseTxResult, error) { + h, err := strToHash(blockHash) + if err != nil { + return nil, err + } + + return c.GetBlockVerbose(ctx, h) +} + +// GetBlockHeightByStr alias for GetBlockVerbose +func (c *Client) GetBlockHeightByStr(ctx context.Context, blockHash string) (int64, error) { + h, err := strToHash(blockHash) + if err != nil { + return 0, err + } + + res, err := c.GetBlockVerbose(ctx, h) + if err != nil { + return 0, errors.Wrap(err, "unable to get block verbose") + } + + return res.Height, nil +} + +// GetTransactionByStr alias for GetTransaction +func (c *Client) GetTransactionByStr( + ctx context.Context, + hash string, +) (*chainhash.Hash, *types.GetTransactionResult, error) { + h, err := strToHash(hash) + if err != nil { + return nil, nil, err + } + + tx, err := c.GetTransaction(ctx, h) + + return h, tx, err +} + +// GetRawTransactionByStr alias for GetRawTransaction +func (c *Client) GetRawTransactionByStr(ctx context.Context, hash string) (*btcutil.Tx, error) { + h, err := strToHash(hash) + if err != nil { + return nil, err + } + + return c.GetRawTransaction(ctx, h) +} + +// GetRawTransactionResult gets the raw tx result +func (c *Client) GetRawTransactionResult(ctx context.Context, + hash *chainhash.Hash, + res *types.GetTransactionResult, +) (types.TxRawResult, error) { + switch { + case res.Confirmations == 0: + // for pending tx, we query the raw tx + rawResult, err := c.GetRawTransactionVerbose(ctx, hash) + if err != nil { + return types.TxRawResult{}, errors.Wrapf(err, "unable to get raw tx verbose %s", res.TxID) + } + + return *rawResult, nil + case res.Confirmations > 0: + // for confirmed tx, we query the block + + blockHash, err := strToHash(res.BlockHash) + if err != nil { + return types.TxRawResult{}, err + } + + block, err := c.GetBlockVerbose(ctx, blockHash) + if err != nil { + return types.TxRawResult{}, errors.Wrapf(err, "unable to get block versobse %s", res.BlockHash) + } + + invalidRange := res.BlockIndex < 0 || res.BlockIndex >= int64(len(block.Tx)) + if invalidRange { + return types.TxRawResult{}, errors.Errorf( + "invalid block index: tx %s, block_index %d", + res.TxID, + res.BlockIndex, + ) + } + + return block.Tx[res.BlockIndex], nil + default: + // res.Confirmations < 0 (meaning not included) + return types.TxRawResult{}, fmt.Errorf("tx %s not included yet", hash) + } +} + +// GetTransactionFeeAndRate gets the transaction fee and rate for a given tx result +func (c *Client) GetTransactionFeeAndRate(ctx context.Context, rawResult *types.TxRawResult) (int64, int64, error) { + var ( + totalInputValue int64 + totalOutputValue int64 + ) + + // make sure the tx Vsize is not zero (should not happen) + if rawResult.Vsize <= 0 { + return 0, 0, fmt.Errorf("tx %s has non-positive Vsize: %d", rawResult.Txid, rawResult.Vsize) + } + + // sum up total input value + for _, vin := range rawResult.Vin { + prevTx, err := c.GetRawTransactionByStr(ctx, vin.Txid) + if err != nil { + return 0, 0, errors.Wrapf(err, "failed to get previous tx: %s", vin.Txid) + } + totalInputValue += prevTx.MsgTx().TxOut[vin.Vout].Value + } + + // query the raw tx + tx, err := c.GetRawTransactionByStr(ctx, rawResult.Txid) + if err != nil { + return 0, 0, errors.Wrapf(err, "failed to get tx: %s", rawResult.Txid) + } + + // sum up total output value + for _, vout := range tx.MsgTx().TxOut { + totalOutputValue += vout.Value + } + + // calculate the transaction fee in satoshis + fee := totalInputValue - totalOutputValue + if fee < 0 { // never happens + return 0, 0, fmt.Errorf("got negative fee: %d", fee) + } + + // Note: the calculation uses 'Vsize' returned by RPC to simplify dev experience: + // - 1. the devs could use the same value returned by their RPC endpoints to estimate deposit fee. + // - 2. the devs don't have to bother 'Vsize' calculation, even though there is more accurate formula. + // Moreoever, the accurate 'Vsize' is usually an adjusted size (float value) by Bitcoin Core. + // - 3. the 'Vsize' calculation could depend on program language and the library used. + // + // calculate the fee rate in satoshis/vByte + // #nosec G115 always in range + feeRate := fee / int64(rawResult.Vsize) + + return fee, feeRate, nil +} + +// Healthcheck / checks the RPC status of the bitcoin chain. Returns the latest block timestamp +func (c *Client) Healthcheck(ctx context.Context, tssAddress btcutil.Address) (time.Time, error) { + // 1. Query latest block header + bn, err := c.GetBlockCount(ctx) + if err != nil { + return time.Time{}, errors.Wrap(err, "unable to get block count") + } + + hash, err := c.GetBlockHash(ctx, bn) + if err != nil { + return time.Time{}, errors.Wrap(err, "unable to get block hash") + } + + header, err := c.GetBlockHeader(ctx, hash) + if err != nil { + return time.Time{}, errors.Wrap(err, "unable to get block header") + } + + // 2. Query utxos owned by TSS address + res, err := c.ListUnspentMinMaxAddresses(ctx, 0, 1000000, []btcutil.Address{tssAddress}) + switch { + case err != nil: + return time.Time{}, errors.Wrap(err, "unable to list TSS UTXOs") + case len(res) == 0: + return time.Time{}, errors.New("no UTXOs found for TSS") + } + + return header.Timestamp, nil +} + +func strToHash(s string) (*chainhash.Hash, error) { + hash, err := chainhash.NewHashFromStr(s) + if err != nil { + return nil, errors.Wrap(err, "unable to create btc hash from string") + } + + return hash, nil +} diff --git a/zetaclient/chains/bitcoin/client/mockgen.go b/zetaclient/chains/bitcoin/client/mockgen.go new file mode 100644 index 0000000000..8200cf20b3 --- /dev/null +++ b/zetaclient/chains/bitcoin/client/mockgen.go @@ -0,0 +1,86 @@ +package client + +import ( + "context" + "encoding/json" + "time" + + types "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil" + hash "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/wire" +) + +// client represents interface version of Client. +// It's unexported on purpose ONLY for mock generation. +// +//go:generate mockery --name client --structname BitcoinClient --filename bitcoin_client.go --output ../../../testutils/mocks +type client interface { + Ping(ctx context.Context) error + Healthcheck(ctx context.Context, tssAddress btcutil.Address) (time.Time, error) + GetNetworkInfo(ctx context.Context) (*types.GetNetworkInfoResult, error) + + GetBlockCount(ctx context.Context) (int64, error) + GetBlockHash(ctx context.Context, blockHeight int64) (*hash.Hash, error) + GetBlockHeader(ctx context.Context, hash *hash.Hash) (*wire.BlockHeader, error) + GetBlockVerbose(ctx context.Context, hash *hash.Hash) (*types.GetBlockVerboseTxResult, error) + + GetTransaction(ctx context.Context, hash *hash.Hash) (*types.GetTransactionResult, error) + GetRawTransaction(ctx context.Context, hash *hash.Hash) (*btcutil.Tx, error) + GetRawTransactionVerbose(ctx context.Context, hash *hash.Hash) (*types.TxRawResult, error) + + GetRawTransactionResult( + ctx context.Context, + hash *hash.Hash, + res *types.GetTransactionResult, + ) (types.TxRawResult, error) + + CreateRawTransaction( + ctx context.Context, + inputs []types.TransactionInput, + amounts map[btcutil.Address]btcutil.Amount, + lockTime *int64, + ) (*wire.MsgTx, error) + + SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*hash.Hash, error) + + GetTransactionFeeAndRate(ctx context.Context, tx *types.TxRawResult) (int64, int64, error) + EstimateSmartFee( + ctx context.Context, + confTarget int64, + mode *types.EstimateSmartFeeMode, + ) (*types.EstimateSmartFeeResult, error) + + GetBlockVerboseByStr(ctx context.Context, blockHash string) (*types.GetBlockVerboseTxResult, error) + GetBlockHeightByStr(ctx context.Context, blockHash string) (int64, error) + GetTransactionByStr(ctx context.Context, hash string) (*hash.Hash, *types.GetTransactionResult, error) + GetRawTransactionByStr(ctx context.Context, hash string) (*btcutil.Tx, error) + + ListUnspent(ctx context.Context) ([]types.ListUnspentResult, error) + ListUnspentMinMaxAddresses( + ctx context.Context, + minConf, maxConf int, + addresses []btcutil.Address, + ) ([]types.ListUnspentResult, error) + + CreateWallet(ctx context.Context, name string, opts ...rpcclient.CreateWalletOpt) (*types.CreateWalletResult, error) + GetNewAddress(ctx context.Context, account string) (btcutil.Address, error) + ImportAddress(ctx context.Context, address string) error + ImportPrivKeyRescan(ctx context.Context, privKeyWIF *btcutil.WIF, label string, rescan bool) error + GetBalance(ctx context.Context, account string) (btcutil.Amount, error) + GenerateToAddress( + ctx context.Context, + numBlocks int64, + address btcutil.Address, + maxTries *int64, + ) ([]*hash.Hash, error) + + SignRawTransactionWithWallet2( + ctx context.Context, + tx *wire.MsgTx, + inputs []types.RawTxWitnessInput, + ) (*wire.MsgTx, bool, error) + + RawRequest(ctx context.Context, method string, params []json.RawMessage) (json.RawMessage, error) +} diff --git a/zetaclient/chains/bitcoin/common/fee.go b/zetaclient/chains/bitcoin/common/fee.go index 84dad1687d..e77de1c6b9 100644 --- a/zetaclient/chains/bitcoin/common/fee.go +++ b/zetaclient/chains/bitcoin/common/fee.go @@ -1,6 +1,7 @@ package common import ( + "context" "encoding/hex" "fmt" "math" @@ -10,11 +11,10 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/pkg/errors" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" - "github.com/zeta-chain/node/zetaclient/chains/interfaces" clientcommon "github.com/zeta-chain/node/zetaclient/common" ) @@ -56,8 +56,16 @@ var ( DefaultDepositorFee = DepositorFee(defaultDepositorFeeRate) ) +type RPC interface { + GetBlockCount(ctx context.Context) (int64, error) + GetBlockHash(ctx context.Context, blockHeight int64) (*chainhash.Hash, error) + GetBlockHeader(ctx context.Context, hash *chainhash.Hash) (*wire.BlockHeader, error) + GetBlockVerbose(ctx context.Context, hash *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) + GetTransactionFeeAndRate(ctx context.Context, tx *btcjson.TxRawResult) (int64, int64, error) +} + // DepositorFeeCalculator is a function type to calculate the Bitcoin depositor fee -type DepositorFeeCalculator func(interfaces.BTCRPCClient, *btcjson.TxRawResult, *chaincfg.Params) (float64, error) +type DepositorFeeCalculator func(context.Context, RPC, *btcjson.TxRawResult, *chaincfg.Params) (float64, error) // FeeRateToSatPerByte converts a fee rate in BTC/KB to sat/byte. func FeeRateToSatPerByte(rate float64) *big.Int { @@ -222,7 +230,8 @@ func CalcBlockAvgFeeRate(blockVb *btcjson.GetBlockVerboseTxResult, netParams *ch // CalcDepositorFee calculates the depositor fee for a given tx result func CalcDepositorFee( - rpcClient interfaces.BTCRPCClient, + ctx context.Context, + rpc RPC, rawResult *btcjson.TxRawResult, netParams *chaincfg.Params, ) (float64, error) { @@ -232,7 +241,7 @@ func CalcDepositorFee( } // get fee rate of the transaction - _, feeRate, err := rpc.GetTransactionFeeAndRate(rpcClient, rawResult) + _, feeRate, err := rpc.GetTransactionFeeAndRate(ctx, rawResult) if err != nil { return 0, errors.Wrapf(err, "error getting fee rate for tx %s", rawResult.Txid) } @@ -246,14 +255,14 @@ func CalcDepositorFee( // GetRecentFeeRate gets the highest fee rate from recent blocks // Note: this method should be used for testnet ONLY -func GetRecentFeeRate(rpcClient interfaces.BTCRPCClient, netParams *chaincfg.Params) (uint64, error) { +func GetRecentFeeRate(ctx context.Context, rpc RPC, netParams *chaincfg.Params) (uint64, error) { // should avoid using this method for mainnet if netParams.Name == chaincfg.MainNetParams.Name { return 0, errors.New("GetRecentFeeRate should not be used for mainnet") } // get the current block number - blockNumber, err := rpcClient.GetBlockCount() + blockNumber, err := rpc.GetBlockCount(ctx) if err != nil { return 0, err } @@ -262,11 +271,11 @@ func GetRecentFeeRate(rpcClient interfaces.BTCRPCClient, netParams *chaincfg.Par highestRate := int64(0) for i := int64(0); i < feeRateCountBackBlocks; i++ { // get the block - hash, err := rpcClient.GetBlockHash(blockNumber - i) + hash, err := rpc.GetBlockHash(ctx, blockNumber-i) if err != nil { return 0, err } - block, err := rpcClient.GetBlockVerboseTx(hash) + block, err := rpc.GetBlockVerbose(ctx, hash) if err != nil { return 0, err } diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index ecaf9f1e7a..b2bd5ea07a 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -15,7 +15,6 @@ import ( "github.com/zeta-chain/node/pkg/coin" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" - "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/logs" "github.com/zeta-chain/node/zetaclient/zetacore" ) @@ -24,7 +23,7 @@ import ( // TODO(revamp): simplify this function into smaller functions func (ob *Observer) ObserveInbound(ctx context.Context) error { // get and update latest block height - currentBlock, err := ob.btcClient.GetBlockCount() + currentBlock, err := ob.rpc.GetBlockCount(ctx) if err != nil { return fmt.Errorf("observeInboundBTC: error getting block number: %s", err) } @@ -60,7 +59,7 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // query incoming gas asset to TSS address // #nosec G115 always in range - res, err := ob.GetBlockByNumberCached(int64(blockNumber)) + res, err := ob.GetBlockByNumberCached(ctx, int64(blockNumber)) if err != nil { ob.logger.Inbound.Error().Err(err).Msgf("observeInboundBTC: error getting bitcoin block %d", blockNumber) return err @@ -75,7 +74,8 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // #nosec G115 always positive events, err := FilterAndParseIncomingTx( - ob.btcClient, + ctx, + ob.rpc, res.Block.Tx, uint64(res.Block.Height), tssAddress, @@ -145,7 +145,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, return "", err } - tx, err := ob.btcClient.GetRawTransactionVerbose(hash) + tx, err := ob.rpc.GetRawTransactionVerbose(ctx, hash) if err != nil { return "", err } @@ -155,7 +155,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, return "", err } - blockVb, err := ob.btcClient.GetBlockVerboseTx(blockHash) + blockVb, err := ob.rpc.GetBlockVerbose(ctx, blockHash) if err != nil { return "", err } @@ -177,7 +177,8 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, // #nosec G115 always positive event, err := GetBtcEvent( - ob.btcClient, + ctx, + ob.rpc, *tx, tss, uint64(blockVb.Height), @@ -210,7 +211,8 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, // vout0: p2wpkh to the TSS address (targetAddress) // vout1: OP_RETURN memo, base64 encoded func FilterAndParseIncomingTx( - rpcClient interfaces.BTCRPCClient, + ctx context.Context, + rpc RPC, txs []btcjson.TxRawResult, blockNumber uint64, tssAddress string, @@ -218,12 +220,14 @@ func FilterAndParseIncomingTx( netParams *chaincfg.Params, ) ([]*BTCInboundEvent, error) { events := make([]*BTCInboundEvent, 0) + for idx, tx := range txs { if idx == 0 { - continue // the first tx is coinbase; we do not process coinbase tx + // the first tx is coinbase; we do not process coinbase tx + continue } - event, err := GetBtcEvent(rpcClient, tx, tssAddress, blockNumber, logger, netParams, common.CalcDepositorFee) + event, err := GetBtcEvent(ctx, rpc, tx, tssAddress, blockNumber, logger, netParams, common.CalcDepositorFee) if err != nil { // unable to parse the tx, the caller should retry return nil, errors.Wrapf(err, "error getting btc event for tx %s in block %d", tx.Txid, blockNumber) @@ -234,6 +238,7 @@ func FilterAndParseIncomingTx( logger.Info().Msgf("FilterAndParseIncomingTx: found btc event for tx %s in block %d", tx.Txid, blockNumber) } } + return events, nil } @@ -282,7 +287,8 @@ func (ob *Observer) GetInboundVoteFromBtcEvent(event *BTCInboundEvent) *crosscha // GetBtcEvent returns a valid BTCInboundEvent or nil // it uses witness data to extract the sender address, except for mainnet func GetBtcEvent( - rpcClient interfaces.BTCRPCClient, + ctx context.Context, + rpc RPC, tx btcjson.TxRawResult, tssAddress string, blockNumber uint64, @@ -291,16 +297,18 @@ func GetBtcEvent( feeCalculator common.DepositorFeeCalculator, ) (*BTCInboundEvent, error) { if netParams.Name == chaincfg.MainNetParams.Name { - return GetBtcEventWithoutWitness(rpcClient, tx, tssAddress, blockNumber, logger, netParams, feeCalculator) + return GetBtcEventWithoutWitness(ctx, rpc, tx, tssAddress, blockNumber, logger, netParams, feeCalculator) } - return GetBtcEventWithWitness(rpcClient, tx, tssAddress, blockNumber, logger, netParams, feeCalculator) + + return GetBtcEventWithWitness(ctx, rpc, tx, tssAddress, blockNumber, logger, netParams, feeCalculator) } // GetBtcEventWithoutWitness either returns a valid BTCInboundEvent or nil // Note: the caller should retry the tx on error (e.g., GetSenderAddressByVin failed) // TODO(revamp): simplify this function func GetBtcEventWithoutWitness( - rpcClient interfaces.BTCRPCClient, + ctx context.Context, + rpc RPC, tx btcjson.TxRawResult, tssAddress string, blockNumber uint64, @@ -332,7 +340,7 @@ func GetBtcEventWithoutWitness( } // calculate depositor fee - depositorFee, err = feeCalculator(rpcClient, &tx, netParams) + depositorFee, err = feeCalculator(ctx, rpc, &tx, netParams) if err != nil { return nil, errors.Wrapf(err, "error calculating depositor fee for inbound %s", tx.Txid) } @@ -361,7 +369,7 @@ func GetBtcEventWithoutWitness( } // get sender address by input (vin) - fromAddress, err := GetSenderAddressByVin(rpcClient, tx.Vin[0], netParams) + fromAddress, err := GetSenderAddressByVin(ctx, rpc, tx.Vin[0], netParams) if err != nil { return nil, errors.Wrapf(err, "error getting sender address for inbound: %s", tx.Txid) } @@ -386,7 +394,12 @@ func GetBtcEventWithoutWitness( } // GetSenderAddressByVin get the sender address from the transaction input (vin) -func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, net *chaincfg.Params) (string, error) { +func GetSenderAddressByVin( + ctx context.Context, + rpc RPC, + vin btcjson.Vin, + net *chaincfg.Params, +) (string, error) { // query previous raw transaction by txid hash, err := chainhash.NewHashFromStr(vin.Txid) if err != nil { @@ -394,7 +407,7 @@ func GetSenderAddressByVin(rpcClient interfaces.BTCRPCClient, vin btcjson.Vin, n } // this requires running bitcoin node with 'txindex=1' - tx, err := rpcClient.GetRawTransaction(hash) + tx, err := rpc.GetRawTransaction(ctx, hash) if err != nil { return "", errors.Wrapf(err, "error getting raw transaction %s", vin.Txid) } diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index b438169b9f..60ac90cb18 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -2,6 +2,7 @@ package observer_test import ( "bytes" + "context" "encoding/hex" "math" "path" @@ -23,7 +24,6 @@ import ( "github.com/zeta-chain/node/testutil" "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" - "github.com/zeta-chain/node/zetaclient/chains/interfaces" clientcommon "github.com/zeta-chain/node/zetaclient/common" "github.com/zeta-chain/node/zetaclient/keys" "github.com/zeta-chain/node/zetaclient/testutils" @@ -33,7 +33,7 @@ import ( // mockDepositFeeCalculator returns a mock depositor fee calculator that returns the given fee and error. func mockDepositFeeCalculator(fee float64, err error) common.DepositorFeeCalculator { - return func(interfaces.BTCRPCClient, *btcjson.TxRawResult, *chaincfg.Params) (float64, error) { + return func(_ context.Context, _ common.RPC, _ *btcjson.TxRawResult, _ *chaincfg.Params) (float64, error) { return fee, err } } @@ -220,6 +220,8 @@ func Test_GetInboundVoteFromBtcEvent(t *testing.T) { } func TestGetSenderAddressByVin(t *testing.T) { + ctx := context.Background() + // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 txHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" chain := chains.BitcoinMainnet @@ -233,28 +235,28 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(ctx, rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", sender) }) t.Run("should return error on invalid txHash", func(t *testing.T) { - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) // use invalid tx hash txVin := btcjson.Vin{Txid: "invalid tx hash", Vout: 2} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(ctx, rpcClient, txVin, net) require.Error(t, err) require.Empty(t, sender) }) t.Run("should return error when RPC client fails to get raw tx", func(t *testing.T) { // create mock rpc client that returns rpc error - rpcClient := mocks.NewBTCRPCClient(t) - rpcClient.On("GetRawTransaction", mock.Anything).Return(nil, errors.New("rpc error")) + rpcClient := mocks.NewBitcoinClient(t) + rpcClient.On("GetRawTransaction", mock.Anything, mock.Anything).Return(nil, errors.New("rpc error")) // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(ctx, rpcClient, txVin, net) require.ErrorContains(t, err, "error getting raw transaction") require.Empty(t, sender) }) @@ -262,15 +264,18 @@ func TestGetSenderAddressByVin(t *testing.T) { t.Run("should return error on invalid output index", func(t *testing.T) { // create mock rpc client with preloaded tx rpcClient := testrpc.CreateBTCRPCAndLoadTx(t, TestDataDir, chain.ChainId, txHash) + // invalid output index txVin := btcjson.Vin{Txid: txHash, Vout: 3} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(ctx, rpcClient, txVin, net) require.ErrorContains(t, err, "out of range") require.Empty(t, sender) }) } func TestGetBtcEventWithoutWitness(t *testing.T) { + ctx := context.Background() + // load archived inbound P2WPKH raw result // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" @@ -310,6 +315,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // get BTC event event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -334,6 +340,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // get BTC event event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -358,6 +365,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // get BTC event event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -382,6 +390,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // get BTC event event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -406,6 +415,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // get BTC event event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -424,8 +434,9 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { tx.Vout = tx.Vout[:1] // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -440,12 +451,13 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { t.Run("should skip tx if Vout[0] is not a P2WPKH output", func(t *testing.T) { // load tx - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) // modify the tx to have Vout[0] a P2SH output tx.Vout[0].ScriptPubKey.Hex = strings.Replace(tx.Vout[0].ScriptPubKey.Hex, "0014", "a914", 1) event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -460,6 +472,7 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { // append 1 byte to script to make it longer than 22 bytes tx.Vout[0].ScriptPubKey.Hex = tx.Vout[0].ScriptPubKey.Hex + "00" event, err = observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -478,8 +491,9 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { tx.Vout[0].ScriptPubKey.Hex = "001471dc3cd95bf4fe0fb7ffd6bb29b865ddf5581196" // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -497,8 +511,9 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -517,8 +532,9 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { tx.Vout[0].Value = depositorFee - 1.0/1e8 // 1 satoshi less than depositor fee // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -537,8 +553,9 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { tx.Vout[1].ScriptPubKey.Hex = strings.Replace(tx.Vout[1].ScriptPubKey.Hex, "6a", "51", 1) // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -557,8 +574,9 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { tx.Vout[1].ScriptPubKey.Hex = strings.Replace(tx.Vout[1].ScriptPubKey.Hex, "6a14", "6a13", 1) // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -579,17 +597,18 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { tx.Vin[0].Vout = preVout // create mock rpc client - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) // load archived MsgTx and modify previous input script to invalid msgTx := testutils.LoadBTCMsgTx(t, TestDataDir, chain.ChainId, preHash) msgTx.TxOut[preVout].PkScript = []byte{0x00, 0x01} // mock rpc response to return invalid tx msg - rpcClient.On("GetRawTransaction", mock.Anything).Return(btcutil.NewTx(msgTx), nil) + rpcClient.On("GetRawTransaction", mock.Anything, mock.Anything).Return(btcutil.NewTx(msgTx), nil) // get BTC event event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -604,6 +623,8 @@ func TestGetBtcEventWithoutWitness(t *testing.T) { } func TestGetBtcEventErrors(t *testing.T) { + ctx := context.Background() + // load archived inbound P2WPKH raw result // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" @@ -622,8 +643,9 @@ func TestGetBtcEventErrors(t *testing.T) { tx.Vout[0].ScriptPubKey.Hex = "0014invalid000000000000000000000000000000000" // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -642,8 +664,9 @@ func TestGetBtcEventErrors(t *testing.T) { tx.Vin = nil // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -661,11 +684,12 @@ func TestGetBtcEventErrors(t *testing.T) { tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) // create mock rpc client that returns rpc error - rpcClient := mocks.NewBTCRPCClient(t) - rpcClient.On("GetRawTransaction", mock.Anything).Return(nil, errors.New("rpc error")) + rpcClient := mocks.NewBitcoinClient(t) + rpcClient.On("GetRawTransaction", mock.Anything, mock.Anything).Return(nil, errors.New("rpc error")) // get BTC event event, err := observer.GetBtcEventWithoutWitness( + ctx, rpcClient, *tx, tssAddress, @@ -680,6 +704,8 @@ func TestGetBtcEventErrors(t *testing.T) { } func TestGetBtcEvent(t *testing.T) { + ctx := context.Background() + t.Run("should not decode inbound event with witness with mainnet chain", func(t *testing.T) { // load archived inbound P2WPKH raw result // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa @@ -693,9 +719,10 @@ func TestGetBtcEvent(t *testing.T) { txHash2 := "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8" tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash2, false) - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) // get BTC event event, err := observer.GetBtcEvent( + ctx, rpcClient, *tx, tssAddress, @@ -747,6 +774,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event event, err := observer.GetBtcEvent( + ctx, rpcClient, *tx, tssAddress, diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index d345f4da36..007f22f965 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -8,10 +8,12 @@ import ( "math/big" "sort" "sync/atomic" + "time" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" + hash "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -26,6 +28,40 @@ import ( clienttypes "github.com/zeta-chain/node/zetaclient/types" ) +type RPC interface { + Healthcheck(ctx context.Context, tssAddress btcutil.Address) (time.Time, error) + + GetBlockCount(ctx context.Context) (int64, error) + GetBlockHash(ctx context.Context, blockHeight int64) (*hash.Hash, error) + GetBlockHeader(ctx context.Context, hash *hash.Hash) (*wire.BlockHeader, error) + GetBlockVerbose(ctx context.Context, hash *hash.Hash) (*btcjson.GetBlockVerboseTxResult, error) + + GetRawTransaction(ctx context.Context, hash *hash.Hash) (*btcutil.Tx, error) + GetRawTransactionVerbose(ctx context.Context, hash *hash.Hash) (*btcjson.TxRawResult, error) + GetRawTransactionResult( + ctx context.Context, + hash *hash.Hash, + res *btcjson.GetTransactionResult, + ) (btcjson.TxRawResult, error) + + GetTransactionFeeAndRate(ctx context.Context, tx *btcjson.TxRawResult) (int64, int64, error) + + EstimateSmartFee( + ctx context.Context, + confTarget int64, + mode *btcjson.EstimateSmartFeeMode, + ) (*btcjson.EstimateSmartFeeResult, error) + + ListUnspentMinMaxAddresses( + ctx context.Context, + minConf, maxConf int, + addresses []btcutil.Address, + ) ([]btcjson.ListUnspentResult, error) + + GetBlockHeightByStr(ctx context.Context, blockHash string) (int64, error) + GetTransactionByStr(ctx context.Context, hash string) (*hash.Hash, *btcjson.GetTransactionResult, error) +} + const ( // btcBlocksPerDay represents Bitcoin blocks per days for LRU block cache size btcBlocksPerDay = 144 @@ -64,7 +100,7 @@ type Observer struct { netParams *chaincfg.Params // btcClient is the Bitcoin RPC client that interacts with the Bitcoin node - btcClient interfaces.BTCRPCClient + rpc RPC // pendingNonce is the outbound artificial pending nonce pendingNonce uint64 @@ -92,7 +128,7 @@ type Observer struct { // NewObserver returns a new Bitcoin chain observer func NewObserver( chain chains.Chain, - btcClient interfaces.BTCRPCClient, + rpc RPC, chainParams observertypes.ChainParams, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, @@ -112,20 +148,20 @@ func NewObserver( logger, ) if err != nil { - return nil, errors.Wrapf(err, "unable to create base observer for chain %d", chain.ChainId) + return nil, errors.Wrapf(err, "unable to create base observer") } // get the bitcoin network params netParams, err := chains.BitcoinNetParamsFromChainID(chain.ChainId) if err != nil { - return nil, errors.Wrapf(err, "unable to get BTC net params for chain %d", chain.ChainId) + return nil, errors.Wrapf(err, "unable to get BTC net params") } // create bitcoin observer ob := &Observer{ Observer: *baseObserver, netParams: netParams, - btcClient: btcClient, + rpc: rpc, pendingNonce: 0, utxos: []btcjson.ListUnspentResult{}, includedTxHashes: make(map[string]bool), @@ -140,7 +176,10 @@ func NewObserver( ob.nodeEnabled.Store(true) // load last scanned block - if err = ob.LoadLastBlockScanned(); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err = ob.LoadLastBlockScanned(ctx); err != nil { return nil, errors.Wrap(err, "unable to load last scanned block") } @@ -189,13 +228,13 @@ func (ob *Observer) PostGasPrice(ctx context.Context) error { // regnet: RPC 'EstimateSmartFee' is not available // testnet: RPC 'EstimateSmartFee' returns unreasonable high gas rate if ob.Chain().NetworkType != chains.NetworkType_mainnet { - feeRateEstimated, err = ob.specialHandleFeeRate() + feeRateEstimated, err = ob.specialHandleFeeRate(ctx) if err != nil { return errors.Wrap(err, "unable to execute specialHandleFeeRate") } } else { // EstimateSmartFee returns the fees per kilobyte (BTC/kb) targeting given block confirmation - feeResult, err := ob.btcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) + feeResult, err := ob.rpc.EstimateSmartFee(ctx, 1, &btcjson.EstimateModeEconomical) if err != nil { return errors.Wrap(err, "unable to estimate smart fee") } @@ -209,7 +248,7 @@ func (ob *Observer) PostGasPrice(ctx context.Context) error { } // query the current block number - blockNumber, err := ob.btcClient.GetBlockCount() + blockNumber, err := ob.rpc.GetBlockCount(ctx) if err != nil { return errors.Wrap(err, "GetBlockCount error") } @@ -244,7 +283,7 @@ func (ob *Observer) FetchUTXOs(ctx context.Context) error { ob.refreshPendingNonce(ctx) // get the current block height. - bh, err := ob.btcClient.GetBlockCount() + bh, err := ob.rpc.GetBlockCount(ctx) if err != nil { return errors.Wrap(err, "unable to get block height") } @@ -257,7 +296,7 @@ func (ob *Observer) FetchUTXOs(ctx context.Context) error { return errors.Wrap(err, "unable to get tss address") } - utxos, err := ob.btcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{tssAddr}) + utxos, err := ob.rpc.ListUnspentMinMaxAddresses(ctx, 0, maxConfirmations, []btcutil.Address{tssAddr}) if err != nil { return errors.Wrap(err, "unable to list unspent utxo") } @@ -314,7 +353,7 @@ func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { } // GetBlockByNumberCached gets cached block (and header) by block number -func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, error) { +func (ob *Observer) GetBlockByNumberCached(ctx context.Context, blockNumber int64) (*BTCBlockNHeader, error) { if result, ok := ob.BlockCache().Get(blockNumber); ok { if block, ok := result.(*BTCBlockNHeader); ok { return block, nil @@ -323,17 +362,17 @@ func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, } // Get the block hash - hash, err := ob.btcClient.GetBlockHash(blockNumber) + hash, err := ob.rpc.GetBlockHash(ctx, blockNumber) if err != nil { return nil, err } // Get the block header - header, err := ob.btcClient.GetBlockHeader(hash) + header, err := ob.rpc.GetBlockHeader(ctx, hash) if err != nil { return nil, err } // Get the block with verbose transactions - block, err := ob.btcClient.GetBlockVerboseTx(hash) + block, err := ob.rpc.GetBlockVerbose(ctx, hash) if err != nil { return nil, err } @@ -347,7 +386,7 @@ func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, } // LoadLastBlockScanned loads the last scanned block from the database -func (ob *Observer) LoadLastBlockScanned() error { +func (ob *Observer) LoadLastBlockScanned(ctx context.Context) error { err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) if err != nil { return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) @@ -357,7 +396,7 @@ func (ob *Observer) LoadLastBlockScanned() error { // 1. environment variable is set explicitly to "latest" // 2. environment variable is empty and last scanned block is not found in DB if ob.LastBlockScanned() == 0 { - blockNumber, err := ob.btcClient.GetBlockCount() + blockNumber, err := ob.rpc.GetBlockCount(ctx) if err != nil { return errors.Wrapf(err, "error GetBlockCount for chain %d", ob.Chain().ChainId) } @@ -388,13 +427,13 @@ func (ob *Observer) LoadBroadcastedTxMap() error { } // specialHandleFeeRate handles the fee rate for regnet and testnet -func (ob *Observer) specialHandleFeeRate() (uint64, error) { +func (ob *Observer) specialHandleFeeRate(ctx context.Context) (uint64, error) { switch ob.Chain().NetworkType { case chains.NetworkType_privnet: // hardcode gas price for regnet return 1, nil case chains.NetworkType_testnet: - feeRateEstimated, err := common.GetRecentFeeRate(ob.btcClient, ob.netParams) + feeRateEstimated, err := common.GetRecentFeeRate(ctx, ob.rpc, ob.netParams) if err != nil { return 0, errors.Wrapf(err, "error GetRecentFeeRate") } diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index ab5415ef66..3b847297ef 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -1,6 +1,7 @@ package observer_test import ( + "context" "math/big" "os" "strconv" @@ -69,14 +70,14 @@ func Test_NewObserver(t *testing.T) { params := mocks.MockChainParams(chain.ChainId, 10) // create mock btc client with block height 100 - btcClient := mocks.NewBTCRPCClient(t) - btcClient.On("GetBlockCount").Return(int64(100), nil) + btcClient := mocks.NewBitcoinClient(t) + btcClient.On("GetBlockCount", mock.Anything).Return(int64(100), nil) // test cases tests := []struct { name string chain chains.Chain - btcClient interfaces.BTCRPCClient + btcClient *mocks.BitcoinClient chainParams observertypes.ChainParams coreClient interfaces.ZetacoreClient tss interfaces.TSSSigner @@ -101,7 +102,7 @@ func Test_NewObserver(t *testing.T) { chainParams: params, coreClient: nil, tss: mocks.NewTSS(t), - errorMessage: "unable to get BTC net params for chain", + errorMessage: "unable to get BTC net params", }, { name: "should fail if env var us invalid", @@ -170,18 +171,18 @@ func Test_BlockCache(t *testing.T) { hash := sample.BtcHash() header := &wire.BlockHeader{Version: 1} block := &btcjson.GetBlockVerboseTxResult{Version: 1} - ob.client.On("GetBlockHash", mock.Anything).Return(&hash, nil) - ob.client.On("GetBlockHeader", &hash).Return(header, nil) - ob.client.On("GetBlockVerboseTx", &hash).Return(block, nil) + ob.client.On("GetBlockHash", mock.Anything, mock.Anything).Return(&hash, nil) + ob.client.On("GetBlockHeader", mock.Anything, &hash).Return(header, nil) + ob.client.On("GetBlockVerbose", mock.Anything, &hash).Return(block, nil) // get block and header from observer, fallback to btc client - result, err := ob.GetBlockByNumberCached(100) + result, err := ob.GetBlockByNumberCached(ob.ctx, 100) require.NoError(t, err) require.EqualValues(t, header, result.Header) require.EqualValues(t, block, result.Block) // get block header from cache - result, err = ob.GetBlockByNumberCached(100) + result, err = ob.GetBlockByNumberCached(ob.ctx, 100) require.NoError(t, err) require.EqualValues(t, header, result.Header) require.EqualValues(t, block, result.Block) @@ -195,7 +196,7 @@ func Test_BlockCache(t *testing.T) { ob.BlockCache().Add(blockNumber, "a string value") // get result from cache - result, err := ob.GetBlockByNumberCached(blockNumber) + result, err := ob.GetBlockByNumberCached(ob.ctx, blockNumber) require.ErrorContains(t, err, "cached value is not of type *BTCBlockNHeader") require.Nil(t, result) }) @@ -204,6 +205,7 @@ func Test_BlockCache(t *testing.T) { func Test_LoadLastBlockScanned(t *testing.T) { // use Bitcoin mainnet chain for testing chain := chains.BitcoinMainnet + ctx := context.Background() t.Run("should load last block scanned", func(t *testing.T) { // create observer and write 199 as last block scanned @@ -211,7 +213,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { ob.WriteLastBlockScannedToDB(199) // load last block scanned - err := ob.LoadLastBlockScanned() + err := ob.LoadLastBlockScanned(ctx) require.NoError(t, err) require.EqualValues(t, 199, ob.LastBlockScanned()) }) @@ -225,7 +227,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { defer os.Unsetenv(envvar) // load last block scanned - err := ob.LoadLastBlockScanned() + err := ob.LoadLastBlockScanned(ctx) require.ErrorContains(t, err, "error LoadLastBlockScanned") }) t.Run("should fail on RPC error", func(t *testing.T) { @@ -237,10 +239,10 @@ func Test_LoadLastBlockScanned(t *testing.T) { // attach a mock btc client that returns rpc error obOther.client.ExpectedCalls = nil - obOther.client.On("GetBlockCount").Return(int64(0), errors.New("rpc error")) + obOther.client.On("GetBlockCount", mock.Anything).Return(int64(0), errors.New("rpc error")) // load last block scanned - err := obOther.LoadLastBlockScanned() + err := obOther.LoadLastBlockScanned(ctx) require.ErrorContains(t, err, "rpc error") }) t.Run("should use hardcode block 100 for regtest", func(t *testing.T) { @@ -248,7 +250,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { obRegnet := newTestSuite(t, chains.BitcoinRegtest) // load last block scanned - err := obRegnet.LoadLastBlockScanned() + err := obRegnet.LoadLastBlockScanned(ctx) require.NoError(t, err) require.EqualValues(t, observer.RegnetStartBlock, obRegnet.LastBlockScanned()) }) @@ -299,18 +301,21 @@ func TestSubmittedTx(t *testing.T) { type testSuite struct { *observer.Observer - client *mocks.BTCRPCClient + ctx context.Context + client *mocks.BitcoinClient zetacore *mocks.ZetacoreClient db *db.DB } func newTestSuite(t *testing.T, chain chains.Chain) *testSuite { + ctx := context.Background() + require.True(t, chain.IsBitcoinChain()) chainParams := mocks.MockChainParams(chain.ChainId, 10) - client := mocks.NewBTCRPCClient(t) - client.On("GetBlockCount").Return(int64(100), nil).Maybe() + client := mocks.NewBitcoinClient(t) + client.On("GetBlockCount", mock.Anything).Return(int64(100), nil).Maybe() zetacore := mocks.NewZetacoreClient(t) @@ -332,6 +337,7 @@ func newTestSuite(t *testing.T, chain chains.Chain) *testSuite { require.NoError(t, err) return &testSuite{ + ctx: ctx, Observer: ob, client: client, zetacore: zetacore, diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index 7a7a0f372c..ac4b75a172 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -15,7 +15,6 @@ import ( "github.com/zeta-chain/node/pkg/constant" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/compliance" "github.com/zeta-chain/node/zetaclient/zetacore" @@ -148,7 +147,7 @@ func (ob *Observer) VoteOutboundIfConfirmed( } // Get outbound block height - blockHeight, err := rpc.GetBlockHeightByHash(ob.btcClient, res.BlockHash) + blockHeight, err := ob.rpc.GetBlockHeightByStr(ctx, res.BlockHash) if err != nil { return false, errors.Wrapf( err, @@ -357,7 +356,7 @@ func (ob *Observer) getOutboundIDByNonce(ctx context.Context, nonce uint64, test return "", fmt.Errorf("getOutboundIDByNonce: cannot find outbound txid for nonce %d", nonce) } // make sure it's a real Bitcoin txid - _, getTxResult, err := rpc.GetTxResultByHash(ob.btcClient, txid) + _, getTxResult, err := ob.rpc.GetTransactionByStr(ctx, txid) if err != nil { return "", errors.Wrapf( err, @@ -400,7 +399,7 @@ func (ob *Observer) checkIncludedTx( txHash string, ) (*btcjson.GetTransactionResult, bool) { outboundID := ob.OutboundID(cctx.GetCurrentOutboundParam().TssNonce) - hash, getTxResult, err := rpc.GetTxResultByHash(ob.btcClient, txHash) + hash, getTxResult, err := ob.rpc.GetTransactionByStr(ctx, txHash) if err != nil { ob.logger.Outbound.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) return nil, false @@ -484,7 +483,7 @@ func (ob *Observer) checkTssOutboundResult( ) error { params := cctx.GetCurrentOutboundParam() nonce := params.TssNonce - rawResult, err := rpc.GetRawTxResult(ob.btcClient, hash, res) + rawResult, err := ob.rpc.GetRawTransactionResult(ctx, hash, res) if err != nil { return errors.Wrapf(err, "checkTssOutboundResult: error GetRawTxResultByHash %s", hash.String()) } diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index fba95b7df3..843f1c0f84 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/ethereum/go-ethereum/crypto" "github.com/rs/zerolog" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/db" @@ -33,8 +34,8 @@ func MockBTCObserverMainnet(t *testing.T, tss interfaces.TSSSigner) *Observer { } // create mock rpc client - btcClient := mocks.NewBTCRPCClient(t) - btcClient.On("GetBlockCount").Return(int64(100), nil) + btcClient := mocks.NewBitcoinClient(t) + btcClient.On("GetBlockCount", mock.Anything).Return(int64(100), nil) database, err := db.NewFromSqliteInMemory(true) require.NoError(t, err) diff --git a/zetaclient/chains/bitcoin/observer/rpc_status.go b/zetaclient/chains/bitcoin/observer/rpc_status.go index f09e3ec32d..fea0ea7f13 100644 --- a/zetaclient/chains/bitcoin/observer/rpc_status.go +++ b/zetaclient/chains/bitcoin/observer/rpc_status.go @@ -4,18 +4,16 @@ import ( "context" "github.com/pkg/errors" - - "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" ) // CheckRPCStatus checks the RPC status of the Bitcoin chain -func (ob *Observer) CheckRPCStatus(_ context.Context) error { +func (ob *Observer) CheckRPCStatus(ctx context.Context) error { tssAddress, err := ob.TSS().PubKey().AddressBTC(ob.Chain().ChainId) if err != nil { return errors.Wrap(err, "unable to get TSS BTC address") } - blockTime, err := rpc.CheckRPCStatus(ob.btcClient, tssAddress) + blockTime, err := ob.rpc.Healthcheck(ctx, tssAddress) switch { case err != nil && !ob.isNodeEnabled(): // suppress error if node is disabled diff --git a/zetaclient/chains/bitcoin/observer/witness.go b/zetaclient/chains/bitcoin/observer/witness.go index 69d2726459..1a7a6c3576 100644 --- a/zetaclient/chains/bitcoin/observer/witness.go +++ b/zetaclient/chains/bitcoin/observer/witness.go @@ -1,6 +1,7 @@ package observer import ( + "context" "encoding/hex" "fmt" @@ -11,14 +12,14 @@ import ( "github.com/rs/zerolog" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" - "github.com/zeta-chain/node/zetaclient/chains/interfaces" ) // GetBtcEventWithWitness either returns a valid BTCInboundEvent or nil. // This method supports data with more than 80 bytes by scanning the witness for possible presence of a tapscript. // It will first prioritize OP_RETURN over tapscript. func GetBtcEventWithWitness( - client interfaces.BTCRPCClient, + ctx context.Context, + rpc RPC, tx btcjson.TxRawResult, tssAddress string, blockNumber uint64, @@ -41,7 +42,7 @@ func GetBtcEventWithWitness( } // calculate depositor fee - depositorFee, err := feeCalculator(client, &tx, netParams) + depositorFee, err := feeCalculator(ctx, rpc, &tx, netParams) if err != nil { return nil, errors.Wrapf(err, "error calculating depositor fee for inbound %s", tx.Txid) } @@ -69,7 +70,7 @@ func GetBtcEventWithWitness( } // event found, get sender address - fromAddress, err := GetSenderAddressByVin(client, tx.Vin[0], netParams) + fromAddress, err := GetSenderAddressByVin(ctx, rpc, tx.Vin[0], netParams) if err != nil { return nil, errors.Wrapf(err, "error getting sender address for inbound: %s", tx.Txid) } diff --git a/zetaclient/chains/bitcoin/observer/witness_test.go b/zetaclient/chains/bitcoin/observer/witness_test.go index 34b676c7ac..94ba7202be 100644 --- a/zetaclient/chains/bitcoin/observer/witness_test.go +++ b/zetaclient/chains/bitcoin/observer/witness_test.go @@ -1,6 +1,7 @@ package observer_test import ( + "context" "encoding/hex" "testing" @@ -50,6 +51,8 @@ func TestParseScriptFromWitness(t *testing.T) { } func TestGetBtcEventWithWitness(t *testing.T) { + ctx := context.Background() + // load archived inbound P2WPKH raw result // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" @@ -88,6 +91,7 @@ func TestGetBtcEventWithWitness(t *testing.T) { // get BTC event event, err := observer.GetBtcEventWithWitness( + ctx, rpcClient, *tx, tssAddress, @@ -126,6 +130,7 @@ func TestGetBtcEventWithWitness(t *testing.T) { // get BTC event event, err := observer.GetBtcEventWithWitness( + ctx, rpcClient, *tx, tssAddress, @@ -167,6 +172,7 @@ func TestGetBtcEventWithWitness(t *testing.T) { // get BTC event event, err := observer.GetBtcEventWithWitness( + ctx, rpcClient, *tx, tssAddress, @@ -185,8 +191,9 @@ func TestGetBtcEventWithWitness(t *testing.T) { tx.Vout[0].ScriptPubKey.Hex = "001471dc3cd95bf4fe0fb7ffd6bb29b865ddf5581196" // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithWitness( + ctx, rpcClient, *tx, tssAddress, @@ -204,8 +211,9 @@ func TestGetBtcEventWithWitness(t *testing.T) { tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithWitness( + ctx, rpcClient, *tx, tssAddress, @@ -224,8 +232,9 @@ func TestGetBtcEventWithWitness(t *testing.T) { tx.Vout[0].Value = depositorFee - 1.0/1e8 // 1 satoshi less than depositor fee // get BTC event - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) event, err := observer.GetBtcEventWithWitness( + ctx, rpcClient, *tx, tssAddress, @@ -243,11 +252,12 @@ func TestGetBtcEventWithWitness(t *testing.T) { tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) // create mock rpc client that returns rpc error - rpcClient := mocks.NewBTCRPCClient(t) - rpcClient.On("GetRawTransaction", mock.Anything).Return(nil, errors.New("rpc error")) + rpcClient := mocks.NewBitcoinClient(t) + rpcClient.On("GetRawTransaction", mock.Anything, mock.Anything).Return(nil, errors.New("rpc error")) // get BTC event event, err := observer.GetBtcEventWithWitness( + ctx, rpcClient, *tx, tssAddress, @@ -271,17 +281,18 @@ func TestGetBtcEventWithWitness(t *testing.T) { tx.Vin[0].Vout = preVout // create mock rpc client - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) // load archived MsgTx and modify previous input script to invalid msgTx := testutils.LoadBTCMsgTx(t, TestDataDir, chain.ChainId, preHash) msgTx.TxOut[preVout].PkScript = []byte{0x00, 0x01} // mock rpc response to return invalid tx msg - rpcClient.On("GetRawTransaction", mock.Anything).Return(btcutil.NewTx(msgTx), nil) + rpcClient.On("GetRawTransaction", mock.Anything, mock.Anything).Return(btcutil.NewTx(msgTx), nil) // get BTC event event, err := observer.GetBtcEventWithWitness( + ctx, rpcClient, *tx, tssAddress, diff --git a/zetaclient/chains/bitcoin/rpc/rpc.go b/zetaclient/chains/bitcoin/rpc/rpc.go deleted file mode 100644 index a553945a7e..0000000000 --- a/zetaclient/chains/bitcoin/rpc/rpc.go +++ /dev/null @@ -1,215 +0,0 @@ -package rpc - -import ( - "fmt" - "time" - - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" - "github.com/pkg/errors" - - "github.com/zeta-chain/node/zetaclient/chains/interfaces" - "github.com/zeta-chain/node/zetaclient/config" -) - -const ( - // RPCAlertLatency is the default threshold for RPC latency to be considered unhealthy and trigger an alert. - // Bitcoin block time is 10 minutes, 1200s (20 minutes) is a reasonable threshold for Bitcoin - RPCAlertLatency = time.Duration(1200) * time.Second -) - -// NewRPCClient creates a new RPC client by the given config. -func NewRPCClient(btcConfig config.BTCConfig) (*rpcclient.Client, error) { - connCfg := &rpcclient.ConnConfig{ - Host: btcConfig.RPCHost, - User: btcConfig.RPCUsername, - Pass: btcConfig.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: btcConfig.RPCParams, - } - - rpcClient, err := rpcclient.New(connCfg, nil) - if err != nil { - return nil, fmt.Errorf("error creating rpc client: %s", err) - } - - err = rpcClient.Ping() - if err != nil { - return nil, fmt.Errorf("error ping the bitcoin server: %s", err) - } - return rpcClient, nil -} - -// GetTxResultByHash gets the transaction result by hash -func GetTxResultByHash( - rpcClient interfaces.BTCRPCClient, - txID string, -) (*chainhash.Hash, *btcjson.GetTransactionResult, error) { - hash, err := chainhash.NewHashFromStr(txID) - if err != nil { - return nil, nil, errors.Wrapf(err, "GetTxResultByHash: error NewHashFromStr: %s", txID) - } - - // The Bitcoin node has to be configured to watch TSS address - txResult, err := rpcClient.GetTransaction(hash) - if err != nil { - return nil, nil, errors.Wrapf(err, "GetTxResultByHash: error GetTransaction %s", hash.String()) - } - return hash, txResult, nil -} - -// GetRawTxByHash gets the raw transaction by hash -func GetRawTxByHash(rpcClient interfaces.BTCRPCClient, txID string) (*btcutil.Tx, error) { - hash, err := chainhash.NewHashFromStr(txID) - if err != nil { - return nil, errors.Wrapf(err, "GetRawTxByHash: error NewHashFromStr: %s", txID) - } - - tx, err := rpcClient.GetRawTransaction(hash) - if err != nil { - return nil, errors.Wrapf(err, "GetRawTxByHash: error GetRawTransaction %s", txID) - } - return tx, nil -} - -// GetBlockHeightByHash gets the block height by block hash -func GetBlockHeightByHash( - rpcClient interfaces.BTCRPCClient, - hash string, -) (int64, error) { - // decode the block hash - var blockHash chainhash.Hash - err := chainhash.Decode(&blockHash, hash) - if err != nil { - return 0, errors.Wrapf(err, "GetBlockHeightByHash: error decoding block hash %s", hash) - } - - // get block by hash - block, err := rpcClient.GetBlockVerbose(&blockHash) - if err != nil { - return 0, errors.Wrapf(err, "GetBlockHeightByHash: error GetBlockVerbose %s", hash) - } - return block.Height, nil -} - -// GetRawTxResult gets the raw tx result -func GetRawTxResult( - rpcClient interfaces.BTCRPCClient, - hash *chainhash.Hash, - res *btcjson.GetTransactionResult, -) (btcjson.TxRawResult, error) { - if res.Confirmations == 0 { // for pending tx, we query the raw tx directly - rawResult, err := rpcClient.GetRawTransactionVerbose(hash) // for pending tx, we query the raw tx - if err != nil { - return btcjson.TxRawResult{}, errors.Wrapf( - err, - "GetRawTxResult: error GetRawTransactionVerbose %s", - res.TxID, - ) - } - return *rawResult, nil - } else if res.Confirmations > 0 { // for confirmed tx, we query the block - blkHash, err := chainhash.NewHashFromStr(res.BlockHash) - if err != nil { - return btcjson.TxRawResult{}, errors.Wrapf(err, "GetRawTxResult: error NewHashFromStr for block hash %s", res.BlockHash) - } - block, err := rpcClient.GetBlockVerboseTx(blkHash) - if err != nil { - return btcjson.TxRawResult{}, errors.Wrapf(err, "GetRawTxResult: error GetBlockVerboseTx %s", res.BlockHash) - } - if res.BlockIndex < 0 || res.BlockIndex >= int64(len(block.Tx)) { - return btcjson.TxRawResult{}, errors.Wrapf(err, "GetRawTxResult: invalid outbound with invalid block index, TxID %s, BlockIndex %d", res.TxID, res.BlockIndex) - } - return block.Tx[res.BlockIndex], nil - } - - // res.Confirmations < 0 (meaning not included) - return btcjson.TxRawResult{}, fmt.Errorf("GetRawTxResult: tx %s not included yet", hash) -} - -// GetTransactionFeeAndRate gets the transaction fee and rate for a given tx result -func GetTransactionFeeAndRate(rpcClient interfaces.BTCRPCClient, rawResult *btcjson.TxRawResult) (int64, int64, error) { - var ( - totalInputValue int64 - totalOutputValue int64 - ) - - // make sure the tx Vsize is not zero (should not happen) - if rawResult.Vsize <= 0 { - return 0, 0, fmt.Errorf("tx %s has non-positive Vsize: %d", rawResult.Txid, rawResult.Vsize) - } - - // sum up total input value - for _, vin := range rawResult.Vin { - prevTx, err := GetRawTxByHash(rpcClient, vin.Txid) - if err != nil { - return 0, 0, errors.Wrapf(err, "failed to get previous tx: %s", vin.Txid) - } - totalInputValue += prevTx.MsgTx().TxOut[vin.Vout].Value - } - - // query the raw tx - tx, err := GetRawTxByHash(rpcClient, rawResult.Txid) - if err != nil { - return 0, 0, errors.Wrapf(err, "failed to get tx: %s", rawResult.Txid) - } - - // sum up total output value - for _, vout := range tx.MsgTx().TxOut { - totalOutputValue += vout.Value - } - - // calculate the transaction fee in satoshis - fee := totalInputValue - totalOutputValue - if fee < 0 { // never happens - return 0, 0, fmt.Errorf("got negative fee: %d", fee) - } - - // Note: the calculation uses 'Vsize' returned by RPC to simplify dev experience: - // - 1. the devs could use the same value returned by their RPC endpoints to estimate deposit fee. - // - 2. the devs don't have to bother 'Vsize' calculation, even though there is more accurate formula. - // Moreoever, the accurate 'Vsize' is usually an adjusted size (float value) by Bitcoin Core. - // - 3. the 'Vsize' calculation could depend on program language and the library used. - // - // calculate the fee rate in satoshis/vByte - // #nosec G115 always in range - feeRate := fee / int64(rawResult.Vsize) - - return fee, feeRate, nil -} - -// CheckRPCStatus checks the RPC status of the bitcoin chain -func CheckRPCStatus(client interfaces.BTCRPCClient, tssAddress btcutil.Address) (time.Time, error) { - // query latest block number - bn, err := client.GetBlockCount() - if err != nil { - return time.Time{}, errors.Wrap(err, "unable to get block count") - } - - // query latest block header - hash, err := client.GetBlockHash(bn) - if err != nil { - return time.Time{}, errors.Wrapf(err, "unable to get hash for block %d", bn) - } - - // query latest block header thru hash - header, err := client.GetBlockHeader(hash) - if err != nil { - return time.Time{}, errors.Wrapf(err, "unable to get block header (%s)", hash.String()) - } - - // should be able to list utxos owned by TSS address - res, err := client.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddress}) - - switch { - case err != nil: - return time.Time{}, errors.Wrap(err, "unable to list TSS UTXOs") - case len(res) == 0: - return time.Time{}, errors.New("no UTXOs found for TSS") - } - - return header.Timestamp, nil -} diff --git a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go deleted file mode 100644 index 61369991d9..0000000000 --- a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go +++ /dev/null @@ -1,614 +0,0 @@ -package rpc_test - -import ( - "context" - "encoding/hex" - "fmt" - "math/big" - "os" - "strings" - "testing" - "time" - - "github.com/btcsuite/btcd/blockchain" - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/stretchr/testify/require" - btc "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" - - "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" - "github.com/zeta-chain/node/zetaclient/common" - "github.com/zeta-chain/node/zetaclient/config" - "github.com/zeta-chain/node/zetaclient/testutils" -) - -// createRPCClient creates a new Bitcoin RPC client for given chainID -func createRPCClient(chainID int64) (*rpcclient.Client, error) { - var connCfg *rpcclient.ConnConfig - rpcMainnet := os.Getenv(common.EnvBtcRPCMainnet) - rpcTestnet := os.Getenv(common.EnvBtcRPCTestnet) - - // mainnet - if chainID == chains.BitcoinMainnet.ChainId { - connCfg = &rpcclient.ConnConfig{ - Host: rpcMainnet, // mainnet endpoint goes here - User: "user", - Pass: "pass", - Params: "mainnet", - HTTPPostMode: true, - DisableTLS: true, - } - } - // testnet3 - if chainID == chains.BitcoinTestnet.ChainId { - connCfg = &rpcclient.ConnConfig{ - Host: rpcTestnet, // testnet endpoint goes here - User: "user", - Pass: "pass", - Params: "testnet3", - HTTPPostMode: true, - DisableTLS: true, - } - } - return rpcclient.New(connCfg, nil) -} - -// getFeeRate is a helper function to get fee rate for a given confirmation target -func getFeeRate( - client *rpcclient.Client, - confTarget int64, - estimateMode *btcjson.EstimateSmartFeeMode, -) (*big.Int, error) { - feeResult, err := client.EstimateSmartFee(confTarget, estimateMode) - if err != nil { - return nil, err - } - if feeResult.Errors != nil { - return nil, errors.New(strings.Join(feeResult.Errors, ", ")) - } - if feeResult.FeeRate == nil { - return nil, errors.New("fee rate is nil") - } - return new(big.Int).SetInt64(int64(*feeResult.FeeRate * 1e8)), nil -} - -// getMempoolSpaceTxsByBlock gets mempool.space txs for a given block -func getMempoolSpaceTxsByBlock( - t *testing.T, - client *rpcclient.Client, - blkNumber int64, - testnet bool, -) (*chainhash.Hash, []testutils.MempoolTx, error) { - blkHash, err := client.GetBlockHash(blkNumber) - if err != nil { - t.Logf("error GetBlockHash for block %d: %s\n", blkNumber, err) - return nil, nil, err - } - - // get mempool.space txs for the block - mempoolTxs, err := testutils.GetBlockTxs(context.Background(), blkHash.String(), testnet) - if err != nil { - t.Logf("error GetBlockTxs %d: %s\n", blkNumber, err) - return nil, nil, err - } - - return blkHash, mempoolTxs, nil -} - -// Test_BitcoinLive is a phony test to run each live test individually -func Test_BitcoinLive(t *testing.T) { - // LiveTest_FilterAndParseIncomingTx(t) - // LiveTest_FilterAndParseIncomingTx_Nop(t) - // LiveTest_NewRPCClient(t) - // LiveTest_GetBlockHeightByHash(t) - // LiveTest_BitcoinFeeRate(t) - // LiveTest_AvgFeeRateMainnetMempoolSpace(t) - // LiveTest_AvgFeeRateTestnetMempoolSpace(t) - // LiveTest_GetRecentFeeRate(t) - // LiveTest_GetSenderByVin(t) - // LiveTest_GetTransactionFeeAndRate(t) - // LiveTest_CalcDepositorFeeV2(t) -} - -func LiveTest_FilterAndParseIncomingTx(t *testing.T) { - // setup Bitcoin client - client, err := createRPCClient(chains.BitcoinTestnet.ChainId) - require.NoError(t, err) - - // get the block that contains the incoming tx - hashStr := "0000000000000032cb372f5d5d99c1ebf4430a3059b67c47a54dd626550fb50d" - hash, err := chainhash.NewHashFromStr(hashStr) - require.NoError(t, err) - - block, err := client.GetBlockVerboseTx(hash) - require.NoError(t, err) - - // filter incoming tx - inbounds, err := observer.FilterAndParseIncomingTx( - client, - block.Tx, - uint64(block.Height), - "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", - log.Logger, - &chaincfg.TestNet3Params, - ) - require.NoError(t, err) - require.Len(t, inbounds, 1) - require.Equal(t, inbounds[0].Value, 0.0001) - require.Equal(t, inbounds[0].ToAddress, "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2") - - // the text memo is base64 std encoded string:DSRR1RmDCwWmxqY201/TMtsJdmA= - // see https://blockstream.info/testnet/tx/889bfa69eaff80a826286d42ec3f725fd97c3338357ddc3a1f543c2d6266f797 - memo, err := hex.DecodeString("4453525231526d444377576d7871593230312f544d74734a646d413d") - require.NoError(t, err) - require.Equal(t, inbounds[0].MemoBytes, memo) - require.Equal(t, inbounds[0].FromAddress, "tb1qyslx2s8evalx67n88wf42yv7236303ezj3tm2l") - require.Equal(t, inbounds[0].BlockNumber, uint64(2406185)) - require.Equal(t, inbounds[0].TxHash, "889bfa69eaff80a826286d42ec3f725fd97c3338357ddc3a1f543c2d6266f797") -} - -func LiveTest_FilterAndParseIncomingTx_Nop(t *testing.T) { - // setup Bitcoin client - client, err := createRPCClient(chains.BitcoinTestnet.ChainId) - require.NoError(t, err) - - // get a block that contains no incoming tx - hashStr := "000000000000002fd8136dbf91708898da9d6ae61d7c354065a052568e2f2888" - hash, err := chainhash.NewHashFromStr(hashStr) - require.NoError(t, err) - - block, err := client.GetBlockVerboseTx(hash) - require.NoError(t, err) - - // filter incoming tx - inbounds, err := observer.FilterAndParseIncomingTx( - client, - block.Tx, - uint64(block.Height), - "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", - log.Logger, - &chaincfg.TestNet3Params, - ) - - require.NoError(t, err) - require.Empty(t, inbounds) -} - -// TestBitcoinObserverLive is a phony test to run each live test individually -func TestBitcoinObserverLive(t *testing.T) { - if !common.LiveTestEnabled() { - return - } - - LiveTest_NewRPCClient(t) - LiveTest_CheckRPCStatus(t) - LiveTest_GetBlockHeightByHash(t) - LiveTest_BitcoinFeeRate(t) - LiveTest_AvgFeeRateMainnetMempoolSpace(t) - LiveTest_AvgFeeRateTestnetMempoolSpace(t) - LiveTest_GetRecentFeeRate(t) - LiveTest_GetSenderByVin(t) -} - -// LiveTestNewRPCClient creates a new Bitcoin RPC client -func LiveTest_NewRPCClient(t *testing.T) { - btcConfig := config.BTCConfig{ - RPCUsername: "user", - RPCPassword: "pass", - RPCHost: os.Getenv(common.EnvBtcRPCTestnet), - RPCParams: "testnet3", - } - - // create Bitcoin RPC client - client, err := rpc.NewRPCClient(btcConfig) - require.NoError(t, err) - - // get block count - bn, err := client.GetBlockCount() - require.NoError(t, err) - require.Greater(t, bn, int64(0)) -} - -// Live_TestCheckRPCStatus checks the RPC status of the Bitcoin chain -func LiveTest_CheckRPCStatus(t *testing.T) { - // setup Bitcoin client - chainID := chains.BitcoinMainnet.ChainId - client, err := createRPCClient(chainID) - require.NoError(t, err) - - // decode tss address - tssAddress, err := chains.DecodeBtcAddress(testutils.TSSAddressBTCMainnet, chainID) - require.NoError(t, err) - - // check RPC status - _, err = rpc.CheckRPCStatus(client, tssAddress) - require.NoError(t, err) -} - -// LiveTestGetBlockHeightByHash queries Bitcoin block height by hash -func LiveTest_GetBlockHeightByHash(t *testing.T) { - // setup Bitcoin client - client, err := createRPCClient(chains.BitcoinMainnet.ChainId) - require.NoError(t, err) - - // the block hashes to test - expectedHeight := int64(835053) - hash := "00000000000000000000994a5d12976ec5bda078a7b9c27981f0a4e7a6d46d23" - invalidHash := "invalidhash" - - // get block by invalid hash - _, err = rpc.GetBlockHeightByHash(client, invalidHash) - require.ErrorContains(t, err, "error decoding block hash") - - // get block height by block hash - height, err := rpc.GetBlockHeightByHash(client, hash) - require.NoError(t, err) - require.Equal(t, expectedHeight, height) -} - -// LiveTestBitcoinFeeRate query Bitcoin mainnet fee rate every 5 minutes -// and compares Conservative and Economical fee rates for different block targets (1 and 2) -func LiveTest_BitcoinFeeRate(t *testing.T) { - // setup Bitcoin client - client, err := createRPCClient(chains.BitcoinMainnet.ChainId) - require.NoError(t, err) - bn, err := client.GetBlockCount() - if err != nil { - t.Error(err) - } - - // get fee rate for 1 block target - feeRateConservative1, errCon1 := getFeeRate(client, 1, &btcjson.EstimateModeConservative) - if errCon1 != nil { - t.Error(errCon1) - } - feeRateEconomical1, errEco1 := getFeeRate(client, 1, &btcjson.EstimateModeEconomical) - if errEco1 != nil { - t.Error(errEco1) - } - // get fee rate for 2 block target - feeRateConservative2, errCon2 := getFeeRate(client, 2, &btcjson.EstimateModeConservative) - if errCon2 != nil { - t.Error(errCon2) - } - feeRateEconomical2, errEco2 := getFeeRate(client, 2, &btcjson.EstimateModeEconomical) - if errEco2 != nil { - t.Error(errEco2) - } - fmt.Printf( - "Block: %d, Conservative-1 fee rate: %d, Economical-1 fee rate: %d\n", - bn, - feeRateConservative1.Uint64(), - feeRateEconomical1.Uint64(), - ) - fmt.Printf( - "Block: %d, Conservative-2 fee rate: %d, Economical-2 fee rate: %d\n", - bn, - feeRateConservative2.Uint64(), - feeRateEconomical2.Uint64(), - ) - - // monitor fee rate every 5 minutes, adjust the iteration count as needed - for i := 0; i < 1; i++ { - // please uncomment this interval for long running test - //time.Sleep(time.Duration(5) * time.Minute) - - bn, err = client.GetBlockCount() - feeRateConservative1, errCon1 = getFeeRate(client, 1, &btcjson.EstimateModeConservative) - feeRateEconomical1, errEco1 = getFeeRate(client, 1, &btcjson.EstimateModeEconomical) - feeRateConservative2, errCon2 = getFeeRate(client, 2, &btcjson.EstimateModeConservative) - feeRateEconomical2, errEco2 = getFeeRate(client, 2, &btcjson.EstimateModeEconomical) - if err != nil || errCon1 != nil || errEco1 != nil || errCon2 != nil || errEco2 != nil { - continue - } - require.True(t, feeRateConservative1.Uint64() >= feeRateEconomical1.Uint64()) - require.True(t, feeRateConservative2.Uint64() >= feeRateEconomical2.Uint64()) - require.True(t, feeRateConservative1.Uint64() >= feeRateConservative2.Uint64()) - require.True(t, feeRateEconomical1.Uint64() >= feeRateEconomical2.Uint64()) - fmt.Printf( - "Block: %d, Conservative-1 fee rate: %d, Economical-1 fee rate: %d\n", - bn, - feeRateConservative1.Uint64(), - feeRateEconomical1.Uint64(), - ) - fmt.Printf( - "Block: %d, Conservative-2 fee rate: %d, Economical-2 fee rate: %d\n", - bn, - feeRateConservative2.Uint64(), - feeRateEconomical2.Uint64(), - ) - } -} - -// compareAvgFeeRate compares fee rate with mempool.space for blocks [startBlock, endBlock] -func compareAvgFeeRate(t *testing.T, client *rpcclient.Client, startBlock int, endBlock int, testnet bool) { - // mempool.space return 15 blocks [bn-14, bn] per request - for bn := startBlock; bn >= endBlock; { - // get mempool.space return blocks in descending order [bn, bn-14] - mempoolBlocks, err := testutils.GetBlocks(context.Background(), bn, testnet) - if err != nil { - fmt.Printf("error GetBlocks %d: %s\n", bn, err) - time.Sleep(10 * time.Second) - continue - } - - // calculate gas rate for each block - for _, mb := range mempoolBlocks { - // stop on end block - if mb.Height < endBlock { - break - } - bn = int(mb.Height) - 1 - - // get block hash - blkHash, err := client.GetBlockHash(int64(mb.Height)) - if err != nil { - fmt.Printf("error: %s\n", err) - continue - } - // get block - blockVb, err := client.GetBlockVerboseTx(blkHash) - if err != nil { - fmt.Printf("error: %s\n", err) - continue - } - // calculate gas rate - netParams := &chaincfg.MainNetParams - if testnet { - netParams = &chaincfg.TestNet3Params - } - gasRate, err := btc.CalcBlockAvgFeeRate(blockVb, netParams) - require.NoError(t, err) - - // compare with mempool.space - if int(gasRate) == mb.Extras.AvgFeeRate { - fmt.Printf("block %d: gas rate %d == mempool.space gas rate\n", mb.Height, gasRate) - } else if int(gasRate) > mb.Extras.AvgFeeRate { - fmt.Printf("block %d: gas rate %d > mempool.space gas rate %d, diff: %f percent\n", - mb.Height, gasRate, mb.Extras.AvgFeeRate, float64(int(gasRate)-mb.Extras.AvgFeeRate)/float64(mb.Extras.AvgFeeRate)*100) - } else { - fmt.Printf("block %d: gas rate %d < mempool.space gas rate %d, diff: %f percent\n", - mb.Height, gasRate, mb.Extras.AvgFeeRate, float64(mb.Extras.AvgFeeRate-int(gasRate))/float64(mb.Extras.AvgFeeRate)*100) - } - } - } -} - -// LiveTestAvgFeeRateMainnetMempoolSpace compares calculated fee rate with mempool.space fee rate for mainnet -func LiveTest_AvgFeeRateMainnetMempoolSpace(t *testing.T) { - // setup Bitcoin client - client, err := createRPCClient(chains.BitcoinMainnet.ChainId) - require.NoError(t, err) - - // test against mempool.space API for 10000 blocks - //startBlock := 210000 * 3 // 3rd halving - startBlock := 829596 - endBlock := startBlock - 1 // go back to whatever block as needed - - compareAvgFeeRate(t, client, startBlock, endBlock, false) -} - -// LiveTestAvgFeeRateTestnetMempoolSpace compares calculated fee rate with mempool.space fee rate for testnet -func LiveTest_AvgFeeRateTestnetMempoolSpace(t *testing.T) { - // setup Bitcoin client - client, err := createRPCClient(chains.BitcoinTestnet.ChainId) - require.NoError(t, err) - - // test against mempool.space API for 10000 blocks - //startBlock := 210000 * 12 // 12th halving - startBlock := 2577600 - endBlock := startBlock - 1 // go back to whatever block as needed - - compareAvgFeeRate(t, client, startBlock, endBlock, true) -} - -// LiveTestGetRecentFeeRate gets the highest fee rate from recent blocks -func LiveTest_GetRecentFeeRate(t *testing.T) { - // setup Bitcoin testnet client - client, err := createRPCClient(chains.BitcoinTestnet.ChainId) - require.NoError(t, err) - - // get fee rate from recent blocks - feeRate, err := btc.GetRecentFeeRate(client, &chaincfg.TestNet3Params) - require.NoError(t, err) - require.Greater(t, feeRate, uint64(0)) -} - -// LiveTest_GetSenderByVin gets sender address for each vin and compares with mempool.space sender address -func LiveTest_GetSenderByVin(t *testing.T) { - // setup Bitcoin client - chainID := chains.BitcoinMainnet.ChainId - client, err := createRPCClient(chainID) - require.NoError(t, err) - - // net params - net, err := chains.GetBTCChainParams(chainID) - require.NoError(t, err) - testnet := false - if chainID == chains.BitcoinTestnet.ChainId { - testnet = true - } - - // calculates block range to test - startBlock, err := client.GetBlockCount() - require.NoError(t, err) - endBlock := startBlock - 1 // go back to whatever block as needed - - // loop through mempool.space blocks backwards -BLOCKLOOP: - for bn := startBlock; bn >= endBlock; { - // get mempool.space txs for the block - _, mempoolTxs, err := getMempoolSpaceTxsByBlock(t, client, bn, testnet) - if err != nil { - time.Sleep(3 * time.Second) - continue - } - - // loop through each tx in the block - for i, mptx := range mempoolTxs { - // sample 10 txs per block - if i >= 10 { - break - } - for _, mpvin := range mptx.Vin { - // skip coinbase tx - if mpvin.IsCoinbase { - continue - } - // get sender address for each vin - vin := btcjson.Vin{ - Txid: mpvin.TxID, - Vout: mpvin.Vout, - } - senderAddr, err := observer.GetSenderAddressByVin(client, vin, net) - if err != nil { - fmt.Printf("error GetSenderAddressByVin for block %d, tx %s vout %d: %s\n", bn, vin.Txid, vin.Vout, err) - time.Sleep(3 * time.Second) - continue BLOCKLOOP // retry the block - } - if senderAddr != mpvin.Prevout.ScriptpubkeyAddress { - t.Errorf("block %d, tx %s, vout %d: want %s, got %s\n", bn, vin.Txid, vin.Vout, mpvin.Prevout.ScriptpubkeyAddress, senderAddr) - } else { - fmt.Printf("block: %d sender address type: %s\n", bn, mpvin.Prevout.ScriptpubkeyType) - } - } - } - bn-- - time.Sleep(100 * time.Millisecond) - } -} - -// LiveTestGetTransactionFeeAndRate gets the transaction fee and rate for each tx and compares with mempool.space fee rate -func LiveTest_GetTransactionFeeAndRate(t *testing.T) { - // setup Bitcoin client - chainID := chains.BitcoinTestnet.ChainId - client, err := createRPCClient(chainID) - require.NoError(t, err) - - // testnet or mainnet - testnet := false - if chainID == chains.BitcoinTestnet.ChainId { - testnet = true - } - - // calculates block range to test - startBlock, err := client.GetBlockCount() - require.NoError(t, err) - endBlock := startBlock - 100 // go back whatever blocks as needed - - // loop through mempool.space blocks backwards - for bn := startBlock; bn >= endBlock; { - // get mempool.space txs for the block - blkHash, mempoolTxs, err := getMempoolSpaceTxsByBlock(t, client, bn, testnet) - if err != nil { - time.Sleep(3 * time.Second) - continue - } - - // get the block from rpc client - block, err := client.GetBlockVerboseTx(blkHash) - if err != nil { - time.Sleep(3 * time.Second) - continue - } - - // loop through each tx in the block (skip coinbase tx) - for i := 1; i < len(block.Tx); { - // sample 20 txs per block - if i >= 20 { - break - } - - // the two txs from two different sources - tx := block.Tx[i] - mpTx := mempoolTxs[i] - require.Equal(t, tx.Txid, mpTx.TxID) - - // get transaction fee rate for the raw result - fee, feeRate, err := rpc.GetTransactionFeeAndRate(client, &tx) - if err != nil { - t.Logf("error GetTransactionFeeRate %s: %s\n", mpTx.TxID, err) - continue - } - require.EqualValues(t, mpTx.Fee, fee) - require.EqualValues(t, mpTx.Weight, tx.Weight) - - // calculate mempool.space fee rate - vBytes := mpTx.Weight / blockchain.WitnessScaleFactor - mpFeeRate := int64(mpTx.Fee / vBytes) - - // compare our fee rate with mempool.space fee rate - var diff int64 - var diffPercent float64 - if feeRate == mpFeeRate { - fmt.Printf("tx %s: [our rate] %5d == %5d [mempool.space]", mpTx.TxID, feeRate, mpFeeRate) - } else if feeRate > mpFeeRate { - diff = feeRate - mpFeeRate - fmt.Printf("tx %s: [our rate] %5d > %5d [mempool.space]", mpTx.TxID, feeRate, mpFeeRate) - } else { - diff = mpFeeRate - feeRate - fmt.Printf("tx %s: [our rate] %5d < %5d [mempool.space]", mpTx.TxID, feeRate, mpFeeRate) - } - - // print the diff percentage - diffPercent = float64(diff) / float64(mpFeeRate) * 100 - if diff > 0 { - fmt.Printf(", diff: %f%%\n", diffPercent) - } else { - fmt.Printf("\n") - } - - // the expected diff percentage should be within 5% - if mpFeeRate >= 20 { - require.LessOrEqual(t, diffPercent, 5.0) - } else { - // for small fee rate, the absolute diff should be within 1 satoshi/vByte - require.LessOrEqual(t, diff, int64(1)) - } - - // next tx - i++ - } - - bn-- - time.Sleep(100 * time.Millisecond) - } -} - -func LiveTest_CalcDepositorFee(t *testing.T) { - // setup Bitcoin client - client, err := createRPCClient(chains.BitcoinMainnet.ChainId) - require.NoError(t, err) - - // test tx hash - // https://mempool.space/tx/8dc0d51f83810cec7fcb5b194caebfc5fc64b10f9fe21845dfecc621d2a28538 - hash, err := chainhash.NewHashFromStr("8dc0d51f83810cec7fcb5b194caebfc5fc64b10f9fe21845dfecc621d2a28538") - require.NoError(t, err) - - // get the raw transaction result - rawResult, err := client.GetRawTransactionVerbose(hash) - require.NoError(t, err) - - t.Run("should return default depositor fee", func(t *testing.T) { - depositorFee, err := btc.CalcDepositorFee(client, rawResult, &chaincfg.RegressionNetParams) - require.NoError(t, err) - require.Equal(t, btc.DefaultDepositorFee, depositorFee) - }) - - t.Run("should return correct depositor fee for a given tx", func(t *testing.T) { - depositorFee, err := btc.CalcDepositorFee(client, rawResult, &chaincfg.MainNetParams) - require.NoError(t, err) - - // the actual fee rate is 860 sat/vByte - // #nosec G115 always in range - expectedRate := int64(float64(860) * common.BTCOutboundGasPriceMultiplier) - expectedFee := btc.DepositorFee(expectedRate) - require.Equal(t, expectedFee, depositorFee) - }) -} diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index 34c0f592a7..b59d9f1232 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -11,9 +11,9 @@ import ( "github.com/btcsuite/btcd/btcec/v2" btcecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/pkg/errors" @@ -28,7 +28,6 @@ import ( "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/compliance" - "github.com/zeta-chain/node/zetaclient/config" "github.com/zeta-chain/node/zetaclient/logs" "github.com/zeta-chain/node/zetaclient/outboundprocessor" ) @@ -47,42 +46,23 @@ const ( broadcastRetries = 5 ) -// Signer deals with signing BTC transactions and implements the ChainSigner interface +type RPC interface { + GetNetworkInfo(ctx context.Context) (*btcjson.GetNetworkInfoResult, error) + SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) +} + +// Signer deals with signing & broadcasting BTC transactions. type Signer struct { *base.Signer - - // client is the RPC client to interact with the Bitcoin chain - client interfaces.BTCRPCClient + rpc RPC } -// NewSigner creates a new Bitcoin signer -func NewSigner( - chain chains.Chain, - tss interfaces.TSSSigner, - logger base.Logger, - cfg config.BTCConfig, -) (*Signer, error) { - // create base signer - baseSigner := base.NewSigner(chain, tss, logger) - - // create the bitcoin rpc client using the provided config - connCfg := &rpcclient.ConnConfig{ - Host: cfg.RPCHost, - User: cfg.RPCUsername, - Pass: cfg.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: cfg.RPCParams, - } - client, err := rpcclient.New(connCfg, nil) - if err != nil { - return nil, errors.Wrap(err, "unable to create bitcoin rpc client") - } - +// New creates a new Bitcoin signer +func New(chain chains.Chain, tss interfaces.TSSSigner, rpc RPC, logger base.Logger) *Signer { return &Signer{ - Signer: baseSigner, - client: client, - }, nil + Signer: base.NewSigner(chain, tss, logger), + rpc: rpc, + } } // AddWithdrawTxOutputs adds the 3 outputs to the withdraw tx @@ -276,7 +256,7 @@ func (signer *Signer) SignWithdrawTx( } // Broadcast sends the signed transaction to the network -func (signer *Signer) Broadcast(signedTx *wire.MsgTx) error { +func (signer *Signer) Broadcast(ctx context.Context, signedTx *wire.MsgTx) error { var outBuff bytes.Buffer if err := signedTx.Serialize(&outBuff); err != nil { return errors.Wrap(err, "unable to serialize tx") @@ -287,7 +267,7 @@ func (signer *Signer) Broadcast(signedTx *wire.MsgTx) error { Str("signer.tx_payload", hex.EncodeToString(outBuff.Bytes())). Msg("Broadcasting transaction") - _, err := signer.client.SendRawTransaction(signedTx, true) + _, err := signer.rpc.SendRawTransaction(ctx, signedTx, true) if err != nil { return errors.Wrap(err, "unable to broadcast raw tx") } @@ -361,7 +341,7 @@ func (signer *Signer) TryProcessOutbound( amount := float64(params.Amount.Uint64()) / 1e8 // Add 1 satoshi/byte to gasPrice to avoid minRelayTxFee issue - networkInfo, err := signer.client.GetNetworkInfo() + networkInfo, err := signer.rpc.GetNetworkInfo(ctx) if err != nil { logger.Error().Err(err).Msgf("cannot get bitcoin network info") return @@ -422,7 +402,7 @@ func (signer *Signer) TryProcessOutbound( backOff := broadcastBackoff for i := 0; i < broadcastRetries; i++ { time.Sleep(backOff) - err := signer.Broadcast(tx) + err := signer.Broadcast(ctx, tx) if err != nil { logger.Warn().Err(err).Fields(lf).Msgf("Broadcasting Bitcoin tx, retry %d", i) backOff *= 2 diff --git a/zetaclient/chains/bitcoin/signer/signer_test.go b/zetaclient/chains/bitcoin/signer/signer_test.go index f06ad4a9c2..10363472bc 100644 --- a/zetaclient/chains/bitcoin/signer/signer_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_test.go @@ -21,7 +21,6 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/zetaclient/chains/base" - "github.com/zeta-chain/node/zetaclient/config" "github.com/zeta-chain/node/zetaclient/testutils/mocks" ) @@ -31,6 +30,10 @@ type BTCSignerSuite struct { var _ = Suite(&BTCSignerSuite{}) +type cWrapper struct{ *C } + +func (cWrapper) Cleanup(func()) { /* noop */ } + func (s *BTCSignerSuite) SetUpTest(c *C) { // test private key with EVM address //// EVM: 0x236C7f53a90493Bb423411fe4117Cb4c2De71DfB @@ -47,13 +50,12 @@ func (s *BTCSignerSuite) SetUpTest(c *C) { tss := mocks.NewTSSFromPrivateKey(c, privateKey) - s.btcSigner, err = NewSigner( + s.btcSigner = New( chains.Chain{}, tss, + mocks.NewBitcoinClient(cWrapper{c}), base.DefaultLogger(), - config.BTCConfig{}, ) - c.Assert(err, IsNil) } func (s *BTCSignerSuite) TestP2PH(c *C) { @@ -227,13 +229,12 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { func TestAddWithdrawTxOutputs(t *testing.T) { // Create test signer and receiver address - signer, err := NewSigner( + signer := New( chains.BitcoinMainnet, mocks.NewTSS(t).FakePubKey(testutils.TSSPubKeyMainnet), + mocks.NewBitcoinClient(t), base.DefaultLogger(), - config.BTCConfig{}, ) - require.NoError(t, err) // tss address and script tssAddr, err := signer.TSS().PubKey().AddressBTC(chains.BitcoinMainnet.ChainId) @@ -377,21 +378,3 @@ func TestAddWithdrawTxOutputs(t *testing.T) { }) } } - -// Coverage doesn't seem to pick this up from the suite -func TestNewBTCSigner(t *testing.T) { - // test private key with EVM address - //// EVM: 0x236C7f53a90493Bb423411fe4117Cb4c2De71DfB - // BTC testnet3: muGe9prUBjQwEnX19zG26fVRHNi8z7kSPo - skHex := "7b8507ba117e069f4a3f456f505276084f8c92aee86ac78ae37b4d1801d35fa8" - privateKey, err := crypto.HexToECDSA(skHex) - require.NoError(t, err) - tss := mocks.NewTSSFromPrivateKey(t, privateKey) - btcSigner, err := NewSigner( - chains.Chain{}, - tss, - base.DefaultLogger(), - config.BTCConfig{}) - require.NoError(t, err) - require.NotNil(t, btcSigner) -} diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index a8fe9c71fc..98976054ba 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -6,11 +6,6 @@ import ( "math/big" sdkmath "cosmossdk.io/math" - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" - "github.com/btcsuite/btcd/wire" cometbfttypes "github.com/cometbft/cometbft/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -148,29 +143,6 @@ type ZetacoreClient interface { NewBlockSubscriber(ctx context.Context) (chan cometbfttypes.EventDataNewBlock, error) } -// BTCRPCClient is the interface for BTC RPC client -// -// WARN: you must add any RPCs used on mainnet/testnet to the whitelist in https://github.com/zeta-chain/bitcoin-core-docker -type BTCRPCClient interface { - GetNetworkInfo() (*btcjson.GetNetworkInfoResult, error) - CreateWallet(name string, opts ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error) - GetNewAddress(account string) (btcutil.Address, error) - GenerateToAddress(numBlocks int64, address btcutil.Address, maxTries *int64) ([]*chainhash.Hash, error) - GetBalance(account string) (btcutil.Amount, error) - SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) - ListUnspent() ([]btcjson.ListUnspentResult, error) - ListUnspentMinMaxAddresses(minConf int, maxConf int, addrs []btcutil.Address) ([]btcjson.ListUnspentResult, error) - EstimateSmartFee(confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) - GetTransaction(txHash *chainhash.Hash) (*btcjson.GetTransactionResult, error) - GetRawTransaction(txHash *chainhash.Hash) (*btcutil.Tx, error) - GetRawTransactionVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) - GetBlockCount() (int64, error) - GetBlockHash(blockHeight int64) (*chainhash.Hash, error) - GetBlockVerbose(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) - GetBlockVerboseTx(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) - GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error) -} - // EVMRPCClient is the interface for EVM RPC client type EVMRPCClient interface { bind.ContractBackend diff --git a/zetaclient/context/app.go b/zetaclient/context/app.go index d796c5beec..ff3db6454d 100644 --- a/zetaclient/context/app.go +++ b/zetaclient/context/app.go @@ -166,8 +166,11 @@ func (a *AppContext) updateChainRegistry( existingChainIDs = a.chainRegistry.ChainIDs() ) + slices.Sort(freshChainIDs) + slices.Sort(existingChainIDs) + // 2. Compare existing chains with fresh ones - if len(existingChainIDs) > 0 && !elementsMatch(existingChainIDs, freshChainIDs) { + if len(existingChainIDs) > 0 && !slicesEqual(existingChainIDs, freshChainIDs) { a.logger.Warn(). Ints64("chains.current", existingChainIDs). Ints64("chains.new", freshChainIDs). @@ -232,16 +235,11 @@ func zetaObserverChainParams(chainID int64) *observertypes.ChainParams { return &observertypes.ChainParams{ChainId: chainID, IsSupported: true} } -// elementsMatch returns true if two slices are equal. -// SORTS the slices before comparison. -func elementsMatch[T constraints.Ordered](a, b []T) bool { +func slicesEqual[T constraints.Ordered](a, b []T) bool { if len(a) != len(b) { return false } - slices.Sort(a) - slices.Sort(b) - for i := range a { if a[i] != b[i] { return false diff --git a/zetaclient/metrics/metrics.go b/zetaclient/metrics/metrics.go index 6e46037209..7be95f170f 100644 --- a/zetaclient/metrics/metrics.go +++ b/zetaclient/metrics/metrics.go @@ -198,6 +198,25 @@ var ( }, []string{"status", "task_group", "task_name"}, ) + + RPCClientCounter = promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: ZetaClientNamespace, + Name: "rpc_client_calls_total", + Help: "Total number of rpc calls", + }, + []string{"status", "client", "method"}, + ) + + RPCClientDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: ZetaClientNamespace, + Name: "rpc_client_duration_seconds", + Help: "Histogram of rpc client calls durations in seconds", + Buckets: []float64{0.05, 0.1, 0.2, 0.3, 0.5, 1, 1.5, 2, 3, 5, 7.5, 10, 15}, // 50ms to 15s + }, + []string{"client"}, + ) ) // NewMetrics creates a new Metrics instance diff --git a/zetaclient/orchestrator/bootstrap_test.go b/zetaclient/orchestrator/bootstrap_test.go index c6f44acf9a..3c410d97ab 100644 --- a/zetaclient/orchestrator/bootstrap_test.go +++ b/zetaclient/orchestrator/bootstrap_test.go @@ -155,11 +155,6 @@ func TestCreateChainObserverMap(t *testing.T) { t.Run("CreateChainObserverMap", func(t *testing.T) { // ARRANGE - // Given a BTC server - btcServer, btcConfig := testrpc.NewBtcServer(t) - - btcServer.SetBlockCount(123) - // Given generic EVM RPC evmServer := testrpc.NewEVMServer(t) evmServer.SetBlockNumber(100) @@ -181,7 +176,6 @@ func TestCreateChainObserverMap(t *testing.T) { Endpoint: evmServer.Endpoint, } - cfg.BTCChainConfigs[chains.BitcoinMainnet.ChainId] = btcConfig cfg.SolanaConfig = solConfig cfg.TONConfig = tonConfig diff --git a/zetaclient/orchestrator/v2_bootstrap.go b/zetaclient/orchestrator/v2_bootstrap.go index 1962433c1c..e05375f6f3 100644 --- a/zetaclient/orchestrator/v2_bootstrap.go +++ b/zetaclient/orchestrator/v2_bootstrap.go @@ -6,8 +6,8 @@ import ( "github.com/pkg/errors" "github.com/zeta-chain/node/zetaclient/chains/bitcoin" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" btcsigner "github.com/zeta-chain/node/zetaclient/chains/bitcoin/signer" zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/db" @@ -29,7 +29,7 @@ func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (*bitcoin. return nil, errors.Wrap(errSkipChain, "unable to find btc config") } - rpcClient, err := rpc.NewRPCClient(cfg) + rpcClient, err := client.New(cfg, chain.ID(), oc.logger.Logger) if err != nil { return nil, errors.Wrap(err, "unable to create rpc client") } @@ -64,10 +64,7 @@ func (oc *V2) bootstrapBitcoin(ctx context.Context, chain zctx.Chain) (*bitcoin. return nil, errors.Wrap(err, "unable to create observer") } - signer, err := btcsigner.NewSigner(*rawChain, oc.deps.TSS, oc.logger.base, cfg) - if err != nil { - return nil, errors.Wrap(err, "unable to create signer") - } + signer := btcsigner.New(*rawChain, oc.deps.TSS, rpcClient, oc.logger.base) return bitcoin.New(oc.scheduler, observer, signer), nil } diff --git a/zetaclient/orchestrator/v2_orchestrator_test.go b/zetaclient/orchestrator/v2_orchestrator_test.go index c10c11481f..8da2a76184 100644 --- a/zetaclient/orchestrator/v2_orchestrator_test.go +++ b/zetaclient/orchestrator/v2_orchestrator_test.go @@ -23,7 +23,6 @@ import ( "github.com/zeta-chain/node/zetaclient/metrics" "github.com/zeta-chain/node/zetaclient/testutils/mocks" "github.com/zeta-chain/node/zetaclient/testutils/testlog" - "github.com/zeta-chain/node/zetaclient/testutils/testrpc" ) func TestOrchestratorV2(t *testing.T) { @@ -110,8 +109,6 @@ func newTestSuite(t *testing.T) *testSuite { Compliance: logger.Logger, } - testrpc.NewBtcServer(t) - chainList, chainParams := parseChainsWithParams(t, defaultChainsWithParams...) ctx, appCtx := newAppContext(t, logger.Logger, chainList, chainParams) diff --git a/zetaclient/testutils/mocks/bitcoin_client.go b/zetaclient/testutils/mocks/bitcoin_client.go new file mode 100644 index 0000000000..c9219f6aab --- /dev/null +++ b/zetaclient/testutils/mocks/bitcoin_client.go @@ -0,0 +1,893 @@ +// Code generated by mockery v2.51.0. DO NOT EDIT. + +package mocks + +import ( + btcjson "github.com/btcsuite/btcd/btcjson" + btcutil "github.com/btcsuite/btcd/btcutil" + + chainhash "github.com/btcsuite/btcd/chaincfg/chainhash" + + context "context" + + json "encoding/json" + + mock "github.com/stretchr/testify/mock" + + rpcclient "github.com/btcsuite/btcd/rpcclient" + + time "time" + + wire "github.com/btcsuite/btcd/wire" +) + +// BitcoinClient is an autogenerated mock type for the client type +type BitcoinClient struct { + mock.Mock +} + +// CreateRawTransaction provides a mock function with given fields: ctx, inputs, amounts, lockTime +func (_m *BitcoinClient) CreateRawTransaction(ctx context.Context, inputs []btcjson.TransactionInput, amounts map[btcutil.Address]btcutil.Amount, lockTime *int64) (*wire.MsgTx, error) { + ret := _m.Called(ctx, inputs, amounts, lockTime) + + if len(ret) == 0 { + panic("no return value specified for CreateRawTransaction") + } + + var r0 *wire.MsgTx + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []btcjson.TransactionInput, map[btcutil.Address]btcutil.Amount, *int64) (*wire.MsgTx, error)); ok { + return rf(ctx, inputs, amounts, lockTime) + } + if rf, ok := ret.Get(0).(func(context.Context, []btcjson.TransactionInput, map[btcutil.Address]btcutil.Amount, *int64) *wire.MsgTx); ok { + r0 = rf(ctx, inputs, amounts, lockTime) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*wire.MsgTx) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []btcjson.TransactionInput, map[btcutil.Address]btcutil.Amount, *int64) error); ok { + r1 = rf(ctx, inputs, amounts, lockTime) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateWallet provides a mock function with given fields: ctx, name, opts +func (_m *BitcoinClient) CreateWallet(ctx context.Context, name string, opts ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateWallet") + } + + var r0 *btcjson.CreateWalletResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error)); ok { + return rf(ctx, name, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...rpcclient.CreateWalletOpt) *btcjson.CreateWalletResult); ok { + r0 = rf(ctx, name, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.CreateWalletResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...rpcclient.CreateWalletOpt) error); ok { + r1 = rf(ctx, name, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EstimateSmartFee provides a mock function with given fields: ctx, confTarget, mode +func (_m *BitcoinClient) EstimateSmartFee(ctx context.Context, confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) { + ret := _m.Called(ctx, confTarget, mode) + + if len(ret) == 0 { + panic("no return value specified for EstimateSmartFee") + } + + var r0 *btcjson.EstimateSmartFeeResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error)); ok { + return rf(ctx, confTarget, mode) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, *btcjson.EstimateSmartFeeMode) *btcjson.EstimateSmartFeeResult); ok { + r0 = rf(ctx, confTarget, mode) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.EstimateSmartFeeResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, *btcjson.EstimateSmartFeeMode) error); ok { + r1 = rf(ctx, confTarget, mode) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GenerateToAddress provides a mock function with given fields: ctx, numBlocks, address, maxTries +func (_m *BitcoinClient) GenerateToAddress(ctx context.Context, numBlocks int64, address btcutil.Address, maxTries *int64) ([]*chainhash.Hash, error) { + ret := _m.Called(ctx, numBlocks, address, maxTries) + + if len(ret) == 0 { + panic("no return value specified for GenerateToAddress") + } + + var r0 []*chainhash.Hash + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, btcutil.Address, *int64) ([]*chainhash.Hash, error)); ok { + return rf(ctx, numBlocks, address, maxTries) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, btcutil.Address, *int64) []*chainhash.Hash); ok { + r0 = rf(ctx, numBlocks, address, maxTries) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*chainhash.Hash) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, btcutil.Address, *int64) error); ok { + r1 = rf(ctx, numBlocks, address, maxTries) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetBalance provides a mock function with given fields: ctx, account +func (_m *BitcoinClient) GetBalance(ctx context.Context, account string) (btcutil.Amount, error) { + ret := _m.Called(ctx, account) + + if len(ret) == 0 { + panic("no return value specified for GetBalance") + } + + var r0 btcutil.Amount + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (btcutil.Amount, error)); ok { + return rf(ctx, account) + } + if rf, ok := ret.Get(0).(func(context.Context, string) btcutil.Amount); ok { + r0 = rf(ctx, account) + } else { + r0 = ret.Get(0).(btcutil.Amount) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, account) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetBlockCount provides a mock function with given fields: ctx +func (_m *BitcoinClient) GetBlockCount(ctx context.Context) (int64, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetBlockCount") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (int64, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) int64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetBlockHash provides a mock function with given fields: ctx, blockHeight +func (_m *BitcoinClient) GetBlockHash(ctx context.Context, blockHeight int64) (*chainhash.Hash, error) { + ret := _m.Called(ctx, blockHeight) + + if len(ret) == 0 { + panic("no return value specified for GetBlockHash") + } + + var r0 *chainhash.Hash + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*chainhash.Hash, error)); ok { + return rf(ctx, blockHeight) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *chainhash.Hash); ok { + r0 = rf(ctx, blockHeight) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*chainhash.Hash) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, blockHeight) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetBlockHeader provides a mock function with given fields: ctx, hash +func (_m *BitcoinClient) GetBlockHeader(ctx context.Context, hash *chainhash.Hash) (*wire.BlockHeader, error) { + ret := _m.Called(ctx, hash) + + if len(ret) == 0 { + panic("no return value specified for GetBlockHeader") + } + + var r0 *wire.BlockHeader + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash) (*wire.BlockHeader, error)); ok { + return rf(ctx, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash) *wire.BlockHeader); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*wire.BlockHeader) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *chainhash.Hash) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetBlockHeightByStr provides a mock function with given fields: ctx, blockHash +func (_m *BitcoinClient) GetBlockHeightByStr(ctx context.Context, blockHash string) (int64, error) { + ret := _m.Called(ctx, blockHash) + + if len(ret) == 0 { + panic("no return value specified for GetBlockHeightByStr") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (int64, error)); ok { + return rf(ctx, blockHash) + } + if rf, ok := ret.Get(0).(func(context.Context, string) int64); ok { + r0 = rf(ctx, blockHash) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, blockHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetBlockVerbose provides a mock function with given fields: ctx, hash +func (_m *BitcoinClient) GetBlockVerbose(ctx context.Context, hash *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) { + ret := _m.Called(ctx, hash) + + if len(ret) == 0 { + panic("no return value specified for GetBlockVerbose") + } + + var r0 *btcjson.GetBlockVerboseTxResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error)); ok { + return rf(ctx, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash) *btcjson.GetBlockVerboseTxResult); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.GetBlockVerboseTxResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *chainhash.Hash) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetBlockVerboseByStr provides a mock function with given fields: ctx, blockHash +func (_m *BitcoinClient) GetBlockVerboseByStr(ctx context.Context, blockHash string) (*btcjson.GetBlockVerboseTxResult, error) { + ret := _m.Called(ctx, blockHash) + + if len(ret) == 0 { + panic("no return value specified for GetBlockVerboseByStr") + } + + var r0 *btcjson.GetBlockVerboseTxResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*btcjson.GetBlockVerboseTxResult, error)); ok { + return rf(ctx, blockHash) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *btcjson.GetBlockVerboseTxResult); ok { + r0 = rf(ctx, blockHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.GetBlockVerboseTxResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, blockHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetNetworkInfo provides a mock function with given fields: ctx +func (_m *BitcoinClient) GetNetworkInfo(ctx context.Context) (*btcjson.GetNetworkInfoResult, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetNetworkInfo") + } + + var r0 *btcjson.GetNetworkInfoResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*btcjson.GetNetworkInfoResult, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *btcjson.GetNetworkInfoResult); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.GetNetworkInfoResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetNewAddress provides a mock function with given fields: ctx, account +func (_m *BitcoinClient) GetNewAddress(ctx context.Context, account string) (btcutil.Address, error) { + ret := _m.Called(ctx, account) + + if len(ret) == 0 { + panic("no return value specified for GetNewAddress") + } + + var r0 btcutil.Address + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (btcutil.Address, error)); ok { + return rf(ctx, account) + } + if rf, ok := ret.Get(0).(func(context.Context, string) btcutil.Address); ok { + r0 = rf(ctx, account) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(btcutil.Address) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, account) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRawTransaction provides a mock function with given fields: ctx, hash +func (_m *BitcoinClient) GetRawTransaction(ctx context.Context, hash *chainhash.Hash) (*btcutil.Tx, error) { + ret := _m.Called(ctx, hash) + + if len(ret) == 0 { + panic("no return value specified for GetRawTransaction") + } + + var r0 *btcutil.Tx + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash) (*btcutil.Tx, error)); ok { + return rf(ctx, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash) *btcutil.Tx); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcutil.Tx) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *chainhash.Hash) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRawTransactionByStr provides a mock function with given fields: ctx, hash +func (_m *BitcoinClient) GetRawTransactionByStr(ctx context.Context, hash string) (*btcutil.Tx, error) { + ret := _m.Called(ctx, hash) + + if len(ret) == 0 { + panic("no return value specified for GetRawTransactionByStr") + } + + var r0 *btcutil.Tx + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*btcutil.Tx, error)); ok { + return rf(ctx, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *btcutil.Tx); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcutil.Tx) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRawTransactionResult provides a mock function with given fields: ctx, hash, res +func (_m *BitcoinClient) GetRawTransactionResult(ctx context.Context, hash *chainhash.Hash, res *btcjson.GetTransactionResult) (btcjson.TxRawResult, error) { + ret := _m.Called(ctx, hash, res) + + if len(ret) == 0 { + panic("no return value specified for GetRawTransactionResult") + } + + var r0 btcjson.TxRawResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash, *btcjson.GetTransactionResult) (btcjson.TxRawResult, error)); ok { + return rf(ctx, hash, res) + } + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash, *btcjson.GetTransactionResult) btcjson.TxRawResult); ok { + r0 = rf(ctx, hash, res) + } else { + r0 = ret.Get(0).(btcjson.TxRawResult) + } + + if rf, ok := ret.Get(1).(func(context.Context, *chainhash.Hash, *btcjson.GetTransactionResult) error); ok { + r1 = rf(ctx, hash, res) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRawTransactionVerbose provides a mock function with given fields: ctx, hash +func (_m *BitcoinClient) GetRawTransactionVerbose(ctx context.Context, hash *chainhash.Hash) (*btcjson.TxRawResult, error) { + ret := _m.Called(ctx, hash) + + if len(ret) == 0 { + panic("no return value specified for GetRawTransactionVerbose") + } + + var r0 *btcjson.TxRawResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash) (*btcjson.TxRawResult, error)); ok { + return rf(ctx, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash) *btcjson.TxRawResult); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.TxRawResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *chainhash.Hash) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransaction provides a mock function with given fields: ctx, hash +func (_m *BitcoinClient) GetTransaction(ctx context.Context, hash *chainhash.Hash) (*btcjson.GetTransactionResult, error) { + ret := _m.Called(ctx, hash) + + if len(ret) == 0 { + panic("no return value specified for GetTransaction") + } + + var r0 *btcjson.GetTransactionResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash) (*btcjson.GetTransactionResult, error)); ok { + return rf(ctx, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, *chainhash.Hash) *btcjson.GetTransactionResult); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btcjson.GetTransactionResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *chainhash.Hash) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransactionByStr provides a mock function with given fields: ctx, hash +func (_m *BitcoinClient) GetTransactionByStr(ctx context.Context, hash string) (*chainhash.Hash, *btcjson.GetTransactionResult, error) { + ret := _m.Called(ctx, hash) + + if len(ret) == 0 { + panic("no return value specified for GetTransactionByStr") + } + + var r0 *chainhash.Hash + var r1 *btcjson.GetTransactionResult + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*chainhash.Hash, *btcjson.GetTransactionResult, error)); ok { + return rf(ctx, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *chainhash.Hash); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*chainhash.Hash) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) *btcjson.GetTransactionResult); ok { + r1 = rf(ctx, hash) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*btcjson.GetTransactionResult) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { + r2 = rf(ctx, hash) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GetTransactionFeeAndRate provides a mock function with given fields: ctx, tx +func (_m *BitcoinClient) GetTransactionFeeAndRate(ctx context.Context, tx *btcjson.TxRawResult) (int64, int64, error) { + ret := _m.Called(ctx, tx) + + if len(ret) == 0 { + panic("no return value specified for GetTransactionFeeAndRate") + } + + var r0 int64 + var r1 int64 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *btcjson.TxRawResult) (int64, int64, error)); ok { + return rf(ctx, tx) + } + if rf, ok := ret.Get(0).(func(context.Context, *btcjson.TxRawResult) int64); ok { + r0 = rf(ctx, tx) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, *btcjson.TxRawResult) int64); ok { + r1 = rf(ctx, tx) + } else { + r1 = ret.Get(1).(int64) + } + + if rf, ok := ret.Get(2).(func(context.Context, *btcjson.TxRawResult) error); ok { + r2 = rf(ctx, tx) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// Healthcheck provides a mock function with given fields: ctx, tssAddress +func (_m *BitcoinClient) Healthcheck(ctx context.Context, tssAddress btcutil.Address) (time.Time, error) { + ret := _m.Called(ctx, tssAddress) + + if len(ret) == 0 { + panic("no return value specified for Healthcheck") + } + + var r0 time.Time + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, btcutil.Address) (time.Time, error)); ok { + return rf(ctx, tssAddress) + } + if rf, ok := ret.Get(0).(func(context.Context, btcutil.Address) time.Time); ok { + r0 = rf(ctx, tssAddress) + } else { + r0 = ret.Get(0).(time.Time) + } + + if rf, ok := ret.Get(1).(func(context.Context, btcutil.Address) error); ok { + r1 = rf(ctx, tssAddress) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ImportAddress provides a mock function with given fields: ctx, address +func (_m *BitcoinClient) ImportAddress(ctx context.Context, address string) error { + ret := _m.Called(ctx, address) + + if len(ret) == 0 { + panic("no return value specified for ImportAddress") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, address) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ImportPrivKeyRescan provides a mock function with given fields: ctx, privKeyWIF, label, rescan +func (_m *BitcoinClient) ImportPrivKeyRescan(ctx context.Context, privKeyWIF *btcutil.WIF, label string, rescan bool) error { + ret := _m.Called(ctx, privKeyWIF, label, rescan) + + if len(ret) == 0 { + panic("no return value specified for ImportPrivKeyRescan") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *btcutil.WIF, string, bool) error); ok { + r0 = rf(ctx, privKeyWIF, label, rescan) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ListUnspent provides a mock function with given fields: ctx +func (_m *BitcoinClient) ListUnspent(ctx context.Context) ([]btcjson.ListUnspentResult, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListUnspent") + } + + var r0 []btcjson.ListUnspentResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]btcjson.ListUnspentResult, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []btcjson.ListUnspentResult); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]btcjson.ListUnspentResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListUnspentMinMaxAddresses provides a mock function with given fields: ctx, minConf, maxConf, addresses +func (_m *BitcoinClient) ListUnspentMinMaxAddresses(ctx context.Context, minConf int, maxConf int, addresses []btcutil.Address) ([]btcjson.ListUnspentResult, error) { + ret := _m.Called(ctx, minConf, maxConf, addresses) + + if len(ret) == 0 { + panic("no return value specified for ListUnspentMinMaxAddresses") + } + + var r0 []btcjson.ListUnspentResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int, int, []btcutil.Address) ([]btcjson.ListUnspentResult, error)); ok { + return rf(ctx, minConf, maxConf, addresses) + } + if rf, ok := ret.Get(0).(func(context.Context, int, int, []btcutil.Address) []btcjson.ListUnspentResult); ok { + r0 = rf(ctx, minConf, maxConf, addresses) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]btcjson.ListUnspentResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int, int, []btcutil.Address) error); ok { + r1 = rf(ctx, minConf, maxConf, addresses) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Ping provides a mock function with given fields: ctx +func (_m *BitcoinClient) Ping(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Ping") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RawRequest provides a mock function with given fields: ctx, method, params +func (_m *BitcoinClient) RawRequest(ctx context.Context, method string, params []json.RawMessage) (json.RawMessage, error) { + ret := _m.Called(ctx, method, params) + + if len(ret) == 0 { + panic("no return value specified for RawRequest") + } + + var r0 json.RawMessage + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []json.RawMessage) (json.RawMessage, error)); ok { + return rf(ctx, method, params) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []json.RawMessage) json.RawMessage); ok { + r0 = rf(ctx, method, params) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(json.RawMessage) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []json.RawMessage) error); ok { + r1 = rf(ctx, method, params) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SendRawTransaction provides a mock function with given fields: ctx, tx, allowHighFees +func (_m *BitcoinClient) SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { + ret := _m.Called(ctx, tx, allowHighFees) + + if len(ret) == 0 { + panic("no return value specified for SendRawTransaction") + } + + var r0 *chainhash.Hash + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *wire.MsgTx, bool) (*chainhash.Hash, error)); ok { + return rf(ctx, tx, allowHighFees) + } + if rf, ok := ret.Get(0).(func(context.Context, *wire.MsgTx, bool) *chainhash.Hash); ok { + r0 = rf(ctx, tx, allowHighFees) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*chainhash.Hash) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *wire.MsgTx, bool) error); ok { + r1 = rf(ctx, tx, allowHighFees) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SignRawTransactionWithWallet2 provides a mock function with given fields: ctx, tx, inputs +func (_m *BitcoinClient) SignRawTransactionWithWallet2(ctx context.Context, tx *wire.MsgTx, inputs []btcjson.RawTxWitnessInput) (*wire.MsgTx, bool, error) { + ret := _m.Called(ctx, tx, inputs) + + if len(ret) == 0 { + panic("no return value specified for SignRawTransactionWithWallet2") + } + + var r0 *wire.MsgTx + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *wire.MsgTx, []btcjson.RawTxWitnessInput) (*wire.MsgTx, bool, error)); ok { + return rf(ctx, tx, inputs) + } + if rf, ok := ret.Get(0).(func(context.Context, *wire.MsgTx, []btcjson.RawTxWitnessInput) *wire.MsgTx); ok { + r0 = rf(ctx, tx, inputs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*wire.MsgTx) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *wire.MsgTx, []btcjson.RawTxWitnessInput) bool); ok { + r1 = rf(ctx, tx, inputs) + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func(context.Context, *wire.MsgTx, []btcjson.RawTxWitnessInput) error); ok { + r2 = rf(ctx, tx, inputs) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewBitcoinClient creates a new instance of BitcoinClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBitcoinClient(t interface { + mock.TestingT + Cleanup(func()) +}) *BitcoinClient { + mock := &BitcoinClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/zetaclient/testutils/mocks/btc_rpc.go b/zetaclient/testutils/mocks/btc_rpc.go deleted file mode 100644 index 487f4b0632..0000000000 --- a/zetaclient/testutils/mocks/btc_rpc.go +++ /dev/null @@ -1,548 +0,0 @@ -// Code generated by mockery v2.42.2. DO NOT EDIT. - -package mocks - -import ( - btcjson "github.com/btcsuite/btcd/btcjson" - btcutil "github.com/btcsuite/btcd/btcutil" - - chainhash "github.com/btcsuite/btcd/chaincfg/chainhash" - - mock "github.com/stretchr/testify/mock" - - rpcclient "github.com/btcsuite/btcd/rpcclient" - - wire "github.com/btcsuite/btcd/wire" -) - -// BTCRPCClient is an autogenerated mock type for the BTCRPCClient type -type BTCRPCClient struct { - mock.Mock -} - -// CreateWallet provides a mock function with given fields: name, opts -func (_m *BTCRPCClient) CreateWallet(name string, opts ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, name) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for CreateWallet") - } - - var r0 *btcjson.CreateWalletResult - var r1 error - if rf, ok := ret.Get(0).(func(string, ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error)); ok { - return rf(name, opts...) - } - if rf, ok := ret.Get(0).(func(string, ...rpcclient.CreateWalletOpt) *btcjson.CreateWalletResult); ok { - r0 = rf(name, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*btcjson.CreateWalletResult) - } - } - - if rf, ok := ret.Get(1).(func(string, ...rpcclient.CreateWalletOpt) error); ok { - r1 = rf(name, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// EstimateSmartFee provides a mock function with given fields: confTarget, mode -func (_m *BTCRPCClient) EstimateSmartFee(confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) { - ret := _m.Called(confTarget, mode) - - if len(ret) == 0 { - panic("no return value specified for EstimateSmartFee") - } - - var r0 *btcjson.EstimateSmartFeeResult - var r1 error - if rf, ok := ret.Get(0).(func(int64, *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error)); ok { - return rf(confTarget, mode) - } - if rf, ok := ret.Get(0).(func(int64, *btcjson.EstimateSmartFeeMode) *btcjson.EstimateSmartFeeResult); ok { - r0 = rf(confTarget, mode) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*btcjson.EstimateSmartFeeResult) - } - } - - if rf, ok := ret.Get(1).(func(int64, *btcjson.EstimateSmartFeeMode) error); ok { - r1 = rf(confTarget, mode) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GenerateToAddress provides a mock function with given fields: numBlocks, address, maxTries -func (_m *BTCRPCClient) GenerateToAddress(numBlocks int64, address btcutil.Address, maxTries *int64) ([]*chainhash.Hash, error) { - ret := _m.Called(numBlocks, address, maxTries) - - if len(ret) == 0 { - panic("no return value specified for GenerateToAddress") - } - - var r0 []*chainhash.Hash - var r1 error - if rf, ok := ret.Get(0).(func(int64, btcutil.Address, *int64) ([]*chainhash.Hash, error)); ok { - return rf(numBlocks, address, maxTries) - } - if rf, ok := ret.Get(0).(func(int64, btcutil.Address, *int64) []*chainhash.Hash); ok { - r0 = rf(numBlocks, address, maxTries) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*chainhash.Hash) - } - } - - if rf, ok := ret.Get(1).(func(int64, btcutil.Address, *int64) error); ok { - r1 = rf(numBlocks, address, maxTries) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetBalance provides a mock function with given fields: account -func (_m *BTCRPCClient) GetBalance(account string) (btcutil.Amount, error) { - ret := _m.Called(account) - - if len(ret) == 0 { - panic("no return value specified for GetBalance") - } - - var r0 btcutil.Amount - var r1 error - if rf, ok := ret.Get(0).(func(string) (btcutil.Amount, error)); ok { - return rf(account) - } - if rf, ok := ret.Get(0).(func(string) btcutil.Amount); ok { - r0 = rf(account) - } else { - r0 = ret.Get(0).(btcutil.Amount) - } - - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(account) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetBlockCount provides a mock function with given fields: -func (_m *BTCRPCClient) GetBlockCount() (int64, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetBlockCount") - } - - var r0 int64 - var r1 error - if rf, ok := ret.Get(0).(func() (int64, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() int64); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(int64) - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetBlockHash provides a mock function with given fields: blockHeight -func (_m *BTCRPCClient) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) { - ret := _m.Called(blockHeight) - - if len(ret) == 0 { - panic("no return value specified for GetBlockHash") - } - - var r0 *chainhash.Hash - var r1 error - if rf, ok := ret.Get(0).(func(int64) (*chainhash.Hash, error)); ok { - return rf(blockHeight) - } - if rf, ok := ret.Get(0).(func(int64) *chainhash.Hash); ok { - r0 = rf(blockHeight) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*chainhash.Hash) - } - } - - if rf, ok := ret.Get(1).(func(int64) error); ok { - r1 = rf(blockHeight) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetBlockHeader provides a mock function with given fields: blockHash -func (_m *BTCRPCClient) GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error) { - ret := _m.Called(blockHash) - - if len(ret) == 0 { - panic("no return value specified for GetBlockHeader") - } - - var r0 *wire.BlockHeader - var r1 error - if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*wire.BlockHeader, error)); ok { - return rf(blockHash) - } - if rf, ok := ret.Get(0).(func(*chainhash.Hash) *wire.BlockHeader); ok { - r0 = rf(blockHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*wire.BlockHeader) - } - } - - if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { - r1 = rf(blockHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetBlockVerbose provides a mock function with given fields: blockHash -func (_m *BTCRPCClient) GetBlockVerbose(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { - ret := _m.Called(blockHash) - - if len(ret) == 0 { - panic("no return value specified for GetBlockVerbose") - } - - var r0 *btcjson.GetBlockVerboseResult - var r1 error - if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*btcjson.GetBlockVerboseResult, error)); ok { - return rf(blockHash) - } - if rf, ok := ret.Get(0).(func(*chainhash.Hash) *btcjson.GetBlockVerboseResult); ok { - r0 = rf(blockHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*btcjson.GetBlockVerboseResult) - } - } - - if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { - r1 = rf(blockHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetBlockVerboseTx provides a mock function with given fields: blockHash -func (_m *BTCRPCClient) GetBlockVerboseTx(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) { - ret := _m.Called(blockHash) - - if len(ret) == 0 { - panic("no return value specified for GetBlockVerboseTx") - } - - var r0 *btcjson.GetBlockVerboseTxResult - var r1 error - if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error)); ok { - return rf(blockHash) - } - if rf, ok := ret.Get(0).(func(*chainhash.Hash) *btcjson.GetBlockVerboseTxResult); ok { - r0 = rf(blockHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*btcjson.GetBlockVerboseTxResult) - } - } - - if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { - r1 = rf(blockHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetNetworkInfo provides a mock function with given fields: -func (_m *BTCRPCClient) GetNetworkInfo() (*btcjson.GetNetworkInfoResult, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetNetworkInfo") - } - - var r0 *btcjson.GetNetworkInfoResult - var r1 error - if rf, ok := ret.Get(0).(func() (*btcjson.GetNetworkInfoResult, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() *btcjson.GetNetworkInfoResult); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*btcjson.GetNetworkInfoResult) - } - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetNewAddress provides a mock function with given fields: account -func (_m *BTCRPCClient) GetNewAddress(account string) (btcutil.Address, error) { - ret := _m.Called(account) - - if len(ret) == 0 { - panic("no return value specified for GetNewAddress") - } - - var r0 btcutil.Address - var r1 error - if rf, ok := ret.Get(0).(func(string) (btcutil.Address, error)); ok { - return rf(account) - } - if rf, ok := ret.Get(0).(func(string) btcutil.Address); ok { - r0 = rf(account) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(btcutil.Address) - } - } - - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(account) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetRawTransaction provides a mock function with given fields: txHash -func (_m *BTCRPCClient) GetRawTransaction(txHash *chainhash.Hash) (*btcutil.Tx, error) { - ret := _m.Called(txHash) - - if len(ret) == 0 { - panic("no return value specified for GetRawTransaction") - } - - var r0 *btcutil.Tx - var r1 error - if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*btcutil.Tx, error)); ok { - return rf(txHash) - } - if rf, ok := ret.Get(0).(func(*chainhash.Hash) *btcutil.Tx); ok { - r0 = rf(txHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*btcutil.Tx) - } - } - - if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { - r1 = rf(txHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetRawTransactionVerbose provides a mock function with given fields: txHash -func (_m *BTCRPCClient) GetRawTransactionVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) { - ret := _m.Called(txHash) - - if len(ret) == 0 { - panic("no return value specified for GetRawTransactionVerbose") - } - - var r0 *btcjson.TxRawResult - var r1 error - if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*btcjson.TxRawResult, error)); ok { - return rf(txHash) - } - if rf, ok := ret.Get(0).(func(*chainhash.Hash) *btcjson.TxRawResult); ok { - r0 = rf(txHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*btcjson.TxRawResult) - } - } - - if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { - r1 = rf(txHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetTransaction provides a mock function with given fields: txHash -func (_m *BTCRPCClient) GetTransaction(txHash *chainhash.Hash) (*btcjson.GetTransactionResult, error) { - ret := _m.Called(txHash) - - if len(ret) == 0 { - panic("no return value specified for GetTransaction") - } - - var r0 *btcjson.GetTransactionResult - var r1 error - if rf, ok := ret.Get(0).(func(*chainhash.Hash) (*btcjson.GetTransactionResult, error)); ok { - return rf(txHash) - } - if rf, ok := ret.Get(0).(func(*chainhash.Hash) *btcjson.GetTransactionResult); ok { - r0 = rf(txHash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*btcjson.GetTransactionResult) - } - } - - if rf, ok := ret.Get(1).(func(*chainhash.Hash) error); ok { - r1 = rf(txHash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ListUnspent provides a mock function with given fields: -func (_m *BTCRPCClient) ListUnspent() ([]btcjson.ListUnspentResult, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for ListUnspent") - } - - var r0 []btcjson.ListUnspentResult - var r1 error - if rf, ok := ret.Get(0).(func() ([]btcjson.ListUnspentResult, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() []btcjson.ListUnspentResult); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]btcjson.ListUnspentResult) - } - } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ListUnspentMinMaxAddresses provides a mock function with given fields: minConf, maxConf, addrs -func (_m *BTCRPCClient) ListUnspentMinMaxAddresses(minConf int, maxConf int, addrs []btcutil.Address) ([]btcjson.ListUnspentResult, error) { - ret := _m.Called(minConf, maxConf, addrs) - - if len(ret) == 0 { - panic("no return value specified for ListUnspentMinMaxAddresses") - } - - var r0 []btcjson.ListUnspentResult - var r1 error - if rf, ok := ret.Get(0).(func(int, int, []btcutil.Address) ([]btcjson.ListUnspentResult, error)); ok { - return rf(minConf, maxConf, addrs) - } - if rf, ok := ret.Get(0).(func(int, int, []btcutil.Address) []btcjson.ListUnspentResult); ok { - r0 = rf(minConf, maxConf, addrs) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]btcjson.ListUnspentResult) - } - } - - if rf, ok := ret.Get(1).(func(int, int, []btcutil.Address) error); ok { - r1 = rf(minConf, maxConf, addrs) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SendRawTransaction provides a mock function with given fields: tx, allowHighFees -func (_m *BTCRPCClient) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { - ret := _m.Called(tx, allowHighFees) - - if len(ret) == 0 { - panic("no return value specified for SendRawTransaction") - } - - var r0 *chainhash.Hash - var r1 error - if rf, ok := ret.Get(0).(func(*wire.MsgTx, bool) (*chainhash.Hash, error)); ok { - return rf(tx, allowHighFees) - } - if rf, ok := ret.Get(0).(func(*wire.MsgTx, bool) *chainhash.Hash); ok { - r0 = rf(tx, allowHighFees) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*chainhash.Hash) - } - } - - if rf, ok := ret.Get(1).(func(*wire.MsgTx, bool) error); ok { - r1 = rf(tx, allowHighFees) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewBTCRPCClient creates a new instance of BTCRPCClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewBTCRPCClient(t interface { - mock.TestingT - Cleanup(func()) -}) *BTCRPCClient { - mock := &BTCRPCClient{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/zetaclient/testutils/testrpc/rpc_btc.go b/zetaclient/testutils/testrpc/rpc_btc.go index 57f184a60d..dc570968e9 100644 --- a/zetaclient/testutils/testrpc/rpc_btc.go +++ b/zetaclient/testutils/testrpc/rpc_btc.go @@ -8,9 +8,9 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/config" "github.com/zeta-chain/node/zetaclient/testutils" "github.com/zeta-chain/node/zetaclient/testutils/mocks" @@ -32,7 +32,7 @@ func NewBtcServer(t *testing.T) (*BtcServer, config.BTCConfig) { RPCUsername: "btc-user", RPCPassword: "btc-password", RPCHost: host, - RPCParams: "", + RPCParams: "mainnet", } rpc.On("ping", func(_ []any) (any, error) { @@ -58,9 +58,9 @@ func formatBitcoinRPCHost(serverURL string) (string, error) { } // CreateBTCRPCAndLoadTx is a helper function to load raw txs and feed them to mock rpc client -func CreateBTCRPCAndLoadTx(t *testing.T, dir string, chainID int64, txHashes ...string) interfaces.BTCRPCClient { +func CreateBTCRPCAndLoadTx(t *testing.T, dir string, chainID int64, txHashes ...string) *mocks.BitcoinClient { // create mock rpc client - rpcClient := mocks.NewBTCRPCClient(t) + rpcClient := mocks.NewBitcoinClient(t) // feed txs to mock rpc client for _, txHash := range txHashes { @@ -73,7 +73,7 @@ func CreateBTCRPCAndLoadTx(t *testing.T, dir string, chainID int64, txHashes ... // mock rpc response tx := btcutil.NewTx(&msgTx) - rpcClient.On("GetRawTransaction", tx.Hash()).Return(tx, nil) + rpcClient.On("GetRawTransaction", mock.Anything, tx.Hash()).Return(tx, nil) } return rpcClient