Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions services/wallet/activity/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"github.com/status-im/status-go/services/wallet/thirdparty"
mock_token "github.com/status-im/status-go/services/wallet/token/mock/token"
tokenTypes "github.com/status-im/status-go/services/wallet/token/types"
"github.com/status-im/status-go/services/wallet/transfer"
"github.com/status-im/status-go/services/wallet/walletevent"
"github.com/status-im/status-go/t/helpers"
"github.com/status-im/status-go/transactions"
Expand Down Expand Up @@ -108,13 +107,6 @@ func setupTransactions(t *testing.T, state testState, txCount int, testTxs []tra
allAddresses = append(allAddresses, p.From, p.To)
}

txs, fromTrs, toTrs := transfer.GenerateTestTransfers(t, state.service.db, len(pendings), txCount)
for i := range txs {
transfer.InsertTestTransfer(t, state.service.db, txs[i].To, &txs[i])
}

allAddresses = append(append(allAddresses, fromTrs...), toTrs...)

state.tokenMock.EXPECT().LookupTokenIdentity(gomock.Any(), gomock.Any(), gomock.Any()).Return(
&tokenTypes.Token{
ChainID: 5,
Expand Down
7 changes: 0 additions & 7 deletions services/wallet/activity/session_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/responses"
"github.com/status-im/status-go/services/wallet/routeexecution"
"github.com/status-im/status-go/services/wallet/transfer"
"github.com/status-im/status-go/services/wallet/walletevent"
"github.com/status-im/status-go/transactions"

Expand Down Expand Up @@ -317,12 +316,6 @@ func (s *Service) processEvents(ctx context.Context) {
Hash: payload.Hash,
})
debounceProcessChangesFn()
case transfer.EventNewTransfers:
eventCount++
// No updates here, these are detected with their final state, just trigger
// the detection of new entries
newTxs = true
debounceProcessChangesFn()
case routeexecution.EventRouteExecutionTransactionSent:
sentTxs, ok := event.EventParams.(*responses.RouterSentTransactions)
if ok && sentTxs != nil {
Expand Down
164 changes: 132 additions & 32 deletions services/wallet/blockchainstate/blockchainstate.go
Original file line number Diff line number Diff line change
@@ -1,63 +1,163 @@
package blockchainstate

//go:generate go tool mockgen -package=mock_blockchainstate -source=blockchainstate.go -destination=mock/blockchainstate.go

import (
"context"
"errors"
"math"
"strconv"
"sync"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"

"github.com/status-im/go-wallet-sdk/pkg/contracts/multicall3"

"github.com/status-im/status-go/rpc/chain"
"github.com/status-im/status-go/services/wallet/common"
)

type EthClientGetter interface {
EthClient(chainID uint64) (chain.ClientInterface, error)
}

type LatestBlockData struct {
blockNumber uint64
timestamp time.Time
blockDuration time.Duration
blockNumber uint64
timestamp time.Time
}

type BlockChainState struct {
blkMu sync.RWMutex
latestBlockNumbers map[uint64]LatestBlockData
sinceFn func(time.Time) time.Duration
ethClientGetter EthClientGetter
latestBlockData map[uint64]LatestBlockData // map[chainID]*LatestBlockData
sinceFn func(time.Time) time.Duration
blockDurationFn func(chainID uint64) time.Duration
mu sync.RWMutex
}

func NewBlockChainState() *BlockChainState {
func NewBlockChainState(ethClientGetter EthClientGetter) *BlockChainState {
return &BlockChainState{
blkMu: sync.RWMutex{},
latestBlockNumbers: make(map[uint64]LatestBlockData),
sinceFn: time.Since,
ethClientGetter: ethClientGetter,
latestBlockData: make(map[uint64]LatestBlockData),
sinceFn: time.Since,
blockDurationFn: func(chainID uint64) time.Duration {
blockDuration, found := common.AverageBlockDurationForChain[common.ChainID(chainID)]
if !found {
blockDuration = common.AverageBlockDurationForChain[common.ChainID(common.UnknownChainID)]
}
return blockDuration
},
}
}

// Estimate the latest block number based on the last real value and the average block duration.
func (s *BlockChainState) GetEstimatedLatestBlockNumber(ctx context.Context, chainID uint64) (uint64, error) {
blockNumber, _ := s.estimateLatestBlockNumber(chainID)
return blockNumber, nil
blockData, err := s.getOrInitializeLatestBlockData(ctx, chainID)
if err != nil {
return 0, err
}

timeDiff := s.sinceFn(blockData.timestamp)
blockDuration := s.blockDurationFn(chainID)
blockDiff := uint64(math.Floor(float64(timeDiff) / float64(blockDuration)))
return blockData.blockNumber + blockDiff, nil
}

// Estimate the timestamp for a given block number based on the last real value and the average block duration.
func (s *BlockChainState) GetEstimatedBlockTime(ctx context.Context, chainID uint64, blockNumber uint64) (time.Time, error) {
blockData, err := s.getOrInitializeLatestBlockData(ctx, chainID)
if err != nil {
return time.Time{}, err
}
blockDiff := int64(blockNumber) - int64(blockData.blockNumber)
blockDuration := s.blockDurationFn(chainID)
timeDiff := time.Duration(blockDiff * int64(blockDuration))
blockTime := blockData.timestamp.Add(timeDiff)
return blockTime, nil
}

// Set the latest known block number for a chain
func (s *BlockChainState) SetLatestBlockNumber(chainID uint64, blockNumber uint64) {
s.setLatestBlockNumber(chainID, blockNumber)
}

func (s *BlockChainState) getOrInitializeLatestBlockData(ctx context.Context, chainID uint64) (LatestBlockData, error) {
// Try read lock first for fast path
s.mu.RLock()
blockData, exists := s.latestBlockData[chainID]
s.mu.RUnlock()
if exists {
return blockData, nil
}

// Not found, need to initialize
s.mu.Lock()
defer s.mu.Unlock()

// Double check after acquiring write lock
blockData, exists = s.latestBlockData[chainID]
if exists {
return blockData, nil
}

blockNumber, err := s.getLatestBlockNumber(ctx, chainID)
if err != nil {
return LatestBlockData{}, err
}
s.latestBlockData[chainID] = buildLatestBlockData(blockNumber)
return s.latestBlockData[chainID], nil
}

func (s *BlockChainState) SetLastBlockNumber(chainID uint64, blockNumber uint64) {
blockDuration, found := common.AverageBlockDurationForChain[common.ChainID(chainID)]
if !found {
blockDuration = common.AverageBlockDurationForChain[common.ChainID(common.UnknownChainID)]
// Get the latest block number for a chain
func (s *BlockChainState) getLatestBlockNumber(ctx context.Context, chainID uint64) (uint64, error) {
ethClient, err := s.ethClientGetter.EthClient(chainID)
if err != nil {
return 0, err
}

// Get multicall3 contract address
multicallAddr, exists := multicall3.GetMulticall3Address(int64(chainID))
if !exists {
return 0, errors.New("Multicall3 not supported on chain ID " + strconv.Itoa(int(chainID)))
}

// Create multicall3 contract instance for the caller interface
multicallContract, err := multicall3.NewMulticall3(multicallAddr, ethClient)
if err != nil {
return 0, err
}
s.setLatestBlockDataForChain(chainID, LatestBlockData{
blockNumber: blockNumber,
timestamp: time.Now(),
blockDuration: blockDuration,

blockNumber, err := multicallContract.GetBlockNumber(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return 0, err
}

return blockNumber.Uint64(), nil
}

func (s *BlockChainState) setLatestBlockDataForChain(chainID uint64, latestBlockData LatestBlockData) {
s.blkMu.Lock()
defer s.blkMu.Unlock()
s.latestBlockNumbers[chainID] = latestBlockData
func (s *BlockChainState) setLatestBlockNumber(chainID uint64, blockNumber uint64) {
s.mu.Lock()
defer s.mu.Unlock()

oldData, exists := s.latestBlockData[chainID]
if !exists {
// No old value, initialized with newData
s.latestBlockData[chainID] = buildLatestBlockData(blockNumber)
return
}
if oldData.blockNumber >= blockNumber {
// Only update if the new block number is greater than the current one
return
}
// Update the existing value
s.latestBlockData[chainID] = buildLatestBlockData(blockNumber)
}

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
func buildLatestBlockData(blockNumber uint64) LatestBlockData {
return LatestBlockData{
blockNumber: blockNumber,
timestamp: time.Now(),
}
timeDiff := s.sinceFn(blockData.timestamp)
return blockData.blockNumber + uint64((timeDiff / blockData.blockDuration)), true
}
128 changes: 101 additions & 27 deletions services/wallet/blockchainstate/blockchainstate_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package blockchainstate

import (
"context"
"errors"
"testing"
"time"

mock_blockchainstate "github.com/status-im/status-go/services/wallet/blockchainstate/mock"

"github.com/stretchr/testify/require"

"go.uber.org/mock/gomock"
)

var mockupTime = time.Unix(946724400, 0) // 2000-01-01 12:00:00
Expand All @@ -13,34 +19,102 @@ func mockupSince(t time.Time) time.Duration {
return mockupTime.Sub(t)
}

func setupTestState(t *testing.T) (s *BlockChainState) {
state := NewBlockChainState()
var mockupBlockDuration = 10 * time.Second

func mockupBlockDurationFn(chainID uint64) time.Duration {
return mockupBlockDuration
}

func setupTestState(t *testing.T) (*BlockChainState, *mock_blockchainstate.MockEthClientGetter) {
ctrl := gomock.NewController(t)
ethClientGetter := mock_blockchainstate.NewMockEthClientGetter(ctrl)

state := NewBlockChainState(ethClientGetter)
state.sinceFn = mockupSince
return state
state.blockDurationFn = mockupBlockDurationFn
return state, ethClientGetter
}

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)
func TestGetEstimatedLatestBlockNumber_WithExistingData(t *testing.T) {
state, _ := setupTestState(t)

// Manually set block data (simulating what would happen after initialization)
state.latestBlockData[1] = LatestBlockData{
blockNumber: 100,
timestamp: mockupTime.Add(-31 * time.Second),
}

state.latestBlockData[2] = LatestBlockData{
blockNumber: 200,
timestamp: mockupTime.Add(-5 * time.Second),
}

// Test chain 1: should estimate 3 blocks ahead (31 seconds / 10 seconds per block)
mockupBlockDuration = 10 * time.Second
blockNumber, err := state.GetEstimatedLatestBlockNumber(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, uint64(103), blockNumber)

// Test chain 2: should not estimate ahead (5 seconds < 12 seconds per block)
mockupBlockDuration = 12 * time.Second
blockNumber, err = state.GetEstimatedLatestBlockNumber(context.Background(), 2)
require.NoError(t, err)
require.Equal(t, uint64(200), blockNumber)
}

func TestGetEstimatedLatestBlockNumber_WithInitialization(t *testing.T) {
state, ethClientGetter := setupTestState(t)

// Mock eth client to return an error (simulating network failure)
ethClientGetter.EXPECT().EthClient(uint64(1)).Return(nil, errors.New("network error")).AnyTimes()

// This should trigger initialization but fail
blockNumber, err := state.GetEstimatedLatestBlockNumber(context.Background(), 1)
require.Error(t, err)
require.Equal(t, uint64(0), blockNumber)
require.Contains(t, err.Error(), "network error")
}

func TestGetEstimatedBlockTime(t *testing.T) {
state, _ := setupTestState(t)

// Set up test data
state.latestBlockData[1] = LatestBlockData{
blockNumber: 100,
timestamp: mockupTime.Add(-10 * time.Second),
}

// Test block time estimation
mockupBlockDuration = 2 * time.Second
blockTime, err := state.GetEstimatedBlockTime(context.Background(), 1, 105)
require.NoError(t, err)

// Block 105 is 5 blocks ahead of 100, so 5 * 2 seconds = 10 seconds ahead
expectedTime := mockupTime
require.Equal(t, expectedTime, blockTime)
}

func TestSetLatestBlockNumber(t *testing.T) {
state, _ := setupTestState(t)

// Test setting block number
state.SetLatestBlockNumber(1, 100)

// Verify it was set
blockData, exists := state.latestBlockData[1]
require.True(t, exists)
require.Equal(t, uint64(100), blockData.blockNumber)
require.True(t, blockData.timestamp.After(mockupTime.Add(-time.Minute))) // Should be recent

// Test setting a smaller block number (should not update)
state.SetLatestBlockNumber(1, 50)
blockData, exists = state.latestBlockData[1]
require.True(t, exists)
require.Equal(t, uint64(100), blockData.blockNumber) // Should still be 100

// Test setting a larger block number (should update)
state.SetLatestBlockNumber(1, 150)
blockData, exists = state.latestBlockData[1]
require.True(t, exists)
require.Equal(t, uint64(150), blockData.blockNumber) // Should be updated to 150
}
Loading
Loading