---
title: "7. Building Transactions in Rust"
url: "https://radix.wiki/developers/transactions/07-rust-transaction-building"
updated: 2026-11-14
license: CC-BY-4.0
license_url: "https://creativecommons.org/licenses/by/4.0/"
version: "1.0.0"
---

# 7. Building Transactions in Rust

|  |  |
| --- | --- |
| Series | [Transactions track](/developers/transactions), part 7 |
| Crates | [radix-transactions](https://docs.rs/radix-transactions/1.4.0/radix_transactions/) and [radix-common](https://docs.rs/radix-common/1.4.0/radix_common/) 1.4.0 |
| Builds | `NotarizedTransactionV2`, with or without subintents |
| Tested with | radix-transactions 1.4.0, submitted to [Stokenet](/contents/tech/releases/stokenet) on 14 November 2026 |

## Introduction

`radix-transactions` is the Rust crate that defines Radix's transaction models, the manifest builder and the manifest compiler. The [Radix Engine](/contents/tech/core-protocols/radix-engine) and the node use the same crate, so a transaction it builds is the format the network validates. The Radix docs recommend it for any Rust integration, over the Rust core of the [Radix Engine Toolkit](/developers/transactions/04-radix-engine-toolkit), which is not published to crates.io ([Rust Libraries](https://docs.radixdlt.com/docs/rust-libraries-overview)).

It is also the most direct way to build the V2 transactions introduced at the [Cuttlefish](/contents/tech/releases/protocol-updates) protocol update. The TypeScript toolkit, `@radixdlt/radix-engine-toolkit`, was at 1.0.6 in November 2026 and its type definitions contain no V2 transaction or subintent builder. The docs recommend V2 for new integrations even without subintents, because it adds expiry by timestamp, a collision-resistant intent discriminator and more assertion instructions ([Transaction Building](https://docs.radixdlt.com/docs/rust-transaction-building)).

This guide builds a transaction that takes free test $XRD from the Stokenet faucet, submits it through the Gateway API, then repeats the transfer through a [subintent](/contents/tech/core-concepts/subintents-and-pre-authorizations). Both ran on Stokenet while this page was written.

## Which crates and versions do I need?

Two crates: `radix-transactions` for the builders and models, and `radix-common` for keys, addresses and network definitions. The engine crates are published together at each protocol update and depend on each other, and they do not follow semantic versioning, so the docs advise pinning one exact version across all of them and moving it by hand ([Rust Libraries](https://docs.radixdlt.com/docs/rust-libraries-overview)). 1.4.0 was the newest release on crates.io in November 2026.

```
[dependencies]
radix-transactions = "=1.4.0"
radix-common = "=1.4.0"
```

Both crates have a `prelude` module, and the docs recommend importing through it, because paths inside the crates move between versions.

## Building and notarizing a V2 transaction

A V2 transaction has two headers. The transaction header names the notary: the key that signs the finished transaction last, sealing it. The intent header names the network and the window in which the transaction may commit. The program below builds both, adds a three-instruction manifest and notarizes it:

```
use radix_common::prelude::*;
use radix_transactions::prelude::*;

fn main() {
    let epoch: u64 = std::env::args().nth(1).expect("current epoch").parse().unwrap();
    let network = NetworkDefinition::stokenet();

    // A throwaway key. Its account exists as soon as something is deposited to it.
    let key = Ed25519PrivateKey::from_u64(914).unwrap();
    let account = ComponentAddress::preallocated_account_from_public_key(&key.public_key());

    let tx = TransactionBuilder::new_v2()
        .transaction_header(TransactionHeaderV2 {
            notary_public_key: key.public_key().into(),
            notary_is_signatory: true,
            tip_basis_points: 0,
        })
        .intent_header(IntentHeaderV2 {
            network_id: network.id,
            start_epoch_inclusive: Epoch::of(epoch),
            end_epoch_exclusive: Epoch::of(epoch + 2),
            min_proposer_timestamp_inclusive: None,
            max_proposer_timestamp_exclusive: None,
            intent_discriminator: 914 ^ epoch,
        })
        .manifest_builder(|builder| {
            builder
                .lock_fee_from_faucet()
                .get_free_xrd_from_faucet()
                .deposit_entire_worktop(account)
        })
        .notarize(&key)
        .build();

    let addresses = AddressBech32Encoder::new(&network);
    let hashes = TransactionHashBech32Encoder::new(&network);
    println!("account   {}", account.to_string(&addresses));
    println!("intent    {}", hashes.encode(&tx.transaction_hashes.transaction_intent_hash).unwrap());
    println!("payload   {}", tx.raw.to_hex());
}
```

The settings that matter:

- **`notary_is_signatory: true`** lets the notary's signature count as a signature on the intent, so one key both signs and notarizes. A wallet or exchange with a single key per account uses this; a separate ephemeral notary key is the alternative, with the account keys added through `.sign()`.
- **The epoch window** is inclusive at the start and exclusive at the end. An epoch lasts about five minutes, so `epoch + 2` leaves five to ten minutes before the transaction permanently rejects; this is the docs' recommended setting. The current epoch comes from the Gateway or Core API.
- **`intent_discriminator`** is a free `u64` that makes two otherwise identical intents hash differently. Production code sets it randomly.
- **`tip_basis_points`** replaces V1's tip percentage. Zero is the normal value outside congestion ([Transaction Fees](/developers/transactions/03-transaction-fees)).

The manifest locks the fee from the faucet component, so the new account needs no $XRD to pay for its first transaction. `deposit_entire_worktop` calls the account's owner-only `deposit_batch`, which works here because the account's own key signed. A deposit to someone else's account uses `try_deposit_entire_worktop_or_abort`, which respects that account's deposit rules.

`build()` returns a `DetailedNotarizedTransactionV2` carrying the transaction, its raw bytes, the names of its buckets and proofs, and every hash. The transaction intent hash, encoded with the network's `txid_` prefix, is the transaction ID the Gateway and dashboards use.

## How do I submit it?

Post the hex payload to the Gateway's `/transaction/submit`, then poll `/transaction/status` with the intent hash ([Gateway API reference](https://docs.radixdlt.com/api-reference/gateway-api-specs.html)). A node's Core API has the equivalent endpoints for integrations that run their own node ([Exchange Integration](/developers/infrastructure/03-exchange-integration)).

```
GATEWAY=https://stokenet.radixdlt.com
EPOCH=$(curl -s -X POST $GATEWAY/status/gateway-status -H 'content-type: application/json' -d '{}' | jq .ledger_state.epoch)
./target/release/rtx $EPOCH > out.txt

curl -s -X POST $GATEWAY/transaction/submit -H 'content-type: application/json' \
  -d "{\"notarized_transaction_hex\":\"$(awk '/^payload/{print $2}' out.txt)\"}"

curl -s -X POST $GATEWAY/transaction/status -H 'content-type: application/json' \
  -d "{\"intent_hash\":\"$(awk '/^intent/{print $2}' out.txt)\"}"
```

On 14 November 2026, at epoch 22518, the submission returned `{"duplicate":false}`. The status read `Pending` twice and then `CommittedSuccess` about nine seconds after submission. The committed details showed 10,000 $XRD leaving the faucet and arriving in the new account, and a fee of 0.58 $XRD paid by the faucet. The payload's first three bytes, `4d 22 0c`, are the Manifest SBOR prefix, the enum value kind and discriminator 12, which the crate assigns to `NotarizedTransactionV2` ([SBOR](/developers/transactions/06-sbor)).

A status other than success needs handling: a temporary rejection can still commit later, for example once the fee payer is topped up, while a permanent rejection or a committed failure needs a new transaction with a new intent hash ([Worked Example 1](https://docs.radixdlt.com/docs/exchange-integration-worked-example-1-transfer-transaction-submission-flow)).

## Adding a subintent

A subintent is a signed piece of a transaction that someone else completes, the mechanism behind wallet pre-authorizations. In the builder it is a partial transaction: an intent header, a subintent manifest that must end by yielding to its parent, and its own signatures. The parent adds it with `add_signed_child`, which also registers its hash, so the parent manifest can call it by name:

```
// The child: takes free $XRD from the faucet and hands the bucket to its parent.
let child = TransactionBuilder::new_partial_v2()
    .intent_header(header(epoch ^ 1))
    .manifest_builder(|builder| {
        builder
            .get_free_xrd_from_faucet()
            .take_all_from_worktop(XRD, "xrd")
            .yield_to_parent_with_name_lookup(|lookup| (lookup.bucket("xrd"),))
    })
    .sign(&helper)
    .build();

// The parent: pays the fee, runs the child, deposits what it returned.
let tx = TransactionBuilder::new_v2()
    .transaction_header(TransactionHeaderV2 {
        notary_public_key: notary.public_key().into(),
        notary_is_signatory: true,
        tip_basis_points: 0,
    })
    .intent_header(header(epoch ^ 2))
    .add_signed_child("child", child.partial_transaction)
    .manifest_builder(|builder| {
        builder
            .lock_fee_from_faucet()
            .yield_to_child("child", ())
            .deposit_entire_worktop(account)
    })
    .notarize(&notary)
    .build();
```

Here `header` is a closure that returns the same `IntentHeaderV2` as above with the discriminator it is given, and `helper` is a second Ed25519 key.

This transaction committed on Stokenet in about nine seconds. The faucet paid the 0.43 $XRD fee because the parent locked it, the child's call to the faucet produced the 10,000 $XRD, and the parent deposited them. A child partial transaction received from elsewhere, such as a pre-authorization a wallet signed, is passed to `add_signed_child` the same way after decoding it from its raw bytes.

The docs give one limit: when the dApp Toolkit sends a manifest to the wallet with `sendTransaction`, the wallet accepts V1 instructions only; V2 instructions reach the wallet through the pre-authorization flow ([Transaction Building](https://docs.radixdlt.com/docs/rust-transaction-building)).

## Next Steps

- [Exchange Integration](/developers/infrastructure/03-exchange-integration) – sign, submit and track transfers for many accounts
- [SBOR](/developers/transactions/06-sbor) – the encoding inside every payload this guide produced

## External Links

- [Transaction Building](https://docs.radixdlt.com/docs/rust-transaction-building) – Radix documentation
- [Rust Manifest Builder](https://docs.radixdlt.com/docs/rust-manifest-builder) – the builder's methods, grouped by instruction
- [Rust Libraries](https://docs.radixdlt.com/docs/rust-libraries-overview) – which Rust crates exist and which to use
- [radix-transactions on docs.rs](https://docs.rs/radix-transactions/1.4.0/radix_transactions/)
- [radix-transactions source](https://github.com/radixdlt/radixdlt-scrypto/tree/main/radix-transactions) – radixdlt-scrypto repository
