fix: Upgrade status-go to the most recent version of release branch which contains memory fix

Fix #4990
This commit is contained in:
Michal Iskierko
2024-05-13 12:21:03 +02:00
committed by Michał Iskierko
parent 03d490156a
commit 66cf3d21b9
230 changed files with 30930 additions and 14243 deletions

View File

@@ -171,10 +171,12 @@ func (api *SettingsAPI) SetBio(bio string) error {
return (*api.messenger).SetBio(bio)
}
// Deprecated: use social links from ProfileShowcasePreferences
func (api *SettingsAPI) GetSocialLinks() (identity.SocialLinks, error) {
return api.db.GetSocialLinks()
}
// Deprecated: use social links from ProfileShowcasePreferences
func (api *SettingsAPI) AddOrReplaceSocialLinks(links identity.SocialLinks) error {
for _, link := range links {
if len(link.Text) == 0 {

View File

@@ -77,8 +77,14 @@ type Chat struct {
FirstMessageTimestamp uint32 `json:"firstMessageTimestamp,omitempty"`
Highlight bool `json:"highlight,omitempty"`
PinnedMessages *PinnedMessages `json:"pinnedMessages,omitempty"`
CanPost bool `json:"canPost"`
Base64Image string `json:"image,omitempty"`
// Deprecated: CanPost is deprecated in favor of CanPostMessages/CanPostReactions/etc.
// For now CanPost will equal to CanPostMessages.
CanPost bool `json:"canPost"`
CanPostMessages bool `json:"canPostMessages"`
CanPostReactions bool `json:"canPostReactions"`
ViewersCanPostReactions bool `json:"viewersCanPostReactions"`
Base64Image string `json:"image,omitempty"`
HideIfPermissionsNotMet bool `json:"hideIfPermissionsNotMet,omitempty"`
}
type ChannelGroup struct {
@@ -472,6 +478,9 @@ func (api *API) getCommunityByID(id string) (*communities.Community, error) {
}
func (chat *Chat) populateCommunityFields(community *communities.Community) error {
chat.CanPost = true
chat.CanPostMessages = true
chat.CanPostReactions = true
if community == nil {
return nil
}
@@ -482,18 +491,27 @@ func (chat *Chat) populateCommunityFields(community *communities.Community) erro
return nil
}
canPost, err := community.CanMemberIdentityPost(chat.ID)
canPostMessages, err := community.CanMemberIdentityPost(chat.ID, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
if err != nil {
return err
}
canPostReactions, err := community.CanMemberIdentityPost(chat.ID, protobuf.ApplicationMetadataMessage_EMOJI_REACTION)
if err != nil {
return err
}
chat.CategoryID = commChat.CategoryId
chat.HideIfPermissionsNotMet = commChat.HideIfPermissionsNotMet
chat.Position = commChat.Position
chat.Permissions = commChat.Permissions
chat.Emoji = commChat.Identity.Emoji
chat.Name = commChat.Identity.DisplayName
chat.Description = commChat.Identity.Description
chat.CanPost = canPost
chat.CanPost = canPostMessages
chat.CanPostMessages = canPostMessages
chat.CanPostReactions = canPostReactions
chat.ViewersCanPostReactions = commChat.ViewersCanPostReactions
return nil
}

View File

@@ -330,9 +330,8 @@ func (api *API) DeployAssets(ctx context.Context, chainID uint64, deploymentPara
}
// Returns gas units + 10%
func (api *API) DeployCollectiblesEstimate(ctx context.Context) (uint64, error) {
// TODO investigate why the code below does not return correct values
/*ethClient, err := api.s.manager.rpcClient.EthClient(420)
func (api *API) DeployCollectiblesEstimate(ctx context.Context, chainID uint64, fromAddress string) (uint64, error) {
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
log.Error(err.Error())
return 0, err
@@ -343,35 +342,63 @@ func (api *API) DeployCollectiblesEstimate(ctx context.Context) (uint64, error)
return 0, err
}
data, err := collectiblesABI.Pack("", "name", "SYMBOL", big.NewInt(20), true, false, "tokenUriwhcih is very long asdkfjlsdkjflk",
// use random parameters, they will not have impact on deployment results
data, err := collectiblesABI.Pack("" /*constructor name is empty*/, "name", "SYMBOL", big.NewInt(20), true, false, "tokenUri",
common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"), common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"))
if err != nil {
return 0, err
}
callMsg := ethereum.CallMsg{
From: common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"),
From: common.HexToAddress(fromAddress),
To: nil,
Value: big.NewInt(0),
Data: data,
Data: append(common.FromHex(collectibles.CollectiblesBin), data...),
}
estimate, err := ethClient.EstimateGas(ctx, callMsg)
if err != nil {
return 0, err
}
return estimate + uint64(float32(estimate)*0.1), nil*/
// TODO compute fee dynamically
// the code above returns too low fees, need to investigate
gasAmount := uint64(2500000)
return gasAmount + uint64(float32(gasAmount)*0.1), nil
finalEstimation := estimate + uint64(float32(estimate)*0.1)
log.Debug("Collectibles deployment gas estimation: ", finalEstimation)
return finalEstimation, nil
}
// Returns gas units + 10%
func (api *API) DeployAssetsEstimate(ctx context.Context) (uint64, error) {
// TODO compute fee dynamically
gasAmount := uint64(1500000)
return gasAmount + uint64(float32(gasAmount)*0.1), nil
func (api *API) DeployAssetsEstimate(ctx context.Context, chainID uint64, fromAddress string) (uint64, error) {
ethClient, err := api.s.manager.rpcClient.EthClient(chainID)
if err != nil {
log.Error(err.Error())
return 0, err
}
assetsABI, err := abi.JSON(strings.NewReader(assets.AssetsABI))
if err != nil {
return 0, err
}
// use random parameters, they will not have impact on deployment results
data, err := assetsABI.Pack("" /*constructor name is empty*/, "name", "SYMBOL", uint8(18), big.NewInt(20), "tokenUri",
common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"), common.HexToAddress("0x77b48394c650520012795a1a25696d7eb542d110"))
if err != nil {
return 0, err
}
callMsg := ethereum.CallMsg{
From: common.HexToAddress(fromAddress),
To: nil,
Value: big.NewInt(0),
Data: append(common.FromHex(assets.AssetsBin), data...),
}
estimate, err := ethClient.EstimateGas(ctx, callMsg)
if err != nil {
return 0, err
}
finalEstimation := estimate + uint64(float32(estimate)*0.1)
log.Debug("Assets deployment gas estimation: ", finalEstimation)
return finalEstimation, nil
}
func (api *API) DeployOwnerTokenEstimate(ctx context.Context, chainID uint64, fromAddress string,

View File

@@ -31,6 +31,7 @@ import (
"github.com/status-im/status-go/protocol/communities/token"
"github.com/status-im/status-go/protocol/discord"
"github.com/status-im/status-go/protocol/encryption/multidevice"
"github.com/status-im/status-go/protocol/identity"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/pushnotificationclient"
"github.com/status-im/status-go/protocol/requests"
@@ -400,6 +401,11 @@ func (api *PublicAPI) JoinedCommunities(parent context.Context) ([]*communities.
return api.service.messenger.JoinedCommunities()
}
// IsDisplayNameDupeOfCommunityMember returns if any controlled or joined community has a member with provided display name
func (api *PublicAPI) IsDisplayNameDupeOfCommunityMember(name string) (bool, error) {
return api.service.messenger.IsDisplayNameDupeOfCommunityMember(name)
}
// CommunityTags return the list of possible community tags
func (api *PublicAPI) CommunityTags(parent context.Context) map[string]string {
return requests.TagsEmojies
@@ -448,6 +454,16 @@ func (api *PublicAPI) SetCommunityShard(request *requests.SetCommunityShard) (*p
return api.service.messenger.SetCommunityShard(request)
}
// Sets the community storenodes for a community
func (api *PublicAPI) SetCommunityStorenodes(request *requests.SetCommunityStorenodes) (*protocol.MessengerResponse, error) {
return api.service.messenger.SetCommunityStorenodes(request)
}
// Gets the community storenodes for a community
func (api *PublicAPI) GetCommunityStorenodes(id types.HexBytes) (*protocol.MessengerResponse, error) {
return api.service.messenger.GetCommunityStorenodes(id)
}
// ExportCommunity exports the private key of the community with given ID
func (api *PublicAPI) ExportCommunity(id types.HexBytes) (types.HexBytes, error) {
key, err := api.service.messenger.ExportCommunity(id)
@@ -861,6 +877,10 @@ func (api *PublicAPI) DismissLatestContactRequestForContact(ctx context.Context,
return api.service.messenger.DismissLatestContactRequestForContact(ctx, request)
}
func (api *PublicAPI) GetLatestContactRequestForContact(ctx context.Context, contactID string) (*protocol.MessengerResponse, error) {
return api.service.messenger.GetLatestContactRequestForContact(contactID)
}
func (api *PublicAPI) RetractContactRequest(ctx context.Context, request *requests.RetractContactRequest) (*protocol.MessengerResponse, error) {
return api.service.messenger.RetractContactRequest(request)
}
@@ -1348,11 +1368,11 @@ func (api *PublicAPI) DeleteActivityCenterNotifications(ctx context.Context, ids
}
func (api *PublicAPI) RequestAllHistoricMessages(forceFetchingBackup bool) (*protocol.MessengerResponse, error) {
return api.service.messenger.RequestAllHistoricMessages(forceFetchingBackup)
return api.service.messenger.RequestAllHistoricMessages(forceFetchingBackup, false)
}
func (api *PublicAPI) RequestAllHistoricMessagesWithRetries(forceFetchingBackup bool) (*protocol.MessengerResponse, error) {
return api.service.messenger.RequestAllHistoricMessagesWithRetries(forceFetchingBackup)
return api.service.messenger.RequestAllHistoricMessages(forceFetchingBackup, true)
}
func (api *PublicAPI) DisconnectActiveMailserver() {
@@ -1479,6 +1499,10 @@ func (api *PublicAPI) ToggleUseMailservers(value bool) error {
return api.service.messenger.ToggleUseMailservers(value)
}
func (api *PublicAPI) TogglePeerSyncing(request *requests.TogglePeerSyncingRequest) error {
return api.service.messenger.TogglePeerSyncing(request)
}
func (api *PublicAPI) SetPinnedMailservers(pinnedMailservers map[string]string) error {
return api.service.messenger.SetPinnedMailservers(pinnedMailservers)
}
@@ -1663,53 +1687,67 @@ func (api *PublicAPI) CreateTokenGatedCommunity() (*protocol.MessengerResponse,
}
// Set profile showcase preference for current user
func (api *PublicAPI) SetProfileShowcasePreferences(preferences *protocol.ProfileShowcasePreferences) error {
return api.service.messenger.SetProfileShowcasePreferences(preferences)
func (api *PublicAPI) SetProfileShowcasePreferences(preferences *identity.ProfileShowcasePreferences) error {
return api.service.messenger.SetProfileShowcasePreferences(preferences, true)
}
// Get all profile showcase preferences for current user
func (api *PublicAPI) GetProfileShowcasePreferences() (*protocol.ProfileShowcasePreferences, error) {
func (api *PublicAPI) GetProfileShowcasePreferences() (*identity.ProfileShowcasePreferences, error) {
return api.service.messenger.GetProfileShowcasePreferences()
}
// Get profile showcase for a contact
func (api *PublicAPI) GetProfileShowcaseForContact(contactID string) (*protocol.ProfileShowcase, error) {
return api.service.messenger.GetProfileShowcaseForContact(contactID)
func (api *PublicAPI) GetProfileShowcaseForContact(contactID string, validate bool) (*identity.ProfileShowcase, error) {
return api.service.messenger.GetProfileShowcaseForContact(contactID, validate)
}
// Get profile showcase accounts by address
func (api *PublicAPI) GetProfileShowcaseAccountsByAddress(address string) ([]*protocol.ProfileShowcaseAccount, error) {
func (api *PublicAPI) GetProfileShowcaseAccountsByAddress(address string) ([]*identity.ProfileShowcaseAccount, error) {
return api.service.messenger.GetProfileShowcaseAccountsByAddress(address)
}
// Get profile showcase max social link entries count
func (api *PublicAPI) GetProfileShowcaseSocialLinksLimit() (int, error) {
return api.service.messenger.GetProfileShowcaseSocialLinksLimit()
}
// Get profile showcase max entries count (excluding social links)
func (api *PublicAPI) GetProfileShowcaseEntriesLimit() (int, error) {
return api.service.messenger.GetProfileShowcaseEntriesLimit()
}
// 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 {
@@ -1737,6 +1775,15 @@ func (api *PublicAPI) SetCustomizationColor(ctx context.Context, request *reques
return api.service.messenger.SetCustomizationColor(ctx, request)
}
func (api *PublicAPI) GetCommunityMemberAllMessages(request *requests.CommunityMemberMessages) ([]*common.Message, error) {
return api.service.messenger.GetCommunityMemberAllMessages(request)
}
// Delete a specific community member messages or all community member messages (based on provided parameters)
func (api *PublicAPI) DeleteCommunityMemberMessages(request *requests.DeleteCommunityMemberMessages) (*protocol.MessengerResponse, error) {
return api.service.messenger.DeleteCommunityMemberMessages(request)
}
// -----
// HELPER
// -----

View File

@@ -39,7 +39,6 @@ import (
"github.com/status-im/status-go/protocol"
"github.com/status-im/status-go/protocol/anonmetrics"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/common/shard"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/communities/token"
"github.com/status-im/status-go/protocol/protobuf"
@@ -53,6 +52,7 @@ import (
"github.com/status-im/status-go/services/ext/mailservers"
mailserversDB "github.com/status-im/status-go/services/mailservers"
"github.com/status-im/status-go/services/wallet"
"github.com/status-im/status-go/services/wallet/collectibles"
w_common "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/wakuv2"
@@ -534,7 +534,30 @@ func (s *Service) GetCommunityID(tokenURI string) string {
return ""
}
func (s *Service) FillCollectibleMetadata(collectible *thirdparty.FullCollectibleData) error {
func (s *Service) FillCollectiblesMetadata(communityID string, cs []*thirdparty.FullCollectibleData) (bool, error) {
if s.messenger == nil {
return false, fmt.Errorf("messenger not ready")
}
community, err := s.fetchCommunityInfoForCollectibles(communityID, collectibles.IDsFromAssets(cs))
if err != nil {
return false, err
}
if community == nil {
return false, nil
}
for _, collectible := range cs {
err := s.FillCollectibleMetadata(community, collectible)
if err != nil {
return true, err
}
}
return true, nil
}
func (s *Service) FillCollectibleMetadata(community *communities.Community, collectible *thirdparty.FullCollectibleData) error {
if s.messenger == nil {
return fmt.Errorf("messenger not ready")
}
@@ -550,18 +573,6 @@ func (s *Service) FillCollectibleMetadata(collectible *thirdparty.FullCollectibl
return fmt.Errorf("invalid communityID")
}
// FetchCommunityInfo should have been previously called once to ensure
// that the latest version of the CommunityDescription is available in the DB
community, err := s.fetchCommunity(communityID, false)
if err != nil {
return err
}
if community == nil {
return nil
}
tokenMetadata, err := s.fetchCommunityCollectibleMetadata(community, id.ContractID)
if err != nil {
@@ -636,47 +647,84 @@ func communityToInfo(community *communities.Community) *thirdparty.CommunityInfo
}
}
func (s *Service) FetchCommunityInfo(communityID string) (*thirdparty.CommunityInfo, error) {
community, err := s.fetchCommunity(communityID, true)
func (s *Service) fetchCommunityFromStoreNodes(communityID string) (*communities.Community, error) {
community, err := s.messenger.FetchCommunity(&protocol.FetchCommunityRequest{
CommunityKey: communityID,
TryDatabase: false,
WaitForResponse: true,
})
if err != nil {
return nil, err
}
return communityToInfo(community), nil
return community, nil
}
func (s *Service) fetchCommunity(communityID string, fetchLatest bool) (*communities.Community, error) {
func (s *Service) GetCommunityInfoFromDB(communityID string) (*thirdparty.CommunityInfo, error) {
community, err := s.messenger.FindCommunityInfoFromDB(communityID)
return communityToInfo(community), err
}
// Fetch latest community from store nodes.
func (s *Service) FetchCommunityInfo(communityID string) (*thirdparty.CommunityInfo, error) {
if s.messenger == nil {
return nil, fmt.Errorf("messenger not ready")
}
// Try to fetch metadata from Messenger communities
community, err := s.messenger.FindCommunityInfoFromDB(communityID)
if err != nil && err != communities.ErrOrgNotFound {
return nil, err
}
// TODO: we need the shard information in the collectible to be able to retrieve info for
// communities that have specific shards
if fetchLatest {
// Try to fetch the latest version of the Community
var shard *shard.Shard = nil // TODO: build this with info from token
// NOTE: The community returned by this function will be nil if
// the version we have in the DB is the latest available.
_, err := s.messenger.FetchCommunity(&protocol.FetchCommunityRequest{
CommunityKey: communityID,
Shard: shard,
TryDatabase: false,
WaitForResponse: true,
})
// Fetch latest version from store nodes
if community == nil || !community.IsControlNode() {
community, err = s.fetchCommunityFromStoreNodes(communityID)
if err != nil {
return nil, err
}
}
// Get the latest successfully fetched version of the Community
return communityToInfo(community), nil
}
// Fetch latest community from store nodes only if any collectibles data is missing.
func (s *Service) fetchCommunityInfoForCollectibles(communityID string, ids []thirdparty.CollectibleUniqueID) (*communities.Community, error) {
community, err := s.messenger.FindCommunityInfoFromDB(communityID)
if err != nil {
if err != nil && err != communities.ErrOrgNotFound {
return nil, err
}
if community == nil {
return s.fetchCommunityFromStoreNodes(communityID)
}
if community.IsControlNode() {
return community, nil
}
contractIDs := func() map[string]thirdparty.ContractID {
result := map[string]thirdparty.ContractID{}
for _, id := range ids {
result[id.HashKey()] = id.ContractID
}
return result
}()
hasAllMetadata := true
for _, contractID := range contractIDs {
tokenMetadata, err := s.fetchCommunityCollectibleMetadata(community, contractID)
if err != nil {
return nil, err
}
if tokenMetadata == nil {
hasAllMetadata = false
break
}
}
if !hasAllMetadata {
return s.fetchCommunityFromStoreNodes(communityID)
}
return community, nil
}

View File

@@ -146,13 +146,16 @@ func (d *Database) Add(mailserver Mailserver) error {
}
func (d *Database) Mailservers() ([]Mailserver, error) {
var result []Mailserver
rows, err := d.db.Query(`SELECT id, name, address, password, fleet FROM mailservers`)
if err != nil {
return nil, err
}
defer rows.Close()
return toMailservers(rows)
}
func toMailservers(rows *sql.Rows) ([]Mailserver, error) {
var result []Mailserver
for rows.Next() {
var (
@@ -198,7 +201,7 @@ func (d *Database) AddGaps(gaps []MailserverRequestGap) error {
for _, gap := range gaps {
_, err := tx.Exec(`INSERT OR REPLACE INTO mailserver_request_gaps(
_, err = tx.Exec(`INSERT OR REPLACE INTO mailserver_request_gaps(
id,
chat_id,
gap_from,

View File

@@ -1,103 +0,0 @@
# Provide dynamic activity updates
Task: https://github.com/status-im/status-desktop/issues/12120
## Intro
In the current approach only static paginated filtering is possible because the filtering is done in SQL
The updated requirements need to support dynamic updates of the current visualized filter
## Plan
- [ ] Required common (runtime/SQL) infrastructure
- [-] Refactor into a session based filter
- [-] Keep a mirror of identities for session
- [-] Capture events (new downloaded and pending first)
- [-] Have the simplest filter to handle new and updated and emit wallet event
- [ ] Handle update filter events in UX and alter the model (add/remove)
- [ ] Asses how the runtime filter grows in complexity/risk
- [ ] Quick prototype of SQL only filter if still make sense
- [ ] Refactor the async handling to fit the session based better (use channels and goroutine)
## How to
I see two ways:
- Keep a **runtime** (go/nim) dynamic in memory filter that is in sync with the SQL filter and use the filter to process transactions updates and propagate to the current visualized model
- The filter will push changes to the in memory model based on the sorting and filtering criteria
- If the filter is completely in sync withe the SQL one, then the dynamic updates to the model should have the same content as fetched from scratch from the DB
- *Advantages*
- Less memory and performance requirements
- *Disadvantages*
- Two sources of truth for the filter
- With tests for each event this can be mitigated
- Complexity around the multi-transaction/sub-transaction relation
- If we miss doing equivalent changes in bot filters (SQL and runtime) the filter might not be in sync with the SQL one and have errors in update
- **Refresh SQL filter** on every transaction (or bulk) update to DB and compare with the current visualized filter to extract differences and push as change notifications
- This approach is more expensive in terms of memory and performance but will use only one source of truth implementation
- This way we know for sure that the updated model is in sync with a newly fetched one
- *Advantages*
- Less complexity and less risk to be out of sync with the SQL filter
- *Disadvantages*
- More memory and performance requirements
- The real improvement will be to do the postponed refactoring of the activity in DB
## Requirements
Expected filter states to be addressed
- Filter is set
- No Filter
- Filter is cleared
- How about if only partially cleared?
Expected dynamic events
- **New transactions**
- Pending
- Downloaded (external)
- Multi-transactions?
- **Transaction changed state**
- Pending to confirmed (new transaction/removed transaction)
Filter criteria
- time interval: start-end
- activity type (send/receive/buy/swap/bridge/contract_deploy/mint)
- status (pending/failed/confirmed/finalized)
- addresses
- tokens
- multi-transaction filtering transaction
## Implementation
### SQL filter
For new events
- keep a mirror of identities on status-go side (optional session based)
- on update events fetch identities and check against the mirror if any is new
- for new entries send the notification with the transaction details
- keep pending changes (not added)
- remove entries that were processed for this session
For update?
- check if entry is in the mirror and propagate update event
### Mirror filter
For new events
- keep a mirror of identities
- on update events pass them through the filter and if they pass send updates
- the filter checks criteria and available mirror interval to dismiss from mirror
- sub-transactions challenge
- TODO
- token challenges
- TODO
For update?
- check if entry is in the mirror and propagate update event

View File

@@ -52,7 +52,7 @@ const (
type Entry struct {
payloadType PayloadType
transaction *transfer.TransactionIdentity
id transfer.MultiTransactionIDType
id common.MultiTransactionIDType
timestamp int64
activityType Type
activityStatus Status
@@ -68,30 +68,32 @@ type Entry struct {
chainIDIn *common.ChainID
transferType *TransferType
contractAddress *eth.Address
communityID *string
isNew bool // isNew is used to indicate if the entry is newer than session start (changed state also)
}
// Only used for JSON marshalling
type EntryData struct {
PayloadType PayloadType `json:"payloadType"`
Transaction *transfer.TransactionIdentity `json:"transaction,omitempty"`
ID *transfer.MultiTransactionIDType `json:"id,omitempty"`
Timestamp *int64 `json:"timestamp,omitempty"`
ActivityType *Type `json:"activityType,omitempty"`
ActivityStatus *Status `json:"activityStatus,omitempty"`
AmountOut *hexutil.Big `json:"amountOut,omitempty"`
AmountIn *hexutil.Big `json:"amountIn,omitempty"`
TokenOut *Token `json:"tokenOut,omitempty"`
TokenIn *Token `json:"tokenIn,omitempty"`
SymbolOut *string `json:"symbolOut,omitempty"`
SymbolIn *string `json:"symbolIn,omitempty"`
Sender *eth.Address `json:"sender,omitempty"`
Recipient *eth.Address `json:"recipient,omitempty"`
ChainIDOut *common.ChainID `json:"chainIdOut,omitempty"`
ChainIDIn *common.ChainID `json:"chainIdIn,omitempty"`
TransferType *TransferType `json:"transferType,omitempty"`
ContractAddress *eth.Address `json:"contractAddress,omitempty"`
PayloadType PayloadType `json:"payloadType"`
Transaction *transfer.TransactionIdentity `json:"transaction,omitempty"`
ID *common.MultiTransactionIDType `json:"id,omitempty"`
Timestamp *int64 `json:"timestamp,omitempty"`
ActivityType *Type `json:"activityType,omitempty"`
ActivityStatus *Status `json:"activityStatus,omitempty"`
AmountOut *hexutil.Big `json:"amountOut,omitempty"`
AmountIn *hexutil.Big `json:"amountIn,omitempty"`
TokenOut *Token `json:"tokenOut,omitempty"`
TokenIn *Token `json:"tokenIn,omitempty"`
SymbolOut *string `json:"symbolOut,omitempty"`
SymbolIn *string `json:"symbolIn,omitempty"`
Sender *eth.Address `json:"sender,omitempty"`
Recipient *eth.Address `json:"recipient,omitempty"`
ChainIDOut *common.ChainID `json:"chainIdOut,omitempty"`
ChainIDIn *common.ChainID `json:"chainIdIn,omitempty"`
TransferType *TransferType `json:"transferType,omitempty"`
ContractAddress *eth.Address `json:"contractAddress,omitempty"`
CommunityID *string `json:"communityId,omitempty"`
IsNew *bool `json:"isNew,omitempty"`
@@ -116,6 +118,7 @@ func (e *Entry) MarshalJSON() ([]byte, error) {
ChainIDIn: e.chainIDIn,
TransferType: e.transferType,
ContractAddress: e.contractAddress,
CommunityID: e.communityID,
}
if e.payloadType == MultiTransactionPT {
@@ -162,6 +165,7 @@ func (e *Entry) UnmarshalJSON(data []byte) error {
e.chainIDOut = aux.ChainIDOut
e.chainIDIn = aux.ChainIDIn
e.transferType = aux.TransferType
e.communityID = aux.CommunityID
e.isNew = aux.IsNew != nil && *aux.IsNew
@@ -192,7 +196,7 @@ func newActivityEntryWithTransaction(pending bool, transaction *transfer.Transac
}
}
func NewActivityEntryWithMultiTransaction(id transfer.MultiTransactionIDType, timestamp int64, activityType Type, activityStatus Status) Entry {
func NewActivityEntryWithMultiTransaction(id common.MultiTransactionIDType, timestamp int64, activityType Type, activityStatus Status) Entry {
return Entry{
payloadType: MultiTransactionPT,
id: id,
@@ -510,14 +514,14 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses
dbPTrAmount := new(big.Int)
var dbMtFromAmount, dbMtToAmount, contractType sql.NullString
var tokenCode, fromTokenCode, toTokenCode sql.NullString
var methodHash sql.NullString
var methodHash, communityID sql.NullString
var transferType *TransferType
var communityMintEventDB sql.NullBool
var communityMintEvent bool
err := rows.Scan(&transferHash, &pendingHash, &chainID, &multiTxID, &timestamp, &dbMtType, &dbTrType, &fromAddress,
&toAddressDB, &ownerAddressDB, &dbTrAmount, (*bigint.SQLBigIntBytes)(dbPTrAmount), &dbMtFromAmount, &dbMtToAmount, &aggregatedStatus, &aggregatedCount,
&tokenAddress, &dbTokenID, &tokenCode, &fromTokenCode, &toTokenCode, &outChainIDDB, &inChainIDDB, &contractType,
&contractAddressDB, &methodHash, &communityMintEventDB)
&contractAddressDB, &methodHash, &communityMintEventDB, &communityID)
if err != nil {
return nil, err
}
@@ -656,7 +660,7 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses
*inChainID = common.ChainID(inChainIDDB.Int64)
}
entry = NewActivityEntryWithMultiTransaction(transfer.MultiTransactionIDType(multiTxID.Int64),
entry = NewActivityEntryWithMultiTransaction(common.MultiTransactionIDType(multiTxID.Int64),
timestamp, activityType, activityStatus)
// Extract tokens
@@ -676,6 +680,10 @@ func getActivityEntries(ctx context.Context, deps FilterDependencies, addresses
return nil, errors.New("invalid row data")
}
if communityID.Valid {
entry.communityID = common.NewAndSet(communityID.String)
}
// Complete common data
entry.recipient = &toAddress
entry.sender = &fromAddress

View File

@@ -99,6 +99,18 @@ type Filter struct {
FilterOutCollectibles bool `json:"filterOutCollectibles"`
}
func (f *Filter) IsEmpty() bool {
return f.Period.StartTimestamp == NoLimitTimestampForPeriod &&
f.Period.EndTimestamp == NoLimitTimestampForPeriod &&
len(f.Types) == 0 &&
len(f.Statuses) == 0 &&
len(f.CounterpartyAddresses) == 0 &&
len(f.Assets) == 0 &&
len(f.Collectibles) == 0 &&
!f.FilterOutAssets &&
!f.FilterOutCollectibles
}
func GetRecipients(ctx context.Context, db *sql.DB, chainIDs []common.ChainID, addresses []eth.Address, offset int, limit int) (recipients []eth.Address, hasMore bool, err error) {
filterAllAddresses := len(addresses) == 0
involvedAddresses := noEntriesInTmpTableSQLValues

View File

@@ -198,7 +198,10 @@ SELECT
END AS agg_status,
1 AS agg_count,
transfers.token_address AS token_address,
transfers.token_id AS token_id,
CASE
WHEN LENGTH(transfers.token_id) = 0 THEN X'00'
ELSE transfers.token_id
END AS tmp_token_id,
NULL AS token_code,
NULL AS from_token_code,
NULL AS to_token_code,
@@ -213,7 +216,12 @@ SELECT
CASE
WHEN transfers.tx_from_address = zeroAddress AND transfers.type = "erc20" THEN (SELECT 1 FROM json_each(transfers.receipt, '$.logs' ) WHERE json_extract( value, '$.topics[0]' ) = communityMintEvent)
ELSE NULL
END AS community_mint_event
END AS community_mint_event,
CASE
WHEN transfers.type = 'erc20' THEN (SELECT community_id FROM tokens WHERE transfers.token_address = tokens.address AND transfers.network_id = tokens.network_id)
WHEN transfers.type = 'erc721' OR transfers.type = 'erc1155' THEN (SELECT community_id FROM collectible_data_cache WHERE transfers.token_address = collectible_data_cache.contract_address AND transfers.network_id = collectible_data_cache.chain_id)
ELSE NULL
END AS community_id
FROM
transfers
CROSS JOIN filter_conditions
@@ -323,7 +331,7 @@ WHERE
AND (
(
transfers.network_id,
transfers.token_id,
tmp_token_id,
transfers.token_address
) IN assets_erc721
)
@@ -373,7 +381,7 @@ SELECT
statusPending AS agg_status,
1 AS agg_count,
NULL AS token_address,
NULL AS token_id,
NULL AS tmp_token_id,
pending_transactions.symbol AS token_code,
NULL AS from_token_code,
NULL AS to_token_code,
@@ -382,7 +390,8 @@ SELECT
pending_transactions.type AS type,
NULL as contract_address,
NULL AS method_hash,
NULL AS community_mint_event
NULL AS community_mint_event,
NULL AS community_id
FROM
pending_transactions
CROSS JOIN filter_conditions
@@ -465,7 +474,7 @@ SELECT
END AS agg_status,
COALESCE(tr_status.count, 0) + COALESCE(pending_status.count, 0) AS agg_count,
NULL AS token_address,
NULL AS token_id,
NULL AS tmp_token_id,
NULL AS token_code,
multi_transactions.from_asset AS from_token_code,
multi_transactions.to_asset AS to_token_code,
@@ -474,7 +483,8 @@ SELECT
NULL AS type,
NULL as contract_address,
NULL AS method_hash,
NULL AS community_mint_event
NULL AS community_mint_event,
NULL AS community_id
FROM
multi_transactions
CROSS JOIN filter_conditions

View File

@@ -68,9 +68,9 @@ type Service struct {
subscriptions event.Subscription
ch chan walletevent.Event
// sessionsRWMutex is used to protect all sessions related members
sessionsRWMutex sync.RWMutex
sessionsRWMutex sync.RWMutex
debounceDuration time.Duration
// TODO #12120: sort out session dependencies
pendingTracker *transactions.PendingTxTracker
}
@@ -87,6 +87,8 @@ func NewService(db *sql.DB, tokenManager token.ManagerInterface, collectibles co
scheduler: async.NewMultiClientScheduler(),
sessions: make(map[SessionID]*Session),
// here to be overwritten by tests
debounceDuration: 1 * time.Second,
pendingTracker: pendingTracker,
}

View File

@@ -4,8 +4,7 @@ import (
"context"
"errors"
"strconv"
"golang.org/x/exp/slices"
"time"
eth "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/event"
@@ -22,14 +21,17 @@ const nilStr = "nil"
type EntryIdentity struct {
payloadType PayloadType
transaction *transfer.TransactionIdentity
id transfer.MultiTransactionIDType
id common.MultiTransactionIDType
}
// func (e EntryIdentity) same(a EntryIdentity) bool {
// return a.payloadType == e.payloadType && (a.transaction == e.transaction && (a.transaction == nil || (a.transaction.ChainID == e.transaction.ChainID &&
// a.transaction.Hash == e.transaction.Hash &&
// a.transaction.Address == e.transaction.Address))) && a.id == e.id
// }
func (e EntryIdentity) same(a EntryIdentity) bool {
return a.payloadType == e.payloadType &&
((a.transaction == nil && e.transaction == nil) ||
(a.transaction.ChainID == e.transaction.ChainID &&
a.transaction.Hash == e.transaction.Hash &&
a.transaction.Address == e.transaction.Address)) &&
a.id == e.id
}
func (e EntryIdentity) key() string {
txID := nilStr
@@ -41,6 +43,13 @@ func (e EntryIdentity) key() string {
type SessionID int32
// Session stores state related to a filter session
// The user happy flow is:
// 1. StartFilterSession to get a new SessionID and client be notified by the current state
// 2. GetMoreForFilterSession anytime to get more entries after the first page
// 3. UpdateFilterForSession to update the filter and get the new state or clean the filter and get the newer entries
// 4. ResetFilterSession in case client receives SessionUpdate with HasNewOnTop = true to get the latest state
// 5. StopFilterSession to stop the session when no used (user changed from activity screens or changed addresses and chains)
type Session struct {
id SessionID
@@ -53,15 +62,22 @@ type Session struct {
// model is a mirror of the data model presentation has (sent by EventActivityFilteringDone)
model []EntryIdentity
// noFilterModel is a mirror of the data model presentation has when filter is empty
noFilterModel map[string]EntryIdentity
// new holds the new entries until user requests update by calling ResetFilterSession
new []EntryIdentity
}
type EntryUpdate struct {
Pos int `json:"pos"`
Entry *Entry `json:"entry"`
}
// SessionUpdate payload for EventActivitySessionUpdated
type SessionUpdate struct {
HasNewEntries *bool `json:"hasNewEntries,omitempty"`
Removed []EntryIdentity `json:"removed,omitempty"`
Updated []Entry `json:"updated,omitempty"`
HasNewOnTop *bool `json:"hasNewOnTop,omitempty"`
New []*EntryUpdate `json:"new,omitempty"`
Removed []EntryIdentity `json:"removed,omitempty"`
}
type fullFilterParams struct {
@@ -99,21 +115,44 @@ func (s *Service) internalFilter(f fullFilterParams, offset int, count int, proc
})
}
// mirrorIdentities for update use
func mirrorIdentities(entries []Entry) []EntryIdentity {
model := make([]EntryIdentity, 0, len(entries))
for _, a := range entries {
model = append(model, EntryIdentity{
payloadType: a.payloadType,
transaction: a.transaction,
id: a.id,
})
}
return model
}
func (s *Service) internalFilterForSession(session *Session, firstPageCount int) {
s.internalFilter(
fullFilterParams{
sessionID: session.id,
addresses: session.addresses,
allAddresses: session.allAddresses,
chainIDs: session.chainIDs,
filter: session.filter,
},
0,
firstPageCount,
func(entries []Entry) (offset int) {
s.sessionsRWMutex.Lock()
defer s.sessionsRWMutex.Unlock()
session.model = mirrorIdentities(entries)
return 0
},
)
}
func (s *Service) StartFilterSession(addresses []eth.Address, allAddresses bool, chainIDs []common.ChainID, filter Filter, firstPageCount int) SessionID {
sessionID := s.nextSessionID()
// TODO #12120: sort rest of the filters
// TODO #12120: prettyfy this
slices.SortFunc(addresses, func(a eth.Address, b eth.Address) bool {
return a.Hex() < b.Hex()
})
slices.Sort(chainIDs)
slices.SortFunc(filter.CounterpartyAddresses, func(a eth.Address, b eth.Address) bool {
return a.Hex() < b.Hex()
})
s.sessionsRWMutex.Lock()
subscribeToEvents := len(s.sessions) == 0
session := &Session{
id: sessionID,
@@ -124,6 +163,10 @@ func (s *Service) StartFilterSession(addresses []eth.Address, allAddresses bool,
model: make([]EntryIdentity, 0, firstPageCount),
}
s.sessionsRWMutex.Lock()
subscribeToEvents := len(s.sessions) == 0
s.sessions[sessionID] = session
if subscribeToEvents {
@@ -131,36 +174,81 @@ func (s *Service) StartFilterSession(addresses []eth.Address, allAddresses bool,
}
s.sessionsRWMutex.Unlock()
s.internalFilter(
fullFilterParams{
sessionID: sessionID,
addresses: addresses,
allAddresses: allAddresses,
chainIDs: chainIDs,
filter: filter,
},
0,
firstPageCount,
func(entries []Entry) (offset int) {
// Mirror identities for update use
s.sessionsRWMutex.Lock()
defer s.sessionsRWMutex.Unlock()
session.model = make([]EntryIdentity, 0, len(entries))
for _, a := range entries {
session.model = append(session.model, EntryIdentity{
payloadType: a.payloadType,
transaction: a.transaction,
id: a.id,
})
}
return 0
},
)
s.internalFilterForSession(session, firstPageCount)
return sessionID
}
// UpdateFilterForSession is to be called for updating the filter of a specific session
// After calling this method to set a filter all the incoming changes will be reported with
// Entry.isNew = true when filter is reset to empty
func (s *Service) UpdateFilterForSession(id SessionID, filter Filter, firstPageCount int) error {
s.sessionsRWMutex.RLock()
session, found := s.sessions[id]
if !found {
s.sessionsRWMutex.RUnlock()
return errors.New("session not found")
}
prevFilterEmpty := session.filter.IsEmpty()
newFilerEmpty := filter.IsEmpty()
s.sessionsRWMutex.RUnlock()
s.sessionsRWMutex.Lock()
session.new = nil
session.filter = filter
if prevFilterEmpty && !newFilerEmpty {
// Session is moving from empty to non-empty filter
// Take a snapshot of the current model
session.noFilterModel = entryIdsToMap(session.model)
session.model = make([]EntryIdentity, 0, firstPageCount)
// In this case there is nothing to flag so we request the first page
s.internalFilterForSession(session, firstPageCount)
} else if !prevFilterEmpty && newFilerEmpty {
// Session is moving from non-empty to empty filter
// In this case we need to flag all the new entries that are not in the noFilterModel
s.internalFilter(
fullFilterParams{
sessionID: session.id,
addresses: session.addresses,
allAddresses: session.allAddresses,
chainIDs: session.chainIDs,
filter: session.filter,
},
0,
firstPageCount,
func(entries []Entry) (offset int) {
s.sessionsRWMutex.Lock()
defer s.sessionsRWMutex.Unlock()
// Mark new entries
for i, a := range entries {
_, found := session.noFilterModel[a.getIdentity().key()]
entries[i].isNew = !found
}
// Mirror identities for update use
session.model = mirrorIdentities(entries)
session.noFilterModel = nil
return 0
},
)
} else {
// Else act as a normal filter update
s.internalFilterForSession(session, firstPageCount)
}
s.sessionsRWMutex.Unlock()
return nil
}
// ResetFilterSession is to be called when SessionUpdate.HasNewOnTop == true to
// update client with the latest state including new on top entries
func (s *Service) ResetFilterSession(id SessionID, firstPageCount int) error {
session, found := s.sessions[id]
if !found {
@@ -189,15 +277,16 @@ func (s *Service) ResetFilterSession(id SessionID, firstPageCount int) error {
}
session.new = nil
// Mirror client identities for checking updates
session.model = make([]EntryIdentity, 0, len(entries))
for _, a := range entries {
session.model = append(session.model, EntryIdentity{
payloadType: a.payloadType,
transaction: a.transaction,
id: a.id,
})
if session.noFilterModel != nil {
// Add reported new entries to mark them as seen
for _, a := range newMap {
session.noFilterModel[a.key()] = a
}
}
// Mirror client identities for checking updates
session.model = mirrorIdentities(entries)
return 0
},
)
@@ -248,55 +337,91 @@ func (s *Service) subscribeToEvents() {
go s.processEvents()
}
// TODO #12120: check that it exits on channel close
// processEvents runs only if more than one session is active
func (s *Service) processEvents() {
eventCount := 0
lastUpdate := time.Now().UnixMilli()
for event := range s.ch {
// TODO #12120: process rest of the events
// TODO #12120: debounce for 1s
if event.Type == transactions.EventPendingTransactionUpdate {
for sessionID := range s.sessions {
session := s.sessions[sessionID]
activities, err := getActivityEntries(context.Background(), s.getDeps(), session.addresses, session.allAddresses, session.chainIDs, session.filter, 0, len(session.model))
if err != nil {
log.Error("Error getting activity entries", "error", err)
continue
}
s.sessionsRWMutex.RLock()
allData := append(session.model, session.new...)
new, _ /*removed*/ := findUpdates(allData, activities)
s.sessionsRWMutex.RUnlock()
s.sessionsRWMutex.Lock()
lastProcessed := -1
for i, idRes := range new {
if i-lastProcessed > 1 {
// The events are not continuous, therefore these are not on top but mixed between existing entries
break
}
lastProcessed = idRes.newPos
// TODO #12120: make it more generic to follow the detection function
// TODO #12120: hold the first few and only send mixed and removed
if session.new == nil {
session.new = make([]EntryIdentity, 0, len(new))
}
session.new = append(session.new, idRes.id)
}
// TODO #12120: mixed
s.sessionsRWMutex.Unlock()
go notify(s.eventFeed, sessionID, len(session.new) > 0)
}
if event.Type == transactions.EventPendingTransactionUpdate ||
event.Type == transactions.EventPendingTransactionStatusChanged ||
event.Type == transfer.EventNewTransfers {
eventCount++
}
// debounce events updates
if eventCount > 0 &&
(time.Duration(time.Now().UnixMilli()-lastUpdate)*time.Millisecond) >= s.debounceDuration {
s.detectNew(eventCount)
eventCount = 0
lastUpdate = time.Now().UnixMilli()
}
}
}
func notify(eventFeed *event.Feed, id SessionID, hasNewEntries bool) {
payload := SessionUpdate{}
if hasNewEntries {
payload.HasNewEntries = &hasNewEntries
func (s *Service) detectNew(changeCount int) {
for sessionID := range s.sessions {
session := s.sessions[sessionID]
fetchLen := len(session.model) + changeCount
activities, err := getActivityEntries(context.Background(), s.getDeps(), session.addresses, session.allAddresses, session.chainIDs, session.filter, 0, fetchLen)
if err != nil {
log.Error("Error getting activity entries", "error", err)
continue
}
s.sessionsRWMutex.RLock()
allData := append(session.new, session.model...)
new, _ /*removed*/ := findUpdates(allData, activities)
s.sessionsRWMutex.RUnlock()
s.sessionsRWMutex.Lock()
lastProcessed := -1
onTop := true
var mixed []*EntryUpdate
for i, idRes := range new {
// Detect on top
if onTop {
// mixedIdentityResult.newPos includes session.new, therefore compensate for it
if ((idRes.newPos - len(session.new)) - lastProcessed) > 1 {
// From now on the events are not on top and continuous but mixed between existing entries
onTop = false
mixed = make([]*EntryUpdate, 0, len(new)-i)
}
lastProcessed = idRes.newPos
}
if onTop {
if session.new == nil {
session.new = make([]EntryIdentity, 0, len(new))
}
session.new = append(session.new, idRes.id)
} else {
modelPos := idRes.newPos - len(session.new)
entry := activities[idRes.newPos]
entry.isNew = true
mixed = append(mixed, &EntryUpdate{
Pos: modelPos,
Entry: &entry,
})
// Insert in session model at modelPos index
session.model = append(session.model[:modelPos], append([]EntryIdentity{{payloadType: entry.payloadType, transaction: entry.transaction, id: entry.id}}, session.model[modelPos:]...)...)
}
}
s.sessionsRWMutex.Unlock()
if len(session.new) > 0 || len(mixed) > 0 {
go notify(s.eventFeed, sessionID, len(session.new) > 0, mixed)
}
}
}
func notify(eventFeed *event.Feed, id SessionID, hasNewOnTop bool, mixed []*EntryUpdate) {
payload := SessionUpdate{
New: mixed,
}
if hasNewOnTop {
payload.HasNewOnTop = &hasNewOnTop
}
sendResponseEvent(eventFeed, (*int32)(&id), EventActivitySessionUpdated, payload, nil)
@@ -305,6 +430,8 @@ func notify(eventFeed *event.Feed, id SessionID, hasNewEntries bool) {
// unsubscribeFromEvents should be called with sessionsRWMutex locked for writing
func (s *Service) unsubscribeFromEvents() {
s.subscriptions.Unsubscribe()
close(s.ch)
s.ch = nil
s.subscriptions = nil
}
@@ -369,6 +496,9 @@ func entriesToMap(entries []Entry) map[string]Entry {
//
// implementation assumes the order of each identity doesn't change from old state (identities) and new state (updated); we have either add or removed.
func findUpdates(identities []EntryIdentity, updated []Entry) (new []mixedIdentityResult, removed []EntryIdentity) {
if len(updated) == 0 {
return
}
idsMap := entryIdsToMap(identities)
updatedMap := entriesToMap(updated)
@@ -381,6 +511,10 @@ func findUpdates(identities []EntryIdentity, updated []Entry) (new []mixedIdenti
id: id,
})
}
if len(identities) > 0 && entry.getIdentity().same(identities[len(identities)-1]) {
break
}
}
// Account for new entries

View File

@@ -316,6 +316,10 @@ func (api *API) FetchBalancesByOwnerAndContractAddress(ctx context.Context, chai
return api.s.collectiblesManager.FetchBalancesByOwnerAndContractAddress(ctx, chainID, ownerAddress, contractAddresses)
}
func (api *API) GetCollectibleOwnership(id thirdparty.CollectibleUniqueID) ([]thirdparty.AccountBalance, error) {
return api.s.collectiblesManager.GetCollectibleOwnership(id)
}
func (api *API) RefetchOwnedCollectibles() error {
log.Debug("wallet.api.RefetchOwnedCollectibles")
@@ -342,6 +346,16 @@ func (api *API) GetCollectibleOwnersByContractAddress(ctx context.Context, chain
return api.s.collectiblesManager.FetchCollectibleOwnersByContractAddress(ctx, chainID, contractAddress)
}
func (api *API) SearchCollectibles(ctx context.Context, chainID wcommon.ChainID, text string, cursor string, limit int, providerID string) (*thirdparty.FullCollectibleDataContainer, error) {
log.Debug("call to SearchCollectibles")
return api.s.collectiblesManager.SearchCollectibles(ctx, chainID, text, cursor, limit, providerID)
}
func (api *API) SearchCollections(ctx context.Context, chainID wcommon.ChainID, text string, cursor string, limit int, providerID string) (*thirdparty.CollectionDataContainer, error) {
log.Debug("call to SearchCollections")
return api.s.collectiblesManager.SearchCollections(ctx, chainID, text, cursor, limit, providerID)
}
/*
Collectibles API End
*/
@@ -567,7 +581,7 @@ func (api *API) ProceedWithTransactionsSignatures(ctx context.Context, signature
return api.s.transactionManager.ProceedWithTransactionsSignatures(ctx, signatures)
}
func (api *API) GetMultiTransactions(ctx context.Context, transactionIDs []transfer.MultiTransactionIDType) ([]*transfer.MultiTransaction, error) {
func (api *API) GetMultiTransactions(ctx context.Context, transactionIDs []wcommon.MultiTransactionIDType) ([]*transfer.MultiTransaction, error) {
log.Debug("wallet.api.GetMultiTransactions", "IDs.len", len(transactionIDs))
return api.s.transactionManager.GetMultiTransactions(ctx, transactionIDs)
}
@@ -582,6 +596,7 @@ func (api *API) FetchAllCurrencyFormats() (currency.FormatPerSymbol, error) {
return api.s.currency.FetchAllCurrencyFormats()
}
// @deprecated replaced by session APIs; see #12120
func (api *API) FilterActivityAsync(requestID int32, addresses []common.Address, allAddresses bool, chainIDs []wcommon.ChainID, filter activity.Filter, offset int, limit int) error {
log.Debug("wallet.api.FilterActivityAsync", "requestID", requestID, "addr.count", len(addresses), "allAddresses", allAddresses, "chainIDs.count", len(chainIDs), "offset", offset, "limit", limit)
@@ -589,6 +604,7 @@ func (api *API) FilterActivityAsync(requestID int32, addresses []common.Address,
return nil
}
// @deprecated replaced by session APIs; see #12120
func (api *API) CancelActivityFilterTask(requestID int32) error {
log.Debug("wallet.api.CancelActivityFilterTask", "requestID", requestID)
@@ -602,6 +618,12 @@ func (api *API) StartActivityFilterSession(addresses []common.Address, allAddres
return api.s.activity.StartFilterSession(addresses, allAddresses, chainIDs, filter, firstPageCount), nil
}
func (api *API) UpdateActivityFilterForSession(sessionID activity.SessionID, filter activity.Filter, firstPageCount int) error {
log.Debug("wallet.api.UpdateActivityFilterForSession", "sessionID", sessionID, "firstPageCount", firstPageCount)
return api.s.activity.UpdateFilterForSession(sessionID, filter, firstPageCount)
}
func (api *API) ResetActivityFilterSession(id activity.SessionID, firstPageCount int) error {
log.Debug("wallet.api.ResetActivityFilterSession", "id", id, "firstPageCount", firstPageCount)

View File

@@ -8,8 +8,6 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/rpc/chain"
)
// Reader interface for reading balance at a specified address.
@@ -17,7 +15,7 @@ type Reader interface {
BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error)
NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error)
HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
FullTransactionByBlockNumberAndIndex(ctx context.Context, blockNumber *big.Int, index uint) (*chain.FullTransaction, error)
CallBlockHashByTransaction(ctx context.Context, blockNumber *big.Int, index uint) (common.Hash, error)
NetworkID() uint64
}

View File

@@ -58,3 +58,36 @@ func (i *SQLBigIntBytes) Value() (driver.Value, error) {
}
return (*big.Int)(i).Bytes(), nil
}
type NilableSQLBigInt struct {
big.Int
isNil bool
}
func (i *NilableSQLBigInt) IsNil() bool {
return i.isNil
}
func (i *NilableSQLBigInt) SetNil() {
i.isNil = true
}
// Scan implements interface.
func (i *NilableSQLBigInt) Scan(value interface{}) error {
if value == nil {
i.SetNil()
return nil
}
val, ok := value.(int64)
if !ok {
return errors.New("not an integer")
}
i.SetInt64(val)
return nil
}
// Not implemented, used only for scanning
func (i *NilableSQLBigInt) Value() (driver.Value, error) {
return nil, errors.New("NilableSQLBigInt.Value is not implemented")
}

View File

@@ -106,4 +106,5 @@ type Bridge interface {
Send(sendArgs *TransactionBridge, verifiedAccount *account.SelectedExtKey) (types.Hash, error)
GetContractAddress(network *params.Network, token *token.Token) *common.Address
BuildTransaction(sendArgs *TransactionBridge) (*ethTypes.Transaction, error)
BuildTx(fromNetwork, toNetwork *params.Network, fromAddress common.Address, toAddress common.Address, token *token.Token, amountIn *big.Int, bonderFee *big.Int) (*ethTypes.Transaction, error)
}

View File

@@ -288,6 +288,27 @@ func (s *CBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.Net
return uint64(increasedEstimation), nil
}
func (s *CBridge) BuildTx(fromNetwork, toNetwork *params.Network, fromAddress common.Address, toAddress common.Address, token *token.Token, amountIn *big.Int, bonderFee *big.Int) (*ethTypes.Transaction, error) {
toAddr := types.Address(toAddress)
sendArgs := &TransactionBridge{
CbridgeTx: &CBridgeTxArgs{
SendTxArgs: transactions.SendTxArgs{
From: types.Address(fromAddress),
To: &toAddr,
Value: (*hexutil.Big)(amountIn),
Data: types.HexBytes("0x0"),
},
ChainID: toNetwork.ChainID,
Symbol: token.Symbol,
Recipient: toAddress,
Amount: (*hexutil.Big)(amountIn),
},
ChainID: fromNetwork.ChainID,
}
return s.BuildTransaction(sendArgs)
}
func (s *CBridge) GetContractAddress(network *params.Network, token *token.Token) *common.Address {
transferConfig, err := s.getTransferConfig(network.IsTest)
if err != nil {

View File

@@ -13,7 +13,6 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
"github.com/status-im/status-go/contracts/ierc1155"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/params"
@@ -59,7 +58,7 @@ func (s *ERC1155TransferBridge) EstimateGas(fromNetwork *params.Network, toNetwo
var input []byte
value := new(big.Int)
abi, err := abi.JSON(strings.NewReader(collectibles.CollectiblesMetaData.ABI))
abi, err := abi.JSON(strings.NewReader(ierc1155.Ierc1155ABI))
if err != nil {
return 0, err
}
@@ -102,6 +101,33 @@ func (s *ERC1155TransferBridge) EstimateGas(fromNetwork *params.Network, toNetwo
return uint64(increasedEstimation), nil
}
func (s *ERC1155TransferBridge) BuildTx(network, _ *params.Network, fromAddress common.Address, toAddress common.Address, token *token.Token, amountIn *big.Int, _ *big.Int) (*ethTypes.Transaction, error) {
contractAddress := types.Address(token.Address)
// We store ERC1155 Token ID using big.Int.String() in token.Symbol
tokenID, success := new(big.Int).SetString(token.Symbol, 10)
if !success {
return nil, fmt.Errorf("failed to convert ERC1155's Symbol %s to big.Int", token.Symbol)
}
sendArgs := &TransactionBridge{
ERC1155TransferTx: &ERC1155TransferTxArgs{
SendTxArgs: transactions.SendTxArgs{
From: types.Address(fromAddress),
To: &contractAddress,
Value: (*hexutil.Big)(amountIn),
Data: types.HexBytes("0x0"),
},
TokenID: (*hexutil.Big)(tokenID),
Recipient: toAddress,
Amount: (*hexutil.Big)(amountIn),
},
ChainID: network.ChainID,
}
return s.BuildTransaction(sendArgs)
}
func (s *ERC1155TransferBridge) sendOrBuild(sendArgs *TransactionBridge, signerFn bind.SignerFn) (tx *ethTypes.Transaction, err error) {
ethClient, err := s.rpcClient.EthClient(sendArgs.ChainID)
if err != nil {

View File

@@ -98,6 +98,32 @@ func (s *ERC721TransferBridge) EstimateGas(fromNetwork *params.Network, toNetwor
return uint64(increasedEstimation), nil
}
func (s *ERC721TransferBridge) BuildTx(network, _ *params.Network, fromAddress common.Address, toAddress common.Address, token *token.Token, amountIn *big.Int, _ *big.Int) (*ethTypes.Transaction, error) {
contractAddress := types.Address(token.Address)
// We store ERC721 Token ID using big.Int.String() in token.Symbol
tokenID, success := new(big.Int).SetString(token.Symbol, 10)
if !success {
return nil, fmt.Errorf("failed to convert ERC721's Symbol %s to big.Int", token.Symbol)
}
sendArgs := &TransactionBridge{
ERC721TransferTx: &ERC721TransferTxArgs{
SendTxArgs: transactions.SendTxArgs{
From: types.Address(fromAddress),
To: &contractAddress,
Value: (*hexutil.Big)(amountIn),
Data: types.HexBytes("0x0"),
},
TokenID: (*hexutil.Big)(tokenID),
Recipient: toAddress,
},
ChainID: network.ChainID,
}
return s.BuildTransaction(sendArgs)
}
func (s *ERC721TransferBridge) sendOrBuild(sendArgs *TransactionBridge, signerFn bind.SignerFn) (tx *ethTypes.Transaction, err error) {
ethClient, err := s.rpcClient.EthClient(sendArgs.ChainID)
if err != nil {

View File

@@ -3,6 +3,7 @@ package bridge
import (
"context"
"errors"
"fmt"
"math"
"math/big"
"strings"
@@ -215,6 +216,28 @@ func (h *HopBridge) EstimateGas(fromNetwork *params.Network, toNetwork *params.N
return uint64(increasedEstimation), nil
}
func (h *HopBridge) BuildTx(fromNetwork, toNetwork *params.Network, fromAddress common.Address, toAddress common.Address, token *token.Token, amountIn *big.Int, bonderFee *big.Int) (*ethTypes.Transaction, error) {
toAddr := types.Address(toAddress)
sendArgs := &TransactionBridge{
HopTx: &HopTxArgs{
SendTxArgs: transactions.SendTxArgs{
From: types.Address(fromAddress),
To: &toAddr,
Value: (*hexutil.Big)(amountIn),
Data: types.HexBytes("0x0"),
},
Symbol: token.Symbol,
Recipient: toAddress,
Amount: (*hexutil.Big)(amountIn),
BonderFee: (*hexutil.Big)(bonderFee),
ChainID: toNetwork.ChainID,
},
ChainID: fromNetwork.ChainID,
}
return h.BuildTransaction(sendArgs)
}
func (h *HopBridge) GetContractAddress(network *params.Network, token *token.Token) *common.Address {
var address common.Address
if network.Layer == 1 {
@@ -229,10 +252,10 @@ func (h *HopBridge) GetContractAddress(network *params.Network, token *token.Tok
func (h *HopBridge) sendOrBuild(sendArgs *TransactionBridge, signerFn bind.SignerFn) (tx *ethTypes.Transaction, err error) {
fromNetwork := h.contractMaker.RPCClient.NetworkManager.Find(sendArgs.ChainID)
if fromNetwork == nil {
return tx, err
return tx, fmt.Errorf("ChainID not supported %d", sendArgs.ChainID)
}
nonce, err := h.transactor.NextNonce(h.contractMaker.RPCClient, sendArgs.ChainID, sendArgs.HopTx.From)
nonce, err := h.transactor.NextNonce(h.contractMaker.RPCClient, fromNetwork.ChainID, sendArgs.HopTx.From)
if err != nil {
return tx, err
}
@@ -292,22 +315,35 @@ func (h *HopBridge) swapAndSend(chainID uint64, hopArgs *HopTxArgs, signerFn bin
return tx, err
}
toNetwork := h.contractMaker.RPCClient.NetworkManager.Find(hopArgs.ChainID)
if toNetwork == nil {
return tx, err
}
txOpts := hopArgs.ToTransactOpts(signerFn)
if token.IsNative() {
txOpts.Value = (*big.Int)(hopArgs.Amount)
}
now := time.Now()
deadline := big.NewInt(now.Unix() + 604800)
amountOutMin := big.NewInt(0)
destinationDeadline := big.NewInt(now.Unix() + 604800)
destinationAmountOutMin := big.NewInt(0)
if toNetwork.Layer == 1 {
destinationDeadline = big.NewInt(0)
}
tx, err = ammWrapper.SwapAndSend(
txOpts,
big.NewInt(int64(hopArgs.ChainID)),
new(big.Int).SetUint64(hopArgs.ChainID),
hopArgs.Recipient,
hopArgs.Amount.ToInt(),
hopArgs.BonderFee.ToInt(),
big.NewInt(0),
deadline,
big.NewInt(0),
amountOutMin,
deadline,
destinationAmountOutMin,
destinationDeadline,
)
return tx, err

View File

@@ -8,6 +8,7 @@ import (
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/contracts/ierc20"
@@ -84,6 +85,21 @@ func (s *TransferBridge) EstimateGas(fromNetwork *params.Network, toNetwork *par
return uint64(increasedEstimation), nil
}
func (s *TransferBridge) BuildTx(network, _ *params.Network, fromAddress common.Address, toAddress common.Address, token *token.Token, amountIn *big.Int, bonderFee *big.Int) (*ethTypes.Transaction, error) {
toAddr := types.Address(toAddress)
sendArgs := &TransactionBridge{
TransferTx: &transactions.SendTxArgs{
From: types.Address(fromAddress),
To: &toAddr,
Value: (*hexutil.Big)(amountIn),
Data: types.HexBytes("0x0"),
},
ChainID: network.ChainID,
}
return s.BuildTransaction(sendArgs)
}
func (s *TransferBridge) Send(sendArgs *TransactionBridge, verifiedAccount *account.SelectedExtKey) (types.Hash, error) {
return s.transactor.SendTransactionWithChainID(sendArgs.ChainID, *sendArgs.TransferTx, verifiedAccount)
}

View File

@@ -203,6 +203,12 @@ func scanCollectiblesDataRow(row *sql.Row) (*thirdparty.CollectibleData, error)
func (o *CollectibleDataDB) GetIDsNotInDB(ids []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleUniqueID, error) {
ret := make([]thirdparty.CollectibleUniqueID, 0, len(ids))
idMap := make(map[string]thirdparty.CollectibleUniqueID, len(ids))
// Ensure we don't have duplicates
for _, id := range ids {
idMap[id.HashKey()] = id
}
exists, err := o.db.Prepare(`SELECT EXISTS (
SELECT 1 FROM collectible_data_cache
@@ -212,7 +218,7 @@ func (o *CollectibleDataDB) GetIDsNotInDB(ids []thirdparty.CollectibleUniqueID)
return nil, err
}
for _, id := range ids {
for _, id := range idMap {
row := exists.QueryRow(
id.ContractID.ChainID,
id.ContractID.Address,

View File

@@ -180,6 +180,12 @@ func scanCollectionsDataRow(row *sql.Row) (*thirdparty.CollectionData, error) {
func (o *CollectionDataDB) GetIDsNotInDB(ids []thirdparty.ContractID) ([]thirdparty.ContractID, error) {
ret := make([]thirdparty.ContractID, 0, len(ids))
idMap := make(map[string]thirdparty.ContractID, len(ids))
// Ensure we don't have duplicates
for _, id := range ids {
idMap[id.HashKey()] = id
}
exists, err := o.db.Prepare(`SELECT EXISTS (
SELECT 1 FROM collection_data_cache
@@ -189,7 +195,7 @@ func (o *CollectionDataDB) GetIDsNotInDB(ids []thirdparty.ContractID) ([]thirdpa
return nil, err
}
for _, id := range ids {
for _, id := range idMap {
row := exists.QueryRow(
id.ChainID,
id.Address,

View File

@@ -164,7 +164,7 @@ type loadOwnedCollectiblesCommand struct {
ownedCollectiblesChangeCh chan<- OwnedCollectiblesChange
// Not to be set by the caller
partialOwnership []thirdparty.CollectibleUniqueID
partialOwnership []thirdparty.CollectibleIDBalance
err error
}
@@ -200,14 +200,18 @@ func (c *loadOwnedCollectiblesCommand) triggerEvent(eventType walletevent.EventT
})
}
func ownedTokensToTokenBalancesPerContractAddress(ownership []thirdparty.CollectibleUniqueID) thirdparty.TokenBalancesPerContractAddress {
func ownedTokensToTokenBalancesPerContractAddress(ownership []thirdparty.CollectibleIDBalance) thirdparty.TokenBalancesPerContractAddress {
ret := make(thirdparty.TokenBalancesPerContractAddress)
for _, id := range ownership {
balance := thirdparty.TokenBalance{
TokenID: id.TokenID,
Balance: &bigint.BigInt{Int: big.NewInt(1)},
for _, idBalance := range ownership {
balanceBigInt := idBalance.Balance
if balanceBigInt == nil {
balanceBigInt = &bigint.BigInt{Int: big.NewInt(1)}
}
ret[id.ContractID.Address] = append(ret[id.ContractID.Address], balance)
balance := thirdparty.TokenBalance{
TokenID: idBalance.ID.TokenID,
Balance: balanceBigInt,
}
ret[idBalance.ID.ContractID.Address] = append(ret[idBalance.ID.ContractID.Address], balance)
}
return ret
}
@@ -295,9 +299,6 @@ func (c *loadOwnedCollectiblesCommand) Run(parent context.Context) (err error) {
// Normally, update the DB once we've finished fetching
// If this is the first fetch, make partial updates to the client to get a better UX
if initialFetch || finished {
// Token balances should come from the providers. For now we assume all balances are 1, which
// is only valid for ERC721.
// TODO (#13025): Fetch balances from the providers.
balances := ownedTokensToTokenBalancesPerContractAddress(c.partialOwnership)
updateMessage.Removed, updateMessage.Updated, updateMessage.Added, err = c.ownershipDB.Update(c.chainID, c.account, balances, start.Unix())

View File

@@ -366,7 +366,7 @@ func (c *Controller) startSettingsWatcher() {
}
settingChangeCb := func(setting settings.SettingField, value interface{}) {
if setting.Equals(settings.TestNetworksEnabled) || setting.Equals(settings.IsSepoliaEnabled) {
if setting.Equals(settings.TestNetworksEnabled) || setting.Equals(settings.IsGoerliEnabled) {
c.stopPeriodicalOwnershipFetch()
err := c.startPeriodicalOwnershipFetch()
if err != nil {

View File

@@ -60,7 +60,7 @@ func filterOwnedCollectibles(ctx context.Context, db *sql.DB, chainIDs []wcommon
return nil, errors.New("no chainIDs provided")
}
q := sq.Select("ownership.chain_id,ownership.contract_address,ownership.token_id")
q := sq.Select("ownership.chain_id,ownership.contract_address,ownership.token_id").Distinct()
q = q.From("collectibles_ownership_cache ownership").
LeftJoin(`collectible_data_cache data ON
ownership.chain_id = data.chain_id AND

View File

@@ -8,15 +8,16 @@ import (
"math/big"
"net/http"
"strings"
"sync"
"time"
"github.com/afex/hystrix-go/hystrix"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/circuitbreaker"
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
"github.com/status-im/status-go/contracts/ierc1155"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/wallet/async"
@@ -31,8 +32,6 @@ import (
const requestTimeout = 5 * time.Second
const signalUpdatedCollectiblesDataPageSize = 10
const hystrixContractOwnershipClientName = "contractOwnershipClient"
const EventCollectiblesConnectionStatusChanged walletevent.EventType = "wallet-collectible-status-changed"
// ERC721 does not support function "TokenURI" if call
@@ -52,12 +51,8 @@ type ManagerInterface interface {
}
type Manager struct {
rpcClient *rpc.Client
contractOwnershipProviders []thirdparty.CollectibleContractOwnershipProvider
accountOwnershipProviders []thirdparty.CollectibleAccountOwnershipProvider
collectibleDataProviders []thirdparty.CollectibleDataProvider
collectionDataProviders []thirdparty.CollectionDataProvider
collectibleProviders []thirdparty.CollectibleProvider
rpcClient *rpc.Client
providers thirdparty.CollectibleProviders
httpClient *http.Client
@@ -68,27 +63,19 @@ type Manager struct {
mediaServer *server.MediaServer
statuses map[string]*connection.Status
statusNotifier *connection.StatusNotifier
feed *event.Feed
statuses map[string]*connection.Status
statusNotifier *connection.StatusNotifier
feed *event.Feed
circuitBreakers sync.Map
}
func NewManager(
db *sql.DB,
rpcClient *rpc.Client,
communityManager *community.Manager,
contractOwnershipProviders []thirdparty.CollectibleContractOwnershipProvider,
accountOwnershipProviders []thirdparty.CollectibleAccountOwnershipProvider,
collectibleDataProviders []thirdparty.CollectibleDataProvider,
collectionDataProviders []thirdparty.CollectionDataProvider,
providers thirdparty.CollectibleProviders,
mediaServer *server.MediaServer,
feed *event.Feed) *Manager {
hystrix.ConfigureCommand(hystrixContractOwnershipClientName, hystrix.CommandConfig{
Timeout: 10000,
MaxConcurrentRequests: 100,
SleepWindow: 300000,
ErrorPercentThreshold: 25,
})
ownershipDB := NewOwnershipDB(db)
@@ -112,32 +99,9 @@ func NewManager(
feed,
)
// Get list of all providers
collectibleProvidersMap := make(map[string]thirdparty.CollectibleProvider)
collectibleProviders := make([]thirdparty.CollectibleProvider, 0)
for _, provider := range contractOwnershipProviders {
collectibleProvidersMap[provider.ID()] = provider
}
for _, provider := range accountOwnershipProviders {
collectibleProvidersMap[provider.ID()] = provider
}
for _, provider := range collectibleDataProviders {
collectibleProvidersMap[provider.ID()] = provider
}
for _, provider := range collectionDataProviders {
collectibleProvidersMap[provider.ID()] = provider
}
for _, provider := range collectibleProvidersMap {
collectibleProviders = append(collectibleProviders, provider)
}
return &Manager{
rpcClient: rpcClient,
contractOwnershipProviders: contractOwnershipProviders,
accountOwnershipProviders: accountOwnershipProviders,
collectibleDataProviders: collectibleDataProviders,
collectionDataProviders: collectionDataProviders,
collectibleProviders: collectibleProviders,
rpcClient: rpcClient,
providers: providers,
httpClient: &http.Client{
Timeout: requestTimeout,
},
@@ -160,35 +124,6 @@ func mapToList[K comparable, T any](m map[K]T) []T {
return list
}
func makeContractOwnershipCall(main func() (any, error), fallback func() (any, error)) (any, error) {
resultChan := make(chan any, 1)
errChan := hystrix.Go(hystrixContractOwnershipClientName, func() error {
res, err := main()
if err != nil {
return err
}
resultChan <- res
return nil
}, func(err error) error {
if fallback == nil {
return err
}
res, err := fallback()
if err != nil {
return err
}
resultChan <- res
return nil
})
select {
case result := <-resultChan:
return result, nil
case err := <-errChan:
return nil, err
}
}
func (o *Manager) doContentTypeRequest(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
if err != nil {
@@ -253,67 +188,169 @@ func (o *Manager) FetchBalancesByOwnerAndContractAddress(ctx context.Context, ch
func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int, providerID string) (*thirdparty.FullCollectibleDataContainer, error) {
defer o.checkConnectionStatus(chainID)
anyProviderAvailable := false
for _, provider := range o.accountOwnershipProviders {
cmd := circuitbreaker.Command{}
for _, provider := range o.providers.AccountOwnershipProviders {
if !provider.IsChainSupported(chainID) {
continue
}
anyProviderAvailable = true
if providerID != thirdparty.FetchFromAnyProvider && providerID != provider.ID() {
continue
}
assetContainer, err := provider.FetchAllAssetsByOwnerAndContractAddress(ctx, chainID, owner, contractAddresses, cursor, limit)
if err != nil {
log.Error("FetchAllAssetsByOwnerAndContractAddress failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
continue
}
_, err = o.processFullCollectibleData(ctx, assetContainer.Items, true)
if err != nil {
return nil, err
}
return assetContainer, nil
provider := provider
f := circuitbreaker.NewFunctor(
func() ([]interface{}, error) {
assetContainer, err := provider.FetchAllAssetsByOwnerAndContractAddress(ctx, chainID, owner, contractAddresses, cursor, limit)
if err != nil {
log.Error("FetchAllAssetsByOwnerAndContractAddress failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
}
return []interface{}{assetContainer}, err
},
)
cmd.Add(f)
}
if anyProviderAvailable {
return nil, ErrAllProvidersFailedForChainID
if cmd.IsEmpty() {
return nil, ErrNoProvidersAvailableForChainID
}
return nil, ErrNoProvidersAvailableForChainID
cmdRes := o.getCircuitBreaker(chainID).Execute(cmd)
if cmdRes.Error() != nil {
log.Error("FetchAllAssetsByOwnerAndContractAddress failed for", "chainID", chainID, "err", cmdRes.Error())
return nil, cmdRes.Error()
}
assetContainer := cmdRes.Result()[0].(*thirdparty.FullCollectibleDataContainer)
_, err := o.processFullCollectibleData(ctx, assetContainer.Items, true)
if err != nil {
return nil, err
}
return assetContainer, nil
}
func (o *Manager) FetchAllAssetsByOwner(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, cursor string, limit int, providerID string) (*thirdparty.FullCollectibleDataContainer, error) {
defer o.checkConnectionStatus(chainID)
anyProviderAvailable := false
for _, provider := range o.accountOwnershipProviders {
cmd := circuitbreaker.Command{}
for _, provider := range o.providers.AccountOwnershipProviders {
if !provider.IsChainSupported(chainID) {
continue
}
anyProviderAvailable = true
if providerID != thirdparty.FetchFromAnyProvider && providerID != provider.ID() {
continue
}
assetContainer, err := provider.FetchAllAssetsByOwner(ctx, chainID, owner, cursor, limit)
if err != nil {
log.Error("FetchAllAssetsByOwner failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
continue
}
_, err = o.processFullCollectibleData(ctx, assetContainer.Items, true)
if err != nil {
return nil, err
}
return assetContainer, nil
provider := provider
f := circuitbreaker.NewFunctor(
func() ([]interface{}, error) {
assetContainer, err := provider.FetchAllAssetsByOwner(ctx, chainID, owner, cursor, limit)
if err != nil {
log.Error("FetchAllAssetsByOwner failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
}
return []interface{}{assetContainer}, err
},
)
cmd.Add(f)
}
if anyProviderAvailable {
return nil, ErrAllProvidersFailedForChainID
if cmd.IsEmpty() {
return nil, ErrNoProvidersAvailableForChainID
}
cmdRes := o.getCircuitBreaker(chainID).Execute(cmd)
if cmdRes.Error() != nil {
log.Error("FetchAllAssetsByOwner failed for", "chainID", chainID, "err", cmdRes.Error())
return nil, cmdRes.Error()
}
assetContainer := cmdRes.Result()[0].(*thirdparty.FullCollectibleDataContainer)
_, err := o.processFullCollectibleData(ctx, assetContainer.Items, true)
if err != nil {
return nil, err
}
return assetContainer, nil
}
func (o *Manager) FetchERC1155Balances(ctx context.Context, owner common.Address, chainID walletCommon.ChainID, contractAddress common.Address, tokenIDs []*bigint.BigInt) ([]*bigint.BigInt, error) {
if len(tokenIDs) == 0 {
return nil, nil
}
backend, err := o.rpcClient.EthClient(uint64(chainID))
if err != nil {
return nil, err
}
caller, err := ierc1155.NewIerc1155Caller(contractAddress, backend)
if err != nil {
return nil, err
}
owners := make([]common.Address, len(tokenIDs))
ids := make([]*big.Int, len(tokenIDs))
for i, tokenID := range tokenIDs {
owners[i] = owner
ids[i] = tokenID.Int
}
balances, err := caller.BalanceOfBatch(&bind.CallOpts{
Context: ctx,
}, owners, ids)
if err != nil {
return nil, err
}
bigIntBalances := make([]*bigint.BigInt, len(balances))
for i, balance := range balances {
bigIntBalances[i] = &bigint.BigInt{Int: balance}
}
return bigIntBalances, err
}
func (o *Manager) fillMissingBalances(ctx context.Context, owner common.Address, collectibles []*thirdparty.FullCollectibleData) {
collectiblesByChainIDAndContractAddress := thirdparty.GroupCollectiblesByChainIDAndContractAddress(collectibles)
for chainID, collectiblesByContract := range collectiblesByChainIDAndContractAddress {
for contractAddress, contractCollectibles := range collectiblesByContract {
collectiblesToFetchPerTokenID := make(map[string]*thirdparty.FullCollectibleData)
for _, collectible := range contractCollectibles {
if collectible.AccountBalance == nil {
switch getContractType(*collectible) {
case walletCommon.ContractTypeERC1155:
collectiblesToFetchPerTokenID[collectible.CollectibleData.ID.TokenID.String()] = collectible
default:
// Any other type of collectible is non-fungible, balance is 1
collectible.AccountBalance = &bigint.BigInt{Int: big.NewInt(1)}
}
}
}
if len(collectiblesToFetchPerTokenID) == 0 {
continue
}
tokenIDs := make([]*bigint.BigInt, 0, len(collectiblesToFetchPerTokenID))
for _, c := range collectiblesToFetchPerTokenID {
tokenIDs = append(tokenIDs, c.CollectibleData.ID.TokenID)
}
balances, err := o.FetchERC1155Balances(ctx, owner, chainID, contractAddress, tokenIDs)
if err != nil {
log.Error("FetchERC1155Balances failed", "chainID", chainID, "contractAddress", contractAddress, "err", err)
continue
}
for i := range balances {
collectible := collectiblesToFetchPerTokenID[tokenIDs[i].String()]
collectible.AccountBalance = balances[i]
}
}
}
return nil, ErrNoProvidersAvailableForChainID
}
func (o *Manager) FetchCollectibleOwnershipByOwner(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, cursor string, limit int, providerID string) (*thirdparty.CollectibleOwnershipContainer, error) {
@@ -324,7 +361,15 @@ func (o *Manager) FetchCollectibleOwnershipByOwner(ctx context.Context, chainID
return nil, err
}
// Some providers do not give us the balances for ERC1155 tokens, so we need to fetch them separately.
collectibles := make([]*thirdparty.FullCollectibleData, 0, len(assetContainer.Items))
for i := range assetContainer.Items {
collectibles = append(collectibles, &assetContainer.Items[i])
}
o.fillMissingBalances(ctx, owner, collectibles)
ret := assetContainer.ToOwnershipContainer()
return &ret, nil
}
@@ -332,50 +377,81 @@ func (o *Manager) FetchCollectibleOwnershipByOwner(ctx context.Context, chainID
// If asyncFetch is true, empty metadata will be returned for any missing collectibles and an EventCollectiblesDataUpdated will be sent when the data is ready.
// If asyncFetch is false, it will wait for all collectibles' metadata to be retrieved before returning.
func (o *Manager) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID, asyncFetch bool) ([]thirdparty.FullCollectibleData, error) {
missingIDs, err := o.collectiblesDataDB.GetIDsNotInDB(uniqueIDs)
err := o.FetchMissingAssetsByCollectibleUniqueID(ctx, uniqueIDs, asyncFetch)
if err != nil {
return nil, err
}
missingIDsPerChainID := thirdparty.GroupCollectibleUIDsByChainID(missingIDs)
return o.getCacheFullCollectibleData(uniqueIDs)
}
group := async.NewGroup(ctx)
group.Add(func(ctx context.Context) error {
for chainID, idsToFetch := range missingIDsPerChainID {
defer o.checkConnectionStatus(chainID)
for _, provider := range o.collectibleDataProviders {
if !provider.IsChainSupported(chainID) {
continue
}
fetchedAssets, err := provider.FetchAssetsByCollectibleUniqueID(ctx, idsToFetch)
if err != nil {
log.Error("FetchAssetsByCollectibleUniqueID failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
continue
}
updatedCollectibles, err := o.processFullCollectibleData(ctx, fetchedAssets, asyncFetch)
if err != nil {
log.Error("processFullCollectibleData failed for", "provider", provider.ID(), "chainID", chainID, "len(fetchedAssets)", len(fetchedAssets), "err", err)
return err
}
if asyncFetch {
o.signalUpdatedCollectiblesData(updatedCollectibles)
}
break
}
}
return nil
})
if !asyncFetch {
group.Wait()
func (o *Manager) FetchMissingAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID, asyncFetch bool) error {
missingIDs, err := o.collectiblesDataDB.GetIDsNotInDB(uniqueIDs)
if err != nil {
return err
}
return o.getCacheFullCollectibleData(uniqueIDs)
missingIDsPerChainID := thirdparty.GroupCollectibleUIDsByChainID(missingIDs)
// Atomic group stores the error from the first failed command and stops other commands on error
group := async.NewAtomicGroup(ctx)
for chainID, idsToFetch := range missingIDsPerChainID {
group.Add(func(ctx context.Context) error {
defer o.checkConnectionStatus(chainID)
fetchedAssets, err := o.fetchMissingAssetsForChainByCollectibleUniqueID(ctx, chainID, idsToFetch)
if err != nil {
log.Error("FetchMissingAssetsByCollectibleUniqueID failed for", "chainID", chainID, "ids", idsToFetch, "err", err)
return err
}
updatedCollectibles, err := o.processFullCollectibleData(ctx, fetchedAssets, asyncFetch)
if err != nil {
log.Error("processFullCollectibleData failed for", "chainID", chainID, "len(fetchedAssets)", len(fetchedAssets), "err", err)
return err
}
o.signalUpdatedCollectiblesData(updatedCollectibles)
return nil
})
}
if asyncFetch {
group.Wait()
return group.Error()
}
return nil
}
func (o *Manager) fetchMissingAssetsForChainByCollectibleUniqueID(ctx context.Context, chainID walletCommon.ChainID, idsToFetch []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
cmd := circuitbreaker.Command{}
for _, provider := range o.providers.CollectibleDataProviders {
if !provider.IsChainSupported(chainID) {
continue
}
provider := provider
cmd.Add(circuitbreaker.NewFunctor(func() ([]any, error) {
fetchedAssets, err := provider.FetchAssetsByCollectibleUniqueID(ctx, idsToFetch)
if err != nil {
log.Error("fetchMissingAssetsForChainByCollectibleUniqueID failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
}
return []any{fetchedAssets}, err
}))
}
if cmd.IsEmpty() {
return nil, ErrNoProvidersAvailableForChainID // lets not stop the group if no providers are available for the chain
}
cmdRes := o.getCircuitBreaker(chainID).Execute(cmd)
if cmdRes.Error() != nil {
log.Error("fetchMissingAssetsForChainByCollectibleUniqueID failed for", "chainID", chainID, "err", cmdRes.Error())
return nil, cmdRes.Error()
}
return cmdRes.Result()[0].([]thirdparty.FullCollectibleData), cmdRes.Error()
}
func (o *Manager) FetchCollectionsDataByContractID(ctx context.Context, ids []thirdparty.ContractID) ([]thirdparty.CollectionData, error) {
@@ -386,27 +462,49 @@ func (o *Manager) FetchCollectionsDataByContractID(ctx context.Context, ids []th
missingIDsPerChainID := thirdparty.GroupContractIDsByChainID(missingIDs)
// Atomic group stores the error from the first failed command and stops other commands on error
group := async.NewAtomicGroup(ctx)
for chainID, idsToFetch := range missingIDsPerChainID {
defer o.checkConnectionStatus(chainID)
group.Add(func(ctx context.Context) error {
defer o.checkConnectionStatus(chainID)
for _, provider := range o.collectionDataProviders {
if !provider.IsChainSupported(chainID) {
continue
cmd := circuitbreaker.Command{}
for _, provider := range o.providers.CollectionDataProviders {
if !provider.IsChainSupported(chainID) {
continue
}
provider := provider
cmd.Add(circuitbreaker.NewFunctor(func() ([]any, error) {
fetchedCollections, err := provider.FetchCollectionsDataByContractID(ctx, idsToFetch)
return []any{fetchedCollections}, err
}))
}
fetchedCollections, err := provider.FetchCollectionsDataByContractID(ctx, idsToFetch)
if err != nil {
log.Error("FetchCollectionsDataByContractID failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
continue
if cmd.IsEmpty() {
return nil
}
cmdRes := o.getCircuitBreaker(chainID).Execute(cmd)
if cmdRes.Error() != nil {
log.Error("FetchCollectionsDataByContractID failed for", "chainID", chainID, "err", cmdRes.Error())
return cmdRes.Error()
}
fetchedCollections := cmdRes.Result()[0].([]thirdparty.CollectionData)
err = o.processCollectionData(ctx, fetchedCollections)
if err != nil {
return nil, err
return err
}
break
}
return err
})
}
group.Wait()
if group.Error() != nil {
return nil, group.Error()
}
data, err := o.collectionsDataDB.GetData(ids)
@@ -417,55 +515,39 @@ func (o *Manager) FetchCollectionsDataByContractID(ctx context.Context, ids []th
return mapToList(data), nil
}
func (o *Manager) getContractOwnershipProviders(chainID walletCommon.ChainID) (mainProvider thirdparty.CollectibleContractOwnershipProvider, fallbackProvider thirdparty.CollectibleContractOwnershipProvider) {
mainProvider = nil
fallbackProvider = nil
for _, provider := range o.contractOwnershipProviders {
if provider.IsChainSupported(chainID) {
if mainProvider == nil {
// First provider found
mainProvider = provider
continue
}
// Second provider found
fallbackProvider = provider
break
}
}
return
}
func getCollectibleOwnersByContractAddressFunc(ctx context.Context, chainID walletCommon.ChainID, contractAddress common.Address, provider thirdparty.CollectibleContractOwnershipProvider) func() (any, error) {
if provider == nil {
return nil
}
return func() (any, error) {
res, err := provider.FetchCollectibleOwnersByContractAddress(ctx, chainID, contractAddress)
if err != nil {
log.Error("FetchCollectibleOwnersByContractAddress failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
}
return res, err
}
func (o *Manager) GetCollectibleOwnership(id thirdparty.CollectibleUniqueID) ([]thirdparty.AccountBalance, error) {
return o.ownershipDB.GetOwnership(id)
}
func (o *Manager) FetchCollectibleOwnersByContractAddress(ctx context.Context, chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
defer o.checkConnectionStatus(chainID)
mainProvider, fallbackProvider := o.getContractOwnershipProviders(chainID)
if mainProvider == nil {
cmd := circuitbreaker.Command{}
for _, provider := range o.providers.ContractOwnershipProviders {
if !provider.IsChainSupported(chainID) {
continue
}
provider := provider
cmd.Add(circuitbreaker.NewFunctor(func() ([]any, error) {
res, err := provider.FetchCollectibleOwnersByContractAddress(ctx, chainID, contractAddress)
if err != nil {
log.Error("FetchCollectibleOwnersByContractAddress failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
}
return []any{res}, err
}))
}
if cmd.IsEmpty() {
return nil, ErrNoProvidersAvailableForChainID
}
mainFn := getCollectibleOwnersByContractAddressFunc(ctx, chainID, contractAddress, mainProvider)
fallbackFn := getCollectibleOwnersByContractAddressFunc(ctx, chainID, contractAddress, fallbackProvider)
owners, err := makeContractOwnershipCall(mainFn, fallbackFn)
if err != nil {
return nil, err
cmdRes := o.getCircuitBreaker(chainID).Execute(cmd)
if cmdRes.Error() != nil {
log.Error("FetchCollectibleOwnersByContractAddress failed for", "chainID", chainID, "err", cmdRes.Error())
return nil, cmdRes.Error()
}
return owners.(*thirdparty.CollectibleContractOwnership), nil
return cmdRes.Result()[0].(*thirdparty.CollectibleContractOwnership), cmdRes.Error()
}
func (o *Manager) fetchTokenURI(ctx context.Context, id thirdparty.CollectibleUniqueID) (string, error) {
@@ -646,25 +728,16 @@ func (o *Manager) fillCommunityID(asset *thirdparty.FullCollectibleData) error {
}
func (o *Manager) fetchCommunityAssets(communityID string, communityAssets []*thirdparty.FullCollectibleData) error {
communityInfo, err := o.communityManager.FetchCommunityInfo(communityID)
communityFound, err := o.communityManager.FillCollectiblesMetadata(communityID, communityAssets)
if err != nil {
log.Error("FillCollectiblesMetadata failed", "communityID", communityID, "err", err)
} else if !communityFound {
log.Warn("fetchCommunityAssets community not found", "communityID", communityID)
}
// If the community is found, we update the DB.
// If the community is not found, we only insert new entries to the DB (don't replace what is already there).
allowUpdate := false
if err != nil {
log.Error("fetchCommunityInfo failed", "communityID", communityID, "err", err)
} else if communityInfo == nil {
log.Warn("fetchCommunityAssets community not found", "communityID", communityID)
} else {
for _, communityAsset := range communityAssets {
err := o.communityManager.FillCollectibleMetadata(communityAsset)
if err != nil {
log.Error("FillCollectibleMetadata failed", "communityID", communityID, "err", err)
return err
}
}
allowUpdate = true
}
allowUpdate := communityFound
collectiblesData := make([]thirdparty.CollectibleData, 0, len(communityAssets))
collectionsData := make([]thirdparty.CollectionData, 0, len(communityAssets))
@@ -827,7 +900,7 @@ func (o *Manager) ResetConnectionStatus() {
}
func (o *Manager) checkConnectionStatus(chainID walletCommon.ChainID) {
for _, provider := range o.collectibleProviders {
for _, provider := range o.providers.GetProviderList() {
if provider.IsChainSupported(chainID) && provider.IsConnected() {
o.statuses[chainID.String()].SetIsConnected(true)
return
@@ -868,3 +941,89 @@ func (o *Manager) signalUpdatedCollectiblesData(ids []thirdparty.CollectibleUniq
o.feed.Send(event)
}
}
func (o *Manager) getCircuitBreaker(chainID walletCommon.ChainID) *circuitbreaker.CircuitBreaker {
cb, ok := o.circuitBreakers.Load(chainID.String())
if !ok {
cb = circuitbreaker.NewCircuitBreaker(circuitbreaker.Config{
CommandName: chainID.String(),
Timeout: 10000,
MaxConcurrentRequests: 100,
RequestVolumeThreshold: 25,
SleepWindow: 300000,
ErrorPercentThreshold: 25,
})
o.circuitBreakers.Store(chainID.String(), cb)
}
return cb.(*circuitbreaker.CircuitBreaker)
}
func (o *Manager) SearchCollectibles(ctx context.Context, chainID walletCommon.ChainID, text string, cursor string, limit int, providerID string) (*thirdparty.FullCollectibleDataContainer, error) {
defer o.checkConnectionStatus(chainID)
anyProviderAvailable := false
for _, provider := range o.providers.SearchProviders {
if !provider.IsChainSupported(chainID) {
continue
}
anyProviderAvailable = true
if providerID != thirdparty.FetchFromAnyProvider && providerID != provider.ID() {
continue
}
// TODO (#13951): Be smarter about how we handle the user-entered string
collections := []common.Address{}
container, err := provider.SearchCollectibles(ctx, chainID, collections, text, cursor, limit)
if err != nil {
log.Error("FetchAllAssetsByOwner failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
continue
}
_, err = o.processFullCollectibleData(ctx, container.Items, true)
if err != nil {
return nil, err
}
return container, nil
}
if anyProviderAvailable {
return nil, ErrAllProvidersFailedForChainID
}
return nil, ErrNoProvidersAvailableForChainID
}
func (o *Manager) SearchCollections(ctx context.Context, chainID walletCommon.ChainID, query string, cursor string, limit int, providerID string) (*thirdparty.CollectionDataContainer, error) {
defer o.checkConnectionStatus(chainID)
anyProviderAvailable := false
for _, provider := range o.providers.SearchProviders {
if !provider.IsChainSupported(chainID) {
continue
}
anyProviderAvailable = true
if providerID != thirdparty.FetchFromAnyProvider && providerID != provider.ID() {
continue
}
// TODO (#13951): Be smarter about how we handle the user-entered string
container, err := provider.SearchCollections(ctx, chainID, query, cursor, limit)
if err != nil {
log.Error("FetchAllAssetsByOwner failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
continue
}
err = o.processCollectionData(ctx, container.Items)
if err != nil {
return nil, err
}
return container, nil
}
if anyProviderAvailable {
return nil, ErrAllProvidersFailedForChainID
}
return nil, ErrNoProvidersAvailableForChainID
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"math"
"math/big"
"sync"
"github.com/ethereum/go-ethereum/common"
@@ -20,6 +21,7 @@ const InvalidTimestamp = int64(-1)
type OwnershipDB struct {
db *sql.DB
mu sync.Mutex
}
func NewOwnershipDB(sqlDb *sql.DB) *OwnershipDB {
@@ -300,7 +302,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,7 +334,36 @@ 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) {
// Ensure all steps are done atomically
o.mu.Lock()
defer o.mu.Unlock()
err = insertTmpOwnership(o.db, chainID, ownerAddress, balances)
if err != nil {
return
@@ -367,7 +397,7 @@ func (o *OwnershipDB) Update(chainID w_common.ChainID, ownerAddress common.Addre
}
func (o *OwnershipDB) GetOwnedCollectibles(chainIDs []w_common.ChainID, ownerAddresses []common.Address, offset int, limit int) ([]thirdparty.CollectibleUniqueID, error) {
query, args, err := sqlx.In(fmt.Sprintf(`SELECT %s
query, args, err := sqlx.In(fmt.Sprintf(`SELECT DISTINCT %s
FROM collectibles_ownership_cache
WHERE chain_id IN (?) AND owner_address IN (?)
LIMIT ? OFFSET ?`, selectOwnershipColumns), chainIDs, ownerAddresses, limit, offset)

View File

@@ -61,6 +61,11 @@ const (
FetchTypeFetchIfCacheOld
)
type TxHashData struct {
Hash common.Hash
TxID common.Hash
}
type FetchCriteria struct {
FetchType FetchType `json:"fetch_type"`
MaxCacheAgeSeconds int64 `json:"max_cache_age_seconds"`
@@ -415,8 +420,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 +442,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]TxHashData {
// 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 +450,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]TxHashData)
for _, id := range ownedCollectibles.ids {
transfer, err := s.transferDB.GetLatestCollectibleTransfer(ownedCollectibles.account, id)
if err != nil {
@@ -452,17 +460,27 @@ func (s *Service) lookupTransferForCollectibles(ownedCollectibles OwnedCollectib
continue
}
if transfer != nil {
result[id] = TxHashData{
Hash: transfer.Transaction.Hash(),
TxID: transfer.ID,
}
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]TxHashData) {
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 +493,47 @@ 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 _, collectible := range communityCollectibles {
txHash := ""
for key, value := range hashMap {
if key.Same(&collectible.ID) {
collectible.LatestTxHash = value.TxID.Hex()
txHash = value.Hash.Hex()
break
}
}
for id, value := range firstCollectibles {
if value && id.Same(&collectible.ID) {
collectible.IsFirst = true
break
}
}
group := CollectibleGroup{
contractID: collectible.ID.ContractID,
txHash: txHash,
}
_, 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
}

View File

@@ -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)
@@ -196,3 +201,11 @@ func communityInfoToData(communityID string, community *thirdparty.CommunityInfo
return ret
}
func IDsFromAssets(assets []*thirdparty.FullCollectibleData) []thirdparty.CollectibleUniqueID {
result := make([]thirdparty.CollectibleUniqueID, len(assets))
for i, asset := range assets {
result[i] = asset.CollectibleData.ID
}
return result
}

View File

@@ -5,6 +5,12 @@ import (
"time"
)
type MultiTransactionIDType int64
const (
NoMultiTransactionID = MultiTransactionIDType(0)
)
type ChainID uint64
const (

View File

@@ -58,16 +58,33 @@ func (cm *Manager) GetCommunityID(tokenURI string) string {
return cm.communityInfoProvider.GetCommunityID(tokenURI)
}
func (cm *Manager) FillCollectibleMetadata(c *thirdparty.FullCollectibleData) error {
return cm.communityInfoProvider.FillCollectibleMetadata(c)
func (cm *Manager) FillCollectiblesMetadata(communityID string, cs []*thirdparty.FullCollectibleData) (bool, error) {
communityFound, err := cm.communityInfoProvider.FillCollectiblesMetadata(communityID, cs)
if err != nil {
return communityFound, err
}
if communityFound {
// Update local community data cache
community, err := cm.communityInfoProvider.GetCommunityInfoFromDB(communityID)
if err != nil {
log.Error("GetCommunityInfoFromDB failed", "communityID", communityID, "err", err)
return communityFound, err
}
err = cm.setCommunityInfo(communityID, community)
if err != nil {
log.Error("SetCommunityInfo failed", "communityID", community)
}
}
return communityFound, nil
}
func (cm *Manager) setCommunityInfo(id string, c *thirdparty.CommunityInfo) (err error) {
return cm.db.SetCommunityInfo(id, c)
}
func (cm *Manager) FetchCommunityInfo(communityID string) (*thirdparty.CommunityInfo, error) {
communityInfo, err := cm.communityInfoProvider.FetchCommunityInfo(communityID)
func (cm *Manager) fetchCommunityInfo(communityID string, fetcher func() (*thirdparty.CommunityInfo, error)) (*thirdparty.CommunityInfo, error) {
communityInfo, err := fetcher()
if err != nil {
dbErr := cm.setCommunityInfo(communityID, nil)
if dbErr != nil {
@@ -79,6 +96,12 @@ func (cm *Manager) FetchCommunityInfo(communityID string) (*thirdparty.Community
return communityInfo, err
}
func (cm *Manager) FetchCommunityInfo(communityID string) (*thirdparty.CommunityInfo, error) {
return cm.fetchCommunityInfo(communityID, func() (*thirdparty.CommunityInfo, error) {
return cm.communityInfoProvider.FetchCommunityInfo(communityID)
})
}
func (cm *Manager) FetchCommunityMetadataAsync(communityID string) {
go func() {
communityInfo, err := cm.FetchCommunityMetadata(communityID)

View File

@@ -7,8 +7,11 @@ import (
"sort"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/consensus/misc"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
gaspriceoracle "github.com/status-im/status-go/contracts/gas-price-oracle"
"github.com/status-im/status-go/rpc"
)
@@ -27,6 +30,7 @@ type SuggestedFees struct {
MaxFeePerGasLow *big.Float `json:"maxFeePerGasLow"`
MaxFeePerGasMedium *big.Float `json:"maxFeePerGasMedium"`
MaxFeePerGasHigh *big.Float `json:"maxFeePerGasHigh"`
L1GasFee *big.Float `json:"l1GasFee"`
EIP1559Enabled bool `json:"eip1559Enabled"`
}
@@ -251,3 +255,35 @@ func (f *FeeManager) getFeeHistorySorted(chainID uint64) ([]*big.Int, error) {
sort.Slice(fees, func(i, j int) bool { return fees[i].Cmp(fees[j]) < 0 })
return fees, nil
}
func (f *FeeManager) getL1Fee(ctx context.Context, chainID uint64, tx *ethTypes.Transaction) (uint64, error) {
ethClient, err := f.RPCClient.EthClient(chainID)
if err != nil {
return 0, err
}
contractAddress, err := gaspriceoracle.ContractAddress(chainID)
if err != nil {
return 0, err
}
contract, err := gaspriceoracle.NewGaspriceoracleCaller(contractAddress, ethClient)
if err != nil {
return 0, err
}
callOpt := &bind.CallOpts{}
data, err := tx.MarshalBinary()
if err != nil {
return 0, err
}
result, err := contract.GetL1Fee(callOpt, data)
if err != nil {
return 0, err
}
return result.Uint64(), nil
}

View File

@@ -120,7 +120,7 @@ func (c *CryptoOnRampManager) getFromStaticDataSource() ([]byte, error) {
"description": "Global crypto to fiat flow",
"fees": "0.49%% - 2.9%%",
"logoUrl": "%s",
"siteUrl": "https://buy.ramp.network/?hostApiKey=zrtf9u2uqebeyzcs37fu5857tktr3eg9w5tffove&swapAsset=DAI,ETH,USDC,USDT",
"siteUrl": "https://ramp.network/buy?hostApiKey=zrtf9u2uqebeyzcs37fu5857tktr3eg9w5tffove&swapAsset=DAI,ETH,USDC,USDT",
"hostname": "ramp.network"
},
{

View File

@@ -33,7 +33,7 @@ func (p *Persistence) SaveTokens(tokens map[common.Address][]Token) (err error)
for address, addressTokens := range tokens {
for _, t := range addressTokens {
for chainID, b := range t.BalancesPerChain {
if b.HasError || b.Balance.Cmp(big.NewFloat(0)) == 0 {
if b.HasError {
continue
}
_, err = tx.Exec(`INSERT INTO token_balances(user_address,token_name,token_symbol,token_address,token_decimals,token_description,token_url,balance,raw_balance,chain_id) VALUES (?,?,?,?,?,?,?,?,?,?)`, address.Hex(), t.Name, t.Symbol, b.Address.Hex(), t.Decimals, t.Description, t.AssetWebsiteURL, b.Balance.String(), b.RawBalance, chainID)

View File

@@ -5,7 +5,6 @@ import (
"math"
"math/big"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
@@ -19,9 +18,8 @@ import (
"github.com/status-im/status-go/services/wallet/community"
"github.com/status-im/status-go/services/wallet/market"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/services/wallet/transfer"
"github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/services/wallet/transfer"
"github.com/status-im/status-go/services/wallet/walletevent"
)
@@ -51,14 +49,14 @@ func belongsToMandatoryTokens(symbol string) bool {
func NewReader(rpcClient *rpc.Client, tokenManager *token.Manager, marketManager *market.Manager, communityManager *community.Manager, accountsDB *accounts.Database, persistence *Persistence, walletFeed *event.Feed) *Reader {
return &Reader{
rpcClient: rpcClient,
tokenManager: tokenManager,
marketManager: marketManager,
communityManager: communityManager,
accountsDB: accountsDB,
persistence: persistence,
walletFeed: walletFeed,
lastWalletTokenUpdateTimestamp: atomic.Int64{},
rpcClient: rpcClient,
tokenManager: tokenManager,
marketManager: marketManager,
communityManager: communityManager,
accountsDB: accountsDB,
persistence: persistence,
walletFeed: walletFeed,
refreshBalanceCache: true,
}
}
@@ -72,7 +70,7 @@ type Reader struct {
walletFeed *event.Feed
cancel context.CancelFunc
walletEventsWatcher *walletevent.Watcher
lastWalletTokenUpdateTimestamp atomic.Int64
lastWalletTokenUpdateTimestamp sync.Map
reloadDelayTimer *time.Timer
refreshBalanceCache bool
rw sync.RWMutex
@@ -91,11 +89,12 @@ type TokenMarketValues struct {
}
type ChainBalance struct {
RawBalance string `json:"rawBalance"`
Balance *big.Float `json:"balance"`
Address common.Address `json:"address"`
ChainID uint64 `json:"chainId"`
HasError bool `json:"hasError"`
RawBalance string `json:"rawBalance"`
Balance *big.Float `json:"balance"`
Balance1DayAgo string `json:"balance1DayAgo"`
Address common.Address `json:"address"`
ChainID uint64 `json:"chainId"`
HasError bool `json:"hasError"`
}
type Token struct {
@@ -184,7 +183,7 @@ func (r *Reader) Stop() {
r.cancelDelayedWalletReload()
r.lastWalletTokenUpdateTimestamp.Store(0)
r.lastWalletTokenUpdateTimestamp = sync.Map{}
}
func (r *Reader) triggerWalletReload() {
@@ -221,13 +220,18 @@ func (r *Reader) startWalletEventsWatcher() {
return
}
timecheck := r.lastWalletTokenUpdateTimestamp.Load() - activityReloadMarginSeconds
if event.At > timecheck {
r.triggerDelayedWalletReload()
}
for _, address := range event.Accounts {
timestamp, ok := r.lastWalletTokenUpdateTimestamp.Load(address)
timecheck := int64(0)
if ok {
timecheck = timestamp.(int64) - activityReloadMarginSeconds
}
if transfer.IsTransferDetectionEvent(event.Type) {
r.invalidateBalanceCache()
if !ok || event.At > timecheck {
r.triggerDelayedWalletReload()
r.invalidateBalanceCache()
break
}
}
}
@@ -243,11 +247,42 @@ func (r *Reader) stopWalletEventsWatcher() {
}
}
func (r *Reader) isBalanceCacheValid() bool {
func (r *Reader) tokensCachedForAddresses(addresses []common.Address) bool {
for _, address := range addresses {
cachedTokens, err := r.GetCachedWalletTokensWithoutMarketData()
if err != nil {
return false
}
_, ok := cachedTokens[address]
if !ok {
return false
}
}
return true
}
func (r *Reader) isCacheTimestampValidForAddress(address common.Address) bool {
_, ok := r.lastWalletTokenUpdateTimestamp.Load(address)
return ok
}
func (r *Reader) areCacheTimestampsValid(addresses []common.Address) bool {
for _, address := range addresses {
if !r.isCacheTimestampValidForAddress(address) {
return false
}
}
return true
}
func (r *Reader) isBalanceCacheValid(addresses []common.Address) bool {
r.rw.RLock()
defer r.rw.RUnlock()
return !r.refreshBalanceCache
return !r.refreshBalanceCache && r.tokensCachedForAddresses(addresses) && r.areCacheTimestampsValid(addresses)
}
func (r *Reader) balanceRefreshed() {
@@ -265,7 +300,7 @@ func (r *Reader) invalidateBalanceCache() {
}
func (r *Reader) FetchOrGetCachedWalletBalances(ctx context.Context, addresses []common.Address) (map[common.Address][]Token, error) {
if !r.isBalanceCacheValid() {
if !r.isBalanceCacheValid(addresses) {
balances, err := r.GetWalletTokenBalances(ctx, addresses)
if err != nil {
return nil, err
@@ -276,20 +311,6 @@ func (r *Reader) FetchOrGetCachedWalletBalances(ctx context.Context, addresses [
}
tokens, err := r.getWalletTokenBalances(ctx, addresses, false)
addressWithoutCachedBalances := false
for _, address := range addresses {
if _, ok := tokens[address]; !ok {
addressWithoutCachedBalances = true
break
}
}
// there should be at least ETH balance
if addressWithoutCachedBalances {
return r.GetWalletTokenBalances(ctx, addresses)
}
return tokens, err
}
@@ -339,37 +360,49 @@ func (r *Reader) getWalletTokenBalances(ctx context.Context, addresses []common.
verifiedTokens, unverifiedTokens := splitVerifiedTokens(allTokens)
cachedBalancesPerChain := map[common.Address]map[common.Address]map[uint64]string{}
updateAnyway := false
cachedBalancesPerChain := map[common.Address]map[common.Address]map[uint64]ChainBalance{}
if !updateBalances {
for address, tokens := range cachedTokens {
if _, ok := cachedBalancesPerChain[address]; !ok {
cachedBalancesPerChain[address] = map[common.Address]map[uint64]ChainBalance{}
cacheCheck:
for _, address := range addresses {
if res, ok := cachedTokens[address]; !ok || len(res) == 0 {
updateAnyway = true
break
}
networkFound := map[uint64]bool{}
for _, token := range cachedTokens[address] {
for _, chain := range chainIDs {
if _, ok := token.BalancesPerChain[chain]; ok {
networkFound[chain] = true
}
}
}
for _, chain := range chainIDs {
if !networkFound[chain] {
updateAnyway = true
break cacheCheck
}
}
}
}
if !updateBalances && !updateAnyway {
for address, tokens := range cachedTokens {
for _, token := range tokens {
for _, balance := range token.BalancesPerChain {
if _, ok := cachedBalancesPerChain[address]; !ok {
cachedBalancesPerChain[address] = map[common.Address]map[uint64]string{}
}
if _, ok := cachedBalancesPerChain[address][balance.Address]; !ok {
cachedBalancesPerChain[address][balance.Address] = map[uint64]ChainBalance{}
cachedBalancesPerChain[address][balance.Address] = map[uint64]string{}
}
cachedBalancesPerChain[address][balance.Address][balance.ChainID] = balance
cachedBalancesPerChain[address][balance.Address][balance.ChainID] = balance.RawBalance
}
}
}
for _, address := range addresses {
for _, tokenList := range [][]*token.Token{verifiedTokens, unverifiedTokens} {
for _, tokens := range getTokenBySymbols(tokenList) {
for _, token := range tokens {
if _, ok := cachedBalancesPerChain[address][token.Address][token.ChainID]; !ok {
updateAnyway = true
break
}
}
}
}
}
}
var latestBalances map[uint64]map[common.Address]map[common.Address]*hexutil.Big
@@ -385,7 +418,7 @@ func (r *Reader) getWalletTokenBalances(ctx context.Context, addresses []common.
}
result := make(map[common.Address][]Token)
communities := make(map[string]bool)
dayAgoTimestamp := time.Now().Add(-24 * time.Hour).Unix()
for _, address := range addresses {
for _, tokenList := range [][]*token.Token{verifiedTokens, unverifiedTokens} {
@@ -395,19 +428,22 @@ func (r *Reader) getWalletTokenBalances(ctx context.Context, addresses []common.
isVisible := false
for _, token := range tokens {
var balance *big.Float
hexBalance := &hexutil.Big{}
hexBalance := &big.Int{}
if latestBalances != nil {
hexBalance = latestBalances[token.ChainID][address][token.Address]
balance = big.NewFloat(0.0)
if hexBalance != nil {
balance = new(big.Float).Quo(
new(big.Float).SetInt(hexBalance.ToInt()),
big.NewFloat(math.Pow(10, float64(decimals))),
)
}
hexBalance = latestBalances[token.ChainID][address][token.Address].ToInt()
} else {
balance = cachedBalancesPerChain[address][token.Address][token.ChainID].Balance
if cachedRawBalance, ok := cachedBalancesPerChain[address][token.Address][token.ChainID]; ok {
hexBalance, _ = new(big.Int).SetString(cachedRawBalance, 10)
}
}
balance = big.NewFloat(0.0)
if hexBalance != nil {
balance = new(big.Float).Quo(
new(big.Float).SetInt(hexBalance),
big.NewFloat(math.Pow(10, float64(decimals))),
)
}
hasError := false
if client, ok := clients[token.ChainID]; ok {
hasError = err != nil || !client.GetIsConnected()
@@ -416,11 +452,12 @@ func (r *Reader) getWalletTokenBalances(ctx context.Context, addresses []common.
isVisible = balance.Cmp(big.NewFloat(0.0)) > 0 || r.isCachedToken(cachedTokens, address, token.Symbol, token.ChainID)
}
balancesPerChain[token.ChainID] = ChainBalance{
RawBalance: hexBalance.ToInt().String(),
Balance: balance,
Address: token.Address,
ChainID: token.ChainID,
HasError: hasError,
RawBalance: hexBalance.String(),
Balance: balance,
Balance1DayAgo: "0",
Address: token.Address,
ChainID: token.ChainID,
HasError: hasError,
}
}
@@ -428,6 +465,17 @@ func (r *Reader) getWalletTokenBalances(ctx context.Context, addresses []common.
continue
}
for _, balance := range balancesPerChain {
balance1DayAgo, err := r.tokenManager.GetTokenHistoricalBalance(address, balance.ChainID, symbol, dayAgoTimestamp)
if err != nil {
return nil, err
}
if balance1DayAgo != nil {
balance.Balance1DayAgo = balance1DayAgo.String()
balancesPerChain[balance.ChainID] = balance
}
}
walletToken := Token{
Name: tokens[0].Name,
Symbol: symbol,
@@ -439,20 +487,12 @@ func (r *Reader) getWalletTokenBalances(ctx context.Context, addresses []common.
Image: tokens[0].Image,
}
if walletToken.CommunityData != nil {
communities[walletToken.CommunityData.ID] = true
}
result[address] = append(result[address], walletToken)
}
}
}
r.lastWalletTokenUpdateTimestamp.Store(time.Now().Unix())
for communityID := range communities {
r.communityManager.FetchCommunityMetadataAsync(communityID)
}
r.updateTokenUpdateTimestamp(addresses)
return result, r.persistence.SaveTokens(result)
}
@@ -618,8 +658,6 @@ func (r *Reader) GetWalletToken(ctx context.Context, addresses []common.Address)
return nil, err
}
communities := make(map[string]bool)
for address, tokens := range result {
for index, token := range tokens {
marketValuesPerCurrency := make(map[string]TokenMarketValues)
@@ -640,10 +678,6 @@ func (r *Reader) GetWalletToken(ctx context.Context, addresses []common.Address)
}
}
if token.CommunityData != nil {
communities[token.CommunityData.ID] = true
}
if _, ok := tokenDetails[token.Symbol]; !ok {
continue
}
@@ -655,11 +689,7 @@ func (r *Reader) GetWalletToken(ctx context.Context, addresses []common.Address)
}
}
r.lastWalletTokenUpdateTimestamp.Store(time.Now().Unix())
for communityID := range communities {
r.communityManager.FetchCommunityMetadataAsync(communityID)
}
r.updateTokenUpdateTimestamp(addresses)
return result, r.persistence.SaveTokens(result)
}
@@ -684,3 +714,9 @@ func (r *Reader) isCachedToken(cachedTokens map[common.Address][]Token, address
func (r *Reader) GetCachedWalletTokensWithoutMarketData() (map[common.Address][]Token, error) {
return r.persistence.GetTokens()
}
func (r *Reader) updateTokenUpdateTimestamp(addresses []common.Address) {
for _, address := range addresses {
r.lastWalletTokenUpdateTimestamp.Store(address, time.Now().Unix())
}
}

View File

@@ -16,7 +16,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/contracts"
"github.com/status-im/status-go/contracts/ierc1155"
gaspriceoracle "github.com/status-im/status-go/contracts/gas-price-oracle"
"github.com/status-im/status-go/contracts/ierc20"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/params"
@@ -136,7 +136,7 @@ func (s SendType) isAvailableFor(network *params.Network) bool {
return true
}
if network.ChainID == 1 || network.ChainID == 5 {
if network.ChainID == 1 || network.ChainID == 5 || network.ChainID == 11155111 {
return true
}
@@ -451,27 +451,28 @@ type Router struct {
rpcClient *rpc.Client
}
func (r *Router) requireApproval(ctx context.Context, sendType SendType, bridge bridge.Bridge, account common.Address, network *params.Network, token *token.Token, amountIn *big.Int) (bool, *big.Int, uint64, *common.Address, error) {
if sendType == ERC721Transfer {
return false, nil, 0, nil, nil
func (r *Router) requireApproval(ctx context.Context, sendType SendType, bridge bridge.Bridge, account common.Address, network *params.Network, token *token.Token, amountIn *big.Int) (
bool, *big.Int, uint64, uint64, *common.Address, error) {
if sendType.IsCollectiblesTransfer() {
return false, nil, 0, 0, nil, nil
}
if token.IsNative() {
return false, nil, 0, nil, nil
return false, nil, 0, 0, nil, nil
}
contractMaker, err := contracts.NewContractMaker(r.rpcClient)
if err != nil {
return false, nil, 0, nil, err
return false, nil, 0, 0, nil, err
}
bridgeAddress := bridge.GetContractAddress(network, token)
if bridgeAddress == nil {
return false, nil, 0, nil, nil
return false, nil, 0, 0, nil, nil
}
contract, err := contractMaker.NewERC20(network.ChainID, token.Address)
if err != nil {
return false, nil, 0, nil, err
return false, nil, 0, 0, nil, err
}
allowance, err := contract.Allowance(&bind.CallOpts{
@@ -479,26 +480,26 @@ func (r *Router) requireApproval(ctx context.Context, sendType SendType, bridge
}, account, *bridgeAddress)
if err != nil {
return false, nil, 0, nil, err
return false, nil, 0, 0, nil, err
}
if allowance.Cmp(amountIn) >= 0 {
return false, nil, 0, nil, nil
return false, nil, 0, 0, nil, nil
}
ethClient, err := r.rpcClient.EthClient(network.ChainID)
if err != nil {
return false, nil, 0, nil, err
return false, nil, 0, 0, nil, err
}
erc20ABI, err := abi.JSON(strings.NewReader(ierc20.IERC20ABI))
if err != nil {
return false, nil, 0, nil, err
return false, nil, 0, 0, nil, err
}
data, err := erc20ABI.Pack("approve", bridgeAddress, amountIn)
if err != nil {
return false, nil, 0, nil, err
return false, nil, 0, 0, nil, err
}
estimate, err := ethClient.EstimateGas(context.Background(), ethereum.CallMsg{
@@ -508,11 +509,25 @@ func (r *Router) requireApproval(ctx context.Context, sendType SendType, bridge
Data: data,
})
if err != nil {
return false, nil, 0, nil, err
return false, nil, 0, 0, nil, err
}
return true, amountIn, estimate, bridgeAddress, nil
// fetching l1 fee
oracleContractAddress, err := gaspriceoracle.ContractAddress(network.ChainID)
if err != nil {
return false, nil, 0, 0, nil, err
}
oracleContract, err := gaspriceoracle.NewGaspriceoracleCaller(oracleContractAddress, ethClient)
if err != nil {
return false, nil, 0, 0, nil, err
}
callOpt := &bind.CallOpts{}
l1Fee, _ := oracleContract.GetL1Fee(callOpt, data)
return true, amountIn, estimate, l1Fee.Uint64(), bridgeAddress, nil
}
func (r *Router) getBalance(ctx context.Context, network *params.Network, token *token.Token, account common.Address) (*big.Int, error) {
@@ -525,24 +540,27 @@ func (r *Router) getBalance(ctx context.Context, network *params.Network, token
}
func (r *Router) getERC1155Balance(ctx context.Context, network *params.Network, token *token.Token, account common.Address) (*big.Int, error) {
client, err := r.s.rpcClient.EthClient(network.ChainID)
if err != nil {
return nil, err
}
tokenID, success := new(big.Int).SetString(token.Symbol, 10)
if !success {
return nil, errors.New("failed to convert token symbol to big.Int")
}
caller, err := ierc1155.NewIerc1155Caller(token.Address, client)
balances, err := r.s.collectiblesManager.FetchERC1155Balances(
ctx,
account,
walletCommon.ChainID(network.ChainID),
token.Address,
[]*bigint.BigInt{&bigint.BigInt{Int: tokenID}},
)
if err != nil {
return nil, err
}
return caller.BalanceOf(&bind.CallOpts{
Context: ctx,
}, account, tokenID)
if len(balances) != 1 || balances[0] == nil {
return nil, errors.New("invalid ERC1155 balance fetch response")
}
return balances[0].Int, nil
}
func (r *Router) suggestedRoutes(
@@ -572,7 +590,6 @@ func (r *Router) suggestedRoutes(
if err != nil {
return nil, err
}
var (
group = async.NewAtomicGroup(ctx)
mu sync.Mutex
@@ -658,7 +675,6 @@ func (r *Router) suggestedRoutes(
if len(preferedChainIDs) > 0 && !containsNetworkChainID(dest, preferedChainIDs) {
continue
}
if containsNetworkChainID(dest, disabledToChaindIDs) {
continue
}
@@ -692,31 +708,45 @@ func (r *Router) suggestedRoutes(
gasLimit = sendType.EstimateGas(r.s, network, addrFrom, tokenID)
}
approvalRequired, approvalAmountRequired, approvalGasLimit, l1ApprovalFee, approvalContractAddress, err := r.requireApproval(ctx, sendType, bridge, addrFrom, network, token, amountIn)
if err != nil {
continue
}
tx, err := bridge.BuildTx(network, dest, addrFrom, addrTo, token, amountIn, bonderFees)
if err != nil {
continue
}
l1GasFeeWei, _ := r.s.feesManager.getL1Fee(ctx, network.ChainID, tx)
l1GasFeeWei += l1ApprovalFee
gasFees.L1GasFee = weiToGwei(big.NewInt(int64(l1GasFeeWei)))
requiredNativeBalance := new(big.Int).Mul(gweiToWei(maxFees), big.NewInt(int64(gasLimit)))
requiredNativeBalance.Add(requiredNativeBalance, new(big.Int).Mul(gweiToWei(maxFees), big.NewInt(int64(approvalGasLimit))))
requiredNativeBalance.Add(requiredNativeBalance, big.NewInt(int64(l1GasFeeWei))) // add l1Fee to requiredNativeBalance, in case of L1 chain l1Fee is 0
if nativeBalance.Cmp(requiredNativeBalance) <= 0 {
continue
}
// Removed the required fees from maxAMount in case of native token tx
if token.IsNative() {
maxAmountIn = (*hexutil.Big)(new(big.Int).Sub(maxAmountIn.ToInt(), requiredNativeBalance))
}
if nativeBalance.Cmp(requiredNativeBalance) <= 0 {
continue
}
approvalRequired, approvalAmountRequired, approvalGasLimit, approvalContractAddress, err := r.requireApproval(ctx, sendType, bridge, addrFrom, network, token, amountIn)
if err != nil {
continue
}
ethPrice := big.NewFloat(prices["ETH"])
approvalGasFees := new(big.Float).Mul(gweiToEth(maxFees), big.NewFloat((float64(approvalGasLimit))))
approvalGasCost := new(big.Float)
approvalGasCost.Mul(
approvalGasFees,
big.NewFloat(prices["ETH"]),
)
approvalGasCost.Mul(approvalGasFees, ethPrice)
l1GasCost := new(big.Float)
l1GasCost.Mul(gasFees.L1GasFee, ethPrice)
gasCost := new(big.Float)
gasCost.Mul(
new(big.Float).Mul(gweiToEth(maxFees), big.NewFloat((float64(gasLimit)))),
big.NewFloat(prices["ETH"]),
)
gasCost.Mul(new(big.Float).Mul(gweiToEth(maxFees), big.NewFloat(float64(gasLimit))), ethPrice)
tokenFeesAsFloat := new(big.Float).Quo(
new(big.Float).SetInt(tokenFees),
@@ -728,6 +758,7 @@ func (r *Router) suggestedRoutes(
cost := new(big.Float)
cost.Add(tokenCost, gasCost)
cost.Add(cost, approvalGasCost)
cost.Add(cost, l1GasCost)
mu.Lock()
candidates = append(candidates, &Path{
BridgeName: bridge.Name(),

View File

@@ -2,6 +2,7 @@ package wallet
import (
"database/sql"
"encoding/json"
"fmt"
"time"
@@ -31,6 +32,32 @@ func (s *SavedAddress) ID() string {
return fmt.Sprintf("%s-%t", s.Address.Hex(), s.IsTest)
}
func (s *SavedAddress) MarshalJSON() ([]byte, error) {
item := struct {
Address common.Address `json:"address"`
MixedcaseAddress string `json:"mixedcaseAddress"`
Name string `json:"name"`
ChainShortNames string `json:"chainShortNames"`
ENSName string `json:"ens"`
ColorID multiAccCommon.CustomizationColor `json:"colorId"`
IsTest bool `json:"isTest"`
CreatedAt int64 `json:"createdAt"`
Removed bool `json:"removed"`
}{
Address: s.Address,
MixedcaseAddress: s.Address.Hex(),
Name: s.Name,
ChainShortNames: s.ChainShortNames,
ENSName: s.ENSName,
ColorID: s.ColorID,
IsTest: s.IsTest,
CreatedAt: s.CreatedAt,
Removed: s.Removed,
}
return json.Marshal(item)
}
type SavedAddressesManager struct {
db *sql.DB
}

View File

@@ -121,7 +121,7 @@ func NewService(
raribleClient := rarible.NewClient(config.WalletConfig.RaribleMainnetAPIKey, config.WalletConfig.RaribleTestnetAPIKey)
alchemyClient := alchemy.NewClient(config.WalletConfig.AlchemyAPIKeys)
// Try OpenSea, Infura, Alchemy in that order
// Collectible providers in priority order (i.e. provider N+1 will be tried only if provider N fails)
contractOwnershipProviders := []thirdparty.CollectibleContractOwnershipProvider{
raribleClient,
alchemyClient,
@@ -145,7 +145,26 @@ func NewService(
openseaV2Client,
}
collectiblesManager := collectibles.NewManager(db, rpcClient, communityManager, contractOwnershipProviders, accountOwnershipProviders, collectibleDataProviders, collectionDataProviders, mediaServer, feed)
collectibleSearchProviders := []thirdparty.CollectibleSearchProvider{
raribleClient,
}
collectibleProviders := thirdparty.CollectibleProviders{
ContractOwnershipProviders: contractOwnershipProviders,
AccountOwnershipProviders: accountOwnershipProviders,
CollectibleDataProviders: collectibleDataProviders,
CollectionDataProviders: collectionDataProviders,
SearchProviders: collectibleSearchProviders,
}
collectiblesManager := collectibles.NewManager(
db,
rpcClient,
communityManager,
collectibleProviders,
mediaServer,
feed,
)
collectibles := collectibles.NewService(db, feed, accountsDB, accountFeed, settingsFeed, communityManager, rpcClient.NetworkManager, collectiblesManager)
activity := activity.NewService(db, tokenManager, collectiblesManager, feed, pendingTxManager)

View File

@@ -33,8 +33,6 @@ func getBaseURL(chainID walletCommon.ChainID) (string, error) {
return "https://eth-sepolia.g.alchemy.com", nil
case walletCommon.OptimismMainnet:
return "https://opt-mainnet.g.alchemy.com", nil
case walletCommon.OptimismGoerli:
return "https://opt-goerli.g.alchemy.com", nil
case walletCommon.OptimismSepolia:
return "https://opt-sepolia.g.alchemy.com", nil
case walletCommon.ArbitrumMainnet:
@@ -129,14 +127,13 @@ func (o *Client) doPostWithJSON(ctx context.Context, url string, payload any) (*
}
func (o *Client) doWithRetries(req *http.Request) (*http.Response, error) {
b := backoff.ExponentialBackOff{
InitialInterval: time.Millisecond * 1000,
RandomizationFactor: 0.1,
Multiplier: 1.5,
MaxInterval: time.Second * 32,
MaxElapsedTime: time.Second * 128,
Clock: backoff.SystemClock,
}
b := backoff.NewExponentialBackOff()
b.InitialInterval = time.Millisecond * 1000
b.RandomizationFactor = 0.1
b.Multiplier = 1.5
b.MaxInterval = time.Second * 32
b.MaxElapsedTime = time.Second * 70
b.Reset()
op := func() (*http.Response, error) {
@@ -151,12 +148,13 @@ func (o *Client) doWithRetries(req *http.Request) (*http.Response, error) {
err = fmt.Errorf("unsuccessful request: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode))
if resp.StatusCode == http.StatusTooManyRequests {
log.Error("doWithRetries failed with http.StatusTooManyRequests", "provider", o.ID(), "elapsed time", b.GetElapsedTime(), "next backoff", b.NextBackOff())
return nil, err
}
return nil, backoff.Permanent(err)
}
return backoff.RetryWithData(op, &b)
return backoff.RetryWithData(op, b)
}
func (o *Client) FetchCollectibleOwnersByContractAddress(ctx context.Context, chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {

View File

@@ -87,7 +87,7 @@ const ownedCollectiblesJSON = `{
},
"owners": null,
"timeLastUpdated": "2024-01-03T19:11:04.681Z",
"balance": "1",
"balance": "15",
"acquiredAt": {
"blockTimestamp": null,
"blockNumber": null

View File

@@ -133,6 +133,7 @@ type Asset struct {
Image Image `json:"image"`
Raw Raw `json:"raw"`
TokenURI string `json:"tokenUri"`
Balance *bigint.BigInt `json:"balance,omitempty"`
}
type OwnedNFTList struct {
@@ -216,6 +217,7 @@ func (c *Asset) toCommon(id thirdparty.CollectibleUniqueID) thirdparty.FullColle
return thirdparty.FullCollectibleData{
CollectibleData: c.toCollectiblesData(id),
CollectionData: &contractData,
AccountBalance: c.Balance,
}
}

View File

@@ -14,45 +14,52 @@ import (
)
var coinGeckoMapping = map[string]string{
"STT": "status",
"SNT": "status",
"ETH": "ethereum",
"AST": "airswap",
"AMB": "",
"ABT": "arcblock",
"ATM": "",
"BNB": "binancecoin",
"BLT": "bloom",
"CDT": "",
"COMP": "compound-coin",
"EDG": "edgeless",
"ELF": "",
"ENG": "enigma",
"EOS": "eos",
"GEN": "daostack",
"MANA": "decentraland-wormhole",
"LEND": "ethlend",
"LRC": "loopring",
"MET": "metronome",
"POLY": "polymath",
"PPT": "populous",
"SAN": "santiment-network-token",
"DNT": "district0x",
"SPN": "sapien",
"USDS": "stableusd",
"STX": "stox",
"SUB": "substratum",
"PAY": "tenx",
"GRT": "the-graph",
"TNT": "tierion",
"TRX": "tron",
"TGT": "",
"RARE": "superrare",
"UNI": "uniswap",
"USDC": "usd-coin",
"USDP": "paxos-standard",
"VRS": "",
"TIME": "",
"STT": "status",
"SNT": "status",
"ETH": "ethereum",
"AST": "airswap",
"AMB": "",
"ABT": "arcblock",
"ATM": "",
"BNB": "binancecoin",
"BLT": "bloom",
"CDT": "",
"COMP": "compound-coin",
"EDG": "edgeless",
"ELF": "",
"ENG": "enigma",
"EOS": "eos",
"GEN": "daostack",
"MANA": "decentraland-wormhole",
"LEND": "ethlend",
"LRC": "loopring",
"MET": "metronome",
"POLY": "polymath",
"PPT": "populous",
"SAN": "santiment-network-token",
"DNT": "district0x",
"SPN": "sapien",
"USDS": "stableusd",
"STX": "stox",
"SUB": "substratum",
"PAY": "tenx",
"GRT": "the-graph",
"TNT": "tierion",
"TRX": "tron",
"TGT": "",
"RARE": "superrare",
"UNI": "uniswap",
"USDC": "usd-coin",
"USDP": "paxos-standard",
"VRS": "",
"TIME": "",
"USDT": "tether",
"SHIB": "shiba-inu",
"LINK": "chainlink",
"MATIC": "matic-network",
"DAI": "dai",
"ARB": "arbitrum",
"OP": "optimism",
}
const baseURL = "https://api.coingecko.com/api/v3/"
@@ -150,6 +157,7 @@ func (c *Client) mapSymbolsToIds(symbols []string) ([]string, error) {
ids = append(ids, token.ID)
}
}
ids = utils.RemoveDuplicates(ids)
return ids, nil
}
@@ -182,7 +190,7 @@ func (c *Client) FetchPrices(symbols []string, currencies []string) (map[string]
prices := make(map[string]map[string]float64)
err = json.Unmarshal(body, &prices)
if err != nil {
return nil, err
return nil, fmt.Errorf("%s - %s", err, string(body))
}
result := make(map[string]map[string]float64)
@@ -240,7 +248,7 @@ func (c *Client) FetchTokenMarketValues(symbols []string, currency string) (map[
var marketValues []GeckoMarketValues
err = json.Unmarshal(body, &marketValues)
if err != nil {
return nil, err
return nil, fmt.Errorf("%s - %s", err, string(body))
}
result := make(map[string]thirdparty.TokenMarketValues)

View File

@@ -97,6 +97,45 @@ func GroupContractIDsByChainID(ids []ContractID) map[w_common.ChainID][]Contract
return ret
}
func GroupCollectiblesByChainID(collectibles []*FullCollectibleData) map[w_common.ChainID][]*FullCollectibleData {
ret := make(map[w_common.ChainID][]*FullCollectibleData)
for i, collectible := range collectibles {
chainID := collectible.CollectibleData.ID.ContractID.ChainID
if _, ok := ret[chainID]; !ok {
ret[chainID] = make([]*FullCollectibleData, 0, len(collectibles))
}
ret[chainID] = append(ret[chainID], collectibles[i])
}
return ret
}
func GroupCollectiblesByContractAddress(collectibles []*FullCollectibleData) map[common.Address][]*FullCollectibleData {
ret := make(map[common.Address][]*FullCollectibleData)
for i, collectible := range collectibles {
contractAddress := collectible.CollectibleData.ID.ContractID.Address
if _, ok := ret[contractAddress]; !ok {
ret[contractAddress] = make([]*FullCollectibleData, 0, len(collectibles))
}
ret[contractAddress] = append(ret[contractAddress], collectibles[i])
}
return ret
}
func GroupCollectiblesByChainIDAndContractAddress(collectibles []*FullCollectibleData) map[w_common.ChainID]map[common.Address][]*FullCollectibleData {
ret := make(map[w_common.ChainID]map[common.Address][]*FullCollectibleData)
collectiblesByChainID := GroupCollectiblesByChainID(collectibles)
for chainID, chainCollectibles := range collectiblesByChainID {
ret[chainID] = GroupCollectiblesByContractAddress(chainCollectibles)
}
return ret
}
type CollectionTrait struct {
Min float64 `json:"min"`
Max float64 `json:"max"`
@@ -138,6 +177,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.
@@ -152,7 +192,8 @@ type FullCollectibleData struct {
CollectionData *CollectionData
CommunityInfo *CommunityInfo
CollectibleCommunityInfo *CollectibleCommunityInfo
Ownership []AccountBalance
Ownership []AccountBalance // This is a list of all the owners of the collectible
AccountBalance *bigint.BigInt // This is the balance of the collectible for the requested account
}
type CollectiblesContainer[T any] struct {
@@ -162,29 +203,38 @@ type CollectiblesContainer[T any] struct {
Provider string
}
type CollectibleOwnershipContainer CollectiblesContainer[CollectibleUniqueID]
type CollectibleOwnershipContainer CollectiblesContainer[CollectibleIDBalance]
type CollectionDataContainer CollectiblesContainer[CollectionData]
type CollectibleDataContainer CollectiblesContainer[CollectibleData]
type FullCollectibleDataContainer CollectiblesContainer[FullCollectibleData]
// Tried to find a way to make this generic, but couldn't, so the code below is duplicated somewhere else
func collectibleItemsToIDs(items []FullCollectibleData) []CollectibleUniqueID {
ret := make([]CollectibleUniqueID, 0, len(items))
func collectibleItemsToBalances(items []FullCollectibleData) []CollectibleIDBalance {
ret := make([]CollectibleIDBalance, 0, len(items))
for _, item := range items {
ret = append(ret, item.CollectibleData.ID)
balance := CollectibleIDBalance{
ID: item.CollectibleData.ID,
Balance: item.AccountBalance,
}
ret = append(ret, balance)
}
return ret
}
func (c *FullCollectibleDataContainer) ToOwnershipContainer() CollectibleOwnershipContainer {
return CollectibleOwnershipContainer{
Items: collectibleItemsToIDs(c.Items),
Items: collectibleItemsToBalances(c.Items),
NextCursor: c.NextCursor,
PreviousCursor: c.PreviousCursor,
Provider: c.Provider,
}
}
type CollectibleIDBalance struct {
ID CollectibleUniqueID `json:"id"`
Balance *bigint.BigInt `json:"balance"`
}
type TokenBalance struct {
TokenID *bigint.BigInt `json:"tokenId"`
Balance *bigint.BigInt `json:"balance"`
@@ -228,3 +278,44 @@ type CollectionDataProvider interface {
CollectibleProvider
FetchCollectionsDataByContractID(ctx context.Context, ids []ContractID) ([]CollectionData, error)
}
type CollectibleSearchProvider interface {
CollectibleProvider
SearchCollections(ctx context.Context, chainID w_common.ChainID, text string, cursor string, limit int) (*CollectionDataContainer, error)
SearchCollectibles(ctx context.Context, chainID w_common.ChainID, collections []common.Address, text string, cursor string, limit int) (*FullCollectibleDataContainer, error)
}
type CollectibleProviders struct {
ContractOwnershipProviders []CollectibleContractOwnershipProvider
AccountOwnershipProviders []CollectibleAccountOwnershipProvider
CollectibleDataProviders []CollectibleDataProvider
CollectionDataProviders []CollectionDataProvider
SearchProviders []CollectibleSearchProvider
}
func (p *CollectibleProviders) GetProviderList() []CollectibleProvider {
ret := make([]CollectibleProvider, 0)
uniqueProviders := make(map[string]CollectibleProvider)
for _, provider := range p.ContractOwnershipProviders {
uniqueProviders[provider.ID()] = provider
}
for _, provider := range p.AccountOwnershipProviders {
uniqueProviders[provider.ID()] = provider
}
for _, provider := range p.CollectibleDataProviders {
uniqueProviders[provider.ID()] = provider
}
for _, provider := range p.CollectionDataProviders {
uniqueProviders[provider.ID()] = provider
}
for _, provider := range p.SearchProviders {
uniqueProviders[provider.ID()] = provider
}
for _, provider := range uniqueProviders {
ret = append(ret, provider)
}
return ret
}

View File

@@ -9,9 +9,10 @@ type CommunityInfo struct {
}
type CommunityInfoProvider interface {
GetCommunityInfoFromDB(communityID string) (*CommunityInfo, error)
FetchCommunityInfo(communityID string) (*CommunityInfo, error)
// Collectible-related methods
GetCommunityID(tokenURI string) string
FillCollectibleMetadata(collectible *FullCollectibleData) error
FillCollectiblesMetadata(communityID string, cs []*FullCollectibleData) (bool, error)
}

View File

@@ -71,7 +71,7 @@ func (c *Client) FetchPrices(symbols []string, currencies []string) (map[string]
prices := make(map[string]map[string]float64)
err = json.Unmarshal(body, &prices)
if err != nil {
return nil, err
return nil, fmt.Errorf("%s - %s", err, string(body))
}
for _, symbol := range smbls {
@@ -132,8 +132,12 @@ func (c *Client) FetchTokenMarketValues(symbols []string, currency string) (map[
container := MarketValuesContainer{}
err = json.Unmarshal(body, &container)
if len(container.Raw) == 0 {
return nil, fmt.Errorf("no data found - %s", string(body))
}
if err != nil {
return item, err
return nil, fmt.Errorf("%s - %s", err, string(body))
}
for _, symbol := range smbls {

View File

@@ -157,7 +157,9 @@ func (o *ClientV2) fetchAssets(ctx context.Context, chainID walletCommon.ChainID
body, err := o.client.doGetRequest(ctx, url, o.apiKey)
if err != nil {
o.connectionStatus.SetIsConnected(false)
if ctx.Err() == nil {
o.connectionStatus.SetIsConnected(false)
}
return nil, err
}
o.connectionStatus.SetIsConnected(true)
@@ -274,7 +276,9 @@ func (o *ClientV2) fetchCollectionDataBySlug(ctx context.Context, chainID wallet
body, err := o.client.doGetRequest(ctx, url, o.apiKey)
if err != nil {
o.connectionStatus.SetIsConnected(false)
if ctx.Err() == nil {
o.connectionStatus.SetIsConnected(false)
}
return nil, err
}
o.connectionStatus.SetIsConnected(true)

View File

@@ -24,6 +24,8 @@ import (
const ownedNFTLimit = 100
const collectionOwnershipLimit = 50
const nftMetadataBatchLimit = 50
const searchCollectiblesLimit = 1000
const searchCollectionsLimit = 1000
func (o *Client) ID() string {
return RaribleID
@@ -42,7 +44,7 @@ func getBaseURL(chainID walletCommon.ChainID) (string, error) {
switch uint64(chainID) {
case walletCommon.EthereumMainnet, walletCommon.ArbitrumMainnet:
return "https://api.rarible.org", nil
case walletCommon.EthereumGoerli, walletCommon.ArbitrumSepolia:
case walletCommon.ArbitrumSepolia:
return "https://testnet-api.rarible.org", nil
}
@@ -143,14 +145,13 @@ func (o *Client) doPostWithJSON(ctx context.Context, url string, payload any, ap
}
func (o *Client) doWithRetries(req *http.Request, apiKey string) (*http.Response, error) {
b := backoff.ExponentialBackOff{
InitialInterval: time.Millisecond * 1000,
RandomizationFactor: 0.1,
Multiplier: 1.5,
MaxInterval: time.Second * 32,
MaxElapsedTime: time.Second * 128,
Clock: backoff.SystemClock,
}
b := backoff.NewExponentialBackOff()
b.InitialInterval = time.Millisecond * 1000
b.RandomizationFactor = 0.1
b.Multiplier = 1.5
b.MaxInterval = time.Second * 32
b.MaxElapsedTime = time.Second * 70
b.Reset()
req.Header.Set("X-API-KEY", apiKey)
@@ -167,12 +168,13 @@ func (o *Client) doWithRetries(req *http.Request, apiKey string) (*http.Response
err = fmt.Errorf("unsuccessful request: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode))
if resp.StatusCode == http.StatusTooManyRequests {
log.Error("doWithRetries failed with http.StatusTooManyRequests", "provider", o.ID(), "elapsed time", b.GetElapsedTime(), "next backoff", b.NextBackOff())
return nil, err
}
return nil, backoff.Permanent(err)
}
return backoff.RetryWithData(op, &b)
return backoff.RetryWithData(op, b)
}
func (o *Client) FetchCollectibleOwnersByContractAddress(ctx context.Context, chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
@@ -197,7 +199,9 @@ func (o *Client) FetchCollectibleOwnersByContractAddress(ctx context.Context, ch
resp, err := o.doQuery(ctx, url, o.getAPIKey(chainID))
if err != nil {
o.connectionStatus.SetIsConnected(false)
if ctx.Err() == nil {
o.connectionStatus.SetIsConnected(false)
}
return nil, err
}
o.connectionStatus.SetIsConnected(true)
@@ -258,7 +262,9 @@ func (o *Client) FetchAllAssetsByOwner(ctx context.Context, chainID walletCommon
resp, err := o.doQuery(ctx, url, o.getAPIKey(chainID))
if err != nil {
o.connectionStatus.SetIsConnected(false)
if ctx.Err() == nil {
o.connectionStatus.SetIsConnected(false)
}
return nil, err
}
o.connectionStatus.SetIsConnected(true)
@@ -398,7 +404,9 @@ func (o *Client) FetchCollectionsDataByContractID(ctx context.Context, contractI
resp, err := o.doQuery(ctx, url, o.getAPIKey(contractID.ChainID))
if err != nil {
o.connectionStatus.SetIsConnected(false)
if ctx.Err() == nil {
o.connectionStatus.SetIsConnected(false)
}
return nil, err
}
o.connectionStatus.SetIsConnected(true)
@@ -426,3 +434,184 @@ func (o *Client) FetchCollectionsDataByContractID(ctx context.Context, contractI
return ret, nil
}
func (o *Client) searchCollectibles(ctx context.Context, chainID walletCommon.ChainID, collections []common.Address, fullText CollectibleFilterFullText, sort CollectibleFilterContainerSort, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
baseURL, err := getItemBaseURL(chainID)
if err != nil {
return nil, err
}
url := fmt.Sprintf("%s/search", baseURL)
ret := &thirdparty.FullCollectibleDataContainer{
Provider: o.ID(),
Items: make([]thirdparty.FullCollectibleData, 0),
PreviousCursor: cursor,
NextCursor: "",
}
if fullText.Text == "" {
return ret, nil
}
tmpLimit := searchCollectiblesLimit
if limit > thirdparty.FetchNoLimit && limit < tmpLimit {
tmpLimit = limit
}
blockchainString := chainIDToChainString(chainID)
filterContainer := CollectibleFilterContainer{
Cursor: cursor,
Limit: tmpLimit,
Filter: CollectibleFilter{
Blockchains: []string{blockchainString},
Deleted: false,
FullText: fullText,
},
Sort: sort,
}
for _, collection := range collections {
filterContainer.Filter.Collections = append(filterContainer.Filter.Collections, fmt.Sprintf("%s:%s", blockchainString, collection.String()))
}
for {
resp, err := o.doPostWithJSON(ctx, url, filterContainer, o.getAPIKey(chainID))
if err != nil {
if ctx.Err() == nil {
o.connectionStatus.SetIsConnected(false)
}
return nil, err
}
o.connectionStatus.SetIsConnected(true)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// if Json is not returned there must be an error
if !json.Valid(body) {
return nil, fmt.Errorf("invalid json: %s", string(body))
}
var collectibles CollectiblesContainer
err = json.Unmarshal(body, &collectibles)
if err != nil {
return nil, err
}
ret.Items = append(ret.Items, raribleToCollectiblesData(collectibles.Collectibles, chainID.IsMainnet())...)
ret.NextCursor = collectibles.Continuation
if len(ret.NextCursor) == 0 {
break
}
filterContainer.Cursor = ret.NextCursor
if limit != thirdparty.FetchNoLimit && len(ret.Items) >= limit {
break
}
}
return ret, nil
}
func (o *Client) searchCollections(ctx context.Context, chainID walletCommon.ChainID, text string, cursor string, limit int) (*thirdparty.CollectionDataContainer, error) {
baseURL, err := getCollectionBaseURL(chainID)
if err != nil {
return nil, err
}
url := fmt.Sprintf("%s/search", baseURL)
ret := &thirdparty.CollectionDataContainer{
Provider: o.ID(),
Items: make([]thirdparty.CollectionData, 0),
PreviousCursor: cursor,
NextCursor: "",
}
if text == "" {
return ret, nil
}
tmpLimit := searchCollectionsLimit
if limit > thirdparty.FetchNoLimit && limit < tmpLimit {
tmpLimit = limit
}
filterContainer := CollectionFilterContainer{
Cursor: cursor,
Limit: tmpLimit,
Filter: CollectionFilter{
Blockchains: []string{chainIDToChainString(chainID)},
Text: text,
},
}
for {
resp, err := o.doPostWithJSON(ctx, url, filterContainer, o.getAPIKey(chainID))
if err != nil {
if ctx.Err() == nil {
o.connectionStatus.SetIsConnected(false)
}
return nil, err
}
o.connectionStatus.SetIsConnected(true)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// if Json is not returned there must be an error
if !json.Valid(body) {
return nil, fmt.Errorf("invalid json: %s", string(body))
}
var collections CollectionsContainer
err = json.Unmarshal(body, &collections)
if err != nil {
return nil, err
}
ret.Items = append(ret.Items, raribleToCollectionsData(collections.Collections, chainID.IsMainnet())...)
ret.NextCursor = collections.Continuation
if len(ret.NextCursor) == 0 {
break
}
filterContainer.Cursor = ret.NextCursor
if limit != thirdparty.FetchNoLimit && len(ret.Items) >= limit {
break
}
}
return ret, nil
}
func (o *Client) SearchCollections(ctx context.Context, chainID walletCommon.ChainID, text string, cursor string, limit int) (*thirdparty.CollectionDataContainer, error) {
return o.searchCollections(ctx, chainID, text, cursor, limit)
}
func (o *Client) SearchCollectibles(ctx context.Context, chainID walletCommon.ChainID, collections []common.Address, text string, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
fullText := CollectibleFilterFullText{
Text: text,
Fields: []string{
CollectibleFilterFullTextFieldName,
},
}
sort := CollectibleFilterContainerSortRelevance
return o.searchCollectibles(ctx, chainID, collections, fullText, sort, cursor, limit)
}

View File

@@ -110,6 +110,51 @@ type BatchTokenIDs struct {
IDs []string `json:"ids"`
}
type CollectibleFilterFullTextField = string
const (
CollectibleFilterFullTextFieldName = "NAME"
CollectibleFilterFullTextFieldDescription = "DESCRIPTION"
)
type CollectibleFilterFullText struct {
Text string `json:"text"`
Fields []CollectibleFilterFullTextField `json:"fields"`
}
type CollectibleFilter struct {
Blockchains []string `json:"blockchains"`
Collections []string `json:"collections,omitempty"`
Deleted bool `json:"deleted"`
FullText CollectibleFilterFullText `json:"fullText"`
}
type CollectibleFilterContainerSort = string
const (
CollectibleFilterContainerSortRelevance = "RELEVANCE"
CollectibleFilterContainerSortLatest = "LATEST"
CollectibleFilterContainerSortEarliest = "EARLIEST"
)
type CollectibleFilterContainer struct {
Limit int `json:"size"`
Cursor string `json:"continuation"`
Filter CollectibleFilter `json:"filter"`
Sort CollectibleFilterContainerSort `json:"sort"`
}
type CollectionFilter struct {
Blockchains []string `json:"blockchains"`
Text string `json:"text"`
}
type CollectionFilterContainer struct {
Limit int `json:"size"`
Cursor string `json:"continuation"`
Filter CollectionFilter `json:"filter"`
}
type CollectiblesContainer struct {
Continuation string `json:"continuation"`
Collectibles []Collectible `json:"items"`
@@ -157,6 +202,11 @@ func (st *AttributeValue) UnmarshalJSON(b []byte) error {
return nil
}
type CollectionsContainer struct {
Continuation string `json:"continuation"`
Collections []Collection `json:"collections"`
}
type Collection struct {
ID string `json:"id"`
Blockchain string `json:"blockchain"`
@@ -247,6 +297,19 @@ func raribleToCollectiblesData(l []Collectible, isMainnet bool) []thirdparty.Ful
return ret
}
func raribleToCollectionsData(l []Collection, isMainnet bool) []thirdparty.CollectionData {
ret := make([]thirdparty.CollectionData, 0, len(l))
for _, c := range l {
id, err := raribleContractIDToUniqueID(c.ID, isMainnet)
if err != nil {
continue
}
item := c.toCommon(id)
ret = append(ret, item)
}
return ret
}
func (c *Collection) toCommon(id thirdparty.ContractID) thirdparty.CollectionData {
ret := thirdparty.CollectionData{
ID: id,

View File

@@ -13,6 +13,18 @@ func RenameSymbols(symbols []string) (renames []string) {
return
}
func RemoveDuplicates(strings []string) []string {
uniqueStrings := make(map[string]bool)
var uniqueSlice []string
for _, str := range strings {
if !uniqueStrings[str] {
uniqueStrings[str] = true
uniqueSlice = append(uniqueSlice, str)
}
}
return uniqueSlice
}
func GetRealSymbol(symbol string) string {
if val, ok := renameMapping[strings.ToUpper(symbol)]; ok {
return val

View File

@@ -30,6 +30,7 @@ import (
"github.com/status-im/status-go/services/communitytokens"
"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/bigint"
"github.com/status-im/status-go/services/wallet/community"
"github.com/status-im/status-go/services/wallet/walletevent"
)
@@ -62,14 +63,10 @@ type Token struct {
}
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"`
Token
Amount float64 `json:"amount"`
TxHash common.Hash `json:"txHash"`
IsFirst bool `json:"isFirst"`
}
func (t *Token) IsNative() bool {
@@ -316,20 +313,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 +806,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,15 +823,14 @@ 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()
t.Image = tm.mediaServer.MakeCommunityTokenImagesURL(t.CommunityData.ID, t.ChainID, t.Symbol)
receivedToken := ReceivedToken{
Address: t.Address,
Name: t.Name,
Symbol: t.Symbol,
Image: t.Image,
ChainID: t.ChainID,
CommunityData: t.CommunityData,
Balance: value,
TxHash: txHash,
Token: *t,
Amount: floatAmount,
TxHash: txHash,
IsFirst: isFirst,
}
encodedMessage, err := json.Marshal(receivedToken)
@@ -869,3 +865,14 @@ func (tm *Manager) fillCommunityData(token *Token) error {
}
return nil
}
func (tm *Manager) GetTokenHistoricalBalance(account common.Address, chainID uint64, symbol string, timestamp int64) (*big.Int, error) {
var balance big.Int
err := tm.db.QueryRow("SELECT balance FROM balance_history WHERE currency = ? AND chain_id = ? AND address = ? AND timestamp < ? order by timestamp DESC LIMIT 1", symbol, chainID, account, timestamp).Scan((*bigint.SQLBigIntBytes)(&balance))
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, err
}
return &balance, nil
}

View File

@@ -15,6 +15,7 @@ type store interface {
var tokenPeg = map[string]string{
"aUSDC": "USD",
"DAI": "USD",
"EUROC": "EUR",
"SAI": "USD",
"sUSD": "USD",
"PAXG": "XAU",
@@ -776,6 +777,14 @@ func newDefaultStore() *DefaultStore {
ChainID: 1,
TokenListID: "status",
},
&Token{
Address: common.HexToAddress("0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c"),
Name: "Euro Coin",
Symbol: "EUROC",
Decimals: 6,
ChainID: 1,
TokenListID: "status",
},
&Token{
Address: common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"),
Name: "USD Coin",
@@ -1729,7 +1738,23 @@ func newDefaultStore() *DefaultStore {
TokenListID: "status",
},
&Token{
Address: common.HexToAddress("0x44a739916D41eC0226d98F83BE5364B69078DA41"),
Address: common.HexToAddress("0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4"),
Name: "Euro Coin",
Symbol: "EUROC",
Decimals: 6,
ChainID: 11155111,
TokenListID: "status",
},
&Token{
Address: common.HexToAddress("0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"),
Name: "USD Coin",
Symbol: "USDC",
Decimals: 6,
ChainID: 11155111,
TokenListID: "status",
},
&Token{
Address: common.HexToAddress("0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d"),
Name: "USD Coin",
Symbol: "USDC",
Decimals: 6,
@@ -1737,7 +1762,7 @@ func newDefaultStore() *DefaultStore {
TokenListID: "status",
},
&Token{
Address: common.HexToAddress("0x44a739916D41eC0226d98F83BE5364B69078DA41"),
Address: common.HexToAddress("0x5fd84259d66Cd46123540766Be93DFE6D43130D7"),
Name: "USD Coin",
Symbol: "USDC",
Decimals: 6,

View File

@@ -10,7 +10,8 @@ import (
)
type BlockRangeDAOer interface {
getBlockRange(chainID uint64, address common.Address) (blockRange *ethTokensBlockRanges, err error)
getBlockRange(chainID uint64, address common.Address) (blockRange *ethTokensBlockRanges, exists bool, err error)
getBlockRanges(chainID uint64, addresses []common.Address) (blockRanges map[common.Address]*ethTokensBlockRanges, err error)
upsertRange(chainID uint64, account common.Address, newBlockRange *ethTokensBlockRanges) (err error)
updateTokenRange(chainID uint64, account common.Address, newBlockRange *BlockRange) (err error)
upsertEthRange(chainID uint64, account common.Address, newBlockRange *BlockRange) (err error)
@@ -27,7 +28,7 @@ type BlockRange struct {
}
func NewBlockRange() *BlockRange {
return &BlockRange{Start: &big.Int{}, FirstKnown: &big.Int{}, LastKnown: &big.Int{}}
return &BlockRange{Start: nil, FirstKnown: nil, LastKnown: nil}
}
type ethTokensBlockRanges struct {
@@ -40,8 +41,48 @@ func newEthTokensBlockRanges() *ethTokensBlockRanges {
return &ethTokensBlockRanges{eth: NewBlockRange(), tokens: NewBlockRange()}
}
func (b *BlockRangeSequentialDAO) getBlockRange(chainID uint64, address common.Address) (blockRange *ethTokensBlockRanges, err error) {
query := `SELECT blk_start, blk_first, blk_last, token_blk_start, token_blk_first, token_blk_last, balance_check_hash FROM blocks_ranges_sequential
func scanRanges(rows *sql.Rows) (map[common.Address]*ethTokensBlockRanges, error) {
blockRanges := make(map[common.Address]*ethTokensBlockRanges)
for rows.Next() {
efk := &bigint.NilableSQLBigInt{}
elk := &bigint.NilableSQLBigInt{}
es := &bigint.NilableSQLBigInt{}
tfk := &bigint.NilableSQLBigInt{}
tlk := &bigint.NilableSQLBigInt{}
ts := &bigint.NilableSQLBigInt{}
addressB := []byte{}
blockRange := newEthTokensBlockRanges()
err := rows.Scan(&addressB, es, efk, elk, ts, tfk, tlk, &blockRange.balanceCheckHash)
if err != nil {
return nil, err
}
address := common.BytesToAddress(addressB)
blockRanges[address] = blockRange
if !es.IsNil() {
blockRanges[address].eth.Start = big.NewInt(es.Int64())
}
if !efk.IsNil() {
blockRanges[address].eth.FirstKnown = big.NewInt(efk.Int64())
}
if !elk.IsNil() {
blockRanges[address].eth.LastKnown = big.NewInt(elk.Int64())
}
if !ts.IsNil() {
blockRanges[address].tokens.Start = big.NewInt(ts.Int64())
}
if !tfk.IsNil() {
blockRanges[address].tokens.FirstKnown = big.NewInt(tfk.Int64())
}
if !tlk.IsNil() {
blockRanges[address].tokens.LastKnown = big.NewInt(tlk.Int64())
}
}
return blockRanges, nil
}
func (b *BlockRangeSequentialDAO) getBlockRange(chainID uint64, address common.Address) (blockRange *ethTokensBlockRanges, exists bool, err error) {
query := `SELECT address, blk_start, blk_first, blk_last, token_blk_start, token_blk_first, token_blk_last, balance_check_hash FROM blocks_ranges_sequential
WHERE address = ?
AND network_id = ?`
@@ -51,25 +92,45 @@ func (b *BlockRangeSequentialDAO) getBlockRange(chainID uint64, address common.A
}
defer rows.Close()
blockRange = &ethTokensBlockRanges{}
if rows.Next() {
blockRange = newEthTokensBlockRanges()
err = rows.Scan((*bigint.SQLBigInt)(blockRange.eth.Start),
(*bigint.SQLBigInt)(blockRange.eth.FirstKnown),
(*bigint.SQLBigInt)(blockRange.eth.LastKnown),
(*bigint.SQLBigInt)(blockRange.tokens.Start),
(*bigint.SQLBigInt)(blockRange.tokens.FirstKnown),
(*bigint.SQLBigInt)(blockRange.tokens.LastKnown),
&blockRange.balanceCheckHash,
)
if err != nil {
return nil, err
}
return blockRange, nil
ranges, err := scanRanges(rows)
if err != nil {
return nil, false, err
}
return blockRange, nil
blockRange, exists = ranges[address]
if !exists {
blockRange = newEthTokensBlockRanges()
}
return blockRange, exists, nil
}
func (b *BlockRangeSequentialDAO) getBlockRanges(chainID uint64, addresses []common.Address) (blockRanges map[common.Address]*ethTokensBlockRanges, err error) {
blockRanges = make(map[common.Address]*ethTokensBlockRanges)
addressesPlaceholder := ""
for i := 0; i < len(addresses); i++ {
addressesPlaceholder += "?"
if i < len(addresses)-1 {
addressesPlaceholder += ","
}
}
query := "SELECT address, blk_start, blk_first, blk_last, token_blk_start, token_blk_first, token_blk_last, balance_check_hash FROM blocks_ranges_sequential WHERE address IN (" +
addressesPlaceholder + ") AND network_id = ?"
params := []interface{}{}
for _, address := range addresses {
params = append(params, address)
}
params = append(params, chainID)
rows, err := b.db.Query(query, params...)
if err != nil {
return
}
defer rows.Close()
return scanRanges(rows)
}
func (b *BlockRangeSequentialDAO) deleteRange(account common.Address) error {
@@ -85,7 +146,7 @@ func (b *BlockRangeSequentialDAO) deleteRange(account common.Address) error {
}
func (b *BlockRangeSequentialDAO) upsertRange(chainID uint64, account common.Address, newBlockRange *ethTokensBlockRanges) (err error) {
ethTokensBlockRange, err := b.getBlockRange(chainID, account)
ethTokensBlockRange, exists, err := b.getBlockRange(chainID, account)
if err != nil {
return err
}
@@ -93,18 +154,38 @@ func (b *BlockRangeSequentialDAO) upsertRange(chainID uint64, account common.Add
ethBlockRange := prepareUpdatedBlockRange(ethTokensBlockRange.eth, newBlockRange.eth)
tokensBlockRange := prepareUpdatedBlockRange(ethTokensBlockRange.tokens, newBlockRange.tokens)
log.Debug("update eth and tokens blocks range", "account", account, "chainID", chainID,
"eth.start", ethBlockRange.Start, "eth.first", ethBlockRange.FirstKnown, "eth.last", ethBlockRange.LastKnown,
"tokens.start", tokensBlockRange.Start, "tokens.first", ethBlockRange.FirstKnown, "eth.last", ethBlockRange.LastKnown, "hash", newBlockRange.balanceCheckHash)
log.Debug("upsert eth and tokens blocks range",
"account", account, "chainID", chainID,
"eth.start", ethBlockRange.Start,
"eth.first", ethBlockRange.FirstKnown,
"eth.last", ethBlockRange.LastKnown,
"tokens.first", tokensBlockRange.FirstKnown,
"tokens.last", tokensBlockRange.LastKnown,
"hash", newBlockRange.balanceCheckHash)
var query *sql.Stmt
if exists {
query, err = b.db.Prepare(`UPDATE blocks_ranges_sequential SET
blk_start = ?,
blk_first = ?,
blk_last = ?,
token_blk_start = ?,
token_blk_first = ?,
token_blk_last = ?,
balance_check_hash = ?
WHERE network_id = ? AND address = ?`)
} else {
query, err = b.db.Prepare(`INSERT INTO blocks_ranges_sequential
(blk_start, blk_first, blk_last, token_blk_start, token_blk_first, token_blk_last, balance_check_hash, network_id, address) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
}
upsert, err := b.db.Prepare(`REPLACE INTO blocks_ranges_sequential
(network_id, address, blk_start, blk_first, blk_last, token_blk_start, token_blk_first, token_blk_last, balance_check_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
return err
}
_, err = upsert.Exec(chainID, account, (*bigint.SQLBigInt)(ethBlockRange.Start), (*bigint.SQLBigInt)(ethBlockRange.FirstKnown), (*bigint.SQLBigInt)(ethBlockRange.LastKnown),
(*bigint.SQLBigInt)(tokensBlockRange.Start), (*bigint.SQLBigInt)(tokensBlockRange.FirstKnown), (*bigint.SQLBigInt)(tokensBlockRange.LastKnown), newBlockRange.balanceCheckHash)
_, err = query.Exec((*bigint.SQLBigInt)(ethBlockRange.Start), (*bigint.SQLBigInt)(ethBlockRange.FirstKnown), (*bigint.SQLBigInt)(ethBlockRange.LastKnown),
(*bigint.SQLBigInt)(tokensBlockRange.Start), (*bigint.SQLBigInt)(tokensBlockRange.FirstKnown), (*bigint.SQLBigInt)(tokensBlockRange.LastKnown), newBlockRange.balanceCheckHash, chainID, account)
return err
}
@@ -112,28 +193,36 @@ func (b *BlockRangeSequentialDAO) upsertRange(chainID uint64, account common.Add
func (b *BlockRangeSequentialDAO) upsertEthRange(chainID uint64, account common.Address,
newBlockRange *BlockRange) (err error) {
ethTokensBlockRange, err := b.getBlockRange(chainID, account)
ethTokensBlockRange, exists, err := b.getBlockRange(chainID, account)
if err != nil {
return err
}
blockRange := prepareUpdatedBlockRange(ethTokensBlockRange.eth, newBlockRange)
log.Debug("update eth blocks range", "account", account, "chainID", chainID,
"start", blockRange.Start, "first", blockRange.FirstKnown, "last", blockRange.LastKnown, "old hash", ethTokensBlockRange.balanceCheckHash)
log.Debug("upsert eth blocks range", "account", account, "chainID", chainID,
"start", blockRange.Start,
"first", blockRange.FirstKnown,
"last", blockRange.LastKnown,
"old hash", ethTokensBlockRange.balanceCheckHash)
var query *sql.Stmt
if exists {
query, err = b.db.Prepare(`UPDATE blocks_ranges_sequential SET
blk_start = ?,
blk_first = ?,
blk_last = ?
WHERE network_id = ? AND address = ?`)
} else {
query, err = b.db.Prepare(`INSERT INTO blocks_ranges_sequential
(blk_start, blk_first, blk_last, network_id, address) VALUES (?, ?, ?, ?, ?)`)
}
upsert, err := b.db.Prepare(`REPLACE INTO blocks_ranges_sequential
(network_id, address, blk_start, blk_first, blk_last, token_blk_start, token_blk_first, token_blk_last, balance_check_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
return err
}
if ethTokensBlockRange.tokens == nil {
ethTokensBlockRange.tokens = NewBlockRange()
}
_, err = upsert.Exec(chainID, account, (*bigint.SQLBigInt)(blockRange.Start), (*bigint.SQLBigInt)(blockRange.FirstKnown), (*bigint.SQLBigInt)(blockRange.LastKnown),
(*bigint.SQLBigInt)(ethTokensBlockRange.tokens.Start), (*bigint.SQLBigInt)(ethTokensBlockRange.tokens.FirstKnown), (*bigint.SQLBigInt)(ethTokensBlockRange.tokens.LastKnown), ethTokensBlockRange.balanceCheckHash)
_, err = query.Exec((*bigint.SQLBigInt)(blockRange.Start), (*bigint.SQLBigInt)(blockRange.FirstKnown), (*bigint.SQLBigInt)(blockRange.LastKnown), chainID, account)
return err
}
@@ -141,15 +230,16 @@ func (b *BlockRangeSequentialDAO) upsertEthRange(chainID uint64, account common.
func (b *BlockRangeSequentialDAO) updateTokenRange(chainID uint64, account common.Address,
newBlockRange *BlockRange) (err error) {
ethTokensBlockRange, err := b.getBlockRange(chainID, account)
ethTokensBlockRange, _, err := b.getBlockRange(chainID, account)
if err != nil {
return err
}
blockRange := prepareUpdatedBlockRange(ethTokensBlockRange.tokens, newBlockRange)
log.Debug("update tokens blocks range", "account", account, "chainID", chainID,
"start", blockRange.Start, "first", blockRange.FirstKnown, "last", blockRange.LastKnown, "old hash", ethTokensBlockRange.balanceCheckHash)
log.Debug("update tokens blocks range",
"first", blockRange.FirstKnown,
"last", blockRange.LastKnown)
update, err := b.db.Prepare(`UPDATE blocks_ranges_sequential SET token_blk_start = ?, token_blk_first = ?, token_blk_last = ? WHERE network_id = ? AND address = ?`)
if err != nil {
@@ -163,31 +253,26 @@ func (b *BlockRangeSequentialDAO) updateTokenRange(chainID uint64, account commo
}
func prepareUpdatedBlockRange(blockRange, newBlockRange *BlockRange) *BlockRange {
// Update existing range
if blockRange != nil {
if newBlockRange != nil {
// Ovewrite start block if there was not any or if new one is older, because it can be precised only
// to a greater value, because no history can be before some block that is considered
// as a start of history, but due to concurrent block range checks, a newer greater block
// can be found that matches criteria of a start block (nonce is zero, balances are equal)
if newBlockRange.Start != nil && (blockRange.Start == nil || blockRange.Start.Cmp(newBlockRange.Start) < 0) {
blockRange.Start = newBlockRange.Start
}
// Overwrite first known block if there was not any or if new one is older
if (blockRange.FirstKnown == nil && newBlockRange.FirstKnown != nil) ||
(blockRange.FirstKnown != nil && newBlockRange.FirstKnown != nil && blockRange.FirstKnown.Cmp(newBlockRange.FirstKnown) > 0) {
blockRange.FirstKnown = newBlockRange.FirstKnown
}
// Overwrite last known block if there was not any or if new one is newer
if (blockRange.LastKnown == nil && newBlockRange.LastKnown != nil) ||
(blockRange.LastKnown != nil && newBlockRange.LastKnown != nil && blockRange.LastKnown.Cmp(newBlockRange.LastKnown) < 0) {
blockRange.LastKnown = newBlockRange.LastKnown
}
if newBlockRange != nil {
// Ovewrite start block if there was not any or if new one is older, because it can be precised only
// to a greater value, because no history can be before some block that is considered
// as a start of history, but due to concurrent block range checks, a newer greater block
// can be found that matches criteria of a start block (nonce is zero, balances are equal)
if newBlockRange.Start != nil && (blockRange.Start == nil || blockRange.Start.Cmp(newBlockRange.Start) < 0) {
blockRange.Start = newBlockRange.Start
}
// Overwrite first known block if there was not any or if new one is older
if (blockRange.FirstKnown == nil && newBlockRange.FirstKnown != nil) ||
(blockRange.FirstKnown != nil && newBlockRange.FirstKnown != nil && blockRange.FirstKnown.Cmp(newBlockRange.FirstKnown) > 0) {
blockRange.FirstKnown = newBlockRange.FirstKnown
}
// Overwrite last known block if there was not any or if new one is newer
if (blockRange.LastKnown == nil && newBlockRange.LastKnown != nil) ||
(blockRange.LastKnown != nil && newBlockRange.LastKnown != nil && blockRange.LastKnown.Cmp(newBlockRange.LastKnown) < 0) {
blockRange.LastKnown = newBlockRange.LastKnown
}
} else {
blockRange = newBlockRange
}
return blockRange

View File

@@ -322,6 +322,20 @@ func (c *transfersCommand) saveAndConfirmPending(allTransfers []Transfer, blockN
return resErr
}
func externalTransactionOrError(err error, mTID int64) bool {
if err == sql.ErrNoRows {
// External transaction downloaded, ignore it
return true
} else if err != nil {
log.Warn("GetOwnedMultiTransactionID", "error", err)
return true
} else if mTID <= 0 {
// Existing external transaction, ignore it
return true
}
return false
}
func (c *transfersCommand) confirmPendingTransactions(tx *sql.Tx, allTransfers []Transfer) (notifyFunctions []func()) {
notifyFunctions = make([]func(), 0)
@@ -335,16 +349,11 @@ func (c *transfersCommand) confirmPendingTransactions(tx *sql.Tx, allTransfers [
continue
} else {
// Outside transaction, already confirmed by another duplicate or not yet downloaded
existingMTID, err := GetOwnedMultiTransactionID(tx, chainID, tr.ID, tr.Address)
if err == sql.ErrNoRows || existingMTID == 0 {
// Outside transaction, ignore it
continue
} else if err != nil {
log.Warn("GetOwnedMultiTransactionID", "error", err)
existingMTID, err := GetOwnedMultiTransactionID(tx, chainID, txHash, tr.Address)
if externalTransactionOrError(err, existingMTID) {
continue
}
mTID = w_common.NewAndSet(existingMTID)
}
} else if err != nil {
log.Warn("GetOwnedPendingStatus", "error", err)
@@ -352,7 +361,7 @@ func (c *transfersCommand) confirmPendingTransactions(tx *sql.Tx, allTransfers [
}
if mTID != nil {
allTransfers[i].MultiTransactionID = MultiTransactionIDType(*mTID)
allTransfers[i].MultiTransactionID = w_common.MultiTransactionIDType(*mTID)
}
if txType != nil && *txType == transactions.WalletTransfer {
notify, err := c.pendingTxManager.DeleteBySQLTx(tx, chainID, txHash)
@@ -366,7 +375,7 @@ func (c *transfersCommand) confirmPendingTransactions(tx *sql.Tx, allTransfers [
}
// Mark all subTxs of a given Tx with the same multiTxID
func setMultiTxID(tx Transaction, multiTxID MultiTransactionIDType) {
func setMultiTxID(tx Transaction, multiTxID w_common.MultiTransactionIDType) {
for _, subTx := range tx {
subTx.MultiTransactionID = multiTxID
}
@@ -378,11 +387,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)
}
}
@@ -422,7 +431,7 @@ func (c *transfersCommand) checkAndProcessBridgeMultiTx(ctx context.Context, tx
}
if multiTransaction != nil {
setMultiTxID(tx, MultiTransactionIDType(multiTransaction.ID))
setMultiTxID(tx, multiTransaction.ID)
c.markMultiTxTokensAsPreviouslyOwned(ctx, multiTransaction, subTx.Address)
return true, nil
}
@@ -439,11 +448,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)
}
}
}

View File

@@ -67,7 +67,7 @@ func (c *findNewBlocksCommand) detectTransfers(parent context.Context, accounts
tokenAddresses = append(tokenAddresses, token.Address)
}
}
log.Info("findNewBlocksCommand detectTransfers", "cnt", len(tokenAddresses), "addresses", tokenAddresses)
log.Debug("findNewBlocksCommand detectTransfers", "cnt", len(tokenAddresses))
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
@@ -79,22 +79,12 @@ func (c *findNewBlocksCommand) detectTransfers(parent context.Context, accounts
addressesToCheck := []common.Address{}
for idx, account := range accounts {
blockRange, err := c.blockRangeDAO.getBlockRange(c.chainClient.NetworkID(), account)
blockRange, _, err := c.blockRangeDAO.getBlockRange(c.chainClient.NetworkID(), account)
if err != nil {
log.Error("findNewBlocksCommand can't block range", "error", err, "account", account, "chain", c.chainClient.NetworkID())
log.Error("findNewBlocksCommand can't get block range", "error", err, "account", account, "chain", c.chainClient.NetworkID())
return nil, nil, err
}
if blockRange.eth == nil {
blockRange.eth = NewBlockRange()
blockRange.tokens = NewBlockRange()
}
if blockRange.eth.FirstKnown == nil {
blockRange.eth.FirstKnown = blockNum
}
if blockRange.eth.LastKnown == nil {
blockRange.eth.LastKnown = blockNum
}
checkHash := common.BytesToHash(hashes[idx][:])
log.Debug("findNewBlocksCommand comparing hashes", "account", account, "network", c.chainClient.NetworkID(), "old hash", blockRange.balanceCheckHash, "new hash", checkHash.String())
if checkHash.String() != blockRange.balanceCheckHash {
@@ -118,7 +108,7 @@ func (c *findNewBlocksCommand) detectNonceChange(parent context.Context, to *big
for _, account := range accounts {
var oldNonce *int64
blockRange, err := c.blockRangeDAO.getBlockRange(c.chainClient.NetworkID(), account)
blockRange, _, err := c.blockRangeDAO.getBlockRange(c.chainClient.NetworkID(), account)
if err != nil {
log.Error("findNewBlocksCommand can't get block range", "error", err, "account", account, "chain", c.chainClient.NetworkID())
return nil, err
@@ -126,12 +116,16 @@ func (c *findNewBlocksCommand) detectNonceChange(parent context.Context, to *big
lastNonceInfo, ok := c.lastNonces[account]
if !ok || lastNonceInfo.blockNumber.Cmp(blockRange.eth.LastKnown) != 0 {
log.Info("Fetching old nonce", "at", blockRange.eth.LastKnown, "acc", account)
oldNonce, err = c.balanceCacher.NonceAt(parent, c.chainClient, account, blockRange.eth.LastKnown)
if err != nil {
log.Error("findNewBlocksCommand can't get nonce", "error", err, "account", account, "chain", c.chainClient.NetworkID())
return nil, err
log.Debug("Fetching old nonce", "at", blockRange.eth.LastKnown, "acc", account)
if blockRange.eth.LastKnown == nil {
blockRange.eth.LastKnown = big.NewInt(0)
oldNonce = new(int64) // At 0 block nonce is 0
} else {
oldNonce, err = c.balanceCacher.NonceAt(parent, c.chainClient, account, blockRange.eth.LastKnown)
if err != nil {
log.Error("findNewBlocksCommand can't get nonce", "error", err, "account", account, "chain", c.chainClient.NetworkID())
return nil, err
}
}
} else {
oldNonce = lastNonceInfo.nonce
@@ -143,7 +137,7 @@ func (c *findNewBlocksCommand) detectNonceChange(parent context.Context, to *big
return nil, err
}
log.Info("Comparing nonces", "oldNonce", *oldNonce, "newNonce", *newNonce, "to", to, "acc", account)
log.Debug("Comparing nonces", "oldNonce", *oldNonce, "newNonce", *newNonce, "to", to, "acc", account)
if *newNonce != *oldNonce {
addressesWithChange[account] = blockRange.eth.LastKnown
@@ -207,7 +201,7 @@ func (c *findNewBlocksCommand) Run(parent context.Context) error {
c.blockChainState.SetLastBlockNumber(c.chainClient.NetworkID(), headNum.Uint64())
if len(accountsWithDetectedChanges) != 0 {
log.Debug("findNewBlocksCommand detected accounts with changes, proceeding", "accounts", accountsWithDetectedChanges)
log.Debug("findNewBlocksCommand detected accounts with changes, proceeding", "accounts", accountsWithDetectedChanges, "from", c.fromBlockNumber)
err = c.findAndSaveEthBlocks(parent, c.fromBlockNumber, headNum, accountsToCheck)
if err != nil {
return err
@@ -241,10 +235,15 @@ func (c *findNewBlocksCommand) Run(parent context.Context) error {
}
if len(accountsWithDetectedChanges) != 0 || c.iteration%c.logsCheckIntervalIterations == 0 {
err = c.findAndSaveTokenBlocks(parent, c.fromBlockNumber, headNum)
from := c.fromBlockNumber
if c.logsCheckLastKnownBlock != nil {
from = c.logsCheckLastKnownBlock
}
err = c.findAndSaveTokenBlocks(parent, from, headNum)
if err != nil {
return err
}
c.logsCheckLastKnownBlock = headNum
}
c.fromBlockNumber = headNum
c.iteration++
@@ -335,11 +334,11 @@ func (c *findNewBlocksCommand) findAndSaveTokenBlocks(parent context.Context, fr
return c.markTokenBlockRangeChecked(c.accounts, fromNum, headNum)
}
func (c *findNewBlocksCommand) markTokenBlockRangeChecked(accounts []common.Address, from, to *big.Int) error {
func (c *findBlocksCommand) markTokenBlockRangeChecked(accounts []common.Address, from, to *big.Int) error {
log.Debug("markTokenBlockRangeChecked", "chain", c.chainClient.NetworkID(), "from", from.Uint64(), "to", to.Uint64())
for _, account := range accounts {
err := c.blockRangeDAO.updateTokenRange(c.chainClient.NetworkID(), account, &BlockRange{LastKnown: to})
err := c.blockRangeDAO.updateTokenRange(c.chainClient.NetworkID(), account, &BlockRange{FirstKnown: from, LastKnown: to})
if err != nil {
log.Error("findNewBlocksCommand upsertTokenRange", "error", err)
return err
@@ -435,6 +434,7 @@ type findBlocksCommand struct {
transactionManager *TransactionManager
tokenManager *token.Manager
fromBlockNumber *big.Int
logsCheckLastKnownBlock *big.Int
toBlockNumber *big.Int
blocksLoadedCh chan<- []*DBHeader
defaultNodeBlockChunkSize int
@@ -529,7 +529,7 @@ func (c *findBlocksCommand) ERC20ScanByBalance(parent context.Context, account c
}
func (c *findBlocksCommand) checkERC20Tail(parent context.Context, account common.Address) ([]*DBHeader, error) {
log.Info("checkERC20Tail", "account", account, "to block", c.startBlockNumber, "from", c.resFromBlock.Number)
log.Debug("checkERC20Tail", "account", account, "to block", c.startBlockNumber, "from", c.resFromBlock.Number)
tokens, err := c.tokenManager.GetTokens(c.chainClient.NetworkID())
if err != nil {
return nil, err
@@ -650,6 +650,10 @@ func (c *findBlocksCommand) Run(parent context.Context) (err error) {
}
if c.reachedETHHistoryStart {
err = c.markTokenBlockRangeChecked([]common.Address{account}, big.NewInt(0), to)
if err != nil {
break
}
log.Debug("findBlocksCommand reached first ETH transfer and checked erc20 tail", "chain", c.chainClient.NetworkID(), "account", account)
break
}
@@ -659,6 +663,11 @@ func (c *findBlocksCommand) Run(parent context.Context) (err error) {
break
}
err = c.markTokenBlockRangeChecked([]common.Address{account}, c.resFromBlock.Number, to)
if err != nil {
break
}
// if we have found first ETH block and we have not reached the start of ETH history yet
if c.startBlockNumber != nil && c.fromBlockNumber.Cmp(from) == -1 {
log.Debug("ERC20 tail should be checked", "initial from", c.fromBlockNumber, "actual from", from, "first ETH block", c.startBlockNumber)
@@ -746,7 +755,7 @@ func (c *findBlocksCommand) checkRange(parent context.Context, from *big.Int, to
func loadBlockRangeInfo(chainID uint64, account common.Address, blockDAO BlockRangeDAOer) (
*ethTokensBlockRanges, error) {
blockRange, err := blockDAO.getBlockRange(chainID, account)
blockRange, _, err := blockDAO.getBlockRange(chainID, account)
if err != nil {
log.Error("failed to load block ranges from database", "chain", chainID, "account", account,
"error", err)
@@ -759,8 +768,9 @@ func loadBlockRangeInfo(chainID uint64, account common.Address, blockDAO BlockRa
// Returns if all blocks are loaded, which means that start block (beginning of account history)
// has been found and all block headers saved to the DB
func areAllHistoryBlocksLoaded(blockInfo *BlockRange) bool {
if blockInfo != nil && blockInfo.FirstKnown != nil && blockInfo.Start != nil &&
blockInfo.Start.Cmp(blockInfo.FirstKnown) >= 0 {
if blockInfo != nil && blockInfo.FirstKnown != nil &&
((blockInfo.Start != nil && blockInfo.Start.Cmp(blockInfo.FirstKnown) >= 0) ||
blockInfo.FirstKnown.Cmp(zero) == 0) {
return true
}
@@ -770,7 +780,7 @@ func areAllHistoryBlocksLoaded(blockInfo *BlockRange) bool {
func areAllHistoryBlocksLoadedForAddress(blockRangeDAO BlockRangeDAOer, chainID uint64,
address common.Address) (bool, error) {
blockRange, err := blockRangeDAO.getBlockRange(chainID, address)
blockRange, _, err := blockRangeDAO.getBlockRange(chainID, address)
if err != nil {
log.Error("findBlocksCommand getBlockRange", "error", err)
return false, err
@@ -964,12 +974,33 @@ func (c *loadBlocksAndTransfersCommand) Run(parent context.Context) (err error)
finiteGroup.Wait()
}()
fromNum := big.NewInt(0)
headNum, err := getHeadBlockNumber(ctx, c.chainClient)
blockRanges, err := c.blockRangeDAO.getBlockRanges(c.chainClient.NetworkID(), c.accounts)
if err != nil {
return err
}
firstScan := false
var headNum *big.Int
for _, address := range c.accounts {
blockRange, ok := blockRanges[address]
if !ok || blockRange.tokens.LastKnown == nil {
firstScan = true
break
}
if headNum == nil || blockRange.tokens.LastKnown.Cmp(headNum) < 0 {
headNum = blockRange.tokens.LastKnown
}
}
fromNum := big.NewInt(0)
if firstScan {
headNum, err = getHeadBlockNumber(ctx, c.chainClient)
if err != nil {
return err
}
}
// It will start loadTransfersCommand which will run until all transfers from DB are loaded or any one failed to load
err = c.startFetchingTransfersForLoadedBlocks(finiteGroup)
if err != nil {
@@ -1046,14 +1077,13 @@ func (c *loadBlocksAndTransfersCommand) fetchHistoryBlocksForAccount(group *asyn
}
ranges := [][]*big.Int{}
// There are 2 history intervals:
// 1) from 0 to FirstKnown
// 2) from LastKnown to `toNum`` (head)
// If we blockRange is nil, we need to load all blocks from `fromNum` to `toNum`
// As current implementation checks ETH first then tokens, tokens ranges maybe behind ETH ranges in
// cases when block searching was interrupted, so we use tokens ranges
if blockRange != nil && blockRange.tokens != nil {
if blockRange.tokens.LastKnown != nil || blockRange.tokens.FirstKnown != nil {
if blockRange.tokens.LastKnown != nil && toNum.Cmp(blockRange.tokens.LastKnown) > 0 {
ranges = append(ranges, []*big.Int{blockRange.tokens.LastKnown, toNum})
}
@@ -1083,6 +1113,7 @@ func (c *loadBlocksAndTransfersCommand) fetchHistoryBlocksForAccount(group *asyn
}
for _, rangeItem := range ranges {
log.Debug("range item", "r", rangeItem, "n", c.chainClient.NetworkID(), "a", account)
fbc := &findBlocksCommand{
accounts: []common.Address{account},
db: c.db,

View File

@@ -183,11 +183,11 @@ func checkRangesWithStartBlock(parent context.Context, client balance.Reader, ca
return err
}
// Obtain block hash from first transaction
firstTransaction, err := client.FullTransactionByBlockNumberAndIndex(ctx, to, 0)
blockHash, err := client.CallBlockHashByTransaction(ctx, to, 0)
if err != nil {
return err
}
c.PushHeader(toDBHeader(header, *firstTransaction.BlockHash, account))
c.PushHeader(toDBHeader(header, blockHash, account))
return nil
}
mid := new(big.Int).Add(from, to)

View File

@@ -150,8 +150,8 @@ func (c *Controller) startAccountWatcher(chainIDs []uint64) {
c.accWatcher = accountsevent.NewWatcher(c.accountsDB, c.accountFeed, func(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address) {
c.onAccountsChanged(changedAddresses, eventType, currentAddresses, chainIDs)
})
c.accWatcher.Start()
}
c.accWatcher.Start()
}
func (c *Controller) onAccountsChanged(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address, chainIDs []uint64) {

View File

@@ -444,7 +444,7 @@ type transferDBFields struct {
log *types.Log
transferType w_common.Type
baseGasFees string
multiTransactionID MultiTransactionIDType
multiTransactionID w_common.MultiTransactionIDType
receiptStatus *uint64
receiptType *uint8
txHash *common.Hash
@@ -492,7 +492,9 @@ func updateOrInsertTransfersDBFields(creator statementCreator, transfers []trans
log.Error("can't save transfer", "b-hash", t.blockHash, "b-n", t.blockNumber, "a", t.address, "h", t.id)
return err
}
}
for _, t := range transfers {
err = removeGasOnlyEthTransfer(creator, t)
if err != nil {
log.Error("can't remove gas only eth transfer", "b-hash", t.blockHash, "b-n", t.blockNumber, "a", t.address, "h", t.id, "err", err)
@@ -503,18 +505,40 @@ func updateOrInsertTransfersDBFields(creator statementCreator, transfers []trans
}
func removeGasOnlyEthTransfer(creator statementCreator, t transferDBFields) error {
if t.transferType != w_common.EthTransfer {
query, err := creator.Prepare(`DELETE FROM transfers WHERE tx_hash = ? AND address = ? AND network_id = ?
AND account_nonce = ? AND type = 'eth' AND amount_padded128hex = '00000000000000000000000000000000'`)
if t.transferType == w_common.EthTransfer {
countQuery, err := creator.Prepare(`SELECT COUNT(*) FROM transfers WHERE tx_hash = ?`)
if err != nil {
return err
}
defer countQuery.Close()
var count int
err = countQuery.QueryRow(t.txHash).Scan(&count)
if err != nil {
return err
}
_, err = query.Exec(t.txHash, t.address, t.chainID, t.txNonce)
if err != nil {
return err
// If there's only one (or none), return without deleting
if count <= 1 {
log.Debug("Only one or no transfer found with the same tx_hash, skipping deletion.")
return nil
}
}
query, err := creator.Prepare(`DELETE FROM transfers WHERE tx_hash = ? AND address = ? AND network_id = ? AND account_nonce = ? AND type = 'eth' AND amount_padded128hex = '00000000000000000000000000000000'`)
if err != nil {
return err
}
defer query.Close()
res, err := query.Exec(t.txHash, t.address, t.chainID, t.txNonce)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
log.Debug("removeGasOnlyEthTransfer row deleted ", count)
return nil
}
@@ -538,8 +562,8 @@ func markBlocksAsLoaded(chainID uint64, creator statementCreator, address common
}
// GetOwnedMultiTransactionID returns sql.ErrNoRows if no transaction is found for the given identity
func GetOwnedMultiTransactionID(tx *sql.Tx, chainID w_common.ChainID, id common.Hash, address common.Address) (mTID int64, err error) {
row := tx.QueryRow(`SELECT COALESCE(multi_transaction_id, 0) FROM transfers WHERE network_id = ? AND hash = ? AND address = ?`, chainID, id, address)
func GetOwnedMultiTransactionID(tx *sql.Tx, chainID w_common.ChainID, hash common.Hash, address common.Address) (mTID int64, err error) {
row := tx.QueryRow(`SELECT COALESCE(multi_transaction_id, 0) FROM transfers WHERE network_id = ? AND tx_hash = ? AND address = ?`, chainID, hash, address)
err = row.Scan(&mTID)
if err != nil {
return 0, err

View File

@@ -63,7 +63,7 @@ type Transfer struct {
TokenValue *big.Int `json:"tokenValue"`
BaseGasFees string
// Internal field that is used to track multi-transaction transfers.
MultiTransactionID MultiTransactionIDType `json:"multi_transaction_id"`
MultiTransactionID w_common.MultiTransactionIDType `json:"multi_transaction_id"`
}
// ETHDownloader downloads regular eth transfers and tokens transfers.
@@ -109,7 +109,7 @@ func getTransferByHash(ctx context.Context, client chain.ClientInterface, signer
return nil, err
}
baseGasFee, err := client.GetBaseFeeFromBlock(big.NewInt(int64(transactionLog.BlockNumber)))
baseGasFee, err := client.GetBaseFeeFromBlock(ctx, big.NewInt(int64(transactionLog.BlockNumber)))
if err != nil {
return nil, err
}
@@ -288,7 +288,7 @@ func (d *ETHDownloader) getTransfersInBlock(ctx context.Context, blk *types.Bloc
Receipt: receipt,
Log: nil,
BaseGasFees: blk.BaseFee().String(),
MultiTransactionID: NoMultiTransactionID})
MultiTransactionID: w_common.NoMultiTransactionID})
}
}
}
@@ -416,7 +416,7 @@ func (d *ETHDownloader) subTransactionsFromPreloaded(preloadedTx *PreloadedTrans
Transaction: tx,
Receipt: receipt,
Timestamp: blk.Time(),
MultiTransactionID: NoMultiTransactionID,
MultiTransactionID: w_common.NoMultiTransactionID,
}
rst = append(rst, transfer)
@@ -451,7 +451,7 @@ func (d *ETHDownloader) subTransactionsFromTransactionData(address, from common.
Transaction: tx,
Receipt: receipt,
Timestamp: blk.Time(),
MultiTransactionID: NoMultiTransactionID,
MultiTransactionID: w_common.NoMultiTransactionID,
}
rst = append(rst, transfer)

View File

@@ -27,7 +27,7 @@ type TestTransaction struct {
Success bool
Nonce uint64
Contract eth_common.Address
MultiTransactionID MultiTransactionIDType
MultiTransactionID common.MultiTransactionIDType
}
type TestTransfer struct {
@@ -38,7 +38,7 @@ type TestTransfer struct {
}
type TestMultiTransaction struct {
MultiTransactionID MultiTransactionIDType
MultiTransactionID common.MultiTransactionIDType
MultiTransactionType MultiTransactionType
FromAddress eth_common.Address
ToAddress eth_common.Address
@@ -78,7 +78,7 @@ func generateTestTransaction(seed int) TestTransaction {
Nonce: uint64(seed),
// In practice this is last20Bytes(Keccak256(RLP(From, nonce)))
Contract: eth_common.HexToAddress(fmt.Sprintf("0x4%d", seed)),
MultiTransactionID: NoMultiTransactionID,
MultiTransactionID: common.NoMultiTransactionID,
}
}
@@ -161,6 +161,11 @@ var TestCollectibles = []TestCollectible{
TokenID: big.NewInt(1),
ChainID: 1,
},
TestCollectible{ // TokenID (big.Int) value 0 might be problematic if not handled properly
TokenAddress: eth_common.HexToAddress("0x97a04fda4d97c6e3547d66b572e29f4a4ff4ABCD"),
TokenID: big.NewInt(0),
ChainID: 420,
},
TestCollectible{
TokenAddress: eth_common.HexToAddress("0x1dea7a3e04849840c0eb15fd26a55f6c40c4a69b"),
TokenID: big.NewInt(11),
@@ -362,7 +367,7 @@ func InsertTestPendingTransaction(tb testing.TB, db *sql.DB, tr *TestTransfer) {
require.NoError(tb, err)
}
func InsertTestMultiTransaction(tb testing.TB, db *sql.DB, tr *TestMultiTransaction) MultiTransactionIDType {
func InsertTestMultiTransaction(tb testing.TB, db *sql.DB, tr *TestMultiTransaction) common.MultiTransactionIDType {
fromTokenType := tr.FromToken
if tr.FromToken == "" {
fromTokenType = testutils.EthSymbol
@@ -381,7 +386,7 @@ func InsertTestMultiTransaction(tb testing.TB, db *sql.DB, tr *TestMultiTransact
require.NoError(tb, err)
rowID, err := result.LastInsertId()
require.NoError(tb, err)
tr.MultiTransactionID = MultiTransactionIDType(rowID)
tr.MultiTransactionID = common.MultiTransactionIDType(rowID)
return tr.MultiTransactionID
}

View File

@@ -21,11 +21,7 @@ import (
"github.com/status-im/status-go/transactions"
)
type MultiTransactionIDType int64
const (
NoMultiTransactionID = MultiTransactionIDType(0)
// EventMTTransactionUpdate is emitted when a multi-transaction is updated (added or deleted)
EventMTTransactionUpdate walletevent.EventType = "multi-transaction-update"
)
@@ -38,6 +34,7 @@ type SignatureDetails struct {
type TransactionDescription struct {
chainID uint64
from common.Address
builtTx *ethTypes.Transaction
signature []byte
}
@@ -89,19 +86,19 @@ const (
)
type MultiTransaction struct {
ID uint `json:"id"`
Timestamp uint64 `json:"timestamp"`
FromNetworkID uint64 `json:"fromNetworkID"`
ToNetworkID uint64 `json:"toNetworkID"`
FromTxHash common.Hash `json:"fromTxHash"`
ToTxHash common.Hash `json:"toTxHash"`
FromAddress common.Address `json:"fromAddress"`
ToAddress common.Address `json:"toAddress"`
FromAsset string `json:"fromAsset"`
ToAsset string `json:"toAsset"`
FromAmount *hexutil.Big `json:"fromAmount"`
ToAmount *hexutil.Big `json:"toAmount"`
Type MultiTransactionType `json:"type"`
ID wallet_common.MultiTransactionIDType `json:"id"`
Timestamp uint64 `json:"timestamp"`
FromNetworkID uint64 `json:"fromNetworkID"`
ToNetworkID uint64 `json:"toNetworkID"`
FromTxHash common.Hash `json:"fromTxHash"`
ToTxHash common.Hash `json:"toTxHash"`
FromAddress common.Address `json:"fromAddress"`
ToAddress common.Address `json:"toAddress"`
FromAsset string `json:"fromAsset"`
ToAsset string `json:"toAsset"`
FromAmount *hexutil.Big `json:"fromAmount"`
ToAmount *hexutil.Big `json:"toAmount"`
Type MultiTransactionType `json:"type"`
CrossTxID string
}
@@ -226,21 +223,5 @@ func (tm *TransactionManager) BuildRawTransaction(chainID uint64, sendArgs trans
}
func (tm *TransactionManager) SendTransactionWithSignature(chainID uint64, txType transactions.PendingTrxType, sendArgs transactions.SendTxArgs, signature []byte) (hash types.Hash, err error) {
hash, err = tm.transactor.BuildTransactionAndSendWithSignature(chainID, sendArgs, signature)
if err != nil {
return hash, err
}
err = tm.pendingTracker.TrackPendingTransaction(
wallet_common.ChainID(chainID),
common.Hash(hash),
common.Address(sendArgs.From),
txType,
transactions.AutoDelete,
)
if err != nil {
return hash, err
}
return hash, nil
return tm.transactor.BuildTransactionAndSendWithSignature(chainID, sendArgs, signature)
}

View File

@@ -19,7 +19,6 @@ import (
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/services/wallet/bridge"
wallet_common "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/walletevent"
@@ -90,11 +89,11 @@ func getMultiTransactionTimestamp(multiTransaction *MultiTransaction) uint64 {
}
// insertMultiTransaction inserts a multi transaction into the database and updates multi-transaction ID and timestamp
func insertMultiTransaction(db *sql.DB, multiTransaction *MultiTransaction) (MultiTransactionIDType, error) {
func insertMultiTransaction(db *sql.DB, multiTransaction *MultiTransaction) (wallet_common.MultiTransactionIDType, error) {
insert, err := db.Prepare(fmt.Sprintf(`INSERT INTO multi_transactions (%s)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, multiTransactionColumns))
if err != nil {
return NoMultiTransactionID, err
return wallet_common.NoMultiTransactionID, err
}
timestamp := getMultiTransactionTimestamp(multiTransaction)
result, err := insert.Exec(
@@ -113,22 +112,22 @@ func insertMultiTransaction(db *sql.DB, multiTransaction *MultiTransaction) (Mul
timestamp,
)
if err != nil {
return NoMultiTransactionID, err
return wallet_common.NoMultiTransactionID, err
}
defer insert.Close()
multiTransactionID, err := result.LastInsertId()
multiTransaction.Timestamp = timestamp
multiTransaction.ID = uint(multiTransactionID)
multiTransaction.ID = wallet_common.MultiTransactionIDType(multiTransactionID)
return MultiTransactionIDType(multiTransactionID), err
return wallet_common.MultiTransactionIDType(multiTransactionID), err
}
func (tm *TransactionManager) InsertMultiTransaction(multiTransaction *MultiTransaction) (MultiTransactionIDType, error) {
func (tm *TransactionManager) InsertMultiTransaction(multiTransaction *MultiTransaction) (wallet_common.MultiTransactionIDType, error) {
return tm.insertMultiTransactionAndNotify(tm.db, multiTransaction, nil)
}
func (tm *TransactionManager) insertMultiTransactionAndNotify(db *sql.DB, multiTransaction *MultiTransaction, chainIDs []uint64) (MultiTransactionIDType, error) {
func (tm *TransactionManager) insertMultiTransactionAndNotify(db *sql.DB, multiTransaction *MultiTransaction, chainIDs []uint64) (wallet_common.MultiTransactionIDType, error) {
id, err := insertMultiTransaction(db, multiTransaction)
if err != nil {
publishMultiTransactionUpdatedEvent(db, multiTransaction, tm.eventFeed, chainIDs)
@@ -156,7 +155,7 @@ func publishMultiTransactionUpdatedEvent(db *sql.DB, multiTransaction *MultiTran
}
func updateMultiTransaction(db *sql.DB, multiTransaction *MultiTransaction) error {
if MultiTransactionIDType(multiTransaction.ID) == NoMultiTransactionID {
if multiTransaction.ID == wallet_common.NoMultiTransactionID {
return fmt.Errorf("no multitransaction ID")
}
@@ -211,7 +210,7 @@ func (tm *TransactionManager) CreateMultiTransactionFromCommand(ctx context.Cont
return nil, err
}
multiTransaction.ID = uint(multiTransactionID)
multiTransaction.ID = multiTransactionID
if password == "" {
acc, err := tm.accountsDB.GetAccountByAddress(types.Address(multiTransaction.FromAddress))
if err != nil {
@@ -244,11 +243,6 @@ func (tm *TransactionManager) CreateMultiTransactionFromCommand(ctx context.Cont
return nil, err
}
err = tm.storePendingTransactions(multiTransaction, hashes, data)
if err != nil {
return nil, err
}
return &MultiTransactionCommandResult{
ID: int64(multiTransactionID),
Hashes: hashes,
@@ -292,64 +286,26 @@ func (tm *TransactionManager) ProceedWithTransactionsSignatures(ctx context.Cont
// send transactions
hashes := make(map[uint64][]types.Hash)
for _, desc := range tm.transactionsForKeycardSingning {
hash, err := tm.transactor.AddSignatureToTransactionAndSend(desc.chainID, desc.builtTx, desc.signature)
hash, err := tm.transactor.AddSignatureToTransactionAndSend(
desc.chainID,
desc.from,
tm.multiTransactionForKeycardSigning.FromAsset,
tm.multiTransactionForKeycardSigning.ID,
desc.builtTx,
desc.signature,
)
if err != nil {
return nil, err
}
hashes[desc.chainID] = append(hashes[desc.chainID], hash)
}
err := tm.storePendingTransactions(tm.multiTransactionForKeycardSigning, hashes, tm.transactionsBridgeData)
if err != nil {
return nil, err
}
return &MultiTransactionCommandResult{
ID: int64(tm.multiTransactionForKeycardSigning.ID),
Hashes: hashes,
}, nil
}
func (tm *TransactionManager) storePendingTransactions(multiTransaction *MultiTransaction,
hashes map[uint64][]types.Hash, data []*bridge.TransactionBridge) error {
txs := createPendingTransactions(hashes, data, multiTransaction)
for _, tx := range txs {
err := tm.pendingTracker.StoreAndTrackPendingTx(tx)
if err != nil {
return err
}
}
return nil
}
func createPendingTransactions(hashes map[uint64][]types.Hash, data []*bridge.TransactionBridge,
multiTransaction *MultiTransaction) []*transactions.PendingTransaction {
txs := make([]*transactions.PendingTransaction, 0)
for _, tx := range data {
for _, hash := range hashes[tx.ChainID] {
pendingTransaction := &transactions.PendingTransaction{
Hash: common.Hash(hash),
Timestamp: uint64(time.Now().Unix()),
Value: bigint.BigInt{Int: multiTransaction.FromAmount.ToInt()},
From: common.Address(tx.From()),
To: common.Address(tx.To()),
Data: tx.Data().String(),
Type: transactions.WalletTransfer,
ChainID: wallet_common.ChainID(tx.ChainID),
MultiTransactionID: int64(multiTransaction.ID),
Symbol: multiTransaction.FromAsset,
AutoDelete: new(bool),
}
// Transaction downloader will delete pending transaction as soon as it is confirmed
*pendingTransaction.AutoDelete = false
txs = append(txs, pendingTransaction)
}
}
return txs
}
func multiTransactionFromCommand(command *MultiTransactionCommand) *MultiTransaction {
log.Info("Creating multi transaction", "command", command)
@@ -380,6 +336,7 @@ func (tm *TransactionManager) buildTransactions(bridges map[string]bridge.Bridge
txHash := signer.Hash(builtTx)
tm.transactionsForKeycardSingning[txHash] = &TransactionDescription{
from: common.Address(bridgeTx.From()),
chainID: bridgeTx.ChainID,
builtTx: builtTx,
}
@@ -403,6 +360,27 @@ func (tm *TransactionManager) sendTransactions(multiTransaction *MultiTransactio
hashes := make(map[uint64][]types.Hash)
for _, tx := range data {
if tx.TransferTx != nil {
tx.TransferTx.MultiTransactionID = multiTransaction.ID
tx.TransferTx.Symbol = multiTransaction.FromAsset
}
if tx.HopTx != nil {
tx.HopTx.MultiTransactionID = multiTransaction.ID
tx.HopTx.Symbol = multiTransaction.FromAsset
}
if tx.CbridgeTx != nil {
tx.CbridgeTx.MultiTransactionID = multiTransaction.ID
tx.CbridgeTx.Symbol = multiTransaction.FromAsset
}
if tx.ERC721TransferTx != nil {
tx.ERC721TransferTx.MultiTransactionID = multiTransaction.ID
tx.ERC721TransferTx.Symbol = multiTransaction.FromAsset
}
if tx.ERC1155TransferTx != nil {
tx.ERC1155TransferTx.MultiTransactionID = multiTransaction.ID
tx.ERC1155TransferTx.Symbol = multiTransaction.FromAsset
}
hash, err := bridges[tx.BridgeName].Send(tx, selectedAccount)
if err != nil {
return nil, err
@@ -412,7 +390,7 @@ func (tm *TransactionManager) sendTransactions(multiTransaction *MultiTransactio
return hashes, nil
}
func (tm *TransactionManager) GetMultiTransactions(ctx context.Context, ids []MultiTransactionIDType) ([]*MultiTransaction, error) {
func (tm *TransactionManager) GetMultiTransactions(ctx context.Context, ids []wallet_common.MultiTransactionIDType) ([]*MultiTransaction, error) {
placeholders := make([]string, len(ids))
args := make([]interface{}, len(ids))
for i, v := range ids {

View File

@@ -1,6 +1,7 @@
package walletevent
import (
"encoding/json"
"math/big"
"strings"
@@ -31,3 +32,16 @@ type Event struct {
// For Internal events only, not serialized
EventParams interface{}
}
func GetPayload[T any](e Event) (*T, error) {
var payload T
err := json.Unmarshal([]byte(e.Message), &payload)
if err != nil {
return nil, err
}
return &payload, nil
}
func ExtractPayload[T any](e Event, payload *T) error {
return json.Unmarshal([]byte(e.Message), payload)
}