Skip to content

Commit

Permalink
fix: chunked transactions
Browse files Browse the repository at this point in the history
Signed-off-by: Ricky Saechao <[email protected]>
  • Loading branch information
RickyLB committed Feb 13, 2024
1 parent f47f427 commit 388ef5c
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 40 deletions.
18 changes: 14 additions & 4 deletions Sources/Hedera/ChunkedTransaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ extension ChunkedTransaction.FirstChunkView: Execute {
internal var nodeAccountIds: [AccountId]? { transaction.nodeAccountIds }
internal var explicitTransactionId: TransactionId? { transaction.transactionId }
internal var requiresTransactionId: Bool { true }
internal var firstTransactionId: TransactionId? { nil }
internal var index: Int? { nil }

internal var operatorAccountId: AccountId? {
self.transaction.operatorAccountId
Expand Down Expand Up @@ -247,6 +249,14 @@ extension ChunkedTransaction.ChunkView: Execute {
internal var explicitTransactionId: TransactionId? { nil }
internal var requiresTransactionId: Bool { true }

internal var firstTransactionId: TransactionId? {
return initialTransactionId
}

internal var index: Int? {
return currentChunk
}

internal var operatorAccountId: AccountId? {
self.transaction.operatorAccountId
}
Expand All @@ -260,16 +270,16 @@ extension ChunkedTransaction.ChunkView: Execute {
) {
assert(transaction.isFrozen)

guard let transactionId = transactionId else {
throw HError.noPayerAccountOrTransactionId
}
let currentTransactionId = transactionId ?? TransactionId(
accountId: initialTransactionId.accountId,
validStart: initialTransactionId.validStart.adding(nanos: UInt64(currentChunk)))

return transaction.makeRequestInner(
chunkInfo: .init(
current: currentChunk,
total: totalChunks,
initialTransactionId: initialTransactionId,
currentTransactionId: transactionId,
currentTransactionId: currentTransactionId,
nodeAccountId: nodeAccountId
)
)
Expand Down
27 changes: 23 additions & 4 deletions Sources/Hedera/Execute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ internal protocol Execute {
/// Whether or not the transaction ID should be refreshed if a ``Status/transactionExpired`` occurs.
var regenerateTransactionId: Bool? { get }

/// The initial transaction Id value (for chunked transaction)
/// Note: Used for multi-chunked transactions
var firstTransactionId: TransactionId? { get }

/// The index of each transactions
/// Note: Used for multi-chunked transactions
var index: Int? { get }

/// Check whether to retry for a given pre-check status.
func shouldRetryPrecheck(forStatus status: Status) -> Bool

Expand Down Expand Up @@ -91,7 +99,9 @@ private struct ExecuteContext {
fileprivate let grpcTimeout: Duration?
}

internal func executeAny<E: Execute & ValidateChecksums>(_ client: Client, _ executable: E, _ timeout: TimeInterval?)
internal func executeAny<E: Execute & ValidateChecksums>(
_ client: Client, _ executable: E, _ timeout: TimeInterval?
)
async throws -> E.Response
{
let timeout = timeout ?? LegacyExponentialBackoff.defaultMaxElapsedTime
Expand All @@ -101,13 +111,17 @@ internal func executeAny<E: Execute & ValidateChecksums>(_ client: Client, _ exe
}

let operatorAccountId: AccountId?

// Where the operatorAccountId is set.
// Determines if transactionId regeneration is disabled.
do {
if executable.explicitTransactionId != nil
|| !(executable.regenerateTransactionId ?? client.defaultRegenerateTransactionId)
{
operatorAccountId = nil
} else {
operatorAccountId = executable.operatorAccountId ?? client.operator?.accountId
operatorAccountId =
executable.firstTransactionId?.accountId ?? executable.operatorAccountId ?? client.operator?.accountId
}
}

Expand Down Expand Up @@ -141,15 +155,19 @@ internal func executeAny<E: Execute & ValidateChecksums>(_ client: Client, _ exe
executable: executable)
}

private func executeAnyInner<E: Execute>(ctx: ExecuteContext, executable: E) async throws -> E.Response {
private func executeAnyInner<E: Execute>(
ctx: ExecuteContext, executable: E
) async throws -> E.Response {
let explicitTransactionId = executable.explicitTransactionId

var backoff = ctx.backoffConfig
var lastError: HError?

var transactionId =
executable.requiresTransactionId
? (explicitTransactionId ?? executable.operatorAccountId.map(TransactionId.generateFrom)
? (explicitTransactionId ?? TransactionId.generateFromInitial(executable.firstTransactionId, executable.index)
?? executable
.operatorAccountId.map(TransactionId.generateFrom)
?? ctx.operatorAccountId.map(TransactionId.generateFrom)) : nil

let explicitNodeIndexes = try executable.nodeAccountIds.map(ctx.network.nodeIndexesForIds)
Expand Down Expand Up @@ -216,6 +234,7 @@ private func executeAnyInner<E: Execute>(ctx: ExecuteContext, executable: E) asy
&& ctx.operatorAccountId != nil:
// the transaction that was generated has since expired
// re-generate the transaction ID and try again, immediately

lastError = executable.makeErrorPrecheck(precheckStatus, transactionId)
transactionId =
executable.operatorAccountId.map(TransactionId.generateFrom)
Expand Down
8 changes: 8 additions & 0 deletions Sources/Hedera/PingQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ extension PingQuery: Execute {

internal var requiresTransactionId: Bool { false }

internal var firstTransactionId: TransactionId? {
nil
}

internal var index: Int? {
nil
}

internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (Proto_Query, ()) {
let header = Proto_QueryHeader.with { $0.responseType = .answerOnly }

Expand Down
8 changes: 8 additions & 0 deletions Sources/Hedera/Query.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ extension Query: Execute {
self.payment.regenerateTransactionId
}

internal var firstTransactionId: TransactionId? {
payment.firstTransactionId
}

internal var index: Int? {
payment.index
}

internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (Proto_Query, ()) {
let request = toQueryProtobufWith(
try .with { proto in
Expand Down
8 changes: 8 additions & 0 deletions Sources/Hedera/QueryCost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ extension QueryCost: Execute {
false
}

internal var firstTransactionId: TransactionId? {
nil
}

internal var index: Int? {
nil
}

internal var requiresTransactionId: Bool { false }

internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (Proto_Query, ()) {
Expand Down
15 changes: 5 additions & 10 deletions Sources/Hedera/Transaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,6 @@ public class Transaction: ValidateChecksums {
return self
}

guard let transactionId = self.transactionId ?? client?.operator?.generateTransactionId() else {
throw HError(
kind: .noPayerAccountOrTransactionId,
description: "transaction frozen without client or explicit transaction ID")
}

guard let nodeAccountIds = self.nodeAccountIds ?? client?.net.randomNodeIds() else {
throw HError(
kind: .freezeUnsetNodeAccountIds, description: "transaction frozen without client or explicit node IDs")
Expand All @@ -327,15 +321,12 @@ public class Transaction: ValidateChecksums {

let `operator` = client?.operator

self.transactionId = transactionId
self.nodeAccountIds = nodeAccountIds
self.maxTransactionFee = maxTransactionFee
self.`operator` = `operator`

isFrozen = true

self.sources = try TransactionSources.init(transactions: try self.makeTransactionList())

if client?.isAutoValidateChecksumsEnabled() == true {
try validateChecksums(on: client!)
}
Expand Down Expand Up @@ -489,6 +480,7 @@ extension Transaction {

extension Transaction {
fileprivate func makeTransactionList() throws -> [Proto_Transaction] {
assert(self.isFrozen)
guard let initialTransactionId = self.transactionId ?? self.operator?.generateTransactionId() else {
throw HError.noPayerAccountOrTransactionId
}
Expand Down Expand Up @@ -530,7 +522,6 @@ extension Transaction {

internal func makeRequestInner(chunkInfo: ChunkInfo) -> (Proto_Transaction, TransactionHash) {
assert(self.isFrozen)

let body: Proto_TransactionBody = self.toTransactionBodyProtobuf(chunkInfo)

// swiftlint:disable:next force_try
Expand Down Expand Up @@ -593,6 +584,10 @@ extension Transaction: Execute {

internal var requiresTransactionId: Bool { true }

internal var firstTransactionId: TransactionId? { nil }

internal var index: Int? { nil }

internal var operatorAccountId: AccountId? {
self.operator?.accountId
}
Expand Down
10 changes: 9 additions & 1 deletion Sources/Hedera/Transaction/TransactionSources.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ internal struct TransactionSources: Sendable {
(0..<chunksCount).lazy.map { SourceChunk(map: self.guts, index: $0) }
}
}

extension TransactionSources {
// this is every bit as insane as the rust method I ported it from :/
// swiftlint:disable:next function_body_length
Expand Down Expand Up @@ -239,6 +238,7 @@ extension TransactionSources {

for chunk in chunks {
let transactionId = transactionInfo[chunk.startIndex].transactionId

guard !transactionIdsTmp.contains(transactionId) else {
throw HError.fromProtobuf("duplicate transaction ID between chunked transaction chunks")
}
Expand Down Expand Up @@ -420,6 +420,14 @@ extension SourceTransactionExecuteView: Execute {
nil
}

internal var firstTransactionId: TransactionId? {
nil
}

internal var index: Int? {
nil
}

internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (
GrpcRequest, Context
) {
Expand Down
15 changes: 15 additions & 0 deletions Sources/Hedera/TransactionId.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ public struct TransactionId: Sendable, Equatable, Hashable, ExpressibleByStringL
return Self(accountId: accountId, validStart: validStart)
}

/// Generates a new transaction ID for chunks.
internal static func generateFromInitial(_ initialTransactionId: TransactionId?, _ index: Int?) -> Self? {
guard let initialTransactionId = initialTransactionId else {
return nil
}

guard let index = index else {
return nil
}

return TransactionId(
accountId: initialTransactionId.accountId,
validStart: initialTransactionId.validStart.adding(nanos: UInt64(index)))
}

/// Creates a new transaction Id with the given account id and valid start.
public static func withValidStart(_ accountId: AccountId, _ validStart: Timestamp) -> Self {
Self(accountId: accountId, validStart: validStart)
Expand Down
31 changes: 10 additions & 21 deletions Tests/HederaE2ETests/Topic/TopicMessageSubmit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,35 +152,24 @@ internal class TopicMessageSubmit: XCTestCase {

let id = try XCTUnwrap(receipt.accountId)

let userClient = Client.forTestnet().setOperator(id, key)
let userClient = try Client.forNetwork(testEnv.client.network).setOperator(id, key)

// Set payer account
let payerAccountId = testEnv.operator.accountId
let payerClient = testEnv.client

// Transaction attributes
let topicId = topic.id // Previously created HCS topic
let message = "12" // 2 bytes message
let chunkSize = 1 // 1 byte chunk size
let transactionId = TransactionId.generateFrom(payerAccountId) // custom transactionId
let transactionId = TransactionId.generateFrom(payerAccountId)

// Transaction creation
let transaction = TopicMessageSubmitTransaction()
let transaction = try await TopicMessageSubmitTransaction()
.transactionId(transactionId)
.topicId(topicId)
.message(message.data(using: .utf8)!)
.chunkSize(chunkSize)

// Transaction signature and execution
let frozenTx = try transaction.freezeWith(userClient)

let signedTx = try frozenTx.signWithOperator(userClient)

let doubleSignedTx = try signedTx.signWithOperator(payerClient)

let txResponses = try await doubleSignedTx.executeAll(payerClient)
.topicId(topic.id)
.message("12".data(using: .utf8)!)
.chunkSize(1)
.freezeWith(userClient)
.signWithOperator(payerClient)
.executeAll(payerClient)

// Compare the transaction payer accounts with each other
XCTAssertEqual(txResponses[0].transactionId.accountId, txResponses[1].transactionId.accountId)
XCTAssertEqual(transaction[0].transactionId.accountId, transaction[1].transactionId.accountId)
}
}

0 comments on commit 388ef5c

Please sign in to comment.