Skip to content

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.

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.

The transaction is only valid starting from this epoch. Unlike max_epoch, it is optional and defaults to unset:

builder.withMinEpoch(100);

Overrides the window set at construction:

builder.withMaxEpoch(200);
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.