github.com/status-im/status-go@v1.1.0/services/local-notifications/transaction.go (about)

     1  package localnotifications
     2  
     3  import (
     4  	"encoding/json"
     5  	"math/big"
     6  
     7  	"github.com/ethereum/go-ethereum/common"
     8  	"github.com/ethereum/go-ethereum/common/hexutil"
     9  	"github.com/ethereum/go-ethereum/event"
    10  	"github.com/ethereum/go-ethereum/log"
    11  
    12  	"github.com/status-im/status-go/eth-node/types"
    13  	"github.com/status-im/status-go/multiaccounts/accounts"
    14  	"github.com/status-im/status-go/services/wallet/transfer"
    15  	"github.com/status-im/status-go/services/wallet/walletevent"
    16  )
    17  
    18  type transactionState string
    19  
    20  const (
    21  	walletDeeplinkPrefix = "status-app://wallet/"
    22  
    23  	failed   transactionState = "failed"
    24  	inbound  transactionState = "inbound"
    25  	outbound transactionState = "outbound"
    26  )
    27  
    28  // TransactionEvent - structure used to pass messages from wallet to bus
    29  type TransactionEvent struct {
    30  	Type           string                      `json:"type"`
    31  	BlockNumber    *big.Int                    `json:"block-number"`
    32  	Accounts       []common.Address            `json:"accounts"`
    33  	MaxKnownBlocks map[common.Address]*big.Int `json:"max-known-blocks"`
    34  }
    35  
    36  type transactionBody struct {
    37  	State       transactionState  `json:"state"`
    38  	From        common.Address    `json:"from"`
    39  	To          common.Address    `json:"to"`
    40  	FromAccount *accounts.Account `json:"fromAccount,omitempty"`
    41  	ToAccount   *accounts.Account `json:"toAccount,omitempty"`
    42  	Value       *hexutil.Big      `json:"value"`
    43  	ERC20       bool              `json:"erc20"`
    44  	Contract    common.Address    `json:"contract"`
    45  	Network     uint64            `json:"network"`
    46  }
    47  
    48  func (t transactionBody) MarshalJSON() ([]byte, error) {
    49  	type Alias transactionBody
    50  	item := struct{ *Alias }{Alias: (*Alias)(&t)}
    51  	return json.Marshal(item)
    52  }
    53  
    54  func (s *Service) buildTransactionNotification(rawTransfer transfer.Transfer) *Notification {
    55  	log.Info("Handled a new transfer in buildTransactionNotification", "info", rawTransfer)
    56  
    57  	var deeplink string
    58  	var state transactionState
    59  	transfer := transfer.CastToTransferView(rawTransfer)
    60  
    61  	switch {
    62  	case transfer.TxStatus == hexutil.Uint64(0):
    63  		state = failed
    64  	case transfer.Address == transfer.To:
    65  		state = inbound
    66  	default:
    67  		state = outbound
    68  	}
    69  
    70  	from, err := s.accountsDB.GetAccountByAddress(types.Address(transfer.From))
    71  
    72  	if err != nil {
    73  		log.Debug("Could not select From account by address", "error", err)
    74  	}
    75  
    76  	to, err := s.accountsDB.GetAccountByAddress(types.Address(transfer.To))
    77  
    78  	if err != nil {
    79  		log.Debug("Could not select To account by address", "error", err)
    80  	}
    81  
    82  	if from != nil {
    83  		deeplink = walletDeeplinkPrefix + from.Address.String()
    84  	} else if to != nil {
    85  		deeplink = walletDeeplinkPrefix + to.Address.String()
    86  	}
    87  
    88  	body := transactionBody{
    89  		State:       state,
    90  		From:        transfer.From,
    91  		To:          transfer.Address,
    92  		FromAccount: from,
    93  		ToAccount:   to,
    94  		Value:       transfer.Value,
    95  		ERC20:       string(transfer.Type) == "erc20",
    96  		Contract:    transfer.Contract,
    97  		Network:     transfer.NetworkID,
    98  	}
    99  
   100  	return &Notification{
   101  		BodyType: TypeTransaction,
   102  		ID:       transfer.ID,
   103  		Body:     body,
   104  		Deeplink: deeplink,
   105  		Category: CategoryTransaction,
   106  	}
   107  }
   108  
   109  func (s *Service) transactionsHandler(payload TransactionEvent) {
   110  	log.Info("Handled a new transaction", "info", payload)
   111  
   112  	limit := 20
   113  	if payload.BlockNumber != nil {
   114  		for _, address := range payload.Accounts {
   115  			if payload.BlockNumber.Cmp(payload.MaxKnownBlocks[address]) >= 0 {
   116  				log.Info("Handled transfer for address", "info", address)
   117  				transfers, err := s.walletDB.GetTransfersByAddressAndBlock(s.chainID, address, payload.BlockNumber, int64(limit))
   118  				if err != nil {
   119  					log.Error("Could not fetch transfers", "error", err)
   120  				}
   121  
   122  				for _, transaction := range transfers {
   123  					n := s.buildTransactionNotification(transaction)
   124  					pushMessage(n)
   125  				}
   126  			}
   127  		}
   128  	}
   129  }
   130  
   131  // SubscribeWallet - Subscribes to wallet signals
   132  func (s *Service) SubscribeWallet(publisher *event.Feed) error {
   133  	s.walletTransmitter.publisher = publisher
   134  
   135  	preference, err := s.db.GetWalletPreference()
   136  
   137  	if err != nil {
   138  		log.Error("Failed to get wallet preference", "error", err)
   139  		s.WatchingEnabled = false
   140  	} else {
   141  		s.WatchingEnabled = preference.Enabled
   142  	}
   143  
   144  	s.StartWalletWatcher()
   145  
   146  	return err
   147  }
   148  
   149  // StartWalletWatcher - Forward wallet events to notifications
   150  func (s *Service) StartWalletWatcher() {
   151  	if s.walletTransmitter.quit != nil {
   152  		// already running, nothing to do
   153  		return
   154  	}
   155  
   156  	if s.walletTransmitter.publisher == nil {
   157  		log.Error("wallet publisher was not initialized")
   158  		return
   159  	}
   160  
   161  	s.walletTransmitter.quit = make(chan struct{})
   162  	events := make(chan walletevent.Event, 10)
   163  	sub := s.walletTransmitter.publisher.Subscribe(events)
   164  
   165  	s.walletTransmitter.wg.Add(1)
   166  
   167  	maxKnownBlocks := map[common.Address]*big.Int{}
   168  	go func() {
   169  		defer s.walletTransmitter.wg.Done()
   170  		historyReady := false
   171  		for {
   172  			select {
   173  			case <-s.walletTransmitter.quit:
   174  				sub.Unsubscribe()
   175  				return
   176  			case err := <-sub.Err():
   177  				// technically event.Feed cannot send an error to subscription.Err channel.
   178  				// the only time we will get an event is when that channel is closed.
   179  				if err != nil {
   180  					log.Error("wallet signals transmitter failed with", "error", err)
   181  				}
   182  				return
   183  			case event := <-events:
   184  				if event.Type == transfer.EventNewTransfers && historyReady && event.BlockNumber != nil {
   185  					newBlocks := false
   186  					for _, address := range event.Accounts {
   187  						if _, ok := maxKnownBlocks[address]; !ok {
   188  							newBlocks = true
   189  							maxKnownBlocks[address] = event.BlockNumber
   190  						} else if event.BlockNumber.Cmp(maxKnownBlocks[address]) == 1 {
   191  							maxKnownBlocks[address] = event.BlockNumber
   192  							newBlocks = true
   193  						}
   194  					}
   195  					if newBlocks && s.WatchingEnabled {
   196  						s.transmitter.publisher.Send(TransactionEvent{
   197  							Type:           string(event.Type),
   198  							BlockNumber:    event.BlockNumber,
   199  							Accounts:       event.Accounts,
   200  							MaxKnownBlocks: maxKnownBlocks,
   201  						})
   202  					}
   203  				} else if event.Type == transfer.EventRecentHistoryReady {
   204  					historyReady = true
   205  					if event.BlockNumber != nil {
   206  						for _, address := range event.Accounts {
   207  							if _, ok := maxKnownBlocks[address]; !ok {
   208  								maxKnownBlocks[address] = event.BlockNumber
   209  							}
   210  						}
   211  					}
   212  				}
   213  			}
   214  		}
   215  	}()
   216  }
   217  
   218  // StopWalletWatcher - stops watching for new wallet events
   219  func (s *Service) StopWalletWatcher() {
   220  	if s.walletTransmitter.quit != nil {
   221  		close(s.walletTransmitter.quit)
   222  		s.walletTransmitter.wg.Wait()
   223  		s.walletTransmitter.quit = nil
   224  	}
   225  }
   226  
   227  // IsWatchingWallet - check if local-notifications are subscribed to wallet updates
   228  func (s *Service) IsWatchingWallet() bool {
   229  	return s.walletTransmitter.quit != nil
   230  }