Merge pull request #5697 from oa-s/#5624

simplify TokenOrContract #5624
pull/5736/head
Crypto Pank 2 years ago committed by GitHub
commit 4d15ce10ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      AlphaWallet/Tokens/Coordinators/NewTokenCoordinator.swift
  2. 5
      AlphaWallet/Tokens/ViewControllers/NewTokenViewController.swift
  3. 4
      AlphaWallet/Transfer/Coordinators/SendCoordinator.swift
  4. 2
      AlphaWalletTests/Coordinators/TokensCoordinatorTests.swift
  5. 4
      AlphaWalletTests/Core/Helpers/LogLargeNftJsonFilesTests.swift
  6. 4
      AlphaWalletTests/Core/Types/RetryPublisherTests.swift
  7. 8
      AlphaWalletTests/Tokens/Helpers/TokenObjectTest.swift
  8. 2
      AlphaWalletTests/Tokens/TokensDataStoreTest.swift
  9. 1
      modules/AlphaWalletFoundation/AlphaWalletFoundation/EtherClient/Models/EthCall.swift
  10. 12
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift
  11. 2
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/ClientSideTokenSourceProvider.swift
  12. 22
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Helpers/NonFungibleErc1155JsonBalanceFetcher.swift
  13. 47
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Logic/ContractDataDetector.swift
  14. 81
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/ImportToken.swift
  15. 9
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/TokensAutodetector.swift
  16. 10
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift
  17. 8
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensService.swift
  18. 7
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Types/ErcToken.swift
  19. 141
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Types/TokensDataStore.swift
  20. 22
      modules/AlphaWalletFoundation/AlphaWalletFoundation/WalletBalance/TokenBalanceFetcher.swift

