2023-06-08 23:52:45 +00:00
|
|
|
package activity
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
|
|
"github.com/ethereum/go-ethereum/event"
|
|
|
|
"github.com/ethereum/go-ethereum/log"
|
|
|
|
|
2023-07-05 13:59:39 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/async"
|
2023-06-08 23:52:45 +00:00
|
|
|
w_common "github.com/status-im/status-go/services/wallet/common"
|
2023-06-13 09:25:23 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/token"
|
2023-06-08 23:52:45 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/walletevent"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// FilterResponse json is sent as a message in the EventActivityFilteringDone event
|
2023-06-22 11:28:35 +00:00
|
|
|
EventActivityFilteringDone walletevent.EventType = "wallet-activity-filtering-done"
|
|
|
|
EventActivityGetRecipientsDone walletevent.EventType = "wallet-activity-get-recipients-result"
|
|
|
|
EventActivityGetOldestTimestampDone walletevent.EventType = "wallet-activity-get-oldest-timestamp-result"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2023-07-05 13:59:39 +00:00
|
|
|
filterTask = async.TaskType{
|
2023-06-22 11:28:35 +00:00
|
|
|
ID: 1,
|
2023-07-05 13:59:39 +00:00
|
|
|
Policy: async.ReplacementPolicyCancelOld,
|
2023-06-22 11:28:35 +00:00
|
|
|
}
|
2023-07-05 13:59:39 +00:00
|
|
|
getRecipientsTask = async.TaskType{
|
2023-06-22 11:28:35 +00:00
|
|
|
ID: 2,
|
2023-07-05 13:59:39 +00:00
|
|
|
Policy: async.ReplacementPolicyIgnoreNew,
|
2023-06-22 11:28:35 +00:00
|
|
|
}
|
2023-07-05 13:59:39 +00:00
|
|
|
getOldestTimestampTask = async.TaskType{
|
2023-06-22 11:28:35 +00:00
|
|
|
ID: 3,
|
2023-07-05 13:59:39 +00:00
|
|
|
Policy: async.ReplacementPolicyCancelOld,
|
2023-06-22 11:28:35 +00:00
|
|
|
}
|
2023-06-08 23:52:45 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Service struct {
|
2023-06-13 09:25:23 +00:00
|
|
|
db *sql.DB
|
|
|
|
tokenManager *token.Manager
|
|
|
|
eventFeed *event.Feed
|
|
|
|
|
2023-07-05 13:59:39 +00:00
|
|
|
scheduler *async.Scheduler
|
2023-06-08 23:52:45 +00:00
|
|
|
}
|
|
|
|
|
2023-06-13 09:25:23 +00:00
|
|
|
func NewService(db *sql.DB, tokenManager *token.Manager, eventFeed *event.Feed) *Service {
|
2023-06-08 23:52:45 +00:00
|
|
|
return &Service{
|
2023-06-13 09:25:23 +00:00
|
|
|
db: db,
|
|
|
|
tokenManager: tokenManager,
|
|
|
|
eventFeed: eventFeed,
|
2023-07-05 13:59:39 +00:00
|
|
|
scheduler: async.NewScheduler(),
|
2023-06-08 23:52:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type ErrorCode = int
|
|
|
|
|
|
|
|
const (
|
|
|
|
ErrorCodeSuccess ErrorCode = iota + 1
|
2023-06-22 11:28:35 +00:00
|
|
|
ErrorCodeTaskCanceled
|
|
|
|
ErrorCodeFailed
|
2023-06-08 23:52:45 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type FilterResponse struct {
|
2023-06-11 14:22:25 +00:00
|
|
|
Activities []Entry `json:"activities"`
|
|
|
|
Offset int `json:"offset"`
|
|
|
|
// Used to indicate that there might be more entries that were not returned
|
|
|
|
// based on a simple heuristic
|
|
|
|
HasMore bool `json:"hasMore"`
|
|
|
|
ErrorCode ErrorCode `json:"errorCode"`
|
2023-06-08 23:52:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// FilterActivityAsync allows only one filter task to run at a time
|
|
|
|
// and it cancels the current one if a new one is started
|
|
|
|
// All calls will trigger an EventActivityFilteringDone event with the result of the filtering
|
2023-06-22 11:28:35 +00:00
|
|
|
func (s *Service) FilterActivityAsync(ctx context.Context, addresses []common.Address, chainIDs []w_common.ChainID, filter Filter, offset int, limit int) {
|
|
|
|
s.scheduler.Enqueue(filterTask, func(ctx context.Context) (interface{}, error) {
|
|
|
|
activities, err := getActivityEntries(ctx, s.getDeps(), addresses, chainIDs, filter, offset, limit)
|
|
|
|
return activities, err
|
2023-07-05 13:59:39 +00:00
|
|
|
}, func(result interface{}, taskType async.TaskType, err error) {
|
2023-06-22 11:28:35 +00:00
|
|
|
res := FilterResponse{
|
|
|
|
ErrorCode: ErrorCodeFailed,
|
|
|
|
}
|
2023-06-08 23:52:45 +00:00
|
|
|
|
2023-07-05 13:59:39 +00:00
|
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, async.ErrTaskOverwritten) {
|
2023-06-22 11:28:35 +00:00
|
|
|
res.ErrorCode = ErrorCodeTaskCanceled
|
|
|
|
} else if err == nil {
|
|
|
|
activities := result.([]Entry)
|
|
|
|
res.Activities = activities
|
|
|
|
res.Offset = offset
|
|
|
|
res.HasMore = len(activities) == limit
|
|
|
|
res.ErrorCode = ErrorCodeSuccess
|
|
|
|
}
|
|
|
|
|
2023-07-04 12:01:45 +00:00
|
|
|
s.sendResponseEvent(EventActivityFilteringDone, res, err)
|
2023-06-22 11:28:35 +00:00
|
|
|
})
|
|
|
|
}
|
2023-06-08 23:52:45 +00:00
|
|
|
|
2023-06-22 11:28:35 +00:00
|
|
|
type GetRecipientsResponse struct {
|
|
|
|
Addresses []common.Address `json:"addresses"`
|
|
|
|
Offset int `json:"offset"`
|
|
|
|
// Used to indicate that there might be more entries that were not returned
|
|
|
|
// based on a simple heuristic
|
|
|
|
HasMore bool `json:"hasMore"`
|
|
|
|
ErrorCode ErrorCode `json:"errorCode"`
|
|
|
|
}
|
2023-06-08 23:52:45 +00:00
|
|
|
|
2023-06-22 11:28:35 +00:00
|
|
|
// GetRecipientsAsync returns true if a task is already running or scheduled due to a previous call; meaning that
|
|
|
|
// this call won't receive an answer but client should rely on the answer from the previous call.
|
|
|
|
// If no task is already scheduled false will be returned
|
|
|
|
func (s *Service) GetRecipientsAsync(ctx context.Context, offset int, limit int) bool {
|
|
|
|
return s.scheduler.Enqueue(getRecipientsTask, func(ctx context.Context) (interface{}, error) {
|
|
|
|
var err error
|
|
|
|
result := &GetRecipientsResponse{
|
|
|
|
Offset: offset,
|
|
|
|
ErrorCode: ErrorCodeSuccess,
|
|
|
|
}
|
|
|
|
result.Addresses, result.HasMore, err = GetRecipients(ctx, s.db, offset, limit)
|
|
|
|
return result, err
|
2023-07-05 13:59:39 +00:00
|
|
|
}, func(result interface{}, taskType async.TaskType, err error) {
|
2023-06-22 11:28:35 +00:00
|
|
|
res := result.(*GetRecipientsResponse)
|
2023-07-05 13:59:39 +00:00
|
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, async.ErrTaskOverwritten) {
|
2023-06-22 11:28:35 +00:00
|
|
|
res.ErrorCode = ErrorCodeTaskCanceled
|
|
|
|
} else if err != nil {
|
|
|
|
res.ErrorCode = ErrorCodeFailed
|
|
|
|
}
|
2023-06-11 14:22:25 +00:00
|
|
|
|
2023-07-04 12:01:45 +00:00
|
|
|
s.sendResponseEvent(EventActivityGetRecipientsDone, result, err)
|
2023-06-22 11:28:35 +00:00
|
|
|
})
|
|
|
|
}
|
2023-06-08 23:52:45 +00:00
|
|
|
|
2023-06-22 11:28:35 +00:00
|
|
|
type GetOldestTimestampResponse struct {
|
|
|
|
Timestamp int64 `json:"timestamp"`
|
|
|
|
ErrorCode ErrorCode `json:"errorCode"`
|
|
|
|
}
|
2023-06-08 23:52:45 +00:00
|
|
|
|
2023-06-22 11:28:35 +00:00
|
|
|
func (s *Service) GetOldestTimestampAsync(ctx context.Context, addresses []common.Address) {
|
|
|
|
s.scheduler.Enqueue(getOldestTimestampTask, func(ctx context.Context) (interface{}, error) {
|
|
|
|
timestamp, err := GetOldestTimestamp(ctx, s.db, addresses)
|
|
|
|
return timestamp, err
|
2023-07-05 13:59:39 +00:00
|
|
|
}, func(result interface{}, taskType async.TaskType, err error) {
|
2023-06-22 11:28:35 +00:00
|
|
|
res := GetOldestTimestampResponse{
|
|
|
|
ErrorCode: ErrorCodeFailed,
|
2023-06-08 23:52:45 +00:00
|
|
|
}
|
|
|
|
|
2023-07-05 13:59:39 +00:00
|
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, async.ErrTaskOverwritten) {
|
2023-06-22 11:28:35 +00:00
|
|
|
res.ErrorCode = ErrorCodeTaskCanceled
|
2023-06-08 23:52:45 +00:00
|
|
|
} else if err == nil {
|
2023-06-22 11:28:35 +00:00
|
|
|
res.Timestamp = result.(int64)
|
2023-06-08 23:52:45 +00:00
|
|
|
res.ErrorCode = ErrorCodeSuccess
|
|
|
|
}
|
|
|
|
|
2023-07-04 12:01:45 +00:00
|
|
|
s.sendResponseEvent(EventActivityGetOldestTimestampDone, res, err)
|
2023-06-22 11:28:35 +00:00
|
|
|
})
|
2023-06-08 23:52:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) Stop() {
|
2023-06-22 11:28:35 +00:00
|
|
|
s.scheduler.Stop()
|
2023-06-08 23:52:45 +00:00
|
|
|
}
|
|
|
|
|
2023-06-13 09:25:23 +00:00
|
|
|
func (s *Service) getDeps() FilterDependencies {
|
|
|
|
return FilterDependencies{
|
|
|
|
db: s.db,
|
|
|
|
tokenSymbol: func(t Token) string {
|
|
|
|
info := s.tokenManager.LookupTokenIdentity(uint64(t.ChainID), t.Address, t.TokenType == Native)
|
|
|
|
if info == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return info.Symbol
|
|
|
|
},
|
|
|
|
tokenFromSymbol: func(chainID *w_common.ChainID, symbol string) *Token {
|
|
|
|
var cID *uint64
|
|
|
|
if chainID != nil {
|
|
|
|
cID = new(uint64)
|
|
|
|
*cID = uint64(*chainID)
|
|
|
|
}
|
|
|
|
t, detectedNative := s.tokenManager.LookupToken(cID, symbol)
|
|
|
|
if t == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
tokenType := Native
|
|
|
|
if !detectedNative {
|
|
|
|
tokenType = Erc20
|
|
|
|
}
|
|
|
|
return &Token{
|
|
|
|
TokenType: tokenType,
|
|
|
|
ChainID: w_common.ChainID(t.ChainID),
|
|
|
|
Address: t.Address,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-04 12:01:45 +00:00
|
|
|
func (s *Service) sendResponseEvent(eventType walletevent.EventType, payloadObj interface{}, resErr error) {
|
2023-06-22 11:28:35 +00:00
|
|
|
payload, err := json.Marshal(payloadObj)
|
2023-06-08 23:52:45 +00:00
|
|
|
if err != nil {
|
2023-07-04 12:01:45 +00:00
|
|
|
log.Error("Error marshaling response: %v; result error: %w", err, resErr)
|
|
|
|
} else {
|
|
|
|
err = resErr
|
2023-06-08 23:52:45 +00:00
|
|
|
}
|
|
|
|
|
2023-06-22 11:28:35 +00:00
|
|
|
log.Debug("wallet.api.activity.Service RESPONSE", "eventType", eventType, "error", err, "payload.len", len(payload))
|
2023-06-13 09:25:23 +00:00
|
|
|
|
2023-06-08 23:52:45 +00:00
|
|
|
s.eventFeed.Send(walletevent.Event{
|
2023-06-22 11:28:35 +00:00
|
|
|
Type: eventType,
|
2023-06-08 23:52:45 +00:00
|
|
|
Message: string(payload),
|
|
|
|
})
|
|
|
|
}
|