From 3400333e513adec62be39e83ed7aec53737b02ca Mon Sep 17 00:00:00 2001 From: Vladyslav shepitko Date: Wed, 15 Sep 2021 17:16:26 +0300 Subject: [PATCH] =?UTF-8?q?A=20wallet=E2=80=99s=20NFTs=20appear=20in=20B?= =?UTF-8?q?=20wallet=20#3132?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WalletBalance/PrivateBalanceFetcher.swift | 16 +- AlphaWallet/EtherClient/OpenSea.swift | 141 +++++++++--------- .../Tokens/Types/TokensDataStore.swift | 70 +-------- 3 files changed, 85 insertions(+), 142 deletions(-) diff --git a/AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift b/AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift index c926ba6a9..d5feb1db6 100644 --- a/AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift +++ b/AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift @@ -41,7 +41,6 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType { }() private let account: Wallet - private let openSea: OpenSea private let queue: DispatchQueue private let server: RPCServer @@ -56,7 +55,7 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType { self.account = account self.server = server self.queue = queue - self.openSea = OpenSea.createInstance(forServer: server) + self.openSea = OpenSea.createInstance(with: AddressAndRPCServer(address: account.address, server: server)) self.tokensDatastore = tokensDatastore self.assetDefinitionStore = assetDefinitionStore @@ -89,7 +88,7 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType { private func getTokensFromOpenSea() -> OpenSea.PromiseResult { //TODO when we no longer create multiple instances of TokensDataStore, we don't have to use singleton for OpenSea class. This was to avoid fetching multiple times from OpenSea concurrently - return openSea.makeFetchPromise(forOwner: account.address) + return openSea.makeFetchPromise() } func refreshBalance(updatePolicy: RefreshBalancePolicy, force: Bool = false) { @@ -169,11 +168,11 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType { let promise1 = refreshBalanceForNonErc721Or1155Tokens(tokens: notErc721Or1155Tokens) let promise2 = refreshBalanceForErc721Or1155Tokens(tokens: erc721Or1155Tokens) - + let tokensDatastore = self.tokensDatastore return when(resolved: [promise1, promise2]).then(on: queue, { value -> Promise in let resolved = value.compactMap { $0.optionalValue }.flatMap { $0 } - return self.tokensDatastore.batchUpdateTokenPromise(resolved).recover { _ -> Guarantee in + return tokensDatastore.batchUpdateTokenPromise(resolved).recover { _ -> Guarantee in return .value(nil) } }) @@ -282,11 +281,12 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType { } private func addUnknownErc1155ContractsToDatabase(contractsTokenIdsAndValue: Erc1155TokenIds.ContractsTokenIdsAndValues, tokens: [Activity.AssignedToken]) -> Promise { - firstly { + let tokensDatastore = self.tokensDatastore + return firstly { functional.fetchUnknownErc1155ContractsDetails(contractsTokenIdsAndValue: contractsTokenIdsAndValue, tokens: tokens, server: server, account: account, assetDefinitionStore: assetDefinitionStore) }.then(on: .main, { tokensToAdd -> Promise in let (promise, seal) = Promise.pending() - self.tokensDatastore.addCustom(tokens: tokensToAdd, shouldUpdateBalance: false) + tokensDatastore.addCustom(tokens: tokensToAdd, shouldUpdateBalance: false) seal.fulfill(contractsTokenIdsAndValue) return promise }) @@ -496,7 +496,7 @@ extension PrivateBalanceFetcher { fileprivate extension PrivateBalanceFetcher.functional { static func fetchUnknownErc1155ContractsDetails(contractsTokenIdsAndValue: Erc1155TokenIds.ContractsTokenIdsAndValues, tokens: [Activity.AssignedToken], server: RPCServer, account: Wallet, assetDefinitionStore: AssetDefinitionStore) -> Promise<[ERCToken]> { let contractsToAdd: [AlphaWallet.Address] = contractsTokenIdsAndValue.keys.filter { contract in - !tokens.contains(where: { $0.contractAddress.sameContract(as: contract)}) + !tokens.contains(where: { $0.contractAddress.sameContract(as: contract) }) } guard !contractsToAdd.isEmpty else { diff --git a/AlphaWallet/EtherClient/OpenSea.swift b/AlphaWallet/EtherClient/OpenSea.swift index c4d69cd47..9ebd0935d 100644 --- a/AlphaWallet/EtherClient/OpenSea.swift +++ b/AlphaWallet/EtherClient/OpenSea.swift @@ -19,22 +19,24 @@ class OpenSea { //Assuming 1 token (token ID, rather than a token) is 4kb, 1500 HyperDragons is 6MB. So we rate limit requests private static let numberOfTokenIdsBeforeRateLimitingRequests = 25 private static let minimumSecondsBetweenRequests = TimeInterval(60) - private static var instances = [RPCServer: WeakRef]() - - private let server: RPCServer + private static var instances = [AddressAndRPCServer: WeakRef]() + //NOTE: using AddressAndRPCServer fixes issue with incorrect tokens returned from makeFetchPromise + // the problem was that cached OpenSea returned tokens from multiple wallets + private let key: AddressAndRPCServer private var recentWalletsWithManyTokens = [AlphaWallet.Address: (Date, PromiseResult)]() private var fetch = OpenSea.makeEmptyFulfilledPromise() + private let queue = DispatchQueue.global(qos: .userInitiated) - private init(server: RPCServer) { - self.server = server + private init(key: AddressAndRPCServer) { + self.key = key } - static func createInstance(forServer server: RPCServer) -> OpenSea { - if let instance = instances[server]?.object { + static func createInstance(with key: AddressAndRPCServer) -> OpenSea { + if let instance = instances[key]?.object { return instance } else { - let instance = OpenSea(server: server) - instances[server] = WeakRef(object: instance) + let instance = OpenSea(key: key) + instances[key] = WeakRef(object: instance) return instance } } @@ -66,12 +68,12 @@ class OpenSea { } ///Uses a promise to make sure we don't fetch from OpenSea multiple times concurrently - func makeFetchPromise(forOwner owner: AlphaWallet.Address) -> PromiseResult { - guard OpenSea.isServerSupported(server) else { + func makeFetchPromise() -> PromiseResult { + guard OpenSea.isServerSupported(key.server) else { fetch = .value([:]) return fetch } - + let owner = key.address trimCachedPromises() if let cachedPromise = cachedPromise(forOwner: owner) { return cachedPromise @@ -83,6 +85,7 @@ class OpenSea { fetchPage(forOwner: owner, offset: offset) { result in switch result { case .success(let result): + seal.fulfill(result) case .failure(let error): seal.reject(error) @@ -94,7 +97,7 @@ class OpenSea { } private func getBaseURLForOpensea() -> String { - switch server { + switch key.server { case .main: return Constants.openseaAPI case .rinkeby: @@ -111,75 +114,75 @@ class OpenSea { completion(.failure(AnyError(OpenSeaError(localizedDescription: "Error calling \(baseURL) API \(Thread.isMainThread)")))) return } + Alamofire.request( url, method: .get, headers: ["X-API-KEY": Constants.Credentials.openseaKey] - ).responseJSON { response in + ).responseJSON(queue: queue, options: .allowFragments, completionHandler: { [weak self] response in + guard let strongSelf = self else { return } guard let data = response.data, let json = try? JSON(data: data) else { completion(.failure(AnyError(OpenSeaError(localizedDescription: "Error calling \(baseURL) API: \(String(describing: response.error))")))) return } - DispatchQueue.global(qos: .userInitiated).async { - var results = sum - for (_, each): (String, JSON) in json["assets"] { - let type = each["asset_contract"]["schema_name"].stringValue - guard let tokenType = NonFungibleFromJsonTokenType(rawString: type) else { continue } - let tokenId = each["token_id"].stringValue - let contractName = each["asset_contract"]["name"].stringValue - //So if it's null in OpenSea, we get a 0, as expected. And 0 works for ERC721 too - let decimals = each["decimals"].intValue - let symbol = each["asset_contract"]["symbol"].stringValue - let name = each["name"].stringValue - let description = each["description"].stringValue - let thumbnailUrl = each["image_thumbnail_url"].stringValue - //We'll get what seems to be the PNG version first, falling back to the sometimes PNG, but sometimes SVG version - var imageUrl = each["image_preview_url"].stringValue - if imageUrl.isEmpty { - imageUrl = each["image_url"].stringValue - } - let contractImageUrl = each["asset_contract"]["image_url"].stringValue - let externalLink = each["external_link"].stringValue - let backgroundColor = each["background_color"].stringValue - var traits = [OpenSeaNonFungibleTrait]() - for each in each["traits"].arrayValue { - let traitCount = each["trait_count"].intValue - let traitType = each["trait_type"].stringValue - let traitValue = each["value"].stringValue - let trait = OpenSeaNonFungibleTrait(count: traitCount, type: traitType, value: traitValue) - traits.append(trait) - } - if let contract = AlphaWallet.Address(string: each["asset_contract"]["address"].stringValue) { - let cat = OpenSeaNonFungible(tokenId: tokenId, tokenType: tokenType, contractName: contractName, decimals: decimals, symbol: symbol, name: name, description: description, thumbnailUrl: thumbnailUrl, imageUrl: imageUrl, contractImageUrl: contractImageUrl, externalLink: externalLink, backgroundColor: backgroundColor, traits: traits) - if var list = results[contract] { - list.append(cat) - results[contract] = list - } else { - let list = [cat] - results[contract] = list - } - } + + var results = sum + for (_, each): (String, JSON) in json["assets"] { + let type = each["asset_contract"]["schema_name"].stringValue + guard let tokenType = NonFungibleFromJsonTokenType(rawString: type) else { continue } + let tokenId = each["token_id"].stringValue + let contractName = each["asset_contract"]["name"].stringValue + //So if it's null in OpenSea, we get a 0, as expected. And 0 works for ERC721 too + let decimals = each["decimals"].intValue + let symbol = each["asset_contract"]["symbol"].stringValue + let name = each["name"].stringValue + let description = each["description"].stringValue + let thumbnailUrl = each["image_thumbnail_url"].stringValue + //We'll get what seems to be the PNG version first, falling back to the sometimes PNG, but sometimes SVG version + var imageUrl = each["image_preview_url"].stringValue + if imageUrl.isEmpty { + imageUrl = each["image_url"].stringValue + } + let contractImageUrl = each["asset_contract"]["image_url"].stringValue + let externalLink = each["external_link"].stringValue + let backgroundColor = each["background_color"].stringValue + var traits = [OpenSeaNonFungibleTrait]() + for each in each["traits"].arrayValue { + let traitCount = each["trait_count"].intValue + let traitType = each["trait_type"].stringValue + let traitValue = each["value"].stringValue + let trait = OpenSeaNonFungibleTrait(count: traitCount, type: traitType, value: traitValue) + traits.append(trait) } - DispatchQueue.main.async { [weak self] in - guard let strongSelf = self else { return } - let fetchedCount = json["assets"].count - if fetchedCount > 0 { - strongSelf.fetchPage(forOwner: owner, offset: offset + fetchedCount, sum: results) { results in - completion(results) - } + if let contract = AlphaWallet.Address(string: each["asset_contract"]["address"].stringValue) { + let cat = OpenSeaNonFungible(tokenId: tokenId, tokenType: tokenType, contractName: contractName, decimals: decimals, symbol: symbol, name: name, description: description, thumbnailUrl: thumbnailUrl, imageUrl: imageUrl, contractImageUrl: contractImageUrl, externalLink: externalLink, backgroundColor: backgroundColor, traits: traits) + if var list = results[contract] { + list.append(cat) + results[contract] = list } else { - //Ignore UEFA from OpenSea, otherwise the token type would be saved wrongly as `.erc721` instead of `.erc721ForTickets` - let excludingUefa = sum.filter { !$0.key.isUEFATicketContract } - var tokenIdCount = 0 - for (_, tokenIds) in excludingUefa { - tokenIdCount += tokenIds.count - } - strongSelf.cachePromise(withTokenIdCount: tokenIdCount, forOwner: owner) - completion(.success(excludingUefa)) + let list = [cat] + results[contract] = list } } } - } + + let fetchedCount = json["assets"].count + if fetchedCount > 0 { + strongSelf.fetchPage(forOwner: owner, offset: offset + fetchedCount, sum: results) { results in + completion(results) + } + } else { + //Ignore UEFA from OpenSea, otherwise the token type would be saved wrongly as `.erc721` instead of `.erc721ForTickets` + let excludingUefa = sum.filter { !$0.key.isUEFATicketContract } + var tokenIdCount = 0 + for (_, tokenIds) in excludingUefa { + tokenIdCount += tokenIds.count + } + strongSelf.cachePromise(withTokenIdCount: tokenIdCount, forOwner: owner) + + completion(.success(excludingUefa)) + } + }) } private func cachePromise(withTokenIdCount tokenIdCount: Int, forOwner wallet: AlphaWallet.Address) { diff --git a/AlphaWallet/Tokens/Types/TokensDataStore.swift b/AlphaWallet/Tokens/Types/TokensDataStore.swift index b19b55b0a..652e825c0 100644 --- a/AlphaWallet/Tokens/Types/TokensDataStore.swift +++ b/AlphaWallet/Tokens/Types/TokensDataStore.swift @@ -244,7 +244,7 @@ class TokensDataStore: NSObject { return newToken } - @discardableResult private func unsafeAddTokenOperation(tokenObject: TokenObject, realm: Realm) -> TokenObject { + @discardableResult private func addTokenUnsafe(tokenObject: TokenObject, realm: Realm) -> TokenObject { //TODO: save existed sort index and displaying state if let object = realm.object(ofType: TokenObject.self, forPrimaryKey: tokenObject.primaryKey) { tokenObject.sortIndex = object.sortIndex @@ -267,10 +267,10 @@ class TokensDataStore: NSObject { realm.add(delegateContract, update: .all) case .ercToken(let token): let newToken = Self.tokenObject(ercToken: token, shouldUpdateBalance: token.type.shouldUpdateBalanceWhenDetected) - unsafeAddTokenOperation(tokenObject: newToken, realm: realm) + addTokenUnsafe(tokenObject: newToken, realm: realm) tokenObjects += [newToken] case .tokenObject(let tokenObject): - unsafeAddTokenOperation(tokenObject: tokenObject, realm: realm) + addTokenUnsafe(tokenObject: tokenObject, realm: realm) tokenObjects += [tokenObject] case .deletedContracts(let deadContracts): realm.add(deadContracts, update: .all) @@ -306,7 +306,7 @@ class TokensDataStore: NSObject { //TODO: save existed sort index and displaying state for token in tokens { - unsafeAddTokenOperation(tokenObject: token, realm: realm) + addTokenUnsafe(tokenObject: token, realm: realm) } try! realm.commitWrite() @@ -359,66 +359,6 @@ class TokensDataStore: NSObject { enabledObjectsSubscription.flatMap { $0.invalidate() } } - func addOrUpdateErc271(contract: AlphaWallet.Address, openSeaNonFungibles: [OpenSeaNonFungible], tokens: [Activity.AssignedToken]) -> Promise { - return Promise { seal in - DispatchQueue.main.async { [weak self] in - guard let strongSelf = self else { return seal.reject(PMKError.cancelled) } - - var listOfJson = [String]() - var anyNonFungible: OpenSeaNonFungible? - for each in openSeaNonFungibles { - if let encodedJson = try? JSONEncoder().encode(each), let jsonString = String(data: encodedJson, encoding: .utf8) { - anyNonFungible = each - listOfJson.append(jsonString) - } else { - //no op - } - } - - if let tokenObject = tokens.first(where: { $0.contractAddress.sameContract(as: contract) }) { - strongSelf.realm.beginWrite() - - var value: Bool? - switch tokenObject.type { - case .nativeCryptocurrency, .erc721, .erc875, .erc721ForTickets, .erc1155: - break - case .erc20: - value = strongSelf.updateTokenUnsafe(primaryKey: tokenObject.primaryKey, action: .type(.erc721)) - } - - let v1 = strongSelf.updateTokenUnsafe(primaryKey: tokenObject.primaryKey, action: .nonFungibleBalance(listOfJson)) - if value != nil { - value = v1 - } - if let anyNonFungible = anyNonFungible { - let v2 = strongSelf.updateTokenUnsafe(primaryKey: tokenObject.primaryKey, action: .name(anyNonFungible.contractName)) - if value != nil { - value = v2 - } - } - - try! strongSelf.realm.commitWrite() - - seal.fulfill(value) - } else { - let token = ERCToken( - contract: contract, - server: strongSelf.server, - name: openSeaNonFungibles[0].contractName, - symbol: openSeaNonFungibles[0].symbol, - decimals: 0, - type: .erc721, - balance: listOfJson - ) - - strongSelf.addCustom(token: token, shouldUpdateBalance: true) - - seal.fulfill(true) - } - } - } - } - func batchUpdateTokenPromise(_ actions: [PrivateBalanceFetcher.TokenBatchOperation]) -> Promise { return Promise { seal in DispatchQueue.main.async { [weak self] in @@ -432,7 +372,7 @@ class TokensDataStore: NSObject { switch each { case .add(let token, let shouldUpdateBalance): let newToken = TokensDataStore.tokenObject(ercToken: token, shouldUpdateBalance: shouldUpdateBalance) - strongSelf.unsafeAddTokenOperation(tokenObject: newToken, realm: strongSelf.realm) + strongSelf.addTokenUnsafe(tokenObject: newToken, realm: strongSelf.realm) value = true case .update(let tokenObject, let action):