diff --git a/Makefile b/Makefile index bc4e8ccda2..f5f3813732 100644 --- a/Makefile +++ b/Makefile @@ -312,7 +312,7 @@ test-all: test-unit test-race PACKAGES_UNIT=$(shell go list ./... | grep -Ev 'vendor|importer') TEST_PACKAGES=./... TEST_TARGETS := test-unit test-unit-cover test-race -SKIP_TEST_METHOD=TestNewInscriptionTool +SKIP_TEST_METHOD='(TestNewInscriptionTool|^TestLocal)' # Test runs-specific rules. To add a new test target, just add # a new rule, customise ARGS or TEST_PACKAGES ad libitum, and diff --git a/bitcoin/bridge.go b/bitcoin/bridge.go index 542c153a6e..11bffa7b10 100644 --- a/bitcoin/bridge.go +++ b/bitcoin/bridge.go @@ -18,6 +18,8 @@ import ( "github.com/ethereum/go-ethereum/ethclient" ) +// Bridge bridge +// TODO: only L1 -> L2, More calls may be supported later type Bridge struct { EthRPCURL string EthPrivKey *ecdsa.PrivateKey @@ -53,46 +55,52 @@ func NewBridge(bridgeCfg BridgeConfig, abiFileDir string) (*Bridge, error) { } // Deposit to ethereum -// TODO: partition method and add test func (b *Bridge) Deposit(bitcoinAddress string, amount int64) error { if bitcoinAddress == "" { return fmt.Errorf("bitcoin address is empty") } ctx := context.Background() - // dail ethereum rpc - client, err := ethclient.Dial(b.EthRPCURL) + + toAddress, err := b.BitcoinAddressToEthAddress(bitcoinAddress) if err != nil { return err } - publicKey := b.EthPrivKey.Public() - publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) - if !ok { - return fmt.Errorf("error casting public key to ECDSA") - } - nonce, err := client.PendingNonceAt(ctx, crypto.PubkeyToAddress(*publicKeyECDSA)) + data, err := b.ABIPack(b.ABI, "deposit", common.HexToAddress(toAddress), new(big.Int).SetInt64(amount)) if err != nil { return err } - gasPrice, err := client.SuggestGasPrice(ctx) + + receipt, err := b.ethContractCall(ctx, b.EthPrivKey, data) if err != nil { return err } - contractAbi, err := abi.JSON(bytes.NewReader([]byte(b.ABI))) - if err != nil { - return err + if receipt.Status != 1 { + return fmt.Errorf("tx failed, receipt:%v", receipt) } + return nil +} - toAddress, err := b.BitcoinAddressToEthAddress(bitcoinAddress) +func (b *Bridge) ethContractCall(ctx context.Context, priv *ecdsa.PrivateKey, data []byte) (*types.Receipt, error) { + client, err := ethclient.Dial(b.EthRPCURL) if err != nil { - return err + return nil, err } - data, err := contractAbi.Pack("deposit", common.HexToAddress(toAddress), new(big.Int).SetInt64(amount)) + publicKey := priv.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("error casting public key to ECDSA") + } + nonce, err := client.PendingNonceAt(ctx, crypto.PubkeyToAddress(*publicKeyECDSA)) if err != nil { - return err + return nil, err + } + gasPrice, err := client.SuggestGasPrice(ctx) + if err != nil { + return nil, err } tx := types.NewTx(&types.LegacyTx{ @@ -106,31 +114,31 @@ func (b *Bridge) Deposit(bitcoinAddress string, amount int64) error { chainID, err := client.ChainID(ctx) if err != nil { - return err + return nil, err } // sign tx - signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), b.EthPrivKey) + signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), priv) if err != nil { - return err + return nil, err } // send tx err = client.SendTransaction(ctx, signedTx) if err != nil { - return err + return nil, err } // wait tx confirm - receipt, err := bind.WaitMined(ctx, client, signedTx) - if err != nil { - return err - } + return bind.WaitMined(ctx, client, signedTx) +} - if receipt.Status != 1 { - return fmt.Errorf("tx failed, receipt:%v", receipt) +// ABIPack the given method name to conform the ABI. Method call's data +func (b *Bridge) ABIPack(abiData string, method string, args ...interface{}) ([]byte, error) { + contractAbi, err := abi.JSON(bytes.NewReader([]byte(abiData))) + if err != nil { + return nil, err } - - return nil + return contractAbi.Pack(method, args...) } // BitcoinAddressToEthAddress bitcoin address to eth address diff --git a/bitcoin/bridge_test.go b/bitcoin/bridge_test.go index 5a1b91472e..7f87cc1975 100644 --- a/bitcoin/bridge_test.go +++ b/bitcoin/bridge_test.go @@ -1,6 +1,8 @@ package bitcoin_test import ( + "errors" + "math/big" "os" "path" "testing" @@ -9,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/evmos/ethermint/bitcoin" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewBridge(t *testing.T) { @@ -41,3 +44,84 @@ func TestNewBridge(t *testing.T) { assert.Equal(t, string(abi), bridge.ABI) assert.Equal(t, bridgeCfg.GasLimit, bridge.GasLimit) } + +// TestLocalDeposit only test in local +func TestLocalDeposit(t *testing.T) { + bridge := bridgeWithConfig(t) + testCase := []struct { + name string + args []interface{} + err error + }{ + { + name: "success", + args: []interface{}{"tb1qjda2l5spwyv4ekwe9keddymzuxynea2m2kj0qy", int64(1234)}, + err: nil, + }, + { + name: "fail: address empty", + args: []interface{}{"", int64(1234)}, + err: errors.New("bitcoin address is empty"), + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + err := bridge.Deposit(tc.args[0].(string), tc.args[1].(int64)) + if err != nil { + assert.Equal(t, tc.err, err) + } + }) + } +} + +func TestABIPack(t *testing.T) { + t.Run("success", func(t *testing.T) { + abiData, err := os.ReadFile(path.Join("./testdata", "abi.json")) + if err != nil { + t.Fatal(err) + } + expectedMethod := "deposit" + expectedArgs := []interface{}{common.HexToAddress("0x12345678"), new(big.Int).SetInt64(1111)} + expectedResult := []byte{71, 231, 239, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 52, 86, 120, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 4, 87} + + // Create a mock bridge object + b := &bitcoin.Bridge{} + + // Call the ABIPack method + result, err := b.ABIPack(string(abiData), expectedMethod, expectedArgs...) + + // Check for errors + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Compare the result with the expected result + require.Equal(t, result, expectedResult) + }) + + t.Run("Invalid ABI data", func(t *testing.T) { + abiData := `{"inputs": [{"type": "address", "name": "to"}, {"type": "uint256", "name": "value"}` + expectedError := errors.New("unexpected EOF") + + // Create a mock bridge object + b := &bitcoin.Bridge{} + + // Call the ABIPack method + _, err := b.ABIPack(abiData, "method", "arg1", "arg2") + + require.EqualError(t, err, expectedError.Error()) + }) +} + +func bridgeWithConfig(t *testing.T) *bitcoin.Bridge { + config, err := bitcoin.LoadBitcoinConfig("./testdata") + require.NoError(t, err) + + bridge, err := bitcoin.NewBridge(config.Bridge, "./testdata") + require.NoError(t, err) + return bridge +} diff --git a/bitcoin/config_test.go b/bitcoin/config_test.go index 95b0321372..be6ee99b3c 100644 --- a/bitcoin/config_test.go +++ b/bitcoin/config_test.go @@ -10,6 +10,22 @@ import ( ) func TestConfig(t *testing.T) { + // clean BITCOIN env set + // This is because the value set by the environment variable affects viper reading file + os.Unsetenv("BITCOIN_NETWORK_NAME") + os.Unsetenv("BITCOIN_RPC_HOST") + os.Unsetenv("BITCOIN_RPC_PORT") + os.Unsetenv("BITCOIN_RPC_USER") + os.Unsetenv("BITCOIN_RPC_PASS") + os.Unsetenv("BITCOIN_WALLET_NAME") + os.Unsetenv("BITCOIN_DESTINATION") + os.Unsetenv("BITCOIN_ENABLE_INDEXER") + os.Unsetenv("BITCOIN_INDEXER_LISTEN_ADDRESS") + os.Unsetenv("BITCOIN_BRIDGE_ETH_RPC_URL") + os.Unsetenv("BITCOIN_BRIDGE_CONTRACT_ADDRESS") + os.Unsetenv("BITCOIN_BRIDGE_ETH_PRIV_KEY") + os.Unsetenv("BITCOIN_BRIDGE_ABI") + os.Unsetenv("BITCOIN_BRIDGE_GAS_LIMIT") config, err := bitcoin.LoadBitcoinConfig("./testdata") require.NoError(t, err) require.Equal(t, "signet", config.NetworkName) diff --git a/bitcoin/indexer_test.go b/bitcoin/indexer_test.go index a5cbb9e0a0..5943d1e0aa 100644 --- a/bitcoin/indexer_test.go +++ b/bitcoin/indexer_test.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/rpcclient" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/evmos/ethermint/bitcoin" + "github.com/evmos/ethermint/types" "github.com/stretchr/testify/require" ) @@ -128,6 +129,54 @@ func TestParseAddress(t *testing.T) { } } +// TestLocalParseTx only test in local +// data source: testnet network +func TestLocalParseTx(t *testing.T) { + indexer := bitcoinIndexerWithConfig(t) + testCases := []struct { + name string + height int64 + dest []*types.BitcoinTxParseResult + }{ + { + name: "success", + height: 2540186, + dest: []*types.BitcoinTxParseResult{ + { + From: []string{"tb1qravmtnqvtpnmugeg7q90ck69lzznflu4w9amnw"}, + To: "tb1qjda2l5spwyv4ekwe9keddymzuxynea2m2kj0qy", + Value: 2306, + }, + }, + }, + { + name: "success empty", + height: 2540180, + dest: []*types.BitcoinTxParseResult{}, + }, + } + + for _, tc := range testCases { + results, err := indexer.ParseBlock(tc.height) + require.NoError(t, err) + require.Equal(t, results, tc.dest) + } +} + +// TestLocalLatestBlock only test in local +func TestLocalLatestBlock(t *testing.T) { + indexer := bitcoinIndexerWithConfig(t) + _, err := indexer.LatestBlock() + require.NoError(t, err) +} + +// TestLocalBlockChainInfo only test in local +func TestLocalBlockChainInfo(t *testing.T) { + indexer := bitcoinIndexerWithConfig(t) + _, err := indexer.BlockChainInfo() + require.NoError(t, err) +} + func mockRpcClient(t *testing.T) *rpcclient.Client { connCfg := &rpcclient.ConnConfig{ Host: "127.0.0.1:38332", @@ -148,3 +197,23 @@ func mockBitcoinIndexer(t *testing.T, chainParams *chaincfg.Params) *bitcoin.Ind require.NoError(t, err) return indexer } + +func bitcoinIndexerWithConfig(t *testing.T) *bitcoin.Indexer { + config, err := bitcoin.LoadBitcoinConfig("./testdata") + require.NoError(t, err) + connCfg := &rpcclient.ConnConfig{ + Host: config.RPCHost + ":" + config.RPCPort, + User: config.RPCUser, + Pass: config.RPCPass, + HTTPPostMode: true, + DisableTLS: true, + } + client, err := rpcclient.New(connCfg, nil) + require.NoError(t, err) + bitcoinParam := bitcoin.ChainParams(config.NetworkName) + indexer, err := bitcoin.NewBitcoinIndexer(client, + bitcoinParam, + config.IndexerListenAddress) + require.NoError(t, err) + return indexer +}