github.com/status-im/status-go@v1.1.0/services/stickers/api.go (about) 1 package stickers 2 3 import ( 4 "context" 5 "math/big" 6 "time" 7 8 "github.com/zenthangplus/goccm" 9 "olympos.io/encoding/edn" 10 11 "github.com/ethereum/go-ethereum/accounts/abi/bind" 12 "github.com/ethereum/go-ethereum/common" 13 "github.com/ethereum/go-ethereum/common/hexutil" 14 "github.com/ethereum/go-ethereum/log" 15 "github.com/status-im/status-go/account" 16 "github.com/status-im/status-go/contracts" 17 "github.com/status-im/status-go/contracts/stickers" 18 "github.com/status-im/status-go/eth-node/types" 19 "github.com/status-im/status-go/ipfs" 20 "github.com/status-im/status-go/multiaccounts/accounts" 21 "github.com/status-im/status-go/rpc" 22 "github.com/status-im/status-go/server" 23 "github.com/status-im/status-go/services/wallet/bigint" 24 "github.com/status-im/status-go/transactions" 25 ) 26 27 const maxConcurrentRequests = 3 28 const requestTimeout = time.Duration(5) * time.Second 29 30 // ConnectionType constants 31 type stickerStatus int 32 33 const ( 34 statusAvailable stickerStatus = iota 35 statusInstalled 36 statusPending 37 statusPurchased 38 ) 39 40 type API struct { 41 contractMaker *contracts.ContractMaker 42 accountsManager *account.GethManager 43 accountsDB *accounts.Database 44 pendingTracker *transactions.PendingTxTracker 45 46 keyStoreDir string 47 downloader *ipfs.Downloader 48 httpServer *server.MediaServer 49 50 ctx context.Context 51 } 52 53 type Sticker struct { 54 PackID *bigint.BigInt `json:"packID,omitempty"` 55 URL string `json:"url,omitempty"` 56 Hash string `json:"hash,omitempty"` 57 } 58 59 type StickerPack struct { 60 ID *bigint.BigInt `json:"id"` 61 Name string `json:"name"` 62 Author string `json:"author"` 63 Owner common.Address `json:"owner,omitempty"` 64 Price *bigint.BigInt `json:"price"` 65 Preview string `json:"preview"` 66 Thumbnail string `json:"thumbnail"` 67 Stickers []Sticker `json:"stickers"` 68 69 Status stickerStatus `json:"status"` 70 } 71 72 type StickerPackCollection map[uint]StickerPack 73 74 type ednSticker struct { 75 Hash string 76 } 77 78 type ednStickerPack struct { 79 Name string 80 Author string 81 Thumbnail string 82 Preview string 83 Stickers []ednSticker 84 } 85 type ednStickerPackInfo struct { 86 Meta ednStickerPack 87 } 88 89 func NewAPI(ctx context.Context, acc *accounts.Database, rpcClient *rpc.Client, accountsManager *account.GethManager, pendingTracker *transactions.PendingTxTracker, keyStoreDir string, downloader *ipfs.Downloader, httpServer *server.MediaServer) *API { 90 result := &API{ 91 contractMaker: &contracts.ContractMaker{ 92 RPCClient: rpcClient, 93 }, 94 accountsManager: accountsManager, 95 accountsDB: acc, 96 pendingTracker: pendingTracker, 97 keyStoreDir: keyStoreDir, 98 downloader: downloader, 99 ctx: ctx, 100 httpServer: httpServer, 101 } 102 103 return result 104 } 105 106 func (api *API) Market(chainID uint64) ([]StickerPack, error) { 107 // TODO: eventually this should be changed to include pagination 108 accs, err := api.accountsDB.GetActiveAccounts() 109 if err != nil { 110 return nil, err 111 } 112 113 allStickerPacks, err := api.getContractPacks(chainID) 114 if err != nil { 115 return nil, err 116 } 117 118 purchasedPacks := make(map[uint]struct{}) 119 120 purchasedPackChan := make(chan *big.Int) 121 errChan := make(chan error) 122 doneChan := make(chan struct{}, 1) 123 go api.getAccountsPurchasedPack(chainID, accs, purchasedPackChan, errChan, doneChan) 124 125 for { 126 select { 127 case err := <-errChan: 128 if err != nil { 129 return nil, err 130 } 131 case packID := <-purchasedPackChan: 132 if packID != nil { 133 purchasedPacks[uint(packID.Uint64())] = struct{}{} 134 } 135 136 case <-doneChan: 137 var result []StickerPack 138 for _, pack := range allStickerPacks { 139 packID := uint(pack.ID.Uint64()) 140 _, isPurchased := purchasedPacks[packID] 141 if isPurchased { 142 pack.Status = statusPurchased 143 } else { 144 pack.Status = statusAvailable 145 } 146 result = append(result, pack) 147 } 148 149 return result, nil 150 } 151 } 152 } 153 154 func (api *API) execTokenPackID(chainID uint64, tokenIDs []*big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) { 155 defer close(doneChan) 156 defer close(errChan) 157 defer close(resultChan) 158 159 stickerPack, err := api.contractMaker.NewStickerPack(chainID) 160 if err != nil { 161 errChan <- err 162 return 163 } 164 165 if len(tokenIDs) == 0 { 166 return 167 } 168 169 callOpts := &bind.CallOpts{Context: api.ctx, Pending: false} 170 171 c := goccm.New(maxConcurrentRequests) 172 for _, tokenID := range tokenIDs { 173 c.Wait() 174 go func(tokenID *big.Int) { 175 defer c.Done() 176 packID, err := stickerPack.TokenPackId(callOpts, tokenID) 177 if err != nil { 178 errChan <- err 179 return 180 } 181 resultChan <- packID 182 }(tokenID) 183 } 184 c.WaitAllDone() 185 } 186 187 func (api *API) getTokenPackIDs(chainID uint64, tokenIDs []*big.Int) ([]*big.Int, error) { 188 tokenPackIDChan := make(chan *big.Int) 189 errChan := make(chan error) 190 doneChan := make(chan struct{}, 1) 191 192 go api.execTokenPackID(chainID, tokenIDs, tokenPackIDChan, errChan, doneChan) 193 194 var tokenPackIDs []*big.Int 195 for { 196 select { 197 case <-doneChan: 198 return tokenPackIDs, nil 199 case err := <-errChan: 200 if err != nil { 201 return nil, err 202 } 203 case t := <-tokenPackIDChan: 204 if t != nil { 205 tokenPackIDs = append(tokenPackIDs, t) 206 } 207 } 208 } 209 } 210 211 func (api *API) getPurchasedPackIDs(chainID uint64, account types.Address) ([]*big.Int, error) { 212 // TODO: this should be replaced in the future by something like TheGraph to reduce the number of requests to infura 213 214 stickerPack, err := api.contractMaker.NewStickerPack(chainID) 215 if err != nil { 216 return nil, err 217 } 218 219 callOpts := &bind.CallOpts{Context: api.ctx, Pending: false} 220 221 balance, err := stickerPack.BalanceOf(callOpts, common.Address(account)) 222 if err != nil { 223 return nil, err 224 } 225 226 tokenIDs, err := api.getTokenOwnerOfIndex(chainID, account, balance) 227 if err != nil { 228 return nil, err 229 } 230 231 return api.getTokenPackIDs(chainID, tokenIDs) 232 } 233 234 func (api *API) fetchStickerPacks(chainID uint64, resultChan chan<- *StickerPack, errChan chan<- error, doneChan chan<- struct{}) { 235 defer close(doneChan) 236 defer close(errChan) 237 defer close(resultChan) 238 239 installedPacks, err := api.Installed() 240 if err != nil { 241 errChan <- err 242 return 243 } 244 245 pendingPacks, err := api.pendingStickerPacks() 246 if err != nil { 247 errChan <- err 248 return 249 } 250 251 stickerType, err := api.contractMaker.NewStickerType(chainID) 252 if err != nil { 253 errChan <- err 254 return 255 } 256 257 callOpts := &bind.CallOpts{Context: api.ctx, Pending: false} 258 259 numPacks, err := stickerType.PackCount(callOpts) 260 if err != nil { 261 errChan <- err 262 return 263 } 264 265 if numPacks.Uint64() == 0 { 266 return 267 } 268 269 c := goccm.New(maxConcurrentRequests) 270 for i := uint64(0); i < numPacks.Uint64(); i++ { 271 c.Wait() 272 go func(i uint64) { 273 defer c.Done() 274 275 packID := new(big.Int).SetUint64(i) 276 277 _, exists := installedPacks[uint(i)] 278 if exists { 279 return // We already have the sticker pack data, no need to query it 280 } 281 282 _, exists = pendingPacks[uint(i)] 283 if exists { 284 return // We already have the sticker pack data, no need to query it 285 } 286 287 stickerPack, err := api.fetchPackData(stickerType, packID, true) 288 if err != nil { 289 log.Warn("Could not retrieve stickerpack data", "packID", packID, "error", err) 290 errChan <- err 291 return 292 } 293 294 resultChan <- stickerPack 295 }(i) 296 } 297 298 c.WaitAllDone() 299 } 300 301 func (api *API) fetchPackData(stickerType *stickers.StickerType, packID *big.Int, translateHashes bool) (*StickerPack, error) { 302 303 timeoutContext, timeoutCancel := context.WithTimeout(api.ctx, requestTimeout) 304 defer timeoutCancel() 305 306 callOpts := &bind.CallOpts{Context: timeoutContext, Pending: false} 307 packData, err := stickerType.GetPackData(callOpts, packID) 308 if err != nil { 309 return nil, err 310 } 311 312 stickerPack := &StickerPack{ 313 ID: &bigint.BigInt{Int: packID}, 314 Owner: packData.Owner, 315 Price: &bigint.BigInt{Int: packData.Price}, 316 } 317 318 err = api.downloadPackData(stickerPack, packData.Contenthash, translateHashes) 319 if err != nil { 320 return nil, err 321 } 322 323 return stickerPack, nil 324 } 325 326 func (api *API) downloadPackData(stickerPack *StickerPack, contentHash []byte, translateHashes bool) error { 327 fileContent, err := api.downloader.Get(hexutil.Encode(contentHash)[2:], true) 328 if err != nil { 329 return err 330 } 331 return api.populateStickerPackAttributes(stickerPack, fileContent, translateHashes) 332 } 333 334 func (api *API) hashToURL(hash string) string { 335 return api.httpServer.MakeStickerURL(hash) 336 } 337 338 func (api *API) populateStickerPackAttributes(stickerPack *StickerPack, ednSource []byte, translateHashes bool) error { 339 var stickerpackIPFSInfo ednStickerPackInfo 340 err := edn.Unmarshal(ednSource, &stickerpackIPFSInfo) 341 if err != nil { 342 return err 343 } 344 345 stickerPack.Author = stickerpackIPFSInfo.Meta.Author 346 stickerPack.Name = stickerpackIPFSInfo.Meta.Name 347 348 if translateHashes { 349 stickerPack.Preview = api.hashToURL(stickerpackIPFSInfo.Meta.Preview) 350 stickerPack.Thumbnail = api.hashToURL(stickerpackIPFSInfo.Meta.Thumbnail) 351 } else { 352 stickerPack.Preview = stickerpackIPFSInfo.Meta.Preview 353 stickerPack.Thumbnail = stickerpackIPFSInfo.Meta.Thumbnail 354 } 355 356 for _, s := range stickerpackIPFSInfo.Meta.Stickers { 357 url := "" 358 if translateHashes { 359 url = api.hashToURL(s.Hash) 360 } 361 362 stickerPack.Stickers = append(stickerPack.Stickers, Sticker{ 363 PackID: stickerPack.ID, 364 URL: url, 365 Hash: s.Hash, 366 }) 367 } 368 369 return nil 370 } 371 372 func (api *API) getContractPacks(chainID uint64) ([]StickerPack, error) { 373 stickerPackChan := make(chan *StickerPack) 374 errChan := make(chan error) 375 doneChan := make(chan struct{}, 1) 376 377 go api.fetchStickerPacks(chainID, stickerPackChan, errChan, doneChan) 378 379 var packs []StickerPack 380 381 for { 382 select { 383 case <-doneChan: 384 return packs, nil 385 case err := <-errChan: 386 if err != nil { 387 return nil, err 388 } 389 case pack := <-stickerPackChan: 390 if pack != nil { 391 packs = append(packs, *pack) 392 } 393 } 394 } 395 } 396 397 func (api *API) getAccountsPurchasedPack(chainID uint64, accs []*accounts.Account, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) { 398 defer close(doneChan) 399 defer close(errChan) 400 defer close(resultChan) 401 402 if len(accs) == 0 { 403 return 404 } 405 406 c := goccm.New(maxConcurrentRequests) 407 for _, account := range accs { 408 c.Wait() 409 go func(acc *accounts.Account) { 410 defer c.Done() 411 packs, err := api.getPurchasedPackIDs(chainID, acc.Address) 412 if err != nil { 413 errChan <- err 414 return 415 } 416 417 for _, p := range packs { 418 resultChan <- p 419 } 420 }(account) 421 } 422 c.WaitAllDone() 423 } 424 425 func (api *API) execTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) { 426 defer close(doneChan) 427 defer close(errChan) 428 defer close(resultChan) 429 430 stickerPack, err := api.contractMaker.NewStickerPack(chainID) 431 if err != nil { 432 errChan <- err 433 return 434 } 435 436 if balance.Int64() == 0 { 437 return 438 } 439 440 callOpts := &bind.CallOpts{Context: api.ctx, Pending: false} 441 442 c := goccm.New(maxConcurrentRequests) 443 for i := uint64(0); i < balance.Uint64(); i++ { 444 c.Wait() 445 go func(i uint64) { 446 defer c.Done() 447 tokenID, err := stickerPack.TokenOfOwnerByIndex(callOpts, common.Address(account), new(big.Int).SetUint64(i)) 448 if err != nil { 449 errChan <- err 450 return 451 } 452 453 resultChan <- tokenID 454 }(i) 455 } 456 c.WaitAllDone() 457 } 458 459 func (api *API) getTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int) ([]*big.Int, error) { 460 tokenIDChan := make(chan *big.Int) 461 errChan := make(chan error) 462 doneChan := make(chan struct{}, 1) 463 464 go api.execTokenOwnerOfIndex(chainID, account, balance, tokenIDChan, errChan, doneChan) 465 466 var tokenIDs []*big.Int 467 for { 468 select { 469 case <-doneChan: 470 return tokenIDs, nil 471 case err := <-errChan: 472 if err != nil { 473 return nil, err 474 } 475 case tokenID := <-tokenIDChan: 476 if tokenID != nil { 477 tokenIDs = append(tokenIDs, tokenID) 478 } 479 } 480 } 481 }