Skip to content

Commit a93fbf7

Browse files
committed
fix: fetch metadata for unkown erc20 tokens spotted in Alchemy transactions
1 parent 391edd6 commit a93fbf7

File tree

11 files changed

+119
-18
lines changed

11 files changed

+119
-18
lines changed

node/get_status_node.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ type StatusNode struct {
126126

127127
walletFeed event.Feed
128128
accountsPublisher *pubsub.Publisher
129+
tokenPublisher *pubsub.Publisher
129130

130131
localBackup *backup.Controller
131132
}
@@ -139,6 +140,7 @@ func New(transactor *transactions.Transactor, gethAccountsManager *accsmanagemen
139140
logger: logger,
140141
publicMethods: make(map[string]bool),
141142
accountsPublisher: pubsub.NewPublisher(),
143+
tokenPublisher: pubsub.NewPublisher(),
142144
rpcServer: gethrpc.NewServer(),
143145
}
144146
}
@@ -370,7 +372,7 @@ func (n *StatusNode) createAndStartTokenManager() error {
370372
}
371373

372374
n.tokenManager = token.NewTokenManager(n.walletDB, n.rpcClient, community.NewManager(n.appDB, n.mediaServer, nil),
373-
n.rpcClient.GetNetworkManager(), n.appDB, n.mediaServer, &n.walletFeed, n.accountsPublisher, accDB,
375+
n.rpcClient.GetNetworkManager(), n.appDB, n.mediaServer, &n.walletFeed, n.accountsPublisher, n.tokenPublisher, accDB,
374376
token.NewPersistence(n.walletDB))
375377

376378
const (
@@ -430,6 +432,7 @@ func (n *StatusNode) Stop() error {
430432
}
431433

432434
n.accountsPublisher.Close()
435+
n.tokenPublisher.Close()
433436

434437
n.rpcClient.Stop()
435438
n.rpcClient = nil

services/wallet/activity/activity_v2.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
eth "github.com/ethereum/go-ethereum/common"
1010

11+
"github.com/status-im/status-go/pkg/pubsub"
1112
ac "github.com/status-im/status-go/services/wallet/activity/common"
1213
wCommon "github.com/status-im/status-go/services/wallet/common"
1314
)
@@ -20,6 +21,8 @@ type FilterDependencies struct {
2021
tokenFromSymbol func(chainID *wCommon.ChainID, symbol string) *ac.Token
2122
// use to get current timestamp
2223
currentTimestamp func() int64
24+
// tokenPublisher for async token discovery requests
25+
tokenPublisher *pubsub.Publisher
2326
}
2427

2528
type TransactionOrigin int

services/wallet/activity/fetched_entries.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@ import (
1212
eth "github.com/ethereum/go-ethereum/common"
1313

1414
"github.com/status-im/status-go/logutils"
15+
"github.com/status-im/status-go/pkg/pubsub"
1516
ac "github.com/status-im/status-go/services/wallet/activity/common"
1617
wCommon "github.com/status-im/status-go/services/wallet/common"
1718
"github.com/status-im/status-go/services/wallet/thirdparty"
1819
"github.com/status-im/status-go/services/wallet/thirdparty/activity/alchemy"
20+
"github.com/status-im/status-go/services/wallet/token/tokenevent"
1921
"github.com/status-im/status-go/sqlite"
2022
)
2123

2224
// getFetchedEntriesByIDs fetches Alchemy transaction details by IDs
2325
// Returns a map keyed by "chainID-hash-address" string
2426
func getFetchedEntriesByIDs(ctx context.Context, deps FilterDependencies, txIDs []OrderedTransactionID) (map[string]Entry, error) {
25-
2627
if len(txIDs) == 0 {
2728
return make(map[string]Entry), nil
2829
}
@@ -75,6 +76,22 @@ func getFetchedEntriesByIDs(ctx context.Context, deps FilterDependencies, txIDs
7576
return nil, fmt.Errorf("failed to convert rows to transfers: %w", err)
7677
}
7778

79+
// Request async token discovery for ERC20 transfers to make sure we have their metadata
80+
if deps.tokenPublisher != nil {
81+
for chainID, addressMap := range transfersMap {
82+
for _, transfers := range addressMap {
83+
for _, transfer := range transfers {
84+
if transfer.Category == alchemy.TransferCategoryErc20 && transfer.RawContract.Address != nil {
85+
pubsub.Publish(deps.tokenPublisher, tokenevent.TokenDiscoveryRequestEvent{
86+
ChainID: uint64(chainID),
87+
Address: *transfer.RawContract.Address,
88+
})
89+
}
90+
}
91+
}
92+
}
93+
}
94+
7895
result := make(map[string]Entry)
7996
for chainID, addressMap := range transfersMap {
8097
for address, transfers := range addressMap {
@@ -176,6 +193,7 @@ func thirdpartyActivityEntriesToEntries(deps FilterDependencies, activityEntries
176193
}
177194

178195
entry.symbolOut, entry.symbolIn = lookupAndFillInTokens(deps, entry.tokenOut, entry.tokenIn)
196+
179197
entries = append(entries, entry)
180198
}
181199

services/wallet/activity/service.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,10 @@ func (s *Service) getDeps() FilterDependencies {
356356
Address: t.Address,
357357
}
358358
},
359-
currentTimestamp: func() int64 {
359+
currentTimestamp: func() int64 {
360360
return time.Now().Unix()
361361
},
362+
tokenPublisher: s.tokenManager.GetTokenPublisher(),
362363
}
363364
}
364365

