+31
@@ -0,0 +1,31 @@
|
||||
package localnotifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
func NewAPI(s *Service) *API {
|
||||
return &API{s}
|
||||
}
|
||||
|
||||
type API struct {
|
||||
s *Service
|
||||
}
|
||||
|
||||
func (api *API) NotificationPreferences(ctx context.Context) ([]NotificationPreference, error) {
|
||||
return api.s.db.GetPreferences()
|
||||
}
|
||||
|
||||
func (api *API) SwitchWalletNotifications(ctx context.Context, preference bool) error {
|
||||
log.Debug("Switch Transaction Notification")
|
||||
err := api.s.db.ChangeWalletPreference(preference)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
api.s.WatchingEnabled = preference
|
||||
|
||||
return nil
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
package localnotifications
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
"github.com/status-im/status-go/services/wallet/transfer"
|
||||
"github.com/status-im/status-go/signal"
|
||||
)
|
||||
|
||||
type PushCategory string
|
||||
|
||||
type NotificationType string
|
||||
|
||||
type NotificationBody interface {
|
||||
json.Marshaler
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID common.Hash
|
||||
Platform float32
|
||||
Body NotificationBody
|
||||
BodyType NotificationType
|
||||
Title string
|
||||
Message string
|
||||
Category PushCategory
|
||||
Deeplink string
|
||||
Image string
|
||||
IsScheduled bool
|
||||
ScheduledTime string
|
||||
IsConversation bool
|
||||
IsGroupConversation bool
|
||||
ConversationID string
|
||||
Timestamp uint64
|
||||
Author NotificationAuthor
|
||||
Deleted bool
|
||||
}
|
||||
|
||||
type NotificationAuthor struct {
|
||||
ID string `json:"id"`
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// notificationAlias is an interim struct used for json un/marshalling
|
||||
type notificationAlias struct {
|
||||
ID common.Hash `json:"id"`
|
||||
Platform float32 `json:"platform,omitempty"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
BodyType NotificationType `json:"bodyType"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Category PushCategory `json:"category,omitempty"`
|
||||
Deeplink string `json:"deepLink,omitempty"`
|
||||
Image string `json:"imageUrl,omitempty"`
|
||||
IsScheduled bool `json:"isScheduled,omitempty"`
|
||||
ScheduledTime string `json:"scheduleTime,omitempty"`
|
||||
IsConversation bool `json:"isConversation,omitempty"`
|
||||
IsGroupConversation bool `json:"isGroupConversation,omitempty"`
|
||||
ConversationID string `json:"conversationId,omitempty"`
|
||||
Timestamp uint64 `json:"timestamp,omitempty"`
|
||||
Author NotificationAuthor `json:"notificationAuthor,omitempty"`
|
||||
Deleted bool `json:"deleted,omitempty"`
|
||||
}
|
||||
|
||||
// MessageEvent - structure used to pass messages from chat to bus
|
||||
type MessageEvent struct{}
|
||||
|
||||
// CustomEvent - structure used to pass custom user set messages to bus
|
||||
type CustomEvent struct{}
|
||||
|
||||
type transmitter struct {
|
||||
publisher *event.Feed
|
||||
|
||||
wg sync.WaitGroup
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
// Service keeps the state of message bus
|
||||
type Service struct {
|
||||
started bool
|
||||
WatchingEnabled bool
|
||||
chainID uint64
|
||||
transmitter *transmitter
|
||||
walletTransmitter *transmitter
|
||||
db *Database
|
||||
walletDB *transfer.Database
|
||||
accountsDB *accounts.Database
|
||||
}
|
||||
|
||||
func NewService(appDB *sql.DB, walletDB *transfer.Database, chainID uint64) (*Service, error) {
|
||||
db := NewDB(appDB, chainID)
|
||||
accountsDB, err := accounts.NewDB(appDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trans := &transmitter{}
|
||||
walletTrans := &transmitter{}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
chainID: chainID,
|
||||
walletDB: walletDB,
|
||||
accountsDB: accountsDB,
|
||||
transmitter: trans,
|
||||
walletTransmitter: walletTrans,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (n *Notification) MarshalJSON() ([]byte, error) {
|
||||
|
||||
var body json.RawMessage
|
||||
if n.Body != nil {
|
||||
encodedBody, err := n.Body.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = encodedBody
|
||||
}
|
||||
|
||||
alias := notificationAlias{
|
||||
ID: n.ID,
|
||||
Platform: n.Platform,
|
||||
Body: body,
|
||||
BodyType: n.BodyType,
|
||||
Category: n.Category,
|
||||
Title: n.Title,
|
||||
Message: n.Message,
|
||||
Deeplink: n.Deeplink,
|
||||
Image: n.Image,
|
||||
IsScheduled: n.IsScheduled,
|
||||
ScheduledTime: n.ScheduledTime,
|
||||
IsConversation: n.IsConversation,
|
||||
IsGroupConversation: n.IsGroupConversation,
|
||||
ConversationID: n.ConversationID,
|
||||
Timestamp: n.Timestamp,
|
||||
Author: n.Author,
|
||||
Deleted: n.Deleted,
|
||||
}
|
||||
|
||||
return json.Marshal(alias)
|
||||
}
|
||||
|
||||
func PushMessages(ns []*Notification) {
|
||||
for _, n := range ns {
|
||||
pushMessage(n)
|
||||
}
|
||||
}
|
||||
|
||||
func pushMessage(notification *Notification) {
|
||||
log.Debug("Pushing a new push notification")
|
||||
signal.SendLocalNotifications(notification)
|
||||
}
|
||||
|
||||
// Start Worker which processes all incoming messages
|
||||
func (s *Service) Start() error {
|
||||
s.started = true
|
||||
|
||||
s.transmitter.quit = make(chan struct{})
|
||||
s.transmitter.publisher = &event.Feed{}
|
||||
|
||||
events := make(chan TransactionEvent, 10)
|
||||
sub := s.transmitter.publisher.Subscribe(events)
|
||||
|
||||
s.transmitter.wg.Add(1)
|
||||
go func() {
|
||||
defer s.transmitter.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-s.transmitter.quit:
|
||||
sub.Unsubscribe()
|
||||
return
|
||||
case err := <-sub.Err():
|
||||
if err != nil {
|
||||
log.Error("Local notifications transmitter failed with", "error", err)
|
||||
}
|
||||
return
|
||||
case event := <-events:
|
||||
s.transactionsHandler(event)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("Successful start")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop worker
|
||||
func (s *Service) Stop() error {
|
||||
s.started = false
|
||||
|
||||
if s.transmitter.quit != nil {
|
||||
close(s.transmitter.quit)
|
||||
s.transmitter.wg.Wait()
|
||||
s.transmitter.quit = nil
|
||||
}
|
||||
|
||||
if s.walletTransmitter.quit != nil {
|
||||
close(s.walletTransmitter.quit)
|
||||
s.walletTransmitter.wg.Wait()
|
||||
s.walletTransmitter.quit = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// APIs returns list of available RPC APIs.
|
||||
func (s *Service) APIs() []rpc.API {
|
||||
return []rpc.API{
|
||||
{
|
||||
Namespace: "localnotifications",
|
||||
Version: "0.1.0",
|
||||
Service: NewAPI(s),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Protocols returns list of p2p protocols.
|
||||
func (s *Service) Protocols() []p2p.Protocol {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) IsStarted() bool {
|
||||
return s.started
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package localnotifications
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
db *sql.DB
|
||||
network uint64
|
||||
}
|
||||
|
||||
type NotificationPreference struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Service string `json:"service"`
|
||||
Event string `json:"event,omitempty"`
|
||||
Identifier string `json:"identifier,omitempty"`
|
||||
}
|
||||
|
||||
func NewDB(db *sql.DB, network uint64) *Database {
|
||||
return &Database{db: db, network: network}
|
||||
}
|
||||
|
||||
func (db *Database) GetPreferences() (rst []NotificationPreference, err error) {
|
||||
rows, err := db.db.Query("SELECT service, event, identifier, enabled FROM local_notifications_preferences")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
pref := NotificationPreference{}
|
||||
err = rows.Scan(&pref.Service, &pref.Event, &pref.Identifier, &pref.Enabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rst = append(rst, pref)
|
||||
}
|
||||
return rst, nil
|
||||
}
|
||||
|
||||
func (db *Database) GetWalletPreference() (rst NotificationPreference, err error) {
|
||||
pref := db.db.QueryRow("SELECT service, event, identifier, enabled FROM local_notifications_preferences WHERE service = 'wallet' AND event = 'transaction' AND identifier = 'all'")
|
||||
|
||||
err = pref.Scan(&rst.Service, &rst.Event, &rst.Identifier, &rst.Enabled)
|
||||
if err == sql.ErrNoRows {
|
||||
return rst, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *Database) ChangeWalletPreference(preference bool) error {
|
||||
_, err := db.db.Exec("INSERT OR REPLACE INTO local_notifications_preferences (service, event, identifier, enabled) VALUES ('wallet', 'transaction', 'all', ?)", preference)
|
||||
return err
|
||||
}
|
||||
Generated
Vendored
+230
@@ -0,0 +1,230 @@
|
||||
package localnotifications
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
|
||||
"github.com/status-im/status-go/eth-node/types"
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
"github.com/status-im/status-go/services/wallet/transfer"
|
||||
"github.com/status-im/status-go/services/wallet/walletevent"
|
||||
)
|
||||
|
||||
type transactionState string
|
||||
|
||||
const (
|
||||
walletDeeplinkPrefix = "status-app://wallet/"
|
||||
|
||||
failed transactionState = "failed"
|
||||
inbound transactionState = "inbound"
|
||||
outbound transactionState = "outbound"
|
||||
)
|
||||
|
||||
// TransactionEvent - structure used to pass messages from wallet to bus
|
||||
type TransactionEvent struct {
|
||||
Type string `json:"type"`
|
||||
BlockNumber *big.Int `json:"block-number"`
|
||||
Accounts []common.Address `json:"accounts"`
|
||||
MaxKnownBlocks map[common.Address]*big.Int `json:"max-known-blocks"`
|
||||
}
|
||||
|
||||
type transactionBody struct {
|
||||
State transactionState `json:"state"`
|
||||
From common.Address `json:"from"`
|
||||
To common.Address `json:"to"`
|
||||
FromAccount *accounts.Account `json:"fromAccount,omitempty"`
|
||||
ToAccount *accounts.Account `json:"toAccount,omitempty"`
|
||||
Value *hexutil.Big `json:"value"`
|
||||
ERC20 bool `json:"erc20"`
|
||||
Contract common.Address `json:"contract"`
|
||||
Network uint64 `json:"network"`
|
||||
}
|
||||
|
||||
func (t transactionBody) MarshalJSON() ([]byte, error) {
|
||||
type Alias transactionBody
|
||||
item := struct{ *Alias }{Alias: (*Alias)(&t)}
|
||||
return json.Marshal(item)
|
||||
}
|
||||
|
||||
func (s *Service) buildTransactionNotification(rawTransfer transfer.Transfer) *Notification {
|
||||
log.Info("Handled a new transfer in buildTransactionNotification", "info", rawTransfer)
|
||||
|
||||
var deeplink string
|
||||
var state transactionState
|
||||
transfer := transfer.CastToTransferView(rawTransfer)
|
||||
|
||||
switch {
|
||||
case transfer.TxStatus == hexutil.Uint64(0):
|
||||
state = failed
|
||||
case transfer.Address == transfer.To:
|
||||
state = inbound
|
||||
default:
|
||||
state = outbound
|
||||
}
|
||||
|
||||
from, err := s.accountsDB.GetAccountByAddress(types.Address(transfer.From))
|
||||
|
||||
if err != nil {
|
||||
log.Debug("Could not select From account by address", "error", err)
|
||||
}
|
||||
|
||||
to, err := s.accountsDB.GetAccountByAddress(types.Address(transfer.To))
|
||||
|
||||
if err != nil {
|
||||
log.Debug("Could not select To account by address", "error", err)
|
||||
}
|
||||
|
||||
if from != nil {
|
||||
deeplink = walletDeeplinkPrefix + from.Address.String()
|
||||
} else if to != nil {
|
||||
deeplink = walletDeeplinkPrefix + to.Address.String()
|
||||
}
|
||||
|
||||
body := transactionBody{
|
||||
State: state,
|
||||
From: transfer.From,
|
||||
To: transfer.Address,
|
||||
FromAccount: from,
|
||||
ToAccount: to,
|
||||
Value: transfer.Value,
|
||||
ERC20: string(transfer.Type) == "erc20",
|
||||
Contract: transfer.Contract,
|
||||
Network: transfer.NetworkID,
|
||||
}
|
||||
|
||||
return &Notification{
|
||||
BodyType: TypeTransaction,
|
||||
ID: transfer.ID,
|
||||
Body: body,
|
||||
Deeplink: deeplink,
|
||||
Category: CategoryTransaction,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) transactionsHandler(payload TransactionEvent) {
|
||||
log.Info("Handled a new transaction", "info", payload)
|
||||
|
||||
limit := 20
|
||||
if payload.BlockNumber != nil {
|
||||
for _, address := range payload.Accounts {
|
||||
if payload.BlockNumber.Cmp(payload.MaxKnownBlocks[address]) >= 0 {
|
||||
log.Info("Handled transfer for address", "info", address)
|
||||
transfers, err := s.walletDB.GetTransfersByAddressAndBlock(s.chainID, address, payload.BlockNumber, int64(limit))
|
||||
if err != nil {
|
||||
log.Error("Could not fetch transfers", "error", err)
|
||||
}
|
||||
|
||||
for _, transaction := range transfers {
|
||||
n := s.buildTransactionNotification(transaction)
|
||||
pushMessage(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeWallet - Subscribes to wallet signals
|
||||
func (s *Service) SubscribeWallet(publisher *event.Feed) error {
|
||||
s.walletTransmitter.publisher = publisher
|
||||
|
||||
preference, err := s.db.GetWalletPreference()
|
||||
|
||||
if err != nil {
|
||||
log.Error("Failed to get wallet preference", "error", err)
|
||||
s.WatchingEnabled = false
|
||||
} else {
|
||||
s.WatchingEnabled = preference.Enabled
|
||||
}
|
||||
|
||||
s.StartWalletWatcher()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// StartWalletWatcher - Forward wallet events to notifications
|
||||
func (s *Service) StartWalletWatcher() {
|
||||
if s.walletTransmitter.quit != nil {
|
||||
// already running, nothing to do
|
||||
return
|
||||
}
|
||||
|
||||
if s.walletTransmitter.publisher == nil {
|
||||
log.Error("wallet publisher was not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
s.walletTransmitter.quit = make(chan struct{})
|
||||
events := make(chan walletevent.Event, 10)
|
||||
sub := s.walletTransmitter.publisher.Subscribe(events)
|
||||
|
||||
s.walletTransmitter.wg.Add(1)
|
||||
|
||||
maxKnownBlocks := map[common.Address]*big.Int{}
|
||||
go func() {
|
||||
defer s.walletTransmitter.wg.Done()
|
||||
historyReady := false
|
||||
for {
|
||||
select {
|
||||
case <-s.walletTransmitter.quit:
|
||||
sub.Unsubscribe()
|
||||
return
|
||||
case err := <-sub.Err():
|
||||
// technically event.Feed cannot send an error to subscription.Err channel.
|
||||
// the only time we will get an event is when that channel is closed.
|
||||
if err != nil {
|
||||
log.Error("wallet signals transmitter failed with", "error", err)
|
||||
}
|
||||
return
|
||||
case event := <-events:
|
||||
if event.Type == transfer.EventNewTransfers && historyReady && event.BlockNumber != nil {
|
||||
newBlocks := false
|
||||
for _, address := range event.Accounts {
|
||||
if _, ok := maxKnownBlocks[address]; !ok {
|
||||
newBlocks = true
|
||||
maxKnownBlocks[address] = event.BlockNumber
|
||||
} else if event.BlockNumber.Cmp(maxKnownBlocks[address]) == 1 {
|
||||
maxKnownBlocks[address] = event.BlockNumber
|
||||
newBlocks = true
|
||||
}
|
||||
}
|
||||
if newBlocks && s.WatchingEnabled {
|
||||
s.transmitter.publisher.Send(TransactionEvent{
|
||||
Type: string(event.Type),
|
||||
BlockNumber: event.BlockNumber,
|
||||
Accounts: event.Accounts,
|
||||
MaxKnownBlocks: maxKnownBlocks,
|
||||
})
|
||||
}
|
||||
} else if event.Type == transfer.EventRecentHistoryReady {
|
||||
historyReady = true
|
||||
if event.BlockNumber != nil {
|
||||
for _, address := range event.Accounts {
|
||||
if _, ok := maxKnownBlocks[address]; !ok {
|
||||
maxKnownBlocks[address] = event.BlockNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// StopWalletWatcher - stops watching for new wallet events
|
||||
func (s *Service) StopWalletWatcher() {
|
||||
if s.walletTransmitter.quit != nil {
|
||||
close(s.walletTransmitter.quit)
|
||||
s.walletTransmitter.wg.Wait()
|
||||
s.walletTransmitter.quit = nil
|
||||
}
|
||||
}
|
||||
|
||||
// IsWatchingWallet - check if local-notifications are subscribed to wallet updates
|
||||
func (s *Service) IsWatchingWallet() bool {
|
||||
return s.walletTransmitter.quit != nil
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package localnotifications
|
||||
|
||||
const (
|
||||
CategoryTransaction PushCategory = "transaction"
|
||||
CategoryMessage PushCategory = "newMessage"
|
||||
CategoryGroupInvite PushCategory = "groupInvite"
|
||||
CategoryCommunityRequestToJoin = "communityRequestToJoin"
|
||||
|
||||
TypeTransaction NotificationType = "transaction"
|
||||
TypeMessage NotificationType = "message"
|
||||
)
|
||||
Reference in New Issue
Block a user