An advanced Ethereum/EVM mobile wallet
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
alpha-wallet-ios/AlphaWallet/UI/TokenObject+UI.swift

169 lines
6.4 KiB

// Copyright © 2020 Stormbird PTE. LTD.
import UIKit
import PromiseKit
typealias TokenImage = (image: UIImage, symbol: String)
extension TokenObject {
fileprivate static let numberOfCharactersOfSymbolToShowInIcon = 4
fileprivate var programmaticallyGeneratedIconImage: UIImage {
UIView.tokenSymbolBackgroundImage(backgroundColor: symbolBackgroundColor)
}
private var symbolBackgroundColor: UIColor {
if contractAddress.sameContract(as: Constants.nativeCryptoAddressInDatabase) {
return server.blockChainNameColor
} else {
let colors = [R.color.radical()!, R.color.cerulean()!, R.color.emerald()!, R.color.indigo()!, R.color.azure()!, R.color.pumpkin()!]
let index: Int
//We just need a random number from the contract. The LSBs are more random than the MSBs
if let i = Int(contractAddress.eip55String.substring(from: 37), radix: 16) {
index = i % colors.count
} else {
index = 0
}
return colors[index]
}
}
var icon: Subscribable<TokenImage> {
switch type {
case .nativeCryptocurrency:
if let img = server.iconImage {
return .init((image: img, symbol: ""))
}
case .erc20, .erc875, .erc721, .erc721ForTickets:
if let img = contractAddress.tokenImage {
return .init((image: img, symbol: ""))
}
}
return TokenImageFetcher.instance.image(forToken: self)
}
}
private class TokenImageFetcher {
private enum ImageAvailabilityError: LocalizedError {
case notAvailable
}
static var instance = TokenImageFetcher()
private var subscribables: [String: Subscribable<TokenImage>] = .init()
private func programmaticallyGenerateIcon(forToken tokenObject: TokenObject) -> TokenImage {
let i = [TokenObject.numberOfCharactersOfSymbolToShowInIcon, tokenObject.symbol.count].min()!
let symbol = tokenObject.symbol.substring(to: i)
return (image: tokenObject.programmaticallyGeneratedIconImage, symbol: symbol)
}
//Relies on built-in HTTP/HTTPS caching in iOS for the images
func image(forToken tokenObject: TokenObject) -> Subscribable<TokenImage> {
let subscribable: Subscribable<TokenImage>
let key = "\(tokenObject.contractAddress.eip55String)-\(tokenObject.server.chainID)"
if let sub = subscribables[key] {
subscribable = sub
} else {
let sub = Subscribable<TokenImage>(nil)
subscribables[key] = sub
subscribable = sub
}
if tokenObject.contractAddress.sameContract(as: Constants.nativeCryptoAddressInDatabase) {
subscribable.value = programmaticallyGenerateIcon(forToken: tokenObject)
return subscribable
}
fetchFromOpenSea(tokenObject).done {
subscribable.value = (image: $0, symbol: "")
}.catch { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.fetchFromAssetGitHubRepo(tokenObject).done {
subscribable.value = (image: $0, symbol: "")
}.catch { [weak self] _ in
guard let strongSelf = self else { return }
subscribable.value = strongSelf.programmaticallyGenerateIcon(forToken: tokenObject)
}
}
return subscribable
}
private func fetchFromOpenSea(_ tokenObject: TokenObject) -> Promise<UIImage> {
Promise { seal in
switch tokenObject.type {
case .erc721:
if let json = tokenObject.balance.first?.balance, let data = json.data(using: .utf8), let openSeaNonFungible = try? JSONDecoder().decode(OpenSeaNonFungible.self, from: data), !openSeaNonFungible.contractImageUrl.isEmpty {
let request = URLRequest(url: URL(string: openSeaNonFungible.contractImageUrl)!)
fetch(request: request).done { image in
seal.fulfill(image)
}.catch { _ in
seal.reject(ImageAvailabilityError.notAvailable)
}
}
case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
seal.reject(ImageAvailabilityError.notAvailable)
}
}
}
private func fetchFromAssetGitHubRepo(_ tokenObject: TokenObject) -> Promise<UIImage> {
return GithubAssetsURLResolver().resolve(for: tokenObject).then { request -> Promise<UIImage> in
self.fetch(request: request)
}
}
private func fetch(request: URLRequest) -> Promise<UIImage> {
Promise { seal in
let task = URLSession.shared.dataTask(with: request) { data, _, _ in
if let data = data {
let image = UIImage(data: data)
if let img = image {
seal.fulfill(img)
} else {
seal.reject(ImageAvailabilityError.notAvailable)
}
} else {
seal.reject(ImageAvailabilityError.notAvailable)
}
}
task.resume()
}
}
}
class GithubAssetsURLResolver {
static let file = "logo.png"
enum Source: String {
case testNetTokensSource = "https://raw.githubusercontent.com/alphawallet/iconassets/master/"
case allTokensSource = "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/"
}
enum AnyError: Error {
case case1
}
func resolve(for tokenObject: TokenObject) -> Promise<URLRequest> {
let value = tokenObject.server.githubAssetsSource.rawValue + tokenObject.contractAddress.eip55String + "/" + GithubAssetsURLResolver.file
guard let url = URL(string: value) else {
return .init(error: AnyError.case1)
}
let request = URLRequest(url: url)
return .value(request)
}
}
fileprivate extension RPCServer {
var githubAssetsSource: GithubAssetsURLResolver.Source {
switch self {
case .rinkeby, .ropsten, .sokol, .kovan, .goerli:
return .testNetTokensSource
case .main, .poa, .classic, .callisto, .xDai, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .custom:
return .allTokensSource
}
}
}