diff --git a/Extensions/PromiseKit/TezosNode/TezosNodeClient+Promises.swift b/Extensions/PromiseKit/TezosNode/TezosNodeClient+Promises.swift index 96878210..9d5a95be 100644 --- a/Extensions/PromiseKit/TezosNode/TezosNodeClient+Promises.swift +++ b/Extensions/PromiseKit/TezosNode/TezosNodeClient+Promises.swift @@ -457,7 +457,7 @@ extension TezosNodeClient { signatureProvider: SignatureProvider ) -> Promise { return Promise { seal in - forgeSignPreapplyAndInject([operation], source: source, signatureProvider: signatureProvider) { result in + forgeParseSignPreapplyAndInject([operation], source: source, signatureProvider: signatureProvider) { result in switch result { case .success(let data): seal.fulfill(data) @@ -483,7 +483,7 @@ extension TezosNodeClient { signatureProvider: SignatureProvider ) -> Promise { return Promise { seal in - forgeSignPreapplyAndInject(operations, source: source, signatureProvider: signatureProvider) { result in + forgeParseSignPreapplyAndInject(operations, source: source, signatureProvider: signatureProvider) { result in switch result { case .success(let data): seal.fulfill(data) diff --git a/TezosKit.xcodeproj/project.pbxproj b/TezosKit.xcodeproj/project.pbxproj index 2e443b73..68af8dec 100644 --- a/TezosKit.xcodeproj/project.pbxproj +++ b/TezosKit.xcodeproj/project.pbxproj @@ -377,6 +377,10 @@ BF8368AEC93713FD0257CB2A /* AbstractOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81BC48461432501639D06FD6 /* AbstractOperation.swift */; }; BFA57870FD2388EB6150946E /* NatMichelsonParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4E74F690D954E7535B88EC /* NatMichelsonParameter.swift */; }; C0382A1E21312C3D3323732F /* BigInt.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7806BE5035AB3100BA7C791C /* BigInt.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C05C0BB924AB5ABF0003CE13 /* ParsingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05C0BB824AB5ABF0003CE13 /* ParsingService.swift */; }; + C05C0BBA24AB5ABF0003CE13 /* ParsingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05C0BB824AB5ABF0003CE13 /* ParsingService.swift */; }; + C05C0BBC24AB5BAD0003CE13 /* ParseOperationRPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05C0BBB24AB5BAD0003CE13 /* ParseOperationRPC.swift */; }; + C05C0BBD24AB5BAD0003CE13 /* ParseOperationRPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05C0BBB24AB5BAD0003CE13 /* ParseOperationRPC.swift */; }; C15CDCE9A538AB91764BCE7E /* LeftMichelsonParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18257A644A0B08F1E48E8808 /* LeftMichelsonParameter.swift */; }; C19A7D4FDB1130C27E6F64FB /* TezosKit_macOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A09B07ABCB8334FE1706D29E /* TezosKit_macOS.framework */; }; C3449CF9473642AADC85DC42 /* MichelsonAnnotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 362C94B98BAA147AB9D080DC /* MichelsonAnnotationTests.swift */; }; @@ -627,7 +631,7 @@ 009F72EE9B4FAB81EA63A88E /* OperationFees.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationFees.swift; sourceTree = ""; }; 01A461E89A11A327962A4232 /* SignedOperationPayloadTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedOperationPayloadTest.swift; sourceTree = ""; }; 039C325259AE4DD3416B4F30 /* JSONArrayResponseAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONArrayResponseAdapter.swift; sourceTree = ""; }; - 03F182F3E55450A2C4C67477 /* TezosKit_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TezosKit_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 03F182F3E55450A2C4C67477 /* TezosKit_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = TezosKit_iOS.framework; path = TezosKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 04781056C289A009489C072C /* OperationWithCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationWithCounter.swift; sourceTree = ""; }; 06027E9BA1340E098E769026 /* GetAddressDelegateRPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAddressDelegateRPC.swift; sourceTree = ""; }; 0675B789B9F5EF98BC4E4568 /* GetReceivedTransactions.RPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetReceivedTransactions.RPC.swift; sourceTree = ""; }; @@ -774,7 +778,7 @@ 9EA478BD897E8082B638E6E0 /* SecretKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretKey.swift; sourceTree = ""; }; 9F6AA3CAB521F463866CFB96 /* TezosNodeClient+Promises.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TezosNodeClient+Promises.swift"; sourceTree = ""; }; A057E76CA636DDEE045ED59C /* OperationMetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMetadataProvider.swift; sourceTree = ""; }; - A09B07ABCB8334FE1706D29E /* TezosKit_macOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TezosKit_macOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A09B07ABCB8334FE1706D29E /* TezosKit_macOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = TezosKit_macOS.framework; path = TezosKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A0A82D9C70BEC192B4D73809 /* TezosNodeClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TezosNodeClient.swift; sourceTree = ""; }; A17556CA46001F8FB5DF8302 /* JSONUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONUtils.swift; sourceTree = ""; }; A227FD388A5B50891D5C7FE1 /* RunOperationRPCTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunOperationRPCTest.swift; sourceTree = ""; }; @@ -812,6 +816,8 @@ BF24BCDA4254C7A2D50F5728 /* ConseilQueryRPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConseilQueryRPC.swift; sourceTree = ""; }; BFC366A9719F3F6D07093E37 /* GetBigMapValueByIDRPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetBigMapValueByIDRPC.swift; sourceTree = ""; }; C014CF988B5D6585A97BC0DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + C05C0BB824AB5ABF0003CE13 /* ParsingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsingService.swift; sourceTree = ""; }; + C05C0BBB24AB5BAD0003CE13 /* ParseOperationRPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseOperationRPC.swift; sourceTree = ""; }; C1968AE0A3A2B4DAD756FD72 /* ConseilClientIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConseilClientIntegrationTests.swift; sourceTree = ""; }; C1FCF6EFDB51394D49E1479C /* PreapplicationServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreapplicationServiceTest.swift; sourceTree = ""; }; C525A21D32496E9B7BB4F79A /* GetAddressCounterRPCTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAddressCounterRPCTest.swift; sourceTree = ""; }; @@ -1459,6 +1465,7 @@ isa = PBXGroup; children = ( AAF8AE07E00BB723F1C5F139 /* ForgeOperationRPC.swift */, + C05C0BBB24AB5BAD0003CE13 /* ParseOperationRPC.swift */, CF8BF4B1C7E42EB59D27150B /* GetAddressBalanceRPC.swift */, CF147FA8F9FDDEBA70FA2759 /* GetAddressCounterRPC.swift */, 06027E9BA1340E098E769026 /* GetAddressDelegateRPC.swift */, @@ -1520,6 +1527,7 @@ B66D157EC9056A7A23FBC45D /* DefaultFeeProvider.swift */, 0F58CAE6BB7F9A44407F558B /* FeeEstimator.swift */, 2035C617A6D99FFB50ED1842 /* ForgingService.swift */, + C05C0BB824AB5ABF0003CE13 /* ParsingService.swift */, 361D83203112185AC915DDD2 /* InjectionService.swift */, AB59AEAEB019E7B641B448B1 /* OperationFactory.swift */, A057E76CA636DDEE045ED59C /* OperationMetadataProvider.swift */, @@ -1678,14 +1686,13 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1020; - TargetAttributes = { - }; }; buildConfigurationList = 719A796CF4303FCE1EA34089 /* Build configuration list for PBXProject "TezosKit" */; compatibilityVersion = "Xcode 10.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( + en, Base, ); mainGroup = 40497E8417609B6A929D29D7; @@ -1928,6 +1935,7 @@ 00A736FA9438F4C332A56A3C /* ForgingPolicy.swift in Sources */, 20C16CFCB554CF81CB2CF03C /* ForgingService.swift in Sources */, 5C9E6119D5B8F50A8C076024 /* GasLimitPolicy.swift in Sources */, + C05C0BBC24AB5BAD0003CE13 /* ParseOperationRPC.swift in Sources */, CB2EFDA5DD8A6BFCE675809F /* GetAddressBalanceRPC.swift in Sources */, 2655ECC6A0A89FDF23DF18A6 /* GetAddressCounterRPC.swift in Sources */, 7E5A3D04C2B733F09D4922D6 /* GetAddressDelegateRPC.swift in Sources */, @@ -1961,6 +1969,7 @@ 39956383AC208C08BA1F8D85 /* KeyChainWallet.swift in Sources */, 828D81A62A8E75DF6DCD5FF2 /* KeyHashMichelsonParameter.swift in Sources */, 0F3B3DB598C464FF2739B6C8 /* KeyMichelsonParameter.swift in Sources */, + C05C0BB924AB5ABF0003CE13 /* ParsingService.swift in Sources */, C15CDCE9A538AB91764BCE7E /* LeftMichelsonParameter.swift in Sources */, A3C3011FB97A95E5AFB9C6D7 /* ListMichelsonParameter.swift in Sources */, 3E2839CA355D9C016F8D57E8 /* Logger.swift in Sources */, @@ -2162,6 +2171,7 @@ AB844D81BBE08DCFBD9FFF79 /* ForgingPolicy.swift in Sources */, 68EE902B48CF6D7D19AC7E67 /* ForgingService.swift in Sources */, 83A3CE6EDD434DE0657A3E39 /* GasLimitPolicy.swift in Sources */, + C05C0BBD24AB5BAD0003CE13 /* ParseOperationRPC.swift in Sources */, B0AD205420E9186FFBDF591A /* GetAddressBalanceRPC.swift in Sources */, 369AFABBC4DC7B34F4122FE7 /* GetAddressCounterRPC.swift in Sources */, 1A0E6F1CE869DB4A7C0D1C18 /* GetAddressDelegateRPC.swift in Sources */, @@ -2195,6 +2205,7 @@ 362D974F94161678F1315F88 /* KeyChainWallet.swift in Sources */, A712A66755B2A0E5A5E23864 /* KeyHashMichelsonParameter.swift in Sources */, B9E166280AEE231EC79A1533 /* KeyMichelsonParameter.swift in Sources */, + C05C0BBA24AB5ABF0003CE13 /* ParsingService.swift in Sources */, B1E5191F0BB57EF3A72555FC /* LeftMichelsonParameter.swift in Sources */, 8B41AC6DD12AF82644C3E88A /* ListMichelsonParameter.swift in Sources */, 515D49DEE731B115152BB63F /* Logger.swift in Sources */, @@ -2483,7 +2494,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks $(PROJECT_DIR)/Carthage/Build/iOS"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2734,7 +2750,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks $(PROJECT_DIR)/Carthage/Build/iOS"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_VERSION = 5.0; diff --git a/TezosKit/Common/Michelson/TimestampMichelsonParameter.swift b/TezosKit/Common/Michelson/TimestampMichelsonParameter.swift index fdba58d1..8e8cc728 100644 --- a/TezosKit/Common/Michelson/TimestampMichelsonParameter.swift +++ b/TezosKit/Common/Michelson/TimestampMichelsonParameter.swift @@ -7,7 +7,8 @@ public class Timestamp: AbstractMichelsonParameter { public init(date: Date, annotations: [MichelsonAnnotation]? = nil) { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" - dateFormatter.timeZone = TimeZone(abbreviation: "GMT") + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") + dateFormatter.locale = Locale(identifier: "en_US_POSIX") let string = dateFormatter.string(from: date) diff --git a/TezosKit/Common/Models/Tez.swift b/TezosKit/Common/Models/Tez.swift index cdeca37d..c5e2b8ce 100644 --- a/TezosKit/Common/Models/Tez.swift +++ b/TezosKit/Common/Models/Tez.swift @@ -36,11 +36,15 @@ public struct Tez { public var rpcRepresentation: String { // Trim any leading zeroes by converting to an Int. let intermediateString = String(normalizedAmount) - return intermediateString.replacingOccurrences( - of: "^0+", - with: "", - options: .regularExpression - ) + let santizedString = intermediateString.replacingOccurrences(of: "^0+", with: "", options: .regularExpression) + + // When implementing the RPC parse function, returning an empty string causes mismatches. + // The Tezos node will replace empty strings with "0", as it always expects a value to be present + if santizedString == "" { + return "0" + } + + return santizedString } /// Initialize a new balance from a given decimal number. diff --git a/TezosKit/Common/Models/Wallet.swift b/TezosKit/Common/Models/Wallet.swift index b939bbb0..08949ec4 100644 --- a/TezosKit/Common/Models/Wallet.swift +++ b/TezosKit/Common/Models/Wallet.swift @@ -101,7 +101,7 @@ public struct Wallet { /// - secretKey: The secret key. /// - mnemonic: An optional mnemonic used to generate the wallet. private init(address: Address, publicKey: PublicKey, secretKey: SecretKey, mnemonic: String? = nil) { - JailbreakUtils.crashIfJailbroken() + //JailbreakUtils.crashIfJailbroken() self.secretKey = secretKey self.publicKey = publicKey diff --git a/TezosKit/Common/Services/NetworkClient.swift b/TezosKit/Common/Services/NetworkClient.swift index 7aae3b83..cdb3fa90 100644 --- a/TezosKit/Common/Services/NetworkClient.swift +++ b/TezosKit/Common/Services/NetworkClient.swift @@ -4,147 +4,170 @@ import Foundation /// An opaque network client which implements requests. public protocol NetworkClient { - /// Send an RPC. - /// - /// - Note: Callbacks for the RPC will run on the callback queue the network client was initialized with. - /// - /// - Parameters: - /// - rpc: The RPC to send. - /// - completion: A completion block which contains the results of the RPC. - func send( - _ rpc: RPC, - completion: @escaping (Result) -> Void - ) - - /// Send an RPC which runs a callback on a custom queue. - /// - /// - Note: Callbacks for the RPC will run on the callback queue provided. - /// - /// - Parameters: - /// - rpc: The RPC to send. - /// - callbackQueus: A callback queue to call the completion block on. If nil, the default queue will be used. - /// - completion: A completion block which contains the results of the RPC. - func send( - _ rpc: RPC, - callbackQueue: DispatchQueue?, - completion: @escaping (Result) -> Void - ) + /// Send an RPC. + /// + /// - Note: Callbacks for the RPC will run on the callback queue the network client was initialized with. + /// + /// - Parameters: + /// - rpc: The RPC to send. + /// - completion: A completion block which contains the results of the RPC. + func send( + _ rpc: RPC, + completion: @escaping (Result) -> Void + ) + + /// Send an RPC which runs a callback on a custom queue. + /// + /// - Note: Callbacks for the RPC will run on the callback queue provided. + /// + /// - Parameters: + /// - rpc: The RPC to send. + /// - callbackQueus: A callback queue to call the completion block on. If nil, the default queue will be used. + /// - completion: A completion block which contains the results of the RPC. + func send( + _ rpc: RPC, + callbackQueue: DispatchQueue?, + completion: @escaping (Result) -> Void + ) + + var errorCallback: ((String?, String?, Error, String) -> Void)? { get set } } /// A standard implementation of the network client. public class NetworkClientImpl: NetworkClient { - - /// The URL session that will be used to manage URL requests. - private let urlSession: URLSession - - /// A URL pointing to a remote node that will handle requests made by this client. - private let remoteNodeURL: URL - - /// Headers which will be added to every request. - private let headers: [Header] - - /// A response handler for RPCs. - private let responseHandler: RPCResponseHandler - - /// The queue that callbacks from requests will be made on. - internal let callbackQueue: DispatchQueue - - /// Initialize a new AbstractNetworkClient. - /// - Parameters: - /// - remoteNodeURL: The path to the remote node. - /// - urlSession: The URLSession that will manage network requests. - /// - headers: Headers which will be added to every request. - /// - callbackQueue: A dispatch queue that callbacks will be made on. - /// - responseHandler: An object which will handle responses. - public init( - remoteNodeURL: URL, - urlSession: URLSession, - headers: [Header] = [], - callbackQueue: DispatchQueue, - responseHandler: RPCResponseHandler - ) { - self.remoteNodeURL = remoteNodeURL - self.urlSession = urlSession - self.headers = headers - self.callbackQueue = callbackQueue - self.responseHandler = responseHandler - } - - public func send( - _ rpc: RPC, - completion: @escaping (Result) -> Void - ) { - send(rpc, callbackQueue: nil, completion: completion) - } - - public func send( - _ rpc: RPC, - callbackQueue: DispatchQueue? = nil, - completion: @escaping (Result) -> Void - ) { - // Determine the queue to call completion on. Opt for the callback queue provided in the call's parameters, if - // provided. - let completionQueue = callbackQueue ?? self.callbackQueue - - let remoteNodeEndpoint = remoteNodeURL.appendingPathComponent(rpc.endpoint) - var urlRequest = URLRequest(url: remoteNodeEndpoint) - - Logger.shared.log(">>>>>> Request", level: .debug) - Logger.shared.log("Endpoint: \(remoteNodeEndpoint)", level: .debug) - - Logger.shared.log("Headers: ", level: .debug) - // Add headers from client. - for header in headers { - Logger.shared.log("\(header.field): \(header.value)", level: .debug) - urlRequest.addValue(header.value, forHTTPHeaderField: header.field) - } - - // Add headers from RPC. - for header in rpc.headers { - Logger.shared.log("\(header.field): \(header.value)", level: .debug) - urlRequest.addValue(header.value, forHTTPHeaderField: header.field) - } - - if - rpc.isPOSTRequest, - let payload = rpc.payload, - let payloadData = payload.data(using: .utf8) - { - Logger.shared.log("Payload: ", level: .debug) - Logger.shared.log(payload, level: .debug) - - urlRequest.httpMethod = "POST" - urlRequest.cachePolicy = .reloadIgnoringCacheData - urlRequest.httpBody = payloadData - } - - Logger.shared.log(">>>>>> End Request", level: .debug) - - let request = urlSession.dataTask(with: urlRequest) { [weak self] data, response, error in - guard let self = self else { - return - } - - Logger.shared.log("<<<<<< Response", level: .debug) - Logger.shared.log("Endpoint: \(remoteNodeEndpoint)", level: .debug) - if - let data = data, - let stringifiedData = String(data: data, encoding: .utf8) - { - Logger.shared.log(stringifiedData, level: .debug) - } - Logger.shared.log("<<<<<< End Response", level: .debug) - - let result = self.responseHandler.handleResponse( - response: response, - data: data, - error: error, - responseAdapterClass: rpc.responseAdapterClass - ) - completionQueue.async { - completion(result) - } - } - request.resume() - } + + /// The URL session that will be used to manage URL requests. + private let urlSession: URLSession + + /// A URL pointing to a remote node that will handle requests made by this client. + private let remoteNodeURL: URL + + /// A URL pointing to a remote node that will be used to parse the output of remote forges to ensure the accuracy of the contents + private let remoteNodeParseURL: URL + + /// Headers which will be added to every request. + private let headers: [Header] + + /// A response handler for RPCs. + private let responseHandler: RPCResponseHandler + + /// The queue that callbacks from requests will be made on. + internal let callbackQueue: DispatchQueue + + public var errorCallback: ((String?, String?, Error, String) -> Void)? = nil + + /// Initialize a new AbstractNetworkClient. + /// - Parameters: + /// - remoteNodeURL: The path to the remote node. + /// - remoteNodeParseURL: The path to the remote node used to parse the contents of forged operations. + /// - urlSession: The URLSession that will manage network requests. + /// - headers: Headers which will be added to every request. + /// - callbackQueue: A dispatch queue that callbacks will be made on. + /// - responseHandler: An object which will handle responses. + public init( + remoteNodeURL: URL, + remoteNodeParseURL: URL, + urlSession: URLSession, + headers: [Header] = [], + callbackQueue: DispatchQueue, + responseHandler: RPCResponseHandler + ) { + self.remoteNodeURL = remoteNodeURL + self.remoteNodeParseURL = remoteNodeParseURL + self.urlSession = urlSession + self.headers = headers + self.callbackQueue = callbackQueue + self.responseHandler = responseHandler + } + + public func send( + _ rpc: RPC, + completion: @escaping (Result) -> Void + ) { + send(rpc, callbackQueue: nil, completion: completion) + } + + public func send( + _ rpc: RPC, + callbackQueue: DispatchQueue? = nil, + completion: @escaping (Result) -> Void + ) { + // Determine the queue to call completion on. Opt for the callback queue provided in the call's parameters, if + // provided. + let completionQueue = callbackQueue ?? self.callbackQueue + + var remoteNodeEndpoint = remoteNodeURL + if rpc is ParseOperationRPC { + remoteNodeEndpoint = remoteNodeParseURL + } + + remoteNodeEndpoint = remoteNodeEndpoint.appendingPathComponent(rpc.endpoint) + var urlRequest = URLRequest(url: remoteNodeEndpoint) + + Logger.shared.log(">>>>>> Request", level: .debug) + Logger.shared.log("Endpoint: \(remoteNodeEndpoint)", level: .debug) + + Logger.shared.log("Headers: ", level: .debug) + // Add headers from client. + for header in headers { + Logger.shared.log("\(header.field): \(header.value)", level: .debug) + urlRequest.addValue(header.value, forHTTPHeaderField: header.field) + } + + // Add headers from RPC. + for header in rpc.headers { + Logger.shared.log("\(header.field): \(header.value)", level: .debug) + urlRequest.addValue(header.value, forHTTPHeaderField: header.field) + } + + if + rpc.isPOSTRequest, + let payload = rpc.payload, + let payloadData = payload.data(using: .utf8) + { + Logger.shared.log("Payload: ", level: .debug) + Logger.shared.log(payload, level: .debug) + + urlRequest.httpMethod = "POST" + urlRequest.cachePolicy = .reloadIgnoringCacheData + urlRequest.httpBody = payloadData + } + + Logger.shared.log(">>>>>> End Request", level: .debug) + + let request = urlSession.dataTask(with: urlRequest) { [weak self] data, response, error in + guard let self = self else { + return + } + + Logger.shared.log("<<<<<< Response", level: .debug) + Logger.shared.log("Endpoint: \(remoteNodeEndpoint)", level: .debug) + if + let data = data, + let stringifiedData = String(data: data, encoding: .utf8) + { + Logger.shared.log(stringifiedData, level: .debug) + } + Logger.shared.log("<<<<<< End Response", level: .debug) + + let result = self.responseHandler.handleResponse( + response: response, + data: data, + error: error, + responseAdapterClass: rpc.responseAdapterClass + ) + + + if case .failure(let error) = result, let errorCallback = self.errorCallback { + errorCallback(rpc.payload, String(data: data ?? Data(), encoding: .utf8), error, remoteNodeEndpoint.absoluteString) + } + + + completionQueue.async { + completion(result) + } + } + request.resume() + } } + diff --git a/TezosKit/Conseil/ConseilClient.swift b/TezosKit/Conseil/ConseilClient.swift index 2460d6a5..e218f2d0 100644 --- a/TezosKit/Conseil/ConseilClient.swift +++ b/TezosKit/Conseil/ConseilClient.swift @@ -39,6 +39,7 @@ public class ConseilClient { let networkClient = NetworkClientImpl( remoteNodeURL: nodeBaseURL, + remoteNodeParseURL: nodeBaseURL, urlSession: urlSession, headers: headers, callbackQueue: callbackQueue, diff --git a/TezosKit/Conseil/Models/ConseilQuery.swift b/TezosKit/Conseil/Models/ConseilQuery.swift index 9600610f..0018994f 100644 --- a/TezosKit/Conseil/Models/ConseilQuery.swift +++ b/TezosKit/Conseil/Models/ConseilQuery.swift @@ -3,7 +3,7 @@ import Foundation public typealias ConseilPredicate = [String: Any] -public typealias ConseilOrderBy = [String: Any] +public typealias ConseilOrderBy = [[String: Any]] public enum ConseilQuery: String { case fields @@ -45,7 +45,7 @@ public enum ConseilQuery: String { case aggregation - case orderBy = "orderby" + case orderBy = "orderBy" public enum OrderBy: String { case field case direction @@ -58,10 +58,10 @@ public enum ConseilQuery: String { field: String, direction: ConseilQuery.OrderBy.Direction = .descending ) -> ConseilOrderBy { - return [ + return [[ ConseilQuery.OrderBy.field.rawValue: field, ConseilQuery.OrderBy.direction.rawValue: direction.rawValue - ] + ]] } } diff --git a/TezosKit/Dexter/DexterExchangeClient.swift b/TezosKit/Dexter/DexterExchangeClient.swift index 522d1372..112d6c0e 100644 --- a/TezosKit/Dexter/DexterExchangeClient.swift +++ b/TezosKit/Dexter/DexterExchangeClient.swift @@ -45,14 +45,52 @@ public class DexterExchangeClient { tezosNodeClient.getBalance(address: exchangeContractAddress, completion: completion) } - /// Get the total balance of the exchange in tokens. - public func getExchangeBalanceTokens( - tokenContractAddress: Address, - completion: @escaping(Result) -> Void - ) { - let tokenClient = TokenContractClient(tokenContractAddress: tokenContractAddress, tezosNodeClient: tezosNodeClient) - tokenClient.getTokenBalance(address: exchangeContractAddress, completion: completion) - } + /// Get the total balance of the exchange in tokens. + public func getExchangeBalanceTokens( + tokenContractAddress: Address, + completion: @escaping(Result) -> Void + ) { + tezosNodeClient.getContractStorage(address: exchangeContractAddress) { result in + guard case let .success(json) = result, let args = json[JSON.Keys.args] as? [Any] else { + completion(result.map { _ in 0 }) + return + } + + + if args.count > 2 { + // Edo + if args.count > 4, + let balanceObj = args[3] as? [String: Any], + let balanceString = balanceObj[JSON.Keys.int] as? String, + let balance = Decimal(string: balanceString) { + completion(.success(balance)) + return + + } else { + completion(result.map { _ in 0 }) + return + } + + } else { + // Delphi + guard let right0 = args[1] as? [String: Any], + let args1 = right0[JSON.Keys.args] as? [Any], + let right1 = args1[1] as? [String: Any], + let args2 = right1[JSON.Keys.args] as? [Any], + let right2 = args2[1] as? [String: Any], + let args3 = right2[JSON.Keys.args] as? [Any], + let left0 = args3[0] as? [String: Any], + let balanceString = left0[JSON.Keys.int] as? String, + let balance = Decimal(string: balanceString) else { + completion(result.map { _ in 0 }) + return + } + + completion(.success(balance)) + return + } + } + } /// Get the total exchange liquidity. public func getExchangeLiquidity(completion: @escaping (Result) -> Void) { @@ -202,7 +240,7 @@ public class DexterExchangeClient { switch result { case .success(let op): - tezosNodeClient.forgeSignPreapplyAndInject(op, source: source, signatureProvider: signatureProvider, completion: completion) + tezosNodeClient.forgeParseSignPreapplyAndInject(op, source: source, signatureProvider: signatureProvider, completion: completion) case .failure(let error): completion(Result.failure(error)) } @@ -219,7 +257,7 @@ public class DexterExchangeClient { /// - completion: A completion block which will be called with the result hash, if successful. public func tradeTezForTokenOperation( source: Address, - destination: Address, + destination: Address, amount: Tez, operationFeePolicy: OperationFeePolicy, signatureProvider: SignatureProvider, @@ -227,12 +265,12 @@ public class DexterExchangeClient { deadline: Date ) -> Result { let parameter = PairMichelsonParameter( - left: PairMichelsonParameter( - left: StringMichelsonParameter(string: destination), - right: IntMichelsonParameter(decimal: minTokensToPurchase) - ), - right: Timestamp(date: deadline) - ) + left: StringMichelsonParameter(string: destination), + right: PairMichelsonParameter( + left: IntMichelsonParameter(decimal: minTokensToPurchase), + right: Timestamp(date: deadline) + ) + ) return tezosNodeClient.operationFactory.smartContractInvocationOperation( amount: amount, @@ -270,7 +308,7 @@ public class DexterExchangeClient { switch result { case .success(let op): - tezosNodeClient.forgeSignPreapplyAndInject(op, source: source, signatureProvider: signatureProvider, completion: completion) + tezosNodeClient.forgeParseSignPreapplyAndInject(op, source: source, signatureProvider: signatureProvider, completion: completion) case .failure(let error): completion(Result.failure(error)) } @@ -300,19 +338,19 @@ public class DexterExchangeClient { return .failure(.unknown(description: nil)) } - let parameter = PairMichelsonParameter( - left: PairMichelsonParameter( - left: PairMichelsonParameter( - left: StringMichelsonParameter(string: owner), - right: StringMichelsonParameter(string: destination) - ), - right: PairMichelsonParameter( - left: IntMichelsonParameter(decimal: tokensToSell), - right: IntMichelsonParameter(decimal: minMutezToBuy) - ) - ), - right: Timestamp(date: deadline) - ) + let addressPair = PairMichelsonParameter( + left: StringMichelsonParameter(string: owner), + right: StringMichelsonParameter(string: destination) + ) + let amountPair = PairMichelsonParameter( + left: IntMichelsonParameter(decimal: tokensToSell), + right: PairMichelsonParameter( + left: IntMichelsonParameter(decimal: minMutezToBuy), + right: Timestamp(date: deadline) + ) + ) + + let parameter = PairMichelsonParameter(left: addressPair, right: amountPair) return tezosNodeClient.operationFactory.smartContractInvocationOperation( amount: Tez.zeroBalance, diff --git a/TezosKit/Dexter/TokenContractClient.swift b/TezosKit/Dexter/TokenContractClient.swift index 8c1d86d8..895dc2b3 100644 --- a/TezosKit/Dexter/TokenContractClient.swift +++ b/TezosKit/Dexter/TokenContractClient.swift @@ -60,7 +60,7 @@ public class TokenContractClient { switch result { case .success(let op): - tezosNodeClient.forgeSignPreapplyAndInject(op, source: source, signatureProvider: signatureProvider, completion: completion) + tezosNodeClient.forgeParseSignPreapplyAndInject(op, source: source, signatureProvider: signatureProvider, completion: completion) case .failure(let error): completion(Result.failure(error)) } @@ -83,12 +83,12 @@ public class TokenContractClient { ) -> Result { let amount = Tez.zeroBalance let parameter = PairMichelsonParameter( - left: PairMichelsonParameter( - left: StringMichelsonParameter(string: source), - right: StringMichelsonParameter(string: destination) - ), - right: IntMichelsonParameter(decimal: numTokens) - ) + left: StringMichelsonParameter(string: source), + right: PairMichelsonParameter( + left: StringMichelsonParameter(string: destination), + right: IntMichelsonParameter(decimal: numTokens) + ) + ) return tezosNodeClient.operationFactory.smartContractInvocationOperation( amount: amount, @@ -121,7 +121,7 @@ public class TokenContractClient { switch result { case .success(let op): - tezosNodeClient.forgeSignPreapplyAndInject(op, source: source, signatureProvider: signatureProvider, completion: completion) + tezosNodeClient.forgeParseSignPreapplyAndInject(op, source: source, signatureProvider: signatureProvider, completion: completion) case .failure(let error): completion(Result.failure(error)) } @@ -175,8 +175,9 @@ public class TokenContractClient { operationFeePolicy: OperationFeePolicy, signatureProvider: SignatureProvider ) -> Result<[TezosKit.Operation], TezosKitError> { + let approveOperation = approveAllowanceOperation(source: source, spender: spender, allowance: numTokens, operationFeePolicy: operationFeePolicy, signatureProvider: signatureProvider) - let transferOperation = transferTokensOperation(from: source, to: destination, numTokens: numTokens, operationFeePolicy: operationFeePolicy, signatureProvider: signatureProvider) + let transferOperation = transferTokensOperation(from: source, to: destination, numTokens: numTokens, operationFeePolicy: operationFeePolicy, signatureProvider: signatureProvider) if case .success(let approveOp) = approveOperation, case .success(let transferOp) = transferOperation { return Result.success([approveOp, transferOp]) @@ -201,7 +202,7 @@ public class TokenContractClient { guard case let .success(json) = result, let args = json[JSON.Keys.args] as? [ Any ], - let second = args[1] as? [String: Any], + let second = args[0] as? [String: Any], let balanceString = second[JSON.Keys.int] as? String, let balance = Decimal(string: balanceString) else { diff --git a/TezosKit/TezosNode/Models/Operation/OperationResponse.swift b/TezosKit/TezosNode/Models/Operation/OperationResponse.swift index cfb9cf19..63cf3f86 100644 --- a/TezosKit/TezosNode/Models/Operation/OperationResponse.swift +++ b/TezosKit/TezosNode/Models/Operation/OperationResponse.swift @@ -97,5 +97,6 @@ public struct OperationResponseInternalResultError: Codable, Equatable { } public struct OperationResponseInternalResultErrorWith: Codable, Equatable { - public let string: String + public let string: String? + public let args: [[String: String]]? } diff --git a/TezosKit/TezosNode/Models/OperationFees.swift b/TezosKit/TezosNode/Models/OperationFees.swift index 126de131..bfde1643 100644 --- a/TezosKit/TezosNode/Models/OperationFees.swift +++ b/TezosKit/TezosNode/Models/OperationFees.swift @@ -5,14 +5,16 @@ import Foundation /// An object encapsulating the payment for an operation on the blockchain. public struct OperationFees { public let fee: Tez + public let burnFee: Tez public let gasLimit: Int public let storageLimit: Int /// A zero-ed fees object. internal static let zeroFees = OperationFees(fee: .zeroBalance, gasLimit: 0, storageLimit: 0) - public init(fee: Tez, gasLimit: Int, storageLimit: Int) { + public init(fee: Tez, burnFee: Tez = .zeroBalance, gasLimit: Int, storageLimit: Int) { self.fee = fee + self.burnFee = burnFee self.gasLimit = gasLimit self.storageLimit = storageLimit } diff --git a/TezosKit/TezosNode/Models/SimulationResult.swift b/TezosKit/TezosNode/Models/SimulationResult.swift index c6413c13..1d717547 100644 --- a/TezosKit/TezosNode/Models/SimulationResult.swift +++ b/TezosKit/TezosNode/Models/SimulationResult.swift @@ -6,4 +6,5 @@ import Foundation public struct SimulationResult { public let consumedGas: Int public let consumedStorage: Int + public let burnFee: Tez } diff --git a/TezosKit/TezosNode/RPC/ParseOperationRPC.swift b/TezosKit/TezosNode/RPC/ParseOperationRPC.swift new file mode 100644 index 00000000..c34c35c1 --- /dev/null +++ b/TezosKit/TezosNode/RPC/ParseOperationRPC.swift @@ -0,0 +1,23 @@ +// +// ParseOperationRPC.swift +// TezosKit +// +// Created by Simon Mcloughlin on 30/06/2020. +// + +import Foundation + +/// An RPC which will parse an operation. +public class ParseOperationRPC: RPC<[[String: Any]]> { + + /// - Parameters: + /// - operationPayload: A payload to forge. + /// - operationMetadata: Metadata about the operation. + public init(hashToParse: String, operationMetadata: OperationMetadata) { + let endpoint = "/chains/main/blocks/" + operationMetadata.branch + "/helpers/parse/operations" + let jsonDictionary = ["operations": [ ["data": hashToParse, "branch": operationMetadata.branch] ]] + let payload = JSONUtils.jsonString(for: jsonDictionary) + + super.init(endpoint: endpoint, headers: [Header.contentTypeApplicationJSON], responseAdapterClass: JSONArrayResponseAdapter.self, payload: payload) + } +} diff --git a/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift b/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift index ff336558..d702fded 100644 --- a/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift +++ b/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift @@ -13,6 +13,8 @@ private enum JSON { public static let result = "result" public static let status = "status" public static let storageSize = "storage_size" + public static let allocatedDestinationContract = "allocated_destination_contract" + public static let paidStorageSizeDiff = "paid_storage_size_diff" } public enum Values { @@ -37,6 +39,8 @@ public class SimulationResultResponseAdapter: AbstractResponseAdapter ) -> Void)) { + + let rpc = ParseOperationRPC(hashToParse: hashToParse, operationMetadata: operationMetadata) + networkClient.send(rpc) { [weak self] (result) in + + switch result { + case .success(let jsonArray): + if let comparisonResult = self?.compare(jsonArray: jsonArray, toOperationPayload: operationPayload), comparisonResult { + completion(Result.success(true)) + + } else { + completion(Result.failure(TezosKitError.transactionFormationFailure(underlyingError: TezosKitError.unexpectedResponse(description: "Unable to parse response")))) + } + + case .failure(let error): + completion(Result.failure(error)) + } + } + } + + private func compare(jsonArray: [[String: Any]], toOperationPayload operationPayload: OperationPayload) -> Bool { + guard let dict = jsonArray.first as? [String: Any] else { + return false + } + + var sanitizedDict = dict + sanitizedDict.removeValue(forKey: "signature") + + if (sanitizedDict as NSDictionary).isEqual(operationPayload.dictionaryRepresentation) { + return true + } + + return false + } +} diff --git a/TezosKit/TezosNode/Services/RPCResponseHandler.swift b/TezosKit/TezosNode/Services/RPCResponseHandler.swift index fc18571a..8302dc02 100644 --- a/TezosKit/TezosNode/Services/RPCResponseHandler.swift +++ b/TezosKit/TezosNode/Services/RPCResponseHandler.swift @@ -40,10 +40,17 @@ public class RPCResponseHandler { // Check for a backtracked operation response // TODO(keefertaylor): Add a test for this logic. do { - let operationResult = try JSONDecoder().decode(OperationResponse.self, from: data) - if operationResult.isFailed() { - return .failure(.operationError(operationResult.errors())) - } + if "\(responseAdapterClass)" == "JSONArrayResponseAdapter" { + let operationResult = try JSONDecoder().decode([OperationResponse].self, from: data) + if let first = operationResult.first, first.isFailed() { + return .failure(.operationError(first.errors())) + } + } else { + let operationResult = try JSONDecoder().decode(OperationResponse.self, from: data) + if operationResult.isFailed() { + return .failure(.operationError(operationResult.errors())) + } + } } catch { // Intentionally ignore parsing failures. Parsing only suceeds if there is an error. } diff --git a/TezosKit/TezosNode/TezosNodeClient.swift b/TezosKit/TezosNode/TezosNodeClient.swift index bb344b2b..66fdd1c2 100644 --- a/TezosKit/TezosNode/TezosNodeClient.swift +++ b/TezosKit/TezosNode/TezosNodeClient.swift @@ -68,747 +68,785 @@ import Foundation /// operation correctly as long as the |requiresReveal| bit on the custom Operation object is set /// correctly. public class TezosNodeClient { - /// The default node URL to use. - public static let defaultNodeURL = URL(string: "https://rpc.tezrpc.me")! - - /// A factory which produces operations. - public let operationFactory: OperationFactory - - /// A service which forges operations. - internal let forgingService: ForgingService - - /// The network client. - internal let networkClient: NetworkClient - - /// The operation metadata provider. - internal let operationMetadataProvider: OperationMetadataProvider - - /// A service that preapplies operations. - internal let preapplicationService: PreapplicationService - - /// A service which simulates operations. - internal let simulationService: SimulationService - - /// An injection service which injects operations. - internal let injectionService: InjectionService - - /// A callback queue that all completions will be called on. - internal let callbackQueue: DispatchQueue - - /// Initialize a new TezosNodeClient. - /// - /// - Parameters: - /// - remoteNodeURL: The path to the remote node, defaults to the default URL - /// - tezosProtocol: The protocol version to use, defaults to Carthage. - /// - forgingPolicy: The policy to apply when forging operations. Default is remote. - /// - urlSession: The URLSession that will manage network requests, defaults to the shared session. - /// - callbackQueue: A dispatch queue that callbacks will be made on, defaults to the main queue. - public convenience init( - remoteNodeURL: URL = defaultNodeURL, - tezosProtocol: TezosProtocol = .carthage, - forgingPolicy: ForgingPolicy = .remote, - urlSession: URLSession = URLSession.shared, - callbackQueue: DispatchQueue = DispatchQueue.main - ) { - let networkClient = NetworkClientImpl( - remoteNodeURL: remoteNodeURL, - urlSession: urlSession, - callbackQueue: callbackQueue, - responseHandler: RPCResponseHandler() - ) - - self.init( - networkClient: networkClient, - tezosProtocol: tezosProtocol, - forgingPolicy: forgingPolicy, - callbackQueue: callbackQueue - ) - } - - /// An internal initializer which allows injection of a network client for testability. - internal init( - networkClient: NetworkClient, - tezosProtocol: TezosProtocol = .carthage, - forgingPolicy: ForgingPolicy = .remote, - callbackQueue: DispatchQueue = DispatchQueue.main - ) { - self.networkClient = networkClient - self.callbackQueue = callbackQueue - - forgingService = ForgingService(forgingPolicy: forgingPolicy, networkClient: networkClient) - operationMetadataProvider = OperationMetadataProvider(networkClient: networkClient) - - simulationService = SimulationService( - networkClient: networkClient, - operationMetadataProvider: operationMetadataProvider - ) - - let feeEstimator = FeeEstimator( - forgingService: forgingService, - operationMetadataProvider: operationMetadataProvider, - simulationService: simulationService - ) - - operationFactory = OperationFactory(tezosProtocol: tezosProtocol, feeEstimator: feeEstimator) - - injectionService = InjectionService(networkClient: networkClient) - preapplicationService = PreapplicationService(networkClient: networkClient) - - JailbreakUtils.crashIfJailbroken() - } - - // MARK: - Queries - - /// Retrieve data about the chain head. - public func getHead(completion: @escaping (Result<[String: Any], TezosKitError>) -> Void) { - let rpc = GetChainHeadRPC() - self.run(rpc, completion: completion) - } - - /// Retrieve the balance of a given wallet. - public func getBalance(wallet: Wallet, completion: @escaping (Result) -> Void) { - getBalance(address: wallet.address, completion: completion) - } - - /// Retrieve the balance of a given address. - public func getBalance(address: Address, completion: @escaping (Result) -> Void) { - let rpc = GetAddressBalanceRPC(address: address) - self.run(rpc, completion: completion) - } - - /// Retrieve the delegate of a given wallet. - public func getDelegate(wallet: Wallet, completion: @escaping (Result) -> Void) { - getDelegate(address: wallet.address, completion: completion) - } - - /// Retrieve the delegate of a given address. - public func getDelegate(address: Address, completion: @escaping (Result) -> Void) { - let rpc = GetDelegateRPC(address: address) - self.run(rpc, completion: completion) - } - - /// Retrieve the hash of the block at the head of the chain. - public func getHeadHash(completion: @escaping (Result) -> Void) { - let rpc = GetChainHeadHashRPC() - self.run(rpc, completion: completion) - } - - /// Retrieve the address counter for the given address. - public func getAddressCounter(address: Address, completion: @escaping (Result) -> Void) { - let rpc = GetAddressCounterRPC(address: address) - self.run(rpc, completion: completion) - } - - /// Retrieve the address manager key for the given address. - public func getAddressManagerKey( - address: Address, - completion: @escaping (Result) -> Void - ) { - let rpc = GetAddressManagerKeyRPC(address: address) - self.run(rpc, completion: completion) - } - - /// Retrieve ballots cast so far during a voting period. - public func getBallotsList(completion: @escaping (Result<[[String: Any]], TezosKitError>) -> Void) { - let rpc = GetBallotsListRPC() - self.run(rpc, completion: completion) - } - - /// Retrieve the expected quorum. - public func getExpectedQuorum(completion: @escaping (Result) -> Void) { - let rpc = GetExpectedQuorumRPC() - self.run(rpc, completion: completion) - } - - /// Retrieve the current period kind for voting. - public func getCurrentPeriodKind(completion: @escaping (Result) -> Void) { - let rpc = GetCurrentPeriodKindRPC() - self.run(rpc, completion: completion) - } - - /// Retrieve the sum of ballots cast so far during a voting period. - public func getBallots(completion: @escaping (Result<[String: Any], TezosKitError>) -> Void) { - let rpc = GetBallotsRPC() - self.run(rpc, completion: completion) } - - /// Retrieve a list of proposals with number of supporters. - public func getProposalsList(completion: @escaping (Result<[[String: Any]], TezosKitError>) -> Void) { - let rpc = GetProposalsListRPC() - self.run(rpc, completion: completion) - } - - /// Retrieve the current proposal under evaluation. - public func getProposalUnderEvaluation(completion: @escaping (Result) -> Void) { - let rpc = GetProposalUnderEvaluationRPC() - self.run(rpc, completion: completion) - } - - /// Retrieve a list of delegates with their voting weight, in number of rolls. - public func getVotingDelegateRights(completion: @escaping (Result<[[String: Any]], TezosKitError>) -> Void) { - let rpc = GetVotingDelegateRightsRPC() - self.run(rpc, completion: completion) - - } - - /// Run an arbitrary RPC. - /// - /// - Parameters: - /// - rpc: The RPC to run. - /// - completion : A completion block which handles the results of the RPC - public func run(_ rpc: RPC, completion: @escaping (Result) -> Void) { - networkClient.send(rpc, completion: completion) - } - - /// Inspect the value of a big map in a smart contract. - /// - /// - Parameters: - /// - address: The address of a smart contract with a big map. - /// - key: The key in the big map to look up. - /// - type: The michelson type of the key. - /// - completion: A completion block to call. - public func getBigMapValue( - address: Address, - key: MichelsonParameter, - type: MichelsonComparable, - completion: @escaping (Result<[String: Any], TezosKitError>) -> Void - ) { - let rpc = GetBigMapValueRPC(address: address, key: key, type: type) - self.run(rpc, completion: completion) - } - - /// Retrieve the storage of a smart contract. - /// - /// - Parameters: - /// - address: The address of the smart contract to inspect. - /// - completion: A completion block which will be called with the storage. - public func getContractStorage( - address: Address, - completion: @escaping (Result<[String: Any], TezosKitError>) -> Void - ) { - let rpc = GetContractStorageRPC(address: address) - self.run(rpc, completion: completion) - } - - /// Retrieve a value from a big map. - /// - /// - Parameters: - /// - bigMapID: The ID of the big map. - /// - key: The key in the big map to look up. - /// - type: The michelson type of the key. - /// - completion: A completion block to call. - public func getBigMapValue( - bigMapID: BigInt, - key: MichelsonParameter, - type: MichelsonComparable, - completion: @escaping (Result<[String: Any], TezosKitError>) -> Void - ) { - let payload = PackDataPayload(michelsonParameter: key, michelsonComparable: type) - let packDataRPC = PackDataRPC(payload: payload) - - self.run(packDataRPC) { [weak self] result in - guard let self = self else { - return - } - - guard case let .success(expression) = result else { - completion( - result.map { _ in [:] } - ) - return - } - - let bigMapValueRPC = GetBigMapValueByIDRPC(bigMapID: bigMapID, expression: expression) - self.run(bigMapValueRPC, completion: completion) - } - } - - // MARK: - Operations - - /// Transact Tezos between accounts. - /// - /// - Parameters: - /// - amount: The amount of Tez to send. - /// - recipientAddress: The address which will receive the Tez. - /// - source: The address sending the balance. - /// - signatureProvider: The object which will sign the operation. - /// - operationFees: OperationFees for the transaction. If nil, default fees are used. - /// - completion: A completion block called with an optional transaction hash and error. - @available(*, deprecated, message: "Please use an OperationFeePolicy API instead.") - public func send( - amount: Tez, - to recipientAddress: String, - from source: Address, - signatureProvider: SignatureProvider, - operationFees: OperationFees? = nil, - completion: @escaping (Result) -> Void - ) { - var policy = OperationFeePolicy.default - if let operationFees = operationFees { - policy = .custom(operationFees) - } - - send( - amount: amount, - to: recipientAddress, - from: source, - signatureProvider: signatureProvider, - operationFeePolicy: policy, - completion: completion - ) - } - - /// Transact Tezos between accounts. - /// - /// - Parameters: - /// - amount: The amount of Tez to send. - /// - recipientAddress: The address which will receive the Tez. - /// - source: The address sending the balance. - /// - signatureProvider: The object which will sign the operation. - /// - operationFeePolicy: A policy to apply when determining operation fees. Default is default fees. - /// - completion: A completion block called with an optional transaction hash and error. - public func send( - amount: Tez, - to recipientAddress: String, - from source: Address, - signatureProvider: SignatureProvider, - operationFeePolicy: OperationFeePolicy = .default, - completion: @escaping (Result) -> Void - ) { - let result = operationFactory.transactionOperation( - amount: amount, - source: source, - destination: recipientAddress, - operationFeePolicy: operationFeePolicy, - signatureProvider: signatureProvider - ) - - switch result { - case .success(let transactionOperation): - forgeSignPreapplyAndInject( - transactionOperation, - source: source, - signatureProvider: signatureProvider, - completion: completion - ) - case .failure(let error): - callbackQueue.async { - completion(.failure(.transactionFormationFailure(underlyingError: error))) - } - } - } - - /// Call a smart contract. - /// - /// - Parameters: - /// - contract: The smart contract to invoke. - /// - amount: The amount of Tez to transfer with the invocation. Default is 0. - /// - parameter: An optional parameter to send to the smart contract. Default is none. - /// - source: The address invoking the contract. - /// - signatureProvider: The object which will sign the operation. - /// - operationFeePolicy: A policy to apply when determining operation fees. - /// - completion: A completion block called with an optional transaction hash and error. - @available(*, deprecated, message: "Please use an OperationFeePolicy API instead.") - public func call( - contract: Address, - amount: Tez = Tez.zeroBalance, - parameter: MichelsonParameter? = nil, - source: Address, - signatureProvider: SignatureProvider, - operationFees: OperationFees? = nil, - completion: @escaping (Result) -> Void - ) { - var policy = OperationFeePolicy.default - if let operationFees = operationFees { - policy = .custom(operationFees) - } - - call( - contract: contract, - amount: amount, - parameter: parameter, - source: source, - signatureProvider: signatureProvider, - operationFeePolicy: policy, - completion: completion - ) - } - - /// Call a smart contract. - /// - /// - Parameters: - /// - contract: The smart contract to invoke. - /// - amount: The amount of Tez to transfer with the invocation. Default is 0. - /// - entrypoint: An optional entrypoint to use for the transaction. Default is nil. - /// - parameter: An optional parameter to send to the smart contract. Default is none. - /// - source: The address invoking the contract. - /// - signatureProvider: The object which will sign the operation. - /// - operationFeePolicy: A policy to apply when determining operation fees. Default is default fees. - /// - completion: A completion block called with an optional transaction hash and error. - public func call( - contract: Address, - amount: Tez = Tez.zeroBalance, - entrypoint: String? = nil, - parameter: MichelsonParameter? = nil, - source: Address, - signatureProvider: SignatureProvider, - operationFeePolicy: OperationFeePolicy = .default, - completion: @escaping (Result) -> Void - ) { - let result = operationFactory.smartContractInvocationOperation( - amount: amount, - entrypoint: entrypoint, - parameter: parameter, - source: source, - destination: contract, - operationFeePolicy: operationFeePolicy, - signatureProvider: signatureProvider - ) - - switch result { - case .success(let smartContractInvocationOperation): - forgeSignPreapplyAndInject( - smartContractInvocationOperation, - source: source, - signatureProvider: signatureProvider, - completion: completion - ) - case .failure(let error): - callbackQueue.async { - completion(.failure(.transactionFormationFailure(underlyingError: error))) - } - } - } - - /// Delegate the balance of an account. - /// - /// - Parameters: - /// - source: The address which will delegate. - /// - delegate: The address which will receive the delegation. - /// - signatureProvider: The object which will sign the operation. - /// - operationFees: OperationFees for the transaction. If nil, default fees are used. - /// - completion: A completion block called with an optional transaction hash and error. - @available(*, deprecated, message: "Please use an OperationFeePolicy API instead.") - public func delegate( - from source: Address, - to delegate: Address, - signatureProvider: SignatureProvider, - operationFees: OperationFees? = nil, - completion: @escaping (Result) -> Void - ) { - var policy = OperationFeePolicy.default - if let operationFees = operationFees { - policy = .custom(operationFees) - } - - self.delegate( - from: source, - to: delegate, - signatureProvider: signatureProvider, - operationFeePolicy: policy, - completion: completion - ) - } - - /// Delegate the balance of an account. - /// - /// - Parameters: - /// - source: The address which will delegate. - /// - delegate: The address which will receive the delegation. - /// - signatureProvider: The object which will sign the operation. - /// - operationFeePolicy: A policy to apply when determining operation fees. Default is default fees. - /// - completion: A completion block called with an optional transaction hash and error. - public func delegate( - from source: Address, - to delegate: Address, - signatureProvider: SignatureProvider, - operationFeePolicy: OperationFeePolicy = .default, - completion: @escaping (Result) -> Void - ) { - let result = operationFactory.delegateOperation( - source: source, - to: delegate, - operationFeePolicy: operationFeePolicy, - signatureProvider: signatureProvider - ) - - switch result { - case .success(let delegationOperation): - forgeSignPreapplyAndInject( - delegationOperation, - source: source, - signatureProvider: signatureProvider, - completion: completion - ) - case .failure(let error): - callbackQueue.async { - completion(.failure(.transactionFormationFailure(underlyingError: error))) - } - } - } - - /// Clear the delegate of an account. - /// - /// - Parameters: - /// - source: The address which is removing the delegate. - /// - signatureProvider: The object which will sign the operation. - /// - operationFees: OperationFees for the transaction. If nil, default fees are used. - /// - completion: A completion block which will be called with a string representing the transaction ID hash if the - /// operation was successful. - @available(*, deprecated, message: "Please use an OperationFeePolicy API instead.") - public func undelegate( - from source: Address, - signatureProvider: SignatureProvider, - operationFees: OperationFees? = nil, - completion: @escaping (Result) -> Void - ) { - var policy = OperationFeePolicy.default - if let operationFees = operationFees { - policy = .custom(operationFees) - } - - undelegate(from: source, signatureProvider: signatureProvider, operationFeePolicy: policy, completion: completion) - } - - /// Clear the delegate of an account. - /// - /// - Parameters: - /// - source: The address which is removing the delegate. - /// - signatureProvider: The object which will sign the operation. - /// - operationFeePolicy: A policy to apply when determining operation fees. Default is default fees. - /// - completion: A completion block which will be called with a string representing the transaction ID hash if the - /// operation was successful. - public func undelegate( - from source: Address, - signatureProvider: SignatureProvider, - operationFeePolicy: OperationFeePolicy = .default, - completion: @escaping (Result) -> Void - ) { - let result = operationFactory.undelegateOperation( - source: source, - operationFeePolicy: operationFeePolicy, - signatureProvider: signatureProvider - ) - - switch result { - case .success(let undelegateOperation): - forgeSignPreapplyAndInject( - undelegateOperation, - source: source, - signatureProvider: signatureProvider, - completion: completion - ) - case .failure(let error): - callbackQueue.async { - completion(.failure(.transactionFormationFailure(underlyingError: error))) - } - return - } - } - - /// Register an address as a delegate. - /// - /// - Parameters: - /// - delegate: The address registering as a delegate. - /// - signatureProvider: The object which will sign the operation. - /// - operationFees: OperationFees for the transaction. If nil, default fees are used. - /// - completion: A completion block called with an optional transaction hash and error. - @available(*, deprecated, message: "Please use an OperationFeePolicy API instead.") - public func registerDelegate( - delegate: Address, - signatureProvider: SignatureProvider, - operationFees: OperationFees? = nil, - completion: @escaping (Result) -> Void - ) { - var policy = OperationFeePolicy.default - if let operationFees = operationFees { - policy = .custom(operationFees) - } - - registerDelegate( - delegate: delegate, - signatureProvider: signatureProvider, - operationFeePolicy: policy, - completion: completion - ) - } - - /// Register an address as a delegate. - /// - /// - Parameters: - /// - delegate: The address registering as a delegate. - /// - signatureProvider: The object which will sign the operation. - /// - operationFeePolicy: A policy to apply when determining operation fees. Default is default fees. - /// - completion: A completion block called with an optional transaction hash and error. - public func registerDelegate( - delegate: Address, - signatureProvider: SignatureProvider, - operationFeePolicy: OperationFeePolicy = .default, - completion: @escaping (Result) -> Void - ) { - let result = operationFactory.registerDelegateOperation( - source: delegate, - operationFeePolicy: operationFeePolicy, - signatureProvider: signatureProvider - ) - - switch result { - case .success(let registerDelegateOperation): - forgeSignPreapplyAndInject( - registerDelegateOperation, - source: delegate, - signatureProvider: signatureProvider, - completion: completion - ) - case .failure(let error): - callbackQueue.async { - completion(.failure(.transactionFormationFailure(underlyingError: error))) - } - } - } - - /// Retrieve metadata and runs an operation. - /// - /// - Parameters: - /// - operation: The operation to run. - /// - wallet: The wallet requesting the run. - /// - completion: A completion block to call. - public func runOperation( - _ operation: Operation, - from wallet: Wallet, - completion: @escaping (Result) -> Void - ) { - simulationService.simulate(operation, from: wallet.address, signatureProvider: wallet, completion: completion) - } - - // MARK: - Private Methods - - /// Forge, sign, preapply and then inject a single operation. - /// - /// - Parameters: - /// - operation: The operation which will be used to forge the operation. - /// - source: The address performing the operation. - /// - signatureProvider: The object which will sign the operation. - /// - completion: A completion block that will be called with the results of the operation. - public func forgeSignPreapplyAndInject( - _ operation: Operation, - source: Address, - signatureProvider: SignatureProvider, - completion: @escaping (Result) -> Void - ) { - forgeSignPreapplyAndInject( - [operation], - source: source, - signatureProvider: signatureProvider, - completion: completion - ) - } - - /// Forge, sign, preapply and then inject a set of operations. - /// - /// Operations are processed in the order they are placed in the operation array. - /// - /// - Parameters: - /// - operations: The operations which will be forged. - /// - source: The address performing the operation. - /// - signatureProvider: The object which will sign the operation. - /// - completion: A completion block that will be called with the results of the operation. - public func forgeSignPreapplyAndInject( - _ operations: [Operation], - source: Address, - signatureProvider: SignatureProvider, - completion: @escaping (Result) -> Void - ) { - operationMetadataProvider.metadata(for: source) { [weak self] result in - guard let self = self else { - return - } - - guard - case let .success(operationMetadata) = result, - let operationPayload = OperationPayloadFactory.operationPayload( - from: operations, - source: source, - signatureProvider: signatureProvider, - operationMetadata: operationMetadata - ) - else { - completion( - result.map { _ in "" } - ) - return - } - - self.forgingService.forge( - operationPayload: operationPayload, - operationMetadata: operationMetadata - ) { [weak self] result in - guard let self = self else { - return - } - guard case let .success(forgedBytes) = result else { - completion( - result.map { _ in "" } - ) - return - } - self.signPreapplyAndInjectOperation( - operationPayload: operationPayload, - operationMetadata: operationMetadata, - forgeResult: forgedBytes, - source: source, - signatureProvider: signatureProvider, - completion: completion - ) - } - } - } - - /// Sign the result of a forged operation, preapply and inject it if successful. - /// - /// - Parameters: - /// - operationPayload: The operation payload which was used to forge the operation. - /// - operationMetadata: Metadata related to the operation. - /// - forgeResult: The result of forging the operation payload. - /// - source: The address performing the operation. - /// - signatureProvider: The object which will sign the operation. - /// - completion: A completion block that will be called with the results of the operation. - private func signPreapplyAndInjectOperation( - operationPayload: OperationPayload, - operationMetadata: OperationMetadata, - forgeResult: String, - source: Address, - signatureProvider: SignatureProvider, - completion: @escaping (Result) -> Void - ) { - guard - let signature = SigningService.sign(forgeResult, with: signatureProvider), - let signatureHex = CryptoUtils.binToHex(signature), - let signedBytesForInjection = JSONUtils.jsonString(for: forgeResult + signatureHex), - let signedOperationPayload = SignedOperationPayload( - operationPayload: operationPayload, - signature: signature, - signingCurve: signatureProvider.publicKey.signingCurve - ) - else { - completion(.failure(.signingError)) - return - } - - let signedProtocolOperationPayload = SignedProtocolOperationPayload( - signedOperationPayload: signedOperationPayload, - operationMetadata: operationMetadata - ) - - preapplicationService.preapply( - signedProtocolOperationPayload: signedProtocolOperationPayload, - signedBytesForInjection: signedBytesForInjection, - operationMetadata: operationMetadata - ) { result in - if let error = result { - completion(.failure(error)) - return - } - self.injectionService.inject(payload: signedBytesForInjection, completion: completion) - } - } + /// The default node URL to use. + public static let defaultNodeURL = URL(string: "https://rpc.tezrpc.me")! + + /// A factory which produces operations. + public let operationFactory: OperationFactory + + /// A service which forges operations. + public let forgingService: ForgingService + + /// A service which parses operations. + public let parsingService: ParsingService + + /// The network client. + public var networkClient: NetworkClient + + /// The operation metadata provider. + public let operationMetadataProvider: OperationMetadataProvider + + /// A service that preapplies operations. + public let preapplicationService: PreapplicationService + + /// A service which simulates operations. + public let simulationService: SimulationService + + /// An injection service which injects operations. + public let injectionService: InjectionService + + /// A callback queue that all completions will be called on. + internal let callbackQueue: DispatchQueue + + /// Initialize a new TezosNodeClient. + /// + /// - Parameters: + /// - remoteNodeURL: The path to the remote node, defaults to the default URL + /// - remoteNodeParseURL: The path to the remote node used to parse the contents of forged operations. + /// - tezosProtocol: The protocol version to use, defaults to Carthage. + /// - forgingPolicy: The policy to apply when forging operations. Default is remote. + /// - urlSession: The URLSession that will manage network requests, defaults to the shared session. + /// - callbackQueue: A dispatch queue that callbacks will be made on, defaults to the main queue. + public convenience init( + remoteNodeURL: URL = defaultNodeURL, + remoteNodeParseURL: URL? = nil, + tezosProtocol: TezosProtocol = .carthage, + forgingPolicy: ForgingPolicy = .remote, + urlSession: URLSession = URLSession.shared, + callbackQueue: DispatchQueue = DispatchQueue.main + ) { + let networkClient = NetworkClientImpl( + remoteNodeURL: remoteNodeURL, + remoteNodeParseURL: remoteNodeParseURL ?? remoteNodeURL, + urlSession: urlSession, + callbackQueue: callbackQueue, + responseHandler: RPCResponseHandler() + ) + + self.init( + networkClient: networkClient, + tezosProtocol: tezosProtocol, + forgingPolicy: forgingPolicy, + callbackQueue: callbackQueue + ) + } + + /// An internal initializer which allows injection of a network client for testability. + internal init( + networkClient: NetworkClient, + tezosProtocol: TezosProtocol = .carthage, + forgingPolicy: ForgingPolicy = .remote, + callbackQueue: DispatchQueue = DispatchQueue.main + ) { + self.networkClient = networkClient + self.callbackQueue = callbackQueue + + forgingService = ForgingService(forgingPolicy: forgingPolicy, networkClient: networkClient) + parsingService = ParsingService(networkClient: networkClient) + operationMetadataProvider = OperationMetadataProvider(networkClient: networkClient) + + simulationService = SimulationService( + networkClient: networkClient, + operationMetadataProvider: operationMetadataProvider + ) + + let feeEstimator = FeeEstimator( + forgingService: forgingService, + operationMetadataProvider: operationMetadataProvider, + simulationService: simulationService + ) + + operationFactory = OperationFactory(tezosProtocol: tezosProtocol, feeEstimator: feeEstimator) + + injectionService = InjectionService(networkClient: networkClient) + preapplicationService = PreapplicationService(networkClient: networkClient) + + //JailbreakUtils.crashIfJailbroken() + } + + // MARK: - Queries + + /// Retrieve data about the chain head. + public func getHead(completion: @escaping (Result<[String: Any], TezosKitError>) -> Void) { + let rpc = GetChainHeadRPC() + self.run(rpc, completion: completion) + } + + /// Retrieve the balance of a given wallet. + public func getBalance(wallet: Wallet, completion: @escaping (Result) -> Void) { + getBalance(address: wallet.address, completion: completion) + } + + /// Retrieve the balance of a given address. + public func getBalance(address: Address, completion: @escaping (Result) -> Void) { + let rpc = GetAddressBalanceRPC(address: address) + self.run(rpc, completion: completion) + } + + /// Retrieve the delegate of a given wallet. + public func getDelegate(wallet: Wallet, completion: @escaping (Result) -> Void) { + getDelegate(address: wallet.address, completion: completion) + } + + /// Retrieve the delegate of a given address. + public func getDelegate(address: Address, completion: @escaping (Result) -> Void) { + let rpc = GetDelegateRPC(address: address) + self.run(rpc, completion: completion) + } + + /// Retrieve the hash of the block at the head of the chain. + public func getHeadHash(completion: @escaping (Result) -> Void) { + let rpc = GetChainHeadHashRPC() + self.run(rpc, completion: completion) + } + + /// Retrieve the address counter for the given address. + public func getAddressCounter(address: Address, completion: @escaping (Result) -> Void) { + let rpc = GetAddressCounterRPC(address: address) + self.run(rpc, completion: completion) + } + + /// Retrieve the address manager key for the given address. + public func getAddressManagerKey( + address: Address, + completion: @escaping (Result) -> Void + ) { + let rpc = GetAddressManagerKeyRPC(address: address) + self.run(rpc, completion: completion) + } + + /// Retrieve ballots cast so far during a voting period. + public func getBallotsList(completion: @escaping (Result<[[String: Any]], TezosKitError>) -> Void) { + let rpc = GetBallotsListRPC() + self.run(rpc, completion: completion) + } + + /// Retrieve the expected quorum. + public func getExpectedQuorum(completion: @escaping (Result) -> Void) { + let rpc = GetExpectedQuorumRPC() + self.run(rpc, completion: completion) + } + + /// Retrieve the current period kind for voting. + public func getCurrentPeriodKind(completion: @escaping (Result) -> Void) { + let rpc = GetCurrentPeriodKindRPC() + self.run(rpc, completion: completion) + } + + /// Retrieve the sum of ballots cast so far during a voting period. + public func getBallots(completion: @escaping (Result<[String: Any], TezosKitError>) -> Void) { + let rpc = GetBallotsRPC() + self.run(rpc, completion: completion) } + + /// Retrieve a list of proposals with number of supporters. + public func getProposalsList(completion: @escaping (Result<[[String: Any]], TezosKitError>) -> Void) { + let rpc = GetProposalsListRPC() + self.run(rpc, completion: completion) + } + + /// Retrieve the current proposal under evaluation. + public func getProposalUnderEvaluation(completion: @escaping (Result) -> Void) { + let rpc = GetProposalUnderEvaluationRPC() + self.run(rpc, completion: completion) + } + + /// Retrieve a list of delegates with their voting weight, in number of rolls. + public func getVotingDelegateRights(completion: @escaping (Result<[[String: Any]], TezosKitError>) -> Void) { + let rpc = GetVotingDelegateRightsRPC() + self.run(rpc, completion: completion) + + } + + /// Run an arbitrary RPC. + /// + /// - Parameters: + /// - rpc: The RPC to run. + /// - completion : A completion block which handles the results of the RPC + public func run(_ rpc: RPC, completion: @escaping (Result) -> Void) { + networkClient.send(rpc, completion: completion) + } + + /// Inspect the value of a big map in a smart contract. + /// + /// - Parameters: + /// - address: The address of a smart contract with a big map. + /// - key: The key in the big map to look up. + /// - type: The michelson type of the key. + /// - completion: A completion block to call. + public func getBigMapValue( + address: Address, + key: MichelsonParameter, + type: MichelsonComparable, + completion: @escaping (Result<[String: Any], TezosKitError>) -> Void + ) { + let rpc = GetBigMapValueRPC(address: address, key: key, type: type) + self.run(rpc, completion: completion) + } + + /// Retrieve the storage of a smart contract. + /// + /// - Parameters: + /// - address: The address of the smart contract to inspect. + /// - completion: A completion block which will be called with the storage. + public func getContractStorage( + address: Address, + completion: @escaping (Result<[String: Any], TezosKitError>) -> Void + ) { + let rpc = GetContractStorageRPC(address: address) + self.run(rpc, completion: completion) + } + + /// Retrieve a value from a big map. + /// + /// - Parameters: + /// - bigMapID: The ID of the big map. + /// - key: The key in the big map to look up. + /// - type: The michelson type of the key. + /// - completion: A completion block to call. + public func getBigMapValue( + bigMapID: BigInt, + key: MichelsonParameter, + type: MichelsonComparable, + completion: @escaping (Result<[String: Any], TezosKitError>) -> Void + ) { + let payload = PackDataPayload(michelsonParameter: key, michelsonComparable: type) + let packDataRPC = PackDataRPC(payload: payload) + + self.run(packDataRPC) { [weak self] result in + guard let self = self else { + return + } + + guard case let .success(expression) = result else { + completion( + result.map { _ in [:] } + ) + return + } + + let bigMapValueRPC = GetBigMapValueByIDRPC(bigMapID: bigMapID, expression: expression) + self.run(bigMapValueRPC, completion: completion) + } + } + + // MARK: - Operations + + /// Transact Tezos between accounts. + /// + /// - Parameters: + /// - amount: The amount of Tez to send. + /// - recipientAddress: The address which will receive the Tez. + /// - source: The address sending the balance. + /// - signatureProvider: The object which will sign the operation. + /// - operationFees: OperationFees for the transaction. If nil, default fees are used. + /// - completion: A completion block called with an optional transaction hash and error. + @available(*, deprecated, message: "Please use an OperationFeePolicy API instead.") + public func send( + amount: Tez, + to recipientAddress: String, + from source: Address, + signatureProvider: SignatureProvider, + operationFees: OperationFees? = nil, + completion: @escaping (Result) -> Void + ) { + var policy = OperationFeePolicy.default + if let operationFees = operationFees { + policy = .custom(operationFees) + } + + send( + amount: amount, + to: recipientAddress, + from: source, + signatureProvider: signatureProvider, + operationFeePolicy: policy, + completion: completion + ) + } + + /// Transact Tezos between accounts. + /// + /// - Parameters: + /// - amount: The amount of Tez to send. + /// - recipientAddress: The address which will receive the Tez. + /// - source: The address sending the balance. + /// - signatureProvider: The object which will sign the operation. + /// - operationFeePolicy: A policy to apply when determining operation fees. Default is default fees. + /// - completion: A completion block called with an optional transaction hash and error. + public func send( + amount: Tez, + to recipientAddress: String, + from source: Address, + signatureProvider: SignatureProvider, + operationFeePolicy: OperationFeePolicy = .default, + completion: @escaping (Result) -> Void + ) { + let result = operationFactory.transactionOperation( + amount: amount, + source: source, + destination: recipientAddress, + operationFeePolicy: operationFeePolicy, + signatureProvider: signatureProvider + ) + + switch result { + case .success(let transactionOperation): + forgeParseSignPreapplyAndInject( + transactionOperation, + source: source, + signatureProvider: signatureProvider, + completion: completion + ) + case .failure(let error): + callbackQueue.async { + completion(.failure(.transactionFormationFailure(underlyingError: error))) + } + } + } + + /// Call a smart contract. + /// + /// - Parameters: + /// - contract: The smart contract to invoke. + /// - amount: The amount of Tez to transfer with the invocation. Default is 0. + /// - parameter: An optional parameter to send to the smart contract. Default is none. + /// - source: The address invoking the contract. + /// - signatureProvider: The object which will sign the operation. + /// - operationFeePolicy: A policy to apply when determining operation fees. + /// - completion: A completion block called with an optional transaction hash and error. + @available(*, deprecated, message: "Please use an OperationFeePolicy API instead.") + public func call( + contract: Address, + amount: Tez = Tez.zeroBalance, + parameter: MichelsonParameter? = nil, + source: Address, + signatureProvider: SignatureProvider, + operationFees: OperationFees? = nil, + completion: @escaping (Result) -> Void + ) { + var policy = OperationFeePolicy.default + if let operationFees = operationFees { + policy = .custom(operationFees) + } + + call( + contract: contract, + amount: amount, + parameter: parameter, + source: source, + signatureProvider: signatureProvider, + operationFeePolicy: policy, + completion: completion + ) + } + + /// Call a smart contract. + /// + /// - Parameters: + /// - contract: The smart contract to invoke. + /// - amount: The amount of Tez to transfer with the invocation. Default is 0. + /// - entrypoint: An optional entrypoint to use for the transaction. Default is nil. + /// - parameter: An optional parameter to send to the smart contract. Default is none. + /// - source: The address invoking the contract. + /// - signatureProvider: The object which will sign the operation. + /// - operationFeePolicy: A policy to apply when determining operation fees. Default is default fees. + /// - completion: A completion block called with an optional transaction hash and error. + public func call( + contract: Address, + amount: Tez = Tez.zeroBalance, + entrypoint: String? = nil, + parameter: MichelsonParameter? = nil, + source: Address, + signatureProvider: SignatureProvider, + operationFeePolicy: OperationFeePolicy = .default, + completion: @escaping (Result) -> Void + ) { + let result = operationFactory.smartContractInvocationOperation( + amount: amount, + entrypoint: entrypoint, + parameter: parameter, + source: source, + destination: contract, + operationFeePolicy: operationFeePolicy, + signatureProvider: signatureProvider + ) + + switch result { + case .success(let smartContractInvocationOperation): + forgeParseSignPreapplyAndInject( + smartContractInvocationOperation, + source: source, + signatureProvider: signatureProvider, + completion: completion + ) + case .failure(let error): + callbackQueue.async { + completion(.failure(.transactionFormationFailure(underlyingError: error))) + } + } + } + + /// Delegate the balance of an account. + /// + /// - Parameters: + /// - source: The address which will delegate. + /// - delegate: The address which will receive the delegation. + /// - signatureProvider: The object which will sign the operation. + /// - operationFees: OperationFees for the transaction. If nil, default fees are used. + /// - completion: A completion block called with an optional transaction hash and error. + @available(*, deprecated, message: "Please use an OperationFeePolicy API instead.") + public func delegate( + from source: Address, + to delegate: Address, + signatureProvider: SignatureProvider, + operationFees: OperationFees? = nil, + completion: @escaping (Result) -> Void + ) { + var policy = OperationFeePolicy.default + if let operationFees = operationFees { + policy = .custom(operationFees) + } + + self.delegate( + from: source, + to: delegate, + signatureProvider: signatureProvider, + operationFeePolicy: policy, + completion: completion + ) + } + + /// Delegate the balance of an account. + /// + /// - Parameters: + /// - source: The address which will delegate. + /// - delegate: The address which will receive the delegation. + /// - signatureProvider: The object which will sign the operation. + /// - operationFeePolicy: A policy to apply when determining operation fees. Default is default fees. + /// - completion: A completion block called with an optional transaction hash and error. + public func delegate( + from source: Address, + to delegate: Address, + signatureProvider: SignatureProvider, + operationFeePolicy: OperationFeePolicy = .default, + completion: @escaping (Result) -> Void + ) { + let result = operationFactory.delegateOperation( + source: source, + to: delegate, + operationFeePolicy: operationFeePolicy, + signatureProvider: signatureProvider + ) + + switch result { + case .success(let delegationOperation): + forgeParseSignPreapplyAndInject( + delegationOperation, + source: source, + signatureProvider: signatureProvider, + completion: completion + ) + case .failure(let error): + callbackQueue.async { + completion(.failure(.transactionFormationFailure(underlyingError: error))) + } + } + } + + /// Clear the delegate of an account. + /// + /// - Parameters: + /// - source: The address which is removing the delegate. + /// - signatureProvider: The object which will sign the operation. + /// - operationFees: OperationFees for the transaction. If nil, default fees are used. + /// - completion: A completion block which will be called with a string representing the transaction ID hash if the + /// operation was successful. + @available(*, deprecated, message: "Please use an OperationFeePolicy API instead.") + public func undelegate( + from source: Address, + signatureProvider: SignatureProvider, + operationFees: OperationFees? = nil, + completion: @escaping (Result) -> Void + ) { + var policy = OperationFeePolicy.default + if let operationFees = operationFees { + policy = .custom(operationFees) + } + + undelegate(from: source, signatureProvider: signatureProvider, operationFeePolicy: policy, completion: completion) + } + + /// Clear the delegate of an account. + /// + /// - Parameters: + /// - source: The address which is removing the delegate. + /// - signatureProvider: The object which will sign the operation. + /// - operationFeePolicy: A policy to apply when determining operation fees. Default is default fees. + /// - completion: A completion block which will be called with a string representing the transaction ID hash if the + /// operation was successful. + public func undelegate( + from source: Address, + signatureProvider: SignatureProvider, + operationFeePolicy: OperationFeePolicy = .default, + completion: @escaping (Result) -> Void + ) { + let result = operationFactory.undelegateOperation( + source: source, + operationFeePolicy: operationFeePolicy, + signatureProvider: signatureProvider + ) + + switch result { + case .success(let undelegateOperation): + forgeParseSignPreapplyAndInject( + undelegateOperation, + source: source, + signatureProvider: signatureProvider, + completion: completion + ) + case .failure(let error): + callbackQueue.async { + completion(.failure(.transactionFormationFailure(underlyingError: error))) + } + return + } + } + + /// Register an address as a delegate. + /// + /// - Parameters: + /// - delegate: The address registering as a delegate. + /// - signatureProvider: The object which will sign the operation. + /// - operationFees: OperationFees for the transaction. If nil, default fees are used. + /// - completion: A completion block called with an optional transaction hash and error. + @available(*, deprecated, message: "Please use an OperationFeePolicy API instead.") + public func registerDelegate( + delegate: Address, + signatureProvider: SignatureProvider, + operationFees: OperationFees? = nil, + completion: @escaping (Result) -> Void + ) { + var policy = OperationFeePolicy.default + if let operationFees = operationFees { + policy = .custom(operationFees) + } + + registerDelegate( + delegate: delegate, + signatureProvider: signatureProvider, + operationFeePolicy: policy, + completion: completion + ) + } + + /// Register an address as a delegate. + /// + /// - Parameters: + /// - delegate: The address registering as a delegate. + /// - signatureProvider: The object which will sign the operation. + /// - operationFeePolicy: A policy to apply when determining operation fees. Default is default fees. + /// - completion: A completion block called with an optional transaction hash and error. + public func registerDelegate( + delegate: Address, + signatureProvider: SignatureProvider, + operationFeePolicy: OperationFeePolicy = .default, + completion: @escaping (Result) -> Void + ) { + let result = operationFactory.registerDelegateOperation( + source: delegate, + operationFeePolicy: operationFeePolicy, + signatureProvider: signatureProvider + ) + + switch result { + case .success(let registerDelegateOperation): + forgeParseSignPreapplyAndInject( + registerDelegateOperation, + source: delegate, + signatureProvider: signatureProvider, + completion: completion + ) + case .failure(let error): + callbackQueue.async { + completion(.failure(.transactionFormationFailure(underlyingError: error))) + } + } + } + + /// Retrieve metadata and runs an operation. + /// + /// - Parameters: + /// - operation: The operation to run. + /// - wallet: The wallet requesting the run. + /// - completion: A completion block to call. + public func runOperation( + _ operation: Operation, + from wallet: Wallet, + completion: @escaping (Result) -> Void + ) { + simulationService.simulate(operation, from: wallet.address, signatureProvider: wallet, completion: completion) + } + + // MARK: - Private Methods + + /// Forge, parse, sign, preapply and then inject a single operation. + /// + /// - Parameters: + /// - operation: The operation which will be used to forge the operation. + /// - source: The address performing the operation. + /// - signatureProvider: The object which will sign the operation. + /// - completion: A completion block that will be called with the results of the operation. + public func forgeParseSignPreapplyAndInject( + _ operation: Operation, + source: Address, + signatureProvider: SignatureProvider, + completion: @escaping (Result) -> Void + ) { + forgeParseSignPreapplyAndInject( + [operation], + source: source, + signatureProvider: signatureProvider, + completion: completion + ) + } + + /// Forge, parse, sign, preapply and then inject a set of operations. + /// + /// Operations are processed in the order they are placed in the operation array. + /// + /// - Parameters: + /// - operations: The operations which will be forged. + /// - source: The address performing the operation. + /// - signatureProvider: The object which will sign the operation. + /// - completion: A completion block that will be called with the results of the operation. + public func forgeParseSignPreapplyAndInject( + _ operations: [Operation], + source: Address, + signatureProvider: SignatureProvider, + completion: @escaping (Result) -> Void + ) { + operationMetadataProvider.metadata(for: source) { [weak self] result in + guard let self = self else { + return + } + + guard + case let .success(operationMetadata) = result, + let operationPayload = OperationPayloadFactory.operationPayload( + from: operations, + source: source, + signatureProvider: signatureProvider, + operationMetadata: operationMetadata + ) + else { + completion( + result.map { _ in "" } + ) + return + } + + self.forgingService.forge( + operationPayload: operationPayload, + operationMetadata: operationMetadata + ) { [weak self] result in + guard let self = self else { + return + } + guard case let .success(forgedBytes) = result else { + completion( + result.map { _ in "" } + ) + return + } + + self.parseAndCompare(hash: forgedBytes, operationMetadata: operationMetadata, operationPayload: operationPayload) { [weak self] (result) in + if case .failure(let error) = result { + completion(Result.failure(error)) + return + } + + self?.signPreapplyAndInjectOperation( + operationPayload: operationPayload, + operationMetadata: operationMetadata, + forgeResult: forgedBytes, + source: source, + signatureProvider: signatureProvider, + completion: completion + ) + } + } + } + } + + /// Ask the tezos node to parse the return block hash and compare with our local copy to ensure it has not been tampered with. This should be performed on a different server + /// + /// - Parameters: + /// - hash: The returned hash from the forge operation. + /// - operationMetadata: Metadata related to the operation. + /// - operations: The array of operations to compare the parsed hash too. + /// - completion: A completion block that will be called with the results of the comparision. + private func parseAndCompare(hash: String, operationMetadata: OperationMetadata, operationPayload: OperationPayload, completion: @escaping ((Result) -> Void)) { + + // Remove first 32 bytes (64 characters), to remove branch and block hash + let stringIndex = hash.index(hash.startIndex, offsetBy: 64) + let stripped = hash[stringIndex..) -> Void + ) { + guard + let signature = SigningService.sign(forgeResult, with: signatureProvider), + let signatureHex = CryptoUtils.binToHex(signature), + let signedBytesForInjection = JSONUtils.jsonString(for: forgeResult + signatureHex), + let signedOperationPayload = SignedOperationPayload( + operationPayload: operationPayload, + signature: signature, + signingCurve: signatureProvider.publicKey.signingCurve + ) + else { + completion(.failure(.signingError)) + return + } + + let signedProtocolOperationPayload = SignedProtocolOperationPayload( + signedOperationPayload: signedOperationPayload, + operationMetadata: operationMetadata + ) + + preapplicationService.preapply( + signedProtocolOperationPayload: signedProtocolOperationPayload, + signedBytesForInjection: signedBytesForInjection, + operationMetadata: operationMetadata + ) { result in + if let error = result { + completion(.failure(error)) + return + } + self.injectionService.inject(payload: signedBytesForInjection, completion: completion) + } + } } +