github.com/status-im/status-go@v1.1.0/services/wallet/collectibles/commands.go (about) 1 package collectibles 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "math/big" 8 "sync/atomic" 9 "time" 10 11 "github.com/ethereum/go-ethereum/common" 12 "github.com/ethereum/go-ethereum/event" 13 "github.com/ethereum/go-ethereum/log" 14 "github.com/status-im/status-go/services/wallet/async" 15 "github.com/status-im/status-go/services/wallet/bigint" 16 walletCommon "github.com/status-im/status-go/services/wallet/common" 17 "github.com/status-im/status-go/services/wallet/thirdparty" 18 "github.com/status-im/status-go/services/wallet/transfer" 19 "github.com/status-im/status-go/services/wallet/walletevent" 20 ) 21 22 const ( 23 fetchLimit = 50 // Limit number of collectibles we fetch per provider call 24 accountOwnershipUpdateInterval = 60 * time.Minute 25 accountOwnershipUpdateDelayInterval = 30 * time.Second 26 ) 27 28 type OwnershipState = int 29 30 type OwnedCollectibles struct { 31 chainID walletCommon.ChainID 32 account common.Address 33 ids []thirdparty.CollectibleUniqueID 34 } 35 36 type OwnedCollectiblesChangeType = int 37 38 const ( 39 OwnedCollectiblesChangeTypeAdded OwnedCollectiblesChangeType = iota + 1 40 OwnedCollectiblesChangeTypeUpdated 41 OwnedCollectiblesChangeTypeRemoved 42 ) 43 44 type OwnedCollectiblesChange struct { 45 ownedCollectibles OwnedCollectibles 46 changeType OwnedCollectiblesChangeType 47 } 48 49 type OwnedCollectiblesChangeCb func(OwnedCollectiblesChange) 50 51 type TransferCb func(common.Address, walletCommon.ChainID, []transfer.Transfer) 52 53 const ( 54 OwnershipStateIdle OwnershipState = iota + 1 55 OwnershipStateDelayed 56 OwnershipStateUpdating 57 OwnershipStateError 58 ) 59 60 type periodicRefreshOwnedCollectiblesCommand struct { 61 chainID walletCommon.ChainID 62 account common.Address 63 manager *Manager 64 ownershipDB *OwnershipDB 65 walletFeed *event.Feed 66 ownedCollectiblesChangeCb OwnedCollectiblesChangeCb 67 68 group *async.Group 69 state atomic.Value 70 } 71 72 func newPeriodicRefreshOwnedCollectiblesCommand( 73 manager *Manager, 74 ownershipDB *OwnershipDB, 75 walletFeed *event.Feed, 76 chainID walletCommon.ChainID, 77 account common.Address, 78 ownedCollectiblesChangeCb OwnedCollectiblesChangeCb) *periodicRefreshOwnedCollectiblesCommand { 79 ret := &periodicRefreshOwnedCollectiblesCommand{ 80 manager: manager, 81 ownershipDB: ownershipDB, 82 walletFeed: walletFeed, 83 chainID: chainID, 84 account: account, 85 ownedCollectiblesChangeCb: ownedCollectiblesChangeCb, 86 } 87 ret.state.Store(OwnershipStateIdle) 88 return ret 89 } 90 91 func (c *periodicRefreshOwnedCollectiblesCommand) DelayedCommand() async.Command { 92 return async.SingleShotCommand{ 93 Interval: accountOwnershipUpdateDelayInterval, 94 Init: func(ctx context.Context) (err error) { 95 c.state.Store(OwnershipStateDelayed) 96 return nil 97 }, 98 Runable: c.Command(), 99 }.Run 100 } 101 102 func (c *periodicRefreshOwnedCollectiblesCommand) Command() async.Command { 103 return async.InfiniteCommand{ 104 Interval: accountOwnershipUpdateInterval, 105 Runable: c.Run, 106 }.Run 107 } 108 109 func (c *periodicRefreshOwnedCollectiblesCommand) Run(ctx context.Context) (err error) { 110 return c.loadOwnedCollectibles(ctx) 111 } 112 113 func (c *periodicRefreshOwnedCollectiblesCommand) GetState() OwnershipState { 114 return c.state.Load().(OwnershipState) 115 } 116 117 func (c *periodicRefreshOwnedCollectiblesCommand) Stop() { 118 if c.group != nil { 119 c.group.Stop() 120 c.group.Wait() 121 c.group = nil 122 } 123 } 124 125 func (c *periodicRefreshOwnedCollectiblesCommand) loadOwnedCollectibles(ctx context.Context) error { 126 c.group = async.NewGroup(ctx) 127 128 ownedCollectiblesChangeCh := make(chan OwnedCollectiblesChange) 129 command := newLoadOwnedCollectiblesCommand(c.manager, c.ownershipDB, c.walletFeed, c.chainID, c.account, ownedCollectiblesChangeCh) 130 131 c.state.Store(OwnershipStateUpdating) 132 defer func() { 133 if command.err != nil { 134 c.state.Store(OwnershipStateError) 135 } else { 136 c.state.Store(OwnershipStateIdle) 137 } 138 }() 139 140 c.group.Add(command.Command()) 141 142 select { 143 case ownedCollectiblesChange := <-ownedCollectiblesChangeCh: 144 if c.ownedCollectiblesChangeCb != nil { 145 c.ownedCollectiblesChangeCb(ownedCollectiblesChange) 146 } 147 case <-ctx.Done(): 148 return ctx.Err() 149 case <-c.group.WaitAsync(): 150 return nil 151 } 152 153 return nil 154 } 155 156 // Fetches owned collectibles for a ChainID+OwnerAddress combination in chunks 157 // and updates the ownershipDB when all chunks are loaded 158 type loadOwnedCollectiblesCommand struct { 159 chainID walletCommon.ChainID 160 account common.Address 161 manager *Manager 162 ownershipDB *OwnershipDB 163 walletFeed *event.Feed 164 ownedCollectiblesChangeCh chan<- OwnedCollectiblesChange 165 166 // Not to be set by the caller 167 partialOwnership []thirdparty.CollectibleIDBalance 168 err error 169 } 170 171 func newLoadOwnedCollectiblesCommand( 172 manager *Manager, 173 ownershipDB *OwnershipDB, 174 walletFeed *event.Feed, 175 chainID walletCommon.ChainID, 176 account common.Address, 177 ownedCollectiblesChangeCh chan<- OwnedCollectiblesChange) *loadOwnedCollectiblesCommand { 178 return &loadOwnedCollectiblesCommand{ 179 manager: manager, 180 ownershipDB: ownershipDB, 181 walletFeed: walletFeed, 182 chainID: chainID, 183 account: account, 184 ownedCollectiblesChangeCh: ownedCollectiblesChangeCh, 185 } 186 } 187 188 func (c *loadOwnedCollectiblesCommand) Command() async.Command { 189 return c.Run 190 } 191 192 func (c *loadOwnedCollectiblesCommand) triggerEvent(eventType walletevent.EventType, chainID walletCommon.ChainID, account common.Address, message string) { 193 c.walletFeed.Send(walletevent.Event{ 194 Type: eventType, 195 ChainID: uint64(chainID), 196 Accounts: []common.Address{ 197 account, 198 }, 199 Message: message, 200 }) 201 } 202 203 func ownedTokensToTokenBalancesPerContractAddress(ownership []thirdparty.CollectibleIDBalance) thirdparty.TokenBalancesPerContractAddress { 204 ret := make(thirdparty.TokenBalancesPerContractAddress) 205 for _, idBalance := range ownership { 206 balanceBigInt := idBalance.Balance 207 if balanceBigInt == nil { 208 balanceBigInt = &bigint.BigInt{Int: big.NewInt(1)} 209 } 210 balance := thirdparty.TokenBalance{ 211 TokenID: idBalance.ID.TokenID, 212 Balance: balanceBigInt, 213 } 214 ret[idBalance.ID.ContractID.Address] = append(ret[idBalance.ID.ContractID.Address], balance) 215 } 216 return ret 217 } 218 219 func (c *loadOwnedCollectiblesCommand) sendOwnedCollectiblesChanges(removed, updated, added []thirdparty.CollectibleUniqueID) { 220 if len(removed) > 0 { 221 c.ownedCollectiblesChangeCh <- OwnedCollectiblesChange{ 222 ownedCollectibles: OwnedCollectibles{ 223 chainID: c.chainID, 224 account: c.account, 225 ids: removed, 226 }, 227 changeType: OwnedCollectiblesChangeTypeRemoved, 228 } 229 } 230 231 if len(updated) > 0 { 232 c.ownedCollectiblesChangeCh <- OwnedCollectiblesChange{ 233 ownedCollectibles: OwnedCollectibles{ 234 chainID: c.chainID, 235 account: c.account, 236 ids: updated, 237 }, 238 changeType: OwnedCollectiblesChangeTypeUpdated, 239 } 240 } 241 242 if len(added) > 0 { 243 c.ownedCollectiblesChangeCh <- OwnedCollectiblesChange{ 244 ownedCollectibles: OwnedCollectibles{ 245 chainID: c.chainID, 246 account: c.account, 247 ids: added, 248 }, 249 changeType: OwnedCollectiblesChangeTypeAdded, 250 } 251 } 252 } 253 254 func (c *loadOwnedCollectiblesCommand) Run(parent context.Context) (err error) { 255 log.Debug("start loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account) 256 257 pageNr := 0 258 cursor := thirdparty.FetchFromStartCursor 259 providerID := thirdparty.FetchFromAnyProvider 260 start := time.Now() 261 262 c.triggerEvent(EventCollectiblesOwnershipUpdateStarted, c.chainID, c.account, "") 263 264 updateMessage := OwnershipUpdateMessage{} 265 266 lastFetchTimestamp, err := c.ownershipDB.GetOwnershipUpdateTimestamp(c.account, c.chainID) 267 if err != nil { 268 c.err = err 269 } else { 270 initialFetch := lastFetchTimestamp == InvalidTimestamp 271 // Fetch collectibles in chunks 272 for { 273 if walletCommon.ShouldCancel(parent) { 274 c.err = errors.New("context cancelled") 275 break 276 } 277 278 pageStart := time.Now() 279 log.Debug("start loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr) 280 281 partialOwnership, err := c.manager.FetchCollectibleOwnershipByOwner(parent, c.chainID, c.account, cursor, fetchLimit, providerID) 282 283 if err != nil { 284 log.Error("failed loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr, "error", err) 285 c.err = err 286 break 287 } 288 289 log.Debug("partial loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr, "in", time.Since(pageStart), "found", len(partialOwnership.Items)) 290 291 c.partialOwnership = append(c.partialOwnership, partialOwnership.Items...) 292 293 pageNr++ 294 cursor = partialOwnership.NextCursor 295 providerID = partialOwnership.Provider 296 297 finished := cursor == thirdparty.FetchFromStartCursor 298 299 // Normally, update the DB once we've finished fetching 300 // If this is the first fetch, make partial updates to the client to get a better UX 301 if initialFetch || finished { 302 balances := ownedTokensToTokenBalancesPerContractAddress(c.partialOwnership) 303 304 updateMessage.Removed, updateMessage.Updated, updateMessage.Added, err = c.ownershipDB.Update(c.chainID, c.account, balances, start.Unix()) 305 if err != nil { 306 log.Error("failed updating ownershipDB in loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "error", err) 307 c.err = err 308 break 309 } 310 311 c.sendOwnedCollectiblesChanges(updateMessage.Removed, updateMessage.Updated, updateMessage.Added) 312 } 313 314 if finished || c.err != nil { 315 break 316 } else if initialFetch { 317 encodedMessage, err := json.Marshal(updateMessage) 318 if err != nil { 319 c.err = err 320 break 321 } 322 c.triggerEvent(EventCollectiblesOwnershipUpdatePartial, c.chainID, c.account, string(encodedMessage)) 323 324 updateMessage = OwnershipUpdateMessage{} 325 } 326 } 327 } 328 329 var encodedMessage []byte 330 if c.err == nil { 331 encodedMessage, c.err = json.Marshal(updateMessage) 332 } 333 334 if c.err != nil { 335 c.triggerEvent(EventCollectiblesOwnershipUpdateFinishedWithError, c.chainID, c.account, c.err.Error()) 336 } else { 337 c.triggerEvent(EventCollectiblesOwnershipUpdateFinished, c.chainID, c.account, string(encodedMessage)) 338 } 339 340 log.Debug("end loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "in", time.Since(start)) 341 return nil 342 }