diff --git a/backend/pkg/commons/rpc/erigon.go b/backend/pkg/commons/rpc/erigon.go index 7abbfd24d..ae7e4f952 100644 --- a/backend/pkg/commons/rpc/erigon.go +++ b/backend/pkg/commons/rpc/erigon.go @@ -142,27 +142,10 @@ func (client *ErigonClient) GetBlock(number int64, traceMode string) (*types.Eth if err := g.Wait(); err != nil { return nil, nil, err } - // we cannot trust block.Hash(), some chain (gnosis) have extra field that are included in the hash computation - // so extract it from the receipts or from the node again if no receipt (it should be very rare) - var blockHash common.Hash - if len(receipts) != 0 { - blockHash = receipts[0].BlockHash - } else { - var res minimalBlock - if err := client.rpcClient.CallContext(ctx, &res, "eth_getBlockByNumber", fmt.Sprintf("0x%x", number), false); err != nil { - return nil, nil, fmt.Errorf("error retrieving blockHash %v: %w", number, err) - } - blockHash = common.HexToHash(res.Hash) - } - withdrawals := make([]*types.Eth1Withdrawal, len(block.Withdrawals())) - for i, withdrawal := range block.Withdrawals() { - withdrawals[i] = &types.Eth1Withdrawal{ - Index: withdrawal.Index, - ValidatorIndex: withdrawal.Validator, - Address: withdrawal.Address.Bytes(), - Amount: new(big.Int).SetUint64(withdrawal.Amount).Bytes(), - } + blockHash, err := client.getBlockHash(number, receipts) + if err != nil { + return nil, nil, err } transactions := make([]*types.Eth1Transaction, len(block.Transactions())) @@ -171,25 +154,6 @@ func (client *ErigonClient) GetBlock(number int64, traceMode string) (*types.Eth return nil, nil, fmt.Errorf("block %s receipts length [%d] mismatch with transactions length [%d]", block.Number(), len(receipts), len(block.Transactions())) } for txPosition, receipt := range receipts { - logs := make([]*types.Eth1Log, len(receipt.Logs)) - for i, log := range receipt.Logs { - topics := make([][]byte, len(log.Topics)) - for j, topic := range log.Topics { - topics[j] = topic.Bytes() - } - logs[i] = &types.Eth1Log{ - Address: log.Address.Bytes(), - Data: log.Data, - Removed: log.Removed, - Topics: topics, - } - } - - var internals []*types.Eth1InternalTransaction - for ; traceIndex < len(traces) && traces[traceIndex].txPosition == txPosition; traceIndex++ { - internals = append(internals, &traces[traceIndex].Eth1InternalTransaction) - } - tx := block.Transactions()[txPosition] transactions[txPosition] = &types.Eth1Transaction{ Type: uint32(tx.Type()), @@ -200,114 +164,47 @@ func (client *ErigonClient) GetBlock(number int64, traceMode string) (*types.Eth Gas: tx.Gas(), Value: tx.Value().Bytes(), Data: tx.Data(), - To: func() []byte { - if tx.To() != nil { - return tx.To().Bytes() - } - return nil - }(), - From: func() []byte { - // this won't make a request in most cases as the sender is already present in the cache - // context https://github.com/ethereum/go-ethereum/blob/v1.14.11/ethclient/ethclient.go#L268 - sender, err := client.ethClient.TransactionSender(context.Background(), tx, blockHash, uint(txPosition)) - if err != nil { - sender = common.HexToAddress("abababababababababababababababababababab") - log.Error(err, "error converting tx to msg", 0, map[string]interface{}{"tx": tx.Hash()}) - } - return sender.Bytes() - }(), - ChainId: tx.ChainId().Bytes(), - AccessList: []*types.AccessList{}, - Hash: tx.Hash().Bytes(), - ContractAddress: receipt.ContractAddress[:], - CommulativeGasUsed: receipt.CumulativeGasUsed, - GasUsed: receipt.GasUsed, - LogsBloom: receipt.Bloom[:], - Status: receipt.Status, - Logs: logs, - Itx: internals, - MaxFeePerBlobGas: func() []byte { - if tx.BlobGasFeeCap() != nil { - return tx.BlobGasFeeCap().Bytes() - } - return nil - }(), - BlobVersionedHashes: func() (b [][]byte) { - for _, h := range tx.BlobHashes() { - b = append(b, h.Bytes()) - } - return b - }(), - BlobGasPrice: func() []byte { - if receipt.BlobGasPrice != nil { - return receipt.BlobGasPrice.Bytes() - } - return nil - }(), - BlobGasUsed: receipt.BlobGasUsed, - } - } - - uncles := make([]*types.Eth1Block, len(block.Uncles())) - for i, uncle := range block.Uncles() { - uncles[i] = &types.Eth1Block{ - Hash: uncle.Hash().Bytes(), - ParentHash: uncle.ParentHash.Bytes(), - UncleHash: uncle.UncleHash.Bytes(), - Coinbase: uncle.Coinbase.Bytes(), - Root: uncle.Root.Bytes(), - TxHash: uncle.TxHash.Bytes(), - ReceiptHash: uncle.ReceiptHash.Bytes(), - Difficulty: uncle.Difficulty.Bytes(), - Number: uncle.Number.Uint64(), - GasLimit: uncle.GasLimit, - GasUsed: uncle.GasUsed, - Time: timestamppb.New(time.Unix(int64(uncle.Time), 0)), - Extra: uncle.Extra, - MixDigest: uncle.MixDigest.Bytes(), - Bloom: uncle.Bloom.Bytes(), + To: getReceiver(tx), + From: client.getSender(tx, blockHash, txPosition), + ChainId: tx.ChainId().Bytes(), + AccessList: []*types.AccessList{}, + Hash: tx.Hash().Bytes(), + ContractAddress: receipt.ContractAddress[:], + CommulativeGasUsed: receipt.CumulativeGasUsed, + GasUsed: receipt.GasUsed, + LogsBloom: receipt.Bloom[:], + Status: receipt.Status, + Logs: getLogsFromReceipts(receipt.Logs), + Itx: getInternalTxs(traceIndex, traces, txPosition), + MaxFeePerBlobGas: getMaxFeePerBlobGas(tx), + BlobVersionedHashes: getBlobVersionedHashes(tx), + BlobGasPrice: getBlobGasPrice(receipt), + BlobGasUsed: receipt.BlobGasUsed, } } return &types.Eth1Block{ - Hash: blockHash.Bytes(), - ParentHash: block.ParentHash().Bytes(), - UncleHash: block.UncleHash().Bytes(), - Coinbase: block.Coinbase().Bytes(), - Root: block.Root().Bytes(), - TxHash: block.TxHash().Bytes(), - ReceiptHash: block.ReceiptHash().Bytes(), - Difficulty: block.Difficulty().Bytes(), - Number: block.NumberU64(), - GasLimit: block.GasLimit(), - GasUsed: block.GasUsed(), - Time: timestamppb.New(time.Unix(int64(block.Time()), 0)), - Extra: block.Extra(), - MixDigest: block.MixDigest().Bytes(), - Bloom: block.Bloom().Bytes(), - BaseFee: func() []byte { - if block.BaseFee() != nil { - return block.BaseFee().Bytes() - } - return nil - }(), - Uncles: uncles, - Transactions: transactions, - Withdrawals: withdrawals, - BlobGasUsed: func() uint64 { - blobGasUsed := block.BlobGasUsed() - if blobGasUsed != nil { - return *blobGasUsed - } - return 0 - }(), - ExcessBlobGas: func() uint64 { - excessBlobGas := block.ExcessBlobGas() - if excessBlobGas != nil { - return *excessBlobGas - } - return 0 - }(), + Hash: blockHash.Bytes(), + ParentHash: block.ParentHash().Bytes(), + UncleHash: block.UncleHash().Bytes(), + Coinbase: block.Coinbase().Bytes(), + Root: block.Root().Bytes(), + TxHash: block.TxHash().Bytes(), + ReceiptHash: block.ReceiptHash().Bytes(), + Difficulty: block.Difficulty().Bytes(), + Number: block.NumberU64(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + Time: timestamppb.New(time.Unix(int64(block.Time()), 0)), + Extra: block.Extra(), + MixDigest: block.MixDigest().Bytes(), + Bloom: block.Bloom().Bytes(), + BaseFee: getBaseFee(block), + Uncles: getBlockUncles(block.Uncles()), + Transactions: transactions, + Withdrawals: getBlockWithdrawals(block.Withdrawals()), + BlobGasUsed: getBlobGasUsed(block), + ExcessBlobGas: getExcessBlobGas(block), }, timings, nil } @@ -458,8 +355,6 @@ func (client *ErigonClient) GetBalances(pairs []*types.Eth1AddressBalance, addre Token: pair.Token, } - // log.LogInfo("retrieving balance for %x / %x", ret[i].Address, ret[i].Token) - if len(pair.Token) < 20 { batchElements = append(batchElements, gethrpc.BatchElem{ Method: "eth_getBalance", @@ -494,35 +389,25 @@ func (client *ErigonClient) GetBalances(pairs []*types.Eth1AddressBalance, addre res := strings.TrimPrefix(*el.Result.(*string), "0x") ret[i].Balance = new(big.Int).SetBytes(common.FromHex(res)).Bytes() - - // log.LogInfo("retrieved balance %x / %x: %x (%v)", ret[i].Address, ret[i].Token, ret[i].Balance, *el.Result.(*string)) } return ret, nil } -func (client *ErigonClient) GetBalancesForAddresse(address string, tokenStr []string) ([]*types.Eth1AddressBalance, error) { +func (client *ErigonClient) GetBalancesForAddress(address string, tokenStr []string) ([]*types.Eth1AddressBalance, error) { opts := &bind.CallOpts{ BlockNumber: nil, } - tokens := make([]common.Address, 0, len(tokenStr)) - - for _, token := range tokenStr { - tokens = append(tokens, common.HexToAddress(token)) - } + tokens := getTokens(tokenStr) balancesInt, err := client.multiChecker.Balances(opts, []common.Address{common.HexToAddress(address)}, tokens) if err != nil { return nil, err } - res := make([]*types.Eth1AddressBalance, len(tokenStr)) - for tokenIdx := range tokens { - res[tokenIdx] = &types.Eth1AddressBalance{ - Address: common.FromHex(address), - Token: common.FromHex(string(tokens[tokenIdx].Bytes())), - Balance: balancesInt[tokenIdx].Bytes(), - } + res, err := parseAddressBalance(tokens, address, balancesInt) + if err != nil { + return nil, err } return res, nil @@ -575,45 +460,19 @@ func (client *ErigonClient) GetERC20TokenMetadata(token []byte) (*types.ERC20Met ret := &types.ERC20Metadata{} g.Go(func() error { - symbol, err := contract.Symbol(nil) - if err != nil { - if strings.Contains(err.Error(), "abi") { - ret.Symbol = "UNKNOWN" - return nil - } - - return fmt.Errorf("error retrieving symbol: %w", err) - } - - ret.Symbol = symbol - return nil + return getERC20ContractSymbol(contract, ret) }) g.Go(func() error { - totalSupply, err := contract.TotalSupply(nil) - if err != nil { - return fmt.Errorf("error retrieving total supply: %w", err) - } - ret.TotalSupply = totalSupply.Bytes() - return nil + return getERC20ContractTotalSupply(contract, ret) }) g.Go(func() error { - decimals, err := contract.Decimals(nil) - if err != nil { - return fmt.Errorf("error retrieving decimals: %w", err) - } - ret.Decimals = big.NewInt(int64(decimals)).Bytes() - return nil + return getERC20ContractDecimals(contract, ret) }) g.Go(func() error { - rate, err := oracle.GetRateToEth(nil, common.BytesToAddress(token), false) - if err != nil { - return fmt.Errorf("error calling oneinchoracle.GetRateToEth: %w", err) - } - ret.Price = rate.Bytes() - return nil + return getRateFromOracle(oracle, token, ret) }) err = g.Wait() @@ -622,7 +481,8 @@ func (client *ErigonClient) GetERC20TokenMetadata(token []byte) (*types.ERC20Met } if err == nil && len(ret.Decimals) == 0 && ret.Symbol == "" && len(ret.TotalSupply) == 0 { - // it's possible that a token contract implements the ERC20 interfaces but does not return any values; we use a backup in this case + // it's possible that a token contract implements the ERC20 interfaces but does not + // return any values; we use a backup in this case ret = &types.ERC20Metadata{ Decimals: []byte{0x0}, Symbol: "UNKNOWN", @@ -753,3 +613,87 @@ func (client *ErigonClient) getTraceGeth(blockNumber *big.Int) ([]*Eth1InternalT } return indexedTraces, nil } + +func (client *ErigonClient) getBlockHash(blockNumber int64, receipts []*gethtypes.Receipt) (common.Hash, error) { + var blockHash common.Hash + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + // we cannot trust block.Hash(), some chain (gnosis) have extra field that are included in the hash computation + // so extract it from the receipts or from the node again if no receipt (it should be very rare) + if len(receipts) != 0 { + blockHash = receipts[0].BlockHash + } else { + var res minimalBlock + if err := client.rpcClient.CallContext(ctx, &res, "eth_getBlockByNumber", fmt.Sprintf("0x%x", blockNumber), false); err != nil { + return common.Hash{}, fmt.Errorf("error retrieving blockHash %v: %w", blockNumber, err) + } + blockHash = common.HexToHash(res.Hash) + } + + return blockHash, nil +} + +func (client *ErigonClient) getSender(tx *gethtypes.Transaction, blockHash common.Hash, txPosition int) []byte { + // this won't make a request in most cases as the sender is already present in the cache + // context https://github.com/ethereum/go-ethereum/blob/v1.14.11/ethclient/ethclient.go#L268 + sender, err := client.ethClient.TransactionSender(context.Background(), tx, blockHash, uint(txPosition)) + if err != nil { + sender = common.HexToAddress("abababababababababababababababababababab") + log.Error(err, "error converting tx to msg", 0, map[string]interface{}{"tx": tx.Hash()}) + } + return sender.Bytes() +} + +func getInternalTxs(traceIndex int, traces []*Eth1InternalTransactionWithPosition, txPosition int) []*types.Eth1InternalTransaction { + var internals []*types.Eth1InternalTransaction + for ; traceIndex < len(traces) && traces[traceIndex].txPosition == txPosition; traceIndex++ { + internals = append(internals, &traces[traceIndex].Eth1InternalTransaction) + } + return internals +} + +func getMaxFeePerBlobGas(tx *gethtypes.Transaction) []byte { + if tx.BlobGasFeeCap() != nil { + return tx.BlobGasFeeCap().Bytes() + } + return nil +} + +func getBlobVersionedHashes(tx *gethtypes.Transaction) [][]byte { + var hashes [][]byte + for _, h := range tx.BlobHashes() { + hashes = append(hashes, h.Bytes()) + } + return hashes +} + +func getBlobGasPrice(receipt *gethtypes.Receipt) []byte { + if receipt.BlobGasPrice != nil { + return receipt.BlobGasPrice.Bytes() + } + return nil +} + +func getBaseFee(block *gethtypes.Block) []byte { + if block.BaseFee() != nil { + return block.BaseFee().Bytes() + } + return nil +} + +func getBlobGasUsed(block *gethtypes.Block) uint64 { + blobGasUsed := block.BlobGasUsed() + if blobGasUsed != nil { + return *blobGasUsed + } + return 0 +} + +func getExcessBlobGas(block *gethtypes.Block) uint64 { + excessBlobGas := block.ExcessBlobGas() + if excessBlobGas != nil { + return *excessBlobGas + } + return 0 +} diff --git a/backend/pkg/commons/rpc/erigon_test.go b/backend/pkg/commons/rpc/erigon_test.go new file mode 100644 index 000000000..177b0fed5 --- /dev/null +++ b/backend/pkg/commons/rpc/erigon_test.go @@ -0,0 +1,439 @@ +package rpc + +import ( + "bytes" + "context" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/gobitfly/beaconchain/internal/th" + "github.com/gobitfly/beaconchain/pkg/commons/types" + "github.com/holiman/uint256" +) + +var ( + johnAddress = common.HexToAddress("0x6d2e03b7EfFEae98BD302A9F836D0d6Ab0002766") + tomAddress = common.HexToAddress("0x10e4597ff93cbee194f4879f8f1d54a370db6969") +) + +// TestGetInternalTxs tests the getInternalTxs function +// which returns the internal transactions of a transaction at a given position +func TestGetInternalTxs(t *testing.T) { + tests := []struct { + name string + traceIndex int + traces []*Eth1InternalTransactionWithPosition + txPosition int + expected []*types.Eth1InternalTransaction + }{ + { + name: "single internal transaction with tx position", + traceIndex: 0, + traces: []*Eth1InternalTransactionWithPosition{ + { + txPosition: 0, + Eth1InternalTransaction: types.Eth1InternalTransaction{ + From: johnAddress.Bytes(), + To: tomAddress.Bytes(), + Value: big.NewInt(100).Bytes(), + }, + }, + }, + txPosition: 0, + expected: []*types.Eth1InternalTransaction{ + { + From: johnAddress.Bytes(), + To: tomAddress.Bytes(), + Value: big.NewInt(100).Bytes(), + }, + }, + }, + { + name: "two internal transactions with tx position", + traceIndex: 1, + traces: []*Eth1InternalTransactionWithPosition{ + { + txPosition: 0, + Eth1InternalTransaction: types.Eth1InternalTransaction{ + From: johnAddress.Bytes(), + To: tomAddress.Bytes(), + Value: big.NewInt(100).Bytes(), + }, + }, + { + txPosition: 1, + Eth1InternalTransaction: types.Eth1InternalTransaction{ + From: tomAddress.Bytes(), + To: johnAddress.Bytes(), + Value: big.NewInt(200).Bytes(), + }, + }, + }, + txPosition: 1, + expected: []*types.Eth1InternalTransaction{ + { + From: tomAddress.Bytes(), + To: johnAddress.Bytes(), + Value: big.NewInt(200).Bytes(), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getInternalTxs(tt.traceIndex, tt.traces, tt.txPosition) + if len(result) != len(tt.expected) { + t.Fatalf("got %v internal transactions, want %v internal transactions", len(result), len(tt.expected)) + } + for i, itx := range result { + if !bytes.Equal(itx.From, tt.expected[i].From) { + t.Errorf("got From %v, want %v", itx.From, tt.expected[i].From) + } + if !bytes.Equal(itx.To, tt.expected[i].To) { + t.Errorf("got To %v, want %v", itx.To, tt.expected[i].To) + } + if !bytes.Equal(itx.Value, tt.expected[i].Value) { + t.Errorf("got Value %v, want %v", itx.Value, tt.expected[i].Value) + } + } + }) + } +} + +// TestGetBlobVersionedHashes tests the getBlobVersionedHashes function +// which extracts the blob versioned hashes from a transaction +func TestGetBlobVersionedHashes(t *testing.T) { + tests := []struct { + name string + tx *gethtypes.Transaction + expected [][]byte + }{ + { + name: "transaction with blob versioned hashes", + tx: gethtypes.NewTx(&gethtypes.BlobTx{ + BlobHashes: []common.Hash{common.HexToHash("0x12345"), common.HexToHash("0x23456")}, + }), + expected: [][]byte{common.HexToHash("0x12345").Bytes(), common.HexToHash("0x23456").Bytes()}, + }, + { + name: "transaction without blob versioned hashes", + tx: gethtypes.NewTransaction(0, johnAddress, big.NewInt(100), 100000, big.NewInt(10), []byte{}), + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getBlobVersionedHashes(tt.tx) + if len(result) != len(tt.expected) { + t.Fatalf("got %v hashes, want %v hashes", len(result), len(tt.expected)) + } + for i, hash := range result { + if !bytes.Equal(hash, tt.expected[i]) { + t.Errorf("got Hash %v, want %v", hash, tt.expected[i]) + } + } + }) + } +} + +// TestGetMaxFeePerBlobGas tests the getMaxFeePerBlobGas function +// which extracts the max fee per blob gas from a transaction +func TestGetMaxFeePerBlobGas(t *testing.T) { + tests := []struct { + name string + tx *gethtypes.Transaction + expected []byte + }{ + { + name: "transaction with blob gas fee cap", + tx: gethtypes.NewTx(&gethtypes.BlobTx{ + BlobFeeCap: uint256.NewInt(10), + }), + expected: big.NewInt(10).Bytes(), + }, + { + name: "transaction with no blob gas fee cap", + tx: gethtypes.NewTransaction(0, johnAddress, big.NewInt(100), 100000, big.NewInt(10), []byte{}), + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getMaxFeePerBlobGas(tt.tx) + if !bytes.Equal(result, tt.expected) { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestGetBlobGasPrice tests the getBlobGasPrice function +// which extracts the blob gas price from a receipt +func TestGetBlobGasPrice(t *testing.T) { + tests := []struct { + name string + receipt *gethtypes.Receipt + expected []byte + }{ + { + name: "receipt with blob gas price", + receipt: &gethtypes.Receipt{BlobGasPrice: big.NewInt(10)}, + expected: big.NewInt(10).Bytes(), + }, + { + name: "receipt with no blob gas price", + receipt: &gethtypes.Receipt{}, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getBlobGasPrice(tt.receipt) + if !bytes.Equal(result, tt.expected) { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestGetBaseFee tests the getBaseFee function +// which extracts the base fee from a block +func TestGetBaseFee(t *testing.T) { + tests := []struct { + name string + block *gethtypes.Block + expected []byte + }{ + { + name: "block with base fee", + block: gethtypes.NewBlockWithHeader(&gethtypes.Header{BaseFee: big.NewInt(10)}), + expected: big.NewInt(10).Bytes(), + }, + { + name: "block with no base fee", + block: gethtypes.NewBlockWithHeader(&gethtypes.Header{}), + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getBaseFee(tt.block) + if !bytes.Equal(result, tt.expected) { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestGetBlobGasUsed tests the getBlobGasUsed function +// which extracts the blob gas used from a block +func TestGetBlobGasUsed(t *testing.T) { + tests := []struct { + name string + block *gethtypes.Block + expected uint64 + }{ + { + name: "block with blob gas used", + block: gethtypes.NewBlockWithHeader(&gethtypes.Header{BlobGasUsed: uint64Ptr(100)}), + expected: 100, + }, + { + name: "block with no blob gas used", + block: gethtypes.NewBlockWithHeader(&gethtypes.Header{}), + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getBlobGasUsed(tt.block) + if result != tt.expected { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestGetExcessBlobGas tests the getExcessBlobGas function +// which extracts the excess blob gas from a block +func TestGetExcessBlobGas(t *testing.T) { + tests := []struct { + name string + block *gethtypes.Block + expected uint64 + }{ + { + name: "block with excess blob gas", + block: gethtypes.NewBlockWithHeader(&gethtypes.Header{ExcessBlobGas: uint64Ptr(100)}), + expected: 100, + }, + { + name: "block with no excess blob gas", + block: gethtypes.NewBlockWithHeader(&gethtypes.Header{}), + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getExcessBlobGas(tt.block) + if result != tt.expected { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestGetSender tests the getSender function +// which extracts the sender of a transaction +func TestGetSender(t *testing.T) { + backend := th.NewBackend(t) + defer backend.Close() + temp := th.CreateEOA(t) + if err := backend.Client().SendTransaction(context.Background(), backend.MakeTx(t, backend.BankAccount, &temp.From, big.NewInt(1), nil)); err != nil { + t.Fatal(err) + } + backend.Commit() + + lastBlock, err := backend.Client().BlockNumber(context.Background()) + if err != nil { + t.Fatal(err) + } + + client, err := NewErigonClient(backend.Endpoint) + if err != nil { + t.Fatal(err) + } + + block, err := client.ethClient.BlockByNumber(context.Background(), big.NewInt(int64(lastBlock))) + if err != nil { + t.Fatal(err) + } + + if len(block.Transactions()) == 0 { + t.Fatal("no transactions in the block") + } + + tests := []struct { + name string + tx *gethtypes.Transaction + blockHash common.Hash + txPosition int + expected []byte + expectError bool + }{ + { + name: "valid transaction with sender", + tx: block.Transactions()[0], + blockHash: block.Hash(), + txPosition: 0, + expected: backend.BankAccount.From.Bytes(), + }, + { + name: "transaction with error retrieving sender", + tx: block.Transactions()[0], + blockHash: common.HexToHash("0x456"), + txPosition: 1, + expected: common.HexToAddress("abababababababababababababababababababab").Bytes(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := client.getSender(tt.tx, tt.blockHash, tt.txPosition) + if !bytes.Equal(result, tt.expected) { + t.Errorf("got %x, want %x", result, tt.expected) + } + }) + } +} + +// TestGetBlockHash tests the getBlockHash function +// which extracts the hash of a block from receipts or from RPC call +func TestGetBlockHash(t *testing.T) { + backend := th.NewBackend(t) + defer backend.Close() + temp := th.CreateEOA(t) + if err := backend.Client().SendTransaction(context.Background(), backend.MakeTx(t, backend.BankAccount, &temp.From, big.NewInt(1), nil)); err != nil { + t.Fatal(err) + } + backend.Commit() + + lastBlock, err := backend.Client().BlockNumber(context.Background()) + if err != nil { + t.Fatal(err) + } + + client, err := NewErigonClient(backend.Endpoint) + if err != nil { + t.Fatal(err) + } + + block, err := client.ethClient.BlockByNumber(context.Background(), big.NewInt(int64(lastBlock))) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + blockNumber int64 + receipts []*gethtypes.Receipt + expected common.Hash + expectError bool + }{ + { + name: "block hash from receipts", + blockNumber: int64(lastBlock), + receipts: []*gethtypes.Receipt{ + {BlockHash: common.HexToHash("0x123")}, + }, + expected: common.HexToHash("0x123"), + expectError: false, + }, + { + name: "block hash from RPC", + blockNumber: int64(lastBlock), + receipts: nil, + expected: block.Hash(), + expectError: false, + }, + { + name: "RPC call error", + blockNumber: -1, + receipts: nil, + expected: common.Hash{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := client.getBlockHash(tt.blockNumber, tt.receipts) + if tt.expectError { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != tt.expected { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// unit64Ptr returns a pointer to a uint64 +func uint64Ptr(i uint64) *uint64 { + return &i +} diff --git a/backend/pkg/commons/rpc/geth.go b/backend/pkg/commons/rpc/geth.go index 741809b7b..226377dab 100644 --- a/backend/pkg/commons/rpc/geth.go +++ b/backend/pkg/commons/rpc/geth.go @@ -103,67 +103,41 @@ func (client *GethClient) GetBlock(number int64) (*types.Eth1Block, *types.GetBl timings.Headers = time.Since(start) start = time.Now() + uncles := getBlockUncles(block.Uncles()) + c := &types.Eth1Block{ - Hash: block.Hash().Bytes(), - ParentHash: block.ParentHash().Bytes(), - UncleHash: block.UncleHash().Bytes(), - Coinbase: block.Coinbase().Bytes(), - Root: block.Root().Bytes(), - TxHash: block.TxHash().Bytes(), - ReceiptHash: block.ReceiptHash().Bytes(), - Difficulty: block.Difficulty().Bytes(), - Number: block.NumberU64(), - GasLimit: block.GasLimit(), - GasUsed: block.GasUsed(), - Time: timestamppb.New(time.Unix(int64(block.Time()), 0)), - Extra: block.Extra(), - MixDigest: block.MixDigest().Bytes(), - Bloom: block.Bloom().Bytes(), - Uncles: []*types.Eth1Block{}, + Hash: block.Hash().Bytes(), + ParentHash: block.ParentHash().Bytes(), + UncleHash: block.UncleHash().Bytes(), + Coinbase: block.Coinbase().Bytes(), + Root: block.Root().Bytes(), + TxHash: block.TxHash().Bytes(), + ReceiptHash: block.ReceiptHash().Bytes(), + Difficulty: block.Difficulty().Bytes(), + Number: block.NumberU64(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + Time: timestamppb.New(time.Unix(int64(block.Time()), 0)), + Extra: block.Extra(), + MixDigest: block.MixDigest().Bytes(), + Bloom: block.Bloom().Bytes(), + BaseFee: func() []byte { + if block.BaseFee() != nil { + return block.BaseFee().Bytes() + } + return nil + }(), + Uncles: uncles, Transactions: []*types.Eth1Transaction{}, } - if block.BaseFee() != nil { - c.BaseFee = block.BaseFee().Bytes() - } - - for _, uncle := range block.Uncles() { - pbUncle := &types.Eth1Block{ - Hash: uncle.Hash().Bytes(), - ParentHash: uncle.ParentHash.Bytes(), - UncleHash: uncle.UncleHash.Bytes(), - Coinbase: uncle.Coinbase.Bytes(), - Root: uncle.Root.Bytes(), - TxHash: uncle.TxHash.Bytes(), - ReceiptHash: uncle.ReceiptHash.Bytes(), - Difficulty: uncle.Difficulty.Bytes(), - Number: uncle.Number.Uint64(), - GasLimit: uncle.GasLimit, - GasUsed: uncle.GasUsed, - Time: timestamppb.New(time.Unix(int64(uncle.Time), 0)), - Extra: uncle.Extra, - MixDigest: uncle.MixDigest.Bytes(), - Bloom: uncle.Bloom.Bytes(), - } - - c.Uncles = append(c.Uncles, pbUncle) - } - receipts := make([]*gethtypes.Receipt, len(block.Transactions())) reqs := make([]gethrpc.BatchElem, len(block.Transactions())) txs := block.Transactions() - for _, tx := range txs { - var from []byte - sender, err := gethtypes.Sender(gethtypes.NewCancunSigner(tx.ChainId()), tx) - if err != nil { - from, _ = hex.DecodeString("abababababababababababababababababababab") - log.Error(err, "error converting tx to msg", 0, map[string]interface{}{"tx": tx.Hash()}) - } else { - from = sender.Bytes() - } - + from := getGethSender(tx) + to := getReceiver(tx) pbTx := &types.Eth1Transaction{ Type: uint32(tx.Type()), Nonce: tx.Nonce(), @@ -174,15 +148,13 @@ func (client *GethClient) GetBlock(number int64) (*types.Eth1Block, *types.GetBl Value: tx.Value().Bytes(), Data: tx.Data(), From: from, + To: to, ChainId: tx.ChainId().Bytes(), AccessList: []*types.AccessList{}, Hash: tx.Hash().Bytes(), Itx: []*types.Eth1InternalTransaction{}, } - if tx.To() != nil { - pbTx.To = tx.To().Bytes() - } c.Transactions = append(c.Transactions, pbTx) } @@ -214,21 +186,7 @@ func (client *GethClient) GetBlock(number int64) (*types.Eth1Block, *types.GetBl c.Transactions[i].CommulativeGasUsed = r.CumulativeGasUsed c.Transactions[i].GasUsed = r.GasUsed c.Transactions[i].LogsBloom = r.Bloom[:] - c.Transactions[i].Logs = make([]*types.Eth1Log, 0, len(r.Logs)) - - for _, l := range r.Logs { - pbLog := &types.Eth1Log{ - Address: l.Address.Bytes(), - Data: l.Data, - Removed: l.Removed, - Topics: make([][]byte, 0, len(l.Topics)), - } - - for _, t := range l.Topics { - pbLog.Topics = append(pbLog.Topics, t.Bytes()) - } - c.Transactions[i].Logs = append(c.Transactions[i].Logs, pbLog) - } + c.Transactions[i].Logs = getLogsFromReceipts(r.Logs) } return c, timings, nil @@ -321,28 +279,20 @@ func (client *GethClient) GetBalances(pairs []string) ([]*types.Eth1AddressBalan return ret, nil } -func (client *GethClient) GetBalancesForAddresse(address string, tokenStr []string) ([]*types.Eth1AddressBalance, error) { +func (client *GethClient) GetBalancesForAddress(address string, tokenStr []string) ([]*types.Eth1AddressBalance, error) { opts := &bind.CallOpts{ BlockNumber: nil, } - tokens := make([]common.Address, 0, len(tokenStr)) - - for _, token := range tokenStr { - tokens = append(tokens, common.HexToAddress(token)) - } + tokens := getTokens(tokenStr) balancesInt, err := client.multiChecker.Balances(opts, []common.Address{common.HexToAddress(address)}, tokens) if err != nil { return nil, err } - res := make([]*types.Eth1AddressBalance, len(tokenStr)) - for tokenIdx := range tokens { - res[tokenIdx] = &types.Eth1AddressBalance{ - Address: common.FromHex(address), - Token: common.FromHex(string(tokens[tokenIdx].Bytes())), - Balance: balancesInt[tokenIdx].Bytes(), - } + res, err := parseAddressBalance(tokens, address, balancesInt) + if err != nil { + return nil, err } return res, nil @@ -380,6 +330,11 @@ func (client *GethClient) GetERC20TokenBalance(address string, token string) ([] func (client *GethClient) GetERC20TokenMetadata(token []byte) (*types.ERC20Metadata, error) { log.Infof("retrieving metadata for token %x", token) + oracle, err := oneinchoracle.NewOneInchOracleByChainID(client.GetChainID(), client.ethClient) + if err != nil { + return nil, fmt.Errorf("error initializing oneinchoracle.NewOneInchOracleByChainID: %w", err) + } + contract, err := contracts.NewIERC20Metadata(common.BytesToAddress(token), client.ethClient) if err != nil { return nil, fmt.Errorf("error getting token-contract: erc20.NewErc20: %w", err) @@ -390,52 +345,22 @@ func (client *GethClient) GetERC20TokenMetadata(token []byte) (*types.ERC20Metad ret := &types.ERC20Metadata{} g.Go(func() error { - symbol, err := contract.Symbol(nil) - if err != nil { - if strings.Contains(err.Error(), "abi") { - ret.Symbol = "UNKNOWN" - return nil - } - - return fmt.Errorf("error retrieving token symbol: %w", err) - } - - ret.Symbol = symbol - return nil + return getERC20ContractSymbol(contract, ret) }) g.Go(func() error { - totalSupply, err := contract.TotalSupply(nil) - if err != nil { - return fmt.Errorf("error retrieving token total supply: %w", err) - } - ret.TotalSupply = totalSupply.Bytes() - return nil + return getERC20ContractTotalSupply(contract, ret) }) g.Go(func() error { - decimals, err := contract.Decimals(nil) - if err != nil { - return fmt.Errorf("error retrieving token decimals: %w", err) - } - ret.Decimals = big.NewInt(int64(decimals)).Bytes() - return nil + return getERC20ContractDecimals(contract, ret) }) g.Go(func() error { if !oneinchoracle.SupportedChainId(client.GetChainID()) { return nil } - oracle, err := oneinchoracle.NewOneInchOracleByChainID(client.GetChainID(), client.ethClient) - if err != nil { - return fmt.Errorf("error initializing oneinchoracle.NewOneInchOracleByChainID: %w", err) - } - rate, err := oracle.GetRateToEth(nil, common.BytesToAddress(token), false) - if err != nil { - return fmt.Errorf("error calling oneinchoracle.GetRateToEth: %w", err) - } - ret.Price = rate.Bytes() - return nil + return getRateFromOracle(oracle, token, ret) }) err = g.Wait() @@ -444,7 +369,8 @@ func (client *GethClient) GetERC20TokenMetadata(token []byte) (*types.ERC20Metad } if err == nil && len(ret.Decimals) == 0 && ret.Symbol == "" && len(ret.TotalSupply) == 0 { - // it's possible that a token contract implements the ERC20 interfaces but does not return any values; we use a backup in this case + // it's possible that a token contract implements the ERC20 interfaces but does not + // return any values; we use a backup in this case ret = &types.ERC20Metadata{ Decimals: []byte{0x0}, Symbol: "UNKNOWN", @@ -453,3 +379,15 @@ func (client *GethClient) GetERC20TokenMetadata(token []byte) (*types.ERC20Metad return ret, err } + +func getGethSender(tx *gethtypes.Transaction) []byte { + var from []byte + sender, err := gethtypes.Sender(gethtypes.NewCancunSigner(tx.ChainId()), tx) + if err != nil { + from, _ = hex.DecodeString("abababababababababababababababababababab") + log.Error(err, "error converting tx to msg", 0, map[string]interface{}{"tx": tx.Hash()}) + } else { + from = sender.Bytes() + } + return from +} diff --git a/backend/pkg/commons/rpc/utils.go b/backend/pkg/commons/rpc/utils.go new file mode 100644 index 000000000..88ae4296a --- /dev/null +++ b/backend/pkg/commons/rpc/utils.go @@ -0,0 +1,171 @@ +package rpc + +import ( + "fmt" + "math/big" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/gobitfly/beaconchain/internal/contracts" + "github.com/gobitfly/beaconchain/pkg/commons/contracts/oneinchoracle" + "github.com/gobitfly/beaconchain/pkg/commons/types" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func getReceiver(tx *gethtypes.Transaction) []byte { + if tx.To() != nil { + return tx.To().Bytes() + } + return nil +} + +func getTokens(tokenStr []string) []common.Address { + tokens := make([]common.Address, 0, len(tokenStr)) + + for _, token := range tokenStr { + tokens = append(tokens, common.HexToAddress(token)) + } + return tokens +} + +func getBlockUncles(blkUncles []*gethtypes.Header) []*types.Eth1Block { + uncles := make([]*types.Eth1Block, len(blkUncles)) + for i, uncle := range blkUncles { + uncles[i] = &types.Eth1Block{ + Hash: uncle.Hash().Bytes(), + ParentHash: uncle.ParentHash.Bytes(), + UncleHash: uncle.UncleHash.Bytes(), + Coinbase: uncle.Coinbase.Bytes(), + Root: uncle.Root.Bytes(), + TxHash: uncle.TxHash.Bytes(), + ReceiptHash: uncle.ReceiptHash.Bytes(), + Difficulty: uncle.Difficulty.Bytes(), + Number: uncle.Number.Uint64(), + GasLimit: uncle.GasLimit, + GasUsed: uncle.GasUsed, + Time: timestamppb.New(time.Unix(int64(uncle.Time), 0)), + Extra: uncle.Extra, + MixDigest: uncle.MixDigest.Bytes(), + Bloom: uncle.Bloom.Bytes(), + } + } + return uncles +} + +func getBlockWithdrawals(blkWithdrawals gethtypes.Withdrawals) []*types.Eth1Withdrawal { + withdrawals := make([]*types.Eth1Withdrawal, len(blkWithdrawals)) + for i, withdrawal := range blkWithdrawals { + withdrawals[i] = &types.Eth1Withdrawal{ + Index: withdrawal.Index, + ValidatorIndex: withdrawal.Validator, + Address: withdrawal.Address.Bytes(), + Amount: new(big.Int).SetUint64(withdrawal.Amount).Bytes(), + } + } + return withdrawals +} + +func getLogsFromReceipts(logs []*gethtypes.Log) []*types.Eth1Log { + eth1Logs := make([]*types.Eth1Log, len(logs)) + for i, log := range logs { + topics := make([][]byte, len(log.Topics)) + for j, topic := range log.Topics { + topics[j] = topic.Bytes() + } + eth1Logs[i] = &types.Eth1Log{ + Address: log.Address.Bytes(), + Data: log.Data, + Removed: log.Removed, + Topics: topics, + } + } + return eth1Logs +} + +func getERC20ContractSymbol(contract *contracts.IERC20Metadata, ret *types.ERC20Metadata) error { + symbol, err := contract.Symbol(nil) + if err != nil { + if strings.Contains(err.Error(), "abi") { + ret.Symbol = "UNKNOWN" + return nil + } + + return fmt.Errorf("error retrieving symbol: %w", err) + } + + ret.Symbol = symbol + return nil +} + +func getERC20ContractTotalSupply(contract *contracts.IERC20Metadata, ret *types.ERC20Metadata) error { + totalSupply, err := contract.TotalSupply(nil) + if err != nil { + return fmt.Errorf("error retrieving total supply: %w", err) + } + ret.TotalSupply = totalSupply.Bytes() + return nil +} + +func getERC20ContractDecimals(contract *contracts.IERC20Metadata, ret *types.ERC20Metadata) error { + decimals, err := contract.Decimals(nil) + if err != nil { + return fmt.Errorf("error retrieving decimals: %w", err) + } + ret.Decimals = big.NewInt(int64(decimals)).Bytes() + return nil +} + +func getRateFromOracle(oracle *oneinchoracle.OneinchOracle, token []byte, ret *types.ERC20Metadata) error { + rate, err := oracle.GetRateToEth(nil, common.BytesToAddress(token), false) + if err != nil { + return fmt.Errorf("error calling oneinchoracle.GetRateToEth: %w", err) + } + ret.Price = rate.Bytes() + return nil +} + +func parseAddressBalance(tokens []common.Address, address string, balances []*big.Int) ([]*types.Eth1AddressBalance, error) { + if len(tokens) == 0 || len(balances) == 0 { + return nil, fmt.Errorf("tokens or balances slice is empty") + } + if len(tokens) != len(balances) { + return nil, fmt.Errorf("tokens and balances slices have mismatched lengths") + } + + if address == "" { + return nil, fmt.Errorf("address is empty") + } + addrBytes := common.FromHex(address) + if len(addrBytes) == 0 { + return nil, fmt.Errorf("invalid address format") + } + + res := make([]*types.Eth1AddressBalance, len(tokens)) + for tokenIdx, token := range tokens { + if token == (common.Address{}) { + return nil, fmt.Errorf("token at index %d is empty", tokenIdx) + } + tokenBytes := token.Bytes() + if len(tokenBytes) == 0 { + return nil, fmt.Errorf("invalid token format at index %d", tokenIdx) + } + + if balances[tokenIdx] == nil { + return nil, fmt.Errorf("balance at index %d is nil", tokenIdx) + } + balanceBytes := balances[tokenIdx].Bytes() + if len(balanceBytes) == 0 { + return nil, fmt.Errorf("invalid balance format at index %d", tokenIdx) + } + + res[tokenIdx] = &types.Eth1AddressBalance{ + Address: addrBytes, + Token: tokenBytes, + Balance: balanceBytes, + } + } + + return res, nil +} diff --git a/backend/pkg/commons/rpc/utils_test.go b/backend/pkg/commons/rpc/utils_test.go new file mode 100644 index 000000000..68b06c168 --- /dev/null +++ b/backend/pkg/commons/rpc/utils_test.go @@ -0,0 +1,803 @@ +package rpc + +import ( + "bytes" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/gobitfly/beaconchain/internal/contracts" + "github.com/gobitfly/beaconchain/internal/th" + "github.com/gobitfly/beaconchain/pkg/commons/types" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + aliceAddress = common.HexToAddress("0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5") + bobAddress = common.HexToAddress("0x388C818CA8B9251b393131C08a736A67ccB19297") + token = common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + token2 = common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") +) + +// TestGetReceiver tests the getReceiver function, which extracts the recipient +// address from a transaction. +func TestGetReceiver(t *testing.T) { + tests := []struct { + name string + tx *gethtypes.Transaction + expected []byte + }{ + { + name: "transaction with recipient", + tx: gethtypes.NewTransaction(0, aliceAddress, big.NewInt(100), 100000, big.NewInt(10), []byte{}), + expected: aliceAddress.Bytes(), + }, + { + name: "contract creation transaction", + tx: gethtypes.NewContractCreation(0, big.NewInt(100), 100000, big.NewInt(10), []byte{}), + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getReceiver(tt.tx) + if !bytes.Equal(result, tt.expected) { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestGetBlockWithdrawals tests the getBlockWithdrawals function, which +// converts a list of withdrawals from a block into a list of Eth1Withdrawal type. +func TestGetBlockWithdrawals(t *testing.T) { + tests := []struct { + name string + blkWithdrawals gethtypes.Withdrawals + expected []*types.Eth1Withdrawal + }{ + { + name: "block with one withdrawal", + blkWithdrawals: gethtypes.Withdrawals{ + { + Index: 1, + Validator: 2, + Address: aliceAddress, + Amount: 100, + }, + }, + expected: []*types.Eth1Withdrawal{ + { + Index: 1, + ValidatorIndex: 2, + Address: aliceAddress.Bytes(), + Amount: big.NewInt(100).Bytes(), + }, + }, + }, + { + name: "block with two withdrawals", + blkWithdrawals: gethtypes.Withdrawals{ + { + Index: 1, + Validator: 2, + Address: aliceAddress, + Amount: 100, + }, + { + Index: 2, + Validator: 3, + Address: bobAddress, + Amount: 200, + }, + }, + expected: []*types.Eth1Withdrawal{ + { + Index: 1, + ValidatorIndex: 2, + Address: aliceAddress.Bytes(), + Amount: big.NewInt(100).Bytes(), + }, + { + Index: 2, + ValidatorIndex: 3, + Address: bobAddress.Bytes(), + Amount: big.NewInt(200).Bytes(), + }, + }, + }, + { + name: "block with no withdrawals", + blkWithdrawals: gethtypes.Withdrawals{}, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getBlockWithdrawals(tt.blkWithdrawals) + if len(result) != len(tt.expected) { + t.Fatalf("got %v withdrawals, want %v withdrawals", len(result), len(tt.expected)) + } + for i, withdrawal := range result { + if withdrawal.Index != tt.expected[i].Index { + t.Errorf("got Index %v, want %v", withdrawal.Index, tt.expected[i].Index) + } + if withdrawal.ValidatorIndex != tt.expected[i].ValidatorIndex { + t.Errorf("got ValidatorIndex %v, want %v", withdrawal.ValidatorIndex, tt.expected[i].ValidatorIndex) + } + if !bytes.Equal(withdrawal.Address, tt.expected[i].Address) { + t.Errorf("got Address %v, want %v", withdrawal.Address, tt.expected[i].Address) + } + if !bytes.Equal(withdrawal.Amount, tt.expected[i].Amount) { + t.Errorf("got Amount %v, want %v", withdrawal.Amount, tt.expected[i].Amount) + } + } + }) + } +} + +// TestGetLogsFromReceipts tests the getLogsFromReceipts function, which converts +// a list of logs from a receipt into a list of Eth1Log type +func TestGetLogsFromReceipts(t *testing.T) { + tests := []struct { + name string + logs []*gethtypes.Log + expected []*types.Eth1Log + }{ + { + name: "receipt with one log", + logs: []*gethtypes.Log{ + { + Address: aliceAddress, + Topics: []common.Hash{common.HexToHash("0x123"), common.HexToHash("0x234")}, + Data: []byte("data"), + Removed: false, + }, + }, + expected: []*types.Eth1Log{ + { + Address: aliceAddress.Bytes(), + Topics: [][]byte{common.HexToHash("0x123").Bytes(), common.HexToHash("0x234").Bytes()}, + Data: []byte("data"), + Removed: false, + }, + }, + }, + { + name: "receipt with two logs", + logs: []*gethtypes.Log{ + { + Address: aliceAddress, + Topics: []common.Hash{common.HexToHash("0x123"), common.HexToHash("0x234")}, + Data: []byte("data"), + Removed: false, + }, + { + Address: bobAddress, + Topics: []common.Hash{common.HexToHash("0x123"), common.HexToHash("0x234")}, + Data: []byte("data"), + Removed: false, + }, + }, + expected: []*types.Eth1Log{ + { + Address: aliceAddress.Bytes(), + Topics: [][]byte{common.HexToHash("0x123").Bytes(), common.HexToHash("0x234").Bytes()}, + Data: []byte("data"), + Removed: false, + }, + { + Address: bobAddress.Bytes(), + Topics: [][]byte{common.HexToHash("0x123").Bytes(), common.HexToHash("0x234").Bytes()}, + Data: []byte("data"), + Removed: false, + }, + }, + }, + { + name: "receipt with no logs", + logs: []*gethtypes.Log{}, + expected: nil, + }, + { + name: "receipt with logs but no topics", + logs: []*gethtypes.Log{ + { + Address: aliceAddress, + Topics: []common.Hash{}, + Data: []byte("data"), + Removed: false, + }, + }, + expected: []*types.Eth1Log{ + { + Address: aliceAddress.Bytes(), + Topics: [][]byte{}, + Data: []byte("data"), + Removed: false, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getLogsFromReceipts(tt.logs) + if len(result) != len(tt.expected) { + t.Fatalf("got %v logs, want %v logs", len(result), len(tt.expected)) + } + for i, log := range result { + if !bytes.Equal(log.Address, tt.expected[i].Address) { + t.Errorf("got Address %v, want %v", log.Address, tt.expected[i].Address) + } + if !bytes.Equal(log.Data, tt.expected[i].Data) { + t.Errorf("got Data %v, want %v", log.Data, tt.expected[i].Data) + } + if log.Removed != tt.expected[i].Removed { + t.Errorf("got Removed %v, want %v", log.Removed, tt.expected[i].Removed) + } + for j, topic := range log.Topics { + if !bytes.Equal(topic, tt.expected[i].Topics[j]) { + t.Errorf("got Topic %v, want %v", topic, tt.expected[i].Topics[j]) + } + } + } + }) + } +} + +// TestGetTokens tests the getTokens function, which converts a list of token +// addresses from a transaction into a list of common.Address type +func TestGetTokens(t *testing.T) { + tests := []struct { + name string + tokenStr []string + expected []common.Address + }{ + { + name: "single token address", + tokenStr: []string{token.Hex()}, + expected: []common.Address{token}, + }, + { + name: "two token addresses", + tokenStr: []string{token.Hex(), token2.Hex()}, + expected: []common.Address{token, token2}, + }, + { + name: "empty token addresses", + tokenStr: []string{}, + expected: []common.Address{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getTokens(tt.tokenStr) + if len(result) != len(tt.expected) { + t.Fatalf("got %v tokens, want %v tokens", len(result), len(tt.expected)) + } + for i, token := range result { + if token != tt.expected[i] { + t.Errorf("got token %v, want %v", token, tt.expected[i]) + } + } + }) + } +} + +// TestParseAddressBalance tests the parseAddressBalance function, which converts +// a list of token addresses, address and a list of balances into a list of +// Eth1AddressBalance type +func TestParseAddressBalance(t *testing.T) { + tests := []struct { + name string + tokens []common.Address + address string + balances []*big.Int + expected []*types.Eth1AddressBalance + expectError bool + }{ + { + name: "valid single token and balance", + tokens: []common.Address{token}, + address: aliceAddress.Hex(), + balances: []*big.Int{ + big.NewInt(100), + }, + expected: []*types.Eth1AddressBalance{ + { + Address: aliceAddress.Bytes(), + Token: token.Bytes(), + Balance: big.NewInt(100).Bytes(), + }, + }, + expectError: false, + }, + { + name: "valid two tokens and balances", + tokens: []common.Address{token, token2}, + address: aliceAddress.Hex(), + balances: []*big.Int{ + big.NewInt(100), + big.NewInt(200), + }, + expected: []*types.Eth1AddressBalance{ + { + Address: aliceAddress.Bytes(), + Token: token.Bytes(), + Balance: big.NewInt(100).Bytes(), + }, + { + Address: aliceAddress.Bytes(), + Token: token2.Bytes(), + Balance: big.NewInt(200).Bytes(), + }, + }, + expectError: false, + }, + { + name: "no tokens", + tokens: []common.Address{}, + address: aliceAddress.Hex(), + balances: []*big.Int{ + big.NewInt(100), + }, + expected: nil, + expectError: true, + }, + { + name: "no balance", + tokens: []common.Address{token}, + address: aliceAddress.Hex(), + balances: []*big.Int{}, + expected: nil, + expectError: true, + }, + { + name: "single token and two balances", + tokens: []common.Address{token}, + address: aliceAddress.Hex(), + balances: []*big.Int{ + big.NewInt(100), + big.NewInt(200), + }, + expected: nil, + expectError: true, + }, + { + name: "invalid address format", + tokens: []common.Address{token}, + address: "invalid-address", + balances: []*big.Int{ + big.NewInt(100), + }, + expected: nil, + expectError: true, + }, + { + name: "empty token address", + tokens: []common.Address{common.HexToAddress("")}, + address: aliceAddress.Hex(), + balances: []*big.Int{ + big.NewInt(100), + }, + expected: nil, + expectError: true, + }, + { + name: "nil balance", + tokens: []common.Address{token}, + address: aliceAddress.Hex(), + balances: []*big.Int{ + nil, + }, + expected: nil, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseAddressBalance(tt.tokens, tt.address, tt.balances) + + if tt.expectError { + if err == nil { + t.Errorf("expected an error but got none") + } + if result != nil { + t.Errorf("expected result to be nil on error, got %v", result) + } + return + } + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + if len(result) != len(tt.expected) { + t.Fatalf("got %v balances, want %v balances", len(result), len(tt.expected)) + } + + for i, res := range result { + if !bytes.Equal(res.Address, tt.expected[i].Address) { + t.Errorf("got Address %v, want %v", res.Address, tt.expected[i].Address) + } + if !bytes.Equal(res.Token, tt.expected[i].Token) { + t.Errorf("got Token %v, want %v", res.Token, tt.expected[i].Token) + } + if !bytes.Equal(res.Balance, tt.expected[i].Balance) { + t.Errorf("got Balance %v, want %v", res.Balance, tt.expected[i].Balance) + } + } + }) + } +} + +// TestGetBlockUncles tests the getBlockUncles function which extracts a list of +// uncles from a block into a list of Eth1Block type +func TestGetBlockUncles(t *testing.T) { + var ( + hash1 = common.HexToHash("0x1234567890abcdef1234567890abcdef12345678") + hash2 = common.HexToHash("0xabcdef1234567890abcdef1234567890abcdef12") + hash3 = common.HexToHash("0xabcdef1234567890abcdef1234567890abcdef13") + hash4 = common.HexToHash("0xabcdef1234567890abcdef1234567890abcdef14") + hash5 = common.HexToHash("0xabcdef1234567890abcdef1234567890abcdef15") + hash6 = common.HexToHash("0xabcdef1234567890abcdef1234567890abcdef16") + ) + tests := []struct { + name string + blkUncles []*gethtypes.Header + expected []*types.Eth1Block + }{ + { + name: "block with single uncle", + blkUncles: []*gethtypes.Header{ + { + Number: big.NewInt(1), + ParentHash: hash1, + UncleHash: hash2, + Coinbase: common.HexToAddress("0xcoinbase"), + Root: hash3, + TxHash: hash4, + ReceiptHash: hash5, + Difficulty: big.NewInt(100), + GasLimit: 1000000, + GasUsed: 500000, + Time: 123456789, + Extra: []byte("extra"), + MixDigest: hash6, + Bloom: gethtypes.Bloom{}, + }, + }, + expected: []*types.Eth1Block{ + { + Hash: hash2.Bytes(), + ParentHash: hash1.Bytes(), + UncleHash: hash2.Bytes(), + Coinbase: common.HexToAddress("0xcoinbase").Bytes(), + Root: hash3.Bytes(), + TxHash: hash4.Bytes(), + ReceiptHash: hash5.Bytes(), + Difficulty: big.NewInt(100).Bytes(), + Number: 1, + GasLimit: 1000000, + GasUsed: 500000, + Time: timestamppb.New(time.Unix(123456789, 0)), + Extra: []byte("extra"), + MixDigest: hash6.Bytes(), + Bloom: gethtypes.Bloom{}.Bytes(), + }, + }, + }, + { + name: "block with two uncles", + blkUncles: []*gethtypes.Header{ + { + Number: big.NewInt(1), + ParentHash: hash1, + UncleHash: hash2, + Coinbase: common.HexToAddress("0xcoinbase"), + Root: hash3, + TxHash: hash4, + ReceiptHash: hash5, + Difficulty: big.NewInt(10), + GasLimit: 1000000, + GasUsed: 500000, + Time: 123456789, + Extra: []byte("extra"), + MixDigest: hash6, + Bloom: gethtypes.Bloom{}, + }, + { + Number: big.NewInt(1), + ParentHash: hash2, + UncleHash: hash3, + Coinbase: common.HexToAddress("0xcoinbase"), + Root: hash4, + TxHash: hash5, + ReceiptHash: hash6, + Difficulty: big.NewInt(100), + GasLimit: 1000000, + GasUsed: 500000, + Time: 987654321, + Extra: []byte("extra"), + MixDigest: hash1, + Bloom: gethtypes.Bloom{}, + }, + }, + expected: []*types.Eth1Block{ + { + Hash: hash2.Bytes(), + ParentHash: hash1.Bytes(), + UncleHash: hash2.Bytes(), + Coinbase: common.HexToAddress("0xcoinbase").Bytes(), + Root: hash3.Bytes(), + TxHash: hash4.Bytes(), + ReceiptHash: hash5.Bytes(), + Difficulty: big.NewInt(10).Bytes(), + Number: 1, + GasLimit: 1000000, + GasUsed: 500000, + Time: timestamppb.New(time.Unix(123456789, 0)), + Extra: []byte("extra"), + MixDigest: hash6.Bytes(), + Bloom: gethtypes.Bloom{}.Bytes(), + }, + { + Hash: hash3.Bytes(), + ParentHash: hash2.Bytes(), + UncleHash: hash3.Bytes(), + Coinbase: common.HexToAddress("0xcoinbase").Bytes(), + Root: hash4.Bytes(), + TxHash: hash5.Bytes(), + ReceiptHash: hash6.Bytes(), + Difficulty: big.NewInt(100).Bytes(), + Number: 1, + GasLimit: 1000000, + GasUsed: 500000, + Time: timestamppb.New(time.Unix(987654321, 0)), + Extra: []byte("extra"), + MixDigest: hash1.Bytes(), + Bloom: gethtypes.Bloom{}.Bytes(), + }, + }, + }, + { + name: "block with no uncles", + blkUncles: []*gethtypes.Header{}, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getBlockUncles(tt.blkUncles) + if len(result) != len(tt.expected) { + t.Fatalf("got %v uncles, want %v uncles", len(result), len(tt.expected)) + } + for i, uncle := range result { + if !bytes.Equal(uncle.ParentHash, tt.expected[i].ParentHash) { + t.Errorf("got ParentHash %v, want %v", uncle.ParentHash, tt.expected[i].ParentHash) + } + if !bytes.Equal(uncle.UncleHash, tt.expected[i].UncleHash) { + t.Errorf("got Hash %v, want %v", uncle.UncleHash, tt.expected[i].UncleHash) + } + if !bytes.Equal(uncle.Coinbase, tt.expected[i].Coinbase) { + t.Errorf("got Coinbase %v, want %v", uncle.Coinbase, tt.expected[i].Coinbase) + } + if !bytes.Equal(uncle.Root, tt.expected[i].Root) { + t.Errorf("got Root %v, want %v", uncle.Root, tt.expected[i].Root) + } + if !bytes.Equal(uncle.TxHash, tt.expected[i].TxHash) { + t.Errorf("got TxHash %v, want %v", uncle.TxHash, tt.expected[i].TxHash) + } + if !bytes.Equal(uncle.ReceiptHash, tt.expected[i].ReceiptHash) { + t.Errorf("got ReceiptHash %v, want %v", uncle.ReceiptHash, tt.expected[i].ReceiptHash) + } + if !bytes.Equal(uncle.Difficulty, tt.expected[i].Difficulty) { + t.Errorf("got Difficulty %v, want %v", uncle.Difficulty, tt.expected[i].Difficulty) + } + if uncle.Number != tt.expected[i].Number { + t.Errorf("got Number %v, want %v", uncle.Number, tt.expected[i].Number) + } + if uncle.GasLimit != tt.expected[i].GasLimit { + t.Errorf("got GasLimit %v, want %v", uncle.GasLimit, tt.expected[i].GasLimit) + } + if uncle.GasUsed != tt.expected[i].GasUsed { + t.Errorf("got GasUsed %v, want %v", uncle.GasUsed, tt.expected[i].GasUsed) + } + if !uncle.Time.AsTime().Equal(tt.expected[i].Time.AsTime()) { + t.Errorf("got Time %v, want %v", uncle.Time.AsTime(), tt.expected[i].Time.AsTime()) + } + if !bytes.Equal(uncle.Extra, tt.expected[i].Extra) { + t.Errorf("got Extra %v, want %v", uncle.Extra, tt.expected[i].Extra) + } + if !bytes.Equal(uncle.MixDigest, tt.expected[i].MixDigest) { + t.Errorf("got MixDigest %v, want %v", uncle.MixDigest, tt.expected[i].MixDigest) + } + if !bytes.Equal(uncle.Bloom, tt.expected[i].Bloom) { + t.Errorf("got Bloom %v, want %v", uncle.Bloom, tt.expected[i].Bloom) + } + } + }) + } +} + +// TestGetERC20ContractSymbol tests the getERC20ContractSymbol function which extracts +// the symbol of an ERC20 contract +func TestGetERC20ContractSymbol(t *testing.T) { + backend := th.NewBackend(t) + ret := &types.ERC20Metadata{} + + contractAddress, _ := backend.DeployERC20(t, "usdt", "USDT", backend.BankAccount.From) + contractMetadata, err := contracts.NewIERC20Metadata(contractAddress, backend.Client()) + if err != nil { + t.Fatalf("could not create contract: %v", err) + } + + test := []struct { + name string + err error + expected string + expectedErr error + }{ + { + name: "valid symbol", + err: nil, + expected: "USDT", + expectedErr: nil, + }, + } + + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + err := getERC20ContractSymbol(contractMetadata, ret) + if err != nil { + if tt.expectedErr == nil { + t.Fatalf("unexpected error: %v", err) + } + } + if err == nil { + if tt.expectedErr != nil { + t.Fatalf("expected error: %v, got nil", tt.expectedErr) + } + } + if ret.Symbol != tt.expected { + t.Errorf("got Symbol %v, want %v", ret.Symbol, tt.expected) + } + }) + } + + contractAddress2, _ := backend.DeployERC20(t, "usdt", "", backend.BankAccount.From) + contractMetadata2, err := contracts.NewIERC20Metadata(contractAddress2, backend.Client()) + if err != nil { + t.Fatalf("could not create contract: %v", err) + } + + test2 := []struct { + name string + err error + expected string + expectedErr error + }{ + { + name: "empty symbol", + err: nil, + expected: "", + expectedErr: nil, + }, + } + + for _, tt := range test2 { + t.Run(tt.name, func(t *testing.T) { + err := getERC20ContractSymbol(contractMetadata2, ret) + if err != nil { + if tt.expectedErr == nil { + t.Fatalf("unexpected error: %v", err) + } + } + if err != nil { + if tt.expectedErr != nil { + t.Fatalf("expected error: %v, got nil", tt.expectedErr) + } + } + if ret.Symbol != tt.expected { + t.Errorf("got Symbol %v, want %v", ret.Symbol, tt.expected) + } + }) + } +} + +// TestGetERC20ContractTotalSupply tests the getERC20ContractTotalSupply function which +// extracts the total supply of an ERC20 contract +func TestGetERC20ContractTotalSupply(t *testing.T) { + backend := th.NewBackend(t) + ret := &types.ERC20Metadata{} + + contractAddress, _ := backend.DeployERC20(t, "usdt", "USDT", backend.BankAccount.From) + contractMetadata, err := contracts.NewIERC20Metadata(contractAddress, backend.Client()) + if err != nil { + t.Fatalf("could not create contract: %v", err) + } + + tests := []struct { + name string + err error + expected []byte + expectedErr error + }{ + { + name: "valid total supply", + err: nil, + expected: big.NewInt(100).Bytes(), + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := getERC20ContractTotalSupply(contractMetadata, ret) + if err != nil { + if tt.expectedErr == nil { + t.Fatalf("unexpected error: %v", err) + } + } + if err == nil && tt.expectedErr != nil { + t.Fatalf("expected error: %v, got nil", tt.expectedErr) + } + if !bytes.Equal(ret.TotalSupply, tt.expected) { + t.Errorf("got TotalSupply %v, want %v", ret.TotalSupply, tt.expected) + } + }) + } +} + +// TestGetERC20ContractDecimals tests the getERC20ContractDecimals function which +// extracts the decimals of an ERC20 contract +func TestGetERC20ContractDecimals(t *testing.T) { + backend := th.NewBackend(t) + ret := &types.ERC20Metadata{} + + contractAddress, _ := backend.DeployERC20(t, "usdt", "USDT", backend.BankAccount.From) + contractMetadata, err := contracts.NewIERC20Metadata(contractAddress, backend.Client()) + if err != nil { + t.Fatalf("could not create contract: %v", err) + } + + tests := []struct { + name string + err error + expected []byte + expectedErr error + }{ + { + name: "valid decimals", + err: nil, + expected: big.NewInt(18).Bytes(), + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := getERC20ContractDecimals(contractMetadata, ret) + if err != nil { + if tt.expectedErr == nil { + t.Fatalf("unexpected error: %v", err) + } + } + if err != nil { + if tt.expectedErr != nil { + t.Fatalf("expected error: %v, got nil", tt.expectedErr) + } + } + if !bytes.Equal(ret.Decimals, tt.expected) { + t.Errorf("got Decimals %v, want %v", ret.Decimals, tt.expected) + } + }) + } +} diff --git a/backend/pkg/executionlayer/transformer.go b/backend/pkg/executionlayer/transformer.go index 3f99fa14a..8d4adaea4 100644 --- a/backend/pkg/executionlayer/transformer.go +++ b/backend/pkg/executionlayer/transformer.go @@ -48,16 +48,8 @@ var AllTransformers = maps.Values(Transformers) func TransformTx(chainID string, block *types.Eth1Block, res *IndexedBlock) error { var transactions []*types.Eth1TransactionIndexed for _, tx := range block.Transactions { - to := tx.GetTo() - isContract := false - if tx.GetContractAddress() != nil && !bytes.Equal(tx.GetContractAddress(), common.Address{}.Bytes()) { - to = tx.GetContractAddress() - isContract = true - } - method := make([]byte, 0) - if len(tx.GetData()) > 3 { - method = tx.GetData()[:4] - } + to, isContract := getTxRecipient(tx) + method := getMethodSignature(tx) fee := new(big.Int).Mul(new(big.Int).SetBytes(tx.GetGasPrice()), big.NewInt(int64(tx.GetGasUsed()))).Bytes() blobFee := new(big.Int).Mul(new(big.Int).SetBytes(tx.GetBlobGasPrice()), big.NewInt(int64(tx.GetBlobGasUsed()))).Bytes() @@ -77,15 +69,8 @@ func TransformTx(chainID string, block *types.Eth1Block, res *IndexedBlock) erro BlobGasPrice: tx.GetBlobGasPrice(), Status: types.StatusType(tx.Status), } - for _, itx := range tx.Itx { - if itx.ErrorMsg != "" { - indexedTx.ErrorMsg = itx.ErrorMsg - if indexedTx.Status == types.StatusType_SUCCESS { - indexedTx.Status = types.StatusType_PARTIAL - } - break - } - } + + updateITxStatus(indexedTx, tx.Itx) transactions = append(transactions, indexedTx) } res.Transactions = transactions @@ -100,15 +85,11 @@ func TransformERC20(chainID string, block *types.Eth1Block, res *IndexedBlock) e var transfers []data.TransferWithIndexes for txIndex, tx := range block.GetTransactions() { for logIndex, log := range tx.GetLogs() { - if len(log.GetTopics()) != 3 || !bytes.Equal(log.GetTopics()[0], erc20.TransferTopic.Bytes()) { + if !isValidERC20Log(log) { continue } - topics := make([]common.Hash, 0, len(log.GetTopics())) - - for _, lTopic := range log.GetTopics() { - topics = append(topics, common.BytesToHash(lTopic)) - } + topics := getLogTopics(log) ethLog := gethtypes.Log{ Address: common.BytesToAddress(log.GetAddress()), @@ -127,10 +108,7 @@ func TransformERC20(chainID string, block *types.Eth1Block, res *IndexedBlock) e continue } - var value []byte - if transfer.Value != nil { - value = transfer.Value.Bytes() - } + value := getERC20TransferValue(transfer) indexedLog := &types.Eth1ERC20Indexed{ ParentHash: tx.GetHash(), @@ -173,24 +151,9 @@ func TransformBlock(chainID string, block *types.Eth1Block, res *IndexedBlock) e ExcessBlobGas: block.GetExcessBlobGas(), } - uncleReward := big.NewInt(0) - r := new(big.Int) + blockUncleReward := calculateBlockUncleReward(block, chainID) - for _, uncle := range block.Uncles { - if len(block.Difficulty) == 0 { // no uncle rewards in PoS - continue - } - - r.Add(big.NewInt(int64(uncle.GetNumber())), big.NewInt(8)) - r.Sub(r, big.NewInt(int64(block.GetNumber()))) - r.Mul(r, eth1BlockReward(chainID, block.GetNumber(), block.Difficulty)) - r.Div(r, big.NewInt(8)) - - r.Div(eth1BlockReward(chainID, block.GetNumber(), block.Difficulty), big.NewInt(32)) - uncleReward.Add(uncleReward, r) - } - - idx.UncleReward = uncleReward.Bytes() + idx.UncleReward = blockUncleReward.Bytes() var maxGasPrice *big.Int var minGasPrice *big.Int @@ -214,30 +177,18 @@ func TransformBlock(chainID string, block *types.Eth1Block, res *IndexedBlock) e minGasPrice = price } - txFee := new(big.Int).Mul(new(big.Int).SetBytes(t.GasPrice), big.NewInt(int64(t.GasUsed))) - - if len(block.BaseFee) > 0 { - effectiveGasPrice := math.BigMin(new(big.Int).Add(new(big.Int).SetBytes(t.MaxPriorityFeePerGas), new(big.Int).SetBytes(block.BaseFee)), new(big.Int).SetBytes(t.MaxFeePerGas)) - proposerGasPricePart := new(big.Int).Sub(effectiveGasPrice, new(big.Int).SetBytes(block.BaseFee)) - - if proposerGasPricePart.Cmp(big.NewInt(0)) >= 0 { - txFee = new(big.Int).Mul(proposerGasPricePart, big.NewInt(int64(t.GasUsed))) - } else { - log.Error(fmt.Errorf("error minerGasPricePart is below 0 for tx %v: %v", t.Hash, proposerGasPricePart), "", 0) - txFee = big.NewInt(0) - } - } + txFee := calculateTxFee(t, block.BaseFee) txReward.Add(txReward, txFee) for _, itx := range t.Itx { - if itx.Path == "[]" || itx.Path == "0" || bytes.Equal(itx.Value, []byte{0x0}) { // skip top level call & empty calls + if !isValidItx(itx) { continue } idx.InternalTransactionCount++ } - if t.GetType() == gethtypes.BlobTxType { + if isBlobTx(t.GetType()) { idx.BlobTransactionCount++ } } @@ -259,10 +210,11 @@ func TransformBlock(chainID string, block *types.Eth1Block, res *IndexedBlock) e func TransformBlob(chainID string, block *types.Eth1Block, res *IndexedBlock) error { var blobs []data.BlobWithIndex for i, tx := range block.Transactions { - if tx.Type != gethtypes.BlobTxType { + if !isBlobTx(tx.Type) { // skip non blob-txs continue } + fee := new(big.Int).Mul(new(big.Int).SetBytes(tx.GetGasPrice()), big.NewInt(int64(tx.GetGasUsed()))).Bytes() blobFee := new(big.Int).Mul(new(big.Int).SetBytes(tx.GetBlobGasPrice()), big.NewInt(int64(tx.GetBlobGasUsed()))).Bytes() indexedTx := &types.Eth1BlobTransactionIndexed{ @@ -298,10 +250,7 @@ func TransformContract(chainID string, block *types.Eth1Block, res *IndexedBlock // also use success status of enclosing transaction, as even successful sub-calls can still be reverted later in the tx Success: itx.GetErrorMsg() == "" && tx.GetErrorMsg() == "", } - address := itx.GetTo() - if itx.GetType() == "suicide" { - address = itx.GetFrom() - } + address := getContractAddress(itx) contracts = append(contracts, metadataupdates.ContractUpdateWithAddress{ Indexed: contractUpdate, @@ -320,7 +269,7 @@ func TransformITx(chainID string, block *types.Eth1Block, res *IndexedBlock) err var transactions []data.InternalWithIndexes for i, tx := range block.GetTransactions() { for j, itx := range tx.GetItx() { - if itx.Path == "0" || itx.Path == "[]" || bytes.Equal(itx.Value, []byte{0x0}) { // skip top level and empty calls + if !isValidItx(itx) { continue } @@ -356,15 +305,11 @@ func TransformERC1155(chainID string, block *types.Eth1Block, res *IndexedBlock) var transfers []data.ERC1155TransferWithIndexes for txIndex, tx := range block.GetTransactions() { for logIndex, log := range tx.GetLogs() { - if len(log.GetTopics()) != 4 || (!bytes.Equal(log.GetTopics()[0], erc1155.TransferBulkTopic.Bytes()) && !bytes.Equal(log.GetTopics()[0], erc1155.TransferSingleTopic.Bytes())) { + if !isValidERC1155Log(log) { continue } - topics := make([]common.Hash, 0, len(log.GetTopics())) - - for _, lTopic := range log.GetTopics() { - topics = append(topics, common.BytesToHash(lTopic)) - } + topics := getLogTopics(log) ethLog := gethtypes.Log{ Address: common.BytesToAddress(log.GetAddress()), @@ -385,15 +330,8 @@ func TransformERC1155(chainID string, block *types.Eth1Block, res *IndexedBlock) } indexedLog := &types.ETh1ERC1155Indexed{} if transferBatch != nil { - ids := make([][]byte, 0, len(transferBatch.Ids)) - for _, id := range transferBatch.Ids { - ids = append(ids, id.Bytes()) - } - - values := make([][]byte, 0, len(transferBatch.Values)) - for _, val := range transferBatch.Values { - values = append(values, val.Bytes()) - } + ids := getERC1155TransferIDs(transferBatch.Ids) + values := getERC1155TransferValues(transferBatch.Values) // TODO - Tangui this is probably a bug, only the last transfer will be saved for ti := range ids { @@ -437,15 +375,11 @@ func TransformERC721(chainID string, block *types.Eth1Block, res *IndexedBlock) var transfers []data.ERC721TransferWithIndexes for txIndex, tx := range block.GetTransactions() { for logIndex, log := range tx.GetLogs() { - if len(log.GetTopics()) != 4 || !bytes.Equal(log.GetTopics()[0], erc721.TransferTopic.Bytes()) { + if !isValidERC721Log(log) { continue } - topics := make([]common.Hash, 0, len(log.GetTopics())) - - for _, lTopic := range log.GetTopics() { - topics = append(topics, common.BytesToHash(lTopic)) - } + topics := getLogTopics(log) ethLog := gethtypes.Log{ Address: common.BytesToAddress(log.GetAddress()), @@ -464,10 +398,7 @@ func TransformERC721(chainID string, block *types.Eth1Block, res *IndexedBlock) continue } - tokenId := new(big.Int) - if transfer.TokenId != nil { - tokenId = transfer.TokenId - } + tokenId := getTokenID(transfer) indexedLog := &types.Eth1ERC721Indexed{ ParentHash: tx.GetHash(), @@ -492,16 +423,7 @@ func TransformERC721(chainID string, block *types.Eth1Block, res *IndexedBlock) func TransformUncle(chainID string, block *types.Eth1Block, res *IndexedBlock) error { var uncles []data.UncleWithIndexes for i, uncle := range block.Uncles { - r := new(big.Int) - - if len(block.Difficulty) > 0 { - r.Add(big.NewInt(int64(uncle.GetNumber())), big.NewInt(8)) - r.Sub(r, big.NewInt(int64(block.GetNumber()))) - r.Mul(r, eth1BlockReward(chainID, block.GetNumber(), block.Difficulty)) - r.Div(r, big.NewInt(8)) - - r.Div(eth1BlockReward(chainID, block.GetNumber(), block.Difficulty), big.NewInt(32)) - } + reward := calculateUncleReward(block, uncle, chainID) uncleIndexed := types.Eth1UncleIndexed{ Number: uncle.GetNumber(), @@ -511,7 +433,7 @@ func TransformUncle(chainID string, block *types.Eth1Block, res *IndexedBlock) e BaseFee: uncle.GetBaseFee(), Difficulty: uncle.GetDifficulty(), Time: uncle.GetTime(), - Reward: r.Bytes(), + Reward: reward.Bytes(), } uncles = append(uncles, data.UncleWithIndexes{ Indexed: &uncleIndexed, @@ -546,15 +468,11 @@ func TransformEnsNameRegistered(chainID string, block *types.Eth1Block, res *Ind for i, tx := range block.GetTransactions() { for j, txLog := range tx.GetLogs() { ensContract := ensContractAddresses[common.BytesToAddress(txLog.Address).String()] + topics := getLogTopics(txLog) - topics := txLog.GetTopics() - ethTopics := make([]common.Hash, 0, len(topics)) - for _, t := range topics { - ethTopics = append(ethTopics, common.BytesToHash(t)) - } ethLog := gethtypes.Log{ Address: common.BytesToAddress(txLog.GetAddress()), - Topics: ethTopics, + Topics: topics, Data: txLog.Data, BlockNumber: block.GetNumber(), TxHash: common.BytesToHash(tx.GetHash()), @@ -564,7 +482,7 @@ func TransformEnsNameRegistered(chainID string, block *types.Eth1Block, res *Ind Removed: txLog.GetRemoved(), } var ensLog data.ENSLog - for _, lTopic := range topics { + for _, lTopic := range txLog.GetTopics() { switch ensContract { case "Registry": filterer, _ := ens.NewENSRegistryFilterer(common.Address{}, nil) @@ -706,3 +624,170 @@ func calculateMevFromBlock(block *types.Eth1Block) *big.Int { } return mevReward } + +func isValidERC20Log(log *types.Eth1Log) bool { + return len(log.GetTopics()) == 3 && bytes.Equal(log.GetTopics()[0], erc20.TransferTopic.Bytes()) +} + +func isValidERC721Log(log *types.Eth1Log) bool { + return len(log.GetTopics()) == 4 && bytes.Equal(log.GetTopics()[0], erc721.TransferTopic.Bytes()) +} + +func isValidERC1155Log(log *types.Eth1Log) bool { + if len(log.GetTopics()) != 4 { + return false + } + + // check if first topic matches TransferSingle or TransferBatch topic + firstTopic := log.GetTopics()[0] + isTransferBulk := bytes.Equal(firstTopic, erc1155.TransferBulkTopic.Bytes()) + isTransferSingle := bytes.Equal(firstTopic, erc1155.TransferSingleTopic.Bytes()) + + return isTransferBulk || isTransferSingle +} + +func isBlobTx(txType uint32) bool { + return txType == gethtypes.BlobTxType +} + +func isValidItx(itx *types.Eth1InternalTransaction) bool { + // skip top level and empty calls + if itx.Path == "[]" || itx.Path == "0" || bytes.Equal(itx.Value, []byte{0x0}) { + return false + } + return true +} + +func getLogTopics(log *types.Eth1Log) []common.Hash { + topics := make([]common.Hash, 0, len(log.GetTopics())) + for _, lTopic := range log.GetTopics() { + topics = append(topics, common.BytesToHash(lTopic)) + } + return topics +} + +func getTxRecipient(tx *types.Eth1Transaction) ([]byte, bool) { + to := tx.GetTo() + isContract := false + if tx.GetContractAddress() != nil && !bytes.Equal(tx.GetContractAddress(), common.Address{}.Bytes()) { + to = tx.GetContractAddress() + isContract = true + } + return to, isContract +} + +func getTokenID(transfer *contracts.ERC721Transfer) *big.Int { + tokenId := new(big.Int) + if transfer.TokenId != nil { + tokenId = transfer.TokenId + } + return tokenId +} + +// extracts the first 4 bytes of the data field for method signature +func getMethodSignature(tx *types.Eth1Transaction) []byte { + method := make([]byte, 0) + if len(tx.GetData()) > 3 { + method = tx.GetData()[:4] + } + return method +} + +func getContractAddress(itx *types.Eth1InternalTransaction) []byte { + address := itx.GetTo() + if itx.GetType() == "suicide" { + address = itx.GetFrom() + } + return address +} + +func getERC20TransferValue(transfer *contracts.ERC20Transfer) []byte { + var value []byte + if transfer.Value != nil { + value = transfer.Value.Bytes() + } + return value +} + +func getERC1155TransferIDs(idList []*big.Int) [][]byte { + ids := make([][]byte, 0, len(idList)) + for _, id := range idList { + ids = append(ids, id.Bytes()) + } + return ids +} + +func getERC1155TransferValues(values []*big.Int) [][]byte { + v := make([][]byte, 0, len(values)) + for _, val := range values { + v = append(v, val.Bytes()) + } + return v +} + +func updateITxStatus(indexedTx *types.Eth1TransactionIndexed, internals []*types.Eth1InternalTransaction) { + for _, itx := range internals { + if itx.ErrorMsg != "" { + indexedTx.ErrorMsg = itx.ErrorMsg + if indexedTx.Status == types.StatusType_SUCCESS { + indexedTx.Status = types.StatusType_PARTIAL + } + break + } + } +} + +// calculates tx fee and priority fee +func calculateTxFee(t *types.Eth1Transaction, baseFee []byte) *big.Int { + txFee := new(big.Int).Mul(new(big.Int).SetBytes(t.GasPrice), big.NewInt(int64(t.GasUsed))) + + if len(baseFee) > 0 { + effectiveGasPrice := math.BigMin(new(big.Int).Add(new(big.Int).SetBytes(t.MaxPriorityFeePerGas), new(big.Int).SetBytes(baseFee)), new(big.Int).SetBytes(t.MaxFeePerGas)) + proposerGasPricePart := new(big.Int).Sub(effectiveGasPrice, new(big.Int).SetBytes(baseFee)) + + if proposerGasPricePart.Cmp(big.NewInt(0)) >= 0 { + txFee = new(big.Int).Mul(proposerGasPricePart, big.NewInt(int64(t.GasUsed))) + } else { + log.Error(fmt.Errorf("error minerGasPricePart is below 0 for tx %v: %v", t.Hash, proposerGasPricePart), "", 0) + txFee = big.NewInt(0) + } + } + return txFee +} + +// calculates the total value of uncle rewards for a block +func calculateBlockUncleReward(block *types.Eth1Block, chainID string) *big.Int { + uncleRewards := big.NewInt(0) + reward := new(big.Int) + + for _, uncle := range block.Uncles { + if len(block.Difficulty) == 0 { // no uncle rewards in PoS + return uncleRewards // return 0 + } + + reward.Add(big.NewInt(int64(uncle.GetNumber())), big.NewInt(8)) + reward.Sub(reward, big.NewInt(int64(block.GetNumber()))) + reward.Mul(reward, eth1BlockReward(chainID, block.GetNumber(), block.Difficulty)) + reward.Div(reward, big.NewInt(8)) + + reward.Div(eth1BlockReward(chainID, block.GetNumber(), block.Difficulty), big.NewInt(32)) + uncleRewards.Add(uncleRewards, reward) + } + + return uncleRewards +} + +// calculates the reward for a single uncle block +func calculateUncleReward(block, uncle *types.Eth1Block, chainID string) *big.Int { + reward := new(big.Int) + + if len(block.Difficulty) > 0 { + reward.Add(big.NewInt(int64(uncle.GetNumber())), big.NewInt(8)) + reward.Sub(reward, big.NewInt(int64(block.GetNumber()))) + reward.Mul(reward, eth1BlockReward(chainID, block.GetNumber(), block.Difficulty)) + reward.Div(reward, big.NewInt(8)) + + reward.Div(eth1BlockReward(chainID, block.GetNumber(), block.Difficulty), big.NewInt(32)) + } + return reward +} diff --git a/backend/pkg/executionlayer/transformer_test.go b/backend/pkg/executionlayer/transformer_test.go index 955af3f30..a55222077 100644 --- a/backend/pkg/executionlayer/transformer_test.go +++ b/backend/pkg/executionlayer/transformer_test.go @@ -2,6 +2,7 @@ package executionlayer import ( "bytes" + "fmt" "math/big" "reflect" "testing" @@ -9,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/gobitfly/beaconchain/internal/contracts" "github.com/gobitfly/beaconchain/pkg/commons/chain" "github.com/gobitfly/beaconchain/pkg/commons/contracts/ens" "github.com/gobitfly/beaconchain/pkg/commons/db2/data" @@ -24,6 +26,7 @@ var ( aliceAddress = common.BytesToAddress(leftPad(alice, 20)) bob = []byte("bob") bobAddress = common.BytesToAddress(leftPad(bob, 20)) + john = []byte("john") contract = []byte("contract") usdc = []byte("usdc") ) @@ -1211,3 +1214,1012 @@ func TestTransformer_FromList(t *testing.T) { }) } } + +// TestIsValidERC20Log tests the isValidERC20Log function to verify +// if the log is a valid ERC20 transfer log +func TestIsValidERC20Log(t *testing.T) { + tests := []struct { + name string + log *types.Eth1Log + expected bool + }{ + { + name: "valid ERC20 transfer log", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc20.TransferTopic.Bytes(), + alice, + bob, + }, + }, + expected: true, + }, + { + name: "invalid ERC20 transfer log with incorrect event topic", + log: &types.Eth1Log{ + Topics: [][]byte{ + common.HexToHash("0x0123").Bytes(), + alice, + bob, + }, + }, + expected: false, + }, + { + name: "invalid ERC20 transfer log with too little topics", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc20.TransferTopic.Bytes(), + alice, + }, + }, + expected: false, + }, + { + name: "invalid ERC20 transfer log with too many topics", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc20.TransferTopic.Bytes(), + alice, + bob, + john, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isValidERC20Log(tt.log) + if result != tt.expected { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestIsValidERC721Log tests the isValidERC721Log function to verify +// if the log is a valid ERC721 transfer log +func TestIsValidERC721Log(t *testing.T) { + tests := []struct { + name string + log *types.Eth1Log + expected bool + }{ + { + name: "valid ERC721 transfer log", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc721.TransferTopic.Bytes(), + alice, + bob, + []byte("tokenID"), + }, + }, + expected: true, + }, + { + name: "invalid ERC721 transfer log with incorrect topic", + log: &types.Eth1Log{ + Topics: [][]byte{ + common.HexToHash("0x1234").Bytes(), + alice, + bob, + []byte("tokenID"), + }, + }, + expected: false, + }, + { + name: "invalid ERC721 transfer log with too little topics", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc721.TransferTopic.Bytes(), + alice, + bob, + }, + }, + expected: false, + }, + { + name: "invalid ERC721 transfer log with too many topics", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc721.TransferTopic.Bytes(), + alice, + bob, + []byte("tokenID"), + john, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isValidERC721Log(tt.log) + if result != tt.expected { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestIsValidERC1155Log tests the isValidERC1155Log function to verify +// if the log is a valid ERC1155 transfer log +func TestIsValidERC1155Log(t *testing.T) { + tests := []struct { + name string + log *types.Eth1Log + expected bool + }{ + { + name: "valid TransferSingleTopic log", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc1155.TransferSingleTopic.Bytes(), + alice, + bob, + john, + }, + }, + expected: true, + }, + { + name: "valid TransferBulkTopic log", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc1155.TransferBulkTopic.Bytes(), + alice, + bob, + john, + }, + }, + expected: true, + }, + { + name: "invalid log with invalid topic", + log: &types.Eth1Log{ + Topics: [][]byte{ + common.HexToHash("0x1234").Bytes(), + alice, + bob, + john, + }, + }, + expected: false, + }, + { + name: "invalid TransferSingleTopic log with too little topics", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc1155.TransferSingleTopic.Bytes(), + alice, + bob, + }, + }, + expected: false, + }, + { + name: "invalid TransferBulkTopic log with too little topics", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc1155.TransferBulkTopic.Bytes(), + alice, + bob, + }, + }, + expected: false, + }, + { + name: "invalid TransferSingleTopic log with too many topics", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc1155.TransferSingleTopic.Bytes(), + alice, + bob, + john, + contract, + }, + }, + expected: false, + }, + { + name: "invalid TransferBulkTopic log with too many topics", + log: &types.Eth1Log{ + Topics: [][]byte{ + erc1155.TransferBulkTopic.Bytes(), + alice, + bob, + john, + contract, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isValidERC1155Log(tt.log) + if result != tt.expected { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestIsValidItx tests the isValidItx function to verify +// if the internal transactions are valid and can be indexed +func TestIsValidItx(t *testing.T) { + tests := []struct { + name string + itx *types.Eth1InternalTransaction + expected bool + }{ + { + name: "valid internal transaction", + itx: &types.Eth1InternalTransaction{ + Path: "0,1", + Value: big.NewInt(100).Bytes(), + }, + expected: true, + }, + { + name: "invalid internal transaction with path '0'", + itx: &types.Eth1InternalTransaction{ + Path: "0", + Value: big.NewInt(100).Bytes(), + }, + expected: false, + }, + { + name: "invalid internal transaction with path is '[]'", + itx: &types.Eth1InternalTransaction{ + Path: "[]", + Value: big.NewInt(100).Bytes(), + }, + expected: false, + }, + { + name: "invalid internal transaction with value 0", + itx: &types.Eth1InternalTransaction{ + Path: "0,1", + Value: []byte{0x0}, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isValidItx(tt.itx) + if result != tt.expected { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestGetLogTopics tests the getLogTopics function to verify +// if the function correctly extracts the topics from the log +func TestGetLogTopics(t *testing.T) { + topic1 := "0x1234" + topic2 := "0x2345" + topic3 := "0x3456" + tests := []struct { + name string + log *types.Eth1Log + expected []common.Hash + }{ + { + name: "log with topics", + log: &types.Eth1Log{ + Topics: [][]byte{ + common.HexToHash(topic1).Bytes(), + common.HexToHash(topic2).Bytes(), + common.HexToHash(topic3).Bytes(), + }, + }, + expected: []common.Hash{ + common.HexToHash(topic1), + common.HexToHash(topic2), + common.HexToHash(topic3), + }, + }, + { + name: "log with no topics", + log: &types.Eth1Log{ + Topics: [][]byte{}, + }, + expected: []common.Hash{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getLogTopics(tt.log) + if len(result) != len(tt.expected) { + t.Errorf("got %v topics, want %v topics", len(result), len(tt.expected)) + } + for i := range result { + if result[i] != tt.expected[i] { + t.Errorf("got topic %v, want %v", result[i], tt.expected[i]) + } + } + }) + } +} + +// TestGetTxRecipient tests the getTxRecipient function to verify +// if the function correctly extracts the recipients from the transaction +func TestGetTxRecipient(t *testing.T) { + tests := []struct { + name string + tx *types.Eth1Transaction + expectedTo []byte + expectedIsContract bool + }{ + { + name: "normal transaction with recipient", + tx: &types.Eth1Transaction{ + To: alice, + ContractAddress: common.Address{}.Bytes(), + }, + expectedTo: alice, + expectedIsContract: false, + }, + { + name: "contract creation transaction", + tx: &types.Eth1Transaction{ + ContractAddress: contract, + }, + expectedTo: contract, + expectedIsContract: true, + }, + { + name: "transaction with no recipient", + tx: &types.Eth1Transaction{ + To: common.Address{}.Bytes(), + ContractAddress: common.Address{}.Bytes(), + }, + expectedTo: common.Address{}.Bytes(), + expectedIsContract: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + to, isContract := getTxRecipient(tt.tx) + if !bytes.Equal(to, tt.expectedTo) { + t.Errorf("got %v, want %v", to, tt.expectedTo) + } + if isContract != tt.expectedIsContract { + t.Errorf("got %v, want %v", isContract, tt.expectedIsContract) + } + }) + } +} + +// TestGetTokenID tests the getTokenID function to verify +// if the function correctly extracts the token ID from the transfer +func TestGetTokenID(t *testing.T) { + tests := []struct { + name string + transfer *contracts.ERC721Transfer + expected *big.Int + }{ + { + name: "transfer with token ID", + transfer: &contracts.ERC721Transfer{ + TokenId: big.NewInt(123), + }, + expected: big.NewInt(123), + }, + { + name: "transfer with empty token ID", + transfer: &contracts.ERC721Transfer{ + TokenId: nil, + }, + expected: big.NewInt(0), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getTokenID(tt.transfer) + if result.Cmp(tt.expected) != 0 { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestGetMethodsSignature tests the getMethodSignature function to verify +// if the function correctly extracts the method signature from the transaction +func TestGetMethodSignature(t *testing.T) { + tests := []struct { + name string + tx *types.Eth1Transaction + expected []byte + }{ + { + name: "transaction with data longer than 4 bytes", + tx: &types.Eth1Transaction{ + Data: []byte("123456789"), + }, + expected: []byte("1234"), + }, + { + name: "transaction with data exactly 4 bytes", + tx: &types.Eth1Transaction{ + Data: []byte("1234"), + }, + expected: []byte("1234"), + }, + { + name: "transaction with data shorter than 4 bytes", + tx: &types.Eth1Transaction{ + Data: []byte("12"), + }, + expected: []byte{}, + }, + { + name: "transaction with no data", + tx: &types.Eth1Transaction{ + Data: []byte{}, + }, + expected: []byte{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + method := getMethodSignature(tt.tx) + if !bytes.Equal(method, tt.expected) { + t.Errorf("got %v, want %v", method, tt.expected) + } + }) + } +} + +// TestGetContractAddress tests the getContractAddress function to verify +// if the function correctly extracts the contract address from the internal transaction +func TestGetContractAddress(t *testing.T) { + tests := []struct { + name string + itx *types.Eth1InternalTransaction + expected []byte + }{ + { + name: "create type transaction", + itx: &types.Eth1InternalTransaction{ + Type: "create", + From: alice, + To: contract, + }, + expected: contract, + }, + { + name: "suicide type transaction", + itx: &types.Eth1InternalTransaction{ + Type: "suicide", + From: contract, + To: alice, + }, + expected: contract, + }, + { + name: "invalid type transaction", + itx: &types.Eth1InternalTransaction{ + Type: "invalid", + From: alice, + To: contract, + }, + expected: contract, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getContractAddress(tt.itx) + if !bytes.Equal(result, tt.expected) { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestGetERC20TransferValue tests getERC20TransferValue function to verify +// if the function correctly extracts the ERC20 transfer value from the ERC20 transfer contract +func TestGetERC20TransferValue(t *testing.T) { + tests := []struct { + name string + transfer *contracts.ERC20Transfer + expected []byte + }{ + { + name: "transfer with value", + transfer: &contracts.ERC20Transfer{ + Value: big.NewInt(1), + }, + expected: big.NewInt(1).Bytes(), + }, + { + name: "transfer with empty value", + transfer: &contracts.ERC20Transfer{ + Value: nil, + }, + expected: []byte{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getERC20TransferValue(tt.transfer) + if !bytes.Equal(result, tt.expected) { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestGetERC1155TransferIDs tests getERC1155TransferIDs function to verify +// if the function correctly extracts the ERC1155 transfer IDs from the ID list +func TestGetERC1155TransferIDs(t *testing.T) { + tests := []struct { + name string + idList []*big.Int + expected [][]byte + }{ + { + name: "single ID", + idList: []*big.Int{ + big.NewInt(1), + }, + expected: [][]byte{ + big.NewInt(1).Bytes(), + }, + }, + { + name: "multiple IDs", + idList: []*big.Int{ + big.NewInt(1), + big.NewInt(2), + big.NewInt(3), + }, + expected: [][]byte{ + big.NewInt(1).Bytes(), + big.NewInt(2).Bytes(), + big.NewInt(3).Bytes(), + }, + }, + { + name: "empty ID list", + idList: []*big.Int{}, + expected: [][]byte{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getERC1155TransferIDs(tt.idList) + if len(result) != len(tt.expected) { + t.Errorf("got %v IDs, want %v IDs", len(result), len(tt.expected)) + } + for i := range result { + if !bytes.Equal(result[i], tt.expected[i]) { + t.Errorf("got ID %v, want %v", result[i], tt.expected[i]) + } + } + }) + } +} + +// TestGetERC1155TransferValues tests getERC1155TransferValues function to verify +// if the function correctly extracts the ERC1155 transfer values from the value list +func TestGetERC1155TransferValues(t *testing.T) { + tests := []struct { + name string + values []*big.Int + expected [][]byte + }{ + { + name: "single value", + values: []*big.Int{ + big.NewInt(1), + }, + expected: [][]byte{ + big.NewInt(1).Bytes(), + }, + }, + { + name: "multiple values", + values: []*big.Int{ + big.NewInt(1), + big.NewInt(2), + big.NewInt(3), + }, + expected: [][]byte{ + big.NewInt(1).Bytes(), + big.NewInt(2).Bytes(), + big.NewInt(3).Bytes(), + }, + }, + { + name: "empty values list", + values: []*big.Int{}, + expected: [][]byte{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getERC1155TransferValues(tt.values) + if len(result) != len(tt.expected) { + t.Errorf("got %v values, want %v values", len(result), len(tt.expected)) + } + for i := range result { + if !bytes.Equal(result[i], tt.expected[i]) { + t.Errorf("got value %v, want %v", result[i], tt.expected[i]) + } + } + }) + } +} + +// TestUpdateITxStatus tests the updateITxStatus function to verify +// if the function correctly updates the status of the internal transaction +func TestUpdateITxStatus(t *testing.T) { + tests := []struct { + name string + internalTx []*types.Eth1InternalTransaction + indexedTx *types.Eth1TransactionIndexed + expectedStatus types.StatusType + expectedErrorMsg string + }{ + { + name: "no internal transactions", + internalTx: []*types.Eth1InternalTransaction{}, + indexedTx: &types.Eth1TransactionIndexed{ + Status: types.StatusType_SUCCESS, + }, + expectedStatus: types.StatusType_SUCCESS, + expectedErrorMsg: "", + }, + { + name: "internal transaction with error", + internalTx: []*types.Eth1InternalTransaction{ + { + ErrorMsg: "fail", + }, + }, + indexedTx: &types.Eth1TransactionIndexed{ + Status: types.StatusType_SUCCESS, + }, + expectedStatus: types.StatusType_PARTIAL, + expectedErrorMsg: "fail", + }, + { + name: "internal transaction with error and status failed", + internalTx: []*types.Eth1InternalTransaction{ + { + ErrorMsg: "fail", + }, + }, + indexedTx: &types.Eth1TransactionIndexed{ + Status: types.StatusType_FAILED, + }, + expectedStatus: types.StatusType_FAILED, + expectedErrorMsg: "fail", + }, + { + name: "multiple internal transactions, one with error", + internalTx: []*types.Eth1InternalTransaction{ + { + ErrorMsg: "", + }, + { + ErrorMsg: "fail", + }, + }, + indexedTx: &types.Eth1TransactionIndexed{ + Status: types.StatusType_SUCCESS, + }, + expectedStatus: types.StatusType_PARTIAL, + expectedErrorMsg: "fail", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + updateITxStatus(tt.indexedTx, tt.internalTx) + if tt.indexedTx.Status != tt.expectedStatus { + t.Errorf("got status %v, want %v", tt.indexedTx.Status, tt.expectedStatus) + } + if tt.indexedTx.ErrorMsg != tt.expectedErrorMsg { + t.Errorf("got error message %v, want %v", tt.indexedTx.ErrorMsg, tt.expectedErrorMsg) + } + }) + } +} + +// TestCalculateTxFee tests the calculateTxFee function to verify +// if the function correctly calculates the transaction fee +func TestCalculateTxFee(t *testing.T) { + tests := []struct { + name string + tx *types.Eth1Transaction + baseFee []byte + expected *big.Int + }{ + { + name: "transaction with no base fee", + tx: &types.Eth1Transaction{ + GasPrice: big.NewInt(10).Bytes(), + GasUsed: 1000, + }, + baseFee: []byte{}, + expected: big.NewInt(10 * 1000), + }, + { + name: "transaction with priority and base fee", + tx: &types.Eth1Transaction{ + GasPrice: big.NewInt(100).Bytes(), + MaxPriorityFeePerGas: big.NewInt(10).Bytes(), + MaxFeePerGas: big.NewInt(200).Bytes(), + GasUsed: 1000, + }, + baseFee: big.NewInt(50).Bytes(), + expected: big.NewInt(10 * 1000), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculateTxFee(tt.tx, tt.baseFee) + if result.Cmp(tt.expected) != 0 { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestCalculateMevFromBlock tests the calculateMevFromBlock function to verify +// if the function correctly calculates the MEV from the block +func TestCalculateMevFromBlock(t *testing.T) { + tests := []struct { + name string + block *types.Eth1Block + expected *big.Int + }{ + { + name: "no MEV", + block: &types.Eth1Block{ + Coinbase: []byte("coinbase"), + Transactions: []*types.Eth1Transaction{ + { + Itx: []*types.Eth1InternalTransaction{ + { + From: alice, + To: common.Address{}.Bytes(), + Value: big.NewInt(100).Bytes(), + }, + }, + }, + }, + }, + expected: big.NewInt(0), + }, + { + name: "MEV from one transaction", + block: &types.Eth1Block{ + Coinbase: []byte("coinbase"), + Transactions: []*types.Eth1Transaction{ + { + Itx: []*types.Eth1InternalTransaction{ + { + From: alice, + To: []byte("coinbase"), + Value: big.NewInt(100).Bytes(), + }, + }, + }, + }, + }, + expected: big.NewInt(100), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculateMevFromBlock(tt.block) + if result.Cmp(tt.expected) != 0 { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestCalculateBlockUncleReward tests the calculateBlockUncleReward function to verify +// if the function correctly calculates the uncle reward from the block +func TestCalculateBlockUncleReward(t *testing.T) { + tests := []struct { + name string + block *types.Eth1Block + chainID string + expected *big.Int + }{ + { + name: "no uncles", + block: &types.Eth1Block{ + Uncles: []*types.Eth1Block{}, + }, + chainID: "1", + expected: big.NewInt(0), + }, + { + name: "one uncle", + block: &types.Eth1Block{ + Number: 10, + Difficulty: big.NewInt(100).Bytes(), + Uncles: []*types.Eth1Block{ + { + Number: 1, + }, + }, + }, + chainID: "1", + expected: new(big.Int).Div(eth1BlockReward("1", 10, big.NewInt(100).Bytes()), big.NewInt(32)), + }, + { + name: "two uncles", + block: &types.Eth1Block{ + Number: 10, + Difficulty: big.NewInt(100).Bytes(), + Uncles: []*types.Eth1Block{ + { + Number: 1, + }, + { + Number: 2, + }, + }, + }, + chainID: "1", + expected: new(big.Int).Mul(big.NewInt(2), new(big.Int).Div(eth1BlockReward("1", 10, big.NewInt(100).Bytes()), big.NewInt(32))), + }, + { + name: "no uncle rewards", + block: &types.Eth1Block{ + Number: 10, + Difficulty: []byte{}, + Uncles: []*types.Eth1Block{ + { + Number: 1, + }, + }, + }, + chainID: "1", + expected: big.NewInt(0), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculateBlockUncleReward(tt.block, tt.chainID) + if result.Cmp(tt.expected) != 0 { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestCalculateUncleReward tests the calculateUncleReward function to verify +// if the function correctly calculates the single uncle reward +func TestCalculateUncleReward(t *testing.T) { + tests := []struct { + name string + block *types.Eth1Block + uncle *types.Eth1Block + chainID string + expected *big.Int + }{ + { + name: "no uncles", + block: &types.Eth1Block{ + Uncles: []*types.Eth1Block{}, + }, + chainID: "1", + expected: big.NewInt(0), + }, + { + name: "one uncle", + block: &types.Eth1Block{ + Number: 10, + Difficulty: big.NewInt(100).Bytes(), + }, + uncle: &types.Eth1Block{ + Number: 1, + }, + chainID: "1", + expected: new(big.Int).Div(eth1BlockReward("1", 10, big.NewInt(100).Bytes()), big.NewInt(32)), + }, + { + name: "no uncle rewards", + block: &types.Eth1Block{ + Number: 10, + Difficulty: []byte{}, + }, + uncle: &types.Eth1Block{ + Number: 1, + }, + chainID: "1", + expected: big.NewInt(0), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculateUncleReward(tt.block, tt.uncle, tt.chainID) + if result.Cmp(tt.expected) != 0 { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} + +// TestVerifyName tests the verifyName function to verify +// if the function correctly validates the name based on the length +func TestVerifyName(t *testing.T) { + tests := []struct { + name string + input string + expected error + }{ + { + name: "valid name", + input: "test", + expected: nil, + }, + { + name: "empty name", + input: "", + expected: nil, + }, + { + name: "maximum length name", + input: string(make([]byte, 2048)), + expected: nil, + }, + { + name: "name too long", + input: string(make([]byte, 2049)), + expected: fmt.Errorf("name too long: %v", string(make([]byte, 2049))), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := verifyName(tt.input) + + if result != nil { + if tt.expected == nil { + t.Errorf("got %v, want %v", result, tt.expected) + } + if tt.expected != nil { + if result.Error() != tt.expected.Error() { + t.Errorf("got %v, want %v", result, tt.expected) + } + } + } + if result == nil { + if tt.expected != nil { + t.Errorf("got %v, want %v", result, tt.expected) + } + } + }) + } +}