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
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 account 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.
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 -
Publicnotes are visible onchain and are stored by the Miden network, whilePrivatenotes are not stored by the network and must be exchanged directly between parties via other channels. - FungibleAsset: Represents tokens that can be divided and exchanged (like currencies)
Let's see this in action:
import { WebClient, AccountStorageMode, NoteType } from "@demox-labs/miden-sdk";
export async function demo() {
// 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.
const nodeEndpoint = "https://rpc.testnet.miden.io:443";
const client = await WebClient.createClient(nodeEndpoint);
await client.syncState();
// Creating Alice's account
const alice = await client.newWallet(AccountStorageMode.public(), true);
console.log("Alice's account ID:", alice.id().toString());
// Creating a faucet account
const symbol = "TEST";
const decimals = 8;
const initialSupply = BigInt(10_000_000 * 10 ** decimals);
const faucet = await client.newFaucet(
AccountStorageMode.public(), // Public: account state is visible onchain
false, // Mutable: account code cannot be upgraded later
symbol, // Symbol of the token
decimals, // Number of decimals
initialSupply // Initial supply of tokens
);
console.log("Faucet account ID:", faucet.id().toString());
// Creating mint transaction
console.log("Minting 1000 tokens to Alice...");
const mintTxRequest = client.newMintTransactionRequest(
alice.id(), // Target account (who receives the tokens)
faucet.id(), // Faucet account (who mints the tokens)
NoteType.Public, // Note visibility (public = onchain)
BigInt(1000) // Amount to mint (in base units)
);
const mintTx = await client.newTransaction(faucet.id(), mintTxRequest);
await client.submitTransaction(mintTx);
console.log(
"Mint transaction submitted successfully, ID:",
mintTx.executedTransaction().id().toHex()
);
}
Expected output
Alice's account ID: 0x49e27aa5fa5686102fde8e81b89999
Faucet account ID: 0x9796be9c72f137206676f7821a9968
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:
import {
WebClient,
AccountStorageMode,
NoteType,
ConsumableNoteRecord,
} from "@demox-labs/miden-sdk";
export async function demo() {
// 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.
const nodeEndpoint = "https://rpc.testnet.miden.io:443";
const client = await WebClient.createClient(nodeEndpoint);
await client.syncState();
// Creating Alice's account
const alice = await client.newWallet(AccountStorageMode.public(), true);
console.log("Alice's account ID:", alice.id().toString());
// Creating a faucet account
const symbol = "TEST";
const decimals = 8;
const initialSupply = BigInt(10_000_000 * 10 ** decimals);
const faucet = await client.newFaucet(
AccountStorageMode.public(), // Public: account state is visible onchain
false, // Mutable: account code cannot be upgraded later
symbol, // Symbol of the token
decimals, // Number of decimals
initialSupply // Initial supply of tokens
);
console.log("Faucet account ID:", faucet.id().toString());
// Create 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
console.log("Minting 1000 tokens to Alice...");
const mintTxRequest = client.newMintTransactionRequest(
alice.id(), // Target account (who receives the tokens)
faucet.id(), // Faucet account (who mints the tokens)
NoteType.Public, // Note visibility (public = onchain)
BigInt(1000) // Amount to mint (in base units)
);
const mintTx = await client.newTransaction(faucet.id(), mintTxRequest);
await client.submitTransaction(mintTx);
console.log(
"Mint transaction submitted successfully, ID:",
mintTx.executedTransaction().id().toHex()
);
await client.syncState();
let consumableNotes: ConsumableNoteRecord[] = [];
while (consumableNotes.length === 0) {
// Find consumable notes
consumableNotes = await client.getConsumableNotes(alice.id());
console.log("Waiting for note to be consumable...");
await new Promise((resolve) => setTimeout(resolve, 3000));
}
const noteIds = consumableNotes.map((note) =>
note.inputNoteRecord().id().toString()
);
// Create transaction request to consume notes
// NOTE: This transaction will consume the notes and add the fungible asset to Alice's vault
const consumeTxRequest = client.newConsumeTransactionRequest(noteIds);
const consumeTx = await client.newTransaction(alice.id(), consumeTxRequest);
await client.submitTransaction(consumeTx);
console.log(
"Consume transaction submitted successfully, ID:",
consumeTx.executedTransaction().id().toHex()
);
console.log(
"Alice's TEST token balance:",
Number(alice.vault().getBalance(faucet.id()))
);
await client.syncState();
}
Expected output
Alice's account ID: "0x49e27aa5fa5686102fde8e81b89999"
Faucet account ID: "0x9796be9c72f137206676f7821a9968"
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: Ok(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
This approach means Alice and Bob's transactions are completely separate and unlinkable, providing strong privacy guarantees.
Let's implement the complete flow - mint, consume, then send:
import {
WebClient,
AccountStorageMode,
NoteType,
ConsumableNoteRecord,
AccountId,
} from "@demox-labs/miden-sdk";
export async function demo() {
// 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.
const nodeEndpoint = "https://rpc.testnet.miden.io:443";
const client = await WebClient.createClient(nodeEndpoint);
await client.syncState();
// Creating Alice's account
const alice = await client.newWallet(AccountStorageMode.public(), true);
console.log("Alice's account ID:", alice.id().toString());
// Creating a faucet account
const symbol = "TEST";
const decimals = 8;
const initialSupply = BigInt(10_000_000 * 10 ** decimals);
const faucet = await client.newFaucet(
AccountStorageMode.public(), // Public: account state is visible onchain
false, // Mutable: account code cannot be upgraded later
symbol, // Symbol of the token
decimals, // Number of decimals
initialSupply // Initial supply of tokens
);
console.log("Faucet account ID:", faucet.id().toString());
// Create 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
console.log("Minting 1000 tokens to Alice...");
const mintTxRequest = client.newMintTransactionRequest(
alice.id(), // Target account (who receives the tokens)
faucet.id(), // Faucet account (who mints the tokens)
NoteType.Public, // Note visibility (public = onchain)
BigInt(1000) // Amount to mint (in base units)
);
const mintTx = await client.newTransaction(faucet.id(), mintTxRequest);
await client.submitTransaction(mintTx);
console.log(
"Mint transaction submitted successfully, ID:",
mintTx.executedTransaction().id().toHex()
);
await client.syncState();
let consumableNotes: ConsumableNoteRecord[] = [];
while (consumableNotes.length === 0) {
// Find consumable notes
consumableNotes = await client.getConsumableNotes(alice.id());
console.log("Waiting for note to be consumable...");
await new Promise((resolve) => setTimeout(resolve, 3000));
}
const noteIds = consumableNotes.map((note) =>
note.inputNoteRecord().id().toString()
);
// Create transaction request to consume notes
// NOTE: This transaction will consume the notes and add the fungible asset to Alice's vault
const consumeTxRequest = client.newConsumeTransactionRequest(noteIds);
const consumeTx = await client.newTransaction(alice.id(), consumeTxRequest);
await client.submitTransaction(consumeTx);
console.log(
"Consume transaction submitted successfully, ID:",
consumeTx.executedTransaction().id().toHex()
);
console.log(
"Alice's TEST token balance:",
Number(alice.vault().getBalance(faucet.id()))
);
await client.syncState();
// Send tokens from Alice to Bob
const bobAccountId = "0x599a54603f0cf9000000ed7a11e379";
console.log("Sending 100 tokens to Bob...");
// Build transaction request to send tokens from Alice to Bob
const sendTxRequest = client.newSendTransactionRequest(
alice.id(), // Sender account
AccountId.fromHex(bobAccountId), // Recipient account
faucet.id(), // Asset ID (faucet that created the tokens)
NoteType.Public, // Note visibility
BigInt(100) // Amount to send
);
const sendTx = await client.newTransaction(alice.id(), sendTxRequest);
await client.submitTransaction(sendTx);
console.log("Send transaction submitted successfully!");
await client.syncState();
}
Expected output
Alice's account ID: 0xd6b8bb0ed10b1610282c513501778a
Faucet account ID: 0xe48c43d6ad6496201bcfa585a5a4b6
Minting 1000 tokens to Alice...
Mint transaction submitted successfully, ID: 0x948a0eef754068b3126dd3261b6b54214fa5608fb13c5e5953faf59bad79c75f
Consume transaction submitted successfully, ID: 0xc69ab84b784120abe858bb536aebda90bd2067695f11d5da93ab0b704f39ad78
Alice's TEST token balance: 100
Send 100 tokens to Bob note transaction ID: "0x51ac27474ade3a54adadd50db6c2b9a2ede254c5f9137f93d7a970f0bc7d66d5"
Key Takeaways
Miden's Note-Based Transaction Model:
- Notes enable private asset transfers between accounts
- 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.