Skip to content

Commit

Permalink
Add test for ConvertTxFeeCapToCurrency
Browse files Browse the repository at this point in the history
  • Loading branch information
Kourin1996 committed Feb 5, 2025
1 parent 9a8da3a commit c4502d2
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 63 deletions.
102 changes: 47 additions & 55 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,11 +528,7 @@ func (api *PersonalAccountAPI) SignTransaction(ctx context.Context, args Transac
}
// Before actually signing the transaction, ensure the transaction fee is reasonable.
tx := args.ToTransaction(types.LegacyTxType)
txFeeCap, err := ConvertTxFeeCapToCurrency(ctx, api.b, tx.FeeCurrency())
if err != nil {
return nil, err
}
if err := checkTxFee(tx.GasPrice(), tx.Gas(), txFeeCap); err != nil {
if err := CheckTxFee(ctx, api.b, tx.GasPrice(), tx.Gas(), tx.FeeCurrency()); err != nil {
return nil, err
}
signed, err := api.signTransaction(ctx, &args, passwd)
Expand Down Expand Up @@ -2226,11 +2222,7 @@ func (api *TransactionAPI) sign(addr common.Address, tx *types.Transaction) (*ty
func SubmitTransaction(ctx context.Context, b CeloBackend, tx *types.Transaction) (common.Hash, error) {
// If the transaction fee cap is already specified, ensure the
// fee of the given transaction is _reasonable_.
txFeeCap, err := ConvertTxFeeCapToCurrency(ctx, b, tx.FeeCurrency())
if err != nil {
return common.Hash{}, err
}
if err := checkTxFee(tx.GasPrice(), tx.Gas(), txFeeCap); err != nil {
if err := CheckTxFee(ctx, b, tx.GasPrice(), tx.Gas(), tx.FeeCurrency()); err != nil {
return common.Hash{}, err
}
if !b.UnprotectedAllowed() && !tx.Protected() {
Expand Down Expand Up @@ -2375,11 +2367,7 @@ func (api *TransactionAPI) SignTransaction(ctx context.Context, args Transaction
}
// Before actually sign the transaction, ensure the transaction fee is reasonable.
tx := args.ToTransaction(types.LegacyTxType)
txFeeCap, err := ConvertTxFeeCapToCurrency(ctx, api.b, tx.FeeCurrency())
if err != nil {
return nil, err
}
if err := checkTxFee(tx.GasPrice(), tx.Gas(), txFeeCap); err != nil {
if err := CheckTxFee(ctx, api.b, tx.GasPrice(), tx.Gas(), tx.FeeCurrency()); err != nil {
return nil, err
}
signed, err := api.sign(args.from(), tx)
Expand Down Expand Up @@ -2447,11 +2435,7 @@ func (api *TransactionAPI) Resend(ctx context.Context, sendArgs TransactionArgs,
if gasLimit != nil {
gas = uint64(*gasLimit)
}
txFeeCap, err := ConvertTxFeeCapToCurrency(ctx, api.b, matchTx.FeeCurrency())
if err != nil {
return common.Hash{}, err
}
if err := checkTxFee(price, gas, txFeeCap); err != nil {
if err := CheckTxFee(ctx, api.b, price, gas, matchTx.FeeCurrency()); err != nil {
return common.Hash{}, err
}
// Iterate the pending list for replacement
Expand Down Expand Up @@ -2483,39 +2467,6 @@ func (api *TransactionAPI) Resend(ctx context.Context, sendArgs TransactionArgs,
return common.Hash{}, fmt.Errorf("transaction %#x not found", matchTx.Hash())
}

// ConvertTxFeeCapToCurrency converts FeeCap in native currency into the specified currency
// If no fee currency is specified, it returns the native currency FeeCap as is
func ConvertTxFeeCapToCurrency(ctx context.Context, backend CeloBackend, feeCurrency *common.Address) (float64, error) {
txFeeCap := backend.RPCTxFeeCap()
if feeCurrency == nil {
return txFeeCap, nil
}

rates, err := backend.GetExchangeRates(ctx, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
if err != nil {
log.Warn("Failed to get exchange rates", "current", backend.CurrentBlock().Number, "err", err)
return 0, err
}

// Convert txFeeCap (float64) to big.Int with proper precision
// Since ConvertCeloToCurrency expects big.Int, directly converting to big.Int would lose decimal precision
scaledTxFeeCap := big.NewInt(int64(txFeeCap * 1e18))
scaledTxFeeCapInCurrency, err := exchange.ConvertCeloToCurrency(rates, feeCurrency, scaledTxFeeCap)
if err != nil {
log.Warn("Failed to convert RPCTxFeeCap to the specified currency", "current", backend.CurrentBlock().Number, "currency", feeCurrency, "err", err)
return 0, err
}
txFeeCapInCurrency := scaledTxFeeCapInCurrency.Div(scaledTxFeeCapInCurrency, big.NewInt(1e18))

result, accuracy := txFeeCapInCurrency.Float64()
if accuracy != big.Exact {
log.Warn("Failed to accurately convert fee currency to float64", "fee cap", txFeeCapInCurrency.String(), "accuracy", accuracy.String())
return 0, fmt.Errorf("failed to accurately convert fee currency to float64")
}

return result, nil
}

// DebugAPI is the collection of Ethereum APIs exposed over the debugging
// namespace.
type DebugAPI struct {
Expand Down Expand Up @@ -2694,6 +2645,47 @@ func checkTxFee(gasPrice *big.Int, gas uint64, cap float64) error {
}

// CheckTxFee exports a helper function used to check whether the fee is reasonable
func CheckTxFee(gasPrice *big.Int, gas uint64, cap float64) error {
return checkTxFee(gasPrice, gas, cap)
func CheckTxFee(ctx context.Context, backend CeloBackend, gasPrice *big.Int, gas uint64, feeCurrency *common.Address) error {
feeCap, err := ConvertTxFeeCapToCurrency(ctx, backend, feeCurrency)
if err != nil {
return err
}

return checkTxFee(gasPrice, gas, feeCap)
}

// ConvertTxFeeCapToCurrency converts FeeCap in native currency into the specified currency
// If no fee currency is specified, it returns the native currency FeeCap as is
func ConvertTxFeeCapToCurrency(ctx context.Context, backend CeloBackend, feeCurrency *common.Address) (float64, error) {
txFeeCap := backend.RPCTxFeeCap()
if feeCurrency == nil {
return txFeeCap, nil
}

rates, err := backend.GetExchangeRates(ctx, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
if err != nil {
log.Warn("Failed to get exchange rates", "err", err)
return 0, err
}

// Convert txFeeCap (float64) to big.Int with proper precision
// Since ConvertCeloToCurrency expects big.Int, directly converting to big.Int would lose decimal precision
scaledTxFeeCap := big.NewInt(int64(txFeeCap * 1e18))
scaledTxFeeCapInCurrency, err := exchange.ConvertCeloToCurrency(rates, feeCurrency, scaledTxFeeCap)
if err != nil {
log.Warn("Failed to convert RPCTxFeeCap to the specified currency", "currency", feeCurrency, "err", err)
return 0, err
}
txFeeCapInCurrency := new(big.Float).Quo(
new(big.Float).SetInt(scaledTxFeeCapInCurrency),
new(big.Float).SetInt64(1e18),
)

result, accuracy := txFeeCapInCurrency.Float64()
if accuracy != big.Exact {
log.Warn("Failed to accurately convert fee currency to float64", "fee cap", txFeeCapInCurrency.String(), "accuracy", accuracy.String())
return 0, fmt.Errorf("failed to accurately convert fee currency to float64")
}

return result, nil
}
44 changes: 44 additions & 0 deletions internal/ethapi/celo_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/exchange"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/blocktest"
Expand Down Expand Up @@ -613,3 +615,45 @@ func TestRPCMarshalBlock_Celo1TotalDifficulty(t *testing.T) {
assert.Equal(t, nil, res["totalDifficulty"])
})
}

// TestConvertTxFeeCapToCurrency verifies the output is accurate for each currency
func TestConvertTxFeeCapToCurrency(t *testing.T) {
t.Parallel()

var (
txFeeCap = float64(1.5)
rates = common.ExchangeRates{
core.DevFeeCurrencyAddr: big.NewRat(2, 1),
core.DevFeeCurrencyAddr2: big.NewRat(1, 2),
}
config = allEnabledChainConfig()
)

backend := newCeloBackendMock(config)
backend.SetExchangeRates(rates)
backend.SetRPCTxFeeCap(txFeeCap)

t.Run("should return given fee cap when fee currency is not specified", func(t *testing.T) {
res, err := ConvertTxFeeCapToCurrency(context.Background(), backend, nil)
require.NoError(t, err)
assert.Equal(t, txFeeCap, res)
})

t.Run("should return the twice of given fee cap when a fee currency is specified", func(t *testing.T) {
res, err := ConvertTxFeeCapToCurrency(context.Background(), backend, &core.DevFeeCurrencyAddr)
require.NoError(t, err)
assert.Equal(t, 2*txFeeCap, res)
})

t.Run("should return the half of given fee cap when a fee currency is specified", func(t *testing.T) {
res, err := ConvertTxFeeCapToCurrency(context.Background(), backend, &core.DevFeeCurrencyAddr2)
require.NoError(t, err)
assert.Equal(t, txFeeCap/2, res)
})

t.Run("should return error when fee currency is not found in exchange rates", func(t *testing.T) {
res, err := ConvertTxFeeCapToCurrency(context.Background(), backend, &core.DevAddr)
require.Zero(t, res)
assert.ErrorIs(t, err, exchange.ErrUnregisteredFeeCurrency)
})
}
16 changes: 13 additions & 3 deletions internal/ethapi/transaction_args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,8 @@ type celoBackendMock struct {
chainDb ethdb.Database
blockByNumber map[int64]*types.Block
receiptsByHash map[common.Hash]types.Receipts
rates common.ExchangeRates
rpcTxFeeCap float64
}

func newCeloBackendMock(config *params.ChainConfig) *celoBackendMock {
Expand All @@ -329,15 +331,21 @@ func newCeloBackendMock(config *params.ChainConfig) *celoBackendMock {
}
}

func (c *celoBackendMock) SetExchangeRates(rates common.ExchangeRates) {
c.rates = rates
}

func (c *celoBackendMock) SetRPCTxFeeCap(txFeeCap float64) {
c.rpcTxFeeCap = txFeeCap
}

func (c *celoBackendMock) GetFeeBalance(ctx context.Context, blockNumOrHash rpc.BlockNumberOrHash, account common.Address, feeCurrency *common.Address) (*big.Int, error) {
// Celo specific backend features are currently not tested
return nil, errCeloNotImplemented
}

func (c *celoBackendMock) GetExchangeRates(ctx context.Context, blockNumOrHash rpc.BlockNumberOrHash) (common.ExchangeRates, error) {
var er common.ExchangeRates
// This Celo specific backend features are currently not tested
return er, errCeloNotImplemented
return c.rates, nil
}

func (c *celoBackendMock) ConvertToCurrency(ctx context.Context, blockNumOrHash rpc.BlockNumberOrHash, value *big.Int, fromFeeCurrency *common.Address) (*big.Int, error) {
Expand Down Expand Up @@ -374,6 +382,8 @@ func (c *celoBackendMock) setReceipts(hash common.Hash, receipts types.Receipts)
c.receiptsByHash[hash] = receipts
}

func (c *celoBackendMock) RPCTxFeeCap() float64 { return c.rpcTxFeeCap }

type backendMock struct {
current *types.Header
config *params.ChainConfig
Expand Down
6 changes: 1 addition & 5 deletions internal/sequencerapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,7 @@ func (s *sendRawTxCond) SendRawTransactionConditional(ctx context.Context, txByt
// forward if seqRPC is set, otherwise submit the tx
if s.seqRPC != nil {
// Some precondition checks done by `ethapi.SubmitTransaction` that are good to also check here
txFeeCap, err := ethapi.ConvertTxFeeCapToCurrency(ctx, s.b, tx.FeeCurrency())
if err != nil {
return common.Hash{}, err
}
if err := ethapi.CheckTxFee(tx.GasPrice(), tx.Gas(), txFeeCap); err != nil {
if err := ethapi.CheckTxFee(ctx, s.b, tx.GasPrice(), tx.Gas(), tx.FeeCurrency()); err != nil {
return common.Hash{}, err
}
if !s.b.UnprotectedAllowed() && !tx.Protected() {
Expand Down

0 comments on commit c4502d2

Please sign in to comment.