Skip to content

Commit

Permalink
Future-atize execute on Transaction and fix minor bug in similar code…
Browse files Browse the repository at this point in the history
… for QueryBuilder
  • Loading branch information
qtbeee committed Jan 11, 2020
1 parent 7616c19 commit 8cf41ad
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 89 deletions.
20 changes: 10 additions & 10 deletions Sources/Hedera/HederaError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ public struct HederaNetworkError: Error {
}

func resultFromCode<T>(
_ code: Proto_ResponseCodeEnum,
success: T,
_ code: Proto_ResponseCodeEnum,
success: () -> T,
allowUnknown: Bool = false
) -> Result<T, HederaError> {
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))
}
}
89 changes: 72 additions & 17 deletions Sources/Hedera/QueryBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,33 @@ public class QueryBuilder<R> {
.toProto()
}

let req = getQueryMethod(client.grpcClient(for: node))(body, nil)
let resp = req.response
let eventLoop = client.eventLoopGroup.next()

return resp.flatMapResult { (response) -> Result<UInt64, HederaError> 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
Expand Down Expand Up @@ -195,12 +213,43 @@ public class QueryBuilder<R> {

// 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<R> {

func innerExecute<T>(
eventLoop: EventLoop,
client: Client,
node: Node,
startTime: Date,
attempt: UInt8,
successValueMapper: @escaping (Proto_Response) -> T
) -> EventLoopFuture<T> {
guard let delay = Backoff.getDelay(startTime: startTime, attempt: attempt) else {
return eventLoop.makeFailedFuture(HederaError.timedOut)
}
Expand All @@ -221,15 +270,21 @@ public class QueryBuilder<R> {

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)
}
}
}
Expand Down
135 changes: 78 additions & 57 deletions Sources/Hedera/Transaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)))
Expand All @@ -108,51 +89,91 @@ public class Transaction {
return self
}

public func execute(client: Client) -> Result<TransactionId, HederaError> {
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<TransactionId> {
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<Result<TransactionId, HederaError>> {
// 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<TransactionId> {
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)
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions Sources/Hedera/TransactionBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result<TransactionId, HederaError>> {
build(client: client).executeAsync(client: client)
public func execute(client: Client) -> EventLoopFuture<TransactionId> {
build(client: client).execute(client: client)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let signature = "73bea53f31ca9c42a422ecb7516ec08d0bbd1a6bfd630ccf10ec1872454814d

final class Ed25519PrivateKeyTests: XCTestCase {
func testGenerate() {
XCTAssertNoThrow(Ed25519PrivateKey())
XCTAssertNotNil(Ed25519PrivateKey.generate())
}

func testFromBytes() {
Expand Down

0 comments on commit 8cf41ad

Please sign in to comment.