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 }