diff --git a/protocol/activity_center.go b/protocol/activity_center.go index a78c00867..a53191ec2 100644 --- a/protocol/activity_center.go +++ b/protocol/activity_center.go @@ -35,6 +35,7 @@ const ( ActivityCenterNotificationTypeSetSignerFailed ActivityCenterNotificationTypeSetSignerDeclined ActivityCenterNotificationTypeShareAccounts + ActivityCenterNotificationTypeCommunityTokenReceived ) type ActivityCenterMembershipStatus int diff --git a/protocol/messenger.go b/protocol/messenger.go index ebde0c9a7..064022e7d 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -456,7 +456,7 @@ func NewMessenger( if c.tokenManager != nil { managerOptions = append(managerOptions, communities.WithTokenManager(c.tokenManager)) } else if c.rpcClient != nil { - tokenManager := token.NewTokenManager(c.walletDb, c.rpcClient, community.NewManager(database, c.httpServer, nil), c.rpcClient.NetworkManager, database, c.httpServer) + tokenManager := token.NewTokenManager(c.walletDb, c.rpcClient, community.NewManager(database, c.httpServer, nil), c.rpcClient.NetworkManager, database, c.httpServer, nil) managerOptions = append(managerOptions, communities.WithTokenManager(communities.NewDefaultTokenManager(tokenManager))) } diff --git a/services/ext/api.go b/services/ext/api.go index e6afc83e0..93dddd651 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -1669,6 +1669,11 @@ func (api *PublicAPI) RegisterReceivedOwnershipNotification(communityID string) return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeOwnershipReceived, false) } +// Returns response with AC notification when community token is received +func (api *PublicAPI) RegisterReceivedCommunityTokenNotification(communityID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeCommunityTokenReceived, false) +} + // Returns response with AC notification when setting signer is failed func (api *PublicAPI) RegisterSetSignerFailedNotification(communityID string) (*protocol.MessengerResponse, error) { return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeSetSignerFailed, false) diff --git a/services/wallet/community/manager.go b/services/wallet/community/manager.go index 9444955b8..58afcede6 100644 --- a/services/wallet/community/manager.go +++ b/services/wallet/community/manager.go @@ -49,7 +49,7 @@ func (cm *Manager) GetCommunityInfo(id string) (*thirdparty.CommunityInfo, *Info return nil, nil, err } if cm.mediaServer != nil && communityInfo != nil && len(communityInfo.CommunityImagePayload) > 0 { - communityInfo.CommunityImage = cm.mediaServer.MakeWalletCommunityImagesURL(id) + communityInfo.CommunityImage = cm.GetCommunityImageURL(id) } return communityInfo, state, err } @@ -81,16 +81,30 @@ func (cm *Manager) FetchCommunityInfo(communityID string) (*thirdparty.Community func (cm *Manager) FetchCommunityMetadataAsync(communityID string) { go func() { - communityInfo, err := cm.FetchCommunityInfo(communityID) + communityInfo, err := cm.FetchCommunityMetadata(communityID) if err != nil { log.Error("FetchCommunityInfo failed", "communityID", communityID, "err", err) - return } - cm.signalUpdatedCommunityMetadata(communityID, communityInfo) }() } +func (cm *Manager) FetchCommunityMetadata(communityID string) (*thirdparty.CommunityInfo, error) { + communityInfo, err := cm.FetchCommunityInfo(communityID) + if err != nil { + return nil, err + } + _ = cm.setCommunityInfo(communityID, communityInfo) + return communityInfo, err +} + +func (cm *Manager) GetCommunityImageURL(communityID string) string { + if cm.mediaServer != nil { + return cm.mediaServer.MakeWalletCommunityImagesURL(communityID) + } + return "" +} + func (cm *Manager) signalUpdatedCommunityMetadata(communityID string, communityInfo *thirdparty.CommunityInfo) { if communityInfo == nil { return @@ -99,7 +113,7 @@ func (cm *Manager) signalUpdatedCommunityMetadata(communityID string, communityI ID: communityID, Name: communityInfo.CommunityName, Color: communityInfo.CommunityColor, - Image: communityInfo.CommunityImage, // TODO make media server url after merging community token media server changes + Image: cm.GetCommunityImageURL(communityID), } payload, err := json.Marshal(data) diff --git a/services/wallet/service.go b/services/wallet/service.go index 582e964dc..a94603129 100644 --- a/services/wallet/service.go +++ b/services/wallet/service.go @@ -101,7 +101,7 @@ func NewService( communityManager := community.NewManager(db, mediaServer, feed) balanceCacher := balance.NewCacherWithTTL(5 * time.Minute) - tokenManager := token.NewTokenManager(db, rpcClient, communityManager, rpcClient.NetworkManager, appDB, mediaServer) + tokenManager := token.NewTokenManager(db, rpcClient, communityManager, rpcClient.NetworkManager, appDB, mediaServer, feed) savedAddressesManager := &SavedAddressesManager{db: db} transactionManager := transfer.NewTransactionManager(db, gethManager, transactor, config, accountsDB, pendingTxManager, feed) transferController := transfer.NewTransferController(db, accountsDB, rpcClient, accountFeed, feed, transactionManager, pendingTxManager, diff --git a/services/wallet/token/token.go b/services/wallet/token/token.go index b325ca769..5b3b8f837 100644 --- a/services/wallet/token/token.go +++ b/services/wallet/token/token.go @@ -3,6 +3,7 @@ package token import ( "context" "database/sql" + "encoding/json" "errors" "math/big" "strconv" @@ -13,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/contracts" "github.com/status-im/status-go/contracts/community-tokens/assets" @@ -29,6 +31,11 @@ import ( "github.com/status-im/status-go/services/utils" "github.com/status-im/status-go/services/wallet/async" "github.com/status-im/status-go/services/wallet/community" + "github.com/status-im/status-go/services/wallet/walletevent" +) + +const ( + EventCommunityTokenReceived walletevent.EventType = "wallet-community-token-received" ) var requestTimeout = 20 * time.Second @@ -54,6 +61,17 @@ type Token struct { TokenListID string `json:"tokenListId"` } +type ReceivedToken struct { + Address common.Address `json:"address"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Image string `json:"image,omitempty"` + ChainID uint64 `json:"chainId"` + CommunityData *community.Data `json:"community_data,omitempty"` + Balance *big.Int `json:"balance"` + TxHash common.Hash `json:"txHash"` +} + func (t *Token) IsNative() bool { return t.Address == nativeChainAddress } @@ -84,6 +102,7 @@ type Manager struct { communityTokensDB *communitytokens.Database communityManager *community.Manager mediaServer *server.MediaServer + walletFeed *event.Feed tokens []*Token @@ -112,6 +131,7 @@ func NewTokenManager( networkManager *network.Manager, appDB *sql.DB, mediaServer *server.MediaServer, + walletFeed *event.Feed, ) *Manager { maker, _ := contracts.NewContractMaker(RPCClient) stores := []store{newUniswapStore(), newDefaultStore()} @@ -148,6 +168,7 @@ func NewTokenManager( communityTokensDB: communitytokens.NewCommunityTokensDatabase(appDB), tokens: tokens, mediaServer: mediaServer, + walletFeed: walletFeed, } } @@ -349,6 +370,10 @@ func (tm *Manager) discoverTokenCommunityID(ctx context.Context, token *Token, a } communityID := eth_node_types.EncodeHex(communityIDHex) + token.CommunityData = &community.Data{ + ID: communityID, + } + _, err = update.Exec(communityID, token.ChainID, token.Address) if err != nil { log.Error("Cannot update community id", err) @@ -549,15 +574,7 @@ func (tm *Manager) getTokensFromDB(query string, args ...any) ([]*Token, error) } } - if tm.communityManager != nil && token.CommunityData != nil { - communityInfo, _, err := tm.communityManager.GetCommunityInfo(token.CommunityData.ID) - if err == nil && communityInfo != nil { - // Fetched data from cache. Cache is refreshed during every wallet token list call. - token.CommunityData.Name = communityInfo.CommunityName - token.CommunityData.Color = communityInfo.CommunityColor - token.CommunityData.Image = communityInfo.CommunityImage - } - } + _ = tm.fillCommunityData(token) rst = append(rst, token) } @@ -785,3 +802,64 @@ func (tm *Manager) GetBalancesAtByChain(parent context.Context, clients map[uint } return response, group.Error() } + +func (tm *Manager) SignalCommunityTokenReceived(address common.Address, txHash common.Hash, value *big.Int, t *Token) { + if tm.walletFeed == nil || t == nil || t.CommunityData == nil { + return + } + + if len(t.CommunityData.Name) == 0 { + _ = tm.fillCommunityData(t) + } + if len(t.CommunityData.Name) == 0 && tm.communityManager != nil { + communityData, _ := tm.communityManager.FetchCommunityMetadata(t.CommunityData.ID) + if communityData != nil { + t.CommunityData.Name = communityData.CommunityName + t.CommunityData.Color = communityData.CommunityColor + t.CommunityData.Image = tm.communityManager.GetCommunityImageURL(t.CommunityData.ID) + } + } + + receivedToken := ReceivedToken{ + Address: t.Address, + Name: t.Name, + Symbol: t.Symbol, + Image: t.Image, + ChainID: t.ChainID, + CommunityData: t.CommunityData, + Balance: value, + TxHash: txHash, + } + + encodedMessage, err := json.Marshal(receivedToken) + if err != nil { + return + } + + tm.walletFeed.Send(walletevent.Event{ + Type: EventCommunityTokenReceived, + ChainID: t.ChainID, + Accounts: []common.Address{ + address, + }, + Message: string(encodedMessage), + }) +} + +func (tm *Manager) fillCommunityData(token *Token) error { + if token == nil || token.CommunityData == nil || tm.communityManager == nil { + return nil + } + + communityInfo, _, err := tm.communityManager.GetCommunityInfo(token.CommunityData.ID) + if err != nil { + return err + } + if err == nil && communityInfo != nil { + // Fetched data from cache. Cache is refreshed during every wallet token list call. + token.CommunityData.Name = communityInfo.CommunityName + token.CommunityData.Color = communityInfo.CommunityColor + token.CommunityData.Image = communityInfo.CommunityImage + } + return nil +} diff --git a/services/wallet/transfer/commands.go b/services/wallet/transfer/commands.go index cc847e68b..14c6d41d2 100644 --- a/services/wallet/transfer/commands.go +++ b/services/wallet/transfer/commands.go @@ -420,8 +420,13 @@ func (c *transfersCommand) processUnknownErc20CommunityTransactions(ctx context. if tx.Type == w_common.Erc20Transfer && tx.Transaction.To() != nil { // Find token in db or if this is a community token, find its metadata token := c.tokenManager.FindOrCreateTokenByAddress(ctx, tx.NetworkID, *tx.Transaction.To()) - if token != nil && (token.Verified || token.CommunityData != nil) { - _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, tx.Address) + if token != nil { + if token.Verified || token.CommunityData != nil { + _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, tx.Address) + } + if token.CommunityData != nil { + go c.tokenManager.SignalCommunityTokenReceived(tx.Address, tx.ID, tx.Transaction.Value(), token) + } } } } diff --git a/services/wallet/transfer/commands_sequential_test.go b/services/wallet/transfer/commands_sequential_test.go index ac808b5fe..74cef4dac 100644 --- a/services/wallet/transfer/commands_sequential_test.go +++ b/services/wallet/transfer/commands_sequential_test.go @@ -937,8 +937,7 @@ func TestFindBlocksCommand(t *testing.T) { } client, _ := statusRpc.NewClient(nil, 1, params.UpstreamRPCConfig{Enabled: false, URL: ""}, []params.Network{}, db) client.SetClient(tc.NetworkID(), tc) - tokenManager := token.NewTokenManager(db, client, community.NewManager(appdb, nil, nil), network.NewManager(appdb), appdb, mediaServer) - + tokenManager := token.NewTokenManager(db, client, community.NewManager(appdb, nil, nil), network.NewManager(appdb), appdb, mediaServer, nil) tokenManager.SetTokens([]*token.Token{ { Address: tokenTXXAddress, @@ -1063,7 +1062,7 @@ func TestFetchTransfersForLoadedBlocks(t *testing.T) { client, _ := statusRpc.NewClient(nil, 1, params.UpstreamRPCConfig{Enabled: false, URL: ""}, []params.Network{}, db) client.SetClient(tc.NetworkID(), tc) - tokenManager := token.NewTokenManager(db, client, community.NewManager(appdb, nil, nil), network.NewManager(appdb), appdb, mediaServer) + tokenManager := token.NewTokenManager(db, client, community.NewManager(appdb, nil, nil), network.NewManager(appdb), appdb, mediaServer, nil) tokenManager.SetTokens([]*token.Token{ { @@ -1183,7 +1182,7 @@ func TestFetchNewBlocksCommand_findBlocksWithEthTransfers(t *testing.T) { client, _ := statusRpc.NewClient(nil, 1, params.UpstreamRPCConfig{Enabled: false, URL: ""}, []params.Network{}, db) client.SetClient(tc.NetworkID(), tc) - tokenManager := token.NewTokenManager(db, client, community.NewManager(appdb, nil, nil), network.NewManager(appdb), appdb, mediaServer) + tokenManager := token.NewTokenManager(db, client, community.NewManager(appdb, nil, nil), network.NewManager(appdb), appdb, mediaServer, nil) tokenManager.SetTokens([]*token.Token{ { @@ -1260,7 +1259,8 @@ func TestFetchNewBlocksCommand(t *testing.T) { client, _ := statusRpc.NewClient(nil, 1, params.UpstreamRPCConfig{Enabled: false, URL: ""}, []params.Network{}, db) client.SetClient(tc.NetworkID(), tc) - tokenManager := token.NewTokenManager(db, client, community.NewManager(appdb, nil, nil), network.NewManager(appdb), appdb, mediaServer) + + tokenManager := token.NewTokenManager(db, client, community.NewManager(appdb, nil, nil), network.NewManager(appdb), appdb, mediaServer, nil) tokenManager.SetTokens([]*token.Token{ {