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 authored Apr 17, 2024
1 parent 15699d3 commit 937f609
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 48 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"
}
}
67 changes: 37 additions & 30 deletions e2e_test/js-tests/send_tx.mjs
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
#!/usr/bin/env node
import { createPublicClient, createWalletClient, http, defineChain } from 'viem'
import { celoAlfajores } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import {
createPublicClient,
createWalletClient,
http,
defineChain,
} from "viem";
import { celoAlfajores } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

const [chainId, privateKey, feeCurrency] = process.argv.slice(2)
const [chainId, privateKey, feeCurrency] = process.argv.slice(2);
const devChain = defineChain({
...celoAlfajores,
id: parseInt(chainId, 10),
name: 'local dev chain',
network: 'dev',
rpcUrls: {
default: {
http: ['http://127.0.0.1:8545'],
},
},
})
...celoAlfajores,
id: parseInt(chainId, 10),
name: "local dev chain",
network: "dev",
rpcUrls: {
default: {
http: ["http://127.0.0.1:8545"],
},
},
});

const account = privateKeyToAccount(privateKey)
const account = privateKeyToAccount(privateKey);
const walletClient = createWalletClient({
account,
chain: devChain,
transport: http(),
})
account,
chain: devChain,
transport: http(),
});

const request = await walletClient.prepareTransactionRequest({
account,
to: '0x00000000000000000000000000000000DeaDBeef',
value: 2,
gas: 90000,
feeCurrency,
maxFeePerGas: 2000000000n,
maxPriorityFeePerGas: 0n,
})
const signature = await walletClient.signTransaction(request)
const hash = await walletClient.sendRawTransaction({ serializedTransaction: signature })
console.log(hash)
account,
to: "0x00000000000000000000000000000000DeaDBeef",
value: 2,
gas: 90000,
feeCurrency,
maxFeePerGas: 2000000000n,
maxPriorityFeePerGas: 0n,
});
const signature = await walletClient.signTransaction(request);
const hash = await walletClient.sendRawTransaction({
serializedTransaction: signature,
});
console.log(hash);
45 changes: 42 additions & 3 deletions e2e_test/js-tests/test_viem_tx.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe("viem send tx", () => {
serializedTransaction: signature,
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
assert.equal(receipt.status, 'success', "receipt status 'failure'")
assert.equal(receipt.status, "success", "receipt status 'failure'");
}).timeout(10_000);

it("send tx with gas estimation and check receipt", async () => {
Expand All @@ -121,7 +121,7 @@ describe("viem send tx", () => {
serializedTransaction: signature,
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
assert.equal(receipt.status, 'success', "receipt status 'failure'")
assert.equal(receipt.status, "success", "receipt status 'failure'");
}).timeout(10_000);

it("send fee currency tx and check receipt", async () => {
Expand All @@ -139,7 +139,46 @@ describe("viem send tx", () => {
serializedTransaction: signature,
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
assert.equal(receipt.status, 'success', "receipt status 'failure'")
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 () => {
Expand Down
8 changes: 8 additions & 0 deletions eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/ethereum/go-ethereum/eth/protocols/snap"
"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 @@ -385,6 +386,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.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)
}

0 comments on commit 937f609

Please sign in to comment.