A wallet’s NFTs appear in B wallet #3132

pull/3191/head
Vladyslav shepitko 3 years ago
parent 5f9cb95721
commit 3400333e51
  1. 16
      AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift
  2. 141
      AlphaWallet/EtherClient/OpenSea.swift
  3. 70
      AlphaWallet/Tokens/Types/TokensDataStore.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<Bool?> in
let resolved = value.compactMap { $0.optionalValue }.flatMap { $0 }
return self.tokensDatastore.batchUpdateTokenPromise(resolved).recover { _ -> Guarantee<Bool?> in
return tokensDatastore.batchUpdateTokenPromise(resolved).recover { _ -> Guarantee<Bool?> in
return .value(nil)
}
})
@ -282,11 +281,12 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
}
private func addUnknownErc1155ContractsToDatabase(contractsTokenIdsAndValue: Erc1155TokenIds.ContractsTokenIdsAndValues, tokens: [Activity.AssignedToken]) -> Promise<Erc1155TokenIds.ContractsTokenIdsAndValues> {
firstly {
let tokensDatastore = self.tokensDatastore
return firstly {
functional.fetchUnknownErc1155ContractsDetails(contractsTokenIdsAndValue: contractsTokenIdsAndValue, tokens: tokens, server: server, account: account, assetDefinitionStore: assetDefinitionStore)
}.then(on: .main, { tokensToAdd -> Promise<Erc1155TokenIds.ContractsTokenIdsAndValues> in
let (promise, seal) = Promise<Erc1155TokenIds.ContractsTokenIdsAndValues>.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 {

@ -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<OpenSea>]()
private let server: RPCServer
private static var instances = [AddressAndRPCServer: WeakRef<OpenSea>]()
//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) {

@ -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<Bool?> {
return Promise<Bool?> { 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<Bool?> {
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):

Loading…
Cancel
Save