Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eth_estimateGas CIP-64 and CIP-66 compatibility #91

Merged
merged 6 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions common/exchange/rates.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ func ConvertCurrencyToGold(exchangeRates common.ExchangeRates, currencyAmount *b
}
exchangeRate, ok := exchangeRates[*feeCurrency]
if !ok {
return nil, ErrNonWhitelistedFeeCurrency
if !ok {
return nil, fmt.Errorf("could not convert to native from fee currency (fee-currency=%s): %w ", feeCurrency, ErrNonWhitelistedFeeCurrency)
}
}
return new(big.Int).Div(new(big.Int).Mul(currencyAmount, exchangeRate.Denom()), exchangeRate.Num()), nil
}
Expand All @@ -46,7 +48,7 @@ func ConvertGoldToCurrency(exchangeRates common.ExchangeRates, feeCurrency *comm
}
exchangeRate, ok := exchangeRates[*feeCurrency]
if !ok {
return nil, ErrNonWhitelistedFeeCurrency
return nil, fmt.Errorf("could not convert from native to fee currency (fee-currency=%s): %w ", feeCurrency, ErrNonWhitelistedFeeCurrency)
}
return new(big.Int).Div(new(big.Int).Mul(goldAmount, exchangeRate.Num()), exchangeRate.Denom()), nil
}
Expand Down
19 changes: 18 additions & 1 deletion e2e_test/js-tests/test_viem_tx.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe("viem send tx", () => {
);

// viem's getGasPrice does not expose additional request parameters,
// but Celo's override 'chain.fees.estimateFeesPerGas' action does.
// but Celo's override 'chain.fees.estimateFeesPerGas' action does.
// this will call the eth_gasPrice and eth_maxPriorityFeePerGas methods
// with the additional feeCurrency parameter internally
var fees = await publicClient.estimateFeesPerGas({
Expand All @@ -181,6 +181,23 @@ describe("viem send tx", () => {
assert.equal(request.maxPriorityFeePerGas, fees.maxPriorityFeePerGas);
}).timeout(10_000);

it("send fee currency with gas estimation tx and check receipt", async () => {
const request = await walletClient.prepareTransactionRequest({
account,
to: "0x00000000000000000000000000000000DeaDBeef",
value: 2,
feeCurrency: process.env.FEE_CURRENCY,
maxFeePerGas: 2000000000n,
maxPriorityFeePerGas: 0n,
});
const signature = await walletClient.signTransaction(request);
const hash = await walletClient.sendRawTransaction({
serializedTransaction: signature,
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
assert.equal(receipt.status, "success", "receipt status 'failure'");
}).timeout(10_000);

it("send overlapping nonce tx in different currencies", async () => {
const priceBump = 1.1;
const rate = 2;
Expand Down
5 changes: 3 additions & 2 deletions eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,8 @@ func makeExtraData(extra []byte) []byte {
// APIs return the collection of RPC services the ethereum package offers.
// NOTE, some of these services probably need to be moved to somewhere else.
func (s *Ethereum) APIs() []rpc.API {
apis := ethapi.GetAPIs(s.APIBackend)
celoBackend := celoapi.NewCeloAPIBackend(s.APIBackend)
apis := ethapi.GetAPIs(celoBackend)

// Append any APIs exposed explicitly by the consensus engine
apis = append(apis, s.engine.APIs(s.BlockChain())...)
Expand Down Expand Up @@ -391,7 +392,7 @@ func (s *Ethereum) APIs() []rpc.API {
// on the eth namespace, this will overwrite the original procedures.
{
Namespace: "eth",
Service: celoapi.NewCeloAPI(s, s.APIBackend),
Service: celoapi.NewCeloAPI(s, celoBackend),
},
}...)
}
Expand Down
2 changes: 1 addition & 1 deletion eth/tracers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
config.BlockOverrides.Apply(&vmctx)
}
// Execute the trace
msg, err := args.ToMessage(api.backend.RPCGasCap(), block.BaseFee())
msg, err := args.ToMessage(api.backend.RPCGasCap(), block.BaseFee(), vmctx.ExchangeRates)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion graphql/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -1282,7 +1282,7 @@ func (p *Pending) EstimateGas(ctx context.Context, args struct {

// Resolver is the top-level object in the GraphQL hierarchy.
type Resolver struct {
backend ethapi.Backend
backend ethapi.CeloBackend
filterSystem *filters.FilterSystem
}

Expand Down
4 changes: 3 additions & 1 deletion graphql/graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/filters"
"github.com/ethereum/go-ethereum/internal/celoapi"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"

Expand Down Expand Up @@ -477,7 +478,8 @@ func newGQLService(t *testing.T, stack *node.Node, shanghai bool, gspec *core.Ge
}
// Set up handler
filterSystem := filters.NewFilterSystem(ethBackend.APIBackend, filters.Config{})
handler, err := newHandler(stack, ethBackend.APIBackend, filterSystem, []string{}, []string{})
celoBackend := celoapi.NewCeloAPIBackend(ethBackend.APIBackend)
handler, err := newHandler(stack, celoBackend, filterSystem, []string{}, []string{})
if err != nil {
t.Fatalf("could not create graphql service: %v", err)
}
Expand Down
6 changes: 4 additions & 2 deletions graphql/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"time"

"github.com/ethereum/go-ethereum/eth/filters"
"github.com/ethereum/go-ethereum/internal/celoapi"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
Expand Down Expand Up @@ -107,13 +108,14 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

// New constructs a new GraphQL service instance.
func New(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterSystem, cors, vhosts []string) error {
_, err := newHandler(stack, backend, filterSystem, cors, vhosts)
celoBackend := celoapi.NewCeloAPIBackend(backend)
_, err := newHandler(stack, celoBackend, filterSystem, cors, vhosts)
return err
}

// newHandler returns a new `http.Handler` that will answer GraphQL queries.
// It additionally exports an interactive query browser on the / endpoint.
func newHandler(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterSystem, cors, vhosts []string) (*handler, error) {
func newHandler(stack *node.Node, backend ethapi.CeloBackend, filterSystem *filters.FilterSystem, cors, vhosts []string) (*handler, error) {
q := Resolver{backend, filterSystem}

s, err := graphql.ParseSchema(schema, &q)
Expand Down
97 changes: 97 additions & 0 deletions internal/celoapi/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package celoapi

import (
"context"
"fmt"
"math/big"

"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/contracts"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/internal/ethapi"
)

type Ethereum interface {
BlockChain() *core.BlockChain
}

type CeloAPI struct {
ethAPI *ethapi.EthereumAPI
eth Ethereum
}

func NewCeloAPI(e Ethereum, b ethapi.CeloBackend) *CeloAPI {
return &CeloAPI{
ethAPI: ethapi.NewEthereumAPI(b),
eth: e,
}
}

func (c *CeloAPI) convertedCurrencyValue(v *hexutil.Big, feeCurrency *common.Address) (*hexutil.Big, error) {
if feeCurrency != nil {
convertedTipCap, err := c.convertGoldToCurrency(v.ToInt(), feeCurrency)
if err != nil {
return nil, fmt.Errorf("convert to feeCurrency: %w", err)
}
v = (*hexutil.Big)(convertedTipCap)
}
return v, nil
}

func (c *CeloAPI) celoBackendCurrentState() (*contracts.CeloBackend, error) {
state, err := c.eth.BlockChain().State()
if err != nil {
return nil, fmt.Errorf("retrieve HEAD blockchain state': %w", err)
}

cb := &contracts.CeloBackend{
ChainConfig: c.eth.BlockChain().Config(),
State: state,
}
return cb, nil
}

func (c *CeloAPI) convertGoldToCurrency(nativePrice *big.Int, feeCurrency *common.Address) (*big.Int, error) {
cb, err := c.celoBackendCurrentState()
if err != nil {
return nil, err
}
er, err := contracts.GetExchangeRates(cb)
if err != nil {
return nil, fmt.Errorf("retrieve exchange rates from current state: %w", err)
}
return exchange.ConvertGoldToCurrency(er, feeCurrency, nativePrice)
}

// GasPrice wraps the original JSON RPC `eth_gasPrice` and adds an additional
// optional parameter `feeCurrency` for fee-currency conversion.
// When `feeCurrency` is not given, then the original JSON RPC method is called without conversion.
func (c *CeloAPI) GasPrice(ctx context.Context, feeCurrency *common.Address) (*hexutil.Big, error) {
tipcap, err := c.ethAPI.GasPrice(ctx)
if err != nil {
return nil, err
}
// Between the call to `ethapi.GasPrice` and the call to fetch and convert the rates,
// there is a chance of a state-change. This means that gas-price suggestion is calculated
// based on state of block x, while the currency conversion could be calculated based on block
// x+1.
// However, a similar race condition is present in the `ethapi.GasPrice` method itself.
return c.convertedCurrencyValue(tipcap, feeCurrency)
}

// MaxPriorityFeePerGas wraps the original JSON RPC `eth_maxPriorityFeePerGas` and adds an additional
// optional parameter `feeCurrency` for fee-currency conversion.
// When `feeCurrency` is not given, then the original JSON RPC method is called without conversion.
func (c *CeloAPI) MaxPriorityFeePerGas(ctx context.Context, feeCurrency *common.Address) (*hexutil.Big, error) {
tipcap, err := c.ethAPI.MaxPriorityFeePerGas(ctx)
if err != nil {
return nil, err
}
// Between the call to `ethapi.MaxPriorityFeePerGas` and the call to fetch and convert the rates,
// there is a chance of a state-change. This means that gas-price suggestion is calculated
// based on state of block x, while the currency conversion could be calculated based on block
// x+1.
return c.convertedCurrencyValue(tipcap, feeCurrency)
}
106 changes: 51 additions & 55 deletions internal/celoapi/backend.go
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff here looks a bit confusing - basically I just moved the CeloAPI from the celoapi/backend.go to the celoapi/api.go file, and then added the new CeloBackend implementation in the celoapi/backend.go file.

Original file line number Diff line number Diff line change
Expand Up @@ -7,91 +7,87 @@ import (

"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/common/lru"
"github.com/ethereum/go-ethereum/contracts"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/rpc"
)

type Ethereum interface {
BlockChain() *core.BlockChain
func NewCeloAPIBackend(b ethapi.Backend) *CeloAPIBackend {
return &CeloAPIBackend{
Backend: b,
exchangeRatesCache: lru.NewCache[common.Hash, common.ExchangeRates](128),
}
}

type CeloAPI struct {
ethAPI *ethapi.EthereumAPI
eth Ethereum
}
// CeloAPIBackend is a wrapper for the ethapi.Backend, that provides additional Celo specific
// functionality. CeloAPIBackend is mainly passed to the JSON RPC services and provides
// an easy way to make extra functionality available in the service internal methods without
// having to change their call signature significantly.
// CeloAPIBackend keeps a threadsafe LRU cache of block-hash to exchange rates for that block.
// Cache invalidation is only a problem when an already existing blocks' hash
// doesn't change, but the rates change. That shouldn't be possible, since changing the rates
// requires different transaction hashes / state and thus a different block hash.
// If the previous rates change during a reorg, the previous block hash should also change
// and with it the new block's hash.
// Stale branches cache values will get evicted eventually.
ezdac marked this conversation as resolved.
Show resolved Hide resolved
type CeloAPIBackend struct {
ethapi.Backend

func NewCeloAPI(e Ethereum, b ethapi.Backend) *CeloAPI {
return &CeloAPI{
ethAPI: ethapi.NewEthereumAPI(b),
eth: e,
}
exchangeRatesCache *lru.Cache[common.Hash, common.ExchangeRates]
}

func (c *CeloAPI) convertedCurrencyValue(v *hexutil.Big, feeCurrency *common.Address) (*hexutil.Big, error) {
if feeCurrency != nil {
convertedTipCap, err := c.convertGoldToCurrency(v.ToInt(), feeCurrency)
if err != nil {
return nil, fmt.Errorf("convert to feeCurrency: %w", err)
}
v = (*hexutil.Big)(convertedTipCap)
func (b *CeloAPIBackend) getContractCaller(ctx context.Context, atBlock common.Hash) (*contracts.CeloBackend, error) {
state, _, err := b.Backend.StateAndHeaderByNumberOrHash(
ctx,
rpc.BlockNumberOrHashWithHash(atBlock, false),
)
if err != nil {
return nil, fmt.Errorf("retrieve state for block hash %s: %w", atBlock.String(), err)
}
return v, nil
return &contracts.CeloBackend{
ChainConfig: b.Backend.ChainConfig(),
State: state,
}, nil
}

func (c *CeloAPI) celoBackendCurrentState() (*contracts.CeloBackend, error) {
state, err := c.eth.BlockChain().State()
func (b *CeloAPIBackend) GetFeeBalance(ctx context.Context, atBlock common.Hash, account common.Address, feeCurrency *common.Address) (*big.Int, error) {
cb, err := b.getContractCaller(ctx, atBlock)
if err != nil {
return nil, fmt.Errorf("retrieve HEAD blockchain state': %w", err)
}

cb := &contracts.CeloBackend{
ChainConfig: c.eth.BlockChain().Config(),
State: state,
return nil, err
}
return cb, nil
return contracts.GetFeeBalance(cb, account, feeCurrency), nil
}

func (c *CeloAPI) convertGoldToCurrency(nativePrice *big.Int, feeCurrency *common.Address) (*big.Int, error) {
cb, err := c.celoBackendCurrentState()
func (b *CeloAPIBackend) GetExchangeRates(ctx context.Context, atBlock common.Hash) (common.ExchangeRates, error) {
cachedRates, ok := b.exchangeRatesCache.Get(atBlock)
if ok {
return cachedRates, nil
}
cb, err := b.getContractCaller(ctx, atBlock)
if err != nil {
return nil, err
}
er, err := contracts.GetExchangeRates(cb)
if err != nil {
return nil, fmt.Errorf("retrieve exchange rates from current state: %w", err)
return nil, err
}
return exchange.ConvertGoldToCurrency(er, feeCurrency, nativePrice)
b.exchangeRatesCache.Add(atBlock, er)
return er, nil
}

// GasPrice wraps the original JSON RPC `eth_gasPrice` and adds an additional
// optional parameter `feeCurrency` for fee-currency conversion.
// When `feeCurrency` is not given, then the original JSON RPC method is called without conversion.
func (c *CeloAPI) GasPrice(ctx context.Context, feeCurrency *common.Address) (*hexutil.Big, error) {
tipcap, err := c.ethAPI.GasPrice(ctx)
func (b *CeloAPIBackend) ConvertToCurrency(ctx context.Context, atBlock common.Hash, value *big.Int, fromFeeCurrency *common.Address) (*big.Int, error) {
er, err := b.GetExchangeRates(ctx, atBlock)
if err != nil {
return nil, err
}
// Between the call to `ethapi.GasPrice` and the call to fetch and convert the rates,
// there is a chance of a state-change. This means that gas-price suggestion is calculated
// based on state of block x, while the currency conversion could be calculated based on block
// x+1.
// However, a similar race condition is present in the `ethapi.GasPrice` method itself.
return c.convertedCurrencyValue(tipcap, feeCurrency)
return exchange.ConvertGoldToCurrency(er, fromFeeCurrency, value)
}

// MaxPriorityFeePerGas wraps the original JSON RPC `eth_maxPriorityFeePerGas` and adds an additional
// optional parameter `feeCurrency` for fee-currency conversion.
// When `feeCurrency` is not given, then the original JSON RPC method is called without conversion.
func (c *CeloAPI) MaxPriorityFeePerGas(ctx context.Context, feeCurrency *common.Address) (*hexutil.Big, error) {
tipcap, err := c.ethAPI.MaxPriorityFeePerGas(ctx)
func (b *CeloAPIBackend) ConvertToGold(ctx context.Context, atBlock common.Hash, value *big.Int, toFeeCurrency *common.Address) (*big.Int, error) {
er, err := b.GetExchangeRates(ctx, atBlock)
if err != nil {
return nil, err
}
// Between the call to `ethapi.MaxPriorityFeePerGas` and the call to fetch and convert the rates,
// there is a chance of a state-change. This means that gas-price suggestion is calculated
// based on state of block x, while the currency conversion could be calculated based on block
// x+1.
return c.convertedCurrencyValue(tipcap, feeCurrency)
return exchange.ConvertCurrencyToGold(er, value, toFeeCurrency)
}
Loading
Loading