From aeb615ef6c79aa23ef29890e9dfc2201acd90a11 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Tue, 30 Jun 2020 16:02:52 +0100 Subject: [PATCH 01/14] - changed forgeSignPreapplyAndInject to include remote node parsing as an extra layer of security - changed Tez rpc representation to return "0" at a minimum to avoid conflicts with the node --- .../TezosNode/TezosNodeClient+Promises.swift | 4 +- TezosKit.xcodeproj/project.pbxproj | 33 ++++++-- TezosKit/Common/Models/Tez.swift | 14 ++-- TezosKit/Common/Models/Wallet.swift | 2 +- TezosKit/Common/Services/NetworkClient.swift | 13 +++- TezosKit/Conseil/ConseilClient.swift | 1 + TezosKit/Dexter/DexterExchangeClient.swift | 6 +- TezosKit/Dexter/TokenContractClient.swift | 7 +- .../TezosNode/RPC/ParseOperationRPC.swift | 23 ++++++ .../TezosNode/Services/ParsingService.swift | 78 +++++++++++++++++++ TezosKit/TezosNode/TezosNodeClient.swift | 75 +++++++++++++----- 11 files changed, 216 insertions(+), 40 deletions(-) create mode 100644 TezosKit/TezosNode/RPC/ParseOperationRPC.swift create mode 100644 TezosKit/TezosNode/Services/ParsingService.swift 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/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..ad58ee38 100644 --- a/TezosKit/Common/Services/NetworkClient.swift +++ b/TezosKit/Common/Services/NetworkClient.swift @@ -40,6 +40,9 @@ public class NetworkClientImpl: NetworkClient { /// 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] @@ -52,18 +55,21 @@ public class NetworkClientImpl: NetworkClient { /// 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 @@ -86,7 +92,12 @@ public class NetworkClientImpl: NetworkClient { // provided. let completionQueue = callbackQueue ?? self.callbackQueue - let remoteNodeEndpoint = remoteNodeURL.appendingPathComponent(rpc.endpoint) + 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) 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/Dexter/DexterExchangeClient.swift b/TezosKit/Dexter/DexterExchangeClient.swift index 522d1372..b1b5861a 100644 --- a/TezosKit/Dexter/DexterExchangeClient.swift +++ b/TezosKit/Dexter/DexterExchangeClient.swift @@ -202,7 +202,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 +219,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, @@ -270,7 +270,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)) } diff --git a/TezosKit/Dexter/TokenContractClient.swift b/TezosKit/Dexter/TokenContractClient.swift index 8c1d86d8..e125fed7 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)) } @@ -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]) 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/Services/ParsingService.swift b/TezosKit/TezosNode/Services/ParsingService.swift new file mode 100644 index 00000000..ebf95f66 --- /dev/null +++ b/TezosKit/TezosNode/Services/ParsingService.swift @@ -0,0 +1,78 @@ +// +// ParsingService.swift +// TezosKit +// +// Created by Simon Mcloughlin on 30/06/2020. +// + +import Foundation + +/// A service which manages parsing and comparsion of remote forge hash's. +public class ParsingService { + + /// A network client that can send requests. + private let networkClient: NetworkClient + + /// Identifier for the internal dispatch queue. + private static let queueIdentifier = "com.keefertaylor.TezosKit.ParsingService" + + /// Internal Queue to use in order to perform asynchronous work. + private let parsingServiceQueue: DispatchQueue + + /// - Parameters: + /// - forgingPolicy: The forging policy to apply to all operations. + /// - networkClient: A network client that can communicate with a Tezos Node. + public init(networkClient: NetworkClient) { + self.networkClient = networkClient + parsingServiceQueue = DispatchQueue(label: ParsingService.queueIdentifier) + } + + public func parse(hashToParse: String, operationsToMatch: [Operation], operationMetadata: OperationMetadata, completion: @escaping ((Result ) -> 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, toOperations: operationsToMatch), 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]], toOperations operations: [Operation]) -> Bool { + guard let contents = jsonArray.first?["contents"] as? [[String: Any]] else { + return false + } + + if contents.count != operations.count { + return false + } + + var validMatches = 0 + + // cycle through each json object returned, and compare to the dictionary representation of the operations + outerloop: for dict in contents { + for op in operations { + + // Operation objects won't have a counter, so remove it from the network JSON before comparing + var sanitizedDict = dict + sanitizedDict.removeValue(forKey: "counter") + + if (sanitizedDict as NSDictionary).isEqual(op.dictionaryRepresentation) { + validMatches += 1 + continue outerloop + } + } + } + + return validMatches == operations.count + } +} diff --git a/TezosKit/TezosNode/TezosNodeClient.swift b/TezosKit/TezosNode/TezosNodeClient.swift index bb344b2b..b4513900 100644 --- a/TezosKit/TezosNode/TezosNodeClient.swift +++ b/TezosKit/TezosNode/TezosNodeClient.swift @@ -77,6 +77,9 @@ public class TezosNodeClient { /// A service which forges operations. internal let forgingService: ForgingService + /// A service which parses operations. + internal let parsingService: ParsingService + /// The network client. internal let networkClient: NetworkClient @@ -99,12 +102,14 @@ public class 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, @@ -112,6 +117,7 @@ public class TezosNodeClient { ) { let networkClient = NetworkClientImpl( remoteNodeURL: remoteNodeURL, + remoteNodeParseURL: remoteNodeParseURL ?? remoteNodeURL, urlSession: urlSession, callbackQueue: callbackQueue, responseHandler: RPCResponseHandler() @@ -136,6 +142,7 @@ public class TezosNodeClient { self.callbackQueue = callbackQueue forgingService = ForgingService(forgingPolicy: forgingPolicy, networkClient: networkClient) + parsingService = ParsingService(networkClient: networkClient) operationMetadataProvider = OperationMetadataProvider(networkClient: networkClient) simulationService = SimulationService( @@ -154,7 +161,7 @@ public class TezosNodeClient { injectionService = InjectionService(networkClient: networkClient) preapplicationService = PreapplicationService(networkClient: networkClient) - JailbreakUtils.crashIfJailbroken() + //JailbreakUtils.crashIfJailbroken() } // MARK: - Queries @@ -384,7 +391,7 @@ public class TezosNodeClient { switch result { case .success(let transactionOperation): - forgeSignPreapplyAndInject( + forgeParseSignPreapplyAndInject( transactionOperation, source: source, signatureProvider: signatureProvider, @@ -466,7 +473,7 @@ public class TezosNodeClient { switch result { case .success(let smartContractInvocationOperation): - forgeSignPreapplyAndInject( + forgeParseSignPreapplyAndInject( smartContractInvocationOperation, source: source, signatureProvider: signatureProvider, @@ -533,7 +540,7 @@ public class TezosNodeClient { switch result { case .success(let delegationOperation): - forgeSignPreapplyAndInject( + forgeParseSignPreapplyAndInject( delegationOperation, source: source, signatureProvider: signatureProvider, @@ -591,7 +598,7 @@ public class TezosNodeClient { switch result { case .success(let undelegateOperation): - forgeSignPreapplyAndInject( + forgeParseSignPreapplyAndInject( undelegateOperation, source: source, signatureProvider: signatureProvider, @@ -653,7 +660,7 @@ public class TezosNodeClient { switch result { case .success(let registerDelegateOperation): - forgeSignPreapplyAndInject( + forgeParseSignPreapplyAndInject( registerDelegateOperation, source: delegate, signatureProvider: signatureProvider, @@ -682,20 +689,20 @@ public class TezosNodeClient { // MARK: - Private Methods - /// Forge, sign, preapply and then inject a single operation. + /// 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 forgeSignPreapplyAndInject( + public func forgeParseSignPreapplyAndInject( _ operation: Operation, source: Address, signatureProvider: SignatureProvider, completion: @escaping (Result) -> Void ) { - forgeSignPreapplyAndInject( + forgeParseSignPreapplyAndInject( [operation], source: source, signatureProvider: signatureProvider, @@ -703,7 +710,7 @@ public class TezosNodeClient { ) } - /// Forge, sign, preapply and then inject a set of operations. + /// Forge, parse, sign, preapply and then inject a set of operations. /// /// Operations are processed in the order they are placed in the operation array. /// @@ -712,7 +719,7 @@ public class TezosNodeClient { /// - 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( + public func forgeParseSignPreapplyAndInject( _ operations: [Operation], source: Address, signatureProvider: SignatureProvider, @@ -751,18 +758,48 @@ public class TezosNodeClient { ) return } - self.signPreapplyAndInjectOperation( - operationPayload: operationPayload, - operationMetadata: operationMetadata, - forgeResult: forgedBytes, - source: source, - signatureProvider: signatureProvider, - completion: completion - ) + + self.parseAndCompare(hash: forgedBytes, operationMetadata: operationMetadata, operations: operations) { [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, operations: [Operation], 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.. Date: Thu, 2 Jul 2020 12:48:27 +0100 Subject: [PATCH 02/14] Fixed an issue with OperationResponseInternalResultError not parsing due to multiple different types of responses --- TezosKit/TezosNode/Models/Operation/OperationResponse.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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]]? } From 127a6c98c47bfd1d81f0b415746eab0d1e25b38c Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Fri, 3 Jul 2020 12:31:59 +0100 Subject: [PATCH 03/14] Change parsing to use operation payload so reveal operations can be accounted for --- .../TezosNode/Services/ParsingService.swift | 42 +++++++------------ TezosKit/TezosNode/TezosNodeClient.swift | 6 +-- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/TezosKit/TezosNode/Services/ParsingService.swift b/TezosKit/TezosNode/Services/ParsingService.swift index ebf95f66..f17b3dc0 100644 --- a/TezosKit/TezosNode/Services/ParsingService.swift +++ b/TezosKit/TezosNode/Services/ParsingService.swift @@ -27,14 +27,14 @@ public class ParsingService { parsingServiceQueue = DispatchQueue(label: ParsingService.queueIdentifier) } - public func parse(hashToParse: String, operationsToMatch: [Operation], operationMetadata: OperationMetadata, completion: @escaping ((Result ) -> Void)) { + public func parse(hashToParse: String, operationPayload: OperationPayload, operationMetadata: OperationMetadata, completion: @escaping ((Result ) -> 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, toOperations: operationsToMatch), comparisonResult { + if let comparisonResult = self?.compare(jsonArray: jsonArray, toOperationPayload: operationPayload), comparisonResult { completion(Result.success(true)) } else { @@ -47,32 +47,18 @@ public class ParsingService { } } - private func compare(jsonArray: [[String: Any]], toOperations operations: [Operation]) -> Bool { - guard let contents = jsonArray.first?["contents"] as? [[String: Any]] else { + private func compare(jsonArray: [[String: Any]], toOperationPayload operationPayload: OperationPayload) -> Bool { + guard let dict = jsonArray.first as? [String: Any] else { return false } - - if contents.count != operations.count { - return false - } - - var validMatches = 0 - - // cycle through each json object returned, and compare to the dictionary representation of the operations - outerloop: for dict in contents { - for op in operations { - - // Operation objects won't have a counter, so remove it from the network JSON before comparing - var sanitizedDict = dict - sanitizedDict.removeValue(forKey: "counter") - - if (sanitizedDict as NSDictionary).isEqual(op.dictionaryRepresentation) { - validMatches += 1 - continue outerloop - } - } - } - - return validMatches == operations.count + + var sanitizedDict = dict + sanitizedDict.removeValue(forKey: "signature") + + if (sanitizedDict as NSDictionary).isEqual(operationPayload.dictionaryRepresentation) { + return true + } + + return false } } diff --git a/TezosKit/TezosNode/TezosNodeClient.swift b/TezosKit/TezosNode/TezosNodeClient.swift index b4513900..628c2cbb 100644 --- a/TezosKit/TezosNode/TezosNodeClient.swift +++ b/TezosKit/TezosNode/TezosNodeClient.swift @@ -759,7 +759,7 @@ public class TezosNodeClient { return } - self.parseAndCompare(hash: forgedBytes, operationMetadata: operationMetadata, operations: operations) { [weak self] (result) in + self.parseAndCompare(hash: forgedBytes, operationMetadata: operationMetadata, operationPayload: operationPayload) { [weak self] (result) in if case .failure(let error) = result { completion(Result.failure(error)) return @@ -785,7 +785,7 @@ public class TezosNodeClient { /// - 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, operations: [Operation], completion: @escaping ((Result) -> Void)) { + 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) @@ -795,7 +795,7 @@ public class TezosNodeClient { let padded = String(stripped).appending(String(repeating: "0", count: 128)) // Use the Tezos node (ideally a different server) to confirm the returned forge hasn't bene tampered with - parsingService.parse(hashToParse: padded, operationsToMatch: operations, operationMetadata: operationMetadata) { (result) in + parsingService.parse(hashToParse: padded, operationPayload: operationPayload, operationMetadata: operationMetadata) { (result) in completion(result) } } From 032f2a694a3a7f11c20f3a95ce1d27f20c68dc1b Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Wed, 8 Jul 2020 12:06:37 +0100 Subject: [PATCH 04/14] update Fa1.2 token send --- TezosKit/Dexter/TokenContractClient.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/TezosKit/Dexter/TokenContractClient.swift b/TezosKit/Dexter/TokenContractClient.swift index e125fed7..764806cd 100644 --- a/TezosKit/Dexter/TokenContractClient.swift +++ b/TezosKit/Dexter/TokenContractClient.swift @@ -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, From 4a0ce05df778754deec58e2960ca8ac05ec58aa8 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Tue, 21 Jul 2020 19:36:45 +0100 Subject: [PATCH 05/14] update dexter contract calls to new storage format --- TezosKit/Dexter/DexterExchangeClient.swift | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/TezosKit/Dexter/DexterExchangeClient.swift b/TezosKit/Dexter/DexterExchangeClient.swift index b1b5861a..c05c4f7d 100644 --- a/TezosKit/Dexter/DexterExchangeClient.swift +++ b/TezosKit/Dexter/DexterExchangeClient.swift @@ -227,12 +227,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, @@ -300,19 +300,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, From 77ade4e0d5c4b00eb472a3925ab27968980bc2e3 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Fri, 31 Jul 2020 14:58:44 +0100 Subject: [PATCH 06/14] temporarily add one kind of burnFee to operationFees to deal with issue sending fees to balance-less acocunts --- TezosKit/TezosNode/Models/OperationFees.swift | 4 +++- TezosKit/TezosNode/Models/SimulationResult.swift | 1 + .../SimulationResultResponseAdapter.swift | 11 ++++++++++- TezosKit/TezosNode/Services/FeeEstimator.swift | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) 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/ResponseAdapters/SimulationResultResponseAdapter.swift b/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift index ff336558..80294ff8 100644 --- a/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift +++ b/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift @@ -13,6 +13,7 @@ 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 enum Values { @@ -37,6 +38,8 @@ public class SimulationResultResponseAdapter: AbstractResponseAdapter Date: Tue, 18 Aug 2020 14:21:19 +0100 Subject: [PATCH 07/14] BugFix: conseil `orderby` not being used due to typo BugFix: conseil `orderBy` not using correct format of array of dictionaries --- TezosKit/Conseil/Models/ConseilQuery.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 - ] + ]] } } From 7a3c3757445de50db6501c6b49ac28801b1301bd Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Thu, 24 Sep 2020 15:54:25 +0100 Subject: [PATCH 08/14] update to Michelson params --- TezosKit/Dexter/TokenContractClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TezosKit/Dexter/TokenContractClient.swift b/TezosKit/Dexter/TokenContractClient.swift index 764806cd..895dc2b3 100644 --- a/TezosKit/Dexter/TokenContractClient.swift +++ b/TezosKit/Dexter/TokenContractClient.swift @@ -202,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 { From 92339a9c4b235cf635d9aff142eff92fccdb61c5 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Tue, 20 Oct 2020 15:28:10 +0100 Subject: [PATCH 09/14] fix dexter exchange, token pool query --- TezosKit/Dexter/DexterExchangeClient.swift | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/TezosKit/Dexter/DexterExchangeClient.swift b/TezosKit/Dexter/DexterExchangeClient.swift index c05c4f7d..67ffc9d8 100644 --- a/TezosKit/Dexter/DexterExchangeClient.swift +++ b/TezosKit/Dexter/DexterExchangeClient.swift @@ -50,8 +50,26 @@ public class DexterExchangeClient { tokenContractAddress: Address, completion: @escaping(Result) -> Void ) { - let tokenClient = TokenContractClient(tokenContractAddress: tokenContractAddress, tezosNodeClient: tezosNodeClient) - tokenClient.getTokenBalance(address: exchangeContractAddress, completion: completion) + tezosNodeClient.getContractStorage(address: exchangeContractAddress) { result in + guard + case let .success(json) = result, + let args0 = json[JSON.Keys.args] as? [Any], + let right0 = args0[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)) + } } /// Get the total exchange liquidity. From c224ec0ae1f4c232c300e62639a2d18a25a3bc08 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Tue, 27 Oct 2020 11:56:35 +0000 Subject: [PATCH 10/14] fixing broken storage_limit processing --- .../ResponseAdapters/SimulationResultResponseAdapter.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift b/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift index 80294ff8..d702fded 100644 --- a/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift +++ b/TezosKit/TezosNode/RPC/ResponseAdapters/SimulationResultResponseAdapter.swift @@ -14,6 +14,7 @@ private enum JSON { 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 { @@ -56,7 +57,7 @@ public class SimulationResultResponseAdapter: AbstractResponseAdapter Date: Thu, 5 Nov 2020 10:45:30 +0000 Subject: [PATCH 11/14] PreapplicationService error checker doesn't report the correct errors for Dexter contracts. OperationResponse is not catching these responses first, as /preapply returns an array, were as all others return a dictionary. Adding a quick hack to detect this difference and parse the errors correctly. --- .../TezosNode/Services/RPCResponseHandler.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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. } From 584dc85e9962b9718b99e0067390642024c91379 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Wed, 16 Dec 2020 11:24:13 +0000 Subject: [PATCH 12/14] - hack: add an errorCallback to NetworkClient so that apps can do something with errors inaccessible, such as estimation errors - hack: make everything public on TezosNodeClient so settings can be accessed and modified by app --- TezosKit/Common/Services/NetworkClient.swift | 312 ++-- TezosKit/TezosNode/TezosNodeClient.swift | 1561 +++++++++--------- 2 files changed, 943 insertions(+), 930 deletions(-) diff --git a/TezosKit/Common/Services/NetworkClient.swift b/TezosKit/Common/Services/NetworkClient.swift index ad58ee38..cdb3fa90 100644 --- a/TezosKit/Common/Services/NetworkClient.swift +++ b/TezosKit/Common/Services/NetworkClient.swift @@ -4,158 +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 - - /// 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 - - /// 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 - ) - 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/TezosNode/TezosNodeClient.swift b/TezosKit/TezosNode/TezosNodeClient.swift index 628c2cbb..66fdd1c2 100644 --- a/TezosKit/TezosNode/TezosNodeClient.swift +++ b/TezosKit/TezosNode/TezosNodeClient.swift @@ -68,784 +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 - - /// A service which parses operations. - internal let parsingService: ParsingService - - /// 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 - /// - 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) - } - } + /// 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) + } + } } + From 48a0fbe2a4671042439f5129f9ca95f46ffada62 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Fri, 18 Dec 2020 21:13:32 +0000 Subject: [PATCH 13/14] - fix: Dexter bug when devices are using 12 hour format --- TezosKit/Common/Michelson/TimestampMichelsonParameter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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) From 882af1590d5e374c7f0a628d985033e0a7f386a1 Mon Sep 17 00:00:00 2001 From: Simon Mcloughlin Date: Mon, 1 Feb 2021 15:10:31 +0000 Subject: [PATCH 14/14] - update dexter query for edonet --- TezosKit/Dexter/DexterExchangeClient.swift | 68 ++++++++++++++-------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/TezosKit/Dexter/DexterExchangeClient.swift b/TezosKit/Dexter/DexterExchangeClient.swift index 67ffc9d8..112d6c0e 100644 --- a/TezosKit/Dexter/DexterExchangeClient.swift +++ b/TezosKit/Dexter/DexterExchangeClient.swift @@ -45,32 +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 - ) { - tezosNodeClient.getContractStorage(address: exchangeContractAddress) { result in - guard - case let .success(json) = result, - let args0 = json[JSON.Keys.args] as? [Any], - let right0 = args0[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 + /// 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 + } } - - completion(.success(balance)) } - } /// Get the total exchange liquidity. public func getExchangeLiquidity(completion: @escaping (Result) -> Void) {