Network Transactions on Miden
Using the Miden client in Rust to deploy and interact with smart contracts using network transactions
Overview
In this tutorial, we will explore Network Transactions (NTXs) on Miden - a powerful feature that enables autonomous smart contract execution and public shared state management. Unlike local transactions that require users to execute and prove, network transactions are executed and proven by a network transaction builder.
We'll build a network counter smart contract using the same MASM code as the regular counter. In v0.15 there is no separate network storage mode. Instead, an account is a network account — one the network transaction builder executes on a user's behalf — if and only if it is public (AccountType::Public) and carries the AuthNetworkAccount auth component with a non-empty note-script allowlist. The allowed note-script roots and transaction-script roots are pinned at account creation. Attaching a NetworkAccountTarget to a note is necessary but not sufficient: without the allowlist the node never classifies the account as a network account, and the note is silently orphaned. See the account changes migration guide.
What we'll cover
- Understanding Network Transactions and when to use them
- Deploying public smart contracts that the network operator can execute
- Using transaction scripts to initialize network contracts on-chain
- Creating network notes for user interactions
- Validating network transaction results
Prerequisites
This tutorial assumes you have completed the counter contract tutorial and understand basic Miden assembly.
What are Network Transactions?
Network transactions are executed and proven by the Miden operator rather than the client. They are useful for:
- Public shared state: Multiple users can interact with the same contract state without race conditions
- Autonomous execution: Smart contracts can execute when conditions are met without user intervention
- Resource-constrained devices: Clients that can't generate ZK proofs efficiently
- AMM applications: Using network notes, you can build sophisticated AMMs where trades execute automatically
The main trade-off is reduced privacy since the operator can see transaction inputs.
Step 1: Initialize your repository
Create a new Rust repository for your Miden project and navigate to it:
cargo new miden-network-transactions
cd miden-network-transactions
Add the following dependencies to your Cargo.toml file:
[dependencies]
miden-client = { version = "0.15", features = ["testing", "tonic"] }
miden-client-sqlite-store = { version = "0.15", package = "miden-client-sqlite-store" }
miden-protocol = { version = "0.15" }
rand = { version = "0.9" }
tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] }
Step 2: Set up MASM files
Create the directory structure:
mkdir -p masm/accounts masm/scripts masm/notes
Counter Contract
We'll use the same counter contract MASM code as the regular counter tutorial. The key difference is in the Rust configuration, not the MASM code.
Create masm/accounts/counter.masm:
use miden::protocol::active_account
use miden::protocol::native_account
use miden::core::word
use miden::core::sys
const COUNTER_SLOT = word("miden::tutorials::counter")
#! Inputs: []
#! Outputs: [count]
pub proc get_count
push.COUNTER_SLOT[0..2] exec.active_account::get_item
# => [count]
exec.sys::truncate_stack
# => [count]
end
#! Inputs: []
#! Outputs: []
pub proc increment_count
push.COUNTER_SLOT[0..2] exec.active_account::get_item
# => [count]
add.1
# => [count+1]
push.COUNTER_SLOT[0..2] exec.native_account::set_item
# => []
exec.sys::truncate_stack
# => []
end
Transaction Script for Deployment
Create masm/scripts/counter_script.masm:
use external_contract::counter_contract
begin
call.counter_contract::increment_count
end
This script executes a function call (increment) that creates a necessary state change for our contract to be deployed and stored on the network on-chain. In Miden, public contracts must have their state modified through a transaction to be properly registered and committed to the blockchain - simply creating the account isn't sufficient.
Network Note for User Interaction
Create masm/notes/network_increment_note.masm. Note scripts are compiled as libraries; the @note_script attribute marks the entrypoint procedure.
use external_contract::counter_contract
#! Inputs: []
#! Outputs: []
@note_script
pub proc main
call.counter_contract::increment_count
end
After deployment, users will interact with the contract through these network notes.
Step 3: Initialize the client and create a user account
Before deploying the network account and creating network notes, we need to set up the client and create a user account that will interact with our network contract.
Copy and paste the following code into your src/main.rs file:
use std::{collections::BTreeSet, path::PathBuf, sync::Arc};
use miden_client::{
account::{
component::{AccountComponentMetadata, AuthNetworkAccount, BasicWallet}, AccountBuilder, AccountComponent,
AccountType, StorageSlot, StorageSlotName,
},
address::NetworkId,
auth::{AuthSchemeId, AuthSecretKey, AuthSingleSig},
builder::ClientBuilder,
crypto::FeltRng,
keystore::{FilesystemKeyStore, Keystore},
note::{
NetworkAccountTarget, Note, NoteAssets, NoteAttachments, NoteError, NoteExecutionHint,
NoteRecipient, NoteStorage, NoteTag, NoteType, PartialNoteMetadata,
},
rpc::{Endpoint, GrpcClient},
store::TransactionFilter,
transaction::{TransactionId, TransactionRequestBuilder, TransactionStatus},
Client, ClientError, Felt, Word,
};
use miden_client_sqlite_store::ClientBuilderSqliteExt;
use rand::RngCore;
use tokio::time::{sleep, Duration};
/// Waits for a specific transaction to be committed.
async fn wait_for_tx(
client: &mut Client<FilesystemKeyStore>,
tx_id: TransactionId,
) -> Result<(), ClientError> {
loop {
client.sync_state().await?;
// Check transaction status
let txs = client
.get_transactions(TransactionFilter::Ids(vec![tx_id]))
.await?;
let tx_committed = if !txs.is_empty() {
matches!(txs[0].status, TransactionStatus::Committed { .. })
} else {
false
};
if tx_committed {
println!("✅ transaction {} committed", tx_id.to_hex());
break;
}
println!(
"Transaction {} not yet committed. Waiting...",
tx_id.to_hex()
);
sleep(Duration::from_secs(2)).await;
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize client
let endpoint = Endpoint::testnet();
let timeout_ms = 10_000;
let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms));
// Initialize keystore
let keystore_path = PathBuf::from("./keystore");
let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap());
let store_path = PathBuf::from("./store.sqlite3");
let mut client = ClientBuilder::new()
.rpc(rpc_client)
.sqlite_store(store_path)
.authenticator(keystore.clone())
.in_debug_mode(true.into())
.build()
.await?;
let sync_summary = client.sync_state().await.unwrap();
println!("Latest block: {}", sync_summary.block_num);
// -------------------------------------------------------------------------
// STEP 1: Create Basic User Account
// -------------------------------------------------------------------------
println!("\n[STEP 1] Creating a new account for Alice");
// Account seed
let mut init_seed = [0_u8; 32];
client.rng().fill_bytes(&mut init_seed);
let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng());
// Build the account
let alice_account = AccountBuilder::new(init_seed)
.account_type(AccountType::Public)
.with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2))
.with_component(BasicWallet)
.build()
.unwrap();
// Add the account to the client
client.add_account(&alice_account, false).await?;
// Add the key pair to the keystore
keystore.add_key(&key_pair, alice_account.id()).await.unwrap();
println!(
"Alice's account ID: {:?}",
alice_account.id().to_bech32(NetworkId::Testnet)
);
Ok(())
}
This step initializes the Miden client and creates a basic user account (Alice) that will interact with our network contract.
Step 4: Create the network counter smart contract
Now we'll create the network smart contract. In v0.15 what makes an account network-executable is its auth component, not a storage mode: the contract is a public account (AccountType::Public) built with the AuthNetworkAccount component. Its note-script allowlist is what marks the account as a network account, and its transaction-script allowlist authorizes the deploy script in Step 5. Both allowlists are fixed at account creation, so we compile the note script and the deploy transaction script first and pass their MAST roots in.
Add this code to your main() function:
// -------------------------------------------------------------------------
// STEP 2: Create Network Counter Smart Contract
// -------------------------------------------------------------------------
println!("\n[STEP 2] Creating a network counter smart contract");
// `include_str!` resolves at compile time relative to this source file,
// so the binary is independent of the working directory it is run from.
let counter_code = include_str!("../masm/accounts/counter.masm");
let script_code = include_str!("../masm/scripts/counter_script.masm");
let network_note_code = include_str!("../masm/notes/network_increment_note.masm");
// In protocol v0.15 an account is a *network account* (one the network
// transaction builder executes on a user's behalf) if and only if it is
// public AND carries the `AuthNetworkAccount` auth component. That component
// holds two allowlists, both fixed at account creation:
// * the note-script allowlist: its presence is what marks the account as a
// network account, and the builder only executes notes whose script root
// is listed here;
// * the tx-script allowlist: the network auth procedure rejects any custom
// tx script whose root is not listed, so the STEP 3 deploy script must be
// in it.
// We therefore compile the note script and the deploy tx script now and feed
// their MAST roots into the allowlists below. Both compiled scripts are reused
// as-is in STEP 3 (tx script) and STEP 4 (note script) — nothing is compiled
// twice.
let note_script = client
.code_builder()
.with_linked_module("external_contract::counter_contract", counter_code)?
.compile_note_script(network_note_code)?;
let note_script_root = note_script.root();
let tx_script = client
.code_builder()
.with_linked_module("external_contract::counter_contract", counter_code)?
.compile_tx_script(script_code)?;
let tx_script_root = tx_script.root();
// Compile the counter MASM into an account component
let counter_slot_name =
StorageSlotName::new("miden::tutorials::counter").expect("valid slot name");
let component_code = client
.code_builder()
.compile_component_code("external_contract::counter_contract", counter_code)?;
let counter_component = AccountComponent::new(
component_code,
vec![StorageSlot::with_value(
counter_slot_name.clone(),
[Felt::new_unchecked(0); 4].into(),
)],
AccountComponentMetadata::new("external_contract::counter_contract"),
)?;
// Generate a random seed for the account
let mut init_seed = [0_u8; 32];
client.rng().fill_bytes(&mut init_seed);
// Build the network account: public + `AuthNetworkAccount` with the note-script
// root allowlisted (this is what makes it a network account) and the deploy
// tx-script root allowlisted (so the auth procedure accepts the STEP 3 deploy).
let network_auth = AuthNetworkAccount::with_allowed_notes(BTreeSet::from([note_script_root]))?
.with_allowed_tx_scripts(BTreeSet::from([tx_script_root]));
let counter_contract = AccountBuilder::new(init_seed)
.account_type(AccountType::Public)
.with_auth_component(network_auth)
.with_component(counter_component)
.build()
.unwrap();
client.add_account(&counter_contract, false).await.unwrap();
println!(
"contract id: {:?}",
counter_contract.id().to_bech32(NetworkId::Testnet)
);
This step creates a public smart contract (AccountType::Public) whose AuthNetworkAccount component allowlists the increment note's script root — marking it as a network account the operator will execute — and the deploy transaction script's root, so the Step 5 deploy is authorized.
Step 5: Deploy the network account with a transaction script
We use a transaction script to deploy the network account and ensure it's properly registered on-chain. The script calls the increment function, which initializes the counter to 1.
Add this code to your main() function:
// -------------------------------------------------------------------------
// STEP 3: Deploy Network Account with Transaction Script
// -------------------------------------------------------------------------
println!("\n[STEP 3] Deploy network counter smart contract");
// Reuse the `tx_script` compiled in STEP 2 (its root is allowlisted on the
// account, so the network auth procedure accepts this deploy transaction).
let tx_increment_request = TransactionRequestBuilder::new()
.custom_script(tx_script)
.build()
.unwrap();
let tx_id = client
.submit_new_transaction(counter_contract.id(), tx_increment_request)
.await
.unwrap();
println!(
"View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
tx_id
);
// Wait for the transaction to be committed
wait_for_tx(&mut client, tx_id).await.unwrap();
This step uses a transaction script to deploy the network account and ensure it's properly registered on-chain. The script calls the increment function, which initializes the counter to 1.
Step 6: Create a network note for user interaction
We create a public note that the network operator can consume to execute the increment function. This increments the counter from 1 to 2.
Add this code to your main() function:
// -------------------------------------------------------------------------
// STEP 4: Prepare & Create the Network Note
// -------------------------------------------------------------------------
println!("\n[STEP 4] Creating a network note for network counter contract");
// Create and submit the network note that will increment the counter
// Generate a random serial number for the note
let serial_num = client.rng().draw_word();
// Reuse the `note_script` compiled in STEP 2 (its root is allowlisted on the
// account, so the network transaction builder will execute this note).
let note_storage = NoteStorage::new([].to_vec())?;
let recipient = NoteRecipient::new(serial_num, note_script, note_storage);
// Set up note metadata - tag it with the counter contract ID so it gets consumed
let tag = NoteTag::with_account_target(counter_contract.id());
let attachment = NetworkAccountTarget::new(counter_contract.id(), NoteExecutionHint::Always)
.map_err(|e| NoteError::other(e.to_string()))?
.into();
let metadata = PartialNoteMetadata::new(alice_account.id(), NoteType::Public).with_tag(tag);
let attachments = NoteAttachments::new(vec![attachment]).unwrap();
// Create the complete note
let increment_note =
Note::with_attachments(NoteAssets::default(), metadata, recipient, attachments);
// Build and submit the transaction containing the note
let note_req = TransactionRequestBuilder::new()
.own_output_notes(vec![increment_note])
.build()?;
let note_tx_id = client
.submit_new_transaction(alice_account.id(), note_req)
.await?;
println!(
"View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
note_tx_id
);
client.sync_state().await?;
println!("network increment note creation tx submitted, waiting for onchain commitment");
// Wait for the note transaction to be committed
wait_for_tx(&mut client, note_tx_id).await.unwrap();
// Waiting for network note to be picked up by the network transaction builder
sleep(Duration::from_secs(6)).await;
let mut last_val = None;
for _ in 0..10 {
client.sync_state().await?;
// Checking updated state
let new_account_state = client.get_account(counter_contract.id()).await.unwrap();
if let Some(account) = new_account_state.as_ref() {
let count: Word = account
.storage()
.get_item(&counter_slot_name)
.unwrap()
.into();
let val = count[0].as_canonical_u64();
if val >= 2 {
println!("🔢 Final counter value: {}", val);
return Ok(());
}
last_val = Some(val);
}
// Give the network note builder time to process the note.
sleep(Duration::from_secs(6)).await;
}
// The network note was submitted, but it is executed asynchronously by the
// network transaction builder. If the counter has not reached 2 within the
// polling window, the tutorial's final state is unconfirmed, so fail rather
// than claim success.
if let Some(val) = last_val {
Err(format!(
"Counter did not reach the expected value 2 within the timeout (last observed {}). \
The network note was submitted but its execution is still pending on the network \
transaction builder; re-run or check Midenscan.",
val
)
.into())
} else {
Err("Counter state was not available within the timeout; the network note execution is still pending."
.into())
}
This step creates a public note that the network operator can consume to execute the increment function. This increments the counter from 1 to 2.
Summary
Your complete main() function should look like this:
use std::{collections::BTreeSet, path::PathBuf, sync::Arc};
use miden_client::{
account::{
component::{AccountComponentMetadata, AuthNetworkAccount, BasicWallet}, AccountBuilder, AccountComponent,
AccountType, StorageSlot, StorageSlotName,
},
address::NetworkId,
auth::{AuthSchemeId, AuthSecretKey, AuthSingleSig},
builder::ClientBuilder,
crypto::FeltRng,
keystore::{FilesystemKeyStore, Keystore},
note::{
NetworkAccountTarget, Note, NoteAssets, NoteAttachments, NoteError, NoteExecutionHint,
NoteRecipient, NoteStorage, NoteTag, NoteType, PartialNoteMetadata,
},
rpc::{Endpoint, GrpcClient},
store::TransactionFilter,
transaction::{
TransactionId, TransactionRequestBuilder, TransactionStatus,
},
Client, ClientError, Felt, Word,
};
use miden_client_sqlite_store::ClientBuilderSqliteExt;
use rand::RngCore;
use tokio::time::{sleep, Duration};
/// Waits for a specific transaction to be committed.
async fn wait_for_tx(
client: &mut Client<FilesystemKeyStore>,
tx_id: TransactionId,
) -> Result<(), ClientError> {
loop {
client.sync_state().await?;
// Check transaction status
let txs = client
.get_transactions(TransactionFilter::Ids(vec![tx_id]))
.await?;
let tx_committed = if !txs.is_empty() {
matches!(txs[0].status, TransactionStatus::Committed { .. })
} else {
false
};
if tx_committed {
println!("✅ transaction {} committed", tx_id.to_hex());
break;
}
println!(
"Transaction {} not yet committed. Waiting...",
tx_id.to_hex()
);
sleep(Duration::from_secs(2)).await;
}
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize client
let endpoint = Endpoint::testnet();
let timeout_ms = 10_000;
let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms));
// Initialize keystore
let keystore_path = PathBuf::from("./keystore");
let keystore = Arc::new(FilesystemKeyStore::new(keystore_path).unwrap());
let store_path = PathBuf::from("./store.sqlite3");
let mut client = ClientBuilder::new()
.rpc(rpc_client)
.sqlite_store(store_path)
.authenticator(keystore.clone())
.in_debug_mode(true.into())
.build()
.await?;
let sync_summary = client.sync_state().await.unwrap();
println!("Latest block: {}", sync_summary.block_num);
// -------------------------------------------------------------------------
// STEP 1: Create Basic User Account
// -------------------------------------------------------------------------
println!("\n[STEP 1] Creating a new account for Alice");
// Account seed
let mut init_seed = [0_u8; 32];
client.rng().fill_bytes(&mut init_seed);
let key_pair = AuthSecretKey::new_falcon512_poseidon2_with_rng(client.rng());
// Build the account
let alice_account = AccountBuilder::new(init_seed)
.account_type(AccountType::Public)
.with_auth_component(AuthSingleSig::new(key_pair.public_key().to_commitment(), AuthSchemeId::Falcon512Poseidon2))
.with_component(BasicWallet)
.build()
.unwrap();
// Add the account to the client
client.add_account(&alice_account, false).await?;
// Add the key pair to the keystore
keystore.add_key(&key_pair, alice_account.id()).await.unwrap();
println!(
"Alice's account ID: {:?}",
alice_account.id().to_bech32(NetworkId::Testnet)
);
// -------------------------------------------------------------------------
// STEP 2: Create Network Counter Smart Contract
// -------------------------------------------------------------------------
println!("\n[STEP 2] Creating a network counter smart contract");
// `include_str!` resolves at compile time relative to this source file,
// so the binary is independent of the working directory it is run from.
let counter_code = include_str!("../masm/accounts/counter.masm");
let script_code = include_str!("../masm/scripts/counter_script.masm");
let network_note_code = include_str!("../masm/notes/network_increment_note.masm");
// In protocol v0.15 an account is a *network account* (one the network
// transaction builder executes on a user's behalf) if and only if it is
// public AND carries the `AuthNetworkAccount` auth component. That component
// holds two allowlists, both fixed at account creation:
// * the note-script allowlist: its presence is what marks the account as a
// network account, and the builder only executes notes whose script root
// is listed here;
// * the tx-script allowlist: the network auth procedure rejects any custom
// tx script whose root is not listed, so the STEP 3 deploy script must be
// in it.
// We therefore compile the note script and the deploy tx script now and feed
// their MAST roots into the allowlists below. Both compiled scripts are reused
// as-is in STEP 3 (tx script) and STEP 4 (note script) — nothing is compiled
// twice.
let note_script = client
.code_builder()
.with_linked_module("external_contract::counter_contract", counter_code)?
.compile_note_script(network_note_code)?;
let note_script_root = note_script.root();
let tx_script = client
.code_builder()
.with_linked_module("external_contract::counter_contract", counter_code)?
.compile_tx_script(script_code)?;
let tx_script_root = tx_script.root();
// Compile the counter MASM into an account component
let counter_slot_name =
StorageSlotName::new("miden::tutorials::counter").expect("valid slot name");
let component_code = client
.code_builder()
.compile_component_code("external_contract::counter_contract", counter_code)?;
let counter_component = AccountComponent::new(
component_code,
vec![StorageSlot::with_value(
counter_slot_name.clone(),
[Felt::new_unchecked(0); 4].into(),
)],
AccountComponentMetadata::new("external_contract::counter_contract"),
)?;
// Generate a random seed for the account
let mut init_seed = [0_u8; 32];
client.rng().fill_bytes(&mut init_seed);
// Build the network account: public + `AuthNetworkAccount` with the note-script
// root allowlisted (this is what makes it a network account) and the deploy
// tx-script root allowlisted (so the auth procedure accepts the STEP 3 deploy).
let network_auth = AuthNetworkAccount::with_allowed_notes(BTreeSet::from([note_script_root]))?
.with_allowed_tx_scripts(BTreeSet::from([tx_script_root]));
let counter_contract = AccountBuilder::new(init_seed)
.account_type(AccountType::Public)
.with_auth_component(network_auth)
.with_component(counter_component)
.build()
.unwrap();
client.add_account(&counter_contract, false).await.unwrap();
println!(
"contract id: {:?}",
counter_contract.id().to_bech32(NetworkId::Testnet)
);
// -------------------------------------------------------------------------
// STEP 3: Deploy Network Account with Transaction Script
// -------------------------------------------------------------------------
println!("\n[STEP 3] Deploy network counter smart contract");
// Reuse the `tx_script` compiled in STEP 2 (its root is allowlisted on the
// account, so the network auth procedure accepts this deploy transaction).
let tx_increment_request = TransactionRequestBuilder::new()
.custom_script(tx_script)
.build()
.unwrap();
let tx_id = client
.submit_new_transaction(counter_contract.id(), tx_increment_request)
.await
.unwrap();
println!(
"View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
tx_id
);
// Wait for the transaction to be committed
wait_for_tx(&mut client, tx_id).await.unwrap();
// -------------------------------------------------------------------------
// STEP 4: Prepare & Create the Network Note
// -------------------------------------------------------------------------
println!("\n[STEP 4] Creating a network note for network counter contract");
// Create and submit the network note that will increment the counter
// Generate a random serial number for the note
let serial_num = client.rng().draw_word();
// Reuse the `note_script` compiled in STEP 2 (its root is allowlisted on the
// account, so the network transaction builder will execute this note).
let note_storage = NoteStorage::new([].to_vec())?;
let recipient = NoteRecipient::new(serial_num, note_script, note_storage);
// Set up note metadata - tag it with the counter contract ID so it gets consumed
let tag = NoteTag::with_account_target(counter_contract.id());
let attachment = NetworkAccountTarget::new(counter_contract.id(), NoteExecutionHint::Always)
.map_err(|e| NoteError::other(e.to_string()))?
.into();
let metadata = PartialNoteMetadata::new(alice_account.id(), NoteType::Public).with_tag(tag);
let attachments = NoteAttachments::new(vec![attachment]).unwrap();
// Create the complete note
let increment_note =
Note::with_attachments(NoteAssets::default(), metadata, recipient, attachments);
// Build and submit the transaction containing the note
let note_req = TransactionRequestBuilder::new()
.own_output_notes(vec![increment_note])
.build()?;
let note_tx_id = client
.submit_new_transaction(alice_account.id(), note_req)
.await?;
println!(
"View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
note_tx_id
);
client.sync_state().await?;
println!("network increment note creation tx submitted, waiting for onchain commitment");
// Wait for the note transaction to be committed
wait_for_tx(&mut client, note_tx_id).await.unwrap();
// Waiting for network note to be picked up by the network transaction builder
sleep(Duration::from_secs(6)).await;
let mut last_val = None;
for _ in 0..10 {
client.sync_state().await?;
// Checking updated state
let new_account_state = client.get_account(counter_contract.id()).await.unwrap();
if let Some(account) = new_account_state.as_ref() {
let count: Word = account
.storage()
.get_item(&counter_slot_name)
.unwrap()
.into();
let val = count[0].as_canonical_u64();
if val >= 2 {
println!("🔢 Final counter value: {}", val);
return Ok(());
}
last_val = Some(val);
}
// Give the network note builder time to process the note.
sleep(Duration::from_secs(6)).await;
}
// The network note was submitted, but it is executed asynchronously by the
// network transaction builder. If the counter has not reached 2 within the
// polling window, the tutorial's final state is unconfirmed, so fail rather
// than claim success.
if let Some(val) = last_val {
Err(format!(
"Counter did not reach the expected value 2 within the timeout (last observed {}). \
The network note was submitted but its execution is still pending on the network \
transaction builder; re-run or check Midenscan.",
val
)
.into())
} else {
Err("Counter state was not available within the timeout; the network note execution is still pending."
.into())
}
}
Step 7: Running the Example
To run the complete network transaction example:
cd rust-client
cargo run --release --bin network_notes_counter_contract
Expected output:
Latest block: 486537
[STEP 1] Creating a new account for Alice
Alice's account ID: "mtst1aqcmenmxlqkw8ugn005s7r09kq0xpqp9"
[STEP 2] Creating a network counter smart contract
one or more warnings were emitted
one or more warnings were emitted
one or more warnings were emitted
contract id: "mtst1aqtvag789v45tvtqdaknugytf5d5gxxu"
[STEP 3] Deploy network counter smart contract
View transaction on MidenScan: https://testnet.midenscan.com/tx/0x37c202efb825f0c7a55bd4416858fbe2d30064a5b1e557876a0053ed5307607f
Transaction 0x37c202efb825f0c7a55bd4416858fbe2d30064a5b1e557876a0053ed5307607f not yet committed. Waiting...
✅ transaction 0x37c202efb825f0c7a55bd4416858fbe2d30064a5b1e557876a0053ed5307607f committed
[STEP 4] Creating a network note for network counter contract
View transaction on MidenScan: https://testnet.midenscan.com/tx/0xa5ec0baa9443a0f5aa056bd4c0e4583c5c3f5a5163aa0536b469a0255b489f75
network increment note creation tx submitted, waiting for onchain commitment
Transaction 0xa5ec0baa9443a0f5aa056bd4c0e4583c5c3f5a5163aa0536b469a0255b489f75 not yet committed. Waiting...
✅ transaction 0xa5ec0baa9443a0f5aa056bd4c0e4583c5c3f5a5163aa0536b469a0255b489f75 committed
🔢 Final counter value: 2
Summary
Network transactions on Miden enable powerful use cases by allowing the operator to execute transactions on behalf of users. The key steps are:
- Create user account: Standard account creation for interaction
- Create network account: Build a public account (
AccountType::Public) with theAuthNetworkAccountauth component, allowlisting the note-script root (this is what marks it as a network account) and the deploy transaction-script root - Deploy with transaction script: Ensures the contract is registered on-chain
- Interact with network notes: Users create public notes that the operator executes
The same MASM code works for both regular and network contracts — the difference is purely in the Rust configuration (the AuthNetworkAccount auth component and its allowlists). This makes network transactions a powerful tool for building applications like AMMs where multiple users need to interact with shared state efficiently.
Continue learning
Next tutorial: How To Create Notes with Custom Logic