feat: Community token received notification (#4682)
This commit is contained in:
parent
54d0cf28c7
commit
a866b8025e
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/protocol/common"
|
||||
"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
|
||||
|
@ -36,6 +37,7 @@ const (
|
|||
ActivityCenterNotificationTypeSetSignerDeclined
|
||||
ActivityCenterNotificationTypeShareAccounts
|
||||
ActivityCenterNotificationTypeCommunityTokenReceived
|
||||
ActivityCenterNotificationTypeFirstCommunityTokenReceived
|
||||
)
|
||||
|
||||
type ActivityCenterMembershipStatus int
|
||||
|
@ -60,6 +62,22 @@ const (
|
|||
|
||||
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 {
|
||||
ID types.HexBytes `json:"id"`
|
||||
ChatID string `json:"chatId"`
|
||||
|
@ -77,6 +95,7 @@ type ActivityCenterNotification struct {
|
|||
Deleted bool `json:"deleted"`
|
||||
Accepted bool `json:"accepted"`
|
||||
ContactVerificationStatus verification.RequestStatus `json:"contactVerificationStatus"`
|
||||
TokenData *ActivityTokenData `json:"tokenData"`
|
||||
//Used for synchronization. Each update should increment the UpdatedAt.
|
||||
//The value should represent the time when the update occurred.
|
||||
UpdatedAt uint64 `json:"updatedAt"`
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -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(`
|
||||
INSERT OR REPLACE
|
||||
INTO activity_center_notifications (
|
||||
|
@ -141,10 +150,11 @@ func (db sqlitePersistence) SaveActivityCenterNotification(notification *Activit
|
|||
read,
|
||||
accepted,
|
||||
dismissed,
|
||||
token_data,
|
||||
deleted,
|
||||
updated_at
|
||||
)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
`,
|
||||
notification.ID,
|
||||
notification.Timestamp,
|
||||
|
@ -159,9 +169,9 @@ func (db sqlitePersistence) SaveActivityCenterNotification(notification *Activit
|
|||
notification.Read,
|
||||
notification.Accepted,
|
||||
notification.Dismissed,
|
||||
encodedTokenData,
|
||||
notification.Deleted,
|
||||
notification.UpdatedAt,
|
||||
notification.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
@ -187,6 +197,7 @@ func (db sqlitePersistence) parseRowFromTableActivityCenterNotification(rows *sq
|
|||
var communityID sql.NullString
|
||||
var messageBytes []byte
|
||||
var replyMessageBytes []byte
|
||||
var tokenDataBytes []byte
|
||||
var author sql.NullString
|
||||
notification := &ActivityCenterNotification{}
|
||||
err := rows.Scan(
|
||||
|
@ -203,6 +214,7 @@ func (db sqlitePersistence) parseRowFromTableActivityCenterNotification(rows *sq
|
|||
&communityID,
|
||||
¬ification.MembershipStatus,
|
||||
¬ification.ContactVerificationStatus,
|
||||
&tokenDataBytes,
|
||||
¬ification.Deleted,
|
||||
¬ification.UpdatedAt,
|
||||
)
|
||||
|
@ -222,6 +234,13 @@ func (db sqlitePersistence) parseRowFromTableActivityCenterNotification(rows *sq
|
|||
notification.Author = author.String
|
||||
}
|
||||
|
||||
if len(tokenDataBytes) > 0 {
|
||||
err = json.Unmarshal(tokenDataBytes, ¬ification.TokenData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(messageBytes) > 0 {
|
||||
err = json.Unmarshal(messageBytes, ¬ification.Message)
|
||||
if err != nil {
|
||||
|
@ -251,6 +270,7 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRow(row *sql.Row)
|
|||
var lastMessageBytes []byte
|
||||
var messageBytes []byte
|
||||
var replyMessageBytes []byte
|
||||
var tokenDataBytes []byte
|
||||
var name sql.NullString
|
||||
var author sql.NullString
|
||||
notification := &ActivityCenterNotification{}
|
||||
|
@ -271,6 +291,7 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRow(row *sql.Row)
|
|||
¬ification.ContactVerificationStatus,
|
||||
&name,
|
||||
&author,
|
||||
&tokenDataBytes,
|
||||
¬ification.UpdatedAt)
|
||||
|
||||
if err != nil {
|
||||
|
@ -293,6 +314,13 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRow(row *sql.Row)
|
|||
notification.Author = author.String
|
||||
}
|
||||
|
||||
if len(tokenDataBytes) > 0 {
|
||||
err = json.Unmarshal(tokenDataBytes, ¬ification.TokenData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Restore last message
|
||||
if lastMessageBytes != nil {
|
||||
lastMessage := common.NewMessage()
|
||||
|
@ -332,6 +360,7 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRows(rows *sql.Ro
|
|||
var lastMessageBytes []byte
|
||||
var messageBytes []byte
|
||||
var replyMessageBytes []byte
|
||||
var tokenDataBytes []byte
|
||||
var name sql.NullString
|
||||
var author sql.NullString
|
||||
notification := &ActivityCenterNotification{}
|
||||
|
@ -351,6 +380,7 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRows(rows *sql.Ro
|
|||
¬ification.ContactVerificationStatus,
|
||||
&name,
|
||||
&author,
|
||||
&tokenDataBytes,
|
||||
&latestCursor,
|
||||
¬ification.UpdatedAt)
|
||||
if err != nil {
|
||||
|
@ -373,6 +403,14 @@ func (db sqlitePersistence) unmarshalActivityCenterNotificationRows(rows *sql.Ro
|
|||
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
|
||||
if lastMessageBytes != nil {
|
||||
lastMessage := common.NewMessage()
|
||||
|
@ -503,6 +541,7 @@ func (db sqlitePersistence) buildActivityCenterQuery(tx *sql.Tx, params activity
|
|||
a.contact_verification_status,
|
||||
c.name,
|
||||
a.author,
|
||||
a.token_data,
|
||||
substr('0000000000000000000000000000000000000000000000000000000000000000' || a.timestamp, -64, 64) || hex(a.id) as cursor,
|
||||
a.updated_at
|
||||
FROM activity_center_notifications a
|
||||
|
@ -623,6 +662,7 @@ func (db sqlitePersistence) GetActivityCenterNotificationsByID(ids []types.HexBy
|
|||
a.contact_verification_status,
|
||||
c.name,
|
||||
a.author,
|
||||
a.token_data,
|
||||
substr('0000000000000000000000000000000000000000000000000000000000000000' || a.timestamp, -64, 64) || hex(a.id) as cursor,
|
||||
a.updated_at
|
||||
FROM activity_center_notifications a
|
||||
|
@ -663,6 +703,7 @@ func (db sqlitePersistence) GetActivityCenterNotificationByID(id types.HexBytes)
|
|||
a.contact_verification_status,
|
||||
c.name,
|
||||
a.author,
|
||||
a.token_data,
|
||||
a.updated_at
|
||||
FROM activity_center_notifications a
|
||||
LEFT JOIN chats c
|
||||
|
@ -1295,6 +1336,7 @@ func (db sqlitePersistence) ActiveContactRequestNotification(contactID string) (
|
|||
a.contact_verification_status,
|
||||
c.name,
|
||||
a.author,
|
||||
a.token_data,
|
||||
a.updated_at
|
||||
FROM activity_center_notifications a
|
||||
LEFT JOIN chats c ON c.id = a.chat_id
|
||||
|
|
|
@ -4189,6 +4189,15 @@ func extractQuotedImages(messages []*common.Message, s *server.MediaServer) []st
|
|||
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 {
|
||||
if msg.QuotedMessage != nil && msg.QuotedMessage.ContentType == int64(protobuf.ChatMessage_IMAGE) {
|
||||
msg.QuotedMessage.ImageLocalURL = s.MakeImageURL(msg.QuotedMessage.ID)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4241,7 +4241,13 @@ func (m *Messenger) PromoteSelfToControlNode(communityID types.HexBytes) (*Messe
|
|||
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
|
||||
notification := &ActivityCenterNotification{
|
||||
ID: types.FromHex(uuid.New().String()),
|
||||
|
@ -4251,11 +4257,17 @@ func (m *Messenger) CreateResponseWithACNotification(communityID string, acType
|
|||
Read: isRead,
|
||||
Deleted: false,
|
||||
UpdatedAt: m.GetCurrentTimeInMillis(),
|
||||
TokenData: &tokenData,
|
||||
}
|
||||
|
||||
err = m.prepareTokenData(notification.TokenData, m.httpServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &MessengerResponse{}
|
||||
|
||||
err := m.addActivityCenterNotification(response, notification, nil)
|
||||
err = m.addActivityCenterNotification(response, notification, nil)
|
||||
if err != nil {
|
||||
m.logger.Error("failed to save notification", zap.Error(err))
|
||||
return response, err
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
ALTER TABLE activity_center_notifications ADD COLUMN token_data BLOB DEFAULT NULL;
|
|
@ -1685,32 +1685,36 @@ func (api *PublicAPI) GetProfileShowcaseAccountsByAddress(address string) ([]*id
|
|||
|
||||
// Returns response with AC notification when owner token is received
|
||||
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
|
||||
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
|
||||
func (api *PublicAPI) RegisterReceivedCommunityTokenNotification(communityID string) (*protocol.MessengerResponse, error) {
|
||||
return api.service.messenger.CreateResponseWithACNotification(communityID, protocol.ActivityCenterNotificationTypeCommunityTokenReceived, false)
|
||||
func (api *PublicAPI) RegisterReceivedCommunityTokenNotification(communityID string, isFirst bool, tokenData string) (*protocol.MessengerResponse, error) {
|
||||
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
|
||||
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
|
||||
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
|
||||
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 {
|
||||
|
|
|
@ -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.
|
||||
// Call before Update for the result to be useful.
|
||||
func (o *OwnershipDB) GetIDsNotInDB(
|
||||
chainID w_common.ChainID,
|
||||
ownerAddress common.Address,
|
||||
newIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
ret := make([]thirdparty.CollectibleUniqueID, 0, len(newIDs))
|
||||
|
@ -333,6 +332,31 @@ func (o *OwnershipDB) GetIDsNotInDB(
|
|||
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) {
|
||||
err = insertTmpOwnership(o.db, chainID, ownerAddress, balances)
|
||||
if err != nil {
|
||||
|
|
|
@ -415,8 +415,8 @@ func (s *Service) onOwnedCollectiblesChange(ownedCollectiblesChange OwnedCollect
|
|||
switch ownedCollectiblesChange.changeType {
|
||||
case OwnedCollectiblesChangeTypeAdded, OwnedCollectiblesChangeTypeUpdated:
|
||||
// For recently added/updated collectibles, try to find a matching transfer
|
||||
s.lookupTransferForCollectibles(ownedCollectiblesChange.ownedCollectibles)
|
||||
s.notifyCommunityCollectiblesReceived(ownedCollectiblesChange.ownedCollectibles)
|
||||
hashMap := s.lookupTransferForCollectibles(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:
|
||||
// - 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
|
||||
|
@ -445,6 +445,9 @@ func (s *Service) lookupTransferForCollectibles(ownedCollectibles OwnedCollectib
|
|||
// - 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.
|
||||
// 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 {
|
||||
transfer, err := s.transferDB.GetLatestCollectibleTransfer(ownedCollectibles.account, id)
|
||||
if err != nil {
|
||||
|
@ -452,17 +455,24 @@ func (s *Service) lookupTransferForCollectibles(ownedCollectibles OwnedCollectib
|
|||
continue
|
||||
}
|
||||
if transfer != nil {
|
||||
result[id] = transfer.Transaction.Hash()
|
||||
err = s.manager.SetCollectibleTransferID(ownedCollectibles.account, id, transfer.ID, false)
|
||||
if err != nil {
|
||||
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()
|
||||
|
||||
firstCollectibles, err := s.ownershipDB.GetIsFirstOfCollection(ownedCollectibles.account, ownedCollectibles.ids)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
collectiblesData, err := s.manager.FetchAssetsByCollectibleUniqueID(ctx, ownedCollectibles.ids, false)
|
||||
if err != nil {
|
||||
log.Error("Error fetching collectibles data", "error", err)
|
||||
|
@ -475,7 +485,45 @@ func (s *Service) notifyCommunityCollectiblesReceived(ownedCollectibles OwnedCol
|
|||
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 {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -15,6 +15,9 @@ type Collectible struct {
|
|||
CollectionData *CollectionData `json:"collection_data,omitempty"`
|
||||
CommunityData *CommunityData `json:"community_data,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 {
|
||||
|
@ -167,10 +170,12 @@ func fullCollectiblesDataToCommunityHeader(data []thirdparty.FullCollectibleData
|
|||
ID: collectibleID,
|
||||
ContractType: getContractType(c),
|
||||
CollectibleData: &CollectibleData{
|
||||
Name: c.CollectibleData.Name,
|
||||
Name: c.CollectibleData.Name,
|
||||
ImageURL: &c.CollectibleData.ImageURL,
|
||||
},
|
||||
CommunityData: &communityData,
|
||||
Ownership: c.Ownership,
|
||||
IsFirst: c.CollectibleData.IsFirst,
|
||||
}
|
||||
|
||||
res = append(res, header)
|
||||
|
|
|
@ -138,6 +138,7 @@ type CollectibleData struct {
|
|||
Traits []CollectibleTrait `json:"traits"`
|
||||
BackgroundColor string `json:"background_color"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
IsFirst bool `json:"is_first"`
|
||||
}
|
||||
|
||||
// Community-related collectible info. Present only for collectibles minted in a community.
|
||||
|
|
|
@ -68,8 +68,9 @@ type ReceivedToken struct {
|
|||
Image string `json:"image,omitempty"`
|
||||
ChainID uint64 `json:"chainId"`
|
||||
CommunityData *community.Data `json:"community_data,omitempty"`
|
||||
Balance *big.Int `json:"balance"`
|
||||
Amount float64 `json:"amount"`
|
||||
TxHash common.Hash `json:"txHash"`
|
||||
IsFirst bool `json:"isFirst"`
|
||||
}
|
||||
|
||||
func (t *Token) IsNative() bool {
|
||||
|
@ -316,20 +317,20 @@ func (tm *Manager) FindOrCreateTokenByAddress(ctx context.Context, chainID uint6
|
|||
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 {
|
||||
return errors.New("token is nil")
|
||||
return false, errors.New("token is nil")
|
||||
}
|
||||
if (owner == common.Address{}) {
|
||||
return errors.New("owner is nil")
|
||||
return false, errors.New("owner is nil")
|
||||
}
|
||||
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)
|
||||
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")
|
||||
return err
|
||||
return true, err
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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 {
|
||||
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{
|
||||
Address: t.Address,
|
||||
Name: t.Name,
|
||||
|
@ -833,8 +836,9 @@ func (tm *Manager) SignalCommunityTokenReceived(address common.Address, txHash c
|
|||
Image: t.Image,
|
||||
ChainID: t.ChainID,
|
||||
CommunityData: t.CommunityData,
|
||||
Balance: value,
|
||||
Amount: floatAmount,
|
||||
TxHash: txHash,
|
||||
IsFirst: isFirst,
|
||||
}
|
||||
|
||||
encodedMessage, err := json.Marshal(receivedToken)
|
||||
|
|
|
@ -208,14 +208,17 @@ func TestMarkAsPreviouslyOwnedToken(t *testing.T) {
|
|||
ChainID: 1,
|
||||
}
|
||||
|
||||
err := manager.MarkAsPreviouslyOwnedToken(nil, owner)
|
||||
isFirst, err := manager.MarkAsPreviouslyOwnedToken(nil, owner)
|
||||
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.False(t, isFirst)
|
||||
|
||||
err = manager.MarkAsPreviouslyOwnedToken(token, owner)
|
||||
isFirst, err = manager.MarkAsPreviouslyOwnedToken(token, owner)
|
||||
require.NoError(t, err)
|
||||
require.True(t, isFirst)
|
||||
|
||||
// Verify that the token balance was inserted correctly
|
||||
var count int
|
||||
|
@ -225,8 +228,9 @@ func TestMarkAsPreviouslyOwnedToken(t *testing.T) {
|
|||
|
||||
token.Name = "123"
|
||||
|
||||
err = manager.MarkAsPreviouslyOwnedToken(token, owner)
|
||||
isFirst, err = manager.MarkAsPreviouslyOwnedToken(token, owner)
|
||||
require.NoError(t, err)
|
||||
require.False(t, isFirst)
|
||||
|
||||
// Not updated because already exists
|
||||
err = manager.db.QueryRow(`SELECT count(*) FROM token_balances`).Scan(&count)
|
||||
|
@ -235,11 +239,12 @@ func TestMarkAsPreviouslyOwnedToken(t *testing.T) {
|
|||
|
||||
token.ChainID = 2
|
||||
|
||||
err = manager.MarkAsPreviouslyOwnedToken(token, owner)
|
||||
isFirst, err = manager.MarkAsPreviouslyOwnedToken(token, owner)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Same token on different chains counts as different token
|
||||
err = manager.db.QueryRow(`SELECT count(*) FROM token_balances`).Scan(&count)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, count)
|
||||
require.True(t, isFirst)
|
||||
}
|
||||
|
|
|
@ -378,11 +378,11 @@ func (c *transfersCommand) markMultiTxTokensAsPreviouslyOwned(ctx context.Contex
|
|||
}
|
||||
if len(multiTransaction.ToAsset) > 0 && multiTransaction.ToNetworkID > 0 {
|
||||
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 {
|
||||
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
|
||||
token := c.tokenManager.FindOrCreateTokenByAddress(ctx, tx.NetworkID, *tx.Transaction.To())
|
||||
if token != nil {
|
||||
isFirst := false
|
||||
if token.Verified || token.CommunityData != nil {
|
||||
_ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, tx.Address)
|
||||
isFirst, _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, tx.Address)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue