Skip to main content

Overview

The optimized approach uses:
  • ABI (Application Binary Interface): Pre-loaded function signatures and parameter types.
  • Replay Protection Nonce: A random 64-bit value embedded in the transaction payload to enable orderless transactions.
  • Chain ID: The network identifier to prevent cross-chain replay attacks.

Orderless Transactions

These transactions are orderless, meaning they can be submitted in any order without requiring sequential sequence numbers. The replayProtectionNonce is embedded directly in the transaction payload to provide replay protection, eliminating the need to fetch the account’s current sequence number from the network. Benefits of orderless transactions:
  • No sequence number fetch: Avoids network round-trip to get account sequence number
  • Parallel execution: Multiple transactions can be built and submitted simultaneously
  • Better performance: Faster transaction construction without waiting for network responses
  • Improved UX: Transactions can be prepared offline and submitted later
Traditional (non-orderless) transactions:
  • Require fetching the account’s current sequence number from the network
  • Must be submitted sequentially (each transaction increments the sequence number)
  • Cannot be built in parallel without coordination
  • Require network calls during transaction construction

Generate Replay Protection Nonce

The replay protection nonce is a random 64-bit value that enables orderless transactions. Instead of using the account’s sequence number (which requires a network fetch), the nonce is embedded in the transaction payload to prevent replay attacks. Generate a random 64-bit nonce for replay protection:
function generateRandomReplayProtectionNonce(): bigint | null {
  const buf = new Uint32Array(2);
  crypto.getRandomValues(buf);

const valueAtIndex0 = buf[0];
const valueAtIndex1 = buf[1];

if (!valueAtIndex0 || !valueAtIndex1) return null;

// Combine two 32-bit parts into a single 64-bit bigint
return (BigInt(valueAtIndex0) << BigInt(32)) | BigInt(valueAtIndex1);
}


Parse ABI to EntryFunctionABI

Before building a transaction, parse the MoveFunction ABI to the EntryFunctionABI format required for transaction payload construction.
const parseMoveFnAbiToEntryFnABI = (
  functionAbi: MoveFunction
): EntryFunctionABI => {
  // Remove the signer arguments
  const numSigners = findFirstNonSignerArg(functionAbi);

const params: TypeTag[] = [];
for (let i = numSigners; i < functionAbi.params.length; i += 1) {
const param = functionAbi.params[i];
if (!param) continue;
params.push(parseTypeTag(param, { allowGenerics: true }));
}

return {
signers: numSigners,
typeParameters: functionAbi.generic_type_params,
parameters: params,
};
};


Generate Expiration Timestamp

A convenience function to compute the expiration timestamp for the transaction.
const generateExpireTimestamp = (aptosConfig: AptosConfig) =>
  Math.floor(Date.now() / 1000) + aptosConfig.getDefaultTxnExpirySecFromNow();

Build Transaction Synchronously

This function builds a transaction payload and constructs a RawTransaction synchronously using all pre-known parameters.
import {
  AccountAddress,
  AccountAddressInput,
  AptosConfig,
  ChainId,
  convertPayloadToInnerPayload,
  EntryFunctionABI,
  findFirstNonSignerArg,
  generateTransactionPayloadWithABI,
  InputEntryFunctionData,
  InputEntryFunctionDataWithABI,
  MoveFunction,
  parseTypeTag,
  RawTransaction,
  SimpleTransaction,
  TypeTag,
} from "@aptos-labs/ts-sdk";

function buildSimpleTransactionSync(args: {
aptosConfig: AptosConfig;
sender: AccountAddressInput;
data: InputEntryFunctionData;
chainId: number;
gasUnitPrice: number;
abi: MoveFunction;
withFeePayer: boolean;
replayProtectionNonce: bigint;
}): SimpleTransaction {
const txnPayload = generateTransactionPayloadWithABI({
aptosConfig: args.aptosConfig,
function: args.data.function,
functionArguments: args.data.functionArguments,
typeArguments: args.data.typeArguments,
abi: parseMoveFnAbiToEntryFnABI(args.abi),
} as InputEntryFunctionDataWithABI);

const expireTimestamp = generateExpireTimestamp(args.aptosConfig);

const rawTxn = new RawTransaction(
AccountAddress.from(args.sender),
BigInt("0xdeadbeef"), // Default Sequence Number as it is unused when replay nonce is provided
convertPayloadToInnerPayload(txnPayload, args.replayProtectionNonce), // Convert payload and embed replay protection nonce
BigInt(args.aptosConfig.getDefaultMaxGasAmount()),
BigInt(args.gasUnitPrice),
BigInt(expireTimestamp),
new ChainId(args.chainId)
);

return new SimpleTransaction(
rawTxn,
args.withFeePayer ? AccountAddress.ZERO : undefined
);
}


Complete Example: Building a Transaction

Below is a complete example of building and submitting a transaction using the optimized (orderless, synchronous) approach.
// Example: Build and submit a transaction synchronously using ABI and replay nonce.
import {
  Aptos,
  AptosConfig,
  AccountAddress,
  SimpleTransaction,
  MoveFunction,
} from "@aptos-labs/ts-sdk";

const subaccountAddr = "0x..."; // Your subaccount address
const accountToDelegateTo = "0x..."; // Address to delegate trading to
const expirationTimestamp = undefined; // Optional: expiration timestamp in seconds

// Initialize Aptos config
const aptosConfig = new AptosConfig({
network: "mainnet",
fullnode: "https://fullnode.mainnet.aptoslabs.com",
});

const aptos = new Aptos(aptosConfig);

// ABI for delegate_trading_to function
const functionAbi: MoveFunction = {
name: "delegate_trading_to_for_subaccount",
visibility: "friend",
is_entry: true,
is_view: false,
generic_type_params: [],
params: ["&signer", "address", "address", "0x1::option::Option<u64>"],
return: [],
};

const replayProtectionNonce = generateRandomReplayProtectionNonce();

// Get gas price (from cache or network)
const gasUnitPrice = await aptos
.getGasPriceEstimation()
.then((r) => r.gas_estimate);

// Build transaction synchronously
const transaction = buildSimpleTransactionSync({
aptosConfig: aptos.config,
sender: account.accountAddress,
data: {
function: `${PACKAGE}::dex_accounts_entry::delegate_trading_to_for_subaccount`,
typeArguments: [],
functionArguments: [
subaccountAddr, // Subaccount address
accountToDelegateTo, // Address to delegate trading to
expirationTimestamp, // Optional expiration timestamp (can be undefined)
],
},
chainId: 1, // Mainnet chain ID
gasUnitPrice,
abi: functionAbi,
withFeePayer: false,
replayProtectionNonce,
});

// Sign and submit the transaction
const senderAuthenticator = aptos.transaction.sign({
signer: account,
transaction,
});

const pendingTransaction = await aptos.transaction.submit.simple({
transaction,
senderAuthenticator,
});

// Wait for transaction confirmation
const committedTransaction = await aptos.waitForTransaction({
transactionHash: pendingTransaction.hash,
});

console.log("Transaction confirmed:", committedTransaction.hash);


Benefits

  • Performance: No network calls during transaction construction
  • Reliability: Deterministic transaction building with pre-loaded ABI
  • Security: Replay protection nonce prevents transaction replay attacks
  • Efficiency: Can build multiple transactions in parallel without blocking

When to Use

Use the optimized synchronous approach when:
  • You have ABI data available locally
  • You know the chain ID
  • You want to build transactions without network latency
  • You’re building multiple transactions in batch

Fallback to Async Building

If ABI or chain ID is not available, the SDK falls back to async transaction building, which may require a network fetch to get the account’s sequence number for replay protection.
// Fallback: Build a transaction asynchronously when ABI or chainId are missing
// This will fetch the account's sequence number from the network.
const transaction = await aptos.transaction.build.simple({
  sender,
  data: payload,
  withFeePayer,
  options: {
    replayProtectionNonce, // Still uses nonce if provided, but may also fetch sequence number
  },
});
What happens without orderless transactions:
  • The SDK makes a network request to fetch the account’s current sequence number when you call aptos.transaction.build.simple()
  • The sequence number is used for replay protection instead of (or in addition to) the nonce
  • Transactions must be submitted sequentially
  • Each transaction increments the sequence number, preventing parallel submission