Skip to content

Commit

Permalink
Add fee currencies to gas price RPC API (#96)
Browse files Browse the repository at this point in the history
* Add fee currencies to gas price RPC API

* Fix typo

Co-authored-by: Karl Bartel <[email protected]>

* Format JS e2e tests

---------

Co-authored-by: Karl Bartel <[email protected]>
  • Loading branch information
ezdac and karlb committed Aug 20, 2024
1 parent 4c619fa commit cd67dcf
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 15 deletions.
30 changes: 15 additions & 15 deletions e2e_test/js-tests/package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "js-tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"chai": "^5.0.0",
"ethers": "^6.10.0",
"mocha": "^10.2.0",
"viem": "^2.9.6"
}
"name": "js-tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"chai": "^5.0.0",
"ethers": "^6.10.0",
"mocha": "^10.2.0",
"viem": "^2.9.6"
}
}
39 changes: 39 additions & 0 deletions e2e_test/js-tests/test_viem_tx.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,45 @@ describe("viem send tx", () => {
assert.equal(receipt.status, "success", "receipt status 'failure'");
}).timeout(10_000);

it("test gas price difference for fee currency", async () => {
const request = await walletClient.prepareTransactionRequest({
account,
to: "0x00000000000000000000000000000000DeaDBeef",
value: 2,
gas: 90000,
feeCurrency: process.env.FEE_CURRENCY,
});

const gasPriceNative = await publicClient.getGasPrice({});
var maxPriorityFeePerGasNative =
await publicClient.estimateMaxPriorityFeePerGas({});
const block = await publicClient.getBlock({});
assert.equal(
BigInt(block.baseFeePerGas) + maxPriorityFeePerGasNative,
gasPriceNative,
);

// viem's getGasPrice does not expose additional request parameters,
// 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({
type: "eip1559",
request: {
feeCurrency: process.env.FEE_CURRENCY,
},
});
// first check that the fee currency denominated gas price
// converts properly to the native gas price
assert.equal(fees.maxFeePerGas, gasPriceNative * 2n);
assert.equal(fees.maxPriorityFeePerGas, maxPriorityFeePerGasNative * 2n);

// check that the prepared transaction request uses the
// converted gas price internally
assert.equal(request.maxFeePerGas, fees.maxFeePerGas);
assert.equal(request.maxPriorityFeePerGas, fees.maxPriorityFeePerGas);
}).timeout(10_000);

it("send overlapping nonce tx in different currencies", async () => {
const priceBump = 1.1;
const rate = 2;
Expand Down
8 changes: 8 additions & 0 deletions eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
"github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/internal/celoapi"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/internal/shutdowncheck"
"github.com/ethereum/go-ethereum/log"
Expand Down Expand Up @@ -395,6 +396,13 @@ func (s *Ethereum) APIs() []rpc.API {
Namespace: "net",
Service: s.netRPCService,
},
// CELO specific API backend.
// For methods in the backend that are already defined (match by name)
// on the eth namespace, this will overwrite the original procedures.
{
Namespace: "eth",
Service: celoapi.NewCeloAPI(s, s.APIBackend),
},
}...)
}

Expand Down
97 changes: 97 additions & 0 deletions internal/celoapi/backend.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.Backend) *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.ConvertCeloToCurrency(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)
}

0 comments on commit cd67dcf

Please sign in to comment.