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 account: Wallet
private let openSea: OpenSea private let openSea: OpenSea
private let queue: DispatchQueue private let queue: DispatchQueue
private let server: RPCServer private let server: RPCServer
@ -56,7 +55,7 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
self.account = account self.account = account
self.server = server self.server = server
self.queue = queue self.queue = queue
self.openSea = OpenSea.createInstance(forServer: server) self.openSea = OpenSea.createInstance(with: AddressAndRPCServer(address: account.address, server: server))
self.tokensDatastore = tokensDatastore self.tokensDatastore = tokensDatastore
self.assetDefinitionStore = assetDefinitionStore self.assetDefinitionStore = assetDefinitionStore
@ -89,7 +88,7 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
private func getTokensFromOpenSea() -> OpenSea.PromiseResult { 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 //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) { func refreshBalance(updatePolicy: RefreshBalancePolicy, force: Bool = false) {
@ -169,11 +168,11 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
let promise1 = refreshBalanceForNonErc721Or1155Tokens(tokens: notErc721Or1155Tokens) let promise1 = refreshBalanceForNonErc721Or1155Tokens(tokens: notErc721Or1155Tokens)
let promise2 = refreshBalanceForErc721Or1155Tokens(tokens: erc721Or1155Tokens) let promise2 = refreshBalanceForErc721Or1155Tokens(tokens: erc721Or1155Tokens)
let tokensDatastore = self.tokensDatastore
return when(resolved: [promise1, promise2]).then(on: queue, { value -> Promise<Bool?> in return when(resolved: [promise1, promise2]).then(on: queue, { value -> Promise<Bool?> in
let resolved = value.compactMap { $0.optionalValue }.flatMap { $0 } 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) return .value(nil)
} }
}) })
@ -282,11 +281,12 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
} }
private func addUnknownErc1155ContractsToDatabase(contractsTokenIdsAndValue: Erc1155TokenIds.ContractsTokenIdsAndValues, tokens: [Activity.AssignedToken]) -> Promise<Erc1155TokenIds.ContractsTokenIdsAndValues> { 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) functional.fetchUnknownErc1155ContractsDetails(contractsTokenIdsAndValue: contractsTokenIdsAndValue, tokens: tokens, server: server, account: account, assetDefinitionStore: assetDefinitionStore)
}.then(on: .main, { tokensToAdd -> Promise<Erc1155TokenIds.ContractsTokenIdsAndValues> in }.then(on: .main, { tokensToAdd -> Promise<Erc1155TokenIds.ContractsTokenIdsAndValues> in
let (promise, seal) = Promise<Erc1155TokenIds.ContractsTokenIdsAndValues>.pending() let (promise, seal) = Promise<Erc1155TokenIds.ContractsTokenIdsAndValues>.pending()
self.tokensDatastore.addCustom(tokens: tokensToAdd, shouldUpdateBalance: false) tokensDatastore.addCustom(tokens: tokensToAdd, shouldUpdateBalance: false)
seal.fulfill(contractsTokenIdsAndValue) seal.fulfill(contractsTokenIdsAndValue)
return promise return promise
}) })
@ -496,7 +496,7 @@ extension PrivateBalanceFetcher {
fileprivate extension PrivateBalanceFetcher.functional { fileprivate extension PrivateBalanceFetcher.functional {
static func fetchUnknownErc1155ContractsDetails(contractsTokenIdsAndValue: Erc1155TokenIds.ContractsTokenIdsAndValues, tokens: [Activity.AssignedToken], server: RPCServer, account: Wallet, assetDefinitionStore: AssetDefinitionStore) -> Promise<[ERCToken]> { 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 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 { 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 //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 numberOfTokenIdsBeforeRateLimitingRequests = 25
private static let minimumSecondsBetweenRequests = TimeInterval(60) private static let minimumSecondsBetweenRequests = TimeInterval(60)
private static var instances = [RPCServer: WeakRef<OpenSea>]() private static var instances = [AddressAndRPCServer: WeakRef<OpenSea>]()
//NOTE: using AddressAndRPCServer fixes issue with incorrect tokens returned from makeFetchPromise
private let server: RPCServer // the problem was that cached OpenSea returned tokens from multiple wallets
private let key: AddressAndRPCServer
private var recentWalletsWithManyTokens = [AlphaWallet.Address: (Date, PromiseResult)]() private var recentWalletsWithManyTokens = [AlphaWallet.Address: (Date, PromiseResult)]()
private var fetch = OpenSea.makeEmptyFulfilledPromise() private var fetch = OpenSea.makeEmptyFulfilledPromise()
private let queue = DispatchQueue.global(qos: .userInitiated)
private init(server: RPCServer) { private init(key: AddressAndRPCServer) {
self.server = server self.key = key
} }
static func createInstance(forServer server: RPCServer) -> OpenSea { static func createInstance(with key: AddressAndRPCServer) -> OpenSea {
if let instance = instances[server]?.object { if let instance = instances[key]?.object {
return instance return instance
} else { } else {
let instance = OpenSea(server: server) let instance = OpenSea(key: key)
instances[server] = WeakRef(object: instance) instances[key] = WeakRef(object: instance)
return instance return instance
} }
} }
@ -66,12 +68,12 @@ class OpenSea {
} }
///Uses a promise to make sure we don't fetch from OpenSea multiple times concurrently ///Uses a promise to make sure we don't fetch from OpenSea multiple times concurrently
func makeFetchPromise(forOwner owner: AlphaWallet.Address) -> PromiseResult { func makeFetchPromise() -> PromiseResult {
guard OpenSea.isServerSupported(server) else { guard OpenSea.isServerSupported(key.server) else {
fetch = .value([:]) fetch = .value([:])
return fetch return fetch
} }
let owner = key.address
trimCachedPromises() trimCachedPromises()
if let cachedPromise = cachedPromise(forOwner: owner) { if let cachedPromise = cachedPromise(forOwner: owner) {
return cachedPromise return cachedPromise
@ -83,6 +85,7 @@ class OpenSea {
fetchPage(forOwner: owner, offset: offset) { result in fetchPage(forOwner: owner, offset: offset) { result in
switch result { switch result {
case .success(let result): case .success(let result):
seal.fulfill(result) seal.fulfill(result)
case .failure(let error): case .failure(let error):
seal.reject(error) seal.reject(error)
@ -94,7 +97,7 @@ class OpenSea {
} }
private func getBaseURLForOpensea() -> String { private func getBaseURLForOpensea() -> String {
switch server { switch key.server {
case .main: case .main:
return Constants.openseaAPI return Constants.openseaAPI
case .rinkeby: case .rinkeby:
@ -111,75 +114,75 @@ class OpenSea {
completion(.failure(AnyError(OpenSeaError(localizedDescription: "Error calling \(baseURL) API \(Thread.isMainThread)")))) completion(.failure(AnyError(OpenSeaError(localizedDescription: "Error calling \(baseURL) API \(Thread.isMainThread)"))))
return return
} }
Alamofire.request( Alamofire.request(
url, url,
method: .get, method: .get,
headers: ["X-API-KEY": Constants.Credentials.openseaKey] 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 { 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))")))) completion(.failure(AnyError(OpenSeaError(localizedDescription: "Error calling \(baseURL) API: \(String(describing: response.error))"))))
return return
} }
DispatchQueue.global(qos: .userInitiated).async {
var results = sum var results = sum
for (_, each): (String, JSON) in json["assets"] { for (_, each): (String, JSON) in json["assets"] {
let type = each["asset_contract"]["schema_name"].stringValue let type = each["asset_contract"]["schema_name"].stringValue
guard let tokenType = NonFungibleFromJsonTokenType(rawString: type) else { continue } guard let tokenType = NonFungibleFromJsonTokenType(rawString: type) else { continue }
let tokenId = each["token_id"].stringValue let tokenId = each["token_id"].stringValue
let contractName = each["asset_contract"]["name"].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 //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 decimals = each["decimals"].intValue
let symbol = each["asset_contract"]["symbol"].stringValue let symbol = each["asset_contract"]["symbol"].stringValue
let name = each["name"].stringValue let name = each["name"].stringValue
let description = each["description"].stringValue let description = each["description"].stringValue
let thumbnailUrl = each["image_thumbnail_url"].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 //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 var imageUrl = each["image_preview_url"].stringValue
if imageUrl.isEmpty { if imageUrl.isEmpty {
imageUrl = each["image_url"].stringValue imageUrl = each["image_url"].stringValue
} }
let contractImageUrl = each["asset_contract"]["image_url"].stringValue let contractImageUrl = each["asset_contract"]["image_url"].stringValue
let externalLink = each["external_link"].stringValue let externalLink = each["external_link"].stringValue
let backgroundColor = each["background_color"].stringValue let backgroundColor = each["background_color"].stringValue
var traits = [OpenSeaNonFungibleTrait]() var traits = [OpenSeaNonFungibleTrait]()
for each in each["traits"].arrayValue { for each in each["traits"].arrayValue {
let traitCount = each["trait_count"].intValue let traitCount = each["trait_count"].intValue
let traitType = each["trait_type"].stringValue let traitType = each["trait_type"].stringValue
let traitValue = each["value"].stringValue let traitValue = each["value"].stringValue
let trait = OpenSeaNonFungibleTrait(count: traitCount, type: traitType, value: traitValue) let trait = OpenSeaNonFungibleTrait(count: traitCount, type: traitType, value: traitValue)
traits.append(trait) 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
}
}
} }
DispatchQueue.main.async { [weak self] in if let contract = AlphaWallet.Address(string: each["asset_contract"]["address"].stringValue) {
guard let strongSelf = self else { return } 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)
let fetchedCount = json["assets"].count if var list = results[contract] {
if fetchedCount > 0 { list.append(cat)
strongSelf.fetchPage(forOwner: owner, offset: offset + fetchedCount, sum: results) { results in results[contract] = list
completion(results)
}
} else { } else {
//Ignore UEFA from OpenSea, otherwise the token type would be saved wrongly as `.erc721` instead of `.erc721ForTickets` let list = [cat]
let excludingUefa = sum.filter { !$0.key.isUEFATicketContract } results[contract] = list
var tokenIdCount = 0
for (_, tokenIds) in excludingUefa {
tokenIdCount += tokenIds.count
}
strongSelf.cachePromise(withTokenIdCount: tokenIdCount, forOwner: owner)
completion(.success(excludingUefa))
} }
} }
} }
}
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) { private func cachePromise(withTokenIdCount tokenIdCount: Int, forOwner wallet: AlphaWallet.Address) {

@ -244,7 +244,7 @@ class TokensDataStore: NSObject {
return newToken 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 //TODO: save existed sort index and displaying state
if let object = realm.object(ofType: TokenObject.self, forPrimaryKey: tokenObject.primaryKey) { if let object = realm.object(ofType: TokenObject.self, forPrimaryKey: tokenObject.primaryKey) {
tokenObject.sortIndex = object.sortIndex tokenObject.sortIndex = object.sortIndex
@ -267,10 +267,10 @@ class TokensDataStore: NSObject {
realm.add(delegateContract, update: .all) realm.add(delegateContract, update: .all)
case .ercToken(let token): case .ercToken(let token):
let newToken = Self.tokenObject(ercToken: token, shouldUpdateBalance: token.type.shouldUpdateBalanceWhenDetected) let newToken = Self.tokenObject(ercToken: token, shouldUpdateBalance: token.type.shouldUpdateBalanceWhenDetected)
unsafeAddTokenOperation(tokenObject: newToken, realm: realm) addTokenUnsafe(tokenObject: newToken, realm: realm)
tokenObjects += [newToken] tokenObjects += [newToken]
case .tokenObject(let tokenObject): case .tokenObject(let tokenObject):
unsafeAddTokenOperation(tokenObject: tokenObject, realm: realm) addTokenUnsafe(tokenObject: tokenObject, realm: realm)
tokenObjects += [tokenObject] tokenObjects += [tokenObject]
case .deletedContracts(let deadContracts): case .deletedContracts(let deadContracts):
realm.add(deadContracts, update: .all) realm.add(deadContracts, update: .all)
@ -306,7 +306,7 @@ class TokensDataStore: NSObject {
//TODO: save existed sort index and displaying state //TODO: save existed sort index and displaying state
for token in tokens { for token in tokens {
unsafeAddTokenOperation(tokenObject: token, realm: realm) addTokenUnsafe(tokenObject: token, realm: realm)
} }
try! realm.commitWrite() try! realm.commitWrite()
@ -359,66 +359,6 @@ class TokensDataStore: NSObject {
enabledObjectsSubscription.flatMap { $0.invalidate() } 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?> { func batchUpdateTokenPromise(_ actions: [PrivateBalanceFetcher.TokenBatchOperation]) -> Promise<Bool?> {
return Promise { seal in return Promise { seal in
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
@ -432,7 +372,7 @@ class TokensDataStore: NSObject {
switch each { switch each {
case .add(let token, let shouldUpdateBalance): case .add(let token, let shouldUpdateBalance):
let newToken = TokensDataStore.tokenObject(ercToken: token, shouldUpdateBalance: 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 value = true
case .update(let tokenObject, let action): case .update(let tokenObject, let action):

Loading…
Cancel
Save