diff --git a/api/api_full.go b/api/api_full.go index ff44b091b..b7eb20027 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -786,6 +786,7 @@ type FullNode interface { NetListening(ctx context.Context) (bool, error) //perm:read EthProtocolVersion(ctx context.Context) (EthUint64, error) //perm:read EthGasPrice(ctx context.Context) (EthBigInt, error) //perm:read + EthFeeHistory(ctx context.Context, blkCount uint64, newestBlk string) (EthFeeHistory, error) //perm:read EthMaxPriorityFeePerGas(ctx context.Context) (EthBigInt, error) //perm:read EthEstimateGas(ctx context.Context, tx EthCall) (EthUint64, error) //perm:read diff --git a/api/docgen/docgen.go b/api/docgen/docgen.go index afd9dbc8f..fc6c82157 100644 --- a/api/docgen/docgen.go +++ b/api/docgen/docgen.go @@ -372,6 +372,8 @@ func init() { ethhash, _ := api.EthHashFromCid(c) addExample(ðhash) + ethFeeHistoryReward := [][]api.EthBigInt{} + addExample(ðFeeHistoryReward) } func GetAPIType(name, pkg string) (i interface{}, t reflect.Type, permStruct []reflect.Type) { diff --git a/api/eth_types.go b/api/eth_types.go index 066b0d594..47aeafac5 100644 --- a/api/eth_types.go +++ b/api/eth_types.go @@ -417,3 +417,10 @@ func (h EthHash) ToCid() cid.Cid { return cid.NewCidV1(cid.DagCBOR, mh) } + +type EthFeeHistory struct { + OldestBlock uint64 `json:"oldestBlock"` + BaseFeePerGas []EthBigInt `json:"baseFeePerGas"` + GasUsedRatio []float64 `json:"gasUsedRatio"` + Reward *[][]EthBigInt `json:"reward,omitempty"` +} diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index 7e724823f..db4f71683 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -996,6 +996,21 @@ func (mr *MockFullNodeMockRecorder) EthEstimateGas(arg0, arg1 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthEstimateGas", reflect.TypeOf((*MockFullNode)(nil).EthEstimateGas), arg0, arg1) } +// EthFeeHistory mocks base method. +func (m *MockFullNode) EthFeeHistory(arg0 context.Context, arg1 uint64, arg2 string) (api.EthFeeHistory, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthFeeHistory", arg0, arg1, arg2) + ret0, _ := ret[0].(api.EthFeeHistory) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthFeeHistory indicates an expected call of EthFeeHistory. +func (mr *MockFullNodeMockRecorder) EthFeeHistory(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthFeeHistory", reflect.TypeOf((*MockFullNode)(nil).EthFeeHistory), arg0, arg1, arg2) +} + // EthGasPrice mocks base method. func (m *MockFullNode) EthGasPrice(arg0 context.Context) (api.EthBigInt, error) { m.ctrl.T.Helper() diff --git a/api/proxy_gen.go b/api/proxy_gen.go index addd38d18..4b6ff6181 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -230,6 +230,8 @@ type FullNodeStruct struct { EthEstimateGas func(p0 context.Context, p1 EthCall) (EthUint64, error) `perm:"read"` + EthFeeHistory func(p0 context.Context, p1 uint64, p2 string) (EthFeeHistory, error) `perm:"read"` + EthGasPrice func(p0 context.Context) (EthBigInt, error) `perm:"read"` EthGetBalance func(p0 context.Context, p1 EthAddress, p2 string) (EthBigInt, error) `perm:"read"` @@ -1894,6 +1896,17 @@ func (s *FullNodeStub) EthEstimateGas(p0 context.Context, p1 EthCall) (EthUint64 return *new(EthUint64), ErrNotSupported } +func (s *FullNodeStruct) EthFeeHistory(p0 context.Context, p1 uint64, p2 string) (EthFeeHistory, error) { + if s.Internal.EthFeeHistory == nil { + return *new(EthFeeHistory), ErrNotSupported + } + return s.Internal.EthFeeHistory(p0, p1, p2) +} + +func (s *FullNodeStub) EthFeeHistory(p0 context.Context, p1 uint64, p2 string) (EthFeeHistory, error) { + return *new(EthFeeHistory), ErrNotSupported +} + func (s *FullNodeStruct) EthGasPrice(p0 context.Context) (EthBigInt, error) { if s.Internal.EthGasPrice == nil { return *new(EthBigInt), ErrNotSupported diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index f4495845b..18382d6ee 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -71,6 +71,7 @@ * [EthCall](#EthCall) * [EthChainId](#EthChainId) * [EthEstimateGas](#EthEstimateGas) + * [EthFeeHistory](#EthFeeHistory) * [EthGasPrice](#EthGasPrice) * [EthGetBalance](#EthGetBalance) * [EthGetBlockByHash](#EthGetBlockByHash) @@ -2228,6 +2229,33 @@ Inputs: Response: `"0x5"` +### EthFeeHistory + + +Perms: read + +Inputs: +```json +[ + 42, + "string value" +] +``` + +Response: +```json +{ + "oldestBlock": 42, + "baseFeePerGas": [ + "0x0" + ], + "gasUsedRatio": [ + 12.3 + ], + "reward": [] +} +``` + ### EthGasPrice diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 3a0fd5899..02e7a8e05 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -45,6 +45,7 @@ type EthModuleAPI interface { EthGetCode(ctx context.Context, address api.EthAddress, blkOpt string) (api.EthBytes, error) EthGetStorageAt(ctx context.Context, address api.EthAddress, position api.EthBytes, blkParam string) (api.EthBytes, error) EthGetBalance(ctx context.Context, address api.EthAddress, blkParam string) (api.EthBigInt, error) + EthFeeHistory(ctx context.Context, blkCount uint64, newestBlk string) (api.EthFeeHistory, error) EthChainId(ctx context.Context) (api.EthUint64, error) NetVersion(ctx context.Context) (string, error) NetListening(ctx context.Context) (bool, error) @@ -375,6 +376,73 @@ func (a *EthModule) EthChainId(ctx context.Context) (api.EthUint64, error) { return api.EthUint64(build.Eip155ChainId), nil } +func (a *EthModule) EthFeeHistory(ctx context.Context, blkCount uint64, newestBlkNum string) (api.EthFeeHistory, error) { + if blkCount > 1024 { + return api.EthFeeHistory{}, fmt.Errorf("block count should be smaller than 1024") + } + + newestBlkHeight := uint64(a.Chain.GetHeaviestTipSet().Height()) + + // TODO https://github.com/filecoin-project/ref-fvm/issues/1016 + var blkNum api.EthUint64 + err := blkNum.UnmarshalJSON([]byte(`"` + newestBlkNum + `"`)) + if err == nil && uint64(blkNum) < newestBlkHeight { + newestBlkHeight = uint64(blkNum) + } + + // Deal with the case that the chain is shorter than the number of + // requested blocks. + oldestBlkHeight := uint64(1) + if blkCount <= newestBlkHeight { + oldestBlkHeight = newestBlkHeight - blkCount + 1 + } + + ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(newestBlkHeight), nil, false) + if err != nil { + return api.EthFeeHistory{}, fmt.Errorf("cannot load find block height: %v", newestBlkHeight) + } + + // FIXME: baseFeePerGas should include the next block after the newest of the returned range, because this + // can be inferred from the newest block. we use the newest block's baseFeePerGas for now but need to fix it + // In other words, due to deferred execution, we might not be returning the most useful value here for the client. + baseFeeArray := []api.EthBigInt{api.EthBigInt(ts.Blocks()[0].ParentBaseFee)} + gasUsedRatioArray := []float64{} + + for ts.Height() >= abi.ChainEpoch(oldestBlkHeight) { + // Unfortunately we need to rebuild the full message view so we can + // totalize gas used in the tipset. + block, err := a.ethBlockFromFilecoinTipSet(ctx, ts, false) + if err != nil { + return api.EthFeeHistory{}, fmt.Errorf("cannot create eth block: %v", err) + } + + // both arrays should be reversed at the end + baseFeeArray = append(baseFeeArray, api.EthBigInt(ts.Blocks()[0].ParentBaseFee)) + gasUsedRatioArray = append(gasUsedRatioArray, float64(block.GasUsed)/float64(build.BlockGasLimit)) + + parentTsKey := ts.Parents() + ts, err = a.Chain.LoadTipSet(ctx, parentTsKey) + if err != nil { + return api.EthFeeHistory{}, fmt.Errorf("cannot load tipset key: %v", parentTsKey) + } + } + + // Reverse the arrays; we collected them newest to oldest; the client expects oldest to newest. + + for i, j := 0, len(baseFeeArray)-1; i < j; i, j = i+1, j-1 { + baseFeeArray[i], baseFeeArray[j] = baseFeeArray[j], baseFeeArray[i] + } + for i, j := 0, len(gasUsedRatioArray)-1; i < j; i, j = i+1, j-1 { + gasUsedRatioArray[i], gasUsedRatioArray[j] = gasUsedRatioArray[j], gasUsedRatioArray[i] + } + + return api.EthFeeHistory{ + OldestBlock: oldestBlkHeight, + BaseFeePerGas: baseFeeArray, + GasUsedRatio: gasUsedRatioArray, + }, nil +} + func (a *EthModule) NetVersion(ctx context.Context) (string, error) { // Note that networkId is not encoded in hex nv, err := a.StateNetworkVersion(ctx, types.EmptyTSK) diff --git a/node/rpc.go b/node/rpc.go index 2127101e9..2648056f6 100644 --- a/node/rpc.go +++ b/node/rpc.go @@ -91,6 +91,7 @@ func FullNodeHandler(a v1api.FullNode, permissioned bool, opts ...jsonrpc.Server rpcServer.AliasMethod("eth_getStorageAt", "Filecoin.EthGetStorageAt") rpcServer.AliasMethod("eth_getBalance", "Filecoin.EthGetBalance") rpcServer.AliasMethod("eth_chainId", "Filecoin.EthChainId") + rpcServer.AliasMethod("eth_feeHistory", "Filecoin.EthFeeHistory") rpcServer.AliasMethod("eth_protocolVersion", "Filecoin.EthProtocolVersion") rpcServer.AliasMethod("eth_maxPriorityFeePerGas", "Filecoin.EthMaxPriorityFeePerGas") rpcServer.AliasMethod("eth_gasPrice", "Filecoin.EthGasPrice")