feat: Waku v2 bridge

Issue #12610
This commit is contained in:
Michal Iskierko
2023-11-12 13:29:38 +01:00
parent 56e7bd01ca
commit 6d31343205
6716 changed files with 1982502 additions and 5891 deletions
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -0,0 +1,11 @@
package localnotifications
const (
CategoryTransaction PushCategory = "transaction"
CategoryMessage PushCategory = "newMessage"
CategoryGroupInvite PushCategory = "groupInvite"
CategoryCommunityRequestToJoin = "communityRequestToJoin"
TypeTransaction NotificationType = "transaction"
TypeMessage NotificationType = "message"
)