github.com/status-im/status-go@v1.1.0/services/wallet/collectibles/controller.go (about) 1 package collectibles 2 3 import ( 4 "context" 5 "database/sql" 6 "errors" 7 "sync" 8 "time" 9 10 "github.com/ethereum/go-ethereum/common" 11 "github.com/ethereum/go-ethereum/event" 12 "github.com/ethereum/go-ethereum/log" 13 "github.com/status-im/status-go/multiaccounts/accounts" 14 "github.com/status-im/status-go/multiaccounts/settings" 15 "github.com/status-im/status-go/rpc/network" 16 "github.com/status-im/status-go/services/accounts/accountsevent" 17 "github.com/status-im/status-go/services/accounts/settingsevent" 18 "github.com/status-im/status-go/services/wallet/async" 19 walletCommon "github.com/status-im/status-go/services/wallet/common" 20 "github.com/status-im/status-go/services/wallet/transfer" 21 "github.com/status-im/status-go/services/wallet/walletevent" 22 ) 23 24 const ( 25 activityRefetchMarginSeconds = 30 * 60 // Trigger a fetch if activity is detected this many seconds before the last fetch 26 ) 27 28 type commandPerChainID = map[walletCommon.ChainID]*periodicRefreshOwnedCollectiblesCommand 29 type commandPerAddressAndChainID = map[common.Address]commandPerChainID 30 31 type timerPerChainID = map[walletCommon.ChainID]*time.Timer 32 type timerPerAddressAndChainID = map[common.Address]timerPerChainID 33 34 type Controller struct { 35 manager *Manager 36 ownershipDB *OwnershipDB 37 walletFeed *event.Feed 38 accountsDB *accounts.Database 39 accountsFeed *event.Feed 40 settingsFeed *event.Feed 41 42 networkManager *network.Manager 43 cancelFn context.CancelFunc 44 45 commands commandPerAddressAndChainID 46 timers timerPerAddressAndChainID 47 group *async.Group 48 accountsWatcher *accountsevent.Watcher 49 walletEventsWatcher *walletevent.Watcher 50 settingsWatcher *settingsevent.Watcher 51 52 ownedCollectiblesChangeCb OwnedCollectiblesChangeCb 53 collectiblesTransferCb TransferCb 54 55 commandsLock sync.RWMutex 56 } 57 58 func NewController( 59 db *sql.DB, 60 walletFeed *event.Feed, 61 accountsDB *accounts.Database, 62 accountsFeed *event.Feed, 63 settingsFeed *event.Feed, 64 networkManager *network.Manager, 65 manager *Manager) *Controller { 66 return &Controller{ 67 manager: manager, 68 ownershipDB: NewOwnershipDB(db), 69 walletFeed: walletFeed, 70 accountsDB: accountsDB, 71 accountsFeed: accountsFeed, 72 settingsFeed: settingsFeed, 73 networkManager: networkManager, 74 commands: make(commandPerAddressAndChainID), 75 timers: make(timerPerAddressAndChainID), 76 } 77 } 78 79 func (c *Controller) SetOwnedCollectiblesChangeCb(cb OwnedCollectiblesChangeCb) { 80 c.ownedCollectiblesChangeCb = cb 81 } 82 83 func (c *Controller) SetCollectiblesTransferCb(cb TransferCb) { 84 c.collectiblesTransferCb = cb 85 } 86 87 func (c *Controller) Start() { 88 // Setup periodical collectibles refresh 89 _ = c.startPeriodicalOwnershipFetch() 90 91 // Setup collectibles fetch when a new account gets added 92 c.startAccountsWatcher() 93 94 // Setup collectibles fetch when relevant activity is detected 95 c.startWalletEventsWatcher() 96 97 // Setup collectibles fetch when chain-related settings change 98 c.startSettingsWatcher() 99 } 100 101 func (c *Controller) Stop() { 102 c.stopSettingsWatcher() 103 104 c.stopWalletEventsWatcher() 105 106 c.stopAccountsWatcher() 107 108 c.stopPeriodicalOwnershipFetch() 109 } 110 111 func (c *Controller) RefetchOwnedCollectibles() { 112 c.stopPeriodicalOwnershipFetch() 113 c.manager.ResetConnectionStatus() 114 _ = c.startPeriodicalOwnershipFetch() 115 } 116 117 func (c *Controller) GetCommandState(chainID walletCommon.ChainID, address common.Address) OwnershipState { 118 c.commandsLock.RLock() 119 defer c.commandsLock.RUnlock() 120 121 state := OwnershipStateIdle 122 if c.commands[address] != nil && c.commands[address][chainID] != nil { 123 state = c.commands[address][chainID].GetState() 124 } 125 126 return state 127 } 128 129 func (c *Controller) isPeriodicalOwnershipFetchRunning() bool { 130 return c.group != nil 131 } 132 133 // Starts periodical fetching for the all wallet addresses and all chains 134 func (c *Controller) startPeriodicalOwnershipFetch() error { 135 c.commandsLock.Lock() 136 defer c.commandsLock.Unlock() 137 138 if c.isPeriodicalOwnershipFetchRunning() { 139 return nil 140 } 141 142 ctx, cancel := context.WithCancel(context.Background()) 143 c.cancelFn = cancel 144 145 c.group = async.NewGroup(ctx) 146 147 addresses, err := c.accountsDB.GetWalletAddresses() 148 if err != nil { 149 return err 150 } 151 152 for _, addr := range addresses { 153 err := c.startPeriodicalOwnershipFetchForAccount(common.Address(addr)) 154 if err != nil { 155 log.Error("Error starting periodical collectibles fetch for accpunt", "address", addr, "error", err) 156 return err 157 } 158 } 159 160 return nil 161 } 162 163 func (c *Controller) stopPeriodicalOwnershipFetch() { 164 c.commandsLock.Lock() 165 defer c.commandsLock.Unlock() 166 167 if !c.isPeriodicalOwnershipFetchRunning() { 168 return 169 } 170 171 if c.cancelFn != nil { 172 c.cancelFn() 173 c.cancelFn = nil 174 } 175 if c.group != nil { 176 c.group.Stop() 177 c.group.Wait() 178 c.group = nil 179 c.commands = make(commandPerAddressAndChainID) 180 } 181 } 182 183 // Starts (or restarts) periodical fetching for the given account address for all chains 184 func (c *Controller) startPeriodicalOwnershipFetchForAccount(address common.Address) error { 185 log.Debug("wallet.api.collectibles.Controller Start periodical fetching", "address", address) 186 187 networks, err := c.networkManager.Get(false) 188 if err != nil { 189 return err 190 } 191 192 areTestNetworksEnabled, err := c.accountsDB.GetTestNetworksEnabled() 193 if err != nil { 194 return err 195 } 196 197 for _, network := range networks { 198 if network.IsTest != areTestNetworksEnabled { 199 continue 200 } 201 chainID := walletCommon.ChainID(network.ChainID) 202 203 err := c.startPeriodicalOwnershipFetchForAccountAndChainID(address, chainID, false) 204 if err != nil { 205 return err 206 } 207 } 208 209 return nil 210 } 211 212 // Starts (or restarts) periodical fetching for the given account address for all chains 213 func (c *Controller) startPeriodicalOwnershipFetchForAccountAndChainID(address common.Address, chainID walletCommon.ChainID, delayed bool) error { 214 log.Debug("wallet.api.collectibles.Controller Start periodical fetching", "address", address, "chainID", chainID, "delayed", delayed) 215 216 if !c.isPeriodicalOwnershipFetchRunning() { 217 return errors.New("periodical fetch not initialized") 218 } 219 220 err := c.stopPeriodicalOwnershipFetchForAccountAndChainID(address, chainID) 221 if err != nil { 222 return err 223 } 224 225 if _, ok := c.commands[address]; !ok { 226 c.commands[address] = make(commandPerChainID) 227 } 228 229 command := newPeriodicRefreshOwnedCollectiblesCommand( 230 c.manager, 231 c.ownershipDB, 232 c.walletFeed, 233 chainID, 234 address, 235 c.ownedCollectiblesChangeCb, 236 ) 237 238 c.commands[address][chainID] = command 239 if delayed { 240 c.group.Add(command.DelayedCommand()) 241 } else { 242 c.group.Add(command.Command()) 243 } 244 245 return nil 246 } 247 248 // Stop periodical fetching for the given account address for all chains 249 func (c *Controller) stopPeriodicalOwnershipFetchForAccount(address common.Address) error { 250 log.Debug("wallet.api.collectibles.Controller Stop periodical fetching", "address", address) 251 252 if !c.isPeriodicalOwnershipFetchRunning() { 253 return errors.New("periodical fetch not initialized") 254 } 255 256 if _, ok := c.commands[address]; ok { 257 for chainID := range c.commands[address] { 258 err := c.stopPeriodicalOwnershipFetchForAccountAndChainID(address, chainID) 259 if err != nil { 260 return err 261 } 262 } 263 264 } 265 266 return nil 267 } 268 269 func (c *Controller) stopPeriodicalOwnershipFetchForAccountAndChainID(address common.Address, chainID walletCommon.ChainID) error { 270 log.Debug("wallet.api.collectibles.Controller Stop periodical fetching", "address", address, "chainID", chainID) 271 272 if !c.isPeriodicalOwnershipFetchRunning() { 273 return errors.New("periodical fetch not initialized") 274 } 275 276 if _, ok := c.commands[address]; ok { 277 if _, ok := c.commands[address][chainID]; ok { 278 c.commands[address][chainID].Stop() 279 delete(c.commands[address], chainID) 280 } 281 // If it was the last chain, delete the address as well 282 if len(c.commands[address]) == 0 { 283 delete(c.commands, address) 284 } 285 } 286 287 return nil 288 } 289 290 func (c *Controller) startAccountsWatcher() { 291 if c.accountsWatcher != nil { 292 return 293 } 294 295 accountChangeCb := func(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address) { 296 c.commandsLock.Lock() 297 defer c.commandsLock.Unlock() 298 // Whenever an account gets added, start fetching 299 if eventType == accountsevent.EventTypeAdded { 300 for _, address := range changedAddresses { 301 err := c.startPeriodicalOwnershipFetchForAccount(address) 302 if err != nil { 303 log.Error("Error starting periodical collectibles fetch", "address", address, "error", err) 304 } 305 } 306 } else if eventType == accountsevent.EventTypeRemoved { 307 for _, address := range changedAddresses { 308 err := c.stopPeriodicalOwnershipFetchForAccount(address) 309 if err != nil { 310 log.Error("Error starting periodical collectibles fetch", "address", address, "error", err) 311 } 312 } 313 } 314 } 315 316 c.accountsWatcher = accountsevent.NewWatcher(c.accountsDB, c.accountsFeed, accountChangeCb) 317 318 c.accountsWatcher.Start() 319 } 320 321 func (c *Controller) stopAccountsWatcher() { 322 if c.accountsWatcher != nil { 323 c.accountsWatcher.Stop() 324 c.accountsWatcher = nil 325 } 326 } 327 328 func (c *Controller) startWalletEventsWatcher() { 329 if c.walletEventsWatcher != nil { 330 return 331 } 332 333 walletEventCb := func(event walletevent.Event) { 334 // EventRecentHistoryReady ? 335 if event.Type != transfer.EventInternalERC721TransferDetected && 336 event.Type != transfer.EventInternalERC1155TransferDetected { 337 return 338 } 339 340 chainID := walletCommon.ChainID(event.ChainID) 341 for _, account := range event.Accounts { 342 // Call external callback 343 if c.collectiblesTransferCb != nil { 344 c.collectiblesTransferCb(account, chainID, event.EventParams.([]transfer.Transfer)) 345 } 346 347 c.refetchOwnershipIfRecentTransfer(account, chainID, event.At) 348 } 349 } 350 351 c.walletEventsWatcher = walletevent.NewWatcher(c.walletFeed, walletEventCb) 352 353 c.walletEventsWatcher.Start() 354 } 355 356 func (c *Controller) stopWalletEventsWatcher() { 357 if c.walletEventsWatcher != nil { 358 c.walletEventsWatcher.Stop() 359 c.walletEventsWatcher = nil 360 } 361 } 362 363 func (c *Controller) startSettingsWatcher() { 364 if c.settingsWatcher != nil { 365 return 366 } 367 368 settingChangeCb := func(setting settings.SettingField, value interface{}) { 369 if setting.Equals(settings.TestNetworksEnabled) || setting.Equals(settings.IsGoerliEnabled) { 370 c.stopPeriodicalOwnershipFetch() 371 err := c.startPeriodicalOwnershipFetch() 372 if err != nil { 373 log.Error("Error starting periodical collectibles fetch", "error", err) 374 } 375 } 376 } 377 378 c.settingsWatcher = settingsevent.NewWatcher(c.settingsFeed, settingChangeCb) 379 380 c.settingsWatcher.Start() 381 } 382 383 func (c *Controller) stopSettingsWatcher() { 384 if c.settingsWatcher != nil { 385 c.settingsWatcher.Stop() 386 c.settingsWatcher = nil 387 } 388 } 389 390 func (c *Controller) refetchOwnershipIfRecentTransfer(account common.Address, chainID walletCommon.ChainID, latestTxTimestamp int64) { 391 392 // Check last ownership update timestamp 393 timestamp, err := c.ownershipDB.GetOwnershipUpdateTimestamp(account, chainID) 394 395 if err != nil { 396 log.Error("Error getting ownership update timestamp", "error", err) 397 return 398 } 399 if timestamp == InvalidTimestamp { 400 // Ownership was never fetched for this account 401 return 402 } 403 404 timeCheck := timestamp - activityRefetchMarginSeconds 405 if timeCheck < 0 { 406 timeCheck = 0 407 } 408 409 if latestTxTimestamp > timeCheck { 410 // Restart fetching for account + chainID 411 c.commandsLock.Lock() 412 err := c.startPeriodicalOwnershipFetchForAccountAndChainID(account, chainID, true) 413 c.commandsLock.Unlock() 414 if err != nil { 415 log.Error("Error starting periodical collectibles fetch", "address", account, "error", err) 416 } 417 } 418 }