619 lines
27 KiB
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()
|
|
}
|
|
}
|