services/wallet/activityfetcher/alchemy/manager.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,24 @@ import (
99
"github.com/ethereum/go-ethereum/common"
1010
geth_rpc "github.com/ethereum/go-ethereum/rpc"
1111

12+
"github.com/status-im/status-go/pkg/pubsub"
1213
wc "github.com/status-im/status-go/services/wallet/common"
1314
"github.com/status-im/status-go/services/wallet/thirdparty"
1415
alchemy "github.com/status-im/status-go/services/wallet/thirdparty/activity/alchemy"
16+
"github.com/status-im/status-go/services/wallet/token/tokenevent"
1517
)
1618

1719
type Manager struct {
18-
client *alchemy.Client
19-
persistence *alchemy.Persistence
20+
client *alchemy.Client
21+
persistence *alchemy.Persistence
22+
tokenPublisher *pubsub.Publisher
2023
}
2124

22-
func NewManager(client *alchemy.Client, persistence *alchemy.Persistence) *Manager {
25+
func NewManager(client *alchemy.Client, persistence *alchemy.Persistence, tokenPublisher *pubsub.Publisher) *Manager {
2326
return &Manager{
24-
client: client,
25-
persistence: persistence,
27+
client: client,
28+
persistence: persistence,
29+
tokenPublisher: tokenPublisher,
2630
}
2731
}
2832

@@ -44,7 +48,6 @@ func (m *Manager) GetLastFetchedBlockAndTimestamp(ctx context.Context, chainID u
4448

4549
// FetchActivity orchestrates fetching, persistence, and type conversion.
4650
func (m *Manager) FetchActivity(ctx context.Context, chainID uint64, parameters thirdparty.ActivityFetchParameters, cursor string, limit int) (thirdparty.ActivityEntryContainer, error) {
47-
4851
transfers, nextCursor, err := m.client.FetchTransfers(ctx, chainID, parameters, cursor, limit)
4952
if err != nil {
5053
return thirdparty.ActivityEntryContainer{}, err
@@ -55,6 +58,18 @@ func (m *Manager) FetchActivity(ctx context.Context, chainID uint64, parameters
5558
return thirdparty.ActivityEntryContainer{}, err
5659
}
5760

61+
// Request async token discovery for ERC20 transfers to make sure we have their metadata
62+
if m.tokenPublisher != nil {
63+
for _, transfer := range transfers {
64+
if transfer.Category == alchemy.TransferCategoryErc20 && transfer.RawContract.Address != nil {
65+
pubsub.Publish(m.tokenPublisher, tokenevent.TokenDiscoveryRequestEvent{
66+
ChainID: chainID,
67+
Address: *transfer.RawContract.Address,
68+
})
69+
}
70+
}
71+
}
72+
5873
items := alchemy.TransfersToThirdpartyActivityEntries(transfers, chainID, parameters.Address)
5974

6075
return thirdparty.ActivityEntryContainer{

services/wallet/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ func NewService(
261261
alchemyEthClientGetter := rpc.NewProviderChainClientGetter(common.SmartProxyAlchemy, rpcClient)
262262
alchemyFetcherDb := activityfetcher_alchemy.NewPersistence(db)
263263
alchemyFetcherClient := activityfetcher_alchemy.NewClient(alchemyEthClientGetter)
264-
alchemyFetcherManager := alchemymanager.NewManager(alchemyFetcherClient, alchemyFetcherDb)
264+
alchemyFetcherManager := alchemymanager.NewManager(alchemyFetcherClient, alchemyFetcherDb, tokenManager.GetTokenPublisher())
265265
activityFetcherManager := activityfetcher.NewManager(alchemyFetcherManager)
266266
activityFetcherService := activityfetcher.NewService(activityFetcherManager, rpcClient.GetNetworkManager(), accountsDB, accountsPublisher, rpcClient, feed)
267267

services/wallet/thirdparty/activity/alchemy/conversions.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,22 +172,30 @@ func extractTransferTokenAndValue(t Transfer, chainID uint64) []transferTokenAnd
172172
Value: (*hexutil.Big)(t.RawContract.Value.Int),
173173
})
174174
case TransferCategoryErc721:
175+
var tokenID *hexutil.Big
176+
if t.TokenID != nil && t.TokenID.Int != nil {
177+
tokenID = (*hexutil.Big)(t.TokenID.Int)
178+
}
175179
transfersData = append(transfersData, transferTokenAndValue{
176180
Token: ac.Token{
177181
TokenType: ac.Erc721,
178182
ChainID: wCommon.ChainID(chainID),
179183
Address: *t.RawContract.Address,
180-
TokenID: (*hexutil.Big)(t.TokenID.Int),
184+
TokenID: tokenID,
181185
},
182186
})
183187
case TransferCategoryErc1155:
184188
for _, m := range t.Erc1155Metadata {
189+
var tokenID *hexutil.Big
190+
if m.TokenID != nil && m.TokenID.Int != nil {
191+
tokenID = (*hexutil.Big)(m.TokenID.Int)
192+
}
185193
transfersData = append(transfersData, transferTokenAndValue{
186194
Token: ac.Token{
187195
TokenType: ac.Erc1155,
188196
ChainID: wCommon.ChainID(chainID),
189197
Address: *t.RawContract.Address,
190-
TokenID: (*hexutil.Big)(m.TokenID.Int),
198+
TokenID: tokenID,
191199
},
192200
Value: (*hexutil.Big)(m.Value.Int),
193201
})

services/wallet/thirdparty/activity/alchemy/types.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const MaxAssetTransfersCount = 1000
1919
// AlchemyBigInt wraps VarHexBigInt to handle Alchemy API inconsistency
2020
// it sometimes returns plain decimal numbers instead of hexadecimal
2121
type AlchemyBigInt struct {
22-
*bigint.VarHexBigInt
22+
bigint.VarHexBigInt
2323
}
2424

2525
func (a *AlchemyBigInt) UnmarshalJSON(data []byte) error {
@@ -31,16 +31,15 @@ func (a *AlchemyBigInt) UnmarshalJSON(data []byte) error {
3131
// alchemy sometimes returns plain numbers like "0" or "1" without "0x" prefix
3232
// so we need to take this into account
3333
if len(str) >= 2 && str[0:2] == "0x" {
34-
// hex string case
35-
a.VarHexBigInt = &bigint.VarHexBigInt{}
34+
// hex string case - unmarshal directly into embedded struct
3635
return a.VarHexBigInt.UnmarshalJSON(data)
3736
} else {
3837
// plain decimal number
3938
val := new(big.Int)
4039
if _, ok := val.SetString(str, 10); !ok {
4140
return fmt.Errorf("invalid decimal number: %s", str)
4241
}
43-
a.VarHexBigInt = &bigint.VarHexBigInt{Int: val}
42+
a.VarHexBigInt.Int = val
4443
return nil
4544
}
4645
}

services/wallet/token/token.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ import (
3838
"github.com/status-im/status-go/services/wallet/community"
3939
tokenlists "github.com/status-im/status-go/services/wallet/token/token-lists"
4040
"github.com/status-im/status-go/services/wallet/token/token-lists/fetcher"
41+
"github.com/status-im/status-go/services/wallet/token/tokenevent"
4142
tokenTypes "github.com/status-im/status-go/services/wallet/token/types"
4243
"github.com/status-im/status-go/services/wallet/walletevent"
44+
"github.com/status-im/status-go/signal"
4345
)
4446

4547
const (
@@ -76,6 +78,7 @@ type ManagerInterface interface {
7678
FindOrCreateTokenByAddress(ctx context.Context, chainID uint64, address common.Address) *tokenTypes.Token
7779
MarkAsPreviouslyOwnedToken(token *tokenTypes.Token, owner common.Address) (bool, error)
7880
SignalCommunityTokenReceived(address common.Address, txHash common.Hash, value *big.Int, t *tokenTypes.Token, isFirst bool)
81+
GetTokenPublisher() *pubsub.Publisher
7982
}
8083

8184
// Manager is used for accessing token store. It changes the token store based on overridden tokens
@@ -90,6 +93,7 @@ type Manager struct {
9093
walletFeed *event.Feed
9194
accountsDB *accounts.Database
9295
accountsPublisher *pubsub.Publisher
96+
tokenPublisher *pubsub.Publisher
9397
tokenBalancesStorage TokenBalancesStorage
9498

9599
tokenLists *tokenlists.TokenLists
@@ -106,6 +110,7 @@ func NewTokenManager(
106110
mediaServer *server.MediaServer,
107111
walletFeed *event.Feed,
108112
accountsPublisher *pubsub.Publisher,
113+
tokenPublisher *pubsub.Publisher,
109114
accountsDB *accounts.Database,
110115
tokenBalancesStorage TokenBalancesStorage,
111116
) *Manager {
@@ -127,6 +132,7 @@ func NewTokenManager(
127132
mediaServer: mediaServer,
128133
walletFeed: walletFeed,
129134
accountsPublisher: accountsPublisher,
135+
tokenPublisher: tokenPublisher,
130136
accountsDB: accountsDB,
131137
tokenBalancesStorage: tokenBalancesStorage,
132138
tokenLists: tokensLists,
@@ -136,6 +142,7 @@ func NewTokenManager(
136142
func (tm *Manager) Start(ctx context.Context, autoRefreshInterval time.Duration, autoRefreshCheckInterval time.Duration) {
137143
tm.stopCh = make(chan struct{})
138144
tm.startAccountsWatcher()
145+
tm.startTokenDiscoveryWatcher()
139146

140147
// For now we don't have the list of tokens lists remotely set so we're uisng the harcoded default lists. Once we have it
141148
//we will just need to update the empty string with the correct URL.
@@ -165,6 +172,35 @@ func (tm *Manager) startAccountsWatcher() {
165172
}()
166173
}
167174

175+
func (tm *Manager) startTokenDiscoveryWatcher() {
176+
if tm.tokenPublisher == nil {
177+
return
178+
}
179+
180+
ch, unsubFn := pubsub.Subscribe[tokenevent.TokenDiscoveryRequestEvent](tm.tokenPublisher, 100)
181+
go func() {
182+
defer gocommon.LogOnPanic()
183+
defer unsubFn()
184+
for {
185+
select {
186+
case <-tm.stopCh:
187+
return
188+
case event, ok := <-ch:
189+
if !ok {
190+
return
191+
}
192+
tm.handleTokenDiscoveryRequest(context.Background(), event)
193+
}
194+
}
195+
}()
196+
}
197+
198+
func (tm *Manager) handleTokenDiscoveryRequest(ctx context.Context, event tokenevent.TokenDiscoveryRequestEvent) {
199+
// Use existing FindOrCreateTokenByAddress logic
200+
// This will fetch token metadata and send TokenListsUpdated signal to frontend
201+
tm.FindOrCreateTokenByAddress(ctx, event.ChainID, event.Address)
202+
}
203+
168204
func (tm *Manager) Stop() {
169205
if tm.stopCh != nil {
170206
close(tm.stopCh)
@@ -173,6 +209,10 @@ func (tm *Manager) Stop() {
173209
tm.tokenLists.Stop()
174210
}
175211

212+
func (tm *Manager) GetTokenPublisher() *pubsub.Publisher {
213+
return tm.tokenPublisher
214+
}
215+
176216
// overrideTokensInPlace overrides tokens in the store with the ones from the networks
177217
// BEWARE: overridden tokens will have their original address removed and replaced by the one in networks
178218
func overrideTokensInPlace(networks []params.Network, tokens []*tokenTypes.Token) {
@@ -298,6 +338,8 @@ func (tm *Manager) FindOrCreateTokenByAddress(ctx context.Context, chainID uint6
298338
}
299339

300340
tm.discoverTokenCommunityID(ctx, token, address)
341+
signal.SendWalletEvent(signal.TokenListsUpdated, nil)
342+
301343
return token
302344
}
303345

@@ -437,7 +479,7 @@ func (tm *Manager) getNativeTokens() ([]*tokenTypes.Token, error) {
437479
}
438480

439481
func (tm *Manager) GetAllTokens() ([]*tokenTypes.Token, error) {
440-
allTokens, err := tm.GetCustoms(true)
482+
allTokens, err := tm.GetCustoms(false)
441483
if err != nil {
442484
logutils.ZapLogger().Error("can't fetch custom tokens", zap.Error(err))
443485
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package tokenevent
2+
3+
import (
4+
"github.com/ethereum/go-ethereum/common"
5+
)
6+
7+
// TokenDiscoveryRequestEvent is emitted when a component needs token metadata.
8+
// The TokenManager will asynchronously discover the token.
9+
type TokenDiscoveryRequestEvent struct {
10+
ChainID uint64
11+
Address common.Address
12+
}

0 commit comments

Comments
 (0)