status-go/services/wallet/blockchainstate.go

150 lines
3.8 KiB
Go

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
}