// // EosioAbiProvider.swift // EosioSwift // // Created by Todd Bowden on 2/24/19. // Copyright (c) 2017-2019 block.one and its contributors. All rights reserved. // import Foundation /// Default implementation of the `EosioAbiProviderProtocol`. For fetching and caching ABIs. public class EosioAbiProvider: EosioAbiProviderProtocol { private let rpcProvider: EosioRpcProviderProtocol private var abis = [String: Data]() private let lock = String() /// Initialize the ABI Provider. /// /// - Parameter rpcProvider: The RPC provider, which this ABI provider will use to fetch ABIs. public init(rpcProvider: EosioRpcProviderProtocol) { self.rpcProvider = rpcProvider } private func getCachedAbi(chainId: String, account: EosioName) -> Data? { return abis[chainId + account.string] } private func cacheAbi(_ abi: Data, chainId: String, account: EosioName) { objc_sync_enter(self.lock) abis[chainId + account.string] = abi objc_sync_exit(self.lock) } /// Get all ABIs for the given accounts, keyed by account name. /// /// - Parameters: /// - chainId: The chain ID. /// - accounts: An array of account names as `EosioName`s. /// - completion: Calls the completion with an `EosioResult` containing a map of ABIs as Data for all of the given accounts, keyed by the account name. An ABI for each account must be /// returned, otherwise an `EosioResult.failure` type will be returned. public func getAbis(chainId: String, accounts: [EosioName], completion: @escaping (EosioResult<[EosioName: Data], EosioError>) -> Void) { let accounts = Array(Set(accounts)) // remove any duplicate account names var responseAbis = [EosioName: Data]() var optionalError: EosioError? let dispatchGroup = DispatchGroup() for account in accounts { dispatchGroup.enter() getAbi(chainId: chainId, account: account) { (response) in switch response { case .success(let abi): responseAbis[account] = abi dispatchGroup.leave() case .failure(let error): if optionalError == nil { optionalError = error } dispatchGroup.leave() } } } dispatchGroup.notify(queue: .main) { if let validError = optionalError { completion(.failure(validError)) } else { completion(.success(responseAbis)) } } } /// Get the ABI as `Data` for the specified account name. /// /// - Parameters: /// - chainId: The chain ID. /// - account: The account name as an `EosioName`. /// - completion: Calls the completion with an `EosioResult` containing the ABI as Data. An `EosioResult.failure` type will be returned if the specified ABI could not be found /// or decoded properly. public func getAbi(chainId: String, account: EosioName, completion: @escaping (EosioResult) -> Void) { if let abi = getCachedAbi(chainId: chainId, account: account) { return completion(.success(abi)) } let requestParameters = EosioRpcRawAbiRequest(accountName: account) rpcProvider.getRawAbiBase(requestParameters: requestParameters) { (response) in switch response { case .success(let abiResponse): do { let abi = try Data(base64: abiResponse.abi) let computedHash = abi.sha256.hex.lowercased() let declaredHash = abiResponse.abiHash.lowercased() guard computedHash == declaredHash else { let errorReason = "Computed hash of abi for \(account) \(computedHash) does not match declared hash \(declaredHash)" return completion(.failure(EosioError(.getRawAbiError, reason: errorReason))) } guard account.string == abiResponse.accountName else { let errorReason = "Requested account \(account) does not match declared account \(abiResponse.accountName)" return completion(.failure(EosioError(.getRawAbiError, reason: errorReason))) } self.cacheAbi(abi, chainId: chainId, account: account) return completion(.success(abi)) } catch { completion(.failure(error.eosioError)) } case .failure(let error): completion(.failure(error)) } } } }