Epoch Bounds
Every transaction carries a mandatory max_epoch: the last epoch in which it may be sequenced. A transaction’s death is therefore deterministic — an attempt that aborts cannot be retried indefinitely, and a stuck transaction stops being a resubmittable intent once its window closes.
maxEpoch is a constructor argument
Section titled “maxEpoch is a constructor argument”TransactionBuilder.new(network, maxEpoch) takes it up front:
import { TransactionBuilder, resolveMaxEpoch } from "@tari-project/ootle";
const builder = TransactionBuilder.new(provider.network(), await resolveMaxEpoch(provider));resolveMaxEpoch(provider, leadEpochs?) reads the chain tip via provider.getCurrentEpoch() and returns currentEpoch + leadEpochs, defaulting to DEFAULT_TRANSACTION_VALIDITY_EPOCHS (10). Pass an explicit number when you want a different window:
const maxEpoch = await resolveMaxEpoch(provider, 50);The network caps the window at MAX_TRANSACTION_VALIDITY_EPOCHS (2160 epochs, roughly 30 days) past the current epoch. A transaction reaching further ahead is aborted with ValidityWindowTooLong.
withMinEpoch
Section titled “withMinEpoch”The transaction is only valid starting from this epoch. Unlike max_epoch, it is optional and defaults to unset:
builder.withMinEpoch(100);withMaxEpoch
Section titled “withMaxEpoch”Overrides the window set at construction:
builder.withMaxEpoch(200);Combining both
Section titled “Combining both”const unsignedTx = TransactionBuilder.new(Network.Esmeralda, await resolveMaxEpoch(provider)) .feeTransactionPayFromComponent(accountAddress, 1000n) .callMethod({ componentAddress: accountAddress, methodName: "withdraw" }, [ { Literal: resourceAddress }, { Literal: "100" }, ]) .saveVar("bucket") .callMethod({ componentAddress: recipientAddress, methodName: "deposit" }, [{ Workspace: "bucket" }]) .withMinEpoch(100) .withMaxEpoch(200) .buildUnsignedTransaction();If the network’s current epoch is outside the specified range, the transaction will be rejected.
The transaction id excludes the seal signature’s witness data, so two identical bodies sealed by the same key are the same transaction — submitting both executes once. When each submission must execute independently, stamp a distinct nonce per intent:
builder.withNonce(1);It defaults to 0.