Skip to content

Commit c5db28a

Browse files
author
Daisuke Kanda
committed
impl replace price check and make ethclient be interface for testing
Signed-off-by: Daisuke Kanda <daisuke.kanda@datachain.jp>
1 parent d412a80 commit c5db28a

File tree

5 files changed

+262
-69
lines changed

5 files changed

+262
-69
lines changed

pkg/client/client.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,41 @@ import (
1616
"github.com/ethereum/go-ethereum/rpc"
1717
)
1818

19+
// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction
20+
type RPCTransaction struct {
21+
BlockHash *common.Hash `json:"blockHash"`
22+
BlockNumber *hexutil.Big `json:"blockNumber"`
23+
From common.Address `json:"from"`
24+
Gas hexutil.Uint64 `json:"gas"`
25+
GasPrice *hexutil.Big `json:"gasPrice"`
26+
GasFeeCap *hexutil.Big `json:"maxFeePerGas,omitempty"`
27+
GasTipCap *hexutil.Big `json:"maxPriorityFeePerGas,omitempty"`
28+
MaxFeePerBlobGas *hexutil.Big `json:"maxFeePerBlobGas,omitempty"`
29+
Hash common.Hash `json:"hash"`
30+
Input hexutil.Bytes `json:"input"`
31+
Nonce hexutil.Uint64 `json:"nonce"`
32+
To *common.Address `json:"to"`
33+
TransactionIndex *hexutil.Uint64 `json:"transactionIndex"`
34+
Value *hexutil.Big `json:"value"`
35+
Type hexutil.Uint64 `json:"type"`
36+
Accesses *gethtypes.AccessList `json:"accessList,omitempty"`
37+
ChainID *hexutil.Big `json:"chainId,omitempty"`
38+
BlobVersionedHashes []common.Hash `json:"blobVersionedHashes,omitempty"`
39+
V *hexutil.Big `json:"v"`
40+
R *hexutil.Big `json:"r"`
41+
S *hexutil.Big `json:"s"`
42+
YParity *hexutil.Uint64 `json:"yParity,omitempty"`
43+
}
44+
45+
// wrapping interface of ethclient.Client struct
46+
type IETHClient interface {
47+
Inner() *ethclient.Client
48+
SuggestGasPrice(ctx context.Context) (*big.Int, error)
49+
HeaderByNumber(ctx context.Context, number *big.Int) (*gethtypes.Header, error);
50+
FeeHistory(ctx context.Context, blockCount uint64, lastBlock *big.Int, rewardPercentiles []float64) (*ethereum.FeeHistory, error)
51+
ContentFrom(ctx context.Context, address common.Address) (map[string]map[string]*RPCTransaction, error);
52+
}
53+
1954
type ETHClient struct {
2055
*ethclient.Client
2156
option option
@@ -47,12 +82,16 @@ func NewETHClient(endpoint string, opts ...Option) (*ETHClient, error) {
4782
if err != nil {
4883
return nil, err
4984
}
85+
return NewETHClientWith(ethclient.NewClient(rpcClient), opts...)
86+
}
87+
88+
func NewETHClientWith(cli *ethclient.Client, opts ...Option) (*ETHClient, error) {
5089
opt := DefaultOption()
5190
for _, o := range opts {
5291
o(opt)
5392
}
5493
return &ETHClient{
55-
Client: ethclient.NewClient(rpcClient),
94+
Client: cli,
5695
option: *opt,
5796
}, nil
5897
}
@@ -61,6 +100,29 @@ func (cl *ETHClient) Raw() *rpc.Client {
61100
return cl.Client.Client()
62101
}
63102

103+
// implements IETHClient
104+
func (cl *ETHClient) Inner() *ethclient.Client {
105+
return cl.Client
106+
}
107+
func (cl *ETHClient) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
108+
return cl.Client.SuggestGasPrice(ctx)
109+
}
110+
func (cl *ETHClient) HeaderByNumber(ctx context.Context, number *big.Int) (*gethtypes.Header, error) {
111+
return cl.Client.HeaderByNumber(ctx, number)
112+
}
113+
func (cl *ETHClient) FeeHistory(ctx context.Context, blockCount uint64, lastBlock *big.Int, rewardPercentiles []float64) (*ethereum.FeeHistory, error) {
114+
return cl.Client.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles)
115+
}
116+
117+
// ContentFrom calls `txpool_contentFrom` of the Ethereum RPC
118+
func (cl *ETHClient) ContentFrom(ctx context.Context, address common.Address) (map[string]map[string]*RPCTransaction, error) {
119+
var res map[string]map[string]*RPCTransaction
120+
if err := cl.Client.Client().CallContext(ctx, &res, "txpool_contentFrom", address); err != nil {
121+
return nil, err
122+
}
123+
return res, nil
124+
}
125+
64126
func (cl *ETHClient) GetTransactionReceipt(ctx context.Context, txHash common.Hash) (rc *Receipt, err error) {
65127
var r *Receipt
66128

pkg/client/txpool/txpool.go

Lines changed: 14 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,13 @@ import (
55
"math/big"
66
"slices"
77

8+
"github.com/datachainlab/ethereum-ibc-relay-chain/pkg/client"
89
"github.com/ethereum/go-ethereum/common"
9-
"github.com/ethereum/go-ethereum/common/hexutil"
10-
"github.com/ethereum/go-ethereum/core/types"
11-
"github.com/ethereum/go-ethereum/ethclient"
1210
)
1311

14-
// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction
15-
type RPCTransaction struct {
16-
BlockHash *common.Hash `json:"blockHash"`
17-
BlockNumber *hexutil.Big `json:"blockNumber"`
18-
From common.Address `json:"from"`
19-
Gas hexutil.Uint64 `json:"gas"`
20-
GasPrice *hexutil.Big `json:"gasPrice"`
21-
GasFeeCap *hexutil.Big `json:"maxFeePerGas,omitempty"`
22-
GasTipCap *hexutil.Big `json:"maxPriorityFeePerGas,omitempty"`
23-
MaxFeePerBlobGas *hexutil.Big `json:"maxFeePerBlobGas,omitempty"`
24-
Hash common.Hash `json:"hash"`
25-
Input hexutil.Bytes `json:"input"`
26-
Nonce hexutil.Uint64 `json:"nonce"`
27-
To *common.Address `json:"to"`
28-
TransactionIndex *hexutil.Uint64 `json:"transactionIndex"`
29-
Value *hexutil.Big `json:"value"`
30-
Type hexutil.Uint64 `json:"type"`
31-
Accesses *types.AccessList `json:"accessList,omitempty"`
32-
ChainID *hexutil.Big `json:"chainId,omitempty"`
33-
BlobVersionedHashes []common.Hash `json:"blobVersionedHashes,omitempty"`
34-
V *hexutil.Big `json:"v"`
35-
R *hexutil.Big `json:"r"`
36-
S *hexutil.Big `json:"s"`
37-
YParity *hexutil.Uint64 `json:"yParity,omitempty"`
38-
}
39-
40-
// ContentFrom calls `txpool_contentFrom` of the Ethereum RPC
41-
func ContentFrom(ctx context.Context, client *ethclient.Client, address common.Address) (map[string]map[string]*RPCTransaction, error) {
42-
var res map[string]map[string]*RPCTransaction
43-
if err := client.Client().CallContext(ctx, &res, "txpool_contentFrom", address); err != nil {
44-
return nil, err
45-
}
46-
return res, nil
47-
}
48-
4912
// PendingTransactions returns pending txs sent from `address` sorted by nonce.
50-
func PendingTransactions(ctx context.Context, client *ethclient.Client, address common.Address) ([]*RPCTransaction, error) {
51-
txs, err := ContentFrom(ctx, client, address)
13+
func PendingTransactions(ctx context.Context, cl client.IETHClient, address common.Address) ([]*client.RPCTransaction, error) {
14+
txs, err := cl.ContentFrom(ctx, address)
5215
if err != nil {
5316
return nil, err
5417
}
@@ -58,12 +21,12 @@ func PendingTransactions(ctx context.Context, client *ethclient.Client, address
5821
return nil, nil
5922
}
6023

61-
var pendingTxs []*RPCTransaction
24+
var pendingTxs []*client.RPCTransaction
6225
for _, pendingTx := range pendingTxMap {
6326
pendingTxs = append(pendingTxs, pendingTx)
6427
}
6528

66-
slices.SortFunc(pendingTxs, func(a, b *RPCTransaction) int {
29+
slices.SortFunc(pendingTxs, func(a, b *client.RPCTransaction) int {
6730
if a.Nonce < b.Nonce {
6831
return -1
6932
} else if a.Nonce > b.Nonce {
@@ -82,30 +45,29 @@ func inclByPercent(n *big.Int, percent uint64) {
8245
}
8346

8447
// GetMinimumRequiredFee returns the minimum fee required to successfully send a transaction
85-
func GetMinimumRequiredFee(ctx context.Context, client *ethclient.Client, address common.Address, nonce uint64, priceBump uint64) (*big.Int, *big.Int, error) {
86-
pendingTxs, err := PendingTransactions(ctx, client, address)
48+
func GetMinimumRequiredFee(ctx context.Context, cl client.IETHClient, address common.Address, nonce uint64, priceBump uint64) (*client.RPCTransaction, *big.Int, *big.Int, error) {
49+
pendingTxs, err := PendingTransactions(ctx, cl, address)
8750
if err != nil {
88-
return nil, nil, err
51+
return nil, nil, nil, err
8952
} else if len(pendingTxs) == 0 {
90-
return common.Big0, common.Big0, nil
53+
return nil, common.Big0, common.Big0, nil
9154
}
9255

93-
var targetTx *RPCTransaction
56+
var targetTx *client.RPCTransaction
9457
for _, pendingTx := range pendingTxs {
9558
if uint64(pendingTx.Nonce) == nonce {
9659
targetTx = pendingTx
9760
break
9861
}
9962
}
10063
if targetTx == nil {
101-
return common.Big0, common.Big0, nil
64+
return nil, common.Big0, common.Big0, nil
10265
}
103-
104-
gasFeeCap := targetTx.GasFeeCap.ToInt()
105-
gasTipCap := targetTx.GasTipCap.ToInt()
66+
gasFeeCap := new(big.Int).Set(targetTx.GasFeeCap.ToInt())
67+
gasTipCap := new(big.Int).Set(targetTx.GasTipCap.ToInt())
10668

10769
inclByPercent(gasFeeCap, priceBump)
10870
inclByPercent(gasTipCap, priceBump)
10971

110-
return gasFeeCap, gasTipCap, nil
72+
return targetTx, gasFeeCap, gasTipCap, nil
11173
}

pkg/client/txpool/txpool_test.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88
"unsafe"
99

10+
"github.com/datachainlab/ethereum-ibc-relay-chain/pkg/client"
1011
"github.com/ethereum/go-ethereum/common"
1112
"github.com/ethereum/go-ethereum/core/types"
1213
"github.com/ethereum/go-ethereum/crypto"
@@ -44,16 +45,20 @@ func getPrivateKey(t *testing.T, account types.Account) *ecdsa.PrivateKey {
4445
return key
4546
}
4647

47-
func getEthClient(sim *simulated.Backend) *ethclient.Client {
48+
func getEthClient(t *testing.T, sim *simulated.Backend) client.IETHClient {
4849
type simClient struct {
4950
*ethclient.Client
5051
}
5152
ifceCli := sim.Client()
5253
ptrCli := unsafe.Add(unsafe.Pointer(&ifceCli), unsafe.Sizeof(uintptr(0)))
53-
return (*simClient)(ptrCli).Client
54+
cli, err := client.NewETHClientWith((*simClient)(ptrCli).Client)
55+
if err != nil {
56+
t.Fatalf("failed to create client: err=%v", err)
57+
}
58+
return cli
5459
}
5560

56-
func transfer(t *testing.T, ctx context.Context, client *ethclient.Client, signer types.Signer, key *ecdsa.PrivateKey, nonce uint64, gasTipCap, gasFeeCap *big.Int, to common.Address, amount *big.Int) {
61+
func transfer(t *testing.T, ctx context.Context, cl client.IETHClient, signer types.Signer, key *ecdsa.PrivateKey, nonce uint64, gasTipCap, gasFeeCap *big.Int, to common.Address, amount *big.Int) {
5762
tx := types.NewTx(&types.DynamicFeeTx{
5863
Nonce: nonce,
5964
GasTipCap: gasTipCap,
@@ -68,15 +73,15 @@ func transfer(t *testing.T, ctx context.Context, client *ethclient.Client, signe
6873
t.Fatalf("failed to sign tx: err=%v", err)
6974
}
7075

71-
if err := client.SendTransaction(ctx, tx); err != nil {
76+
if err := cl.Inner().SendTransaction(ctx, tx); err != nil {
7277
t.Fatalf("failed to send tx: err=%v", err)
7378
}
7479
}
7580

76-
func replace(t *testing.T, ctx context.Context, client *ethclient.Client, signer types.Signer, key *ecdsa.PrivateKey, priceBump uint64, nonce uint64, gasTipCap, gasFeeCap *big.Int, to common.Address, amount *big.Int) {
81+
func replace(t *testing.T, ctx context.Context, cl client.IETHClient, signer types.Signer, key *ecdsa.PrivateKey, priceBump uint64, nonce uint64, gasTipCap, gasFeeCap *big.Int, to common.Address, amount *big.Int) {
7782
addr := crypto.PubkeyToAddress(key.PublicKey)
7883

79-
if minFeeCap, minTipCap, err := GetMinimumRequiredFee(ctx, client, addr, nonce, priceBump); err != nil {
84+
if _, minFeeCap, minTipCap, err := GetMinimumRequiredFee(ctx, cl, addr, nonce, priceBump); err != nil {
8085
t.Fatalf("failed to get the minimum fee required to replace tx: err=%v", err)
8186
} else if minFeeCap.Cmp(common.Big0) == 0 {
8287
t.Fatalf("tx to replace not found")
@@ -92,7 +97,7 @@ func replace(t *testing.T, ctx context.Context, client *ethclient.Client, signer
9297
}
9398
}
9499

95-
transfer(t, ctx, client, signer, key, nonce, gasTipCap, gasFeeCap, to, amount)
100+
transfer(t, ctx, cl, signer, key, nonce, gasTipCap, gasFeeCap, to, amount)
96101
}
97102

98103
func TestContentFrom(t *testing.T) {
@@ -109,10 +114,10 @@ func TestContentFrom(t *testing.T) {
109114
})
110115
defer sim.Close()
111116

112-
cli := getEthClient(sim)
117+
cli := getEthClient(t, sim)
113118

114119
// make signer
115-
chainID, err := cli.ChainID(ctx)
120+
chainID, err := cli.Inner().ChainID(ctx)
116121
if err != nil {
117122
t.Fatalf("failed to get chain ID: err=%v", err)
118123
}
@@ -129,7 +134,7 @@ func TestContentFrom(t *testing.T) {
129134
}
130135

131136
// check block info
132-
block, err := cli.BlockByNumber(ctx, nil)
137+
block, err := cli.Inner().BlockByNumber(ctx, nil)
133138
if err != nil {
134139
t.Fatalf("failed to get block by number: err=%v", err)
135140
} else {
@@ -140,7 +145,7 @@ func TestContentFrom(t *testing.T) {
140145
sim.Commit()
141146

142147
// check block info
143-
block, err = cli.BlockByNumber(ctx, nil)
148+
block, err = cli.Inner().BlockByNumber(ctx, nil)
144149
if err != nil {
145150
t.Fatalf("failed to get block by number: err=%v", err)
146151
} else {
@@ -163,7 +168,7 @@ func TestContentFrom(t *testing.T) {
163168
sim.Commit()
164169

165170
// check block info
166-
block, err = cli.BlockByNumber(ctx, nil)
171+
block, err = cli.Inner().BlockByNumber(ctx, nil)
167172
if err != nil {
168173
t.Fatalf("failed to get block by number: err=%v", err)
169174
} else {

pkg/relay/ethereum/gas.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ import (
1313
)
1414

1515
type GasFeeCalculator struct {
16-
client *client.ETHClient
16+
client client.IETHClient
1717
config *ChainConfig
1818
}
1919

20-
func NewGasFeeCalculator(client *client.ETHClient, config *ChainConfig) *GasFeeCalculator {
20+
func NewGasFeeCalculator(client client.IETHClient, config *ChainConfig) *GasFeeCalculator {
2121
return &GasFeeCalculator{
2222
client: client,
2323
config: config,
@@ -27,19 +27,22 @@ func NewGasFeeCalculator(client *client.ETHClient, config *ChainConfig) *GasFeeC
2727
func (m *GasFeeCalculator) Apply(ctx context.Context, txOpts *bind.TransactOpts) error {
2828
minFeeCap := common.Big0
2929
minTipCap := common.Big0
30+
var oldTx *client.RPCTransaction
3031
if m.config.PriceBump > 0 && txOpts.Nonce != nil {
3132
var err error
32-
if minFeeCap, minTipCap, err = txpool.GetMinimumRequiredFee(ctx, m.client.Client, txOpts.From, txOpts.Nonce.Uint64(), m.config.PriceBump); err != nil {
33+
if oldTx, minFeeCap, minTipCap, err = txpool.GetMinimumRequiredFee(ctx, m.client, txOpts.From, txOpts.Nonce.Uint64(), m.config.PriceBump); err != nil {
3334
return err
3435
}
3536
}
36-
3737
switch m.config.TxType {
3838
case TxTypeLegacy:
3939
gasPrice, err := m.client.SuggestGasPrice(ctx)
4040
if err != nil {
4141
return fmt.Errorf("failed to suggest gas price: %v", err)
4242
}
43+
if oldTx != nil && oldTx.GasPrice != nil && oldTx.GasPrice.ToInt().Cmp(gasPrice) > 0 {
44+
return fmt.Errorf("old tx's gasPrice(%v) is higher than suggestion(%v)", oldTx.GasPrice.ToInt(), gasPrice)
45+
}
4346
if gasPrice.Cmp(minFeeCap) < 0 {
4447
gasPrice = minFeeCap
4548
}
@@ -52,6 +55,9 @@ func (m *GasFeeCalculator) Apply(ctx context.Context, txOpts *bind.TransactOpts)
5255
}
5356
// GasTipCap = min(LimitPriorityFeePerGas, simulated_eth_maxPriorityFeePerGas * PriorityFeeRate)
5457
m.config.DynamicTxGasConfig.PriorityFeeRate.Mul(gasTipCap)
58+
if oldTx != nil && oldTx.GasTipCap != nil && oldTx.GasTipCap.ToInt().Cmp(gasTipCap) > 0 {
59+
return fmt.Errorf("old tx's gasTipCap(%v) is higher than suggestion(%v)", oldTx.GasTipCap.ToInt(), gasTipCap)
60+
}
5561
if gasTipCap.Cmp(minTipCap) < 0 {
5662
gasTipCap = minTipCap
5763
}
@@ -61,6 +67,9 @@ func (m *GasFeeCalculator) Apply(ctx context.Context, txOpts *bind.TransactOpts)
6167
// GasFeeCap = min(LimitFeePerGas, GasTipCap + BaseFee * BaseFeeRate)
6268
m.config.DynamicTxGasConfig.BaseFeeRate.Mul(gasFeeCap)
6369
gasFeeCap.Add(gasFeeCap, gasTipCap)
70+
if oldTx != nil && oldTx.GasFeeCap != nil && oldTx.GasFeeCap.ToInt().Cmp(gasFeeCap) > 0 {
71+
return fmt.Errorf("old tx's gasFeeCap(%v) is higher than suggestion(%v)", oldTx.GasFeeCap.ToInt(), gasFeeCap)
72+
}
6473
if gasFeeCap.Cmp(minFeeCap) < 0 {
6574
gasFeeCap = minFeeCap
6675
}

0 commit comments

Comments
 (0)