Move some balance update callback to background queue

pull/4461/head
Krypto Pank 3 years ago
parent d89360a492
commit d141d994ea
  1. 35
      AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift
  2. 2
      AlphaWallet/Core/ENS/ENSDelegateImpl.swift
  3. 3
      AlphaWallet/Core/NFT/Enjin/EnjinNetworkProvider.swift
  4. 8
      AlphaWallet/Core/Types/Session.swift
  5. 7
      AlphaWallet/Core/Types/TokenProviderType.swift
  6. 10
      AlphaWallet/EtherClient/TrustClient/Models/RawTransaction.swift
  7. 2
      AlphaWallet/Tokens/Coordinators/GetERC20BalanceCoordinator.swift
  8. 8
      AlphaWallet/Tokens/Coordinators/GetERC721BalanceCoordinator.swift
  9. 2
      AlphaWallet/Tokens/Coordinators/GetERC721ForTicketsBalanceCoordinator.swift
  10. 14
      AlphaWallet/Tokens/Helpers/CallSmartContractFunction.swift
  11. 16
      AlphaWallet/Tokens/Models/NonFungibleContract.swift
  12. 4
      AlphaWallet/Transactions/TokensFromTransactionsFetcher.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<AnyCancellable>()
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<String>] = 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<String> {
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

@ -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? {

@ -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),

@ -8,10 +8,15 @@ 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
@ -19,7 +24,6 @@ class WalletSession {
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

@ -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<Bool> in
return .value(false)

@ -69,7 +69,7 @@ extension TransactionInstance {
return firstly {
createOperationForTokenTransfer(forTransaction: transaction, tokensDataStore: tokensDataStore, session: session)
}.then { operations -> Promise<TransactionInstance?> in
}.then(on: session.queue, { operations -> Promise<TransactionInstance?> 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([])
}
})
}
}

@ -17,7 +17,7 @@ class GetERC20BalanceCoordinator {
func getBalance(for address: AlphaWallet.Address, contract: AlphaWallet.Address) -> Promise<BigInt> {
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) {

@ -19,10 +19,10 @@ class GetERC721BalanceCoordinator {
func getERC721TokenBalance(for address: AlphaWallet.Address, contract: AlphaWallet.Address) -> Promise<BigUInt> {
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 {

@ -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"])
})
}

@ -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)
}
})
}
}

@ -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<URL> {
@ -22,29 +24,29 @@ class NonFungibleContract {
private func getTokenUriImpl(for tokenId: String, contract: AlphaWallet.Address) -> Promise<URL> {
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<URL> {
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)")
}
}
})
}
}

@ -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)
}
})
}
}

Loading…
Cancel
Save