Transactions
client.transactions is the resource namespace for sending, minting, consuming, swapping, running custom scripts, and inspecting transaction history. The simplified operations below handle the full lifecycle — execute, prove, submit — in one call.
Simplified operations
send
Creates a pay-to-ID note that transfers tokens from one account to another.
import { MidenClient } from "@miden-sdk/miden-sdk";
const client = await MidenClient.createTestnet();
const { txId } = await client.transactions.send({
account: senderWallet,
to: recipientWallet,
token: faucet,
amount: 100n,
type: "private", // "public" or "private" — default "public"
reclaimAfter: 100, // optional: block number after which sender can reclaim
timelockUntil: 90, // optional: block number until which note is timelocked
});
console.log("Send tx:", txId.toHex());
Set returnNote: true to receive the created Note in the result (useful when you need the note body — for QR delivery, for example):
const { txId, note } = await client.transactions.send({
account: senderWallet,
to: recipientWallet,
token: faucet,
amount: 100n,
returnNote: true,
});
// `note` is guaranteed non-null when returnNote is true
reclaimAfter and timelockUntil are block numbers, not wall-clock times.
The returnNote: true branch is a separate overload: it does not accept reclaimAfter or timelockUntil. If you need either of those, use the default branch (without returnNote) and retrieve the note through client.notes.listSent() instead.
mint
const { txId } = await client.transactions.mint({
account: faucet, // the faucet account
to: wallet, // recipient
amount: 1000n,
type: "private", // optional, default "public"
});
console.log("Mint tx:", txId.toHex());
consume
// Consume one or more input notes
const { txId } = await client.transactions.consume({
account: wallet,
notes: [noteId1, noteId2], // note references — hex strings, NoteIds, records, or Notes
});
// A different, unconsumed note can also be passed directly, without an array.
await client.transactions.consume({
account: wallet,
notes: noteId3,
});
consumeAll
Consumes every available note for an account, up to an optional limit. Useful for quickly draining an inbox.
const result = await client.transactions.consumeAll({ account: wallet });
console.log(`Consumed ${result.consumed}, ${result.remaining} remaining`);
if (result.txId) {
console.log("Tx:", result.txId.toHex());
}
// Cap the number of notes consumed in one transaction
await client.transactions.consumeAll({ account: wallet, maxNotes: 5 });
result.remaining > 0 signals pagination — call consumeAll again to drain the rest. Both counts include only notes consumable at the last synced block; remaining === 0 does not mean that there are no block-locked notes.
swap
Atomic swap between two assets.
const { txId } = await client.transactions.swap({
account: wallet,
offer: { token: faucetA, amount: 100n },
request: { token: faucetB, amount: 200n },
type: "public", // optional — visibility of the offer note
paybackType: "private", // optional — visibility of the payback note
});
Waiting for confirmation
All simplified operations return as soon as the transaction is submitted to the mempool. To block until the network commits the transaction, use waitFor:
const { txId } = await client.transactions.mint({
account: faucet,
to: wallet,
amount: 1000n,
});
// Default: 60s timeout, 5s polling interval
await client.transactions.waitFor(txId.toHex());
// Custom polling
await client.transactions.waitFor(txId.toHex(), {
timeout: 120_000, // 2 minutes — set to 0 to poll indefinitely
interval: 3_000,
onProgress: (status) => {
console.log(`Status: ${status}`); // "pending" | "submitted" | "committed"
},
});
waitFor throws on rejection or timeout.
Preview transactions awaiting authorization
preview() derives the TransactionSummary that an account is being asked to authorize without proving or submitting the transaction. It only returns a summary when authorization is still pending, such as a multisig request that has not reached its threshold:
const summary = await client.transactions.preview({
operation: "send",
account: multisigAccount,
to: recipient,
token: faucet,
amount: 100n,
});
If the account already authorizes the request, execution succeeds without producing a pending summary and preview() rejects with TRANSACTION_ALREADY_AUTHORIZED. Submit that transaction normally instead. The built-in preview operations follow the same rule. Use operation: "custom" when previewing a pre-built TransactionRequest, as in the cross-client flow below.
Keep a cross-client summary reproducible
A 0.17 multisig summary commits to the bound block in its auth arguments. Build the request with feeAwareTransactionRequestBuilder, send the serialized request with the summary, and let each participant preview or execute it at its current synced tip:
import { TransactionRequest, TransactionSummary } from "@miden-sdk/miden-sdk";
// Proposer: build once, then share that exact request.
await client.sync();
const request = (
await client.feeAwareTransactionRequestBuilder(multisigAccount, {
approvalExpirationDelta: 100,
})
)
.withCustomScript(txScript)
.build();
const summary = await client.transactions.preview({
operation: "custom",
account: multisigAccount,
request,
});
const requestBytes = request.serialize();
const summaryBytes = summary.serialize();
await sendProposal(requestBytes, summaryBytes);
// Co-signer or executor: sync to at least the proposal's declared blocks.
const receivedRequest = TransactionRequest.deserialize(requestBytes);
const proposedSummary = TransactionSummary.deserialize(summaryBytes);
await client.sync();
if ((await client.getSyncHeight()) < Math.max(0, ...receivedRequest.blockNumbers())) {
throw new Error("The client has not synced to the proposal's bound block");
}
const derivedSummary = await client.transactions.preview({
operation: "custom",
account: multisigAccount,
request: receivedRequest,
});
if (derivedSummary.toCommitment().toHex() !== proposedSummary.toCommitment().toHex()) {
throw new Error("The request does not match the proposed summary");
}
// The application collects signatures and attaches the authorization advice.
const authorizedRequest = await collectAuthorization(derivedSummary, receivedRequest);
await client.transactions.submit(multisigAccount, authorizedRequest);
Do not pass an anchor for this multisig flow or call withFeeConversionSalt / withAuthArg on its builder: these setters replace the auth arguments it already carries. Inspect the summary's account delta and input/output notes before signing. If the approval expires, build a new proposal and collect authorization again.
ChainAnchor still serves flows whose summary binds the execution reference block. Capture it with captureAnchor(request) and pass that same anchor to preview and submit; execute while the node still retains that block's account state.
Custom transaction scripts (execute)
When the simplified operations aren't enough — for example to call a procedure on a contract account — compile a transaction script with client.compile.txScript() and run it through execute:
import { MidenClient } from "@miden-sdk/miden-sdk";
const client = await MidenClient.createTestnet();
const script = await client.compile.txScript({
code: `
use external_contract::counter_contract
@transaction_script
pub proc main
call.counter_contract::increment_count
end
`,
// Reuse the component installed on contractAccount.
libraries: [{ component: counterComponent }],
});
const { txId } = await client.transactions.execute({
account: contractAccount.id(),
script,
});
console.log("Tx:", txId.toHex());
Foreign procedure invocation (FPI)
Pass foreignAccounts to read state from other contracts during execution:
import { MidenClient, StorageSlot } from "@miden-sdk/miden-sdk";
const client = await MidenClient.createTestnet();
// Compile the foreign contract to get a procedure hash
const counterComponent = await client.compile.component({
code: counterContractCode,
namespace: "external_contract::counter_contract",
slots: [StorageSlot.emptyValue("miden::tutorials::counter")],
});
const getCountHash = counterComponent.getProcedureHash("get_count");
const script = await client.compile.txScript({
code: `
use external_contract::count_reader_contract
use miden::core::sys
@transaction_script
pub proc main
padw padw padw padw
push.${getCountHash}
push.${counterAccount.id().prefix()}
push.${counterAccount.id().suffix()}
call.count_reader_contract::copy_count
exec.sys::truncate_stack
end
`,
// Reuse the component installed on countReaderAccount.
libraries: [{ component: countReaderComponent }],
});
const { txId } = await client.transactions.execute({
account: countReaderAccount.id(),
script,
foreignAccounts: [
// A bare reference is sufficient here because get_count reads a value slot.
counterAccount.id(),
// Storage-map entries require explicit storage requirements:
// { id: counterAccount.id(), storage: requirements },
],
});
View calls (executeProgram)
executeProgram runs a transaction script locally, returning the resulting stack without submitting or proving anything. Use it to read onchain state — similar to eth_call in Ethereum.
const script = await client.compile.txScript({
code: `
use external_contract::counter_contract
@transaction_script
pub proc main
call.counter_contract::get_count
end
`,
libraries: [{ component: counterComponent }],
});
const stack = await client.transactions.executeProgram({
account: contractAccount.id(),
script,
});
// Node.js returns Felt[]; browser WASM returns the FeltArray wrapper.
const first = Array.isArray(stack) ? stack[0] : stack.get(0);
const count = first.asInt();
console.log("Count:", count);
Options:
| Option | Type | Description |
|---|---|---|
account | AccountRef | Account to execute the script against. |
script | TransactionScript | Compiled script. |
adviceInputs | AdviceInputs? | Advice inputs for the VM. Defaults to empty. |
foreignAccounts | (AccountRef | { id, storage? })[]? | Foreign accounts for FPI reads. |
Manual TransactionRequest
For full control over note inputs and outputs — e.g. emitting multiple output notes from one transaction — build a TransactionRequest yourself and pass it to submit.
The builder accepts WASM array classes (NoteArray, NoteDetailsAndTagArray, NoteRecipientArray) rather than plain TypeScript arrays.
import {
MidenClient,
TransactionRequestBuilder,
NoteArray,
} from "@miden-sdk/miden-sdk";
const client = await MidenClient.createTestnet();
// Populate a NoteArray with your output notes
const ownOutputs = new NoteArray();
for (const note of outputNotes) {
ownOutputs.push(note);
}
const request = new TransactionRequestBuilder()
.withOwnOutputNotes(ownOutputs)
.build();
const { txId } = await client.transactions.submit(wallet, request);
console.log("Tx:", txId.toHex());
Expected-note hints are also available:
withExpectedFutureNotes(NoteDetailsAndTagArray)— notes the transaction should produce but won't emit directly (e.g. for later pickup).withExpectedOutputRecipients(NoteRecipientArray)— recipients for expected output notes.
Setting an expiration
const request = new TransactionRequestBuilder()
.withOwnOutputNotes(ownOutputs)
.withExpirationDelta(10) // expires 10 blocks after the reference block
.build();
await client.transactions.submit(wallet, request);
withExpirationDelta() cannot be combined with withCustomScript(); build() rejects that combination. A custom script must set its expiration through the transaction context instead.
Remote proving
Local proving is CPU-intensive. Offload globally via ClientOptions.proverUrl, or per-transaction via the prover field:
// Global: every transaction uses the remote prover
const client = await MidenClient.create({
rpcUrl: "testnet",
proverUrl: "https://prover.example.com",
});
// Per-transaction: pass a TransactionProver instance
await client.transactions.send({
account: wallet,
to: recipient,
token: faucet,
amount: 100n,
prover: customProver,
});
History
Query past transactions through client.transactions.list().
The expiredBefore filter was removed in v0.16. Expiry is resolved during state sync: sync first and inspect each transaction record's status. { status: "uncommitted" } selects pending transactions, not expired transactions.
await client.sync();
// All transactions
const all = await client.transactions.list();
// Only uncommitted
const uncommitted = await client.transactions.list({ status: "uncommitted" });
// By ID
const specific = await client.transactions.list({ ids: [txId1, txId2] });
Each record exposes:
for (const tx of all) {
tx.id().toHex();
tx.accountId().toString(); // AccountId.toString() returns canonical hex
tx.blockNum().toString();
const status = tx.transactionStatus();
if (status.isPending()) console.log("Pending");
if (status.isCommitted()) console.log("Committed at", status.getBlockNum(), status.getCommitTimestamp());
if (status.isDiscarded()) console.log("Discarded");
tx.initAccountState().toHex();
tx.finalAccountState().toHex();
tx.inputNoteNullifiers().map((n) => n.toHex());
tx.outputNotes().notes().map((note) => note.id().toString());
}