* Fixed token autodetection for several chains

* ARTIS sigma1
    * ARTIS tau1
    * Sokol
    * Callisto
    * Binance and testnet
    * Heco and testnet
    * Mumbai testnet
* Refactoring of how and where Etherscan API and web URLs are specified
pull/2838/head
Hwee-Boon Yar 3 years ago
parent e099d58d5a
commit dd58f1134e
  1. 4
      AlphaWallet/InCoordinator.swift
  2. 40
      AlphaWallet/Settings/Types/Constants.swift
  3. 164
      AlphaWallet/Settings/Types/RPCServers.swift
  4. 8
      AlphaWallet/Tokens/Coordinators/GetContractInteractions.swift

@ -792,12 +792,12 @@ extension InCoordinator: CanOpenURL {
func didPressViewContractWebPage(forContract contract: AlphaWallet.Address, server: RPCServer, in viewController: UIViewController) {
if contract.sameContract(as: Constants.nativeCryptoAddressInDatabase) {
guard let url = server.etherscanContractDetailsWebPageURL(for: wallet.address) else { return }
logExplorerUse(type: .wallet)
let url = server.etherscanContractDetailsWebPageURL(for: wallet.address)
open(url: url, in: viewController)
} else {
guard let url = server.etherscanTokenDetailsWebPageURL(for: contract) else { return }
logExplorerUse(type: .token)
let url = server.etherscanTokenDetailsWebPageURL(for: contract)
open(url: url, in: viewController)
}
}

@ -76,46 +76,6 @@ public struct Constants {
//UEFA 721 balances function hash
static let balances165Hash721Ticket = "0xc84aae17"
//etherscan-compatible erc20 transaction event APIs
//The fetch ERC20 transactions endpoint from Etherscan returns only ERC20 token transactions but the Blockscout version also includes ERC721 transactions too (so it's likely other types that it can detect will be returned too); thus we check the token type rather than assume that they are all ERC20
public static let mainnetEtherscanAPIErc20Events = "https://api-cn.etherscan.com/api?module=account&action=tokentx&address="
public static let ropstenEtherscanAPIErc20Events = "https://api-ropsten.etherscan.io/api?module=account&action=tokentx&address="
public static let kovanEtherscanAPIErc20Events = "https://api-kovan.etherscan.io/api?module=account&action=tokentx&address="
public static let rinkebyEtherscanAPIErc20Events = "https://api-rinkeby.etherscan.io/api?module=account&action=tokentx&address="
public static let classicAPIErc20Events = "https://blockscout.com/etc/mainnet/api?module=account&action=tokentx&address="
public static let xDaiAPIErc20Events = "https://blockscout.com/poa/dai/api?module=account&action=tokentx&address="
public static let poaNetworkCoreAPIErc20Events = "https://blockscout.com/poa/core/api?module=account&action=tokentx&address="
public static let goerliEtherscanAPIErc20Events = "https://api-goerli.etherscan.io/api?module=account&action=tokentx&address="
public static let artisSigma1NetworkCoreAPIErc20Events = "https://explorer.sigma1.artis.network/api?module=account&action=tokentx&address="
public static let artisTau1NetworkCoreAPIErc20Events = "https://explorer.tau1.artis.network/api?module=account&action=tokentx&address="
public static let mainnetEtherscanTokenDetailsWebPageURL = "https://cn.etherscan.com/token/"
//etherscan-compatible contract details web page
public static let mainnetEtherscanContractDetailsWebPageURL = "https://cn.etherscan.com/address/"
public static let kovanEtherscanContractDetailsWebPageURL = "https://kovan.etherscan.io/address/"
public static let rinkebyEtherscanContractDetailsWebPageURL = "https://rinkeby.etherscan.io/address/"
public static let ropstenEtherscanContractDetailsWebPageURL = "https://ropsten.etherscan.io/address/"
//Can't use https://blockscout.com/poa/dai/address/ even though it ultimately redirects there because blockscout (tested on 20190620), blockscout.com is only able to show that URL after the address has been searched (with the ?q= URL)
public static let xDaiContractPage = "https://blockscout.com/poa/dai/search?q="
public static let poaContractPage = "https://blockscout.com/poa/core/search?q="
public static let goerliContractPage = "https://goerli.etherscan.io/address/"
public static let sokolContractPage = "https://blockscout.com/poa/sokol/search?q="
public static let etcContractPage = "https://blockscout.com/etc/mainnet/search?q="
public static let callistoContractPage = "https://blockscout.com/callisto/mainnet/search?q="
public static let artisSigma1ContractPage = "https://explorer.sigma1.artis.network/search?q="
public static let artisTau1ContractPage = "https://explorer.tau1.artis.network/search?q="
public static let binanceContractPage = "https://bscscan.com/search?q="
public static let binanceTestnetContractPage = "https://testnet.bscscan.com/search?q="
public static let hecoContractPage = "https://scan.hecochain.com/address/"
public static let hecoTestnetContractPage = "https://scan-testnet.hecochain.com/address/"
public static let fantomContractPage = "https://ftmscan.com/address/"
public static let fantomTestnetContractPage = "https://ftmscan.com/address/"
public static let avalancheContractPage = "https://cchain.explorer.avax.network/address/"
public static let avalancheTestnetContractPage = "https://cchain.explorer.avax-test.network/address/"
public static let maticContractPage = "https://explorer-mainnet.maticvigil.com/address/"
public static let mumbaiContractPage = "https://explorer-mumbai.maticvigil.com/address/"
//OpenSea links for erc721 assets
public static let openseaAPI = "https://api.opensea.io/"
public static let openseaRinkebyAPI = "https://rinkeby-api.opensea.io/"

@ -109,10 +109,10 @@ enum RPCServer: Hashable, CaseIterable {
}
}
var getEtherscanURL: URL? {
var etherscanURLForGeneralTransactionHistory: URL? {
switch self {
case .main, .ropsten, .rinkeby, .kovan, .poa, .classic, .goerli, .xDai, .artis_sigma1, .artis_tau1, .polygon, .binance_smart_chain, .binance_smart_chain_testnet, .sokol, .callisto:
return etherscanRoot.appendingQueryString("module=account&action=txlist")
return etherscanApiRoot.appendingQueryString("module=account&action=txlist")
case .heco: return nil
case .heco_testnet: return nil
case .custom: return nil
@ -124,35 +124,48 @@ enum RPCServer: Hashable, CaseIterable {
}
}
var getEtherscanURLERC20Events: String? {
switch self {
case .main: return Constants.mainnetEtherscanAPIErc20Events
case .ropsten: return Constants.ropstenEtherscanAPIErc20Events
case .rinkeby: return Constants.rinkebyEtherscanAPIErc20Events
case .kovan: return Constants.kovanEtherscanAPIErc20Events
case .poa: return Constants.poaNetworkCoreAPIErc20Events
case .sokol: return nil
case .classic: return Constants.classicAPIErc20Events
case .callisto: return nil
case .goerli: return Constants.goerliEtherscanAPIErc20Events
case .xDai: return Constants.xDaiAPIErc20Events
case .artis_sigma1: return nil
case .artis_tau1: return nil
case .binance_smart_chain: return nil
case .binance_smart_chain_testnet: return nil
case .heco: return nil
case .heco_testnet: return nil
case .custom: return nil
case .fantom: return nil
case .fantom_testnet: return nil
case .avalanche: return nil
case .avalanche_testnet: return nil
case .polygon: return "https://explorer-mainnet.maticvigil.com/api/v2/transactions?module=account&action=tokentx&address="
case .mumbai_testnet: return nil
///etherscan-compatible erc20 transaction event APIs
///The fetch ERC20 transactions endpoint from Etherscan returns only ERC20 token transactions but the Blockscout version also includes ERC721 transactions too (so it's likely other types that it can detect will be returned too); thus we should check the token type rather than assume that they are all ERC20
var etherscanURLForTokenTransactionHistory: URL? {
switch etherscanCompatibleType {
case .etherscan, .blockscout:
return etherscanApiRoot.appendingQueryString("module=account&action=tokentx")
case .unknown:
return nil
}
}
var etherscanRoot: URL {
var etherscanWebpageRoot: URL? {
let urlString: String? = {
switch self {
case .main: return "https://cn.etherscan.com"
case .ropsten: return "https://ropsten.etherscan.io"
case .rinkeby: return "https://rinkeby.etherscan.io"
case .kovan: return "https://kovan.etherscan.io"
case .goerli: return "https://goerli.etherscan.io"
case .heco_testnet: return "https://scan-testnet.hecochain.com"
case .heco: return "https://scan.hecochain.com"
case .fantom: return "https://ftmscan.com"
case .xDai: return "https://blockscout.com/poa/dai"
case .poa: return "https://blockscout.com/poa/core"
case .sokol: return "https://blockscout.com/poa/sokol"
case .classic: return "https://blockscout.com/etc/mainnet"
case .callisto: return "https://blockscout.com/callisto/mainnet"
case .artis_sigma1: return "https://explorer.sigma1.artis.network"
case .artis_tau1: return "https://explorer.tau1.artis.network"
case .binance_smart_chain: return "https://bscscan.com"
case .binance_smart_chain_testnet: return "https://testnet.bscscan.com"
case .polygon: return "https://explorer-mainnet.maticvigil.com"
case .mumbai_testnet: return "https://explorer-mumbai.maticvigil.com"
case .custom: return nil
case .fantom_testnet, .avalanche, .avalanche_testnet:
return nil
}
}()
return urlString.flatMap { URL(string: $0) }
}
var etherscanApiRoot: URL {
let urlString: String = {
switch self {
case .main: return "https://api-cn.etherscan.com/api"
@ -179,7 +192,7 @@ enum RPCServer: Hashable, CaseIterable {
case .avalanche: return "https://cchain.explorer.avax.network/tx/api"
//TODO fix etherscan-compatible API endpoint
case .avalanche_testnet: return "https://cchain.explorer.avax-test.network/tx/api"
case .polygon: return "https://explorer-mainnet.maticvigil.com/api/v2/"
case .polygon: return "https://explorer-mainnet.maticvigil.com/api/v2"
case .mumbai_testnet: return "https://explorer-mumbai.maticvigil.com/api/v2"
}
}()
@ -187,56 +200,18 @@ enum RPCServer: Hashable, CaseIterable {
}
//If Etherscan, action=tokentx for ERC20 and action=tokennfttx for ERC721. If Blockscout-compatible, action=tokentx includes both ERC20 and ERC721. tokennfttx is not supported.
var getEtherscanURLERC721Events: URL? {
switch erc721TransactionHistoryType {
var etherscanURLForERC721TransactionHistory: URL? {
switch etherscanCompatibleType {
case .etherscan:
return etherscanRoot.appendingQueryString("module=account&action=tokennfttx")
return etherscanApiRoot.appendingQueryString("module=account&action=tokennfttx")
case .blockscout:
return etherscanRoot.appendingPathComponent("transactions").appendingQueryString("module=account&action=tokentx")
return etherscanApiRoot.appendingQueryString("module=account&action=tokentx")
case .unknown:
return nil
}
}
var etherscanContractDetailsWebPageURL: String {
switch self {
case .main: return Constants.mainnetEtherscanContractDetailsWebPageURL
case .ropsten: return Constants.ropstenEtherscanContractDetailsWebPageURL
case .rinkeby: return Constants.rinkebyEtherscanContractDetailsWebPageURL
case .kovan: return Constants.kovanEtherscanContractDetailsWebPageURL
case .xDai: return Constants.xDaiContractPage
case .goerli: return Constants.goerliContractPage
case .poa: return Constants.poaContractPage
case .sokol: return Constants.sokolContractPage
case .classic: return Constants.etcContractPage
case .callisto: return Constants.callistoContractPage
case .artis_sigma1: return Constants.artisSigma1ContractPage
case .artis_tau1: return Constants.artisTau1ContractPage
case .binance_smart_chain: return Constants.binanceContractPage
case .binance_smart_chain_testnet: return Constants.binanceTestnetContractPage
case .custom: return Constants.mainnetEtherscanContractDetailsWebPageURL
case .heco_testnet: return Constants.hecoTestnetContractPage
case .heco: return Constants.hecoContractPage
case .fantom: return Constants.fantomContractPage
case .fantom_testnet: return Constants.fantomTestnetContractPage
case .avalanche: return Constants.avalancheContractPage
case .avalanche_testnet: return Constants.avalancheTestnetContractPage
case .polygon: return Constants.maticContractPage
case .mumbai_testnet: return Constants.mumbaiContractPage
}
}
//We assume that only Etherscan supports this and only for Ethereum mainnet: The token page instead of contract page
var etherscanTokenDetailsWebPageURL: String {
switch self {
case .main:
return Constants.mainnetEtherscanTokenDetailsWebPageURL
case .ropsten, .rinkeby, .kovan, .xDai, .goerli, .poa, .sokol, .classic, .callisto, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .custom, .heco, .heco_testnet, .fantom, .fantom_testnet, .avalanche, .avalanche_testnet, .polygon, .mumbai_testnet:
return etherscanContractDetailsWebPageURL
}
}
private var erc721TransactionHistoryType: EtherscanCompatibleType {
private var etherscanCompatibleType: EtherscanCompatibleType {
switch self {
case .main, .ropsten, .rinkeby, .kovan, .goerli, .fantom, .heco, .heco_testnet:
return .etherscan
@ -247,8 +222,8 @@ enum RPCServer: Hashable, CaseIterable {
}
}
func etherscanAPIURLForTransactionList(for address: AlphaWallet.Address, startBlock: Int?) -> URL? {
getEtherscanURL.flatMap {
func getEtherscanURLForGeneralTransactionHistory(for address: AlphaWallet.Address, startBlock: Int?) -> URL? {
etherscanURLForGeneralTransactionHistory.flatMap {
let url = $0.appendingQueryString("address=\(address.eip55String)&apikey=\(Constants.Credentials.etherscanKey)")
if let startBlock = startBlock {
return url?.appendingQueryString("startBlock=\(startBlock)")
@ -258,18 +233,19 @@ enum RPCServer: Hashable, CaseIterable {
}
}
func etherscanAPIURLForERC20TxList(for address: AlphaWallet.Address, startBlock: Int?) -> URL? {
getEtherscanURLERC20Events.flatMap {
var url = "\($0)\(address.eip55String)&apikey=\(Constants.Credentials.etherscanKey)"
func getEtherscanURLForTokenTransactionHistory(for address: AlphaWallet.Address, startBlock: Int?) -> URL? {
etherscanURLForTokenTransactionHistory.flatMap {
let url = $0.appendingQueryString("address=\(address.eip55String)&apikey=\(Constants.Credentials.etherscanKey)")
if let startBlock = startBlock {
url = "\(url)&startBlock=\(startBlock)"
return url?.appendingQueryString("startBlock=\(startBlock)")
} else {
return url
}
return URL(string: url)
}
}
func etherscanAPIURLForERC721TxList(for address: AlphaWallet.Address, startBlock: Int?) -> URL? {
getEtherscanURLERC721Events.flatMap {
func getEtherscanURLForERC721TransactionHistory(for address: AlphaWallet.Address, startBlock: Int?) -> URL? {
etherscanURLForERC721TransactionHistory.flatMap {
let url = $0.appendingQueryString("address=\(address.eip55String)&apikey=\(Constants.Credentials.etherscanKey)")
if let startBlock = startBlock {
return url?.appendingQueryString("startBlock=\(startBlock)")
@ -279,12 +255,28 @@ enum RPCServer: Hashable, CaseIterable {
}
}
func etherscanContractDetailsWebPageURL(for address: AlphaWallet.Address) -> URL {
return URL(string: etherscanContractDetailsWebPageURL + address.eip55String)!
//Can't use https://blockscout.com/poa/dai/address/ even though it ultimately redirects there because blockscout (tested on 20190620), blockscout.com is only able to show that URL after the address has been searched (with the ?q= URL)
func etherscanContractDetailsWebPageURL(for address: AlphaWallet.Address) -> URL? {
switch etherscanCompatibleType {
case .etherscan:
return etherscanWebpageRoot?.appendingPathComponent("address").appendingPathComponent(address.eip55String)
case .blockscout:
return etherscanWebpageRoot?.appendingPathComponent("search").appendingQueryString("q=\(address.eip55String)")
case .unknown:
return nil
}
}
func etherscanTokenDetailsWebPageURL(for address: AlphaWallet.Address) -> URL {
return URL(string: etherscanTokenDetailsWebPageURL + address.eip55String)!
//We assume that only Etherscan supports this and only for Ethereum mainnet: The token page instead of contract page
//TODO check if other Etherscan networks can support this
//TODO check if Blockscout can support this
func etherscanTokenDetailsWebPageURL(for address: AlphaWallet.Address) -> URL? {
switch self {
case .main:
return etherscanWebpageRoot?.appendingPathComponent("token").appendingPathComponent(address.eip55String)
case .ropsten, .rinkeby, .kovan, .xDai, .goerli, .poa, .sokol, .classic, .callisto, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .custom, .heco, .heco_testnet, .fantom, .fantom_testnet, .avalanche, .avalanche_testnet, .polygon, .mumbai_testnet:
return etherscanContractDetailsWebPageURL(for: address)
}
}
var priceID: AlphaWallet.Address {
@ -452,7 +444,7 @@ enum RPCServer: Hashable, CaseIterable {
let urlString: String = {
switch self {
case .main, .kovan, .ropsten, .rinkeby, .goerli, .classic, .poa, .xDai, .sokol, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .fantom, .polygon, .heco, .heco_testnet, .callisto:
return etherscanRoot.absoluteString
return etherscanApiRoot.absoluteString
case .custom: return "" // Enable? make optional
case .fantom_testnet: return "https://explorer.testnet.fantom.network/tx/"
case .avalanche: return "https://cchain.explorer.avax.network/tx/"

@ -17,7 +17,7 @@ class GetContractInteractions {
//TODO rename this since it might include ERC721 (blockscout and compatible like Polygon's). Or can we make this really fetch ERC20, maybe by filtering the results?
func getErc20Interactions(contractAddress: AlphaWallet.Address? = nil, address: AlphaWallet.Address, server: RPCServer, startBlock: Int? = nil, completion: @escaping ([TransactionInstance]) -> Void) {
guard let etherscanURL = server.etherscanAPIURLForERC20TxList(for: address, startBlock: startBlock) else { return }
guard let etherscanURL = server.getEtherscanURLForTokenTransactionHistory(for: address, startBlock: startBlock) else { return }
Alamofire.request(etherscanURL).validate().responseJSON(queue: queue, options: [], completionHandler: { response in
switch response.result {
@ -89,7 +89,7 @@ class GetContractInteractions {
//TODO Almost a duplicate of the the ERC20 version. De-dup maybe?
func getErc721Interactions(contractAddress: AlphaWallet.Address? = nil, address: AlphaWallet.Address, server: RPCServer, startBlock: Int? = nil, completion: @escaping ([TransactionInstance]) -> Void) {
guard let etherscanURL = server.etherscanAPIURLForERC721TxList(for: address, startBlock: startBlock) else { return }
guard let etherscanURL = server.getEtherscanURLForERC721TransactionHistory(for: address, startBlock: startBlock) else { return }
Alamofire.request(etherscanURL).validate().responseJSON(queue: queue, options: [], completionHandler: { response in
switch response.result {
@ -162,13 +162,13 @@ class GetContractInteractions {
func getContractList(address: AlphaWallet.Address, server: RPCServer, startBlock: Int? = nil, erc20: Bool, completion: @escaping ([AlphaWallet.Address], Int?) -> Void) {
let etherscanURL: URL
if erc20 {
if let url = server.etherscanAPIURLForERC20TxList(for: address, startBlock: startBlock) {
if let url = server.getEtherscanURLForTokenTransactionHistory(for: address, startBlock: startBlock) {
etherscanURL = url
} else {
return
}
} else {
if let url = server.etherscanAPIURLForTransactionList(for: address, startBlock: startBlock) {
if let url = server.getEtherscanURLForGeneralTransactionHistory(for: address, startBlock: startBlock) {
etherscanURL = url
} else {
return

Loading…
Cancel
Save