Building Offline
This page demonstrates offline transaction building and signing on IOTA. You can switch between CLI-based and TypeScript SDK-based workflows below.
- CLI
- TypeScript SDK
Offline Signing (CLI)
IOTA supports offline signing, which is signing transactions using a device not connected to an IOTA network, or in a wallet implemented in a different programming language without relying on the IOTA key store. The steps to implement offline signing include:
- Serialize the data for signing.
- Sign the serialized data. Put the serialized data in a location to sign (such as the wallet of your choice, or tools in other programming languages) and to produce a signature with the corresponding public key.
- Execute the signed transaction.
Serialize data for a transfer
You must serialize transaction data following Binary Canonical Serialization (BCS). It is supported in other languages.
The following example demonstrates how to serialize data for a transfer using the IOTA CLI. This returns serialized transaction data in Base64. Submit the raw transaction to execute as tx_bytes.
iota client pay-all-iota --input-coins <COIN-OBJECT-ID> --recipient <IOTA-ADDRESS> --gas-budget 2000000 --serialize-unsigned-transaction
The console responds with the resulting <TX_BYTES> value.
All other CLI commands that craft a transaction (such as iota client publish and iota client call) also accept the --serialize-unsigned-transaction flag used in the same way.
Decoding before signing (recommended)
Before signing an offline transaction, you can decode and inspect the unsigned transaction bytes to verify its contents.
This allows you to review:
- the sender address
- gas configuration
- inputs and commands
- recipient and amounts
This step is especially useful for:
- offline or hardware wallet workflows
- auditing transactions before signing
- debugging incorrect inputs
Use the following command to decode an unsigned transaction:
iota keytool decode-or-verify-tx --tx-bytes <UNSIGNED_TX_BYTES>
Decodes Base64 encoded transaction bytes and, if a signature is provided, verifies it against the transaction.
Sign the serialized data
You can sign the data using the device and programming language you choose. IOTA accepts signatures for pure Ed25519, ECDSA secp256k1, ECDSA secp256r1 and native multisig. To learn more about the requirements of the signatures, see IOTA Signatures.
This example uses the iota keytool command to sign, using the Ed25519 key corresponding to the provided address stored in iota.keystore. This command outputs the signature, the public key, and the flag encoded in Base64. This command is backed by fastcrypto.
iota keytool sign --address <IOTA-ADDRESS> --data <TX_BYTES>
You receive the following response:
Signer address: <IOTA-ADDRESS>
Raw tx_bytes to execute: <TX_BYTES>
Intent: Intent { scope: TransactionData, version: V0, app_id: IOTA }
Raw intent message: <INTENT-MESSAGE>
Digest to sign: <DIGEST>
Serialized signature (`flag || sig || pk` in Base64): <SERIALIZED-SIGNATURE>
To ensure the signature produced offline matches with IOTA validity rules for testing purposes, you can import the mnemonics to iota.keystore using iota keytool import. You can then sign with it using iota keytool sign and then compare the signature results. Additionally, you can find test vectors in ~/iota/sdk/typescript/test/e2e/raw-signer.test.ts.
To verify a signature against the cryptography library backing IOTA when debugging, see sigs-cli.
Execute the signed transaction
After you obtain the serialized signature, you can submit it using the execution transaction command. This command takes --tx-bytes as the raw transaction bytes to execute (see output of the previous iota client transfer command) and the serialized signature (Base64 encoded flag || sig || pk, see output of iota keytool sign). This executes the signed transaction and returns the certificate and transaction effects if successful.
iota client execute-signed-tx --tx-bytes <TX_BYTES> --signatures <SERIALIZED-SIGNATURE>
You get the following response:
----- Certificate ----
Transaction Hash: <TRANSACTION-ID>
Transaction Signature: <SIGNATURE>
Signed Authorities Bitmap: RoaringBitmap<[0, 1, 3]>
Transaction Kind : Transfer IOTA
Recipient : <IOTA-ADDRESS>
Amount: Full Balance
----- Transaction Effects ----
Status : Success
Mutated Objects:
- ID: <OBJECT_ID> , Owner: Account Address ( <IOTA-ADDRESS> )
Alternative: Sign with IOTA Keystore and Execute Transaction
Alternatively, you can use the active key in IOTA Keystore to sign and output a Base64-encoded sender signed data with flag --serialize-signed-transaction.
iota client pay-all-iota --input-coins <COIN-OBJECT-ID> --recipient <IOTA-ADDRESS> --gas-budget 2000000 --serialize-signed-transaction
The console responds with the resulting <SIGNED-TX-BYTES> value.
After you obtain the signed transaction bytes, you can submit it using the execute-combined-signed-tx command. This command takes --signed-tx-bytes as the signed transaction bytes to execute (see output of the previous iota client transfer command). This executes the signed transaction and returns the certificate and transaction effects if successful.
iota client execute-combined-signed-tx --signed-tx-bytes <SIGNED-TX-BYTES>
Offline Signing (TypeScript SDK)
This section demonstrates how to perform offline transaction building and signing using the IOTA TypeScript SDK, mirroring the same steps shown in the CLI tab.
The workflow is intentionally split into offline and online phases to support:
- offline-only signing
- hardware wallets
- external signers
- deterministic transaction inspection before submission
Overview of the flow
- The TypeScript SDK offline-signing flow consists of the following steps:
- Derive the address from a private key (offline)
- Fetch input object references required for execution (online)
- Build the unsigned transaction bytes (offline)
- Decode and inspect the transaction before signing (offline, optional)
- Sign the transaction bytes (offline)
- Submit the signed transaction to the network (online, optional)
Each step is shown explicitly in the example below.
Here is a complete example of offline signing using the TypeScript SDK:
import { IotaClient, getFullnodeUrl } from '@iota/iota-sdk/client';
import {
decodeIotaPrivateKey,
parseSerializedSignature,
} from '@iota/iota-sdk/cryptography';
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
import { Transaction } from '@iota/iota-sdk/transactions';
// ================= CONFIG =================
const RPC = getFullnodeUrl('testnet'); // devnet | localnet | mainnet
const ONLINE = true; // set to false to skip online submission
const IOTA_BECH32_PRIV = 'iotaprivkey1....';
const AMOUNT = 100; // nanos
const RECIPIENT_OVERRIDE: string | null = null;
// ==========================================
async function main() {
// ---------- OFFLINE: Keypair & address ----------
const decoded = decodeIotaPrivateKey(IOTA_BECH32_PRIV);
const keypair = Ed25519Keypair.fromSecretKey(decoded.secretKey);
const senderAddress = keypair.getPublicKey().toIotaAddress();
const recipient = RECIPIENT_OVERRIDE ?? senderAddress;
console.log('Sender:', senderAddress);
console.log('Recipient:', recipient);
// ---------- ONLINE: client ----------
const client = new IotaClient({ url: RPC });
// ---------- ONLINE : fetch ALL owned objects ----------
const ownedRes = await client.getOwnedObjects({
owner: senderAddress,
options: { showType: true },
});
const owned = ownedRes.data ?? [];
if (owned.length === 0) {
console.warn('No owned objects found. Fund address via faucet.');
return;
}
// ---------- Try extracting gas coins from owned objects ----------
let gasObjects = owned
.filter(
(o) =>
o.data &&
o.data.type === '0x2::coin::Coin<0x2::iota::IOTA>',
)
.map((o) => ({
objectId: o.data!.objectId,
version: Number(o.data!.version),
digest: o.data!.digest,
}));
// ---------- Fallback: canonical gas coin API ----------
if (gasObjects.length === 0) {
console.warn(
'No gas coins found via getOwnedObjects(). Falling back to getCoins().',
);
const coinsRes = await client.getCoins({
owner: senderAddress,
});
if (coinsRes.data.length === 0) {
throw new Error('No gas coins available. Fund address via faucet.');
}
gasObjects = coinsRes.data.map((coin) => ({
objectId: coin.coinObjectId,
version: Number(coin.version),
digest: coin.digest,
}));
}
console.log('Gas coins (sample):', gasObjects.slice(0, 3));
// ---------- OFFLINE: build transaction ----------
const tx = new Transaction();
// Split gas coin to create transfer amount
const [coin] = tx.splitCoins(tx.gas, [AMOUNT]);
tx.transferObjects([coin], recipient);
// Fully define gas & sender for offline build
tx.setGasPayment(gasObjects);
tx.setGasBudget(3_000_000);
tx.setGasPrice(1_000);
tx.setSender(senderAddress);
// Build unsigned BCS transaction bytes
const unsignedTxBytes = await tx.build();
console.log('Unsigned tx bytes length:', unsignedTxBytes.length);
// ---------- decode unsigned tx ----------
const decodedTx = Transaction.from(unsignedTxBytes);
console.log(
'Decoded unsigned tx:',
JSON.stringify(decodedTx.getData(), null, 2),
);
// ---------- OFFLINE: sign transaction ----------
const { signature } = await keypair.signTransaction(unsignedTxBytes);
console.log('Serialized signature (base64):', signature);
// ---------- inspect signature ----------
const parsed = parseSerializedSignature(signature);
console.log('Parsed signature:', {
scheme: parsed.signatureScheme,
signatureBytes: parsed.signature?.length,
publicKeyBytes: parsed.publicKey?.length,
});
// ---------- ONLINE: submit signed transaction ----------
if (ONLINE) {
const execResult = await client.executeTransactionBlock({
transactionBlock: unsignedTxBytes,
signature,
requestType: 'WaitForLocalExecution',
options: {
showEffects: true,
showInput: true,
},
});
console.log('Transaction digest:', execResult.digest);
await client.waitForTransaction({
digest: execResult.digest,
options: { showEffects: true },
});
}
console.log('Done.');
}
main().catch((e) => {
console.error('Error:', e);
process.exit(1);
});
Explanation of key concepts
Fully-resolved object references
When building a transaction offline, the SDK cannot query the network to resolve inputs.
For this reason, every object used by the transaction must be specified using:
objectIdversiondigest
These values are retrieved online (for example via getCoins()), then passed into the transaction before calling build().
Gas configuration in offline mode
Gas estimation is not performed automatically when building transactions offline.
You must explicitly provide:
setGasPayment(...)setGasBudget(...)setGasPrice(...)
If the gas budget is too low, the transaction will execute and fail with InsufficientGas, still incurring gas costs.
When building offline, it is recommended to over-provision the gas budget.
Decoding before signing (recommended)
Calling:
Transaction.from(unsignedTxBytes)
allows you to inspect the transaction contents in a human-readable form before signing. This is especially useful for:
- hardware wallet review
- audits
- external signers
- debugging incorrect inputs
This step mirrors the CLI’s --serialize-unsigned-transaction inspection flow.
Offline transaction signing in IOTA follows the same core process in both the CLI and the TypeScript SDK: serialize the transaction data, sign it with intent awareness, and submit the signed transaction to the network. Both tools produce identical, protocol-level artifacts using the same BCS format and signature structure, ensuring full interoperability. The CLI is well suited for manual workflows and operational tasks, while the TypeScript SDK is ideal for applications, wallets, and services that require programmatic control and offline-only signing flows. Regardless of the tooling used, transactions provide the same security guarantees and execution semantics on the IOTA network.