diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 31444cf5e7..647e2520e1 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -3,7 +3,6 @@ package main import ( "fmt" - "github.com/btcsuite/btcd/rpcclient" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" @@ -11,6 +10,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/authz" "github.com/zeta-chain/zetacore/zetaclient/chains/base" btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" + btcrpc "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" btcsigner "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/signer" evmobserver "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" evmsigner "github.com/zeta-chain/zetacore/zetaclient/chains/evm/signer" @@ -168,41 +168,27 @@ func CreateChainObserverMap( // create BTC chain observer btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() if enabled { - // create BTC client - connCfg := &rpcclient.ConnConfig{ - Host: btcConfig.RPCHost, - User: btcConfig.RPCUsername, - Pass: btcConfig.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: btcConfig.RPCParams, - } - btcClient, err := rpcclient.New(connCfg, nil) - if err != nil { - return nil, fmt.Errorf("error creating rpc client: %s", err) - } - err = btcClient.Ping() + btcClient, err := btcrpc.NewRPCClient(btcConfig) if err != nil { - return nil, fmt.Errorf("error ping the bitcoin server: %s", err) - } - - // create BTC chain observer - co, err := btcobserver.NewObserver( - btcChain, - btcClient, - *chainParams, - zetacoreContext, - zetacoreClient, - tss, - dbpath, - logger, - ts, - ) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) - + logger.Std.Error().Err(err).Msgf("error creating rpc client for bitcoin chain %s", btcChain.String()) } else { - observerMap[btcChain.ChainId] = co + // create BTC chain observer + co, err := btcobserver.NewObserver( + btcChain, + btcClient, + *chainParams, + zetacoreContext, + zetacoreClient, + tss, + dbpath, + logger, + ts, + ) + if err != nil { + logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) + } else { + observerMap[btcChain.ChainId] = co + } } } diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 0cf1766895..b1a8e2b671 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -531,79 +531,6 @@ func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { ob.logger.Outbound.Info().Msgf("SaveBroadcastedTx: saved broadcasted txHash %s for outbound %s", txHash, outboundID) } -// 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 -} - -// 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) -} - // GetBlockByNumberCached gets cached block (and header) by block number func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, error) { if result, ok := ob.BlockCache().Get(blockNumber); ok { diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index 6e80e1ea5c..bddbee223a 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -13,6 +13,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/context" @@ -163,7 +164,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } // Get outbound block height - blockHeight, err := GetBlockHeightByHash(ob.btcClient, res.BlockHash) + blockHeight, err := rpc.GetBlockHeightByHash(ob.btcClient, res.BlockHash) if err != nil { return true, false, errors.Wrapf( err, @@ -346,7 +347,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return "", fmt.Errorf("getOutboundIDByNonce: cannot find outbound txid for nonce %d", nonce) } // make sure it's a real Bitcoin txid - _, getTxResult, err := GetTxResultByHash(ob.btcClient, txid) + _, getTxResult, err := rpc.GetTxResultByHash(ob.btcClient, txid) if err != nil { return "", errors.Wrapf( err, @@ -387,7 +388,7 @@ func (ob *Observer) checkIncludedTx( txHash string, ) (*btcjson.GetTransactionResult, bool) { outboundID := ob.GetTxID(cctx.GetCurrentOutboundParam().TssNonce) - hash, getTxResult, err := GetTxResultByHash(ob.btcClient, txHash) + hash, getTxResult, err := rpc.GetTxResultByHash(ob.btcClient, txHash) if err != nil { ob.logger.Outbound.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) return nil, false @@ -471,7 +472,7 @@ func (ob *Observer) checkTssOutboundResult( ) error { params := cctx.GetCurrentOutboundParam() nonce := params.TssNonce - rawResult, err := GetRawTxResult(ob.btcClient, hash, res) + rawResult, err := rpc.GetRawTxResult(ob.btcClient, hash, res) if err != nil { return errors.Wrapf(err, "checkTssOutboundResult: error GetRawTxResultByHash %s", hash.String()) } diff --git a/zetaclient/chains/bitcoin/rpc/rpc.go b/zetaclient/chains/bitcoin/rpc/rpc.go new file mode 100644 index 0000000000..32144d77f2 --- /dev/null +++ b/zetaclient/chains/bitcoin/rpc/rpc.go @@ -0,0 +1,109 @@ +package rpc + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" + "github.com/pkg/errors" + + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/config" +) + +// 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 +} + +// 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) +} diff --git a/zetaclient/chains/bitcoin/observer/live_test.go b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go similarity index 95% rename from zetaclient/chains/bitcoin/observer/live_test.go rename to zetaclient/chains/bitcoin/rpc/rpc_live_test.go index c5512a1fae..26355ad837 100644 --- a/zetaclient/chains/bitcoin/observer/live_test.go +++ b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go @@ -1,4 +1,4 @@ -package observer_test +package rpc_test import ( "context" @@ -24,6 +24,8 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" + "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -207,26 +209,11 @@ func (suite *BitcoinObserverTestSuite) Test2() { suite.Require().Equal(0, len(inbounds)) } -func (suite *BitcoinObserverTestSuite) Test3() { - client := suite.rpcClient - res, err := client.EstimateSmartFee(1, &btcjson.EstimateModeConservative) - suite.Require().NoError(err) - suite.T().Logf("fee: %f", *res.FeeRate) - suite.T().Logf("blocks: %d", res.Blocks) - suite.T().Logf("errors: %s", res.Errors) - gasPrice := big.NewFloat(0) - gasPriceU64, _ := gasPrice.Mul(big.NewFloat(*res.FeeRate), big.NewFloat(1e8)).Uint64() - suite.T().Logf("gas price: %d", gasPriceU64) - - bn, err := client.GetBlockCount() - suite.Require().NoError(err) - suite.T().Logf("block number %d", bn) -} - // TestBitcoinObserverLive is a phony test to run each live test individually func TestBitcoinObserverLive(t *testing.T) { // suite.Run(t, new(BitcoinClientTestSuite)) + // LiveTestNewRPCClient(t) // LiveTestGetBlockHeightByHash(t) // LiveTestBitcoinFeeRate(t) // LiveTestAvgFeeRateMainnetMempoolSpace(t) @@ -234,6 +221,25 @@ func TestBitcoinObserverLive(t *testing.T) { // LiveTestGetSenderByVin(t) } +// LiveTestNewRPCClient creates a new Bitcoin RPC client +func LiveTestNewRPCClient(t *testing.T) { + btcConfig := config.BTCConfig{ + RPCUsername: "user", + RPCPassword: "pass", + RPCHost: "bitcoin.rpc.zetachain.com/6315704c-49bc-4649-8b9d-e9278a1dfeb3", + RPCParams: "mainnet", + } + + // 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)) +} + // LiveTestGetBlockHeightByHash queries Bitcoin block height by hash func LiveTestGetBlockHeightByHash(t *testing.T) { // setup Bitcoin client @@ -246,11 +252,11 @@ func LiveTestGetBlockHeightByHash(t *testing.T) { invalidHash := "invalidhash" // get block by invalid hash - _, err = observer.GetBlockHeightByHash(client, invalidHash) + _, err = rpc.GetBlockHeightByHash(client, invalidHash) require.ErrorContains(t, err, "error decoding block hash") // get block height by block hash - height, err := observer.GetBlockHeightByHash(client, hash) + height, err := rpc.GetBlockHeightByHash(client, hash) require.NoError(t, err) require.Equal(t, expectedHeight, height) }