diff --git a/AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift b/AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift index 7d31733f4..b0ef539a1 100644 --- a/AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift +++ b/AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift @@ -66,9 +66,13 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType { private let config: Config private let tokensDataStore: TokensDataStore private let assetDefinitionStore: AssetDefinitionStore - private var cachedErc1155TokenIdsFetchers: [AddressAndRPCServer: Erc1155TokenIdsFetcher] = [:] private var cancelable = Set() + private lazy var nonErc1155BalanceFetcher = TokenProvider(account: account, server: server, queue: queue) + private lazy var nonFungibleContract = NonFungibleContract(server: server, queue: queue) + + private lazy var erc1155TokenIdsFetcher = Erc1155TokenIdsFetcher(address: account.address, server: server, config: config, queue: queue) + private lazy var erc1155BalanceFetcher = Erc1155BalanceFetcher(address: account.address, server: server) let server: RPCServer let etherToken: Activity.AssignedToken @@ -175,39 +179,26 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType { private func updateNonOpenSeaNonFungiblesBalance(contracts: [AlphaWallet.Address], enjinTokens: EnjinSemiFungiblesToTokenId) { let erc721Contracts = filterAwayErc1155Tokens(contracts: contracts) - erc721Contracts.forEach { updateNonOpenSeaErc721Balance(contract: $0) } + erc721Contracts.forEach { updateNonOpenSeaErc721Balance(contract: $0, enjinTokens: enjinTokens) } updateNonOpenSeaErc1155Balance(enjinTokens: enjinTokens) } - //NOTE: avoid memory leak while creating a lot of `Erc1155TokenIdsFetcher` instances - private func createOrGetErc1155TokenIdsFetcher(address: AlphaWallet.Address, server: RPCServer) -> Erc1155TokenIdsFetcher { - let key = AddressAndRPCServer(address: address, server: server) - if let value = cachedErc1155TokenIdsFetchers[key] { - return value - } else { - let fetcher = Erc1155TokenIdsFetcher(address: account.address, server: server, config: config, queue: queue) - cachedErc1155TokenIdsFetchers[key] = fetcher - - return fetcher - } - } - private func filterAwayErc1155Tokens(contracts: [AlphaWallet.Address]) -> [AlphaWallet.Address] { - if let erc1155Contracts = createOrGetErc1155TokenIdsFetcher(address: account.address, server: server).knownErc1155Contracts() { + if let erc1155Contracts = erc1155TokenIdsFetcher.knownErc1155Contracts() { return contracts.filter { !erc1155Contracts.contains($0) } } else { return contracts } } - private func updateNonOpenSeaErc721Balance(contract: AlphaWallet.Address) { + private func updateNonOpenSeaErc721Balance(contract: AlphaWallet.Address, enjinTokens: EnjinSemiFungiblesToTokenId) { guard let erc721TokenIdsFetcher = erc721TokenIdsFetcher else { return } firstly { erc721TokenIdsFetcher.tokenIdsForErc721Token(contract: contract, forServer: server, inAccount: account.address) }.then(on: queue, { tokenIds -> Promise<[String]> in let guarantees: [Guarantee] = tokenIds - .map { self.fetchNonFungibleJson(forTokenId: $0, tokenType: .erc721, address: contract, enjinTokens: [:]) } + .map { self.fetchNonFungibleJson(forTokenId: $0, tokenType: .erc721, address: contract, enjinTokens: enjinTokens) } return when(fulfilled: guarantees) }).done(on: queue, { [weak self, weak tokensDataStore] jsons in guard let strongSelf = self else { return } @@ -223,10 +214,8 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType { guard Features.default.isAvailable(.isErc1155Enabled) else { return } //Local copies so we don't access the wrong ones during async operation - let tokenIdsFetcher = createOrGetErc1155TokenIdsFetcher(address: account.address, server: server) - let balanceFetcher = Erc1155BalanceFetcher(address: account.address, server: server) firstly { - tokenIdsFetcher.detectContractsAndTokenIds() + erc1155TokenIdsFetcher.detectContractsAndTokenIds() }.then(on: queue, { contractsAndTokenIds in self.addUnknownErc1155ContractsToDatabase(contractsAndTokenIds: contractsAndTokenIds.tokens) }).then(on: queue, { (contractsAndTokenIds: Erc1155TokenIds.ContractsAndTokenIds) -> Promise<(contractsAndTokenIds: Erc1155TokenIds.ContractsAndTokenIds, tokenIdMetaDatas: [TokenIdMetaData])> in @@ -238,7 +227,7 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType { tokenIds.compactMap { BigInt($0) } } let promises = contractsToTokenIds.map { contract, tokenIds in - balanceFetcher + self.erc1155BalanceFetcher .fetch(contract: contract, tokenIds: Set(tokenIds)) .map { (contract: contract, balances: $0 ) } } @@ -336,7 +325,7 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType { //Misnomer, we call this "nonFungible", but this includes ERC1155 which can contain (semi-)fungibles, but there's no better name private func fetchNonFungibleJson(forTokenId tokenId: String, tokenType: TokenType, address: AlphaWallet.Address, enjinTokens: EnjinSemiFungiblesToTokenId) -> Guarantee { firstly { - NonFungibleContract(server: server).getTokenUri(for: tokenId, contract: address) + nonFungibleContract.getTokenUri(for: tokenId, contract: address) }.then(on: queue, { self.fetchTokenJson(forTokenId: tokenId, tokenType: tokenType, uri: $0, address: address, enjinTokens: enjinTokens) }).recover(on: queue, { _ in diff --git a/AlphaWallet/Core/ENS/ENSDelegateImpl.swift b/AlphaWallet/Core/ENS/ENSDelegateImpl.swift index 824b3bec3..6468ce905 100644 --- a/AlphaWallet/Core/ENS/ENSDelegateImpl.swift +++ b/AlphaWallet/Core/ENS/ENSDelegateImpl.swift @@ -18,7 +18,7 @@ protocol ENSDelegateImpl: ENSDelegate { extension ENSDelegateImpl { func callSmartContract(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject], timeout: TimeInterval?) -> Promise<[String: Any]> { let server = RPCServer(chainID: chainId) - return globalCallSmartContract(server, contract, functionName, abiString, parameters, timeout, false) + return globalCallSmartContract(server, contract, functionName, abiString, parameters, timeout, false, nil) } func getSmartContractCallData(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject], timeout: TimeInterval?) -> Data? { @@ -30,4 +30,4 @@ extension ENSDelegateImpl { let server = RPCServer(chainID: chainId) return GetInterfaceSupported165Coordinator(forServer: server).getInterfaceSupported165(hash: hash, contract: contract) } -} \ No newline at end of file +} diff --git a/AlphaWallet/Core/NFT/Enjin/EnjinNetworkProvider.swift b/AlphaWallet/Core/NFT/Enjin/EnjinNetworkProvider.swift index 2ba93170d..fdb45f564 100644 --- a/AlphaWallet/Core/NFT/Enjin/EnjinNetworkProvider.swift +++ b/AlphaWallet/Core/NFT/Enjin/EnjinNetworkProvider.swift @@ -146,6 +146,7 @@ final private class NetworkInterceptorProvider: InterceptorProvider { // will be handed to different interceptors. private let store: ApolloStore private let client: URLSessionClient + private let enjinUserManagementInterceptor = EnjinUserManagementInterceptor() init(store: ApolloStore, client: URLSessionClient) { self.store = store @@ -156,7 +157,7 @@ final private class NetworkInterceptorProvider: InterceptorProvider { return [ MaxRetryInterceptor(), CacheReadInterceptor(store: self.store), - EnjinUserManagementInterceptor(), + enjinUserManagementInterceptor, NetworkFetchInterceptor(client: self.client), ResponseCodeInterceptor(), JSONResponseParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject), diff --git a/AlphaWallet/Core/Types/Session.swift b/AlphaWallet/Core/Types/Session.swift index 2dc4c6a62..ac9d86de4 100644 --- a/AlphaWallet/Core/Types/Session.swift +++ b/AlphaWallet/Core/Types/Session.swift @@ -8,18 +8,22 @@ class WalletSession { let tokenBalanceService: TokenBalanceService let config: Config let chainState: ChainState - let tokenProvider: TokenProviderType + lazy private (set) var tokenProvider: TokenProviderType = { + return TokenProvider(account: account, server: server, queue: queue) + }() var sessionID: String { return WalletSession.functional.sessionID(account: account, server: server) } - + lazy private (set) var queue: DispatchQueue = { + return DispatchQueue(label: "com.WalletSession.\(account.address.eip55String).\(server)") + }() + init(account: Wallet, server: RPCServer, config: Config, tokenBalanceService: TokenBalanceService) { self.account = account self.server = server self.config = config self.chainState = ChainState(config: config, server: server) self.tokenBalanceService = tokenBalanceService - self.tokenProvider = TokenProvider(account: account, server: server) if config.development.isAutoFetchingDisabled { //no-op diff --git a/AlphaWallet/Core/Types/TokenProviderType.swift b/AlphaWallet/Core/Types/TokenProviderType.swift index bc24a2cd9..0656c5959 100644 --- a/AlphaWallet/Core/Types/TokenProviderType.swift +++ b/AlphaWallet/Core/Types/TokenProviderType.swift @@ -28,6 +28,7 @@ class TokenProvider: TokenProviderType { private let numberOfTimesToRetryFetchContractData = 2 private let server: RPCServer private let queue: DispatchQueue? + private lazy var isERC1155ContractDetector = GetIsERC1155ContractCoordinator(forServer: server) init(account: Wallet, server: RPCServer, queue: DispatchQueue? = .none) { self.account = account @@ -134,13 +135,13 @@ class TokenProvider: TokenProviderType { attempt(maximumRetryCount: numberOfTimesToRetryFetchContractData) { GetERC721BalanceCoordinator(forServer: server, queue: queue) .getERC721TokenBalance(for: account, contract: address) - }.map { balance -> [String] in + }.map(on: queue, { balance -> [String] in if balance >= Int.max { throw AnyError(Web3Error(description: "")) } else { return [String](repeating: "0", count: Int(balance)) } - } + }) } } @@ -190,7 +191,7 @@ class TokenProvider: TokenProviderType { let isErc1155Promise = firstly { attempt(maximumRetryCount: numberOfTimesToRetryFetchContractData) { - GetIsERC1155ContractCoordinator(forServer: server) + self.isERC1155ContractDetector .getIsERC1155Contract(for: address) }.recover { _ -> Promise in return .value(false) diff --git a/AlphaWallet/EtherClient/TrustClient/Models/RawTransaction.swift b/AlphaWallet/EtherClient/TrustClient/Models/RawTransaction.swift index 9b6354314..918f29736 100644 --- a/AlphaWallet/EtherClient/TrustClient/Models/RawTransaction.swift +++ b/AlphaWallet/EtherClient/TrustClient/Models/RawTransaction.swift @@ -69,7 +69,7 @@ extension TransactionInstance { return firstly { createOperationForTokenTransfer(forTransaction: transaction, tokensDataStore: tokensDataStore, session: session) - }.then { operations -> Promise in + }.then(on: session.queue, { operations -> Promise in let result = TransactionInstance( id: transaction.hash, server: session.server, @@ -89,7 +89,7 @@ extension TransactionInstance { ) return .value(result) - } + }) } static private func createOperationForTokenTransfer(forTransaction transaction: RawTransaction, tokensDataStore: TokensDataStore, session: WalletSession) -> Promise<[LocalizedOperationObjectInstance]> { @@ -110,14 +110,14 @@ extension TransactionInstance { return firstly { when(fulfilled: getContractName, getContractSymbol, getDecimals, getTokenType) - }.then { name, symbol, decimals, tokenType -> Promise<[LocalizedOperationObjectInstance]> in + }.then(on: session.queue, { name, symbol, decimals, tokenType -> Promise<[LocalizedOperationObjectInstance]> in let operationType = mapTokenTypeToTransferOperationType(tokenType, functionCall: functionCall) let result = LocalizedOperationObjectInstance(from: transaction.from, to: recipient.eip55String, contract: contract, type: operationType.rawValue, value: String(value), tokenId: "", symbol: symbol, name: name, decimals: Int(decimals)) return .value([result]) - }.recover { _ -> Promise<[LocalizedOperationObjectInstance]> in + }).recover(on: session.queue, { _ -> Promise<[LocalizedOperationObjectInstance]> in //NOTE: Return an empty array when failure to fetch contracts data, instead of failing whole TransactionInstance creating return Promise.value([]) - } + }) } } diff --git a/AlphaWallet/Tokens/Coordinators/GetERC20BalanceCoordinator.swift b/AlphaWallet/Tokens/Coordinators/GetERC20BalanceCoordinator.swift index 675a32276..34e7e4628 100644 --- a/AlphaWallet/Tokens/Coordinators/GetERC20BalanceCoordinator.swift +++ b/AlphaWallet/Tokens/Coordinators/GetERC20BalanceCoordinator.swift @@ -17,7 +17,7 @@ class GetERC20BalanceCoordinator { func getBalance(for address: AlphaWallet.Address, contract: AlphaWallet.Address) -> Promise { let functionName = "balanceOf" - return callSmartContract(withServer: server, contract: contract, functionName: functionName, abiString: web3swift.Web3.Utils.erc20ABI, parameters: [address.eip55String] as [AnyObject], timeout: Constants.fetchContractDataTimeout).map(on: queue, { balanceResult in + return callSmartContract(withServer: server, contract: contract, functionName: functionName, abiString: web3swift.Web3.Utils.erc20ABI, parameters: [address.eip55String] as [AnyObject], timeout: Constants.fetchContractDataTimeout, queue: queue).map(on: queue, { balanceResult in if let balanceWithUnknownType = balanceResult["0"] { let string = String(describing: balanceWithUnknownType) if let balance = BigInt(string) { diff --git a/AlphaWallet/Tokens/Coordinators/GetERC721BalanceCoordinator.swift b/AlphaWallet/Tokens/Coordinators/GetERC721BalanceCoordinator.swift index 28e183c4a..2b1701e96 100644 --- a/AlphaWallet/Tokens/Coordinators/GetERC721BalanceCoordinator.swift +++ b/AlphaWallet/Tokens/Coordinators/GetERC721BalanceCoordinator.swift @@ -19,10 +19,10 @@ class GetERC721BalanceCoordinator { func getERC721TokenBalance(for address: AlphaWallet.Address, contract: AlphaWallet.Address) -> Promise { let function = GetERC721Balance() - return callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [address.eip55String] as [AnyObject], timeout: Constants.fetchContractDataTimeout).map(on: queue, { balanceResult -> BigUInt in - let balance = GetERC721BalanceCoordinator.adapt(balanceResult["0"] as Any) - return balance - }) + return callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [address.eip55String] as [AnyObject], timeout: Constants.fetchContractDataTimeout, queue: queue).map(on: queue, { balanceResult -> BigUInt in + let balance = GetERC721BalanceCoordinator.adapt(balanceResult["0"] as Any) + return balance + }) } private static func adapt(_ value: Any) -> BigUInt { diff --git a/AlphaWallet/Tokens/Coordinators/GetERC721ForTicketsBalanceCoordinator.swift b/AlphaWallet/Tokens/Coordinators/GetERC721ForTicketsBalanceCoordinator.swift index 586fa38d1..9e7baa49a 100644 --- a/AlphaWallet/Tokens/Coordinators/GetERC721ForTicketsBalanceCoordinator.swift +++ b/AlphaWallet/Tokens/Coordinators/GetERC721ForTicketsBalanceCoordinator.swift @@ -16,7 +16,7 @@ class GetERC721ForTicketsBalanceCoordinator { func getERC721ForTicketsTokenBalance(for address: AlphaWallet.Address, contract: AlphaWallet.Address) -> Promise<[String]> { let function = GetERC721ForTicketsBalance() - return callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [address.eip55String] as [AnyObject], timeout: Constants.fetchContractDataTimeout).map(on: queue, { balanceResult in + return callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [address.eip55String] as [AnyObject], timeout: Constants.fetchContractDataTimeout, queue: queue).map(on: queue, { balanceResult in return GetERC721ForTicketsBalanceCoordinator.adapt(balanceResult["0"]) }) } diff --git a/AlphaWallet/Tokens/Helpers/CallSmartContractFunction.swift b/AlphaWallet/Tokens/Helpers/CallSmartContractFunction.swift index 5f819b638..85e9624d1 100644 --- a/AlphaWallet/Tokens/Helpers/CallSmartContractFunction.swift +++ b/AlphaWallet/Tokens/Helpers/CallSmartContractFunction.swift @@ -58,7 +58,7 @@ func getCachedWeb3(forServer server: RPCServer, timeout: TimeInterval) throws -> private let callSmartContractQueue = DispatchQueue(label: "com.callSmartContractQueue.updateQueue") //`shouldDelayIfCached` is a hack for TokenScript views -func callSmartContract(withServer server: RPCServer, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject] = [], timeout: TimeInterval? = nil, shouldDelayIfCached: Bool = false) -> Promise<[String: Any]> { +func callSmartContract(withServer server: RPCServer, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject] = [], timeout: TimeInterval? = nil, shouldDelayIfCached: Bool = false, queue: DispatchQueue? = nil) -> Promise<[String: Any]> { let timeout: TimeInterval = 60 //We must include the ABI string in the key because the order of elements in a dictionary when serialized in the string is not ordered. Parameters (which is ordered) should ensure it's the same function let cacheKey = "\(contract).\(functionName) \(parameters) \(server.chainID)" @@ -84,27 +84,27 @@ func callSmartContract(withServer server: RPCServer, contract: AlphaWallet.Addre let result: Promise<[String: Any]> = Promise { seal in callSmartContractQueue.async { guard let web3 = try? getCachedWeb3(forServer: server, timeout: timeout) else { - seal.reject( Web3Error(description: "Error creating web3 for: \(server.rpcURL) + \(server.web3Network)")) + seal.reject(Web3Error(description: "Error creating web3 for: \(server.rpcURL) + \(server.web3Network)")) return } let contractAddress = EthereumAddress(address: contract) guard let contractInstance = web3swift.web3.web3contract(web3: web3, abiString: abiString, at: contractAddress, options: web3.options) else { - seal.reject( Web3Error(description: "Error creating web3swift contract instance to call \(functionName)()")) + seal.reject(Web3Error(description: "Error creating web3swift contract instance to call \(functionName)()")) return } guard let promiseCreator = contractInstance.method(functionName, parameters: parameters, options: nil) else { - seal.reject( Web3Error(description: "Error calling \(contract.eip55String).\(functionName)() with parameters: \(parameters)")) + seal.reject(Web3Error(description: "Error calling \(contract.eip55String).\(functionName)() with parameters: \(parameters)")) return } //callPromise() creates a promise. It doesn't "call" a promise. Bad name - promiseCreator.callPromise(options: nil).done(on: .main) { d in + promiseCreator.callPromise(options: nil).done(on: queue ?? .main, { d in seal.fulfill(d) - }.catch(on: .main) { e in + }).catch(on: queue ?? .main, { e in seal.reject(e) - } + }) } } diff --git a/AlphaWallet/Tokens/Models/NonFungibleContract.swift b/AlphaWallet/Tokens/Models/NonFungibleContract.swift index 92048ab06..a61f0d617 100644 --- a/AlphaWallet/Tokens/Models/NonFungibleContract.swift +++ b/AlphaWallet/Tokens/Models/NonFungibleContract.swift @@ -6,9 +6,11 @@ import PromiseKit class NonFungibleContract { private let server: RPCServer + private let queue: DispatchQueue - init(server: RPCServer) { + init(server: RPCServer, queue: DispatchQueue) { self.server = server + self.queue = queue } func getTokenUri(for tokenId: String, contract: AlphaWallet.Address) -> Promise { @@ -22,29 +24,29 @@ class NonFungibleContract { private func getTokenUriImpl(for tokenId: String, contract: AlphaWallet.Address) -> Promise { let function = GetTokenUri() return firstly { - callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [tokenId] as [AnyObject], timeout: Constants.fetchContractDataTimeout) - }.map { uriResult -> URL in + callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [tokenId] as [AnyObject], timeout: Constants.fetchContractDataTimeout, queue: queue) + }.map(on: queue, { uriResult -> URL in let string = ((uriResult["0"] as? String) ?? "").stringWithTokenIdSubstituted(tokenId) if let url = URL(string: string) { return url } else { throw Web3Error(description: "Error extracting tokenUri uri for contract \(contract.eip55String) tokenId: \(tokenId) string: \(uriResult)") } - } + }) } private func getUri(for tokenId: String, contract: AlphaWallet.Address) -> Promise { let function = GetUri() return firstly { - callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [tokenId] as [AnyObject], timeout: Constants.fetchContractDataTimeout) - }.map { uriResult -> URL in + callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [tokenId] as [AnyObject], timeout: Constants.fetchContractDataTimeout, queue: queue) + }.map(on: queue, { uriResult -> URL in let string = ((uriResult["0"] as? String) ?? "").stringWithTokenIdSubstituted(tokenId) if let url = URL(string: string) { return url } else { throw Web3Error(description: "Error extracting token uri for contract \(contract.eip55String) tokenId: \(tokenId) string: \(uriResult)") } - } + }) } } diff --git a/AlphaWallet/Transactions/TokensFromTransactionsFetcher.swift b/AlphaWallet/Transactions/TokensFromTransactionsFetcher.swift index bffc755b4..02320a7a8 100644 --- a/AlphaWallet/Transactions/TokensFromTransactionsFetcher.swift +++ b/AlphaWallet/Transactions/TokensFromTransactionsFetcher.swift @@ -65,10 +65,10 @@ final class TokensFromTransactionsFetcher { let tokenTypePromises = contracts.map { session.tokenProvider.getTokenType(for: $0) } return when(fulfilled: tokenTypePromises) - .map { tokenTypes in + .map(on: session.queue, { tokenTypes in let contractsToTokenTypes = Dictionary(uniqueKeysWithValues: zip(contracts, tokenTypes)) return (transactions: filteredTransactions, contractTypes: contractsToTokenTypes) - } + }) } }