From 8cf41adc9ffe972c475ffdf880c47c9300c21eb4 Mon Sep 17 00:00:00 2001 From: Kristy Brambila Date: Fri, 10 Jan 2020 16:07:00 -0800 Subject: [PATCH] Future-atize execute on Transaction and fix minor bug in similar code for QueryBuilder --- Sources/Hedera/HederaError.swift | 20 +-- Sources/Hedera/QueryBuilder.swift | 89 +++++++++--- Sources/Hedera/Transaction.swift | 135 ++++++++++-------- Sources/Hedera/TransactionBuilder.swift | 5 +- .../AccountCreateTransactionTests.swift | 2 +- .../ed25519/Ed25519PrivateKeyTests.swift | 2 +- 6 files changed, 164 insertions(+), 89 deletions(-) diff --git a/Sources/Hedera/HederaError.swift b/Sources/Hedera/HederaError.swift index aa2057ac..7724d045 100644 --- a/Sources/Hedera/HederaError.swift +++ b/Sources/Hedera/HederaError.swift @@ -12,20 +12,20 @@ public struct HederaNetworkError: Error { } func resultFromCode( - _ code: Proto_ResponseCodeEnum, - success: T, + _ code: Proto_ResponseCodeEnum, + success: () -> T, allowUnknown: Bool = false ) -> Result { switch code { - case .success, .ok: - return .success(success) + case .success, .ok: + return .success(success()) - case .unknown where allowUnknown, - .receiptNotFound where allowUnknown, - .recordNotFound where allowUnknown: - return .success(success) + case .unknown where allowUnknown, + .receiptNotFound where allowUnknown, + .recordNotFound where allowUnknown: + return .success(success()) - default: - return .failure(.status(code.rawValue)) + default: + return .failure(.status(code.rawValue)) } } diff --git a/Sources/Hedera/QueryBuilder.swift b/Sources/Hedera/QueryBuilder.swift index 62b6478e..37153e48 100644 --- a/Sources/Hedera/QueryBuilder.swift +++ b/Sources/Hedera/QueryBuilder.swift @@ -64,15 +64,33 @@ public class QueryBuilder { .toProto() } - let req = getQueryMethod(client.grpcClient(for: node))(body, nil) - let resp = req.response + let eventLoop = client.eventLoopGroup.next() - return resp.flatMapResult { (response) -> Result in - let header = self.getResponseHeader(response) - let preCheckCode = header.nodeTransactionPrecheckCode + return self.getQueryMethod(client.grpcClient(for: node))(self.body, nil) + .response + .flatMap { resp in + let header = self.getResponseHeader(resp) + let code = header.nodeTransactionPrecheckCode + + if self.shouldRetry(code) { + return self.innerExecute( + eventLoop: eventLoop, + client: client, + node: node, + startTime: Date(), + attempt: 0, + successValueMapper: { self.getResponseHeader($0).cost } + ) + } - return resultFromCode(preCheckCode, success: header.cost) - } + switch resultFromCode(code, success: { header.cost }) { + case .success(let res): + return eventLoop.makeSucceededFuture(res) + + case .failure(let err): + return eventLoop.makeFailedFuture(err) + } + } } @discardableResult @@ -195,12 +213,43 @@ public class QueryBuilder { // TODO: Sometime in the future run a local validator - return nodeFut.flatMap { node in - self.innerExecute(eventLoop: eventLoop, client: client, node: node, startTime: Date(), attempt: 0) + return nodeFut.flatMap { node in + self.getQueryMethod(client.grpcClient(for: node))(self.body, nil) + .response + .flatMap { resp in + let header = self.getResponseHeader(resp) + let code = header.nodeTransactionPrecheckCode + + if self.shouldRetry(code) { + return self.innerExecute( + eventLoop: eventLoop, + client: client, + node: node, + startTime: Date(), + attempt: 0, + successValueMapper: self.mapResponse + ) + } + + switch resultFromCode(code, success: { self.mapResponse(resp) }) { + case .success(let res): + return eventLoop.makeSucceededFuture(res) + + case .failure(let err): + return eventLoop.makeFailedFuture(err) + } + } } } - - func innerExecute(eventLoop: EventLoop, client: Client, node: Node, startTime: Date, attempt: UInt8) -> EventLoopFuture { + + func innerExecute( + eventLoop: EventLoop, + client: Client, + node: Node, + startTime: Date, + attempt: UInt8, + successValueMapper: @escaping (Proto_Response) -> T + ) -> EventLoopFuture { guard let delay = Backoff.getDelay(startTime: startTime, attempt: attempt) else { return eventLoop.makeFailedFuture(HederaError.timedOut) } @@ -221,15 +270,21 @@ public class QueryBuilder { if self.shouldRetry(code) { return self.innerExecute( - eventLoop: eventLoop, client: client, node: node, startTime: startTime, attempt: attempt + 1) + eventLoop: eventLoop, + client: client, + node: node, + startTime: startTime, + attempt: attempt + 1, + successValueMapper: successValueMapper + ) } - switch resultFromCode(code, success: self.mapResponse(resp)) { - case .success(let res): - return eventLoop.makeSucceededFuture(res) + switch resultFromCode(code, success: { successValueMapper(resp) }) { + case .success(let res): + return eventLoop.makeSucceededFuture(res) - case .failure(let err): - return eventLoop.makeFailedFuture(err) + case .failure(let err): + return eventLoop.makeFailedFuture(err) } } } diff --git a/Sources/Hedera/Transaction.swift b/Sources/Hedera/Transaction.swift index bd7117f7..7c66f309 100644 --- a/Sources/Hedera/Transaction.swift +++ b/Sources/Hedera/Transaction.swift @@ -65,28 +65,9 @@ public class Transaction { inner } - // MARK: - Public API - - public convenience init?(bytes: Data) { - guard let tx = try? Proto_Transaction.init(serializedData: bytes) else { return nil } - guard (try? Proto_TransactionBody.init(serializedData: tx.bodyBytes)) != nil else { return nil } - self.init(tx) - } - - public var bytes: Bytes { - Bytes(inner.bodyBytes) - } - - @discardableResult - public func sign(with key: Ed25519PrivateKey) -> Self { - let sig = key.sign(message: Bytes(inner.bodyBytes)) - - return addSigPair(publicKey: key.publicKey, signature: sig) - } - /// Add an Ed25519 signature pair to the signature map @discardableResult - public func addSigPair(publicKey: Ed25519PublicKey, signature: Bytes) -> Self { + func addSigPair(publicKey: Ed25519PublicKey, signature: Bytes) -> Self { var sigPair = Proto_SignaturePair() sigPair.pubKeyPrefix = Data(publicKey.bytes) sigPair.ed25519 = Data(signature) @@ -98,7 +79,7 @@ public class Transaction { /// Add an Ed25519 signature pair to the signature map @discardableResult - public func addSigPair(publicKey: Ed25519PublicKey, signer: Signer) -> Self { + func addSigPair(publicKey: Ed25519PublicKey, signer: Signer) -> Self { var sigPair = Proto_SignaturePair() sigPair.pubKeyPrefix = Data(publicKey.bytes) sigPair.ed25519 = Data(signer(Bytes(inner.bodyBytes))) @@ -108,51 +89,91 @@ public class Transaction { return self } - public func execute(client: Client) -> Result { - do { - return try executeAsync(client: client).wait() - } catch { - return .failure(HederaError.message("RPC error: \(error)")) + func signWithOperator(client: Client) { + guard let oper = client.operator else { return } + + addSigPair(publicKey: oper.publicKey, signer: oper.signer) + } + + func innerExecute( + eventLoop: EventLoop, + client: Client, + node: Node, + startTime: Date, + attempt: UInt8 + ) -> EventLoopFuture { + guard let delay = Backoff.getDelay(startTime: startTime, attempt: attempt) else { + return eventLoop.makeFailedFuture(HederaError.timedOut) + } + + let delayPromise = eventLoop.makePromise(of: Void.self) + + eventLoop.scheduleTask(in: delay) { + delayPromise.succeed(()) + } + + return delayPromise.futureResult.flatMap { + self.methodForTransaction(client.grpcClient(for: node))(self.inner, nil).response + }.flatMap { resp in + let code = resp.nodeTransactionPrecheckCode + + if code == .busy { + return self.innerExecute(eventLoop: eventLoop, client: client, node: node, startTime: startTime, attempt: attempt + 1) + } + + switch resultFromCode(code, success: { self.transactionId }) { + case .success(let res): + return eventLoop.makeSucceededFuture(res) + case .failure(let err): + return eventLoop.makeFailedFuture(err) + } } } - public func executeAsync(client: Client) -> EventLoopFuture> { + // MARK: - Public API + + public convenience init?(bytes: Data) { + guard let tx = try? Proto_Transaction.init(serializedData: bytes) else { return nil } + guard (try? Proto_TransactionBody.init(serializedData: tx.bodyBytes)) != nil else { return nil } + self.init(tx) + } + + public var bytes: Bytes { + Bytes(inner.bodyBytes) + } + + @discardableResult + public func sign(with key: Ed25519PrivateKey) -> Self { + let sig = key.sign(message: Bytes(inner.bodyBytes)) + + return addSigPair(publicKey: key.publicKey, signature: sig) + } + + @discardableResult + public func signWith(publicKey: Ed25519PublicKey, signer: Signer) -> Self { + addSigPair(publicKey: publicKey, signer: signer) + } + + public func execute(client: Client) -> EventLoopFuture { guard let node = client.network[nodeId] else { return client.eventLoopGroup .next() .makeFailedFuture(HederaError.message("node ID for transaction not found in Client")) } + let eventLoop = client.eventLoopGroup.next() + + return self.methodForTransaction(client.grpcClient(for: node))(self.inner, nil).response.flatMap { resp in + let code = resp.nodeTransactionPrecheckCode + + if code == .busy { + return self.innerExecute(eventLoop: eventLoop, client: client, node: node, startTime: Date(), attempt: 0) + } - return client.eventLoopGroup.next().submit { - let startTime = Date() - var attempt: UInt8 = 0 - - var delay = Backoff.initialDelay - while true { - // client.eventLoopGroup.next().scheduleTask(in: delay, task: () throws -> T) - attempt += 1 - - let response = Result { - try self.methodForTransaction(client.grpcClient(for: node))(self.inner, nil).response.wait() - } - switch response { - case .success(let response): - switch response.nodeTransactionPrecheckCode { - case .busy: - // stop trying if the delay will put us over `validDuration` - guard let delayUs = Backoff.getDelayUs(startTime: startTime, attempt: attempt) else { - return .failure(HederaError.message("execute timed out")) - } - - usleep(delayUs) - case .ok: - return .success(self.transactionId) - default: - return .failure(HederaError.message("preCheckCode was not OK: \(response.nodeTransactionPrecheckCode)")) - } - case .failure(let error): - return .failure(HederaError.message("\(error)")) - } + switch resultFromCode(code, success: { self.transactionId }) { + case .success(let res): + return eventLoop.makeSucceededFuture(res) + case .failure(let err): + return eventLoop.makeFailedFuture(err) } } } diff --git a/Sources/Hedera/TransactionBuilder.swift b/Sources/Hedera/TransactionBuilder.swift index 35f6f1cf..a6ac6a3b 100644 --- a/Sources/Hedera/TransactionBuilder.swift +++ b/Sources/Hedera/TransactionBuilder.swift @@ -63,13 +63,12 @@ public class TransactionBuilder { } var tx = Proto_Transaction() - tx.body = body tx.bodyBytes = try! body.serializedData() return Transaction(tx) } - public func execute(client: Client) -> EventLoopFuture> { - build(client: client).executeAsync(client: client) + public func execute(client: Client) -> EventLoopFuture { + build(client: client).execute(client: client) } } diff --git a/Tests/HederaTests/account/AccountCreateTransactionTests.swift b/Tests/HederaTests/account/AccountCreateTransactionTests.swift index 5a203864..a4463e88 100644 --- a/Tests/HederaTests/account/AccountCreateTransactionTests.swift +++ b/Tests/HederaTests/account/AccountCreateTransactionTests.swift @@ -12,7 +12,7 @@ final class AccountCreateTransactionTests: XCTestCase { let date = Date(timeIntervalSince1970: 1554158542) let key = Ed25519PrivateKey("302e020100300506032b6570042204203b054fade7a2b0869c6bd4a63b7017cbae7855d12acc357bea718e2c3e805962")! let tx = AccountCreateTransaction() - .setNodeAccount(AccountId(3)) + .setNodeAccountId(AccountId(3)) .setTransactionId(TransactionId(account: AccountId(2), validStart: date)) .setKey(key.publicKey) .setInitialBalance(450) diff --git a/Tests/HederaTests/crypto/ed25519/Ed25519PrivateKeyTests.swift b/Tests/HederaTests/crypto/ed25519/Ed25519PrivateKeyTests.swift index fe7ab528..88a90ca9 100644 --- a/Tests/HederaTests/crypto/ed25519/Ed25519PrivateKeyTests.swift +++ b/Tests/HederaTests/crypto/ed25519/Ed25519PrivateKeyTests.swift @@ -15,7 +15,7 @@ let signature = "73bea53f31ca9c42a422ecb7516ec08d0bbd1a6bfd630ccf10ec1872454814d final class Ed25519PrivateKeyTests: XCTestCase { func testGenerate() { - XCTAssertNoThrow(Ed25519PrivateKey()) + XCTAssertNotNil(Ed25519PrivateKey.generate()) } func testFromBytes() {