feat: Community token received notification (#4682)

This commit is contained in:
Cuteivist 2024-02-19 14:55:38 +01:00 committed by GitHub
parent 54d0cf28c7
commit a866b8025e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1090 additions and 763 deletions

View File

@ -7,6 +7,7 @@ import (
"github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/verification" "github.com/status-im/status-go/protocol/verification"
"github.com/status-im/status-go/services/wallet/thirdparty"
) )
// The activity center is a place where we store incoming notifications before // The activity center is a place where we store incoming notifications before
@ -36,6 +37,7 @@ const (
ActivityCenterNotificationTypeSetSignerDeclined ActivityCenterNotificationTypeSetSignerDeclined
ActivityCenterNotificationTypeShareAccounts ActivityCenterNotificationTypeShareAccounts
ActivityCenterNotificationTypeCommunityTokenReceived ActivityCenterNotificationTypeCommunityTokenReceived
ActivityCenterNotificationTypeFirstCommunityTokenReceived
) )
type ActivityCenterMembershipStatus int type ActivityCenterMembershipStatus int
@ -60,6 +62,22 @@ const (
var ErrInvalidActivityCenterNotification = errors.New("invalid activity center notification") var ErrInvalidActivityCenterNotification = errors.New("invalid activity center notification")
type ActivityTokenData struct {
ChainID uint64 `json:"chainId,omitempty"`
CollectibleID thirdparty.CollectibleUniqueID `json:"collectibleId,omitempty"`
TxHash string `json:"txHash,omitempty"`
WalletAddress string `json:"walletAddress,omitempty"`
IsFirst bool `json:"isFirst,omitempty"`
// Community data
CommunityID string `json:"communityId,omitempty"`
// Token data
Amount string `json:"amount,omitempty"`
Name string `json:"name,omitempty"`
Symbol string `json:"symbol,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
TokenType int `json:"tokenType,omitempty"`
}
type ActivityCenterNotification struct { type ActivityCenterNotification struct {
ID types.HexBytes `json:"id"` ID types.HexBytes `json:"id"`
ChatID string `json:"chatId"` ChatID string `json:"chatId"`
@ -77,6 +95,7 @@ type ActivityCenterNotification struct {
Deleted bool `json:"deleted"` Deleted bool `json:"deleted"`
Accepted bool `json:"accepted"` Accepted bool `json:"accepted"`
ContactVerificationStatus verification.RequestStatus `json:"contactVerificationStatus"` ContactVerificationStatus verification.RequestStatus `json:"contactVerificationStatus"`
TokenData *ActivityTokenData `json:"tokenData"`
//Used for synchronization. Each update should increment the UpdatedAt. //Used for synchronization. Each update should increment the UpdatedAt.
//The value should represent the time when the update occurred. //The value should represent the time when the update occurred.
UpdatedAt uint64 `json:"updatedAt"` UpdatedAt uint64 `json:"updatedAt"`

View File

@ -12,7 +12,7 @@ import (
) )
const allFieldsForTableActivityCenterNotification = `id, timestamp, notification_type, chat_id, read, dismissed, accepted, message, author, const allFieldsForTableActivityCenterNotification = `id, timestamp, notification_type, chat_id, read, dismissed, accepted, message, author,
reply_message, community_id, membership_status, contact_verification_status, deleted, updated_at` reply_message, community_id, membership_status, contact_verification_status, token_data, deleted, updated_at`
var emptyNotifications = make([]*ActivityCenterNotification, 0) var emptyNotifications = make([]*ActivityCenterNotification, 0)
@ -125,6 +125,15 @@ func (db sqlitePersistence) SaveActivityCenterNotification(notification *Activit
} }
} }
// encode token data
var encodedTokenData []byte
if notification.TokenData != nil {
encodedTokenData, err = json.Marshal(notification.TokenData)
if err != nil {
return 0, err
}
}
result, err := tx.Exec(` result, err := tx.Exec(`
INSERT OR REPLACE INSERT OR REPLACE
INTO activity_center_notifications ( INTO activity_center_notifications (
@ -141,10 +150,11 @@ func (db sqlitePersistence) SaveActivityCenterNotification(notification *Activit
read, read,
accepted, accepted,
dismissed, dismissed,
token_data,
deleted, deleted,
updated_at updated_at
) )
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`, `,
notification.ID, notification.ID,
notification.Timestamp, notification.Timestamp,
@ -159,9 +169,9 @@ func (db sqlitePersistence) SaveActivityCenterNotification(notification *Activit
notification.Read, notification.Read,
notification.Accepted, notification.Accepted,
notification.Dismissed, notification.Dismissed,
encodedTokenData,
notification.Deleted, notification.Deleted,
notification.UpdatedAt, notification.UpdatedAt,
notification.ID,
) )
if err != nil { if err != nil {
return 0, err return 0, err
@ -187,6 +197,7 @@ func (db sqlitePersistence) parseRowFromTableActivityCenterNotification(rows *sq
var communityID sql.NullString var communityID sql.NullString
var messageBytes []byte var messageBytes []byte
var replyMessageBytes []byte var replyMessageBytes []byte
var tokenDataBytes []byte
var author sql.NullString var author sql.NullString
notification := &ActivityCenterNotification{} notification := &ActivityCenterNotification{}
err := rows.Scan( err := rows.Scan(
@ -203,6 +214,7 @@ func (db sqlitePersistence) parseRowFromTableActivityCenterNotification(rows *sq
&communityID, &communityID,
&notification.MembershipStatus, &notification.MembershipStatus,
&notification.ContactVerificationStatus, &notification.ContactVerificationStatus,
&tokenDataBytes,
&notification.Deleted, &notification.Deleted,
&notification.UpdatedAt, &notification.UpdatedAt,
) )
@ -222,6 +234,13 @@ func (db sqlitePersistence) parseRowFromTableActivityCenterNotification(rows *sq
notification.Author = author.String notification.Author = author.String
} }
if len(tokenDataBytes) > 0 {
err = json.Unmarshal(tokenDataBytes, &notification.TokenData)
if err != nil {
return nil, err
}
}
if len(messageBytes) > 0 { if len(messageBytes) > 0 {
err = json.Unmarshal(messageBytes, &notification.Message) err = json.Unmarshal(messageBytes, &notification.Message)
if err != nil { if err != nil {
@ -251,6 +270,7 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRow(row *sql.Row)
var lastMessageBytes []byte var lastMessageBytes []byte
var messageBytes []byte var messageBytes []byte
var replyMessageBytes []byte var replyMessageBytes []byte
var tokenDataBytes []byte
var name sql.NullString var name sql.NullString
var author sql.NullString var author sql.NullString
notification := &ActivityCenterNotification{} notification := &ActivityCenterNotification{}
@ -271,6 +291,7 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRow(row *sql.Row)
&notification.ContactVerificationStatus, &notification.ContactVerificationStatus,
&name, &name,
&author, &author,
&tokenDataBytes,
&notification.UpdatedAt) &notification.UpdatedAt)
if err != nil { if err != nil {
@ -293,6 +314,13 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRow(row *sql.Row)
notification.Author = author.String notification.Author = author.String
} }
if len(tokenDataBytes) > 0 {
err = json.Unmarshal(tokenDataBytes, &notification.TokenData)
if err != nil {
return nil, err
}
}
// Restore last message // Restore last message
if lastMessageBytes != nil { if lastMessageBytes != nil {
lastMessage := common.NewMessage() lastMessage := common.NewMessage()
@ -332,6 +360,7 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRows(rows *sql.Ro
var lastMessageBytes []byte var lastMessageBytes []byte
var messageBytes []byte var messageBytes []byte
var replyMessageBytes []byte var replyMessageBytes []byte
var tokenDataBytes []byte
var name sql.NullString var name sql.NullString
var author sql.NullString var author sql.NullString
notification := &ActivityCenterNotification{} notification := &ActivityCenterNotification{}
@ -351,6 +380,7 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRows(rows *sql.Ro
&notification.ContactVerificationStatus, &notification.ContactVerificationStatus,
&name, &name,
&author, &author,
&tokenDataBytes,
&latestCursor, &latestCursor,
&notification.UpdatedAt) &notification.UpdatedAt)
if err != nil { if err != nil {
@ -373,6 +403,14 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRows(rows *sql.Ro
notification.Author = author.String notification.Author = author.String
} }
if len(tokenDataBytes) > 0 {
tokenData := &ActivityTokenData{}
if err = json.Unmarshal(tokenDataBytes, &tokenData); err != nil {
return "", nil, err
}
notification.TokenData = tokenData
}
// Restore last message // Restore last message
if lastMessageBytes != nil { if lastMessageBytes != nil {
lastMessage := common.NewMessage() lastMessage := common.NewMessage()
@ -503,6 +541,7 @@ func (db sqlitePersistence) buildActivityCenterQuery(tx *sql.Tx, params activity
a.contact_verification_status, a.contact_verification_status,
c.name, c.name,
a.author, a.author,
a.token_data,
substr('0000000000000000000000000000000000000000000000000000000000000000' || a.timestamp, -64, 64) || hex(a.id) as cursor, substr('0000000000000000000000000000000000000000000000000000000000000000' || a.timestamp, -64, 64) || hex(a.id) as cursor,
a.updated_at a.updated_at
FROM activity_center_notifications a FROM activity_center_notifications a
@ -623,6 +662,7 @@ func (db sqlitePersistence) GetActivityCenterNotificationsByID(ids []types.HexBy
a.contact_verification_status, a.contact_verification_status,
c.name, c.name,
a.author, a.author,
a.token_data,
substr('0000000000000000000000000000000000000000000000000000000000000000' || a.timestamp, -64, 64) || hex(a.id) as cursor, substr('0000000000000000000000000000000000000000000000000000000000000000' || a.timestamp, -64, 64) || hex(a.id) as cursor,
a.updated_at a.updated_at
FROM activity_center_notifications a FROM activity_center_notifications a
@ -663,6 +703,7 @@ func (db sqlitePersistence) GetActivityCenterNotificationByID(id types.HexBytes)
a.contact_verification_status, a.contact_verification_status,
c.name, c.name,
a.author, a.author,
a.token_data,
a.updated_at a.updated_at
FROM activity_center_notifications a FROM activity_center_notifications a
LEFT JOIN chats c LEFT JOIN chats c
@ -1295,6 +1336,7 @@ func (db sqlitePersistence) ActiveContactRequestNotification(contactID string) (
a.contact_verification_status, a.contact_verification_status,
c.name, c.name,
a.author, a.author,
a.token_data,
a.updated_at a.updated_at
FROM activity_center_notifications a FROM activity_center_notifications a
LEFT JOIN chats c ON c.id = a.chat_id LEFT JOIN chats c ON c.id = a.chat_id

View File

@ -4189,6 +4189,15 @@ func extractQuotedImages(messages []*common.Message, s *server.MediaServer) []st
return quotedImages return quotedImages
} }
func (m *Messenger) prepareTokenData(tokenData *ActivityTokenData, s *server.MediaServer) error {
if tokenData.TokenType == int(protobuf.CommunityTokenType_ERC721) {
tokenData.ImageURL = s.MakeWalletCollectibleImagesURL(tokenData.CollectibleID)
} else if tokenData.TokenType == int(protobuf.CommunityTokenType_ERC20) {
tokenData.ImageURL = s.MakeCommunityTokenImagesURL(tokenData.CommunityID, tokenData.ChainID, tokenData.Symbol)
}
return nil
}
func (m *Messenger) prepareMessage(msg *common.Message, s *server.MediaServer) error { func (m *Messenger) prepareMessage(msg *common.Message, s *server.MediaServer) error {
if msg.QuotedMessage != nil && msg.QuotedMessage.ContentType == int64(protobuf.ChatMessage_IMAGE) { if msg.QuotedMessage != nil && msg.QuotedMessage.ContentType == int64(protobuf.ChatMessage_IMAGE) {
msg.QuotedMessage.ImageLocalURL = s.MakeImageURL(msg.QuotedMessage.ID) msg.QuotedMessage.ImageLocalURL = s.MakeImageURL(msg.QuotedMessage.ID)

View File

@ -70,6 +70,14 @@ func (m *Messenger) ActivityCenterNotifications(request ActivityCenterNotificati
} }
} }
} }
if notification.TokenData != nil {
if notification.Type == ActivityCenterNotificationTypeCommunityTokenReceived || notification.Type == ActivityCenterNotificationTypeFirstCommunityTokenReceived {
err = m.prepareTokenData(notification.TokenData, m.httpServer)
if err != nil {
return nil, err
}
}
}
} }
} }

View File

@ -4241,7 +4241,13 @@ func (m *Messenger) PromoteSelfToControlNode(communityID types.HexBytes) (*Messe
return &response, nil return &response, nil
} }
func (m *Messenger) CreateResponseWithACNotification(communityID string, acType ActivityCenterType, isRead bool) (*MessengerResponse, error) { func (m *Messenger) CreateResponseWithACNotification(communityID string, acType ActivityCenterType, isRead bool, tokenDataJSON string) (*MessengerResponse, error) {
tokenData := ActivityTokenData{}
err := json.Unmarshal([]byte(tokenDataJSON), &tokenData)
if len(tokenDataJSON) > 0 && err != nil {
// Only return error when activityDataString is not empty
return nil, err
}
// Activity center notification // Activity center notification
notification := &ActivityCenterNotification{ notification := &ActivityCenterNotification{
ID: types.FromHex(uuid.New().String()), ID: types.FromHex(uuid.New().String()),
@ -4251,11 +4257,17 @@ func (m *Messenger) CreateResponseWithACNotification(communityID string, acType
Read: isRead, Read: isRead,
Deleted: false, Deleted: false,
UpdatedAt: m.GetCurrentTimeInMillis(), UpdatedAt: m.GetCurrentTimeInMillis(),
TokenData: &tokenData,
}
err = m.prepareTokenData(notification.TokenData, m.httpServer)
if err != nil {
return nil, err
} }
response := &MessengerResponse{} response := &MessengerResponse{}
err := m.addActivityCenterNotification(response, notification, nil) err = m.addActivityCenterNotification(response, notification, nil)
if err != nil { if err != nil {
m.logger.Error("failed to save notification", zap.Error(err)) m.logger.Error("failed to save notification", zap.Error(err))
return response, err return response, err

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
ALTER TABLE activity_center_notifications ADD COLUMN token_data BLOB DEFAULT NULL;

View File

@ -1685,32 +1685,36 @@ func (api *PublicAPI) GetProfileShowcaseAccountsByAddress(address string) ([]*id
// Returns response with AC notification when owner token is received // Returns response with AC notification when owner token is received
func (api *PublicAPI) RegisterOwnerTokenReceivedNotification(communityID string) (*protocol.MessengerResponse, error) { func (api *PublicAPI) RegisterOwnerTokenReceivedNotification(communityID string) (*protocol.MessengerResponse, error) {
return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeOwnerTokenReceived, false) return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeOwnerTokenReceived, false, "")
} }
// Returns response with AC notification when setting signer is successful // Returns response with AC notification when setting signer is successful
func (api *PublicAPI) RegisterReceivedOwnershipNotification(communityID string) (*protocol.MessengerResponse, error) { func (api *PublicAPI) RegisterReceivedOwnershipNotification(communityID string) (*protocol.MessengerResponse, error) {
return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeOwnershipReceived, false) return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeOwnershipReceived, false, "")
} }
// Returns response with AC notification when community token is received // Returns response with AC notification when community token is received
func (api *PublicAPI) RegisterReceivedCommunityTokenNotification(communityID string) (*protocol.MessengerResponse, error) { func (api *PublicAPI) RegisterReceivedCommunityTokenNotification(communityID string, isFirst bool, tokenData string) (*protocol.MessengerResponse, error) {
return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeCommunityTokenReceived, false) activityType := protocol.ActivityCenterNotificationTypeCommunityTokenReceived
if isFirst {
activityType = protocol.ActivityCenterNotificationTypeFirstCommunityTokenReceived
}
return api.service.messenger.CreateResponseWithACNotification(communityID, activityType, false, tokenData)
} }
// Returns response with AC notification when setting signer is failed // Returns response with AC notification when setting signer is failed
func (api *PublicAPI) RegisterSetSignerFailedNotification(communityID string) (*protocol.MessengerResponse, error) { func (api *PublicAPI) RegisterSetSignerFailedNotification(communityID string) (*protocol.MessengerResponse, error) {
return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeSetSignerFailed, false) return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeSetSignerFailed, false, "")
} }
// Returns response with AC notification when setting signer is declined // Returns response with AC notification when setting signer is declined
func (api *PublicAPI) RegisterSetSignerDeclinedNotification(communityID string) (*protocol.MessengerResponse, error) { func (api *PublicAPI) RegisterSetSignerDeclinedNotification(communityID string) (*protocol.MessengerResponse, error) {
return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeSetSignerDeclined, true) return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeSetSignerDeclined, true, "")
} }
// Returns response with AC notification when ownership is lost // Returns response with AC notification when ownership is lost
func (api *PublicAPI) RegisterLostOwnershipNotification(communityID string) (*protocol.MessengerResponse, error) { func (api *PublicAPI) RegisterLostOwnershipNotification(communityID string) (*protocol.MessengerResponse, error) {
return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeOwnershipLost, false) return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeOwnershipLost, false, "")
} }
func (api *PublicAPI) PromoteSelfToControlMode(communityID string) error { func (api *PublicAPI) PromoteSelfToControlMode(communityID string) error {

View File

@ -300,7 +300,6 @@ func updateAddressOwnershipTimestamp(creator sqlite.StatementCreator, ownerAddre
// Returns the list of added/removed IDs when comparing the given list of IDs with the ones in the DB. // Returns the list of added/removed IDs when comparing the given list of IDs with the ones in the DB.
// Call before Update for the result to be useful. // Call before Update for the result to be useful.
func (o *OwnershipDB) GetIDsNotInDB( func (o *OwnershipDB) GetIDsNotInDB(
chainID w_common.ChainID,
ownerAddress common.Address, ownerAddress common.Address,
newIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleUniqueID, error) { newIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleUniqueID, error) {
ret := make([]thirdparty.CollectibleUniqueID, 0, len(newIDs)) ret := make([]thirdparty.CollectibleUniqueID, 0, len(newIDs))
@ -333,6 +332,31 @@ func (o *OwnershipDB) GetIDsNotInDB(
return ret, nil return ret, nil
} }
func (o *OwnershipDB) GetIsFirstOfCollection(onwerAddress common.Address, newIDs []thirdparty.CollectibleUniqueID) (map[thirdparty.CollectibleUniqueID]bool, error) {
ret := make(map[thirdparty.CollectibleUniqueID]bool)
exists, err := o.db.Prepare(`SELECT count(*) FROM collectibles_ownership_cache
WHERE chain_id=? AND contract_address=? AND owner_address=?`)
if err != nil {
return nil, err
}
for _, id := range newIDs {
row := exists.QueryRow(
id.ContractID.ChainID,
id.ContractID.Address,
onwerAddress,
)
var count int
err = row.Scan(&count)
if err != nil {
return nil, err
}
ret[id] = count <= 1
}
return ret, nil
}
func (o *OwnershipDB) Update(chainID w_common.ChainID, ownerAddress common.Address, balances thirdparty.TokenBalancesPerContractAddress, timestamp int64) (removedIDs, updatedIDs, insertedIDs []thirdparty.CollectibleUniqueID, err error) { func (o *OwnershipDB) Update(chainID w_common.ChainID, ownerAddress common.Address, balances thirdparty.TokenBalancesPerContractAddress, timestamp int64) (removedIDs, updatedIDs, insertedIDs []thirdparty.CollectibleUniqueID, err error) {
err = insertTmpOwnership(o.db, chainID, ownerAddress, balances) err = insertTmpOwnership(o.db, chainID, ownerAddress, balances)
if err != nil { if err != nil {

View File

@ -415,8 +415,8 @@ func (s *Service) onOwnedCollectiblesChange(ownedCollectiblesChange OwnedCollect
switch ownedCollectiblesChange.changeType { switch ownedCollectiblesChange.changeType {
case OwnedCollectiblesChangeTypeAdded, OwnedCollectiblesChangeTypeUpdated: case OwnedCollectiblesChangeTypeAdded, OwnedCollectiblesChangeTypeUpdated:
// For recently added/updated collectibles, try to find a matching transfer // For recently added/updated collectibles, try to find a matching transfer
s.lookupTransferForCollectibles(ownedCollectiblesChange.ownedCollectibles) hashMap := s.lookupTransferForCollectibles(ownedCollectiblesChange.ownedCollectibles)
s.notifyCommunityCollectiblesReceived(ownedCollectiblesChange.ownedCollectibles) s.notifyCommunityCollectiblesReceived(ownedCollectiblesChange.ownedCollectibles, hashMap)
} }
} }
@ -437,7 +437,7 @@ func (s *Service) onCollectiblesTransfer(account common.Address, chainID walletC
} }
} }
func (s *Service) lookupTransferForCollectibles(ownedCollectibles OwnedCollectibles) { func (s *Service) lookupTransferForCollectibles(ownedCollectibles OwnedCollectibles) map[thirdparty.CollectibleUniqueID]common.Hash {
// There are some limitations to this approach: // There are some limitations to this approach:
// - Collectibles ownership and transfers are not in sync and might represent the state at different moments. // - Collectibles ownership and transfers are not in sync and might represent the state at different moments.
// - We have no way of knowing if the latest collectible transfer we've detected is actually the latest one, so the timestamp we // - We have no way of knowing if the latest collectible transfer we've detected is actually the latest one, so the timestamp we
@ -445,6 +445,9 @@ func (s *Service) lookupTransferForCollectibles(ownedCollectibles OwnedCollectib
// - There might be detected transfers that are temporarily not reflected in the collectibles ownership. // - There might be detected transfers that are temporarily not reflected in the collectibles ownership.
// - For ERC721 tokens we should only look for incoming transfers. For ERC1155 tokens we should look for both incoming and outgoing transfers. // - For ERC721 tokens we should only look for incoming transfers. For ERC1155 tokens we should look for both incoming and outgoing transfers.
// We need to get the contract standard for each collectible to know which approach to take. // We need to get the contract standard for each collectible to know which approach to take.
result := make(map[thirdparty.CollectibleUniqueID]common.Hash)
for _, id := range ownedCollectibles.ids { for _, id := range ownedCollectibles.ids {
transfer, err := s.transferDB.GetLatestCollectibleTransfer(ownedCollectibles.account, id) transfer, err := s.transferDB.GetLatestCollectibleTransfer(ownedCollectibles.account, id)
if err != nil { if err != nil {
@ -452,17 +455,24 @@ func (s *Service) lookupTransferForCollectibles(ownedCollectibles OwnedCollectib
continue continue
} }
if transfer != nil { if transfer != nil {
result[id] = transfer.Transaction.Hash()
err = s.manager.SetCollectibleTransferID(ownedCollectibles.account, id, transfer.ID, false) err = s.manager.SetCollectibleTransferID(ownedCollectibles.account, id, transfer.ID, false)
if err != nil { if err != nil {
log.Error("Error setting transfer ID for collectible", "error", err) log.Error("Error setting transfer ID for collectible", "error", err)
} }
} }
} }
return result
} }
func (s *Service) notifyCommunityCollectiblesReceived(ownedCollectibles OwnedCollectibles) { func (s *Service) notifyCommunityCollectiblesReceived(ownedCollectibles OwnedCollectibles, hashMap map[thirdparty.CollectibleUniqueID]common.Hash) {
ctx := context.Background() ctx := context.Background()
firstCollectibles, err := s.ownershipDB.GetIsFirstOfCollection(ownedCollectibles.account, ownedCollectibles.ids)
if err != nil {
return
}
collectiblesData, err := s.manager.FetchAssetsByCollectibleUniqueID(ctx, ownedCollectibles.ids, false) collectiblesData, err := s.manager.FetchAssetsByCollectibleUniqueID(ctx, ownedCollectibles.ids, false)
if err != nil { if err != nil {
log.Error("Error fetching collectibles data", "error", err) log.Error("Error fetching collectibles data", "error", err)
@ -475,7 +485,45 @@ func (s *Service) notifyCommunityCollectiblesReceived(ownedCollectibles OwnedCol
return return
} }
encodedMessage, err := json.Marshal(communityCollectibles) type CollectibleGroup struct {
contractID thirdparty.ContractID
txHash string
}
groups := make(map[CollectibleGroup]Collectible)
for i, collectible := range communityCollectibles {
for key, value := range hashMap {
if key.Same(&collectible.ID) {
communityCollectibles[i].LatestTxHash = value.Hex()
break
}
}
for id, value := range firstCollectibles {
if value && id.Same(&collectible.ID) {
communityCollectibles[i].IsFirst = true
break
}
}
group := CollectibleGroup{
contractID: collectible.ID.ContractID,
txHash: collectible.LatestTxHash,
}
_, ok := groups[group]
if !ok {
collectible.ReceivedAmount = float64(0)
}
collectible.ReceivedAmount = collectible.ReceivedAmount + 1
groups[group] = collectible
}
groupedCommunityCollectibles := make([]Collectible, 0, len(groups))
for _, collectible := range groups {
groupedCommunityCollectibles = append(groupedCommunityCollectibles, collectible)
}
encodedMessage, err := json.Marshal(groupedCommunityCollectibles)
if err != nil { if err != nil {
return return
} }

View File

@ -15,6 +15,9 @@ type Collectible struct {
CollectionData *CollectionData `json:"collection_data,omitempty"` CollectionData *CollectionData `json:"collection_data,omitempty"`
CommunityData *CommunityData `json:"community_data,omitempty"` CommunityData *CommunityData `json:"community_data,omitempty"`
Ownership []thirdparty.AccountBalance `json:"ownership,omitempty"` Ownership []thirdparty.AccountBalance `json:"ownership,omitempty"`
IsFirst bool `json:"is_first,omitempty"`
LatestTxHash string `json:"latest_tx_hash,omitempty"`
ReceivedAmount float64 `json:"received_amount,omitempty"`
} }
type CollectibleData struct { type CollectibleData struct {
@ -167,10 +170,12 @@ func fullCollectiblesDataToCommunityHeader(data []thirdparty.FullCollectibleData
ID: collectibleID, ID: collectibleID,
ContractType: getContractType(c), ContractType: getContractType(c),
CollectibleData: &CollectibleData{ CollectibleData: &CollectibleData{
Name: c.CollectibleData.Name, Name: c.CollectibleData.Name,
ImageURL: &c.CollectibleData.ImageURL,
}, },
CommunityData: &communityData, CommunityData: &communityData,
Ownership: c.Ownership, Ownership: c.Ownership,
IsFirst: c.CollectibleData.IsFirst,
} }
res = append(res, header) res = append(res, header)

View File

@ -138,6 +138,7 @@ type CollectibleData struct {
Traits []CollectibleTrait `json:"traits"` Traits []CollectibleTrait `json:"traits"`
BackgroundColor string `json:"background_color"` BackgroundColor string `json:"background_color"`
TokenURI string `json:"token_uri"` TokenURI string `json:"token_uri"`
IsFirst bool `json:"is_first"`
} }
// Community-related collectible info. Present only for collectibles minted in a community. // Community-related collectible info. Present only for collectibles minted in a community.

View File

@ -68,8 +68,9 @@ type ReceivedToken struct {
Image string `json:"image,omitempty"` Image string `json:"image,omitempty"`
ChainID uint64 `json:"chainId"` ChainID uint64 `json:"chainId"`
CommunityData *community.Data `json:"community_data,omitempty"` CommunityData *community.Data `json:"community_data,omitempty"`
Balance *big.Int `json:"balance"` Amount float64 `json:"amount"`
TxHash common.Hash `json:"txHash"` TxHash common.Hash `json:"txHash"`
IsFirst bool `json:"isFirst"`
} }
func (t *Token) IsNative() bool { func (t *Token) IsNative() bool {
@ -316,20 +317,20 @@ func (tm *Manager) FindOrCreateTokenByAddress(ctx context.Context, chainID uint6
return token return token
} }
func (tm *Manager) MarkAsPreviouslyOwnedToken(token *Token, owner common.Address) error { func (tm *Manager) MarkAsPreviouslyOwnedToken(token *Token, owner common.Address) (bool, error) {
if token == nil { if token == nil {
return errors.New("token is nil") return false, errors.New("token is nil")
} }
if (owner == common.Address{}) { if (owner == common.Address{}) {
return errors.New("owner is nil") return false, errors.New("owner is nil")
} }
count := 0 count := 0
err := tm.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM token_balances WHERE user_address = ? AND token_address = ? AND chain_id = ?)`, owner.Hex(), token.Address.Hex(), token.ChainID).Scan(&count) err := tm.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM token_balances WHERE user_address = ? AND token_address = ? AND chain_id = ?)`, owner.Hex(), token.Address.Hex(), token.ChainID).Scan(&count)
if err != nil || count > 0 { if err != nil || count > 0 {
return err return false, err
} }
_, err = tm.db.Exec(`INSERT INTO token_balances(user_address,token_name,token_symbol,token_address,token_decimals,chain_id,token_decimals,raw_balance,balance) VALUES (?,?,?,?,?,?,?,?,?)`, owner.Hex(), token.Name, token.Symbol, token.Address.Hex(), token.Decimals, token.ChainID, 0, "0", "0") _, err = tm.db.Exec(`INSERT INTO token_balances(user_address,token_name,token_symbol,token_address,token_decimals,chain_id,token_decimals,raw_balance,balance) VALUES (?,?,?,?,?,?,?,?,?)`, owner.Hex(), token.Name, token.Symbol, token.Address.Hex(), token.Decimals, token.ChainID, 0, "0", "0")
return err return true, err
} }
func (tm *Manager) discoverTokenCommunityID(ctx context.Context, token *Token, address common.Address) { func (tm *Manager) discoverTokenCommunityID(ctx context.Context, token *Token, address common.Address) {
@ -809,7 +810,7 @@ func (tm *Manager) GetBalancesAtByChain(parent context.Context, clients map[uint
return response, group.Error() return response, group.Error()
} }
func (tm *Manager) SignalCommunityTokenReceived(address common.Address, txHash common.Hash, value *big.Int, t *Token) { func (tm *Manager) SignalCommunityTokenReceived(address common.Address, txHash common.Hash, value *big.Int, t *Token, isFirst bool) {
if tm.walletFeed == nil || t == nil || t.CommunityData == nil { if tm.walletFeed == nil || t == nil || t.CommunityData == nil {
return return
} }
@ -826,6 +827,8 @@ func (tm *Manager) SignalCommunityTokenReceived(address common.Address, txHash c
} }
} }
floatAmount, _ := new(big.Float).Quo(new(big.Float).SetInt(value), new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(t.Decimals)), nil))).Float64()
receivedToken := ReceivedToken{ receivedToken := ReceivedToken{
Address: t.Address, Address: t.Address,
Name: t.Name, Name: t.Name,
@ -833,8 +836,9 @@ func (tm *Manager) SignalCommunityTokenReceived(address common.Address, txHash c
Image: t.Image, Image: t.Image,
ChainID: t.ChainID, ChainID: t.ChainID,
CommunityData: t.CommunityData, CommunityData: t.CommunityData,
Balance: value, Amount: floatAmount,
TxHash: txHash, TxHash: txHash,
IsFirst: isFirst,
} }
encodedMessage, err := json.Marshal(receivedToken) encodedMessage, err := json.Marshal(receivedToken)

View File

@ -208,14 +208,17 @@ func TestMarkAsPreviouslyOwnedToken(t *testing.T) {
ChainID: 1, ChainID: 1,
} }
err := manager.MarkAsPreviouslyOwnedToken(nil, owner) isFirst, err := manager.MarkAsPreviouslyOwnedToken(nil, owner)
require.Error(t, err) require.Error(t, err)
require.False(t, isFirst)
err = manager.MarkAsPreviouslyOwnedToken(token, common.Address{}) isFirst, err = manager.MarkAsPreviouslyOwnedToken(token, common.Address{})
require.Error(t, err) require.Error(t, err)
require.False(t, isFirst)
err = manager.MarkAsPreviouslyOwnedToken(token, owner) isFirst, err = manager.MarkAsPreviouslyOwnedToken(token, owner)
require.NoError(t, err) require.NoError(t, err)
require.True(t, isFirst)
// Verify that the token balance was inserted correctly // Verify that the token balance was inserted correctly
var count int var count int
@ -225,8 +228,9 @@ func TestMarkAsPreviouslyOwnedToken(t *testing.T) {
token.Name = "123" token.Name = "123"
err = manager.MarkAsPreviouslyOwnedToken(token, owner) isFirst, err = manager.MarkAsPreviouslyOwnedToken(token, owner)
require.NoError(t, err) require.NoError(t, err)
require.False(t, isFirst)
// Not updated because already exists // Not updated because already exists
err = manager.db.QueryRow(`SELECT count(*) FROM token_balances`).Scan(&count) err = manager.db.QueryRow(`SELECT count(*) FROM token_balances`).Scan(&count)
@ -235,11 +239,12 @@ func TestMarkAsPreviouslyOwnedToken(t *testing.T) {
token.ChainID = 2 token.ChainID = 2
err = manager.MarkAsPreviouslyOwnedToken(token, owner) isFirst, err = manager.MarkAsPreviouslyOwnedToken(token, owner)
require.NoError(t, err) require.NoError(t, err)
// Same token on different chains counts as different token // Same token on different chains counts as different token
err = manager.db.QueryRow(`SELECT count(*) FROM token_balances`).Scan(&count) err = manager.db.QueryRow(`SELECT count(*) FROM token_balances`).Scan(&count)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 2, count) require.Equal(t, 2, count)
require.True(t, isFirst)
} }

View File

@ -378,11 +378,11 @@ func (c *transfersCommand) markMultiTxTokensAsPreviouslyOwned(ctx context.Contex
} }
if len(multiTransaction.ToAsset) > 0 && multiTransaction.ToNetworkID > 0 { if len(multiTransaction.ToAsset) > 0 && multiTransaction.ToNetworkID > 0 {
token := c.tokenManager.GetToken(multiTransaction.ToNetworkID, multiTransaction.ToAsset) token := c.tokenManager.GetToken(multiTransaction.ToNetworkID, multiTransaction.ToAsset)
_ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, ownerAddress) _, _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, ownerAddress)
} }
if len(multiTransaction.FromAsset) > 0 && multiTransaction.FromNetworkID > 0 { if len(multiTransaction.FromAsset) > 0 && multiTransaction.FromNetworkID > 0 {
token := c.tokenManager.GetToken(multiTransaction.FromNetworkID, multiTransaction.FromAsset) token := c.tokenManager.GetToken(multiTransaction.FromNetworkID, multiTransaction.FromAsset)
_ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, ownerAddress) _, _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, ownerAddress)
} }
} }
@ -439,11 +439,12 @@ func (c *transfersCommand) processUnknownErc20CommunityTransactions(ctx context.
// Find token in db or if this is a community token, find its metadata // Find token in db or if this is a community token, find its metadata
token := c.tokenManager.FindOrCreateTokenByAddress(ctx, tx.NetworkID, *tx.Transaction.To()) token := c.tokenManager.FindOrCreateTokenByAddress(ctx, tx.NetworkID, *tx.Transaction.To())
if token != nil { if token != nil {
isFirst := false
if token.Verified || token.CommunityData != nil { if token.Verified || token.CommunityData != nil {
_ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, tx.Address) isFirst, _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, tx.Address)
} }
if token.CommunityData != nil { if token.CommunityData != nil {
go c.tokenManager.SignalCommunityTokenReceived(tx.Address, tx.ID, tx.Transaction.Value(), token) go c.tokenManager.SignalCommunityTokenReceived(tx.Address, tx.ID, tx.TokenValue, token, isFirst)
} }
} }
} }