// // NetworkServiceBlockchain.swift // List // // Created by Saveliy Stavitsky on 7/28/20. // Copyright © 2020 List. All rights reserved. // import EosioSwift import EosioSwiftSoftkeySignatureProvider import EosioSwiftAbieosSerializationProvider import PromiseKit import PMKFoundation import Alamofire import EosioSwiftEcc import Foundation import UIKit import WalletKit extension Network.Service.Blockchain { typealias Action = Network.Model.Blockchain.Action typealias Contract = EOSContract static func environment() -> ApplicationEnvironmentRecord { ApplicationEnvironment.shared().current } static func execute(contract: Contract, action: Action, data: Encodable, privateKeys: [String], closure: @escaping (Swift.Result) -> Void) { execute(actions: [(contract, action, data)], privateKeys: privateKeys, closure: closure) } static func execute(contract: String, action: Action, data: Encodable, privateKeys: [String], closure: @escaping (Swift.Result) -> Void) { executeRaw( actions: [(contract, action, data)], privateKeys: privateKeys, closure: closure ) } static func execute(actions: (contract: String, action: Action, data: Encodable)..., privateKeys: [String], closure: @escaping (Swift.Result) -> Void) { execute(actions: actions, privateKeys: privateKeys, closure: closure) } static func execute(actions: [(contract: String, action: Action, data: Encodable)], privateKeys: [String], closure: @escaping (Swift.Result) -> Void) { executeRaw( actions: actions, privateKeys: privateKeys, closure: closure ) } // static func execute(action: (contract: Contract, action: Action, data: Encodable), // privateKeys: [String], // closure: @escaping (Swift.Result) -> Void) { // execute(actions: [action], privateKeys: privateKeys, closure: closure) // } static func execute(actions: (contract: Contract, action: Action, data: Encodable)..., privateKeys: [String], closure: @escaping (Swift.Result) -> Void) { execute(actions: actions, privateKeys: privateKeys, closure: closure) } static func execute(actions: [(contract: Contract, action: Action, data: Encodable)], privateKeys: [String], closure: @escaping (Swift.Result) -> Void) { executeRaw( actions: actions.map({ (environment().contract($0.contract), $0.action, $0.data) }), privateKeys: privateKeys, closure: closure ) } } extension Network.Service.Blockchain { private enum Constants { static let outOfRamErrorCode = 3080002 static let outOfCpuErrorCode = 3080004 } // static var transaction: EosioTransaction! static func signWithK1(privateKey: String, data: Data) -> String { guard !privateKey.isEmpty else { return "" } let privateKeyData = (try? Data(eosioPrivateKey: privateKey)) ?? Data() let publicKeyData = (try? EccRecoverKey.recoverPublicKey(privateKey: privateKeyData, curve: .k1)) ?? Data() let result = (try? EosioEccSign.signWithK1(publicKey: publicKeyData, privateKey: privateKeyData, data: data)) ?? Data() return result.toEosioK1Signature } private enum EOSError { struct Response: Codable { let error: Error } struct Error: Codable { let name: String? let code: Int? let details: [Detail] } struct Detail: Codable { let message: String let method: String } struct Container: Codable { let json: Response } static func message(data: Data?) -> String? { guard let error = error(data: data) else { return nil } return message(error: error) } static func message(error: Error) -> String { if error.details.contains(where: { $0.method == "eosio_assert" }) { return error.details .filter({ $0.method == "eosio_assert" }) .map({ $0.message.replacingOccurrences(of: "assertion failure with message: ", with: "") }) .joined(separator: "\n") } else { return error.details.map({ $0.message }).joined(separator: "\n") } } static func error(data: Data?) -> Error? { guard let data = data else { return nil } if let response = try? JSONDecoder().decode(Response.self, from: data) { return response.error } else if let value = try? JSONDecoder().decode([String:Response].self, from: data), let response = value["json"] { return response.error } else if let container = try? JSONDecoder().decode(Container.self, from: data) { return container.json.error } return nil } } private struct Key { let eosioPublicKey: String let uncompressedPublicKey: Data let compressedPublicKey: Data let privateKey: Data } private static func showNoFreeTransactionsPopUp(type: QuotaType) { delayed(Animation.slow) { let resourcesControllerPopup = StoryboardScene.Resources.popup.instantiate() resourcesControllerPopup.selectQuota(type: type) Popup.show(content: resourcesControllerPopup) } } fileprivate struct Prepare: Codable { let signatures: [String] let serializedTransaction: String } static func signActions( actions: [(contract: String, action: R, data: Encodable)], privateKeys: [String], closure: @escaping (Swift.Result<([String], String), Error>) -> Void) where R.RawValue == String { let environment = ApplicationEnvironment.shared().current guard let node = environment.node, let endpoint = URL(string: node) else { fatalError("Can't create URL") return } if actions.isEmpty { closure(.success(([], ""))) } guard privateKeys.count > 0 else { closure(.failure("ExecuteRaw privateKeys empty")) return } guard let username = Accounts().current?.name, let keyType = Accounts().current?.keyType, let sender = try? EosioName(username) else { closure(.failure("Accounts().current username empty or not valid")) return } do { let signatureProvider = try EosioSoftkeySignatureProvider(privateKeys: privateKeys) let transaction = EosioTransactionFactory( rpcProvider: EosioRpcProvider(endpoint: endpoint, headers: environment.headers), signatureProvider: signatureProvider, serializationProvider: EosioAbieosSerializationProvider() ).newTransaction() for action in actions { let eosAction = try EosioTransaction.Action( account: EosioName(action.contract), name: EosioName(action.action.rawValue), authorization: [EosioTransaction.Action.Authorization( actor: sender, permission: EosioName(keyType.rawValue) )], data: action.data ) transaction.add(action: eosAction) } if Accounts().quota.isEnabled && !(((actions.first?.data.toDictionary()?["to"] as? String) ?? "") == environment.contract(.freeCPU)) { if Accounts().quota.paid + Accounts().quota.free >= actions.count { } else { Loader.hideAll() self.showNoFreeTransactionsPopUp(type: QuotaType.accountResources) // closure(.failure(.error("quota limit exceeded"))) return () } } if Accounts().quota.isEnabled || ((actions.first?.data.toDictionary()?["to"] as? String) ?? "") == environment.contract(.freeCPU) { let account = try EosioName(environment.contract(.freeCPU)) let eosAction = try EosioTransaction.Action( account: account, name: EosioName("freecpu"), authorization: [EosioTransaction.Action.Authorization( actor: account, permission: EosioName(WalletKeyType.active.rawValue) )], data: [String: Any]() ) transaction.add(action: eosAction, at: 0) } transaction.prepare { switch $0 { case .success: let serializedTransaction = (try? transaction.serializeTransaction()) ?? Data() var signatures = [String]() for privateKey in privateKeys { let chainIdData = Data(hexString: transaction.chainId) ?? Data() let zeros = Data(repeating: 0, count: 32) let data = chainIdData + serializedTransaction + zeros signatures.append(signWithK1(privateKey: privateKey, data: data)) } closure(.success((signatures, serializedTransaction.hex))) case let .failure(error): trackError(actions: transaction.actions, message: error.reason) closure(.failure(error.reason)) } } } catch { closure(.failure(error.localizedDescription)) print(error.eosioError.description) trackError(actions: [], message: error.eosioError.description) } } private static func executeRaw( actions: [(contract: String, action: R, data: Encodable)], privateKeys: [String], closure: @escaping (Swift.Result) -> Void) where R.RawValue == String { let environment = ApplicationEnvironment.shared().current let endpoint = URL(string: environment.node.orCreate(""))! guard privateKeys.count > 0 else { closure(.failure("ExecuteRaw privateKeys empty")) return } guard let username = Accounts().current?.name, let keyType = Accounts().current?.keyType, let sender = try? EosioName(username) else { closure(.failure("Accounts().current username empty or not valid")) return } do { let signatureProvider = try EosioSoftkeySignatureProvider(privateKeys: privateKeys) let transaction = EosioTransactionFactory( rpcProvider: EosioRpcProvider(endpoint: endpoint, headers: environment.headers), signatureProvider: signatureProvider, serializationProvider: EosioAbieosSerializationProvider() ).newTransaction() for action in actions { let eosAction = try EosioTransaction.Action( account: EosioName(action.contract), name: EosioName(action.action.rawValue), authorization: [EosioTransaction.Action.Authorization( actor: sender, permission: EosioName(keyType.rawValue) )], data: action.data ) transaction.add(action: eosAction) } if Accounts().quota.isEnabled && !(((actions.first?.data.toDictionary()?["to"] as? String) ?? "") == environment.contract(.freeCPU)) { if Accounts().quota.paid + Accounts().quota.free >= actions.count { } else { Loader.hideAll() self.showNoFreeTransactionsPopUp(type: QuotaType.freeTransactions) let finalError = BlockchainError.commonError(description: L10n.Resources.Setup.Buy.noFreeTransactions) Alert.notify(finalError) closure(.failure(finalError)) return () } } if Accounts().quota.isEnabled || ((actions.first?.data.toDictionary()?["to"] as? String) ?? "") == environment.contract(.freeCPU) { let account = try EosioName(environment.contract(.freeCPU)) let eosAction = try EosioTransaction.Action( account: account, name: EosioName("freecpu"), authorization: [EosioTransaction.Action.Authorization( actor: account, permission: EosioName(WalletKeyType.active.rawValue) )], data: [String: Any]() ) transaction.add(action: eosAction, at: 0) transaction.prepare { switch $0 { case .success: let serializedTransaction = (try? transaction.serializeTransaction()) ?? Data() var signatures = [String]() for privateKey in privateKeys { let chainIdData = Data(hexString: transaction.chainId) ?? Data() let zeros = Data(repeating: 0, count: 32) let data = chainIdData + serializedTransaction + zeros signatures.append(signWithK1(privateKey: privateKey, data: data)) } let url = URL(string: "\(environment.backend.urlPath)/push_transaction")! let parameters = Prepare( signatures: signatures, serializedTransaction: serializedTransaction.hex ) let request = AF.request(url, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: HTTPHeaders(environment.headers)) request.responseDecodable(of: EosioRpcTransactionResponse.self) { guard let data = $0.data else { closure(.failure($0.error?.localizedDescription ?? "no data")) return } let string = String(data: data, encoding: .utf8) ?? "" if let error = $0.error?.localizedDescription { trackError(actions: transaction.actions, message: string) if string.contains("quota limit exceeded") { Loader.hideAll() self.showNoFreeTransactionsPopUp(type: QuotaType.accountResources) return () } else if let value = EOSError.message(data: $0.data) { closure(.failure(value)) } else { closure(.failure(string)) } } else { closure(.success($0.value?.transactionId ?? "")) if Accounts().quota.isEmpty { Accounts().fetchQuota() } else if ((actions.first?.data.toDictionary()?["to"] as? String) ?? "") != environment.contract(.freeCPU) { let free = Accounts().quota.free - actions.count let paid = free >= 0 ? Accounts().quota.paid : Accounts().quota.paid + free Accounts().quota.update(free: free >= 0 ? free : 0, paid: paid >= 0 ? paid : 0) if paid + free <= 4 { self.showNoFreeTransactionsPopUp(type: QuotaType.accountResources) } } } } case let .failure(error): trackError(actions: transaction.actions, message: error.reason) closure(.failure(error.reason)) } } return () } transaction.signAndBroadcast { result in print((try? transaction.toJson(prettyPrinted: true)) ?? "") transaction.actions.enumerated().forEach({ print("Action \($0): \($1.dataJson ?? "")") }) switch result { case .failure(let error): Self.handleRaw(error: error, transaction: transaction, closure: closure) case .success: if let transactionId = transaction.transactionId { closure(.success(transactionId)) } } } } catch { closure(.failure(error.localizedDescription)) print(error.eosioError.description) trackError(actions: [], message: error.eosioError.description) } } private static func handleRaw(error: EosioError, transaction: EosioTransaction, closure: @escaping (Swift.Result) -> Void) { DispatchQueue.main.async { print(error.eosioError.description) } var message = error.localizedDescription if let value = (error.originalError as Error?) as? PMKFoundation.PMKHTTPError { switch value { case .badStatusCode(_, let data, let response): let trackErrorMessage = String(data: data, encoding: .utf8) ?? response.description Self.trackError(actions: transaction.actions, message: trackErrorMessage) if let error = EOSError.error(data: data) { message = EOSError.message(error: error) let outOfResourcesErrorCodes = [Constants.outOfRamErrorCode, Constants.outOfCpuErrorCode] if let code = error.code, outOfResourcesErrorCodes.contains(code) { message = L10n.Resources.Setup.Buy.cpuError Loader.hideAll() let resultError = BlockchainError.lackOfResources(description: message) Alert.notify(resultError) closure(.failure(resultError)) Self.showNoFreeTransactionsPopUp(type: .accountResources) } else { closure(.failure(message)) } return } else { closure(.failure(trackErrorMessage)) return } } } else { Self.trackError(actions: transaction.actions, message: error.localizedDescription) } print("ERROR SIGNING OR BROADCASTING TRANSACTION") print(error.reason) print(message) closure(.failure(message)) } static func queryRaw( endpoint: String = ApplicationEnvironment.shared().current.node.orCreate(""), contract: Contract, scope: String, table: String, limit: Int = 1000) async -> Swift.Result<[[String: Any]], String> { await withCheckedContinuation { (continuation: CheckedContinuation, Never>) in let environment = ApplicationEnvironment.shared().current let endpoint = URL(string: endpoint)! let rpcProvider = EosioRpcProvider(endpoint: endpoint, headers: environment.headers) let request = EosioRpcTableRowsRequest(scope: scope, code: environment.contract(contract), table: table, limit: 1000) rpcProvider.getTableRows(requestParameters: request) { result in switch result { case let .success(rows): continuation.resume(returning: .success((rows.rows as? [[String: Any]]) ?? [[:]])) case let .failure(error): var message = error.localizedDescription if let value = (error.originalError as Error?) as? PMKFoundation.PMKHTTPError { switch value { case .badStatusCode(_, let data, _): if let value = EOSError.message(data: data) { message = value } } } print("ERROR SIGNING OR BROADCASTING TRANSACTION") print(error.reason) print(message) continuation.resume(returning: .failure(message)) } } } } static func queryRaw( endpoint: String = ApplicationEnvironment.shared().current.node.orCreate(""), contract: Contract, scope: String, table: String, limit: Int = 1000, closure: @escaping (Swift.Result<[[String: Any]], String>) -> Void) { let environment = ApplicationEnvironment.shared().current let endpoint = URL(string: endpoint)! let rpcProvider = EosioRpcProvider(endpoint: endpoint, headers: environment.headers) let request = EosioRpcTableRowsRequest(scope: scope, code: environment.contract(contract), table: table, limit: 1000) rpcProvider.getTableRows(requestParameters: request) { result in switch result { case let .success(rows): closure(.success((rows.rows as? [[String: Any]]) ?? [[:]])) case let .failure(error): var message = error.localizedDescription if let value = (error.originalError as Error?) as? PMKFoundation.PMKHTTPError { switch value { case .badStatusCode(_, let data, _): if let value = EOSError.message(data: data) { message = value } } } print("ERROR SIGNING OR BROADCASTING TRANSACTION") print(error.reason) print(message) closure(.failure(message)) } } } private static func trackError(actions: [EosioTransaction.Action], message: String) { let environment = ApplicationEnvironment.shared().current guard let url = URL(string: environment.backend.errorUrlPath) else { return } // Details info let details = Network.Model.Blockchain.ErrorTrackingRequestExceptionDetails( actions: actions.map({ Network.Model.Blockchain.ErrorTrackingRequestExceptionDetailsAction( authorization: $0.authorization.map({ Network.Model.Blockchain.ErrorTrackingRequestExceptionDetailsActionAuthorization( actor: $0.actor.string, permission: $0.permission.string ) }), account: $0.account.string, name: $0.name.string, data: ($0.data.jsonString ?? "").data(using: .utf8)?.hex ?? "" ) }), message: message ) // Settings info let settings = Network.Model.Blockchain.ErrorTrackingRequestSettings( isFreeCpuEnabled: Accounts().quota.isEnabled, build: "\(Bundle.main.versionNumber).\(Bundle.main.buildNumber)", activeAccountUsername: Accounts().current?.name ?? "", environment: environment.backend.name, endpoints: environment.nodes + environment.hyperions + [ environment.other.listSite, environment.other.listGraphQL, environment.backend.urlPath, environment.backend.webSocket ], eosNodeEndpoint: environment.node.orCreate(""), hyperionEndpoint: environment.hyperion.orCreate("") ) // Device info let deviceOS = Network.Model.Blockchain .ErrorTrackingRequestDeviceOS( name: UIDevice.current.systemName, version: UIDevice.current.systemVersion ) let device = Network.Model.Blockchain.ErrorTrackingRequestDevice( type: Bundle.main.deviceName, locale: Locale.current.identifier, timestamp: Date().toISODateString, // "2021-01-19T17:39:39.000", timezone: Locale.current.calendar.timeZone.identifier, os: deviceOS ) let exception = Network.Model.Blockchain.ErrorTrackingRequestVariablesException( device: device, settings: settings, exceptionDetails: details ) let errorTrackingRequestData = Network.Model.Blockchain.ErrorTrackingRequest( query: "mutation AddException($exception:ExceptionInput!){\n addException(exception:$exception){\n errors\n }\n}", variables: Network.Model.Blockchain.ErrorTrackingRequestVariables(exception: exception), operationName: "AddException") print((try? errorTrackingRequestData.toJsonString()) ?? "") var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Application/json", forHTTPHeaderField: "Content-Type") environment.headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) } request.httpBody = ((try? errorTrackingRequestData.toJsonString()) ?? "").data(using: .utf8) URLSession.shared.dataTask(with: request) { (data, response, error) in if let response = response { print(response) } if let data = data { do { let json = try JSONSerialization.jsonObject(with: data, options: []) print(json) } catch { print(error) } } }.resume() } }