Notes & Transactions
Miden's transaction model is uniquely powerful, combining private asset transfers through notes with zero-knowledge proofs. Let's explore how to mint, consume, and send tokens using this innovative approach.
Understanding Miden's Transaction Model
Traditional blockchains move tokens directly between account balances. Miden uses a more sophisticated note-based system that provides enhanced privacy and flexibility.
Think of Notes Like Sealed Envelopes:
- Alice puts 100 tokens in a sealed envelope (note) addressed to Bob
- She posts the envelope to the public board (network)
- Only Bob can open envelopes addressed to him
- When Bob opens it, the 100 tokens move to his vault
Key Components:
- Notes: Sealed containers that carry data and assets between accounts
- P2ID (Pay-To-ID) Notes: Notes addressed to a specific account ID (like Bob's address)
- Nullifiers: Prevent someone from opening the same envelope twice
- Zero-Knowledge Proofs: Prove transactions are valid without revealing private details
The Two-Transaction Model
A core principle of Miden is that a transaction is the state transition of a single account. This means each transaction only modifies one account's state, which enables parallel execution and strong privacy guarantees.
Miden uses a two-transaction model for asset transfers that provides enhanced privacy and scalability:
Transaction 1: Sender Creates Note
- Alice's account creates a P2ID (Pay-To-ID) note containing 100 tokens
- The note specifies Bob as the only valid consumer
- Alice's balance decreases, note is available for consumption
- Alice's transaction is complete and final
Transaction 2: Recipient Consumes Note
- Bob's client discovers the note (addressed to his ID)
- Bob creates a transaction to consume the note
- Tokens move from the note into Bob's vault
- Bob's balance increases, note is nullified
Benefits
This approach provides several advantages over direct transfers:
- Privacy: Alice and Bob's transactions are unlinkable
- Parallelization: Multiple transactions can be processed concurrently, enabling simultaneous creation of notes.
- Flexibility: Notes can include complex conditions (time locks, multi-sig, etc.)
- Scalability: No global state synchronization required
Set Up Development Environment
To run the code examples in this guide, you'll need to set up a development environment. If you haven't already, follow the setup instructions in the Accounts guide.
Each account that submits a transaction needs native fee tokens, including a faucet that mints TEST. The bootstrap below pauses each program so you can fund a newly created account before its first transaction.
Bootstrap native fee funding
Add the following shared helpers once, then use them with each program on this page. In the Rust project, add pub mod funding; to the existing integration/src/lib.rs file.
Start a program and keep its terminal or browser tab running after it prints the account IDs. For each prompt, request a public native-fee funding note for that displayed account from a trusted faucet on the same network, then paste only the funding note ID. Do not consume the note with the CLI or another client store: the running program must discover and consume it. Request enough native tokens to cover this bootstrap transaction and the account's later transactions in the example; the required amount depends on network fees.
use miden_client::{
account::AccountId,
auth::TransactionAuthenticator,
note::{Note, NoteId},
store::TransactionFilter,
transaction::{TransactionRequestBuilder, TransactionStatus},
Client,
};
use std::io::{self, Write};
use tokio::time::{sleep, Duration, Instant};
const WAIT_TIMEOUT: Duration = Duration::from_secs(300);
pub async fn fund_account<AUTH>(
client: &mut Client<AUTH>,
account_id: AccountId,
) -> anyhow::Result<()>
where
AUTH: TransactionAuthenticator + Sync + 'static,
{
println!("Request a public native-fee funding note for {account_id}.");
print!("Paste its note ID here: ");
io::stdout().flush()?;
let mut note_id = String::new();
io::stdin().read_line(&mut note_id)?;
let note_id = NoteId::try_from_hex(note_id.trim())?;
let deadline = Instant::now() + WAIT_TIMEOUT;
let funding_note: Note = loop {
client.sync_state().await?;
let notes = client.get_consumable_notes(Some(account_id)).await?;
if let Some((record, _)) = notes
.into_iter()
.find(|(record, _)| record.id() == Some(note_id))
{
break record.try_into()?;
}
if Instant::now() >= deadline {
anyhow::bail!(
"Timed out waiting for funding note {note_id}; verify its ID, visibility, and network"
);
}
sleep(Duration::from_secs(2)).await;
};
let request = TransactionRequestBuilder::new().build_consume_notes(vec![funding_note])?;
let tx_id = client.submit_new_transaction(account_id, request).await?;
println!("Native funding transaction submitted, ID: {}", tx_id.to_hex());
let deadline = Instant::now() + WAIT_TIMEOUT;
loop {
client.sync_state().await?;
let records = client
.get_transactions(TransactionFilter::Ids(vec![tx_id]))
.await?;
if let Some(record) = records.first() {
match &record.status {
TransactionStatus::Committed { .. } => {
println!("Native funding confirmed for {account_id}");
return Ok(());
},
TransactionStatus::Discarded(cause) => {
anyhow::bail!("Native funding transaction was discarded: {cause}");
},
TransactionStatus::Pending => {},
}
}
if Instant::now() >= deadline {
anyhow::bail!(
"Timed out waiting for transaction {} to commit; check this ID before retrying",
tx_id.to_hex()
);
}
sleep(Duration::from_secs(2)).await;
}
}
import {
type AccountId,
MidenClient,
NoteId,
} from "@miden-sdk/miden-sdk";
const WAIT_TIMEOUT_MS = 300_000;
export async function fundAccount(
client: MidenClient,
accountId: AccountId,
): Promise<void> {
console.log(
`Request a public native-fee funding note for ${accountId.toString()}.`,
);
const enteredId = window.prompt("Paste its note ID here:");
if (!enteredId?.trim()) {
throw new Error("A native funding note ID is required");
}
const noteId = NoteId.fromHex(enteredId.trim());
const normalizedId = noteId.toString();
const deadline = Date.now() + WAIT_TIMEOUT_MS;
while (Date.now() < deadline) {
await client.sync();
const notes = await client.notes.listAvailable({ account: accountId });
const fundingNote = notes.find(
(note) => note.id()?.toString() === normalizedId,
);
if (fundingNote) {
const { txId } = await client.transactions.consume({
account: accountId,
notes: [fundingNote],
});
console.log(
"Native funding transaction submitted, ID:",
txId.toHex(),
);
await client.transactions.waitFor(txId, {
timeout: WAIT_TIMEOUT_MS,
});
await client.sync();
console.log("Native funding confirmed for", accountId.toString());
return;
}
await new Promise<void>((resolve) => window.setTimeout(resolve, 2_000));
}
throw new Error(
`Timed out waiting for funding note ${normalizedId}; verify its ID, visibility, and network`,
);
}
Minting Tokens
What is Minting? Minting in Miden creates new tokens and packages them into a P2ID note (Pay-to-ID note) addressed to a specific account. Unlike traditional blockchains where tokens appear directly in your balance, Miden uses a two-step process:
- Faucet mints tokens → Creates a P2ID note containing the tokens
- Recipient consumes the note → Tokens move into their account vault
Key Concepts:
- P2ID Note: A note that can only be consumed by the account it's addressed to
- NoteType: Determines visibility.
Publicnote details are stored onchain.Privatenote details must reach the consumer separately; their commitments, metadata, and attachments remain public. - FungibleAsset: Represents tokens that can be divided and exchanged (like currencies)
Let's see this in action:
use miden_client::{
account::{
component::{
AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy,
TokenName, TokenPolicyManager, TransferPolicy,
create_singlesig_user_fungible_faucet,
},
AccountBuilder, AccountType,
},
asset::{AssetAmount, FungibleAsset, TokenSymbol},
auth::{Approver, AuthSecretKey},
builder::ClientBuilder,
keystore::{FilesystemKeyStore, Keystore},
note::NoteType,
rpc::Endpoint,
transaction::TransactionRequestBuilder,
};
use miden_client_sqlite_store::ClientBuilderSqliteExt;
use rand::Rng;
use std::sync::Arc;
use integration::funding::fund_account;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize RPC connection
let endpoint = Endpoint::testnet();
let timeout_ms = 10_000;
// Initialize keystore
let keystore_path = std::path::PathBuf::from("./keystore");
let keystore =
Arc::new(FilesystemKeyStore::new(keystore_path).unwrap());
let store_path = std::path::PathBuf::from("./store.sqlite3");
// Initialize client to connect with the Miden Testnet.
// NOTE: The client is our entry point to the Miden network.
// All interactions with the network go through the client.
let mut client = ClientBuilder::new()
.grpc_client(&endpoint, Some(timeout_ms))
.sqlite_store(store_path)
.authenticator(keystore.clone())
.build()
.await?;
client.sync_state().await?;
//------------------------------------------------------------
// CREATING A FAUCET AND MINTING TOKENS
//------------------------------------------------------------
// Account seeds
let mut alice_seed = [0u8; 32];
client.rng().fill_bytes(&mut alice_seed);
let mut faucet_seed = [0u8; 32];
client.rng().fill_bytes(&mut faucet_seed);
// Faucet parameters
let symbol = TokenSymbol::new("TEST")?;
let decimals = 8;
let max_supply = AssetAmount::from(1_000_000u32);
// Generate key pair
let alice_key_pair = AuthSecretKey::new_falcon512_poseidon2();
let faucet_key_pair = AuthSecretKey::new_falcon512_poseidon2();
// Build the account
let account_builder = AccountBuilder::new(alice_seed)
.account_type(AccountType::Public)
.with_component(AuthSingleSig::new(Approver::new(
alice_key_pair.public_key().to_commitment(),
AuthScheme::Falcon512Poseidon2,
)))
.with_component(BasicWallet);
// Build the faucet
let faucet = FungibleFaucet::builder()
.name(TokenName::new("Test Token")?)
.symbol(symbol)
.decimals(decimals)
.max_supply(max_supply)
.build()?;
let policies = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::allow_all())
.active_burn_policy(BurnPolicy::allow_all())
.active_send_policy(TransferPolicy::allow_all())
.active_receive_policy(TransferPolicy::allow_all())
.build();
let alice_account = account_builder.build()?;
let faucet_auth = AuthSingleSig::new(Approver::new(
faucet_key_pair.public_key().to_commitment(),
AuthScheme::Falcon512Poseidon2,
));
let faucet_account = create_singlesig_user_fungible_faucet(
faucet_seed,
faucet,
faucet_auth,
policies,
AccountType::Public,
)?;
println!("Alice's account ID: {}", alice_account.id().to_hex());
println!("Faucet account ID: {}", faucet_account.id().to_hex());
// Add accounts to client
client.add_account(&alice_account, false).await?;
client.add_account(&faucet_account, false).await?;
// Add keys to keystore
keystore.add_key(&alice_key_pair, alice_account.id()).await?;
keystore.add_key(&faucet_key_pair, faucet_account.id()).await?;
// Keep this program running while you fund the faucet at the prompt.
fund_account(&mut client, faucet_account.id()).await?;
let amount: u64 = 1000;
// The faucet account ID encodes callback support for the transfer policies above.
let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?;
// Build transaction request to mint fungible asset to Alice's account
// NOTE: This transaction will create a P2ID note (a Miden note containing the minted asset)
// for Alice's account. Alice will be able to consume these notes to get the fungible asset in her vault
println!("Minting 1000 tokens to Alice...");
let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset(
fungible_asset,
alice_account.id(),
NoteType::Public,
client.rng(),
)?;
// Create transaction and submit it to create P2ID notes for Alice's account
let tx_id = client
.submit_new_transaction(faucet_account.id(), transaction_request)
.await?;
client.sync_state().await?;
println!(
"Mint transaction submitted successfully, ID: {}",
tx_id.to_hex()
);
Ok(())
}
import { MidenClient } from "@miden-sdk/miden-sdk";
import { fundAccount } from "./funding";
export async function demo() {
// Initialize client to connect with the Miden Testnet.
const client = await MidenClient.createTestnet();
// Creating Alice's account
const alice = await client.accounts.create({
storage: "public", // Public: account state is visible onchain
});
console.log("Alice's account ID:", alice.id().toString());
// Creating a faucet account
const decimals = 8;
const maxSupply = 10_000_000n * 10n ** BigInt(decimals);
const faucet = await client.accounts.create({
type: 0, // Fungible faucet
symbol: "TEST",
decimals,
maxSupply,
storage: "public",
});
console.log("Faucet account ID:", faucet.id().toString());
// Keep this browser tab running while you fund the faucet at the prompt.
await fundAccount(client, faucet.id());
// Mint 1000 tokens to Alice.
// This creates a P2ID note containing the asset; Alice consumes it
// to actually receive the tokens in her vault (see the next section).
console.log("Minting 1000 tokens to Alice...");
const { txId } = await client.transactions.mint({
account: faucet.id(), // faucet is the executing account
to: alice.id(),
amount: 1000n,
type: "public", // note visibility
});
console.log("Mint transaction submitted successfully, ID:", txId.toHex());
}
Expected output
Alice's account ID: 0x5b2840a923dedc102ea67e0c1eba3c
Faucet account ID: 0x29dd1dc628d2842032e751ed1b5da7
Minting 1000 tokens to Alice...
Mint transaction submitted successfully, ID: 0x7a2dbde87ea2f4d41b396d6d3f6bdb9a8d7e2a51555fa57064a1657ad70fca06
Consuming Notes
Why Consume Notes? After minting creates a P2ID note containing tokens, the recipient must consume the note to actually receive the tokens in their account vault. This two-step process provides several benefits:
- Privacy: The mint transaction and consume transaction are unlinkable
- Flexibility: Recipients can consume notes when they choose
- Atomic Operations: Each step either succeeds completely or fails safely
The Process:
- Find consumable notes addressed to your account
- Create a consume transaction referencing the note IDs
- Submit the transaction to move tokens into your vault
Here's how to consume notes programmatically:
This program includes the setup and minting steps from the previous section and requires the native fee funding described above. The new consume logic starts at the CONSUMING P2ID NOTES comment.
use miden_client::{
account::{
component::{
AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy,
TokenName, TokenPolicyManager, TransferPolicy,
create_singlesig_user_fungible_faucet,
},
Account, AccountBuilder, AccountType,
},
asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol},
auth::{Approver, AuthSecretKey},
builder::ClientBuilder,
keystore::{FilesystemKeyStore, Keystore},
note::NoteType,
rpc::Endpoint,
transaction::TransactionRequestBuilder,
};
use miden_client_sqlite_store::ClientBuilderSqliteExt;
use rand::Rng;
use std::sync::Arc;
use tokio::time::Duration;
use integration::funding::fund_account;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize RPC connection
let endpoint = Endpoint::testnet();
let timeout_ms = 10_000;
// Initialize keystore
let keystore_path = std::path::PathBuf::from("./keystore");
let keystore =
Arc::new(FilesystemKeyStore::new(keystore_path).unwrap());
let store_path = std::path::PathBuf::from("./store.sqlite3");
// Initialize client to connect with the Miden Testnet.
// NOTE: The client is our entry point to the Miden network.
// All interactions with the network go through the client.
let mut client = ClientBuilder::new()
.grpc_client(&endpoint, Some(timeout_ms))
.sqlite_store(store_path)
.authenticator(keystore.clone())
.build()
.await?;
client.sync_state().await?;
//------------------------------------------------------------
// CREATING A FAUCET AND MINTING TOKENS
//------------------------------------------------------------
// Account seeds
let mut alice_seed = [0u8; 32];
client.rng().fill_bytes(&mut alice_seed);
let mut faucet_seed = [0u8; 32];
client.rng().fill_bytes(&mut faucet_seed);
// Faucet parameters
let symbol = TokenSymbol::new("TEST")?;
let decimals = 8;
let max_supply = AssetAmount::from(1_000_000u32);
// Generate key pair
let alice_key_pair = AuthSecretKey::new_falcon512_poseidon2();
let faucet_key_pair = AuthSecretKey::new_falcon512_poseidon2();
// Build the account
let account_builder = AccountBuilder::new(alice_seed)
.account_type(AccountType::Public)
.with_component(AuthSingleSig::new(Approver::new(
alice_key_pair.public_key().to_commitment(),
AuthScheme::Falcon512Poseidon2,
)))
.with_component(BasicWallet);
// Build the faucet
let faucet = FungibleFaucet::builder()
.name(TokenName::new("Test Token")?)
.symbol(symbol)
.decimals(decimals)
.max_supply(max_supply)
.build()?;
let policies = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::allow_all())
.active_burn_policy(BurnPolicy::allow_all())
.active_send_policy(TransferPolicy::allow_all())
.active_receive_policy(TransferPolicy::allow_all())
.build();
let alice_account = account_builder.build()?;
let faucet_auth = AuthSingleSig::new(Approver::new(
faucet_key_pair.public_key().to_commitment(),
AuthScheme::Falcon512Poseidon2,
));
let faucet_account = create_singlesig_user_fungible_faucet(
faucet_seed,
faucet,
faucet_auth,
policies,
AccountType::Public,
)?;
println!("Alice's account ID: {}", alice_account.id().to_hex());
println!("Faucet account ID: {}", faucet_account.id().to_hex());
// Add accounts to client
client.add_account(&alice_account, false).await?;
client.add_account(&faucet_account, false).await?;
// Add keys to keystore
keystore.add_key(&alice_key_pair, alice_account.id()).await?;
keystore.add_key(&faucet_key_pair, faucet_account.id()).await?;
// The faucet mints; Alice later consumes. Fund both before either transaction.
fund_account(&mut client, faucet_account.id()).await?;
fund_account(&mut client, alice_account.id()).await?;
let amount: u64 = 1000;
// The faucet account ID encodes callback support for the transfer policies above.
let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?;
// Build transaction request to mint fungible asset to Alice's account
// NOTE: This transaction will create a P2ID note (a Miden note containing the minted asset)
// for Alice's account. Alice will be able to consume these notes to get the fungible asset in her vault
println!("Minting 1000 tokens to Alice...");
let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset(
fungible_asset,
alice_account.id(),
NoteType::Public,
client.rng(),
)?;
// Create transaction and submit it to create P2ID notes for Alice's account
let tx_id = client
.submit_new_transaction(faucet_account.id(), transaction_request)
.await?;
client.sync_state().await?;
println!(
"Mint transaction submitted successfully, ID: {}",
tx_id.to_hex()
);
//------------------------------------------------------------
// CONSUMING P2ID NOTES
//------------------------------------------------------------
// Public notes must be committed to a block before they can be consumed.
// Poll until the network includes our mint note in a block.
println!("Waiting for note to be consumable...");
loop {
// Sync state to get the latest block
client.sync_state().await?;
let consumable_notes = client
.get_consumable_notes(Some(alice_account.id()))
.await?;
if consumable_notes.is_empty() {
tokio::time::sleep(Duration::from_secs(2)).await;
continue;
}
let notes: Vec<miden_client::note::Note> = consumable_notes
.into_iter()
.map(|(record, _)| record.try_into().expect("Failed to convert to Note"))
.collect();
let consume_tx_request = TransactionRequestBuilder::new().build_consume_notes(notes)?;
// Create transaction and submit it to consume notes
let consume_tx_id = client
.submit_new_transaction(alice_account.id(), consume_tx_request)
.await?;
println!(
"Consume transaction submitted successfully, ID: {}",
consume_tx_id.to_hex()
);
client.sync_state().await?;
let alice_account: Account = client
.get_account(alice_account.id())
.await?
.ok_or_else(|| anyhow::anyhow!("Account not found"))?
.try_into()?;
let vault = alice_account.vault();
let asset_id = AssetId::new_fungible(faucet_account.id());
println!(
"Alice's TEST token balance: {}",
vault.get_balance(asset_id)?
);
break; // Exit the loop after consuming the note
}
Ok(())
}
import { MidenClient } from "@miden-sdk/miden-sdk";
import { fundAccount } from "./funding";
export async function demo() {
// Initialize client to connect with the Miden Testnet.
const client = await MidenClient.createTestnet();
// Creating Alice's account
const alice = await client.accounts.create({
storage: "public",
});
console.log("Alice's account ID:", alice.id().toString());
// Creating a faucet account
const decimals = 8;
const maxSupply = 10_000_000n * 10n ** BigInt(decimals);
const faucet = await client.accounts.create({
type: 0, // Fungible faucet
symbol: "TEST",
decimals,
maxSupply,
storage: "public",
});
console.log("Faucet account ID:", faucet.id().toString());
// The faucet mints; Alice later consumes. Fund both before either transaction.
await fundAccount(client, faucet.id());
await fundAccount(client, alice.id());
// Mint 1000 tokens to Alice. Creates a P2ID note that she'll consume.
console.log("Minting 1000 tokens to Alice...");
const mintResult = await client.transactions.mint({
account: faucet.id(),
to: alice.id(),
amount: 1000n,
type: "public",
waitForConfirmation: true,
});
console.log(
"Mint transaction submitted successfully, ID:",
mintResult.txId.toHex(),
);
// List notes available to Alice and consume them — tokens move into her vault.
console.log("Waiting for note to be consumable...");
const notes = await client.notes.listAvailable({ account: alice.id() });
const consumeResult = await client.transactions.consume({
account: alice.id(),
notes: [notes[0]],
waitForConfirmation: true,
});
console.log(
"Consume transaction submitted successfully, ID:",
consumeResult.txId.toHex(),
);
// Fetch Alice again so her vault reflects the consumed note.
const updatedAlice = await client.accounts.get(alice.id());
if (!updatedAlice) {
throw new Error("Alice's account was not found");
}
const balance = updatedAlice.vault().getBalance(faucet.id());
console.log("Alice's TEST token balance:", Number(balance));
}
Expected output
Alice's account ID: 0x5b2840a923dedc102ea67e0c1eba3c
Faucet account ID: 0x29dd1dc628d2842032e751ed1b5da7
Minting 1000 tokens to Alice...
Mint transaction submitted successfully, ID: 0x7a2dbde87ea2f4d41b396d6d3f6bdb9a8d7e2a51555fa57064a1657ad70fca06
Waiting for note to be consumable...
Consume transaction submitted successfully, ID: 0xa75872c498ee71cd6725aef9411d2559094cec1e1e89670dbf99c60bb8843481
Alice's TEST token balance: 1000
Sending Tokens Between Accounts
How Sending Works in Miden Sending tokens between accounts follows the same note-based pattern. The sender creates a new P2ID note containing tokens from their vault and addresses it to the recipient:
The Flow:
- Sender creates P2ID note containing tokens and recipient's account ID
- Sender submits transaction - their balance decreases, note is published
- Recipient discovers note addressed to their account ID
- Recipient consumes note - tokens move into their vault
Alice and Bob execute separate transactions. Their visibility depends on the account and note types they use; public notes expose their details.
Let's implement the complete flow - mint, consume, then send:
This program includes all previous steps and requires the native fee funding described above. The new send logic starts at the SENDING TOKENS TO BOB comment.
use miden_client::{
account::{
component::{
AuthScheme, AuthSingleSig, BasicWallet, BurnPolicy, FungibleFaucet, MintPolicy,
TokenName, TokenPolicyManager, TransferPolicy,
create_singlesig_user_fungible_faucet,
},
Account, AccountBuilder, AccountType,
},
asset::{AssetAmount, AssetId, FungibleAsset, TokenSymbol},
auth::{Approver, AuthSecretKey},
builder::ClientBuilder,
keystore::{FilesystemKeyStore, Keystore},
note::{NoteType, P2idNote},
rpc::Endpoint,
transaction::TransactionRequestBuilder,
};
use miden_client_sqlite_store::ClientBuilderSqliteExt;
use rand::Rng;
use std::sync::Arc;
use tokio::time::Duration;
use integration::funding::fund_account;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize RPC connection
let endpoint = Endpoint::testnet();
let timeout_ms = 10_000;
// Initialize keystore
let keystore_path = std::path::PathBuf::from("./keystore");
let keystore =
Arc::new(FilesystemKeyStore::new(keystore_path).unwrap());
let store_path = std::path::PathBuf::from("./store.sqlite3");
// Initialize client to connect with the Miden Testnet.
// NOTE: The client is our entry point to the Miden network.
// All interactions with the network go through the client.
let mut client = ClientBuilder::new()
.grpc_client(&endpoint, Some(timeout_ms))
.sqlite_store(store_path)
.authenticator(keystore.clone())
.build()
.await?;
client.sync_state().await?;
//------------------------------------------------------------
// CREATING A FAUCET AND MINTING TOKENS
//------------------------------------------------------------
// Account seeds
let mut alice_seed = [0u8; 32];
client.rng().fill_bytes(&mut alice_seed);
let mut faucet_seed = [0u8; 32];
client.rng().fill_bytes(&mut faucet_seed);
// Faucet parameters
let symbol = TokenSymbol::new("TEST")?;
let decimals = 8;
let max_supply = AssetAmount::from(1_000_000u32);
// Generate key pair
let alice_key_pair = AuthSecretKey::new_falcon512_poseidon2();
let faucet_key_pair = AuthSecretKey::new_falcon512_poseidon2();
// Build the account
let account_builder = AccountBuilder::new(alice_seed)
.account_type(AccountType::Public)
.with_component(AuthSingleSig::new(Approver::new(
alice_key_pair.public_key().to_commitment(),
AuthScheme::Falcon512Poseidon2,
)))
.with_component(BasicWallet);
// Build the faucet
let faucet = FungibleFaucet::builder()
.name(TokenName::new("Test Token")?)
.symbol(symbol)
.decimals(decimals)
.max_supply(max_supply)
.build()?;
let policies = TokenPolicyManager::builder()
.active_mint_policy(MintPolicy::allow_all())
.active_burn_policy(BurnPolicy::allow_all())
.active_send_policy(TransferPolicy::allow_all())
.active_receive_policy(TransferPolicy::allow_all())
.build();
let alice_account = account_builder.build()?;
let faucet_auth = AuthSingleSig::new(Approver::new(
faucet_key_pair.public_key().to_commitment(),
AuthScheme::Falcon512Poseidon2,
));
let faucet_account = create_singlesig_user_fungible_faucet(
faucet_seed,
faucet,
faucet_auth,
policies,
AccountType::Public,
)?;
println!("Alice's account ID: {}", alice_account.id().to_hex());
println!("Faucet account ID: {}", faucet_account.id().to_hex());
// Add accounts to client
client.add_account(&alice_account, false).await?;
client.add_account(&faucet_account, false).await?;
// Add keys to keystore
keystore.add_key(&alice_key_pair, alice_account.id()).await?;
keystore.add_key(&faucet_key_pair, faucet_account.id()).await?;
// The faucet mints; Alice later consumes and sends. Fund both first.
fund_account(&mut client, faucet_account.id()).await?;
fund_account(&mut client, alice_account.id()).await?;
let amount: u64 = 1000;
// The faucet account ID encodes callback support for the transfer policies above.
let fungible_asset = FungibleAsset::new(faucet_account.id(), amount)?;
// Build transaction request to mint fungible asset to Alice's account
// NOTE: This transaction will create a P2ID note (a Miden note containing the minted asset)
// for Alice's account. Alice will be able to consume these notes to get the fungible asset in her vault
println!("Minting 1000 tokens to Alice...");
let transaction_request = TransactionRequestBuilder::new().build_mint_fungible_asset(
fungible_asset,
alice_account.id(),
NoteType::Public,
client.rng(),
)?;
// Create transaction and submit it to create P2ID notes for Alice's account
let tx_id = client
.submit_new_transaction(faucet_account.id(), transaction_request)
.await?;
client.sync_state().await?;
println!(
"Mint transaction submitted successfully, ID: {}",
tx_id.to_hex()
);
//------------------------------------------------------------
// CONSUMING P2ID NOTES
//------------------------------------------------------------
// Public notes must be committed to a block before they can be consumed.
// Poll until the network includes our mint note in a block.
println!("Waiting for note to be consumable...");
loop {
// Sync state to get the latest block
client.sync_state().await?;
let consumable_notes = client
.get_consumable_notes(Some(alice_account.id()))
.await?;
if consumable_notes.is_empty() {
tokio::time::sleep(Duration::from_secs(2)).await;
continue;
}
let notes: Vec<miden_client::note::Note> = consumable_notes
.into_iter()
.map(|(record, _)| record.try_into().expect("Failed to convert to Note"))
.collect();
let consume_tx_request = TransactionRequestBuilder::new().build_consume_notes(notes)?;
// Create transaction and submit it to consume notes
let consume_tx_id = client
.submit_new_transaction(alice_account.id(), consume_tx_request)
.await?;
println!(
"Consume transaction submitted successfully, ID: {}",
consume_tx_id.to_hex()
);
client.sync_state().await?;
let alice_account: Account = client
.get_account(alice_account.id())
.await?
.ok_or_else(|| anyhow::anyhow!("Account not found"))?
.try_into()?;
let vault = alice_account.vault();
let asset_id = AssetId::new_fungible(faucet_account.id());
println!(
"Alice's TEST token balance: {}",
vault.get_balance(asset_id)?
);
break; // Exit the loop after consuming the note
}
//------------------------------------------------------------
// SENDING TOKENS TO BOB
//------------------------------------------------------------
// Create Bob's account so this example is self-contained.
let mut bob_seed = [0u8; 32];
client.rng().fill_bytes(&mut bob_seed);
let bob_key_pair = AuthSecretKey::new_falcon512_poseidon2();
let bob_account = AccountBuilder::new(bob_seed)
.account_type(AccountType::Public)
.with_component(AuthSingleSig::new(Approver::new(
bob_key_pair.public_key().to_commitment(),
AuthScheme::Falcon512Poseidon2,
)))
.with_component(BasicWallet)
.build()?;
client.add_account(&bob_account, false).await?;
keystore.add_key(&bob_key_pair, bob_account.id()).await?;
println!("Bob's account ID: {}", bob_account.id().to_hex());
let bob_account_id = bob_account.id();
let send_amount = 100;
let fungible_asset_to_send = FungibleAsset::new(faucet_account.id(), send_amount)?;
println!("Sending 100 tokens to Bob...");
let p2id_note = P2idNote::builder()
.sender(alice_account.id())
.target(bob_account_id)
.asset(fungible_asset_to_send)
.note_type(NoteType::Public)
.generate_serial_number(client.rng())
.build()?
.into();
// Create transaction request to send P2ID note to Bob
let send_p2id_note_transaction_request = TransactionRequestBuilder::new()
.own_output_notes(vec![p2id_note])
.build()?;
// Create transaction and submit it to send P2ID note to Bob
let send_p2id_note_tx_id = client
.submit_new_transaction(alice_account.id(), send_p2id_note_transaction_request)
.await?;
client.sync_state().await?;
println!(
"Send transaction submitted successfully, ID: {}",
send_p2id_note_tx_id.to_hex()
);
Ok(())
}
import { MidenClient } from "@miden-sdk/miden-sdk";
import { fundAccount } from "./funding";
export async function demo() {
// Initialize client to connect with the Miden Testnet.
const client = await MidenClient.createTestnet();
// Create Alice's account and a faucet.
const alice = await client.accounts.create({
storage: "public",
});
console.log("Alice's account ID:", alice.id().toString());
const decimals = 8;
const maxSupply = 10_000_000n * 10n ** BigInt(decimals);
const faucet = await client.accounts.create({
type: 0, // Fungible faucet
symbol: "TEST",
decimals,
maxSupply,
storage: "public",
});
console.log("Faucet account ID:", faucet.id().toString());
// The faucet mints; Alice later consumes and sends. Fund both first.
await fundAccount(client, faucet.id());
await fundAccount(client, alice.id());
// Mint 1000 tokens to Alice and consume the resulting P2ID note.
console.log("Minting 1000 tokens to Alice...");
const mintResult = await client.transactions.mint({
account: faucet.id(),
to: alice.id(),
amount: 1000n,
type: "public",
waitForConfirmation: true,
});
console.log(
"Mint transaction submitted successfully, ID:",
mintResult.txId.toHex(),
);
console.log("Waiting for note to be consumable...");
const notes = await client.notes.listAvailable({ account: alice.id() });
const consumeResult = await client.transactions.consume({
account: alice.id(),
notes: [notes[0]],
waitForConfirmation: true,
});
console.log(
"Consume transaction submitted successfully, ID:",
consumeResult.txId.toHex(),
);
// Fetch Alice again so her vault reflects the consumed note.
const updatedAlice = await client.accounts.get(alice.id());
if (!updatedAlice) {
throw new Error("Alice's account was not found");
}
const balance = updatedAlice.vault().getBalance(faucet.id());
console.log("Alice's TEST token balance:", Number(balance));
// Create Bob's account so this example is self-contained.
const bob = await client.accounts.create({
storage: "public",
});
console.log("Bob's account ID:", bob.id().toString());
// Send 100 tokens from Alice to Bob.
console.log("Sending 100 tokens to Bob...");
const { txId } = await client.transactions.send({
account: alice.id(),
to: bob.id(),
token: faucet.id(),
amount: 100n,
type: "public",
waitForConfirmation: true,
});
console.log("Send transaction submitted successfully, ID:", txId.toHex());
}
Expected output
Alice's account ID: 0xd6b8bb0ed10b1610282c513501778a
Faucet account ID: 0xe48c43d6ad6496201bcfa585a5a4b6
Minting 1000 tokens to Alice...
Mint transaction submitted successfully, ID: 0x948a0eef754068b3126dd3261b6b54214fa5608fb13c5e5953faf59bad79c75f
Waiting for note to be consumable...
Consume transaction submitted successfully, ID: 0xc69ab84b784120abe858bb536aebda90bd2067695f11d5da93ab0b704f39ad78
Alice's TEST token balance: 1000
Bob's account ID: 0x103f8a1ad4b983104aec0412ab0b0d
Sending 100 tokens to Bob...
Send transaction submitted successfully, ID: 0x51ac27474ade3a54adadd50db6c2b9a2ede254c5f9137f93d7a970f0bc7d66d5
Key Takeaways
Miden's Note-Based Transaction Model:
- Notes enable asset transfers between accounts (both public and private)
- Two-transaction model provides privacy and parallelization benefits
- Zero-knowledge proofs validate transaction execution without revealing details
- P2ID notes target specific recipients using their account IDs
Transaction Flow:
- Mint tokens to create notes containing assets
- Consume notes to add assets to account vaults
- Send tokens using P2ID notes targeted to recipients
- Nullify consumed notes to prevent double-spending
This innovative approach provides unprecedented privacy and flexibility while maintaining the security guarantees of blockchain technology. The note-based model enables scalable, private transactions that can be processed in parallel without global state synchronization.