Fix contract detection (of name, symbol, decimals, balance) to handle ERC721 not having name and symbol. Also refactoring

pull/3100/head
Hwee-Boon Yar 3 years ago
parent 7f3a158c4f
commit a540450678
  1. 12
      AlphaWallet.xcodeproj/project.pbxproj
  2. 2
      AlphaWallet/Browser/Coordinators/QRCodeResolutionCoordinator.swift
  3. 2
      AlphaWallet/Core/Coordinators/CustomUrlSchemeCoordinator.swift
  4. 135
      AlphaWallet/Tokens/Coordinators/SingleChainTokenCoordinator.swift
  5. 171
      AlphaWallet/Tokens/Logic/ContractDataDetector.swift
  6. 2
      AlphaWallet/Transfer/Coordinators/SendCoordinator.swift

@ -576,6 +576,7 @@
5E7C7DCE5242D2AC0A8DA65C /* TokenCardRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CAA3D0C19444005EA83 /* TokenCardRowViewModel.swift */; };
5E7C7DCF09B84E5675D58CED /* DiscoverDappCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74294562EB79EFCD3559 /* DiscoverDappCellViewModel.swift */; };
5E7C7DD4D2EAA036961F18F0 /* DAppRequster.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C778A54D7D3E196BC5542 /* DAppRequster.swift */; };
5E7C7DE51E4829295CABDC21 /* ContractDataDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F9030DA27EF7C3FBDEB /* ContractDataDetector.swift */; };
5E7C7DEE91E5A1B4A41826EB /* ActivityRowModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76B6284FE1484292EB4F /* ActivityRowModel.swift */; };
5E7C7DFC309C7CC499202375 /* DecodedFunctionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7A71851FA227231270BD /* DecodedFunctionCall.swift */; };
5E7C7E02785866606FF298F3 /* OpenSeaNonFungibleTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72FBC0D2787AAA804098 /* OpenSeaNonFungibleTokenViewCellViewModel.swift */; };
@ -1581,6 +1582,7 @@
5E7C7F840AFFD4459FD3DBD6 /* DiscoverDappsViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoverDappsViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C7F89E3480D3680750EA9 /* TokenRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenRowView.swift; sourceTree = "<group>"; };
5E7C7F8F3CB3847D0E4E977B /* AlphaWalletAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlphaWalletAddress.swift; sourceTree = "<group>"; };
5E7C7F9030DA27EF7C3FBDEB /* ContractDataDetector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContractDataDetector.swift; sourceTree = "<group>"; };
5E7C7F932B48011A24C26733 /* TokensCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensCoordinator.swift; sourceTree = "<group>"; };
5E7C7FA63647CED0B0BA5A9C /* WalletConnectSessionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionCell.swift; sourceTree = "<group>"; };
5E7C7FB7C3FB2A9CC0CC51D7 /* TokensViewControllerTableViewSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensViewControllerTableViewSectionHeader.swift; sourceTree = "<group>"; };
@ -2290,6 +2292,7 @@
442FCB1A0F74A68425907AC9 /* Helpers */,
5E7C7E2DCCE0D775ECF83088 /* WalletFilter.swift */,
5E7C72C3D9C939508F8EBFAC /* Models */,
5E7C7CF7DCE51F1980CFE2BE /* Logic */,
);
path = Tokens;
sourceTree = "<group>";
@ -3702,6 +3705,14 @@
path = RPC;
sourceTree = "<group>";
};
5E7C7CF7DCE51F1980CFE2BE /* Logic */ = {
isa = PBXGroup;
children = (
5E7C7F9030DA27EF7C3FBDEB /* ContractDataDetector.swift */,
);
path = Logic;
sourceTree = "<group>";
};
5E7C7D05CF5206E7F9573D5F /* TrustCore */ = {
isa = PBXGroup;
children = (
@ -5742,6 +5753,7 @@
5E7C72B4302A10E137EEF94A /* DappRequestSwitchCustomChainCoordinator.swift in Sources */,
5E7C732AA43385E80681B24F /* EmailList.swift in Sources */,
5E7C776B88B6CDC18CA39D72 /* Environment.swift in Sources */,
5E7C7DE51E4829295CABDC21 /* ContractDataDetector.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

@ -251,7 +251,7 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate {
return .value((transactionType, token))
} else {
return Promise { resolver in
fetchContractDataFor(address: contract, storage: storage, assetDefinitionStore: assetDefinitionStore) { result in
ContractDataDetector(address: contract, storage: storage, assetDefinitionStore: assetDefinitionStore).fetch { result in
switch result {
case .name, .symbol, .balance, .decimals, .nonFungibleTokenComplete, .delegateTokenComplete, .failed:
resolver.reject(CheckEIP681Error.contractInvalid)

@ -39,7 +39,7 @@ class CustomUrlSchemeCoordinator: Coordinator {
if tokensDatastore.token(forContract: contract) != nil {
strongSelf.openSendPayFlowFor(server: server, contract: contract, recipient: recipient, amount: amount)
} else {
fetchContractDataFor(address: contract, storage: tokensDatastore, assetDefinitionStore: strongSelf.assetDefinitionStore) { data in
ContractDataDetector(address: contract, storage: tokensDatastore, assetDefinitionStore: strongSelf.assetDefinitionStore).fetch { data in
switch data {
case .name, .symbol, .balance, .decimals:
break

@ -8,17 +8,6 @@ import RealmSwift
import PromiseKit
import Result
enum ContractData {
case name(String)
case symbol(String)
case balance(balance: [String], tokenType: TokenType)
case decimals(UInt8)
case nonFungibleTokenComplete(name: String, symbol: String, balance: [String], tokenType: TokenType)
case fungibleTokenComplete(name: String, symbol: String, decimals: UInt8)
case delegateTokenComplete
case failed(networkReachable: Bool?)
}
struct NoTokenError: LocalizedError {
var errorDescription: String? {
return R.string.localizable.aWalletNoTokens()
@ -369,7 +358,7 @@ class SingleChainTokenCoordinator: Coordinator {
}
func fetchContractData(for address: AlphaWallet.Address, completion: @escaping (ContractData) -> Void) {
fetchContractDataFor(address: address, storage: storage, assetDefinitionStore: assetDefinitionStore, completion: completion)
ContractDataDetector(address: address, storage: storage, assetDefinitionStore: assetDefinitionStore).fetch(completion: completion)
}
func showTokenList(for type: PaymentFlow, token: TokenObject, navigationController: UINavigationController) {
@ -718,125 +707,3 @@ extension SingleChainTokenCoordinator: TransactionInProgressCoordinatorDelegate
removeCoordinator(coordinator)
}
}
/// Failure to obtain contract data may be due to no-connectivity. So we should check .failed(networkReachable: Bool)
// swiftlint:disable function_body_length
func fetchContractDataFor(address: AlphaWallet.Address, storage: TokensDataStore, assetDefinitionStore: AssetDefinitionStore, completion: @escaping (ContractData) -> Void) {
var completedName: String?
var completedSymbol: String?
var completedBalance: [String]?
var completedDecimals: UInt8?
var completedTokenType: TokenType?
var failed = false
func callCompletionFailed() {
guard !failed else { return }
failed = true
//TODO maybe better to share an instance of the reachability manager
completion(.failed(networkReachable: NetworkReachabilityManager()?.isReachable))
}
func callCompletionAsDelegateTokenOrNot() {
assert(completedSymbol != nil && completedSymbol?.isEmpty == true)
//Must check because we also get an empty symbol (and name) if there's no connectivity
//TODO maybe better to share an instance of the reachability manager
if let reachabilityManager = NetworkReachabilityManager(), reachabilityManager.isReachable {
completion(.delegateTokenComplete)
} else {
callCompletionFailed()
}
}
func callCompletionOnAllData() {
if let completedName = completedName, let completedSymbol = completedSymbol, let completedBalance = completedBalance, let tokenType = completedTokenType {
if completedSymbol.isEmpty {
callCompletionAsDelegateTokenOrNot()
} else {
completion(.nonFungibleTokenComplete(name: completedName, symbol: completedSymbol, balance: completedBalance, tokenType: tokenType))
}
} else if let completedName = completedName, let completedSymbol = completedSymbol, let completedDecimals = completedDecimals {
if completedSymbol.isEmpty {
callCompletionAsDelegateTokenOrNot()
} else {
completion(.fungibleTokenComplete(name: completedName, symbol: completedSymbol, decimals: completedDecimals))
}
}
}
assetDefinitionStore.fetchXML(forContract: address)
storage.getContractName(for: address) { result in
switch result {
case .success(let name):
completedName = name
completion(.name(name))
callCompletionOnAllData()
case .failure:
callCompletionFailed()
}
}
storage.getContractSymbol(for: address) { result in
switch result {
case .success(let symbol):
completedSymbol = symbol
completion(.symbol(symbol))
callCompletionOnAllData()
case .failure:
callCompletionFailed()
}
}
storage.getTokenType(for: address) { tokenType in
completedTokenType = tokenType
switch tokenType {
case .erc875:
storage.getERC875Balance(for: address) { result in
switch result {
case .success(let balance):
completedBalance = balance
completion(.balance(balance: balance, tokenType: .erc875))
callCompletionOnAllData()
case .failure:
callCompletionFailed()
}
}
case .erc721:
storage.getERC721Balance(for: address) { result in
switch result {
case .success(let balance):
completedBalance = balance
completion(.balance(balance: balance, tokenType: .erc721))
callCompletionOnAllData()
case .failure:
callCompletionFailed()
}
}
case .erc721ForTickets:
storage.getERC721ForTicketsBalance(for: address) { result in
switch result {
case .success(let balance):
completedBalance = balance
completion(.balance(balance: balance, tokenType: .erc721ForTickets))
callCompletionOnAllData()
case .failure:
callCompletionFailed()
}
}
case .erc20:
storage.getDecimals(for: address) { result in
switch result {
case .success(let decimal):
completedDecimals = decimal
completion(.decimals(decimal))
callCompletionOnAllData()
case .failure:
callCompletionFailed()
}
}
case .nativeCryptocurrency:
break
}
}
}
// swiftlint:enable function_body_length

@ -0,0 +1,171 @@
// Copyright © 2021 Stormbird PTE. LTD.
import Foundation
import Alamofire
import PromiseKit
enum ContractData {
case name(String)
case symbol(String)
case balance(balance: [String], tokenType: TokenType)
case decimals(UInt8)
case nonFungibleTokenComplete(name: String, symbol: String, balance: [String], tokenType: TokenType)
case fungibleTokenComplete(name: String, symbol: String, decimals: UInt8)
case delegateTokenComplete
case failed(networkReachable: Bool?)
}
class ContractDataDetector {
private let address: AlphaWallet.Address
private let storage: TokensDataStore
private let assetDefinitionStore: AssetDefinitionStore
private let namePromise: Promise<String>
private let symbolPromise: Promise<String>
private let tokenTypePromise: Promise<TokenType>
private let (nonFungibleBalancePromise, nonFungibleBalanceSeal) = Promise<[String]>.pending()
private let (decimalsPromise, decimalsSeal) = Promise<UInt8>.pending()
private var failed = false
private var completion: ((ContractData) -> Void)?
init(address: AlphaWallet.Address, storage: TokensDataStore, assetDefinitionStore: AssetDefinitionStore) {
self.address = address
self.storage = storage
self.assetDefinitionStore = assetDefinitionStore
namePromise = storage.getContractName(for: address)
symbolPromise = storage.getContractSymbol(for: address)
tokenTypePromise = storage.getTokenType(for: address)
}
/// Failure to obtain contract data may be due to no-connectivity. So we should check .failed(networkReachable: Bool)
//Have to use strong self in promises below, otherwise `self` will be destroyed before fetching completes
func fetch(completion: @escaping (ContractData) -> Void) {
self.completion = completion
assetDefinitionStore.fetchXML(forContract: address)
firstly {
namePromise
}.done { name in
self.completionOfPartialData(.name(name))
}.catch { error in
self.callCompletionFailed()
//We consider name and symbol and empty string because NFTs (ERC721 and ERC1155) don't have to implement `name` and `symbol`. Eg. ENS/721 (0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85) and Enjin/1155 (0xfaafdc07907ff5120a76b34b731b278c38d6043c)
self.completionOfPartialData(.name(""))
}
firstly {
symbolPromise
}.done { symbol in
self.completionOfPartialData(.symbol(symbol))
}.catch { error in
self.callCompletionFailed()
//We consider name and symbol and empty string because NFTs (ERC721 and ERC1155) don't have to implement `name` and `symbol`. Eg. ENS/721 (0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85) and Enjin/1155 (0xfaafdc07907ff5120a76b34b731b278c38d6043c)
self.completionOfPartialData(.symbol(""))
}
firstly {
tokenTypePromise
}.done { tokenType in
switch tokenType {
case .erc875:
self.storage.getERC875Balance(for: self.address) { result in
switch result {
case .success(let balance):
self.nonFungibleBalanceSeal.fulfill(balance)
self.completionOfPartialData(.balance(balance: balance, tokenType: .erc875))
case .failure(let error):
self.nonFungibleBalanceSeal.reject(error)
self.callCompletionFailed()
}
}
case .erc721:
self.storage.getERC721Balance(for: self.address) { result in
switch result {
case .success(let balance):
self.nonFungibleBalanceSeal.fulfill(balance)
self.completionOfPartialData(.balance(balance: balance, tokenType: .erc721))
case .failure(let error):
self.nonFungibleBalanceSeal.reject(error)
self.callCompletionFailed()
}
}
case .erc721ForTickets:
self.storage.getERC721ForTicketsBalance(for: self.address) { result in
switch result {
case .success(let balance):
self.nonFungibleBalanceSeal.fulfill(balance)
self.completionOfPartialData(.balance(balance: balance, tokenType: .erc721ForTickets))
case .failure(let error):
self.nonFungibleBalanceSeal.reject(error)
self.callCompletionFailed()
}
}
case .erc20:
self.storage.getDecimals(for: self.address) { result in
switch result {
case .success(let decimal):
self.decimalsSeal.fulfill(decimal)
self.completionOfPartialData(.decimals(decimal))
case .failure(let error):
self.decimalsSeal.reject(error)
self.callCompletionFailed()
}
}
case .nativeCryptocurrency:
break
}
}.cauterize()
}
private func completionOfPartialData(_ data: ContractData) -> Void {
completion?(data)
callCompletionOnAllData()
}
private func callCompletionFailed() {
guard !failed else {
return
}
failed = true
//TODO maybe better to share an instance of the reachability manager
completion?(.failed(networkReachable: NetworkReachabilityManager()?.isReachable))
}
private func callCompletionAsDelegateTokenOrNot() {
assert(symbolPromise.value != nil && symbolPromise.value?.isEmpty == true)
//Must check because we also get an empty symbol (and name) if there's no connectivity
//TODO maybe better to share an instance of the reachability manager
if let reachabilityManager = NetworkReachabilityManager(), reachabilityManager.isReachable {
completion?(.delegateTokenComplete)
} else {
callCompletionFailed()
}
}
private func callCompletionOnAllData() {
if namePromise.isResolved, symbolPromise.isResolved, let tokenType = tokenTypePromise.value {
switch tokenType {
case .erc875, .erc721, .erc721ForTickets:
if let nonFungibleBalance = nonFungibleBalancePromise.value {
let name = namePromise.value
let symbol = symbolPromise.value
completion?(.nonFungibleTokenComplete(name: name ?? "", symbol: symbol ?? "", balance: nonFungibleBalance, tokenType: tokenType))
}
case .nativeCryptocurrency, .erc20:
if let name = namePromise.value, let symbol = symbolPromise.value, let decimals = decimalsPromise.value {
if symbol.isEmpty {
callCompletionAsDelegateTokenOrNot()
} else {
completion?(.fungibleTokenComplete(name: name, symbol: symbol, decimals: decimals))
}
}
}
} else if let name = namePromise.value, let symbol = symbolPromise.value, let decimals = decimalsPromise.value {
if symbol.isEmpty {
callCompletionAsDelegateTokenOrNot()
} else {
completion?(.fungibleTokenComplete(name: name, symbol: symbol, decimals: decimals))
}
}
}
}

@ -135,7 +135,7 @@ extension SendCoordinator: SendViewControllerDelegate {
}
func lookup(contract: AlphaWallet.Address, in viewController: SendViewController, completion: @escaping (ContractData) -> Void) {
fetchContractDataFor(address: contract, storage: storage, assetDefinitionStore: assetDefinitionStore, completion: completion)
ContractDataDetector(address: contract, storage: storage, assetDefinitionStore: assetDefinitionStore).fetch(completion: completion)
}
}

Loading…
Cancel
Save