Files
raspberry/iOS/Wallet/Sources/Network/Service/NetworkServiceBlockchain.swift

619 lines
27 KiB
Swift

//
// 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<String, Error>) -> 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<String, Error>) -> 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<String, Error>) -> Void) {
execute(actions: actions, privateKeys: privateKeys, closure: closure)
}
static func execute(actions: [(contract: String, action: Action, data: Encodable)],
privateKeys: [String],
closure: @escaping (Swift.Result<String, Error>) -> Void) {
executeRaw(
actions: actions,
privateKeys: privateKeys,
closure: closure
)
}
// static func execute(action: (contract: Contract, action: Action, data: Encodable),
// privateKeys: [String],
// closure: @escaping (Swift.Result<String, Error>) -> Void) {
// execute(actions: [action], privateKeys: privateKeys, closure: closure)
// }
static func execute(actions: (contract: Contract, action: Action, data: Encodable)...,
privateKeys: [String],
closure: @escaping (Swift.Result<String, Error>) -> Void) {
execute(actions: actions, privateKeys: privateKeys, closure: closure)
}
static func execute(actions: [(contract: Contract, action: Action, data: Encodable)],
privateKeys: [String],
closure: @escaping (Swift.Result<String, Error>) -> 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<R: RawRepresentable>(
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<R: RawRepresentable>(
actions: [(contract: String, action: R, data: Encodable)],
privateKeys: [String],
closure: @escaping (Swift.Result<String, Error>) -> 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<String, Error>) -> 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<Swift.Result<[[String: Any]], String>, 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()
}
}