@ -96,7 +96,7 @@ extension NewTokenCoordinator: NewTokenViewControllerDelegate {
delegate?.didClose(in: self)
}
func didAddToken(token: ERCToken, in viewController: NewTokenViewController) {
func didAddToken(token: ErcToken, in viewController: NewTokenViewController) {
let token = importToken.importToken(token: token)
delegate?.coordinator(self, didAddToken: token)
@ -146,12 +146,12 @@ extension NewTokenCoordinator: NewTokenViewControllerDelegate {
viewController.updateBalanceValue(balance.rawValue, tokenType: tokenType)
verboseLog("[TokenType] contract: \(address.eip55String) server: \(server) to token type: nonFungibleTokenComplete")
seal.fulfill(tokenType)
case .fungibleTokenComplete(let name, let symbol, let decimals):
case .fungibleTokenComplete(let name, let symbol, let decimals, let tokenType):
viewController.updateNameValue(name)
viewController.updateSymbolValue(symbol)
viewController.updateDecimalsValue(decimals)
verboseLog("[TokenType] contract: \(address.eip55String) server: \(server) to token type: fungibleTokenComplete")
seal.fulfill(.erc20)
seal.fulfill(tokenType)
case .delegateTokenComplete:
verboseLog("[TokenType] contract: \(address.eip55String) server: \(server) to token type: delegateTokenComplete")
seal.reject(NoContractDetailsDetected())

@ -5,7 +5,7 @@ import UIKit
import AlphaWalletFoundation
protocol NewTokenViewControllerDelegate: AnyObject {
func didAddToken(token: ERCToken, in viewController: NewTokenViewController)
func didAddToken(token: ErcToken, in viewController: NewTokenViewController)
func didAddAddress(address: AlphaWallet.Address, in viewController: NewTokenViewController)
func didTapChangeServer(in viewController: NewTokenViewController)
func openQRCode(in controller: NewTokenViewController)
@ -350,13 +350,14 @@ class NewTokenViewController: UIViewController {
balance.append("0")
}
let ercToken = ERCToken(
let ercToken = ErcToken(
contract: address,
server: server,
name: name,
symbol: symbol,
decimals: decimals,
type: tokenType,
value: .zero,
balance: .erc875(balance)
)

@ -122,10 +122,6 @@ extension SendCoordinator: SendViewControllerDelegate {
.displayError(message: error.prettyError)
}
}
func lookup(contract: AlphaWallet.Address, in viewController: SendViewController, completion: @escaping (ContractData) -> Void) {
ContractDataDetector(address: contract, session: session, assetDefinitionStore: assetDefinitionStore, analytics: analytics).fetch(completion: completion)
}
}
extension SendCoordinator: TransactionConfirmationCoordinatorDelegate {

@ -54,7 +54,7 @@ final class FakeImportToken: ImportToken {
return .init(error: PMKError.badInput)
}
override func importToken(token: ERCToken, shouldUpdateBalance: Bool = true) -> Token {
override func importToken(token: ErcToken, shouldUpdateBalance: Bool = true) -> Token {
return Token()
}

@ -19,10 +19,10 @@ class LogLargeNftJsonFilesTests: XCTestCase {
let uri = URL(string: "https://www.google.com/")!
let asset_1 = NonFungibleBalance.NftAssetRawValue(json: largeImage, source: .uri(uri))
XCTAssertTrue(crashlytics.logLargeNftJsonFiles(for: [.update(token: token, action: .nonFungibleBalance(.assets([asset_1])))], fileSizeThreshold: 0.5))
XCTAssertTrue(crashlytics.logLargeNftJsonFiles(for: [.update(token: token, field: .nonFungibleBalance(.assets([asset_1])))], fileSizeThreshold: 0.5))
let asset_2 = NonFungibleBalance.NftAssetRawValue(json: "", source: .uri(uri))
XCTAssertFalse(crashlytics.logLargeNftJsonFiles(for: [.update(token: token, action: .nonFungibleBalance(.assets([asset_2])))], fileSizeThreshold: 0.5))
XCTAssertFalse(crashlytics.logLargeNftJsonFiles(for: [.update(token: token, field: .nonFungibleBalance(.assets([asset_2])))], fileSizeThreshold: 0.5))
}
}
// swiftlint:enable all

@ -233,7 +233,7 @@ class RetryPublisherTests: XCTestCase {
let resultPublisher = upstreamPublisher.retry(.custom(retries: 2, delayCalculator: { _ in
return TimeInterval(Int.random(in: 5 ... 8))
}), scheduler: DispatchQueue.global())
}), scheduler: DispatchQueue.global(qos: .userInitiated))
XCTAssertEqual(asyncAPICallCount, 0)
XCTAssertEqual(futureClosureHandlerCount, 0)
@ -248,7 +248,7 @@ class RetryPublisherTests: XCTestCase {
XCTFail("no value should be returned")
}).store(in: &cancelable)
wait(for: [expectation], timeout: 50.0)
wait(for: [expectation], timeout: 500.0)
}
func testRetryWithoutDelay() {

@ -13,7 +13,7 @@ class TokenObjectTest: XCTestCase {
func testTokenInfo() {
let dataStore = FakeTokensDataStore()
let _token = Token(contract: AlphaWallet.Address.make(address: "0x1000000000000000000000000000000000000004"), type: .erc20)
dataStore.addOrUpdate(tokensOrContracts: [.token(_token)])
dataStore.addOrUpdate(with: [.init(_token)])
let token1 = dataStore.token(forContract: _token.contractAddress, server: _token.server)
@ -21,12 +21,12 @@ class TokenObjectTest: XCTestCase {
XCTAssertEqual(token1?.info, _token.info)
let url = URL(string: "http://google.com")
dataStore.addOrUpdate([.update(token: _token, action: .imageUrl(url))])
dataStore.addOrUpdate(with: [.update(token: _token, field: .imageUrl(url))])
let token2 = dataStore.token(forContract: _token.contractAddress, server: _token.server)
XCTAssertEqual(token2?.info.imageUrl, url?.absoluteString)
dataStore.addOrUpdate([.update(token: _token, action: .imageUrl(nil))])
dataStore.addOrUpdate(with: [.update(token: _token, field: .imageUrl(nil))])
let token3 = dataStore.token(forContract: _token.contractAddress, server: _token.server)
XCTAssertNil(token3?.info.imageUrl)
@ -35,7 +35,7 @@ class TokenObjectTest: XCTestCase {
func testTokenBalanceDeletion() {
let dataStore = FakeTokensDataStore()
let _token = Token(contract: AlphaWallet.Address.make(address: "0x1000000000000000000000000000000000000004"), type: .erc721)
dataStore.addOrUpdate(tokensOrContracts: [.token(_token)])
dataStore.addOrUpdate(with: [.init(_token)])
let _token1 = dataStore.token(forContract: _token.contractAddress, server: _token.server)
XCTAssertEqual(_token1?.balance, [])

@ -15,7 +15,7 @@ class TokensDataStoreTest: XCTestCase {
)
override func setUp() {
storage.addOrUpdate(tokensOrContracts: [.token(token)])
storage.addOrUpdate(with: [.init(token)])
}
//We make a call to update token in datastore to store the updated balance after an async call to fetch the balance over the web. Token in the datastore might have been deleted when the web call is completed. Make sure this doesn't crash

@ -1,7 +1,6 @@
// Copyright © 2022 Stormbird PTE. LTD.
import Foundation
//import APIKit
import JSONRPCKit
import PromiseKit

@ -132,7 +132,7 @@ public class AlphaWalletTokensService: TokensService {
stop()
}
public func addCustom(tokens: [ERCToken], shouldUpdateBalance: Bool) -> [Token] {
public func addCustom(tokens: [ErcToken], shouldUpdateBalance: Bool) -> [Token] {
tokensDataStore.addCustom(tokens: tokens, shouldUpdateBalance: shouldUpdateBalance)
}
@ -148,11 +148,11 @@ public class AlphaWalletTokensService: TokensService {
tokensDataStore.addOrUpdate(tokensOrContracts: tokensOrContracts)
}
public func addOrUpdate(_ actions: [AddOrUpdateTokenAction]) -> Bool? {
tokensDataStore.addOrUpdate(actions)
public func addOrUpdate(with actions: [AddOrUpdateTokenAction]) -> Bool? {
tokensDataStore.addOrUpdate(with: actions)
}
public func updateToken(primaryKey: String, action: TokenUpdateAction) -> Bool? {
public func updateToken(primaryKey: String, action: TokenFieldUpdate) -> Bool? {
tokensDataStore.updateToken(primaryKey: primaryKey, action: action)
}
@ -184,7 +184,7 @@ public class AlphaWalletTokensService: TokensService {
.eraseToAnyPublisher()
}
public func update(token: TokenIdentifiable, value: TokenUpdateAction) {
public func update(token: TokenIdentifiable, value: TokenFieldUpdate) {
let primaryKey = TokenObject.generatePrimaryKey(fromContract: token.contractAddress, server: token.server)
tokensDataStore.updateToken(primaryKey: primaryKey, action: value)
}
@ -212,7 +212,7 @@ extension AlphaWalletTokensService: TokensServiceTests {
}
public func addOrUpdateTokenTestsOnly(token: Token) {
tokensDataStore.addOrUpdate(tokensOrContracts: [.token(token)])
tokensDataStore.addOrUpdate(with: [.init(token)])
}
public func deleteTokenTestsOnly(token: Token) {

@ -112,6 +112,6 @@ public class ClientSideTokenSourceProvider: TokenSourceProvider {
extension ClientSideTokenSourceProvider: TokenBalanceFetcherDelegate {
public func didUpdateBalance(value actions: [AddOrUpdateTokenAction], in fetcher: TokenBalanceFetcher) {
crashlytics.logLargeNftJsonFiles(for: actions, fileSizeThreshold: 10)
tokensDataStore.addOrUpdate(actions)
tokensDataStore.addOrUpdate(with: actions)
}
}

@ -14,7 +14,7 @@ import PromiseKit
import SwiftyJSON
protocol NonFungibleErc1155JsonBalanceFetcherDelegate: AnyObject {
func addTokens(tokensToAdd: [ERCToken]) -> Promise<Void>
func addTokens(tokensToAdd: [ErcToken]) -> Promise<Void>
}
//TODO: think about the name, remove queue later, replace with any publisher
@ -105,19 +105,27 @@ class NonFungibleErc1155JsonBalanceFetcher {
})
}
private func fetchUnknownErc1155ContractsDetails(contractsAndTokenIds: Erc1155TokenIds.ContractsAndTokenIds) -> Promise<[ERCToken]> {
private func fetchUnknownErc1155ContractsDetails(contractsAndTokenIds: Erc1155TokenIds.ContractsAndTokenIds) -> Promise<[ErcToken]> {
let contractsToAdd: [AlphaWallet.Address] = contractsAndTokenIds.keys.filter { tokensService.token(for: $0, server: session.server) == nil }
let promises = contractsToAdd.map { importToken.fetchTokenOrContract(for: $0, server: session.server) }
return when(resolved: promises).map(on: queue, { result -> [ERCToken] in
result.compactMap { each -> ERCToken? in
return when(resolved: promises).map(on: queue, { result -> [ErcToken] in
result.compactMap { each -> ErcToken? in
switch each {
case .fulfilled(let tokenOrContract):
switch tokenOrContract {
case .nonFungibleToken(let token): return token
case .token, .delegateContracts, .deletedContracts, .fungibleTokenComplete, .none: return nil
case .ercToken(let token):
switch token.type {
case .erc1155, .erc721:
return token
case .erc875, .nativeCryptocurrency, .erc20, .erc721ForTickets:
return nil
}
case .delegateContracts, .deletedContracts:
return nil
}
case .rejected: return nil
case .rejected:
return nil
}
}
})

@ -1,7 +1,6 @@
// Copyright © 2021 Stormbird PTE. LTD.
import Foundation
import Alamofire
import PromiseKit
public enum ContractData {
@ -10,9 +9,13 @@ public enum ContractData {
case balance(balance: NonFungibleBalance, tokenType: TokenType)
case decimals(Int)
case nonFungibleTokenComplete(name: String, symbol: String, balance: NonFungibleBalance, tokenType: TokenType)
case fungibleTokenComplete(name: String, symbol: String, decimals: Int)
case fungibleTokenComplete(name: String, symbol: String, decimals: Int, tokenType: TokenType)
case delegateTokenComplete
case failed(networkReachable: Bool?)
case failed(networkReachable: Bool, error: Error)
}
enum ContractDataDetectorError: Error {
case symbolIsEmpty
}
public class ContractDataDetector {
@ -26,8 +29,10 @@ public class ContractDataDetector {
private let (decimalsPromise, decimalsSeal) = Promise<Int>.pending()
private var failed = false
private var completion: ((ContractData) -> Void)?
private let reachability: ReachabilityManagerProtocol
public init(address: AlphaWallet.Address, session: WalletSession, assetDefinitionStore: AssetDefinitionStore, analytics: AnalyticsLogger) {
public init(address: AlphaWallet.Address, session: WalletSession, assetDefinitionStore: AssetDefinitionStore, analytics: AnalyticsLogger, reachability: ReachabilityManagerProtocol) {
self.reachability = reachability
self.address = address
self.tokenProvider = session.tokenProvider
self.assetDefinitionStore = assetDefinitionStore
@ -61,7 +66,7 @@ public class ContractDataDetector {
}.catch { error in
self.nonFungibleBalanceSeal.reject(error)
self.decimalsSeal.fulfill(0)
self.callCompletionFailed()
self.callCompletionFailed(error: error)
}
case .erc721:
tokenProvider.getErc721Balance(for: address).done { balance in
@ -71,7 +76,7 @@ public class ContractDataDetector {
}.catch { error in
self.nonFungibleBalanceSeal.reject(error)
self.decimalsSeal.fulfill(0)
self.callCompletionFailed()
self.callCompletionFailed(error: error)
}
case .erc721ForTickets:
tokenProvider.getErc721ForTicketsBalance(for: address).done { balance in
@ -80,7 +85,7 @@ public class ContractDataDetector {
self.completionOfPartialData(.balance(balance: .erc721ForTickets(balance), tokenType: .erc721ForTickets))
}.catch { error in
self.nonFungibleBalanceSeal.reject(error)
self.callCompletionFailed()
self.callCompletionFailed(error: error)
}
case .erc1155:
let balance: [String] = .init()
@ -93,7 +98,7 @@ public class ContractDataDetector {
self.completionOfPartialData(.decimals(decimal))
}.catch { error in
self.decimalsSeal.reject(error)
self.callCompletionFailed()
self.callCompletionFailed(error: error)
}
case .nativeCryptocurrency:
break
@ -105,9 +110,9 @@ public class ContractDataDetector {
namePromise
}.done { name in
self.completionOfPartialData(.name(name))
}.catch { _ in
}.catch { error in
if tokenType.shouldHaveNameAndSymbol {
self.callCompletionFailed()
self.callCompletionFailed(error: error)
} else {
//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)
//no-op
@ -121,9 +126,9 @@ public class ContractDataDetector {
symbolPromise
}.done { symbol in
self.completionOfPartialData(.symbol(symbol))
}.catch { _ in
}.catch { error in
if tokenType.shouldHaveNameAndSymbol {
self.callCompletionFailed()
self.callCompletionFailed(error: error)
} else {
//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)
//no-op
@ -137,21 +142,21 @@ public class ContractDataDetector {
callCompletionOnAllData()
}
private func callCompletionFailed() {
private func callCompletionFailed(error: Error) {
guard !failed else { return }
failed = true
//TODO maybe better to share an instance of the reachability manager
completion?(.failed(networkReachable: NetworkReachabilityManager()?.isReachable))
completion?(.failed(networkReachable: reachability.isReachable, error: error))
}
private func callCompletionAsDelegateTokenOrNot() {
private func callCompletionAsDelegateTokenOrNot(error: Error) {
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 {
if reachability.isReachable {
completion?(.delegateTokenComplete)
} else {
callCompletionFailed()
callCompletionFailed(error: error)
}
}
@ -167,17 +172,17 @@ public class ContractDataDetector {
case .nativeCryptocurrency, .erc20:
if let name = namePromise.value, let symbol = symbolPromise.value, let decimals = decimalsPromise.value {
if symbol.isEmpty {
callCompletionAsDelegateTokenOrNot()
callCompletionAsDelegateTokenOrNot(error: ContractDataDetectorError.symbolIsEmpty)
} else {
completion?(.fungibleTokenComplete(name: name, symbol: symbol, decimals: decimals))
completion?(.fungibleTokenComplete(name: name, symbol: symbol, decimals: decimals, tokenType: tokenType))
}
}
}
} else if let name = namePromise.value, let symbol = symbolPromise.value, let decimals = decimalsPromise.value {
if symbol.isEmpty {
callCompletionAsDelegateTokenOrNot()
callCompletionAsDelegateTokenOrNot(error: ContractDataDetectorError.symbolIsEmpty)
} else {
completion?(.fungibleTokenComplete(name: name, symbol: symbol, decimals: decimals))
completion?(.fungibleTokenComplete(name: name, symbol: symbol, decimals: decimals, tokenType: .erc20))
}
}
}

@ -10,7 +10,7 @@ import PromiseKit
import Combine
public protocol TokenImportable {
func importToken(token: ERCToken, shouldUpdateBalance: Bool) -> Token
func importToken(token: ErcToken, shouldUpdateBalance: Bool) -> Token
func importToken(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool) -> Promise<Token>
}
@ -24,8 +24,9 @@ public protocol ContractDataFetchable {
open class ImportToken: TokenImportable, ContractDataFetchable {
enum ImportTokenError: Error {
case serverIsDisabled
case nothingToImport
case others
case zeroBalanceDetected
case `internal`(error: Error)
case notContractOrFailed(TokenOrContract)
}
private let defaultTokens: [(AlphaWallet.Address, RPCServer)] = [
@ -39,6 +40,7 @@ open class ImportToken: TokenImportable, ContractDataFetchable {
private var cancelable = Set<AnyCancellable>()
private let queue = DispatchQueue(label: "org.alphawallet.swift.importToken")
private var inFlightPromises: [String: Promise<TokenOrContract>] = [:]
private let reachability = ReachabilityManager()
public let wallet: Wallet
@ -72,10 +74,12 @@ open class ImportToken: TokenImportable, ContractDataFetchable {
case .serverIsDisabled:
//no-op. Since we didn't check if chain is enabled, we just let it be. But if there are other enum-cases, we don't want to eat the errors, we should re-throw those
break
case .nothingToImport:
case .zeroBalanceDetected:
//no-op. We don't import it, possibly because balance is 0
break
case .others:
case .notContractOrFailed:
throw error
case .internal(let error):
throw error
}
} else {
@ -93,16 +97,11 @@ open class ImportToken: TokenImportable, ContractDataFetchable {
} else {
return firstly {
fetchTokenOrContract(for: contract, server: server, onlyIfThereIsABalance: onlyIfThereIsABalance)
}.map { [tokensDataStore] operation -> Token in
switch operation {
case .none:
throw ImportTokenError.nothingToImport
case .nonFungibleToken, .token, .delegateContracts, .deletedContracts, .fungibleTokenComplete:
if let token = tokensDataStore.addOrUpdate(tokensOrContracts: [operation]).first {
return token
} else {
throw ImportTokenError.others
}
}.map { [tokensDataStore] tokenOrContract -> Token in
if let token = tokensDataStore.addOrUpdate(tokensOrContracts: [tokenOrContract]).first {
return token
} else {
throw ImportTokenError.notContractOrFailed(tokenOrContract)
}
}
}
@ -123,12 +122,10 @@ open class ImportToken: TokenImportable, ContractDataFetchable {
return session.tokenProvider.getErc875Balance(for: contract)
.then(on: queue, { balance -> Promise<TokenOrContract> in
if balance.isEmpty {
return .value(.none)
return .init(error: ImportTokenError.zeroBalanceDetected)
} else {
return self.fetchTokenOrContract(for: contract, server: server, onlyIfThereIsABalance: false)
}
}).recover(on: queue, { _ -> Guarantee<TokenOrContract> in
return .value(.none)
})
case .erc20:
return session.tokenProvider.getErc20Balance(for: contract)
@ -136,39 +133,36 @@ open class ImportToken: TokenImportable, ContractDataFetchable {
if balance > 0 {
return self.fetchTokenOrContract(for: contract, server: server, onlyIfThereIsABalance: false)
} else {
return .value(.none)
return .init(error: ImportTokenError.zeroBalanceDetected)
}
}).recover(on: queue, { _ -> Guarantee<TokenOrContract> in
return .value(.none)
})
case .erc721, .erc721ForTickets, .erc1155, .nativeCryptocurrency:
//Handled in TokenBalanceFetcher.refreshBalanceForErc721Or1155Tokens()
return .value(.none)
return .init(error: ImportTokenError.zeroBalanceDetected)
}
})
})
}
open func importToken(token: ERCToken, shouldUpdateBalance: Bool = true) -> Token {
open func importToken(token: ErcToken, shouldUpdateBalance: Bool = true) -> Token {
let token = tokensDataStore.addCustom(tokens: [token], shouldUpdateBalance: shouldUpdateBalance)
return token[0]
}
open func fetchContractData(for contract: AlphaWallet.Address, server: RPCServer, completion: @escaping (ContractData) -> Void) {
guard let session = sessionProvider.session(for: server) else {
completion(.failed(networkReachable: false))
return
if let session = sessionProvider.session(for: server) {
let detector = ContractDataDetector(address: contract, session: session, assetDefinitionStore: assetDefinitionStore, analytics: analytics, reachability: reachability)
detector.fetch(completion: completion)
} else {
completion(.failed(networkReachable: reachability.isReachable, error: ImportTokenError.serverIsDisabled))
}
let detector = ContractDataDetector(address: contract, session: session, assetDefinitionStore: assetDefinitionStore, analytics: analytics)
detector.fetch(completion: completion)
}
open func fetchTokenOrContract(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool = false) -> Promise<TokenOrContract> {
firstly {
.value(contract)
}.then(on: queue, { [queue] contract -> Promise<TokenOrContract> in
}.then(on: queue, { [queue, tokensDataStore] contract -> Promise<TokenOrContract> in
let key = "\(contract.hashValue)-\(onlyIfThereIsABalance)-\(server)"
if let promise = self.inFlightPromises[key] {
@ -181,21 +175,30 @@ open class ImportToken: TokenImportable, ContractDataFetchable {
break
case .nonFungibleTokenComplete(let name, let symbol, let balance, let tokenType):
guard !onlyIfThereIsABalance || (onlyIfThereIsABalance && !balance.isEmpty) else {
seal.fulfill(.none)
break
seal.reject(ImportTokenError.zeroBalanceDetected)
return
}
let ercToken = ErcToken(contract: contract, server: server, name: name, symbol: symbol, decimals: 0, type: tokenType, value: "0", balance: balance)
seal.fulfill(.ercToken(ercToken))
case .fungibleTokenComplete(let name, let symbol, let decimals, let tokenType):
let existedToken = tokensDataStore.token(forContract: contract, server: server)
let value = existedToken?.value ?? "0"
guard !onlyIfThereIsABalance || (onlyIfThereIsABalance && !(value != "0")) else {
seal.reject(ImportTokenError.zeroBalanceDetected)
return
}
let ercToken = ERCToken(contract: contract, server: server, name: name, symbol: symbol, decimals: 0, type: tokenType, balance: balance)
seal.fulfill(.nonFungibleToken(ercToken))
case .fungibleTokenComplete(let name, let symbol, let decimals):
seal.fulfill(.fungibleTokenComplete(name: name, symbol: symbol, decimals: decimals, contract: contract, server: server, onlyIfThereIsABalance: onlyIfThereIsABalance))
let ercToken = ErcToken(contract: contract, server: server, name: name, symbol: symbol, decimals: decimals, type: tokenType, value: value, balance: .balance(["0"]))
seal.fulfill(.ercToken(ercToken))
case .delegateTokenComplete:
seal.fulfill(.delegateContracts([AddressAndRPCServer(address: contract, server: server)]))
case .failed(let networkReachable):
if let networkReachable = networkReachable, networkReachable {
case .failed(let networkReachable, let error):
//TODO: maybe its need to handle some cases of error here?
if networkReachable {
seal.fulfill(.deletedContracts([AddressAndRPCServer(address: contract, server: server)]))
} else {
seal.fulfill(.none)
seal.reject(ImportTokenError.internal(error: error))
}
}
}

@ -193,15 +193,6 @@ extension SingleChainTokensAutodetector: AutoDetectTokensOperationDelegate {
}
public func didDetect(tokensOrContracts: [TokenOrContract]) {
let tokensOrContracts = tokensOrContracts.filter { tokenOrContract in
switch tokenOrContract {
case .delegateContracts, .deletedContracts, .nonFungibleToken, .token, .fungibleTokenComplete:
return true
case .none:
return false
}
}
tokensOrContractsDetectedSubject.send(tokensOrContracts)
}

@ -143,7 +143,7 @@ public final class WalletDataProcessingPipeline: TokensProcessingPipeline {
tokensService.token(for: contract, server: server)
}
public func addCustom(tokens: [ERCToken], shouldUpdateBalance: Bool) -> [Token] {
public func addCustom(tokens: [ErcToken], shouldUpdateBalance: Bool) -> [Token] {
tokensService.addCustom(tokens: tokens, shouldUpdateBalance: shouldUpdateBalance)
}
@ -155,8 +155,8 @@ public final class WalletDataProcessingPipeline: TokensProcessingPipeline {
tokensService.addOrUpdate(tokensOrContracts: tokensOrContracts)
}
public func addOrUpdate(_ actions: [AddOrUpdateTokenAction]) -> Bool? {
tokensService.addOrUpdate(actions)
public func addOrUpdate(with actions: [AddOrUpdateTokenAction]) -> Bool? {
tokensService.addOrUpdate(with: actions)
}
public func tokenPublisher(for contract: AlphaWallet.Address, server: RPCServer) -> AnyPublisher<Token?, Never> {
@ -218,9 +218,9 @@ public final class WalletDataProcessingPipeline: TokensProcessingPipeline {
coinTickersFetcher.updateTickerIds
.map { [tokensService] data -> [AddOrUpdateTokenAction] in
let v = data.compactMap { i in tokensService.token(for: i.key.address, server: i.key.server).flatMap { ($0, i.tickerId) } }
return v.map { AddOrUpdateTokenAction.update(token: $0.0, action: .coinGeckoTickerId($0.1)) }
return v.map { AddOrUpdateTokenAction.update(token: $0.0, field: .coinGeckoTickerId($0.1)) }
}.sink { [tokensService] actions in
tokensService.addOrUpdate(actions)
tokensService.addOrUpdate(with: actions)
}.store(in: &cancelable)
}

@ -19,9 +19,9 @@ public protocol TokenProvidable {
public protocol TokenAddable {
func add(tokenUpdates updates: [TokenUpdate])
@discardableResult func addCustom(tokens: [ERCToken], shouldUpdateBalance: Bool) -> [Token]
@discardableResult func addCustom(tokens: [ErcToken], shouldUpdateBalance: Bool) -> [Token]
@discardableResult func addOrUpdate(tokensOrContracts: [TokenOrContract]) -> [Token]
@discardableResult func addOrUpdate(_ actions: [AddOrUpdateTokenAction]) -> Bool?
@discardableResult func addOrUpdate(with actions: [AddOrUpdateTokenAction]) -> Bool?
}
public protocol TokenAutoDetectable {
@ -47,8 +47,8 @@ public protocol TokensServiceTests {
public protocol PipelineTests: CoinTickersFetcherTests { }
public protocol TokenUpdatable {
func update(token: TokenIdentifiable, value: TokenUpdateAction)
@discardableResult func updateToken(primaryKey: String, action: TokenUpdateAction) -> Bool?
func update(token: TokenIdentifiable, value: TokenFieldUpdate)
@discardableResult func updateToken(primaryKey: String, action: TokenFieldUpdate) -> Bool?
}
public protocol TokensService: TokensState, TokenProvidable, TokenAddable, TokenHidable, TokenAutoDetectable, TokenBalanceRefreshable, TokensServiceTests, TokenUpdatable, DetectedContractsProvideble {

@ -1,23 +1,26 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import BigInt
public struct ERCToken {
public struct ErcToken {
public let contract: AlphaWallet.Address
public let server: RPCServer
public let name: String
public let symbol: String
public let decimals: Int
public let type: TokenType
public let value: BigInt
public let balance: NonFungibleBalance
public init(contract: AlphaWallet.Address, server: RPCServer, name: String, symbol: String, decimals: Int, type: TokenType, balance: NonFungibleBalance) {
public init(contract: AlphaWallet.Address, server: RPCServer, name: String, symbol: String, decimals: Int, type: TokenType, value: BigInt, balance: NonFungibleBalance) {
self.contract = contract
self.server = server
self.name = name
self.symbol = symbol
self.decimals = decimals
self.type = type
self.value = value
self.balance = balance
}
}

@ -28,15 +28,15 @@ public protocol TokensDataStore: NSObjectProtocol {
func deleteTestsOnly(tokens: [Token])
func tokenBalancesTestsOnly() -> [TokenBalanceValue]
func add(tokenUpdates updates: [TokenUpdate])
@discardableResult func addCustom(tokens: [ERCToken], shouldUpdateBalance: Bool) -> [Token]
@discardableResult func updateToken(primaryKey: String, action: TokenUpdateAction) -> Bool?
@discardableResult func addCustom(tokens: [ErcToken], shouldUpdateBalance: Bool) -> [Token]
@discardableResult func updateToken(primaryKey: String, action: TokenFieldUpdate) -> Bool?
@discardableResult func addOrUpdate(tokensOrContracts: [TokenOrContract]) -> [Token]
@discardableResult func addOrUpdate(_ actions: [AddOrUpdateTokenAction]) -> Bool?
@discardableResult func addOrUpdate(with actions: [AddOrUpdateTokenAction]) -> Bool?
}
extension TokensDataStore {
@discardableResult func updateToken(addressAndRpcServer: AddressAndRPCServer, action: TokenUpdateAction) -> Bool? {
@discardableResult func updateToken(addressAndRpcServer: AddressAndRPCServer, action: TokenFieldUpdate) -> Bool? {
let primaryKey = TokenObject.generatePrimaryKey(fromContract: addressAndRpcServer.address, server: addressAndRpcServer.server)
return updateToken(primaryKey: primaryKey, action: action)
}
@ -67,31 +67,25 @@ extension TokensDataStore {
}
public enum TokenOrContract {
case nonFungibleToken(ERCToken)
case token(Token)
/// ercToken - tokens meta data
case ercToken(ErcToken)
/// delegateContracts - partially detect contract data and its rpc server
case delegateContracts([AddressAndRPCServer])
/// deletedContracts - failed to detect contact and its rpc server
case deletedContracts([AddressAndRPCServer])
/// We re-use the existing balance value to avoid the `Wallets` tab showing that token (if it already exist) as `balance = 0` momentarily
case fungibleTokenComplete(name: String, symbol: String, decimals: Int, contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool)
case none
var addressAndRPCServer: AddressAndRPCServer? {
switch self {
case .nonFungibleToken(let eRCToken):
return .init(address: eRCToken.contract, server: eRCToken.server)
case .token(let token):
return .init(address: token.contractAddress, server: token.server)
case .delegateContracts, .deletedContracts, .none:
return nil
case .fungibleTokenComplete(_, _, _, let contract, let server, _):
return .init(address: contract, server: server)
}
}
}
public enum AddOrUpdateTokenAction {
case add(ERCToken, shouldUpdateBalance: Bool)
case update(token: Token, action: TokenUpdateAction)
/// - ercToken - erc meta information for token creating
/// - shouldUpdateBalance - should be non fungible/ semifungible balance unpdated
case add(ercToken: ErcToken, shouldUpdateBalance: Bool)
/// - action - update some of tokens fields, nil for create a new token or update if its already exists
/// - token - token to update
case update(token: Token, field: TokenFieldUpdate?)
public init(_ token: Token) {
self = .update(token: token, field: nil)
}
}
//TODO: Rename with more better name
@ -182,7 +176,7 @@ public enum NonFungibleBalance {
}
}
public enum TokenUpdateAction {
public enum TokenFieldUpdate {
case value(BigInt)
case isDisabled(Bool)
case nonFungibleBalance(NonFungibleBalance)
@ -385,12 +379,12 @@ open class MultipleChainsTokensDataStore: NSObject, TokensDataStore {
.first
}
@discardableResult public func addCustom(tokens: [ERCToken], shouldUpdateBalance: Bool) -> [Token] {
@discardableResult public func addCustom(tokens: [ErcToken], shouldUpdateBalance: Bool) -> [Token] {
guard !tokens.isEmpty else { return [] }
var tokensToReturn: [Token] = []
store.performSync { realm in
let newTokens = tokens.compactMap { MultipleChainsTokensDataStore.functional.createTokenObject(ercToken: $0, shouldUpdateBalance: shouldUpdateBalance) }
let newTokens = tokens.compactMap { TokenObject(ercToken: $0, shouldUpdateBalance: shouldUpdateBalance) }
try? realm.safeWrite {
//TODO: save existed sort index and displaying state
for token in newTokens {
@ -406,6 +400,7 @@ open class MultipleChainsTokensDataStore: NSObject, TokensDataStore {
@discardableResult public func addOrUpdate(tokensOrContracts: [TokenOrContract]) -> [Token] {
guard !tokensOrContracts.isEmpty else { return [] }
var tokens: [Token] = []
store.performSync { realm in
try? realm.safeWrite {
@ -415,44 +410,22 @@ open class MultipleChainsTokensDataStore: NSObject, TokensDataStore {
let delegateContract = values.map { DelegateContract(contractAddress: $0.address, server: $0.server) }
realm.add(delegateContract, update: .all)
case .nonFungibleToken(let token):
let tokenObject = MultipleChainsTokensDataStore.functional.createTokenObject(ercToken: token, shouldUpdateBalance: token.type.shouldUpdateBalanceWhenDetected)
self.addTokenWithoutCommitWrite(tokenObject: tokenObject, realm: realm)
case .token(let token):
let tokenObject = TokenObject(token: token)
self.addTokenWithoutCommitWrite(tokenObject: tokenObject, realm: realm)
case .ercToken(let ercToken):
let newTokenObject = TokenObject(ercToken: ercToken, shouldUpdateBalance: ercToken.type.shouldUpdateBalanceWhenDetected)
self.addTokenWithoutCommitWrite(tokenObject: newTokenObject, realm: realm)
if let tokenObject = self.tokenObject(forContract: ercToken.contract, server: ercToken.server, realm: realm) {
tokens += [Token(tokenObject: tokenObject)]
}
case .deletedContracts(let values):
let deadContracts = values.map { DelegateContract(contractAddress: $0.address, server: $0.server) }
realm.add(deadContracts, update: .all)
case .fungibleTokenComplete(let name, let symbol, let decimals, let contract, let server, let onlyIfThereIsABalance):
let existedTokenObject = self.tokenObject(forContract: contract, server: server, realm: realm)
let value = existedTokenObject?.value ?? "0"
guard !onlyIfThereIsABalance || (onlyIfThereIsABalance && !(value != "0")) else {
continue
}
let tokenObject = TokenObject(
contract: contract,
server: server,
name: name,
symbol: symbol,
decimals: Int(decimals),
value: value,
type: .erc20
)
self.addTokenWithoutCommitWrite(tokenObject: tokenObject, realm: realm)
case .none:
break
}
}
}
}
let tokenObjects = tokensOrContracts
.compactMap { $0.addressAndRPCServer }
.compactMap { token(forContract: $0.address, server: $0.server) }
return tokenObjects
return tokens
}
public func add(hiddenContracts: [AddressAndRPCServer]) {
@ -485,7 +458,7 @@ open class MultipleChainsTokensDataStore: NSObject, TokensDataStore {
}
}
@discardableResult public func addOrUpdate(_ actions: [AddOrUpdateTokenAction]) -> Bool? {
@discardableResult public func addOrUpdate(with actions: [AddOrUpdateTokenAction]) -> Bool? {
guard !actions.isEmpty else { return nil }
var result: Bool?
@ -495,11 +468,17 @@ open class MultipleChainsTokensDataStore: NSObject, TokensDataStore {
var value: Bool?
switch each {
case .add(let token, let shouldUpdateBalance):
let newToken = MultipleChainsTokensDataStore.functional.createTokenObject(ercToken: token, shouldUpdateBalance: shouldUpdateBalance)
self.addTokenWithoutCommitWrite(tokenObject: newToken, realm: realm)
let tokenObject = TokenObject(ercToken: token, shouldUpdateBalance: shouldUpdateBalance)
self.addTokenWithoutCommitWrite(tokenObject: tokenObject, realm: realm)
value = true
case .update(let tokenObject, let action):
value = self.updateTokenWithoutCommitWrite(primaryKey: tokenObject.primaryKey, action: action, realm: realm)
case .update(let token, let action):
if let action = action {
value = self.updateTokenWithoutCommitWrite(primaryKey: token.primaryKey, action: action, realm: realm)
} else {
let tokenObject = TokenObject(token: token)
self.addTokenWithoutCommitWrite(tokenObject: tokenObject, realm: realm)
value = true
}
}
if result == nil {
@ -511,7 +490,7 @@ open class MultipleChainsTokensDataStore: NSObject, TokensDataStore {
return result
}
@discardableResult public func updateToken(primaryKey: String, action: TokenUpdateAction) -> Bool? {
@discardableResult public func updateToken(primaryKey: String, action: TokenFieldUpdate) -> Bool? {
var result: Bool?
store.performSync { realm in
try? realm.safeWrite {
@ -532,7 +511,7 @@ open class MultipleChainsTokensDataStore: NSObject, TokensDataStore {
realm.add(tokenObject, update: .all)
}
@discardableResult private func updateTokenWithoutCommitWrite(primaryKey: String, action: TokenUpdateAction, realm: Realm) -> Bool? {
@discardableResult private func updateTokenWithoutCommitWrite(primaryKey: String, action: TokenFieldUpdate, realm: Realm) -> Bool? {
guard let tokenObject = realm.object(ofType: TokenObject.self, forPrimaryKey: primaryKey) else { return nil }
var result: Bool = false
@ -623,6 +602,19 @@ open class MultipleChainsTokensDataStore: NSObject, TokensDataStore {
}
}
extension TokenObject {
convenience init(ercToken token: ErcToken, shouldUpdateBalance: Bool) {
self.init(contract: token.contract, server: token.server, name: token.name, symbol: token.symbol, decimals: token.decimals, value: token.value.description, isCustom: true, type: token.type)
if shouldUpdateBalance {
token.balance.rawValue.forEach { balance in
self.balance.append(TokenBalance(balance: balance))
}
}
}
}
extension MultipleChainsTokensDataStore: DetectedContractsProvideble {
public func alreadyAddedContracts(for server: RPCServer) -> [AlphaWallet.Address] {
enabledTokens(for: [server]).map { $0.contractAddress }
@ -762,27 +754,6 @@ extension MultipleChainsTokensDataStore.functional {
)
}
//TODO: Rename tokenObject(ercToken with createTokenObject(ercToken, more clear name
static func createTokenObject(ercToken token: ERCToken, shouldUpdateBalance: Bool) -> TokenObject {
let newToken = TokenObject(
contract: token.contract,
server: token.server,
name: token.name,
symbol: token.symbol,
decimals: token.decimals,
value: "0",
isCustom: true,
type: token.type
)
if shouldUpdateBalance {
token.balance.rawValue.forEach { balance in
newToken.balance.append(TokenBalance(balance: balance))
}
}
return newToken
}
public static func erc20AddressForNativeTokenFilter(servers: [RPCServer], tokens: [Token]) -> [Token] {
var result = tokens
for server in servers {

@ -100,7 +100,7 @@ public class TokenBalanceFetcher: TokenBalanceFetcherType {
nonErc1155BalanceFetcher
.getEthBalance(for: session.account.address)
.done(on: queue, { [weak self] balance in
self?.notifyUpdateBalance([.update(token: etherToken, action: .value(balance.value))])
self?.notifyUpdateBalance([.update(token: etherToken, field: .value(balance.value))])
}).cauterize()
}
}
@ -113,19 +113,19 @@ public class TokenBalanceFetcher: TokenBalanceFetcherType {
nonErc1155BalanceFetcher
.getErc20Balance(for: token.contractAddress)
.done(on: queue, { [weak self] value in
self?.notifyUpdateBalance([.update(token: token, action: .value(value))])
self?.notifyUpdateBalance([.update(token: token, field: .value(value))])
}).cauterize()
case .erc875:
nonErc1155BalanceFetcher
.getErc875Balance(for: token.contractAddress)
.done(on: queue, { [weak self] balance in
self?.notifyUpdateBalance([.update(token: token, action: .nonFungibleBalance(.erc875(balance)))])
self?.notifyUpdateBalance([.update(token: token, field: .nonFungibleBalance(.erc875(balance)))])
}).cauterize()
case .erc721ForTickets:
nonErc1155BalanceFetcher
.getErc721ForTicketsBalance(for: token.contractAddress)
.done(on: queue, { [weak self] balance in
self?.notifyUpdateBalance([.update(token: token, action: .nonFungibleBalance(.erc721ForTickets(balance)))])
self?.notifyUpdateBalance([.update(token: token, field: .nonFungibleBalance(.erc721ForTickets(balance)))])
}).cauterize()
}
}
@ -182,7 +182,7 @@ public class TokenBalanceFetcher: TokenBalanceFetcherType {
let listOfAssets = jsons.map { NonFungibleBalance.NftAssetRawValue(json: $0.value, source: $0.source) }
strongSelf.notifyUpdateBalance([
.update(token: token, action: .nonFungibleBalance(.assets(listOfAssets)))
.update(token: token, field: .nonFungibleBalance(.assets(listOfAssets)))
])
}).cauterize()
}
@ -211,16 +211,16 @@ public class TokenBalanceFetcher: TokenBalanceFetcherType {
}
if let token = tokensService.token(for: contract, server: session.server) {
actions += [
.update(token: token, action: .type(tokenType)),
.update(token: token, action: .nonFungibleBalance(.assets(listOfAssets))),
.update(token: token, field: .type(tokenType)),
.update(token: token, field: .nonFungibleBalance(.assets(listOfAssets))),
]
if let anyNonFungible = anyNonFungible {
actions += [.update(token: token, action: .name(anyNonFungible.contractName))]
actions += [.update(token: token, field: .name(anyNonFungible.contractName))]
}
} else {
let token = ERCToken(contract: contract, server: session.server, name: nonFungibles[0].value.contractName, symbol: nonFungibles[0].value.symbol, decimals: 0, type: tokenType, balance: .assets(listOfAssets))
let token = ErcToken(contract: contract, server: session.server, name: nonFungibles[0].value.contractName, symbol: nonFungibles[0].value.symbol, decimals: 0, type: tokenType, value: .zero, balance: .assets(listOfAssets))
actions += [.add(token, shouldUpdateBalance: tokenType.shouldUpdateBalanceWhenDetected)]
actions += [.add(ercToken: token, shouldUpdateBalance: tokenType.shouldUpdateBalanceWhenDetected)]
}
}
return actions
@ -279,7 +279,7 @@ public class TokenBalanceFetcher: TokenBalanceFetcherType {
}
extension TokenBalanceFetcher: NonFungibleErc1155JsonBalanceFetcherDelegate {
func addTokens(tokensToAdd: [ERCToken]) -> PromiseKit.Promise<Void> {
func addTokens(tokensToAdd: [ErcToken]) -> PromiseKit.Promise<Void> {
firstly {
.value(tokensToAdd)
}.map(on: queue, { [tokensService] tokensToAdd in

Loading…
Cancel
Save