feat: Estimate block number (#3973)
This commit is contained in:
parent
c0f32748b4
commit
a0eb205ca9
|
@ -406,6 +406,11 @@ func (api *API) GetSuggestedFees(ctx context.Context, chainID uint64) (*Suggeste
|
||||||
return api.s.feesManager.suggestedFees(ctx, chainID)
|
return api.s.feesManager.suggestedFees(ctx, chainID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) GetEstimatedLatestBlockNumber(ctx context.Context, chainID uint64) (uint64, error) {
|
||||||
|
log.Debug("call to GetEstimatedLatestBlockNumber, chainID:", chainID)
|
||||||
|
return api.s.blockChainState.GetEstimatedLatestBlockNumber(ctx, chainID)
|
||||||
|
}
|
||||||
|
|
||||||
func (api *API) GetTransactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Float) (TransactionEstimation, error) {
|
func (api *API) GetTransactionEstimatedTime(ctx context.Context, chainID uint64, maxFeePerGas *big.Float) (TransactionEstimation, error) {
|
||||||
log.Debug("call to getTransactionEstimatedTime")
|
log.Debug("call to getTransactionEstimatedTime")
|
||||||
return api.s.feesManager.transactionEstimatedTime(ctx, chainID, maxFeePerGas), nil
|
return api.s.feesManager.transactionEstimatedTime(ctx, chainID, maxFeePerGas), nil
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||||
|
"github.com/status-im/status-go/rpc"
|
||||||
|
"github.com/status-im/status-go/services/wallet/async"
|
||||||
|
"github.com/status-im/status-go/services/wallet/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fetchLatestBlockNumbersInterval = 10 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type fetchLatestBlockNumberCommand struct {
|
||||||
|
state *BlockChainState
|
||||||
|
rpcClient *rpc.Client
|
||||||
|
accountsDB *accounts.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fetchLatestBlockNumberCommand) Command() async.Command {
|
||||||
|
return async.InfiniteCommand{
|
||||||
|
Interval: fetchLatestBlockNumbersInterval,
|
||||||
|
Runable: c.Run,
|
||||||
|
}.Run
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fetchLatestBlockNumberCommand) Run(parent context.Context) (err error) {
|
||||||
|
log.Debug("start fetchLatestBlockNumberCommand")
|
||||||
|
|
||||||
|
networks, err := c.rpcClient.NetworkManager.Get(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
areTestNetworksEnabled, err := c.accountsDB.GetTestNetworksEnabled()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, network := range networks {
|
||||||
|
if network.IsTest != areTestNetworksEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, _ = c.state.fetchLatestBlockNumber(ctx, network.ChainID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LatestBlockData struct {
|
||||||
|
blockNumber uint64
|
||||||
|
timestamp time.Time
|
||||||
|
blockDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockChainState struct {
|
||||||
|
rpcClient *rpc.Client
|
||||||
|
accountsDB *accounts.Database
|
||||||
|
blkMu sync.RWMutex
|
||||||
|
latestBlockNumbers map[uint64]LatestBlockData
|
||||||
|
group *async.Group
|
||||||
|
cancelFn context.CancelFunc
|
||||||
|
sinceFn func(time.Time) time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBlockChainState(rpcClient *rpc.Client, accountsDb *accounts.Database) *BlockChainState {
|
||||||
|
return &BlockChainState{
|
||||||
|
rpcClient: rpcClient,
|
||||||
|
accountsDB: accountsDb,
|
||||||
|
blkMu: sync.RWMutex{},
|
||||||
|
latestBlockNumbers: make(map[uint64]LatestBlockData),
|
||||||
|
sinceFn: time.Since,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BlockChainState) Start() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
s.cancelFn = cancel
|
||||||
|
s.group = async.NewGroup(ctx)
|
||||||
|
|
||||||
|
command := &fetchLatestBlockNumberCommand{
|
||||||
|
state: s,
|
||||||
|
accountsDB: s.accountsDB,
|
||||||
|
rpcClient: s.rpcClient,
|
||||||
|
}
|
||||||
|
s.group.Add(command.Command())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BlockChainState) Stop() {
|
||||||
|
if s.cancelFn != nil {
|
||||||
|
s.cancelFn()
|
||||||
|
s.cancelFn = nil
|
||||||
|
}
|
||||||
|
if s.group != nil {
|
||||||
|
s.group.Stop()
|
||||||
|
s.group.Wait()
|
||||||
|
s.group = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BlockChainState) GetEstimatedLatestBlockNumber(ctx context.Context, chainID uint64) (uint64, error) {
|
||||||
|
blockNumber, ok := s.estimateLatestBlockNumber(chainID)
|
||||||
|
if ok {
|
||||||
|
return blockNumber, nil
|
||||||
|
}
|
||||||
|
return s.fetchLatestBlockNumber(ctx, chainID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BlockChainState) fetchLatestBlockNumber(ctx context.Context, chainID uint64) (uint64, error) {
|
||||||
|
client, err := s.rpcClient.EthClient(chainID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
blockNumber, err := client.BlockNumber(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
blockDuration, found := common.AverageBlockDurationForChain[common.ChainID(chainID)]
|
||||||
|
if !found {
|
||||||
|
blockDuration = common.AverageBlockDurationForChain[common.ChainID(common.UnknownChainID)]
|
||||||
|
}
|
||||||
|
s.setLatestBlockDataForChain(chainID, LatestBlockData{
|
||||||
|
blockNumber: blockNumber,
|
||||||
|
timestamp: time.Now(),
|
||||||
|
blockDuration: blockDuration,
|
||||||
|
})
|
||||||
|
return blockNumber, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BlockChainState) setLatestBlockDataForChain(chainID uint64, latestBlockData LatestBlockData) {
|
||||||
|
s.blkMu.Lock()
|
||||||
|
defer s.blkMu.Unlock()
|
||||||
|
s.latestBlockNumbers[chainID] = latestBlockData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BlockChainState) estimateLatestBlockNumber(chainID uint64) (uint64, bool) {
|
||||||
|
s.blkMu.RLock()
|
||||||
|
defer s.blkMu.RUnlock()
|
||||||
|
blockData, ok := s.latestBlockNumbers[chainID]
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
timeDiff := s.sinceFn(blockData.timestamp)
|
||||||
|
return blockData.blockNumber + uint64((timeDiff / blockData.blockDuration)), true
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mockupTime = time.Unix(946724400, 0) // 2000-01-01 12:00:00
|
||||||
|
|
||||||
|
func mockupSince(t time.Time) time.Duration {
|
||||||
|
return mockupTime.Sub(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestState(t *testing.T) (s *BlockChainState) {
|
||||||
|
state := NewBlockChainState(nil, nil)
|
||||||
|
state.sinceFn = mockupSince
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEstimateLatestBlockNumber(t *testing.T) {
|
||||||
|
state := setupTestState(t)
|
||||||
|
|
||||||
|
state.setLatestBlockDataForChain(1, LatestBlockData{
|
||||||
|
blockNumber: uint64(100),
|
||||||
|
timestamp: mockupTime.Add(-31 * time.Second),
|
||||||
|
blockDuration: 10 * time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
|
state.setLatestBlockDataForChain(2, LatestBlockData{
|
||||||
|
blockNumber: uint64(200),
|
||||||
|
timestamp: mockupTime.Add(-5 * time.Second),
|
||||||
|
blockDuration: 12 * time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
|
val, ok := state.estimateLatestBlockNumber(1)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, uint64(103), val)
|
||||||
|
val, ok = state.estimateLatestBlockNumber(2)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, uint64(200), val)
|
||||||
|
val, ok = state.estimateLatestBlockNumber(3)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, uint64(0), val)
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package common
|
package common
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type ChainID uint64
|
type ChainID uint64
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -12,3 +14,13 @@ const (
|
||||||
ArbitrumMainnet uint64 = 42161
|
ArbitrumMainnet uint64 = 42161
|
||||||
ArbitrumGoerli uint64 = 421613
|
ArbitrumGoerli uint64 = 421613
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var AverageBlockDurationForChain = map[ChainID]time.Duration{
|
||||||
|
ChainID(UnknownChainID): time.Duration(12000) * time.Millisecond,
|
||||||
|
ChainID(EthereumMainnet): time.Duration(12000) * time.Millisecond,
|
||||||
|
ChainID(EthereumGoerli): time.Duration(12000) * time.Millisecond,
|
||||||
|
ChainID(OptimismMainnet): time.Duration(400) * time.Millisecond,
|
||||||
|
ChainID(OptimismGoerli): time.Duration(2000) * time.Millisecond,
|
||||||
|
ChainID(ArbitrumMainnet): time.Duration(300) * time.Millisecond,
|
||||||
|
ChainID(ArbitrumGoerli): time.Duration(1500) * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
|
||||||
|
w_common "github.com/status-im/status-go/services/wallet/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Balance struct {
|
type Balance struct {
|
||||||
|
@ -17,18 +19,9 @@ type Balance struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultChains = uint64(0)
|
|
||||||
aDay = time.Duration(24) * time.Hour
|
aDay = time.Duration(24) * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
var averageBlockDurationForChain = map[uint64]time.Duration{
|
|
||||||
defaultChains: time.Duration(12000) * time.Millisecond,
|
|
||||||
10: time.Duration(400) * time.Millisecond, // Optimism
|
|
||||||
420: time.Duration(2000) * time.Millisecond, // Optimism Testnet
|
|
||||||
42161: time.Duration(300) * time.Millisecond, // Arbitrum
|
|
||||||
421611: time.Duration(1500) * time.Millisecond, // Arbitrum Testnet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must have a common divisor to share common blocks and increase the cache hit
|
// Must have a common divisor to share common blocks and increase the cache hit
|
||||||
const (
|
const (
|
||||||
twiceADayStride time.Duration = time.Duration(12) * time.Hour
|
twiceADayStride time.Duration = time.Duration(12) * time.Hour
|
||||||
|
@ -78,9 +71,9 @@ var timeIntervalToStrideDuration = map[TimeInterval]time.Duration{
|
||||||
}
|
}
|
||||||
|
|
||||||
func strideBlockCount(timeInterval TimeInterval, chainID uint64) int {
|
func strideBlockCount(timeInterval TimeInterval, chainID uint64) int {
|
||||||
blockDuration, found := averageBlockDurationForChain[chainID]
|
blockDuration, found := w_common.AverageBlockDurationForChain[w_common.ChainID(chainID)]
|
||||||
if !found {
|
if !found {
|
||||||
blockDuration = averageBlockDurationForChain[defaultChains]
|
blockDuration = w_common.AverageBlockDurationForChain[w_common.ChainID(w_common.UnknownChainID)]
|
||||||
}
|
}
|
||||||
|
|
||||||
return int(timeIntervalToStrideDuration[timeInterval] / blockDuration)
|
return int(timeIntervalToStrideDuration[timeInterval] / blockDuration)
|
||||||
|
|
|
@ -102,6 +102,7 @@ func NewService(
|
||||||
reader := NewReader(rpcClient, tokenManager, marketManager, accountsDB, NewPersistence(db), feed)
|
reader := NewReader(rpcClient, tokenManager, marketManager, accountsDB, NewPersistence(db), feed)
|
||||||
history := history.NewService(db, accountsDB, feed, rpcClient, tokenManager, marketManager)
|
history := history.NewService(db, accountsDB, feed, rpcClient, tokenManager, marketManager)
|
||||||
currency := currency.NewService(db, feed, tokenManager, marketManager)
|
currency := currency.NewService(db, feed, tokenManager, marketManager)
|
||||||
|
blockChainState := NewBlockChainState(rpcClient, accountsDB)
|
||||||
|
|
||||||
openseaHTTPClient := opensea.NewHTTPClient()
|
openseaHTTPClient := opensea.NewHTTPClient()
|
||||||
openseaClient := opensea.NewClient(config.WalletConfig.OpenseaAPIKey, openseaHTTPClient, feed)
|
openseaClient := opensea.NewClient(config.WalletConfig.OpenseaAPIKey, openseaHTTPClient, feed)
|
||||||
|
@ -165,6 +166,7 @@ func NewService(
|
||||||
currency: currency,
|
currency: currency,
|
||||||
activity: activity,
|
activity: activity,
|
||||||
decoder: NewDecoder(),
|
decoder: NewDecoder(),
|
||||||
|
blockChainState: blockChainState,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,6 +197,7 @@ type Service struct {
|
||||||
currency *currency.Service
|
currency *currency.Service
|
||||||
activity *activity.Service
|
activity *activity.Service
|
||||||
decoder *Decoder
|
decoder *Decoder
|
||||||
|
blockChainState *BlockChainState
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start signals transmitter.
|
// Start signals transmitter.
|
||||||
|
@ -204,6 +207,7 @@ func (s *Service) Start() error {
|
||||||
err := s.signals.Start()
|
err := s.signals.Start()
|
||||||
s.history.Start()
|
s.history.Start()
|
||||||
s.collectibles.Start()
|
s.collectibles.Start()
|
||||||
|
s.blockChainState.Start()
|
||||||
s.started = true
|
s.started = true
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -223,6 +227,7 @@ func (s *Service) Stop() error {
|
||||||
s.history.Stop()
|
s.history.Stop()
|
||||||
s.activity.Stop()
|
s.activity.Stop()
|
||||||
s.collectibles.Stop()
|
s.collectibles.Stop()
|
||||||
|
s.blockChainState.Stop()
|
||||||
s.started = false
|
s.started = false
|
||||||
log.Info("wallet stopped")
|
log.Info("wallet stopped")
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Reference in New Issue