|
|
@ -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) { |
|
|
|