Polygon Miden

A rollup for high-throughput, private applications.

Using Polygon Miden, builders can create novel, high-throughput, private applications for payments, DeFi, digital assets, and gaming. Applications and users are secured by Ethereum and AggLayer.

If you want to join the technical discussion, please check out the following:

Info

  • These docs are still work-in-progress.
  • Some topics have been discussed in greater depth, while others require additional clarification.

Status and features

Polygon Miden is currently on release v0.7. This is an early version of the protocol and its components.

Important We expect breaking changes on all components.

At the time of writing, Polygon Miden doesn't offer all the features you may expect from a zkRollup. During 2024, we expect to gradually implement more features.

Feature highlights

Private accounts

The Miden operator only tracks a commitment to account data in the public database. Users can only execute smart contracts when they know the interface.

Private notes

Like private accounts, the Miden operator only tracks a commitment to notes in the public database. Users need to communicate note details to each other off-chain (via a side channel) in order to consume private notes in transactions.

Public accounts

Polygon Miden supports public smart contracts like Ethereum. The code and state of those accounts are visible to the network and anyone can execute transactions against them.

Public notes

As with public accounts, public notes are also supported. That means, the Miden operator publicly stores note data. Note consumption is not private.

Local transaction execution

The Miden client allows for local transaction execution and proving. The Miden operator verifies the proof and, if valid, updates the state DBs with the new data.

Simple smart contracts

Currently, there are three different smart contracts available. A basic wallet smart contract that sends and receives assets, and fungible and non-fungible faucets to mint and burn assets.

All accounts are written in MASM.

P2ID, P2IDR, and SWAP note scripts

Currently, there are three different note scripts available. Two different versions of pay-to-id scripts of which P2IDR is reclaimable, and a swap script that allows for simple token swaps.

Simple block building

The Miden operator running the Miden node builds the blocks containing transactions.

Maintaining state

The Miden node stores all necessary information in its state DBs and provides this information via its RPC endpoints.

Planned features

Warning The following features are at a planning stage only.

Customized smart contracts

Accounts can expose any interface in the future. This is the Miden version of a smart contract. Account code can be arbitrarily complex due to the underlying Turing-complete Miden VM.

Customized note scripts

Users will be able to write their own note scripts using the Miden client. Note scripts are executed during note consumption and they can be arbitrarily complex due to the underlying Turing-complete Miden VM.

Network transactions

Transaction execution and proving can be outsourced to the network and to the Miden operator. Those transactions will be necessary when it comes to public shared state, and they can be useful if the user's device is not powerful enough to prove transactions efficiently.

Rust compiler

In order to write account code, note or transaction scripts, in Rust, there will be a Rust -> Miden Assembly compiler.

Block and epoch proofs

The Miden node will recursively verify transactions and in doing so build batches of transactions, blocks, and epochs.

Benefits of Polygon Miden

  • Ethereum security.
  • Developers can build applications that are infeasible on other systems. For example:
    • on-chain order book exchange due to parallel transaction execution and updatable transactions.
    • complex, incomplete information games due to client-side proving and cheap complex computations.
    • safe wallets due to hidden account state.
  • Better privacy properties than on Ethereum - first web2 privacy, later even stronger self-sovereignty.
  • Transactions can be recalled and updated.
  • Lower fees due to client-side proving.
  • dApps on Miden are safe to use due to account abstraction and compile-time safe Rust smart contracts.

License

Licensed under the MIT license.

This section shows you how to get started with Miden by generating a new Miden account, requesting funds from a public faucet, consuming private notes, and creating public pay-to-id-notes.

By the end of this tutorial, you will have:

  • Configured the Miden client.
  • Connected to a Miden node.
  • Created an account and requested funds from the faucet.
  • Transferred assets between accounts by creating and consuming notes.

Prerequisites

Rust

Download from the Rust website.

In this section, we show you how to create a new local Miden account and how to receive funds from the public Miden faucet website.

Configure the Miden client

The Miden client facilitates interaction with the Miden rollup and provides a way to execute and prove transactions.

Tip Check the Miden client documentation for more information.

  1. If you haven't already done so as part of another tutorial, open your terminal and create a new directory to store the Miden client.

    mkdir miden-client
    cd miden-client
    
  2. Install the Miden client.

    cargo install miden-cli --features concurrent
    

    You can now use the miden --version command, and you should see Miden 0.7.0.

  3. Initialize the client. This creates the miden-client.toml file.

    miden init --network testnet # Creates a miden-client.toml configured with the testnet node's IP
    

Create a new Miden account

  1. Create a new account of type mutable using the following command:

    miden new-wallet --mutable
    
  2. List all created accounts by running the following command:

    miden account -l
    

    You should see something like this:

    Result of listing miden accounts

Save the account ID for a future step.

Request tokens from the public faucet

  1. To request funds from the faucet navigate to the following website: Miden faucet website.

  2. Copy the Account ID printed by the miden account -l command in the previous step. Feel free to change the amount of tokens to issue.

  3. Paste this ID into the Request test tokens input field on the faucet website and click Send Private Note.

Tip You can also click Send Public Note. If you do this, the note's details will be public and you will not need to download and import it, so you can skip to Sync the client.

  1. After a few seconds your browser should download - or prompt you to download - a file called note.mno (mno = Miden note). It contains the funds the faucet sent to your address.

  2. Save this file on your computer, you will need it for the next step.

Import the note into the Miden client

  1. Import the private note that you have received using the following commands:

    miden import <path-to-note>/note.mno
    
  2. You should see something like this:

    Successfully imported note 0x0ff340133840d35e95e0dc2e62c88ed75ab2e383dc6673ce0341bd486fed8cb6
    
  3. Now that the note has been successfully imported, you can view the note's information using the following command:

    miden notes
    
  4. You should see something like this:

    Result of viewing miden notes

Tip: The importance of syncing

  • As you can see, the note is listed as Expected.
  • This is because you have received a private note but have not yet synced your view of the rollup to check that the note is the result of a valid transaction.
  • Hence, before consuming the note we will need to update our view of the rollup by syncing.
  • Many users could have received the same private note, but only one user can consume the note in a transaction that gets verified by the Miden operator.

Sync the client

Do this periodically to keep informed about any updates on the node by running the sync command:

miden sync

You will see something like this as output:

State synced to block 179672
New public notes: 0
Tracked notes updated: 1
Tracked notes consumed: 0
Tracked accounts updated: 0
Commited transactions: 0

Consume the note & receive the funds

  1. Now that we have synced the client, the input-note imported from the faucet should have a Committed status, confirming it exists at the rollup level:

    miden notes
    
  2. You should see something like this:

    Viewing commit height info

  3. Find your account and note id by listing both accounts and notes:

    miden account
    miden notes
    
  4. Consume the note and add the funds from its vault to our account using the following command:

    miden consume-notes --account <Account-Id> <Note-Id>
    
  5. You should see a confirmation message like this:

    Transaction confirmation message

  6. After confirming you can view the new note status by running the following command:

    miden notes
    
  7. You should see something like this:

    Viewing process info

  8. The note is Processing. This means that the proof of the transaction was sent, but there is no network confirmation yet. You can update your view of the rollup by syncing again:

    miden sync
    
  9. After syncing, you should have received confirmation of the consumed note. You should see the note as Consumed after listing the notes:

    miden notes
    

    Viewing consumed note

Amazing! You just have created a client-side zero-knowledge proof locally on your machine and submitted it to the Miden rollup.

Tip You only need to copy the top line of characters of the Note ID.

View confirmations

  1. View your updated account's vault containing the tokens sent by the faucet by running the following command:

    miden account --show <Account-Id>
    
  2. You should now see your accounts vault containing the funds sent by the faucet.

    Viewing account vault with funds

Congratulations!

You have successfully configured and used the Miden client to interact with a Miden rollup and faucet.

You have performed basic Miden rollup operations like submitting proofs of transactions, generating and consuming notes.

For more information on the Miden client, refer to the Miden client documentation.

Debugging tips (clear state and folder)

  • Need a fresh start? All state is maintained in store.sqlite3, located in the directory defined in the miden-client.toml file. If you want to clear all state, delete this file. It recreates on any command execution.

  • Getting an error? Only execute the miden-client command in the folder where your miden-client.toml is located.

In this section, we show you how to make private transactions and send funds to another account using the Miden client.

Important: Prerequisite steps

Create a second account

Tip Remember to use the Miden client documentation for clarifications.

  1. Create a second account to send funds with. Previously, we created a type mutable account (account A). Now, create another mutable (account B) using the following command:

    miden new-wallet --mutable
    
  2. List and view the newly created accounts with the following command:

    miden account -l
    
  3. You should see two accounts:

    Result of listing miden accounts

Transfer assets between accounts

  1. Now we can transfer some of the tokens we received from the faucet to our second account B.

    To do this, run:

    miden send --sender <regular-account-id-A> --target <regular-account-id-B> --asset 50::<faucet-account-id> --note-type private
    

    Note The faucet account id can be found on the Miden faucet website under the title Miden faucet.

    This generates a private Pay-to-ID (P2ID) note containing 50 assets, transferred from one account to the other.

  2. First, sync the accounts.

    miden sync
    
  3. Get the second note id.

    miden notes
    
  4. Have the second account consume the note.

    miden consume-notes --account <regular-account-ID-B> <input-note-id>
    

    Tip It's possible to use a short version of the note id: 7 characters after the 0x is sufficient, e.g. 0x6ae613a.

    You should now see both accounts containing faucet assets with amounts transferred from Account A to Account B.

  5. Check the second account:

    miden account --show <regular-account-ID-B>
    

    Result of listing miden accounts

  6. Check the original account:

    miden account --show <regular-account-ID-A>
    

    Result of listing miden accounts

Wanna do more? Sending public notes

Congratulations!

You have successfully configured and used the Miden client to interact with a Miden rollup and faucet.

You have performed basic Miden rollup operations like submitting proofs of transactions, generating and consuming notes.

For more information on the Miden client, refer to the Miden client documentation.

Clear data

All state is maintained in store.sqlite3, located in the directory defined in the miden-client.toml file.

To clear all state, delete this file. It recreates on any command execution.

In this section, we show you how to execute transactions and send funds to another account using the Miden client and public notes.

Important: Prerequisite steps

Create a second client

Tip Remember to use the Miden client documentation for clarifications.

This is an alternative to the private P2P transactions process.

In this tutorial, we use two different clients to simulate two different remote users who don't share local state.

To do this, we use two terminals with their own state (using their own miden-client.toml).

  1. Create a new directory to store the new client.

    mkdir miden-client-2
    cd miden-client-2
    
  2. Initialize the client. This creates the miden-client.toml file line-by-line.

    miden init --network testnet # Creates a miden-client.toml file configured with the testnet node's IP
    
  3. On the new client, create a new basic account:

    miden new-wallet --mutable -s public
    

    We refer to this account as Account C. Note that we set the account's storage mode to public, which means that the account details are public and its latest state can be retrieved from the node.

  4. List and view the account with the following command:

    miden account -l
    

Transfer assets between accounts

  1. Now we can transfer some of the tokens we received from the faucet to our new account C. Remember to switch back to miden-client directory, since you'll be making the txn from Account ID A.

    To do this, from the first client run:

    miden send --sender <basic-account-id-A> --target <basic-account-id-C> --asset 50::<faucet-account-id> --note-type public
    

    Note The faucet account id is 0xad904b3138d71d3e and can also be found on the Miden faucet website under the title Miden faucet.

    This generates a Pay-to-ID (P2ID) note containing 50 tokens, transferred from one account to the other. As the note is public, the second account can receive the necessary details by syncing with the node.

  2. First, sync the account on the new client.

    miden sync
    
  3. At this point, we should have received the public note details.

    miden notes --list
    

    Because the note was retrieved from the node, the commit height will be included and displayed.

  4. Have account C consume the note.

    miden consume-notes --account <regular-account-ID-C> <input-note-id>
    

    Tip It's possible to use a short version of the note id: 7 characters after the 0x is sufficient, e.g. 0x6ae613a.

That's it!

Account C has now consumed the note and there should be new assets in the account:

miden account --show <account-ID> 

Clear state

All state is maintained in store.sqlite3, located in the directory defined in the miden-client.toml file.

To clear all state, delete this file. It recreates on any command execution.

Welcome to MkDocs

For full documentation visit mkdocs.org.

Commands

  • mkdocs new [dir-name] - Create a new project.
  • mkdocs serve - Start the live-reloading docs server.
  • mkdocs build - Build the documentation site.
  • mkdocs -h - Print help message and exit.

Project layout

mkdocs.yml    # The configuration file.
docs/
    index.md  # The documentation homepage.
    ...       # Other markdown pages, images and other files.

Overview

Components

The Miden client currently has two main components:

  1. Miden client library.
  2. Miden client CLI.

Miden client library

The Miden client library is a Rust library that can be integrated into projects, allowing developers to interact with the Miden rollup.

The library provides a set of APIs and functions for executing transactions, generating proofs, and managing activity on the Miden network.

Miden client CLI

The Miden client also includes a command-line interface (CLI) that serves as a wrapper around the library, exposing its basic functionality in a user-friendly manner.

The CLI provides commands for interacting with the Miden rollup, such as submitting transactions, syncing with the network, and managing account data.

Software prerequisites

Install the client

We currently recommend installing and running the client with the concurrent feature.

Run the following command to install the miden-client:

cargo install miden-cli --features concurrent

This installs the miden binary (at ~/.cargo/bin/miden) with the concurrent feature.

Concurrent feature

The concurrent flag enables optimizations that result in faster transaction execution and proving times.

Run the client

  1. Make sure you have already installed the client. If you don't have a miden-client.toml file in your directory, create one or run miden init to initialize one at the current working directory. You can do so without any arguments to use its defaults or define either the RPC endpoint or the store config via --network and --store-path

  2. Run the client CLI using:

    miden
    

The Miden client offers a range of functionality for interacting with the Miden rollup.

Transaction execution

The Miden client facilitates the execution of transactions on the Miden rollup; allowing users to transfer assets, mint new tokens, and perform various other operations.

Proof generation

The Miden rollup supports user-generated proofs which are key to ensuring the validity of transactions on the Miden rollup.

To enable such proofs, the client contains the functionality for executing, proving, and submitting transactions.

Miden network interactivity

The Miden client enables users to interact with the Miden network. This includes syncing with the latest blockchain data and managing account information.

Account generation and tracking

The Miden client provides features for generating and tracking accounts within the Miden rollup ecosystem. Users can create accounts and track their transaction status.

The Miden client has the following architectural components:

Important "Customizable"

  • The RPC client and the store are Rust traits.
  • This allow developers and users to easily customize their implementations.

Store

The store is central to the client's design.

It manages the persistence of the following entities:

  • Accounts; including their state history and related information such as vault assets and account code.
  • Transactions and their scripts.
  • Notes.
  • Note tags.
  • Block headers and chain information that the client needs to execute transactions and consume notes.

Because Miden allows off-chain executing and proving, the client needs to know about the state of the blockchain at the moment of execution. To avoid state bloat, however, the client does not need to see the whole blockchain history, just the chain history intervals that are relevant to the user.

The store can track any number of accounts, and any number of notes that those accounts might have created or may want to consume.

RPC client

The RPC client communicates with the node through a defined set of gRPC methods.

Currently, these include:

  • GetBlockHeaderByNumber: Returns the block header information given a specific block number.
  • SyncState: Asks the node for information relevant to the client. For example, specific account changes, whether relevant notes have been created or consumed, etc.
  • SubmitProvenTransaction: Sends a locally-proved transaction to the node for inclusion in the blockchain.

Transaction executor

The transaction executor executes transactions using the Miden VM.

When executing, the executor needs access to relevant blockchain history. The executor uses a DataStore interface for accessing this data. This means that there may be some coupling between the executor and the store.

To use the Miden client library in a Rust project, include it as a dependency.

In your project's Cargo.toml, add:

miden-client = { version = "0.7" }

Features

The Miden client library supports the concurrent feature which is recommended for developing applications with the client. To use it, add the following to your project's Cargo.toml:

miden-client = { version = "0.7", features = ["concurrent"] }

The library also supports several other features. Please refer to the crate's documentation to learn more.

Client instantiation

Spin up a client using the following Rust code and supplying a store and RPC endpoint.

#![allow(unused)]
fn main() {
let sqlite_store = SqliteStore::new("path/to/store".try_into()?).await?;
let store = Arc::new(sqlite_store);

// Generate a random seed for the RpoRandomCoin.
let mut rng = rand::thread_rng();
let coin_seed: [u64; 4] = rng.gen();

// Initialize the random coin using the generated seed.
let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

// Create a store authenticator with the store and random coin.
let authenticator = StoreAuthenticator::new_with_rng(store.clone(), rng);

// Instantiate the client using a Tonic RPC client
let endpoint = Endpoint::new("https".into(), "localhost".into(), Some(57291));
let client: Client<RpoRandomCoin> = Client::new(
    Box::new(TonicRpcClient::new(endpoint, 10_000)),
    rng,
    store,
    Arc::new(authenticator),
    false, // Set to true for debug mode, if needed.
);
}

Create local account

With the Miden client, you can create and track any number of public and local accounts. For local accounts, the state is tracked locally, and the rollup only keeps commitments to the data, which in turn guarantees privacy.

The AccountBuilder can be used to create a new account with the specified parameters and components. The following code creates a new local account:

#![allow(unused)]
fn main() {
let key_pair = SecretKey::with_rng(client.rng());
let anchor_block = client.get_latest_epoch_block().await.unwrap();

let (new_account, seed) = AccountBuilder::new(init_seed) // Seed should be random for each account
    .anchor((&anchor_block).try_into().unwrap())
    .account_type(AccountType::RegularAccountImmutableCode)
    .storage_mode(AccountStorageMode::Private)
    .with_component(RpoFalcon512::new(key_pair.public_key()))
    .with_component(BasicWallet)
    .build()?;

client.add_account(&new_account, Some(seed), &AuthSecretKey::RpoFalcon512(key_pair), false).await?;
}

Once an account is created, it is kept locally and its state is automatically tracked by the client.

To create an public account, you can specify AccountStorageMode::Public like so:

let key_pair = SecretKey::with_rng(client.rng());
let anchor_block = client.get_latest_epoch_block().await.unwrap();

let (new_account, seed) = AccountBuilder::new(init_seed) // Seed should be random for each account
    .anchor((&anchor_block).try_into().unwrap())
    .account_type(AccountType::RegularAccountImmutableCode)
    .storage_mode(AccountStorageMode::Public)
    .with_component(RpoFalcon512::new(key_pair.public_key()))
    .with_component(BasicWallet)
    .build()?;

client.add_account(&new_account, Some(seed), &AuthSecretKey::RpoFalcon512(key_pair), false).await?;

The account's state is also tracked locally, but during sync the client updates the account state by querying the node for the most recent account data.

Execute transaction

In order to execute a transaction, you first need to define which type of transaction is to be executed. This may be done with the TransactionRequest which represents a general definition of a transaction. Some standardized constructors are available for common transaction types.

Here is an example for a pay-to-id transaction type:

#![allow(unused)]
fn main() {
// Define asset
let faucet_id = AccountId::from_hex(faucet_id)?;
let fungible_asset = FungibleAsset::new(faucet_id, *amount)?.into();

let sender_account_id = AccountId::from_hex(bob_account_id)?;
let target_account_id = AccountId::from_hex(alice_account_id)?;
let payment_transaction = PaymentTransactionData::new(
    vec![fungible_asset.into()],
    sender_account_id,
    target_account_id,
);

let transaction_request = TransactionRequestBuilder::pay_to_id(
    payment_transaction,
    None,
    NoteType::Private,
    client.rng(),
)?;

// Execute transaction. No information is tracked after this.
let transaction_execution_result = client.new_transaction(sender_account_id, transaction_request.clone()).await?;

// Prove and submit the transaction, which is stored alongside created notes (if any)
client.submit_transaction(transaction_execution_result).await?
}

You can decide whether you want the note details to be public or private through the note_type parameter. You may also execute a transaction by manually defining a TransactionRequest instance. This allows you to run custom code, with custom note arguments as well.

The following document lists the commands that the CLI currently supports.

Note Use --help as a flag on any command for more information.

Usage

Call a command on the miden-client like this:

miden <command> <flags> <arguments>

Optionally, you can include the --debug flag to run the command with debug mode, which enables debug output logs from scripts that were compiled in this mode:

miden --debug <flags> <arguments>

Note that the debug flag overrides the MIDEN_DEBUG environment variable.

Commands

init

Creates a configuration file for the client in the current directory.

# This will create a config file named `miden-client.toml` using default values
# This file contains information useful for the CLI like the RPC provider and database path
miden init

# You can set up the CLI for any of the default networks
miden init --network testnet # This is the default value if no network is provided
miden init --network devnet
miden init --network localhost

# You can use the --network flag to override the default RPC config
miden init --network 18.203.155.106
# You can specify the port
miden init --network 18.203.155.106:8080
# You can also specify the protocol (http/https)
miden init --network https://18.203.155.106
# You can specify both
miden init --network https://18.203.155.106:1234

# You can use the --store_path flag to override the default store config
miden init --store_path db/store.sqlite3

# You can provide both flags
miden init --network 18.203.155.106 --store_path db/store.sqlite3

account

Inspect account details.

Action Flags

FlagsDescriptionShort Flag
--listList all accounts monitored by this client-l
--show <ID>Show details of the account for the specified ID-s
--default <ID>Manage the setting for the default account-d

The --show flag also accepts a partial ID instead of the full ID. For example, instead of:

miden account --show 0x8fd4b86a6387f8d8

You can call:

miden account --show 0x8fd4b86

For the --default flag, if <ID> is "none" then the previous default account is cleared. If no <ID> is specified then the default account is shown.

new-wallet

Creates a new wallet account.

This command has three optional flags:

  • --storage-mode <TYPE>: Used to select the storage mode of the account (private if not specified). It may receive "private" or "public".
  • --mutable: Makes the account code mutable (it's immutable by default).
  • --extra-components <TEMPLATE_FILES_LIST>: Allows to pass a list of account component template files which can be added to the account. If the templates contain placeholders, the CLI will prompt the user to enter the required data for instantiating storage appropriately.

After creating an account with the new-wallet command, it is automatically stored and tracked by the client. This means the client can execute transactions that modify the state of accounts and track related changes by synchronizing with the Miden node.

new-faucet

Creates a new faucet account.

This command has three optional flags:

  • --storage-mode <TYPE>: Used to select the storage mode of the account (private if not specified). It may receive "private" or "public".
  • --non-fungible: Makes the faucet asset non-fungible (it's fungible by default).
  • --extra-components <TEMPLATE_FILES_LIST>: Allows to pass a list of account component template files which can be added to the account. If the templates contain placeholders, the CLI will prompt the user to enter the required data for instantiating storage appropriately.

After creating an account with the new-faucet command, it is automatically stored and tracked by the client. This means the client can execute transactions that modify the state of accounts and track related changes by synchronizing with the Miden node.

Examples

# Create a new wallet with default settings (private storage, immutable, no extra components)
miden new-wallet

# Create a new wallet with public storage and a mutable code
miden new-wallet --storage-mode public --mutable

# Create a new wallet that includes extra components from local templates
miden new-wallet --extra-components template1,template2

# Create a fungible faucet 
miden new-faucet --token-symbol TST --decimals 10 --max-supply 100000 --storage-mode private

info

View a summary of the current client state.

notes

View and manage notes.

Action Flags

FlagsDescriptionShort Flag
--list [<filter>]List input notes-l
--show <ID>Show details of the input note for the specified note ID-s

The --list flag receives an optional filter: - expected: Only lists expected notes. - committed: Only lists committed notes. - consumed: Only lists consumed notes. - processing: Only lists processing notes. - consumable: Only lists consumable notes. An additional --account-id <ID> flag may be added to only show notes consumable by the specified account. If no filter is specified then all notes are listed.

The --show flag also accepts a partial ID instead of the full ID. For example, instead of:

miden notes --show 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0

You can call:

miden notes --show 0x70b7ec

sync

Sync the client with the latest state of the Miden network. Shows a brief summary at the end.

tags

View and add tags.

Action Flags

FlagDescriptionAliases
--listList all tags monitored by this client-l
--add <tag>Add a new tag to the list of tags monitored by this client-a
--remove <tag>Remove a tag from the list of tags monitored by this client-r

tx

View transactions.

Action Flags

CommandDescriptionAliases
--listList tracked transactions-l

After a transaction gets executed, two entities start being tracked:

  • The transaction itself: It follows a lifecycle from Pending (initial state) and Committed (after the node receives it). It may also be Discarded if the transaction was not included in a block.
  • Output notes that might have been created as part of the transaction (for example, when executing a pay-to-id transaction).

Transaction creation commands

mint

Creates a note that contains a specific amount tokens minted by a faucet, that the target Account ID can consume.

Usage: miden mint --target <TARGET ACCOUNT ID> --asset <AMOUNT>::<FAUCET ID> --note-type <NOTE_TYPE>

consume-notes

Account ID consumes a list of notes, specified by their Note ID.

Usage: miden consume-notes --account <ACCOUNT ID> [NOTES]

For this command, you can also provide a partial ID instead of the full ID for each note. So instead of

miden consume-notes --account <some-account-id> 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 0x80b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0

You can do:

miden consume-notes --account <some-account-id> 0x70b7ecb 0x80b7ecb

Additionally, you can optionally not specify note IDs, in which case any note that is known to be consumable by the executor account ID will be consumed.

Either Expected or Committed notes may be consumed by this command, changing their state to Processing. It's state will be updated to Consumed after the next sync.

send

Sends assets to another account. Sender Account creates a note that a target Account ID can consume. The asset is identified by the tuple (FAUCET ID, AMOUNT). The note can be configured to be recallable making the sender able to consume it after a height is reached.

Usage: miden send --sender <SENDER ACCOUNT ID> --target <TARGET ACCOUNT ID> --asset <AMOUNT>::<FAUCET ID> --note-type <NOTE_TYPE> <RECALL_HEIGHT>

swap

The source account creates a Swap note that offers some asset in exchange for some other asset. When another account consumes that note, it'll receive the offered amount and it'll have the requested amount removed from its assets (and put into a new note which the first account can then consume). Consuming the note will fail if the account doesn't have enough of the requested asset.

Usage: miden swap --source <SOURCE ACCOUNT ID> --offered-asset <OFFERED AMOUNT>::<OFFERED FAUCET ID> --requested-asset <REQUESTED AMOUNT>::<REQUESTED FAUCET ID> --note-type <NOTE_TYPE>

Tips

For send and consume-notes, you can omit the --sender and --account flags to use the default account defined in the config. If you omit the flag but have no default account defined in the config, you'll get an error instead.

For every command which needs an account ID (either wallet or faucet), you can also provide a partial ID instead of the full ID for each account. So instead of

miden send --sender 0x80519a1c5e3680fc --target 0x8fd4b86a6387f8d8 --asset 100::0xa99c5c8764d4e011

You can do:

miden send --sender 0x80519 --target 0x8fd4b --asset 100::0xa99c5c8764d4e011

!!! note The only exception is for using IDs as part of the asset, those should have the full faucet's account ID.

Transaction confirmation

When creating a new transaction, a summary of the transaction updates will be shown and confirmation for those updates will be prompted:

miden <tx command> ...

TX Summary:

...

Continue with proving and submission? Changes will be irreversible once the proof is finalized on the rollup (Y/N)

This confirmation can be skipped in non-interactive environments by providing the --force flag (miden send --force ...):

Importing and exporting

export

Export input note data to a binary file .

FlagDescriptionAliases
--filename <FILENAME>Desired filename for the binary file.-f
--export-type <EXPORT_TYPE>Exported note type.-e
Export type

The user needs to specify how the note should be exported via the --export-type flag. The following options are available:

  • id: Only the note ID is exported. When importing, if the note ID is already tracked by the client, the note will be updated with missing information fetched from the node. This works for both public and private notes. If the note isn't tracked and the note is public, the whole note is fetched from the node and is stored for later use.
  • full: The note is exported with all of its information (metadata and inclusion proof). When importing, the note is considered unverified. The note may not be consumed directly after importing as its block header will not be stored in the client. The block header will be fetched and be used to verify the note during the next sync. At this point the note will be committed and may be consumed.
  • partial: The note is exported with minimal information and may be imported even if the note is not yet committed on chain. At the moment of importing the note, the client will check the state of the note by doing a note sync, using the note's tag. Depending on the response, the note will be either stored as "Expected" or "Committed".

import

Import entities managed by the client, such as accounts and notes. The type of entities is inferred.

After installation, use the client by running the following and adding the relevant commands:

miden

Info "Help" Run miden --help for information on miden commands.

Client Configuration

We configure the client using a TOML file (miden-client.toml).

[rpc]
endpoint = { protocol = "http", host = "localhost", port = 57291 }
timeout_ms = 10000

[store]
database_filepath = "store.sqlite3"

[cli]
default_account_id = "0x012345678"

The TOML file should reside in same the directory from which you run the CLI.

In the configuration file, you will find a section for defining the node's rpc endpoint and timeout and the store's filename database_filepath.

By default, the node is set up to run on localhost:57291.

Note

  • Running the node locally for development is encouraged.
  • However, the endpoint can point to any remote node.

There's an additional optional section used for CLI configuration. It currently contains the default account ID, which is used to execute transactions against it when the account flag is not provided.

By default none is set, but you can set and unset it with:

miden account --default <ACCOUNT_ID> #Sets default account
miden account --default none #Unsets default account

Note

  • The account must be tracked by the client in order to be set as the default account.

You can also see the current default account ID with:

miden account --default

Environment variables

  • MIDEN_DEBUG: When set to true, enables debug mode on the transaction executor and the script compiler. For any script that has been compiled and executed in this mode, debug logs will be output in order to facilitate MASM debugging (these instructions can be used to do so). This variable can be overridden by the --debug CLI flag.

Example "Executing, proving, and submitting transactions to the Miden node" For a complete example on how to run the client and submit transactions to the Miden node, refer to the Getting started documentation.

Example "Miden client API docs" The latest and complete reference for the Miden client API can be found at Miden client docs.rs.

Miden architecture overview

Polygon Miden’s architecture departs considerably from typical blockchain designs to support privacy and parallel transaction execution.

In traditional blockchains, state and transactions must be transparent to be verifiable. This is necessary for block production and execution.

However, user generated zero-knowledge proofs allow state transitions, e.g. transactions, to be verifiable without being transparent.

Miden design goals

  • High throughput: The ability to process a high number of transactions (state changes) over a given time interval.
  • Privacy: The ability to keep data known to one’s self and anonymous while processing and/or storing it.
  • Asset safety: Maintaining a low risk of mistakes or malicious behavior leading to asset loss.

Actor model

The actor model inspires Polygon Miden’s execution model. This is a well-known computational design paradigm in concurrent systems. In the actor model, actors are state machines responsible for maintaining their own state. In the context of Polygon Miden, each account is an actor. Actors communicate with each other by exchanging messages asynchronously. One actor can send a message to another, but it is up to the recipient to apply the requested change to their state.

Polygon Miden’s architecture takes the actor model further and combines it with zero-knowledge proofs. Now, actors not only maintain and update their own state, but they can also prove the validity of their own state transitions to the rest of the network. This ability to independently prove state transitions enables local smart contract execution, private smart contracts, and much more. And it is quite unique in the rollup space. Normally only centralized entities - sequencer or prover - create zero-knowledge proofs, not the users.

Core concepts

Miden uses accounts and notes, both of which hold assets. Accounts consume and produce notes during transactions. Transactions describe the account state changes of single accounts.

Accounts

Accounts can hold assets and define rules how assets can be transferred. Accounts can represent users or autonomous smart contracts. The accounts chapter describes the design of an account, its storage types, and creating an account.

Notes

Notes are messages that accounts send to each other. A note stores assets and a script that defines how the note can be consumed. The note chapter describes the design, the storage types, and the creation of a note.

Assets

Assets can be fungible and non-fungible. They are stored in the owner’s account itself or in a note. The assets chapter describes asset issuance, customization, and storage.

Transactions

Transactions describe the production and consumption of notes by a single account.

Executing a transaction always results in a STARK proof.

The transaction chapter describes the transaction design and implementation, including an in-depth discussion of how transaction execution happens in the transaction kernel program.

Limits

Limits topic describes limits currently enforced in miden-base and miden-node.

Accounts produce and consume notes to communicate

Architecture core concepts

State and execution

The actor-based execution model requires a radically different approach to recording the system's state. Actors and the messages they exchange must be treated as first-class citizens. Polygon Miden addresses this by combining the state models of account-based systems like Ethereum and UTXO-based systems like Bitcoin and Zcash.

Miden's state model captures the individual states of all accounts and notes, and the execution model describes state progress in a sequence of blocks.

State model

State describes everything that is the case at a certain point in time. Individual states of accounts or notes can be stored on-chain and off-chain. This chapter describes the three different state databases in Miden.

Execution model

Execution defines how state progresses as aggregated-state-updates in batches, blocks, and epochs. The execution chapter describes the execution model and how blocks are built.

Operators capture and progress state

Architecture state process

Account / Smart Contract

An Account represents the primary entity of the protocol. Capable of holding assets, storing data, and executing custom code. Each Account is a specialized smart contract providing a programmable interface for interacting with its state and assets.

What is the purpose of an account?

In Miden's hybrid UTXO- and account-based model Accounts enable the creation of expressive smart contracts via a Turing-complete language.

Account core components

An Account is composed of several core components, illustrated below:

Account diagram

These components are:

  1. ID
  2. Code
  3. Storage
  4. Vault
  5. Nonce

ID

An immutable and unique identifier for the Account.

A 120-bit long number represents the Account ID. This identifier is structured to encapsulate specific account metadata while preventing precomputed attack vectors (e.g., rainbow table attacks).

The ID is generated by hashing a user-generated random seed together with commitments to the initial code and storage of the Account and the anchor block. The anchor block refers to specific blockchain epoch block in which the account is created. The resulting 256-bit long hash is then manipulated and shortened to 120-bit. Manipulation includes encoding the account type, account storage mode, the version of the Account ID scheme, and the anchor block.

Account type, storage mode, and version are included in the ID, to ensure these properties can be determined without additional computation. Anyone can immediately tell those properties by just looking at the ID in bit representation.

Also, the ID generation process ensures that an attacker cannot precompute an ID before the anchor block's commitment is available. This significantly mitigates the risk of ID hijacking, where an adversary might attempt to claim assets sent to an unregistered ID. By anchoring the ID to a recent epoch block, the window for potential attacks is minimized, reinforcing the security of asset transfers and account registration.

An Account ID is considered invalid if:

  • The metadata (storage mode, type, or version) does not match any recognized values.
  • The anchor epoch exceeds $2^{16}-1$.
  • The least significant 8 bits of the ID are nonzero.

Code

A collection of functions defining the Account’s programmable interface.

Every Miden Account is essentially a smart contract. The Code component defines the account’s functions, which can be invoked through both Note scripts and transaction scripts. Key characteristics include:

  • Mutable access: Only the Account’s own functions can modify its storage and vault. All state changes—such as updating storage slots, incrementing the nonce, or transferring assets—must occur through these functions.
  • Function commitment: Each function can be called by its MAST root. The root represents the underlying code tree as a 32-byte commitment. This ensures integrity, i.e., the caller calls what he expects.
  • Note creation: Account functions can generate new notes.

Storage

A flexible, arbitrary data store within the Account.

The storage is divided into a maximum of 255 indexed storage slots. Each slot can either store a 32-byte value or serve as a pointer to a key-value store with large amounts capacity.

  • StorageSlot::Value: Contains 32 bytes of arbitrary data.
  • StorageSlot::Map: Contains a StorageMap, a key-value store where both keys and values are 32 bytes. The slot's value is a commitment to the entire map.

Vault

A collection of assets stored by the Account.

Large amounts of fungible and non-fungible assets can be stored in the Accounts vault.

Nonce

A counter incremented with each state update to the Account.

The nonce enforces ordering and prevents replay attacks. It must strictly increase with every Account state update. The increment must be less than $2^{32}$ but always greater than the previous nonce, ensuring a well-defined sequence of state changes.

If a smart contract function should be callable by other users, it must increment the Account's nonce. Otherwise, only the contract owner—i.e., the party possessing the contract's key—can execute the function.

Account lifecycle

Throughout its lifetime, an Account progresses through various phases:

  • Creation and Deployment: Initialization of the Account on the network.
  • Active Operation: Continuous state updates via Account functions that modify the storage, nonce, and vault.
  • Termination or Deactivation: Optional, depending on the contract’s design and governance model.

Account creation

For an Account to be recognized by the network, it must exist in the account database maintained by Miden node(s).

However, a user can locally create a new Account ID before it’s recognized network-wide. The typical process might be:

  1. Alice generates a new Account ID locally (according to the desired Account type) using the Miden client.
  2. The Miden client checks with a Miden node to ensure the ID does not already exist.
  3. Alice shares the new ID with Bob (for example, to receive assets).
  4. Bob executes a transaction, creating a note containing assets for Alice.
  5. Alice consumes Bob’s note in her own transaction to claim the asset.
  6. Depending on the Account’s storage mode and transaction type, the operator receives the new Account ID and, if all conditions are met, includes it in the Account database.

Additional information

Account type

There are two main categories of Accounts in Miden: basic accounts and faucets.

  • Basic Accounts: Basic Accounts may be either mutable or immutable:

    • Mutable: Code can be changed after deployment.
    • Immutable: Code cannot be changed once deployed.
  • Faucets: Faucets are always immutable and can be specialized by the type of assets they issue:

    • Fungible Faucet: Can issue fungible assets.
    • Non-fungible Faucet: Can issue non-fungible assets.

Type and mutability are encoded in the two most significant bits of the Account's ID.

Account storage mode

Users can choose whether their Accounts are stored publicly or privately. The preference is encoded in the third and forth most significant bits of the Accounts ID:

  • Public Accounts: The Account’s state is stored on-chain, similar to how Accounts are stored in public blockchains like Ethereum. Contracts that rely on a shared, publicly accessible state (e.g., a DEX) should be public.

  • Private Accounts: Only a commitment (hash) to the Account’s state is stored on-chain. This mode is suitable for users who prioritize privacy or plan to store a large amount of data in their Account. To interact with a private Account, a user must have knowledge of its interface.

The storage mode is chosen during Account creation, it cannot be changed later.

Note

A Note is the medium through which Accounts communicate. A Note holds assets and defines how they can be consumed.

What is the purpose of a note?

In Miden's hybrid UTXO and account-based model Notes represent UTXO's which enable parallel transaction execution and privacy through asynchronous local Note production and consumption.

Note core components

A Note is composed of several core components, illustrated below:

Note diagram

These components are:

  1. Assets
  2. Script
  3. Inputs
  4. Serial number
  5. Metadata

Assets

An asset container for a Note.

A Note can contain up to 256 different assets. These assets represent fungible or non-fungible tokens, enabling flexible asset transfers.

Script

The code executed when the Note is consumed.

Each Note has a script that defines the conditions under which it can be consumed. When accounts consume Notes in transactions, Note scripts call the account’s interface functions. This enables all sorts of operations beyond simple asset transfers. The Miden VM’s Turing completeness allows for arbitrary logic, making Note scripts highly versatile.

Inputs

Arguments passed to the Note script during execution.

A Note can have up to 128 input values, which adds up to a maximum of 1 KB of data. The Note script can access these inputs. They can convey arbitrary parameters for Note consumption.

Serial number

A unique and immutable identifier for the Note.

The serial number has two main purposes. Firstly by adding some randomness to the Note it ensures it's uniqueness, secondly in private Notes it helps prevent linkability between the Note's hash and its nullifier. The serial number should be a random 32 bytes number chosen by the user. If leaked, the Note’s nullifier can be easily computed, potentially compromising privacy.

Metadata

Additional Note information.

Notes include metadata such as the sender’s account ID and a tag that aids in discovery. Regardless of storage mode, these metadata fields remain public.

Note Lifecycle

Architecture core concepts

The Note lifecycle proceeds through four primary phases: creation, validation, discovery, and consumption. Throughout this process, Notes function as secure, privacy-preserving vehicles for asset transfers and logic execution.

Note creation

Accounts can create Notes in a transaction. The Note exists if it is included in the global Notes DB.

  • Users: Executing local or network transactions.
  • Miden operators: Facilitating on-chain actions, e.g. such as executing user Notes against a DEX or other contracts.

Note storage mode

As with accounts, Notes can be stored either publicly or privately:

  • Public mode: The Note data is stored in the note database, making it fully visible on-chain.
  • Private mode: Only the Note’s hash is stored publicly. The Note’s actual data remains off-chain, enhancing privacy.

Note validation

Once created, a Note must be validated by a Miden operator. Validation involves checking the transaction proof that produced the Note to ensure it meets all protocol requirements.

After validation Notes become “live” and eligible for consumption. If creation and consumption happens within the same block, there is no entry in the Notes DB. All other notes, are being added either as a commitment or fully public.

Note discovery

Clients often need to find specific Notes of interest. Miden allows clients to query the Note database using Note tags. These lightweight, 32-bit data fields serve as best-effort filters, enabling quick lookups for Notes related to particular use cases, scripts, or account prefixes.

Using Note tags strikes a balance between privacy and efficiency. Without tags, querying a specific Note ID reveals a user’s interest to the operator. Conversely, downloading and filtering all registered Notes locally is highly inefficient. Tags allow users to adjust their level of privacy by choosing how broadly or narrowly they define their search criteria, letting them find the right balance between revealing too much information and incurring excessive computational overhead.

Note consumption

To consume a Note, the consumer must know its data, including the inputs needed to compute the nullifier. Consumption occurs as part of a transaction. Upon successful consumption a nullifier is generated for the consumed Notes.

Upon successful verification of the transaction:

  1. The Miden operator records the Note’s nullifier as “consumed” in the nullifier database.
  2. The Note’s one-time claim is thus extinguished, preventing reuse.

Note recipient restricting consumption

Consumption of a Note can be restricted to certain accounts or entities. For instance, the P2ID and P2IDR Note scripts target a specific account ID. Alternatively, Miden defines a RECIPIENT (represented as 32 bytes) computed as:

hash(hash(hash(serial_num, [0; 4]), script_hash), input_hash)

Only those who know the RECIPIENT’s pre-image can consume the Note. For private Notes, this ensures an additional layer of control and privacy, as only parties with the correct data can claim the Note.

The transaction prologue requires all necessary data to compute the Note hash. This setup allows scenario-specific restrictions on who may consume a Note.

For a practical example, refer to the SWAP note script, where the RECIPIENT ensures that only a defined target can consume the swapped asset.

Note nullifier ensuring private consumption

The Note nullifier, computed as:

hash(serial_num, script_hash, input_hash, vault_hash)

This achieves the following properties:

  • Every Note can be reduced to a single unique nullifier.
  • One cannot derive a Note's hash from its nullifier.
  • To compute the nullifier, one must know all components of the Note: serial_num, script_hash, input_hash, and vault_hash.

That means if a Note is private and the operator stores only the Note's hash, only those with the Note details know if this Note has been consumed already. Zcash first introduced this approach.

Architecture core concepts

Asset

An Asset is a unit of value that can be transferred from one account to another using notes.

What is the purpose of an asset?

In Miden, Assets serve as the primary means of expressing and transferring value between accounts through notes. They are designed with four key principles in mind:

  1. Parallelizable exchange:
    By managing ownership and transfers directly at the account level instead of relying on global structures like ERC20 contracts, accounts can exchange Assets concurrently, boosting scalability and efficiency.

  2. Self-sovereign ownership:
    Assets are stored in the accounts directly. This ensures that users retain complete control over their Assets.

  3. Censorship resistance:
    Users can transact freely and privately with no single contract or entity controlling Asset transfers. This reduces the risk of censored transactions, resulting in a more open and resilient system.

  4. Flexible fee payment:
    Unlike protocols that require a specific base Asset for fees, Miden allows users to pay fees in any supported Asset. This flexibility simplifies the user experience.

Native asset

All data structures following the Miden asset model that can be exchanged.

Native Assets adhere to the Miden Asset model (encoding, issuance, storage). Every native Asset is encoded using 32 bytes, including both the ID of the issuing account and the Asset details.

Issuance

Info

  • Only faucet accounts can issue assets.

Faucets can issue either fungible or non-fungible Assets as defined at account creation. The faucet's code specifies the Asset minting conditions: i.e., how, when, and by whom these Assets can be minted. Once minted, they can be transferred to other accounts using notes.

Architecture core concepts

Type

Fungible asset

Fungible Assets are encoded with the amount and the faucet_id of the issuing faucet. The amount is always 2^{63} - 1 or smaller, representing the maximum supply for any fungible Asset. Examples include ETH and various stablecoins (e.g., DAI, USDT, USDC).

Non-fungible asset

Non-fungible Assets are encoded by hashing the Asset data into 32 bytes and placing the faucet_id as the second element. Examples include NFTs like a DevCon ticket.

Storage

Accounts and notes have vaults used to store Assets. Accounts use a sparse Merkle tree as a vault while notes use a simple list. This enables an account to store a practically unlimited number of assets while a note can only store 255 assets.

Architecture core concepts

Burning

Assets in Miden can be burned through various methods, such as rendering them unspendable by storing them in an unconsumable note, or sending them back to their original faucet for burning using it's dedicated function.

Alternative asset models

All data structures not following the Miden asset model that can be exchanged.

Miden is flexible enough to support other Asset models. For example, developers can replicate Ethereum’s ERC20 pattern, where fungible Asset ownership is recorded in a single account. To transact, users send a note to that account, triggering updates in the global hashmap state.

Transaction

A Transaction in Miden is the state transition of a single account. A Transaction takes as input a single account and zero or more notes, and outputs the same account with an updated state, together with zero or more notes. Transactions in Miden are Miden VM programs, their execution resulting in the generation of a zero-knowledge proof.

Miden's Transaction model aims for the following:

  • Parallel transaction execution: Accounts can update their state independently from each other and in parallel.
  • Private transaction execution: Client-side Transaction proving allows the network to verify Transactions validity with zero knowledge.

Transaction diagram

Compared to most blockchains, where a Transaction typically involves more than one account (e.g., sender and receiver), a Transaction in Miden involves a single account. To illustrate, Alice sends 5 ETH to Bob. In Miden, sending 5 ETH from Alice to Bob takes two Transactions, one in which Alice creates a note containing 5 ETH and one in which Bob consumes that note and receives the 5 ETH. This model removes the need for a global lock on the blockchain's state, enabling Miden to process Transactions in parallel.

Currently the protocol limits the number of notes that can be consumed and produced in a transaction to 1000 each, which means that in a single Transaction an application could serve up to 2000 different user requests like deposits or withdrawals into/from a pool.

A simple transaction currently takes about 1-2 seconds on a MacBook Pro. It takes around 90K cycles to create the proof, as of now the signature verification step is the dominant cost.

Transaction lifecycle

Every Transaction describes the process of an account changing its state. This process is described as a Miden VM program, resulting in the generation of a zero-knowledge proof. Transactions are being executed in a specified sequence, in which several notes and a transaction script can interact with an account.

Transaction execution flow

Inputs

A Transaction requires several inputs:

  • Account: A Transaction is always executed against a single account. The executor must have complete knowledge of the account's state.

  • Notes: A Transaction can consume up to 1K notes. The executor must have complete knowledge of the note data, including note inputs, before consumption. For private notes, the data cannot be fetched from the blockchain and must be received through an off-chain channel.

  • Blockchain state: The current reference block and information about the notes database used to authenticate notes to be consumed must be retrieved from the Miden operator before execution. Usually, notes to be consumed in a Transaction must have been created before the reference block.

  • Transaction script (optional): Transaction scripts are defined by the executor. And like note scripts, they can invoke account methods, e.g., sign a transaction.

  • Transaction arguments (optional): For every note, the executor can inject transaction arguments that are present at runtime. If the note script — and therefore the note creator — allows, the note script can read those arguments to allow dynamic execution. See below for an example.

  • Foreign account data (optional): Any foreign account data accessed during a Transaction, whether private or public, must be available beforehand. There is no need to know the full account storage, but the data necessary for the Transaction, e.g., the key/value pair that is read and the corresponding storage root.

Flow

  1. Prologue

    Executes at the beginning of a transaction. It validates on-chain commitments against the provided data. This is to ensure that the transaction executes against a valid on-chain recorded state of the account and to be consumed notes. Notes to be consumed must be registered on-chain — except for erasable notes which can be consumed without block inclusion.

  2. Note processing

    Notes are executed sequentially against the account, following a sequence defined by the executor. To execute a note means processing the note script that calls methods exposed on the account interface. Notes must be consumed fully, which means that all assets must be transferred into the account or into other created notes. Note scripts can invoke the account interface during execution. They can push assets into the account's vault, create new notes, set a transaction expiration, and read from or write to the account’s storage. Any method they call must be explicitly exposed by the account interface. Note scripts can also invoke methods of foreign accounts to read their state.

  3. Transaction script processing

    Transaction scripts are an optional piece of code defined by the executor which interacts with account methods after all notes have been executed. For example, Transaction scripts can be used to sign the Transaction (e.g., sign the transaction by incrementing the nonce of the account, without which, the transaction would fail), to mint tokens from a faucet, create notes, or modify account storage. Transaction scripts can also invoke methods of foreign accounts to read their state.

  4. Epilogue

    Completes the execution, resulting in an updated account state and a generated zero-knowledge proof. The validity of the resulting state change is checked. The account's Nonce must have been incremented, which is how the entire transaction is authenticated. Also, the net sum of all involved assets must be 0 (if the account is not a faucet).

The proof together with the corresponding data needed for verification and updates of the global state can then be submitted and processed by the network.

Examples

To illustrate the Transaction protocol, we provide two examples for a basic Transaction. We will use references to the existing Miden Transaction kernel — the reference implementation of the protocol — and to the methods in Miden Assembly.

Creating a P2ID note

Let's assume account A wants to create a P2ID note. P2ID notes are pay-to-ID notes that can only be consumed by a specified target account ID. Note creators can provide the target account ID using the note inputs.

In this example, account A uses the basic wallet and the authentication component provided by miden-lib. The basic wallet component defines the methods wallets::basic::create_note and wallets::basic::move_asset_to_note to create notes with assets, and wallets::basic::receive_asset to receive assets. The authentication component exposes auth::basic::auth_tx_rpo_falcon512 which allows for signing a transaction. Some account methods like account::get_id are always exposed.

The executor inputs to the Miden VM a Transaction script in which he places on the stack the data (tag, aux, note_type, execution_hint, RECIPIENT) of the note(s) that he wants to create using wallets::basic::create_note during the said Transaction. The NoteRecipient is a value that describes under which condition a note can be consumed and is built using a serial_number, the note_script (in this case P2ID script) and the note_inputs. The Miden VM will execute the Transaction script and create the note(s). After having been created, the executor can use wallets::basic::move_asset_to_note to move assets from the account's vault to the notes vault.

After finalizing the Transaction the updated state and created note(s) can now be submitted to the Miden operator to be recorded on-chain.

Consuming a P2ID note

Let's now assume that account A wants to consume a P2ID note to receive the assets contained in that note.

To start the transaction process, the executor fetches and prepares all the input data to the Transaction. First, it retrieves blockchain data, like global inputs and block data of the most recent block. This information is needed to authenticate the native account's state and that the P2ID note exists on-chain. Then it loads the full account and note data, to start the Transaction execution.

In the transaction's prologue the data is being authenticated by re-hashing the provided values and comparing them to the blockchain's data (this is how private data can be used and verified during the execution of transaction without actually revealing it to the network).

Then the P2ID note script is being executed. The script starts by reading the note inputs note::get_inputs — in our case the account ID of the intended target account. It checks if the provided target account ID equals the account ID of the executing account. This is the first time the note invokes a method exposed by the Transaction kernel, account::get_id.

If the check passes, the note script pushes the assets it holds into the account's vault. For every asset the note contains, the script calls the wallets::basic::receive_asset method exposed by the account's wallet component. The wallets::basic::receive_asset procedure calls account::add_asset, which cannot be called from the note itself. This allows accounts to control what functionality to expose, e.g. whether the account supports receiving assets or not, and the note cannot bypass that.

After the assets are stored in the account's vault, the transaction script is being executed. The script calls auth::basic::auth_tx_rpo_falcon512 which is explicitly exposed in the account interface. The method is used to verify a provided signature against a public key stored in the account's storage and a commitment to this specific transaction. If the signature can be verified, the method increments the nonce.

The Epilogue finalizes the transaction by computing the final account hash, asserting the nonce increment and checking that no assets were created or destroyed in the transaction — that means the net sum of all assets must stay the same.

Transaction types

There are two types of Transactions in Miden: local transactions and network transactions [not yet implemented].

Local transaction

Users transition their account's state locally using the Miden VM and generate a Transaction proof that can be verified by the network, which we call client-side proving. The network then only has to verify the proof and to change the global parts of the state to apply the state transition.

They are useful, because:

  1. They enable privacy as neither the account state nor account code are needed to verify the zero-knowledge proof. Public inputs are only commitments and block information that are stored on-chain.
  2. They are cheaper (i.e., lower in fees) as the execution of the state transition and the generation of the zero-knowledge proof are already made by the users. Hence privacy is the cheaper option on Miden.
  3. They allow arbitrarily complex computation to be done. The proof size doesn't grow linearly with the complexity of the computation. Hence there is no gas limit for client-side proving.

Client-side proving or local transactions on low-power devices can be slow, but Miden offers a pragmatic alternative: delegated proving. Instead of waiting for complex computations to finish on your device, you can hand off proof generation to a service, ensuring a consistent 1-2 second proving time, even on mobile.

Network transaction

The Miden operator executes the Transaction and generates the proof. Miden uses network Transactions for smart contracts with public shared state. This type of Transaction is quite similar to the ones in traditional blockchains (e.g., Ethereum).

They are useful, because:

  1. For public shared state of smart contracts. Network Transactions allow orchestrated state changes of public smart contracts without race conditions.
  2. Smart contracts should be able to be executed autonomously, ensuring liveness. Local Transactions require a user to execute and prove, but in some cases a smart contract should be able to execute when certain conditions are met.
  3. Clients may not have sufficient resources to generate zero-knowledge proofs.

The ability to facilitate both, local and network Transactions, is one of the differentiating factors of Miden compared to other blockchains. Local Transaction execution and proving can happen in parallel as for most Transactions there is no need for public state changes. This increases the network's throughput tremendously and provides privacy. Network Transactions on the other hand enable autonomous smart contracts and public shared state.


Good to know

  • Usually, notes that are consumed in a Transaction must be recorded on-chain in order for the Transaction to succeed. However, Miden supports erasable notes which are notes that can be consumed in a Transaction before being registered on-chain. For example, one can build a sub-second order book by allowing its traders to build faster transactions that depend on each other and are being validated or erased in batches.

  • There is no nullifier check during a Transaction. Nullifiers are checked by the Miden operator during Transaction verification. So at the local level, there is "double spending." If a note was already spent, i.e. there exists a nullifier for that note, the block producer would never include the Transaction as it would make the block invalid.

  • One of the main reasons for separating execution and proving steps is to allow stateless provers; i.e., the executed Transaction has all the data it needs to re-execute and prove a Transaction without database access. This supports easier proof-generation distribution.

  • Not all Transactions require notes. For example, the owner of a faucet can mint new tokens using only a Transaction script, without interacting with external notes.

  • In Miden executors can choose arbitrary reference blocks to execute against their state. Hence it is possible to set Transaction expiration heights and in doing so, to define a block height until a Transaction should be included into a block. If the Transaction is expired, the resulting account state change is not valid and the Transaction cannot be verified anymore.

  • Note and Transaction scripts can read the state of foreign accounts during execution. This is called foreign procedure invocation. For example, the price of an asset for the Swap script might depend on a certain value stored in the oracle account.

  • An example of the right usage of Transaction arguments is the consumption of a Swap note. Those notes allow asset exchange based on predefined conditions. Example:

    • The note's consumption condition is defined as "anyone can consume this note to take X units of asset A if they simultaneously create a note sending Y units of asset B back to the creator." If an executor wants to buy only a fraction (X-m) of asset A, they provide this amount via transaction arguments. The executor would provide the value m. The note script then enforces the correct transfer:
      • A new note is created returning Y-((m*Y)/X) of asset B to the sender.
      • A second note is created, holding the remaining (X-m) of asset A for future consumption.

State

The State describes the current condition of all accounts, notes, nullifiers and their statuses. Reflecting the “current reality” of the protocol at any given time.

What is the purpose of the Miden state model?

By employing a concurrent State model with local execution and proving, Miden achieves three primary properties: preserving privacy, supporting parallel transactions, and reducing state-bloat by minimizing on-chain data storage.

Miden’s State model focuses on:

  • Concurrency:
    Multiple transactions can be processed concurrently by distinct actors using local transaction execution which improves throughput and efficiency.

  • Flexible data storage:
    Users can store data privately on their own devices or within the network. This approach reduces reliance on the network for data availability, helps maintain user sovereignty, and minimizes unnecessary on-chain storage.

  • Privacy:
    By using notes and nullifiers, Miden ensures that value transfers remain confidential. Zero-knowledge proofs allow users to prove correctness without revealing sensitive information.

State model components

The Miden node maintains three databases to describe State:

  1. Accounts
  2. Notes
  3. Nullifiers

Architecture core concepts

Account database

The accounts database has two main purposes:

  1. Track state commitments of all accounts
  2. Store account data for public accounts

This is done using an authenticated data structure, a sparse Merkle tree.

Architecture core concepts

As described in the accounts section, there are two types of accounts:

  • Public accounts: where all account data is stored on-chain.
  • Private accounts: where only the commitments to the account is stored on-chain.

Private accounts significantly reduce storage overhead. A private account contributes only 40 bytes to the global State (15 bytes for the account ID + 32 bytes for the account commitment + 4 bytes for the block number). For example, 1 billion private accounts take up only 47.47 GB of State.

The storage contribution of a public account depends on the amount of data it stores.

Warning

  • In Miden, when the user is the custodian of their account State (in the case of a private account), losing this State amounts to losing their funds, similar to losing a private key.

Note database

As described in the notes section, there are two types of notes:

  • Public notes: where the entire note content is stored on-chain.
  • Private notes: where only the note’s commitment is stored on-chain.

Private notes greatly reduce storage requirements and thus result in lower fees. At high throughput (e.g., 1K TPS), the note database could grow by about 1TB/year. However, only unconsumed public notes and enough information to construct membership proofs must be stored explicitly. Private notes, as well as consumed public notes, can be discarded. This solves the issue of infinitely growing note databases.

Notes are recorded in an append-only accumulator, a Merkle Mountain Range.

Using a Merkle Mountain Range (append-only accumulator) is important for two reasons:

  1. Membership witnesses (that a note exists in the database) against such an accumulator needs to be updated very infrequently.
  2. Old membership witnesses can be extended to a new accumulator value, but this extension does not need to be done by the original witness holder.

Both of these properties are needed for supporting local transactions using client-side proofs and privacy. In an append-only data structure, witness data does not become stale when the data structure is updated. That means users can generate valid proofs even if they don’t have the latest State of this database; so there is no need to query the operator on a constantly changing State.

Architecture core concepts

Nullifier database

Each note has an associated nullifier which enables the tracking of whether it's associated note has been consumed or not, preventing double-spending.

To prove that a note has not been consumed, the operator must provide a Merkle path to the corresponding node and show that the node’s value is 0. Since nullifiers are 32 bytes each, the sparse Merkle tree height must be sufficient to represent all possible nullifiers. Operators must maintain the entire nullifier set to compute the new tree root after inserting new nullifiers. For each nullifier we also record the block in which it was created. This way "unconsumed" nullifiers have block 0, but all consumed nullifiers have a non-zero block.

Note

  • Nullifiers in Miden break linkability between privately stored notes and their consumption details. To know the note’s nullifier, one must know the note’s data.

Architecture core concepts

Additional information

Public shared state

In most blockchains, most smart contracts and decentralized applications (e.g., AAVE, Uniswap) need public shared State. Public shared State is also available on Miden and can be represented as in the following example:

Public shared state

In this diagram, multiple participants interact with a common, publicly accessible State. The figure illustrates how notes are created and consumed:

  1. Independent Transactions Creating Notes (tx1 & tx2):
    Two separate users (Acc1 and Acc2) execute transactions independently:

    • tx1 produces note 1
    • tx2 produces note 2

    These transactions occur in parallel and do not rely on each other, allowing concurrent processing without contention.

  2. Sequencing and Consuming Notes (tx3):
    The Miden node executes tx3 against the shared account, consuming notes 1 & 2 and producing notes 3 & 4. tx3 is a network transaction executed by the Miden operator. It merges independent contributions into a unified State update.

  3. Further Independent Transactions (tx4 & tx5):
    After the shared State is updated:

    • tx4 consumes note 4
    • tx5 consumes note 5

    Both users can now interact with notes generated by the public account, continuing the cycle of State evolution.

State bloat minimization

Miden nodes do not need to know the entire State to verify or produce new blocks. Rather than storing the full State data with the nodes, users keep their data locally, and the rollup stores only commitments to that data. While some contracts must remain publicly visible, this approach minimizes State bloat. Furthermore the Miden rollup can discard non-required data after certain conditions have been met.

This ensures that the account and note databases remain manageable, even under sustained high usage.

Blockchain

The Miden blockchain protocol describes how the state progresses through Blocks, which are containers that aggregate account state changes and their proofs, together with created and consumed notes. Blocks represent the delta of the global state between two time periods, and each is accompanied by a corresponding proof that attests to the correctness of all state transitions it contains. The current global state can be derived by applying all the Blocks to the genesis state.

Miden's blockchain protocol aims for the following:

  • Proven transactions: All included transactions have already been proven and verified when they reach the block.
  • Fast genesis syncing: New nodes can efficiently sync to the tip of the chain.

Execution diagram

Batch production

To reduce the required space on the blockchain, transaction proofs are not directly put into blocks. First, they are batched together by verifying them in the batch producer. The purpose of the batch producer is to generate a single proof that some number of proven transactions have been verified. This involves recursively verifying individual transaction proofs inside the Miden VM. As with any program that runs in the Miden VM, there is a proof of correct execution running the Miden verifier to verify transaction proofs. This results into a single batch proof.

Batch diagram

The batch producer aggregates transactions sequentially by verifying that their proofs and state transitions are correct. More specifically, the batch producer ensures:

  1. Ordering of transactions: If several transactions within the same batch affect a single account, the correct ordering must be enforced. For example, if Tx1 and Tx2 both describe state changes of account A, then the batch kernel must verify them in the order: A -> Tx1 -> A' -> Tx2 -> A''.
  2. Uniqueness of notes in a single batch: The batch producer must ensure the uniqueness of all notes across transactions in the batch. This will prevent the situation where duplicate notes, which would share identical nullifiers, are created. Only one such duplicate note could later be consumed, as the nullifier will be marked as spent after the first consumption. It also checks for double spends in the set of consumed notes, even though the real double spent check only happens at the block production level.
  3. Expiration windows: It is possible to set an expiration window for transactions, which in turn sets an expiration window for the entire batch. For instance, if transaction Tx1 expires at block 8 and transaction Tx2 expires at block 5, then the batch expiration will be set to the minimum of all transaction expirations, which is 5.
  4. Note erasure of erasable notes: Erasable notes don't exist in the Notes DB. They are unauthenticated. Accounts can still consume unauthenticated notes to consume those notes faster, they don't have to wait for notes being included into a block. If creation and consumption of an erasable note happens in the same batch, the batch producer erases this note.

Block production

To create a Block, multiple batches and their respective proofs are aggregated. Block production is not parallelizable and must be performed by the Miden operator. In the future, several Miden operators may compete for Block production. The schema used for Block production is similar to that in batch production—recursive verification. Multiple batch proofs are aggregated into a single Block proof.

The block producer ensures:

  1. Account DB integrity: The Block N+1 Account DB commitment must be authenticated against all previous and resulting account commitments across transactions, ensuring valid state transitions and preventing execution on stale states.
  2. Nullifier DB integrity: Nullifiers of newly created notes are added to the Nullifier DB. The Block N+1 Nullifier DB commitment must be authenticated against all new nullifiers to guarantee completeness.
  3. Block hash references: Check that all block hashes references by batches are in the chain.
  4. Double-spend prevention: Each consumed note’s nullifier is checked against prior consumption. The Block N Nullifier DB commitment is authenticated against all provided nullifiers for consumed notes, ensuring no nullifier is reused.
  5. Global note uniqueness: All created and consumed notes must be unique across batches.
  6. Batch expiration: The block height of the created block must be smaller or equal than the lowest batch expiration.
  7. Block time increase: The block timestamp must increase monotonically from the previous block.
  8. Note erasure of erasable notes: If an erasable note is created and consumed in different batches, it is erased now. If, however, an erasable note is consumed but not created within the block, the batch it contains is rejected. The Miden operator's mempool should preemptively filter such transactions.

In final Block contains:

  • The commitments to the current global state.
  • The newly created nullifiers.
  • The commitments to newly created notes.
  • The new state commitments for affected private accounts.
  • The full states for all affected public accounts and newly created notes.

The Block proof attests to the correct state transition from the previous Block commitment to the next, and therefore to the change in Miden's global state.

Block diagram

Tip: Block Contents

  • State updates: Contains only the hashes of updated elements. For example, for each updated account, a tuple is recorded as ([account id], [new account hash]).
  • ZK Proof: This proof attests that, given a state commitment from the previous Block, a set of valid batches was executed that resulted in the new state commitment.
  • The Block also includes the full account and note data for public accounts and notes. For example, if account 123 is a public account that has been updated, you would see a record in the state updates section as (123, 0x456..), and the full new state of this account (which should hash to 0x456..) would be included in a separate section.

Verifying blocks

To verify that a Block corresponds to a valid global state transition, the following steps must be performed:

  1. Compute the hashes of public accounts and note states.
  2. Ensure that these hashes match the records in the state updates section.
  3. Verify the included Block proof using the following public inputs and output:
    • Input: Previous Block commitment.
    • Input: Set of batch commitments.
    • Output: Current Block commitment.

These steps can be performed by any verifier (e.g., a contract on Ethereum, Polygon AggLayer, or a decentralized network of Miden nodes).

Syncing from genesis

Nodes can sync efficiently from genesis to the tip of the chain through a multi-step process:

  1. Download historical Blocks from genesis to the present.
  2. Verify zero-knowledge proofs for all Blocks.
  3. Retrieve current state data (accounts, notes, and nullifiers).
  4. Validate that the downloaded state matches the latest Block's state commitment.

This approach enables fast blockchain syncing by verifying Block proofs rather than re-executing individual transactions, resulting in exponentially faster performance. Consequently, state sync is dominated by the time needed to download the data.

Consensus and decentralization

Miden will start as a centralized L2 on the Ethereum network. Over time, Miden will decentralize, but this part of the protocol, especially consensus is not yet set.

Introduction

Basic tutorials and examples of how to build applications on Miden.

The goal is to make getting up to speed with building on Miden as quick and simple as possible.

All of the following tutorials are accompanied by code examples in Rust and TypeScript, which can be found in the Miden Tutorials repository.

Miden Node Setup Tutorial

To run the Miden tutorial examples, you will need to set up a test enviorment and connect to a Miden node.

There are two ways to connect to a Miden node:

  1. Run the Miden node locally
  2. Connect to the Miden testnet

Prerequisites

To run miden-node locally, you need to:

  1. Install the miden-node crate.
  2. Provide a genesis.toml file.
  3. Provide a miden-node.toml file.

Example genesis.toml and miden-node.toml files can be found in the miden-tutorials repository:

  • The genesis.toml file defines the start timestamp for the miden-node testnet and allows you to pre-deploy accounts and funding faucets.
  • The miden-node.toml file configures the RPC endpoint and other settings for the miden-node.

Running the Miden node locally

Step 1: Clone the miden-tutorials repository

In a terminal window, clone the miden-tutorials repository and navigate to the root of the repository using this command:

git clone git@github.com:0xPolygonMiden/miden-tutorials.git
cd miden-tutorials

Step 2: Install the Miden node

Next, install the miden-node crate using this command:

cargo install miden-node --locked

Step 3: Initializing the node

To start the node, we first need to generate the genesis file. To do so, navigate to the /node directory and create the genesis file using this command:

cd node
miden-node make-genesis \
  --inputs-path  config/genesis.toml \
  --output-path  storage/genesis.dat

Expected output:

Genesis input file: config/genesis.toml has successfully been loaded.
Creating fungible faucet account...
Account "faucet" has successfully been saved to: storage/accounts/faucet.mac
Miden node genesis successful: storage/genesis.dat has been created

Step 4: Starting the node

Now, to start the node, navigate to the storage directory and run this command:

cd storage
miden-node start \
  --config node/config/miden-node.toml \
  node

Expected output:

2025-01-17T12:14:55.432445Z  INFO try_build_batches: miden-block-producer: /Users/username/.cargo/registry/src/index.crates.io-6f17d22bba15001f/miden-node-block-producer-0.6.0/src/txqueue/mod.rs:85: close, time.busy: 8.88µs, time.idle: 103µs
2025-01-17T12:14:57.433162Z  INFO try_build_batches: miden-block-producer: /Users/username/.cargo/registry/src/index.crates.io-6f17d22bba15001f/miden-node-block-producer-0.6.0/src/txqueue/mod.rs:85: new
2025-01-17T12:14:57.433256Z  INFO try_build_batches: miden-block-producer: /Users/username/.cargo/registry/src/index.crates.io-6f17d22bba15001f/miden-node-block-producer-0.6.0/src/txqueue/mod.rs:85: close, time.busy: 6.46µs, time.idle: 94.0µs

Congratulations, you now have a Miden node running locally. Now we can start creating a testing environment for building applications on Miden!

The endpoint of the Miden node running locally is:

http://localhost:57291

Reseting the node

If you need to reset the local state of the node and the rust-client, navigate to the root of the miden-tutorials repository and run this command:

rm -rf rust-client/store.sqlite3 
rm -rf node/storage/accounts
rm -rf node/storage/blocks

Connecting to the Miden testnet

To run the tutorial examples using the Miden testnet, use this endpoint:

https://rpc.testnet.miden.io:443

Rust Client

Rust library, which can be used to programmatically interact with the Miden rollup.

The Miden Rust client can be used for a variety of things, including:

  • Deploying, testing, and creating transactions to interact with accounts and notes on Miden.
  • Storing the state of accounts and notes locally.
  • Generating and submitting proofs of transactions.

This section of the docs is an overview of the different things one can achieve using the Rust client, and how to implement them.

Keep in mind that both the Rust client and the documentation are works-in-progress!

Creating Accounts and Faucets

Using the Miden client in Rust to create accounts and deploy faucets

Overview

In this tutorial, we will create a Miden account for Alice and deploy a fungible faucet. In the next section, we will mint tokens from the faucet to fund her account and transfer tokens from Alice's account to other Miden accounts.

What we'll cover

  • Understanding the differences between public and private accounts & notes
  • Instantiating the Miden client
  • Creating new accounts (public or private)
  • Deploying a faucet to fund an account

Prerequisites

Before you begin, ensure that a Miden node is running locally in a separate terminal window. To get the Miden node running locally, you can follow the instructions on the Miden Node Setup page.

Public vs. private accounts & notes

Before diving into coding, let's clarify the concepts of public and private accounts & notes on Miden:

  • Public accounts: The account's data and code are stored on-chain and are openly visible, including its assets.
  • Private accounts: The account's state and logic are off-chain, only known to its owner.
  • Public notes: The note's state is visible to anyone - perfect for scenarios where transparency is desired.
  • Private notes: The note's state is stored off-chain, you will need to share the note data with the relevant parties (via email or Telegram) for them to be able to consume the note.

Note: The term "account" can be used interchangeably with the term "smart contract" since account abstraction on Miden is handled natively.

It is useful to think of notes on Miden as "cryptographic cashier's checks" that allow users to send tokens. If the note is private, the note transfer is only known to the sender and receiver.

Step 1: Initialize your repository

Create a new Rust repository for your Miden project and navigate to it with the following command:

cargo new miden-rust-client
cd miden-rust-client 

Add the following dependencies to your Cargo.toml file:

[dependencies]
miden-client = { version = "0.7", features = ["testing", "concurrent", "tonic", "sqlite"] }
miden-lib = { version = "0.7", default-features = false }
miden-objects = { version = "0.7.2", default-features = false }
miden-crypto = { version = "0.13.2", features = ["executable"] }
rand = { version = "0.8" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
tokio = { version = "1.40", features = ["rt-multi-thread", "net", "macros"] }
rand_chacha = "0.3.1"

Step 2: Initialize the client

Before interacting with the Miden network, we must instantiate the client. In this step, we specify several parameters:

  • RPC endpoint - The URL of the Miden node you will connect to.
  • Client RNG - The random number generator used by the client, ensuring that the serial number of newly created notes are unique.
  • SQLite Store – An SQL database used by the client to store account and note data.
  • Authenticator - The component responsible for generating transaction signatures.

Copy and paste the following code into your src/main.rs file.

use miden_client::{
    account::{
        component::{BasicFungibleFaucet, BasicWallet, RpoFalcon512},
        AccountBuilder, AccountId, AccountStorageMode, AccountType,
    },
    asset::{FungibleAsset, TokenSymbol},
    auth::AuthSecretKey,
    crypto::{RpoRandomCoin, SecretKey},
    note::NoteType,
    rpc::{Endpoint, TonicRpcClient},
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{OutputNote, PaymentTransactionData, TransactionRequestBuilder},
    Client, ClientError, Felt,
};
use miden_lib::note::create_p2id_note;
use miden_objects::account::AccountIdVersion;

use rand::Rng;
use std::sync::Arc;
use tokio::time::Duration;

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new("http".to_string(), "localhost".to_string(), Some(57291));
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the same store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate the client. Toggle `in_debug_mode` as needed
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    let sync_summary = client.sync_state().await.unwrap();
    let block_number = sync_summary.block_num;
    println!("Latest block number: {}", block_number);

    Ok(())
}

When running the code above, there will be some unused imports, however, we will use these imports later on in the tutorial.

In this step, we will initialize a Miden client capable of syncing with the blockchain (in this case, our local node). Run the following command to execute src/main.rs:

cargo run --release 

After the program executes, you should see the latest block number printed to the terminal, for example:

Latest block number: 3855 

Step 3: Creating a wallet

Now that we've initialized the client, we can create a wallet for Alice.

To create a wallet for Alice using the Miden client, we define the account type as mutable or immutable and specify whether it is public or private. A mutable wallet means you can change the account code after deployment. A wallet on Miden is simply an account with standardized code.

In the example below we create a mutable public account for Alice.

Add this snippet to the end of your file in the main() function:

#![allow(unused)]
fn main() {
//------------------------------------------------------------
// STEP 1: Create a basic wallet for Alice
//------------------------------------------------------------
println!("\n[STEP 1] Creating a new account for Alice");

// Account seed
let mut init_seed = [0u8; 32];
client.rng().fill_bytes(&mut init_seed);

// Generate key pair
let key_pair = SecretKey::with_rng(client.rng());

// Anchor block
let anchor_block = client.get_latest_epoch_block().await.unwrap();

// Build the account
let builder = AccountBuilder::new(init_seed)
    .anchor((&anchor_block).try_into().unwrap())
    .account_type(AccountType::RegularAccountUpdatableCode)
    .storage_mode(AccountStorageMode::Public)
    .with_component(RpoFalcon512::new(key_pair.public_key()))
    .with_component(BasicWallet);

let (alice_account, seed) = builder.build().unwrap();

// Add the account to the client
client
    .add_account(
        &alice_account,
        Some(seed),
        &AuthSecretKey::RpoFalcon512(key_pair),
        false,
    )
    .await?;

println!("Alice's account ID: {:?}", alice_account.id().to_hex());
}

Step 4: Deploying a fungible faucet

To provide Alice with testnet assets, we must first deploy a faucet. A faucet account on Miden mints fungible tokens.

We'll create a public faucet with a token symbol, decimals, and a max supply. We will use this faucet to mint tokens to Alice's account in the next section.

Add this snippet to the end of your file in the main() function:

#![allow(unused)]
fn main() {
//------------------------------------------------------------
// STEP 2: Deploy a fungible faucet
//------------------------------------------------------------
println!("\n[STEP 2] Deploying a new fungible faucet.");

// Faucet seed
let mut init_seed = [0u8; 32];
client.rng().fill_bytes(&mut init_seed);

// Faucet parameters
let symbol = TokenSymbol::new("MID").unwrap();
let decimals = 8;
let max_supply = Felt::new(1_000_000);

// Generate key pair
let key_pair = SecretKey::with_rng(client.rng());

// Build the account
let builder = AccountBuilder::new(init_seed)
    .anchor((&anchor_block).try_into().unwrap())
    .account_type(AccountType::FungibleFaucet)
    .storage_mode(AccountStorageMode::Public)
    .with_component(RpoFalcon512::new(key_pair.public_key()))
    .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap());

let (faucet_account, seed) = builder.build().unwrap();

// Add the faucet to the client
client
    .add_account(
        &faucet_account,
        Some(seed),
        &AuthSecretKey::RpoFalcon512(key_pair),
        false,
    )
    .await?;

println!("Faucet account ID: {:?}", faucet_account.id().to_hex());
}

When tokens are minted from this faucet, each token batch is represented as a "note" (UTXO). You can think of a Miden Note as a cryptographic cashier's check that has certain spend conditions attached to it.

Summary

Your updated main() function in src/main.rs should look like this:

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    let sync_summary = client.sync_state().await.unwrap();
    let block_number = sync_summary.block_num;
    println!("Latest block number: {}", block_number);

    //------------------------------------------------------------
    // STEP 1: Create a basic wallet for Alice
    //------------------------------------------------------------
    println!("\n[STEP 1] Creating a new account for Alice");

    // Account seed
    let mut init_seed = [0u8; 32];
    client.rng().fill_bytes(&mut init_seed);

    // Generate key pair
    let key_pair = SecretKey::with_rng(client.rng());

    // Anchor block
    let anchor_block = client.get_latest_epoch_block().await.unwrap();

    // Build the account
    let builder = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::RegularAccountUpdatableCode)
        .storage_mode(AccountStorageMode::Public)
        .with_component(RpoFalcon512::new(key_pair.public_key()))
        .with_component(BasicWallet);

    let (alice_account, seed) = builder.build().unwrap();

    // Add the account to the client
    client
        .add_account(
            &alice_account,
            Some(seed),
            &AuthSecretKey::RpoFalcon512(key_pair),
            false,
        )
        .await?;

    println!("Alice's account ID: {:?}", alice_account.id().to_hex());

    //------------------------------------------------------------
    // STEP 2: Deploy a fungible faucet
    //------------------------------------------------------------
    println!("\n[STEP 2] Deploying a new fungible faucet.");

    // Faucet seed
    let mut init_seed = [0u8; 32];
    client.rng().fill_bytes(&mut init_seed);

    // Faucet parameters
    let symbol = TokenSymbol::new("MID").unwrap();
    let decimals = 8;
    let max_supply = Felt::new(1_000_000);

    // Generate key pair
    let key_pair = SecretKey::with_rng(client.rng());

    // Build the account
    let builder = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::FungibleFaucet)
        .storage_mode(AccountStorageMode::Public)
        .with_component(RpoFalcon512::new(key_pair.public_key()))
        .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap());

    let (faucet_account, seed) = builder.build().unwrap();

    // Add the faucet to the client
    client
        .add_account(
            &faucet_account,
            Some(seed),
            &AuthSecretKey::RpoFalcon512(key_pair),
            false,
        )
        .await?;

    println!("Faucet account ID: {:?}", faucet_account.id().to_hex());

    // Resync to show newly deployed faucet
    client.sync_state().await?;

    Ok(())
}

Let's run the src/main.rs program again:

cargo run --release 

The output will look like this:

[STEP 1] Creating a new account for Alice
Alice's account ID: "0x715abc291819b1100000e7cd88cf3e"

[STEP 2] Deploying a new fungible faucet.
Faucet account ID: "0xab5fb36dd552982000009c440264ce"

In this section we explained how to instantiate the Miden client, create a wallet account, and deploy a faucet.

In the next section we will cover how to mint tokens from the faucet, consume notes, and send tokens to other accounts.

Running the example

To run a full working example navigate to the rust-client directory in the miden-tutorials repository and run this command:

cd rust-client
cargo run --release --bin create_mint_consume_send

Continue learning

Next tutorial: Mint, Consume, and Create Notes

Mint, Consume, and Create Notes

Using the Miden client in Rust to mint, consume, and create notes

Overview

In the previous section, we initialized our repository and covered how to create an account and deploy a faucet. In this section, we will mint tokens from the faucet for Alice, consume the newly created notes, and demonstrate how to send assets to other accounts.

What we'll cover

  • Minting tokens from a faucet
  • Consuming notes to fund an account
  • Sending tokens to other users

Step 1: Minting tokens from the faucet

To mint notes with tokens from the faucet we created, Alice needs to call the faucet with a mint transaction request.

In essence, a transaction request is a structured template that outlines the data required to generate a zero-knowledge proof of a state change of an account. It specifies which input notes (if any) will be consumed, includes an optional transaction script to execute, and enumerates the set of notes expected to be created (if any).

Below is an example of a transaction request minting tokens from the faucet for Alice. This code snippet will create 5 transaction mint transaction requests.

Add this snippet to the end of your file in the main() function that we created in the previous chapter:

#![allow(unused)]
fn main() {
//------------------------------------------------------------
// STEP 3: Mint 5 notes of 100 tokens for Alice
//------------------------------------------------------------
println!("\n[STEP 3] Minting 5 notes of 100 tokens each for Alice.");

let amount: u64 = 100;
let fungible_asset = FungibleAsset::new(faucet_account.id(), amount).unwrap();

for i in 1..=5 {
    let transaction_request = TransactionRequestBuilder::mint_fungible_asset(
        fungible_asset.clone(),
        alice_account.id(),
        NoteType::Public,
        client.rng(),
    )
    .unwrap()
    .build();
    let tx_execution_result = client
        .new_transaction(faucet_account.id(), transaction_request)
        .await?;

    client.submit_transaction(tx_execution_result).await?;
    println!("Minted note #{} of {} tokens for Alice.", i, amount);
}
println!("All 5 notes minted for Alice successfully!");

// Re-sync so minted notes become visible
client.sync_state().await?;
}

Step 2: Identifying consumable notes

Once Alice has minted a note from the faucet, she will eventually want to spend the tokens that she received in the note created by the mint transaction.

Minting a note from a faucet on Miden means a faucet account creates a new note targeted to the requesting account. The requesting account needs to consume this new note to have the assets appear in their account.

To identify consumable notes, the Miden client provides the get_consumable_notes function. Before calling it, ensure that the client state is synced.

Tip: If you know how many notes to expect after a transaction, use an await or loop condition to check how many notes of the type you expect are available for consumption instead of using a set timeout before calling get_consumable_notes. This ensures your application isn't idle for longer than necessary.

Identifying which notes are available:

#![allow(unused)]
fn main() {
let consumable_notes = client.get_consumable_notes(Some(alice_account.id())).await?;
}

Step 3: Consuming multiple notes in a single transaction:

Now that we know how to identify notes ready to consume, let's consume the notes created by the faucet in a single transaction. After consuming the notes, Alice's wallet balance will be updated.

The following code snippet identifies consumable notes and consumes them in a single transaction.

Add this snippet to the end of your file in the main() function:

//------------------------------------------------------------
// STEP 4: Alice consumes all her notes
//------------------------------------------------------------
println!("\n[STEP 4] Alice will now consume all of her notes to consolidate them.");

// Consume all minted notes in a single transaction
loop {
    // Resync to get the latest data
    client.sync_state().await?;

    let consumable_notes = client
        .get_consumable_notes(Some(alice_account.id()))
        .await?;
    let list_of_note_ids: Vec<_> = consumable_notes.iter().map(|(note, _)| note.id()).collect();

    if list_of_note_ids.len() == 5 {
        println!("Found 5 consumable notes for Alice. Consuming them now...");
        let transaction_request =
            TransactionRequestBuilder::consume_notes(list_of_note_ids).build();
        let tx_execution_result = client
            .new_transaction(alice_account.id(), transaction_request)
            .await?;

        client.submit_transaction(tx_execution_result).await?;
        println!("All of Alice's notes consumed successfully.");
        break;
    } else {
        println!(
            "Currently, Alice has {} consumable notes. Waiting for 5...",
            list_of_note_ids.len()
        );
        tokio::time::sleep(Duration::from_secs(3)).await;
    }
}

Step 4: Sending tokens to other accounts

After consuming the notes, Alice has tokens in her wallet. Now, she wants to send tokens to her friends. She has two options: create a separate transaction for each transfer or batch multiple transfers into a single transaction.

The standard asset transfer note on Miden is the P2ID note (Pay to Id). There is also the P2IDR (Pay to Id Reclaimable) variant which allows the creator of the note to reclaim the note after a certain block height.

In our example, Alice will now send 50 tokens to 5 different accounts.

For the sake of the example, the first four P2ID transfers are handled in a single transaction, and the fifth transfer is a standard P2ID transfer.

Output multiple P2ID notes in a single transaction

To output multiple notes in a single transaction we need to create a list of our expected output notes. The expected output notes are the notes that we expect to create in our transaction request.

In the snippet below, we create an empty vector to store five P2ID output notes, loop over five iterations (using 0..=4) to create five unique dummy account IDs, build a P2ID note for each one, and push each note onto the vector. Finally, we build a transaction request using .with_own_output_notes()—passing in all five notes—and submit it to the node.

Add this snippet to the end of your file in the main() function:

//------------------------------------------------------------
// STEP 5: Alice sends 5 notes of 50 tokens to 5 users
//------------------------------------------------------------
println!("\n[STEP 5] Alice sends 5 notes of 50 tokens each to 5 different users.");

// Send 50 tokens to 4 accounts in one transaction
println!("Creating multiple P2ID notes for 4 target accounts in one transaction...");
let mut p2id_notes = vec![];
for _ in 1..=4 {
    let init_seed = {
        let mut seed = [0u8; 15];
        rand::thread_rng().fill(&mut seed);
        seed[0] = 99u8;
        seed
    };
    let target_account_id = AccountId::dummy(
        init_seed,
        AccountIdVersion::Version0,
        AccountType::RegularAccountUpdatableCode,
        AccountStorageMode::Public,
    );

    let send_amount = 50;
    let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap();

    let p2id_note = create_p2id_note(
        alice_account.id(),
        target_account_id,
        vec![fungible_asset.into()],
        NoteType::Public,
        Felt::new(0),
        client.rng(),
    )?;
    p2id_notes.push(p2id_note);
}
let output_notes: Vec<OutputNote> = p2id_notes.into_iter().map(OutputNote::Full).collect();

let transaction_request = TransactionRequestBuilder::new()
    .with_own_output_notes(output_notes)
    .unwrap()
    .build();

let tx_execution_result = client
    .new_transaction(alice_account.id(), transaction_request)
    .await?;

client.submit_transaction(tx_execution_result).await?;
println!("Submitted a transaction with 4 P2ID notes.");

Basic P2ID transfer

Now as an example, Alice will send some tokens to an account in a single transaction.

Add this snippet to the end of your file in the main() function:

// Send 50 tokens to 1 more account as a single P2ID transaction
println!("Submitting one more single P2ID transaction...");
let init_seed = {
    let mut seed = [0u8; 15];
    rand::thread_rng().fill(&mut seed);
    seed[0] = 99u8;
    seed
};
let target_account_id = AccountId::dummy(
    init_seed,
    AccountIdVersion::Version0,
    AccountType::RegularAccountUpdatableCode,
    AccountStorageMode::Public,
);

let send_amount = 50;
let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap();

let payment_transaction = PaymentTransactionData::new(
    vec![fungible_asset.into()],
    alice_account.id(),
    target_account_id,
);
let transaction_request = TransactionRequestBuilder::pay_to_id(
    payment_transaction,
    None,             // recall_height
    NoteType::Public, // note type
    client.rng(),     // rng
)
.unwrap()
.build();
let tx_execution_result = client
    .new_transaction(alice_account.id(), transaction_request)
    .await?;

client.submit_transaction(tx_execution_result).await?;

Note: In a production environment do not use AccountId::new_dummy(), this is simply for the sake of the tutorial example.

Summary

Your src/main.rs function should now look like this:

use miden_client::{
    account::{
        component::{BasicFungibleFaucet, BasicWallet, RpoFalcon512},
        AccountBuilder, AccountId, AccountStorageMode, AccountType,
    },
    asset::{FungibleAsset, TokenSymbol},
    auth::AuthSecretKey,
    crypto::{RpoRandomCoin, SecretKey},
    note::NoteType,
    rpc::{Endpoint, TonicRpcClient},
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{OutputNote, PaymentTransactionData, TransactionRequestBuilder},
    Client, ClientError, Felt,
};
use miden_lib::note::create_p2id_note;
use miden_objects::account::AccountIdVersion;

use rand::Rng;
use std::sync::Arc;
use tokio::time::Duration;

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new("http".to_string(), "localhost".to_string(), Some(57291));
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the same store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate the client. Toggle `in_debug_mode` as needed
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    let sync_summary = client.sync_state().await.unwrap();
    let block_number = sync_summary.block_num;
    println!("Latest block number: {}", block_number);

    //------------------------------------------------------------
    // STEP 1: Create a basic wallet for Alice
    //------------------------------------------------------------
    println!("\n[STEP 1] Creating a new account for Alice");

    // Account seed
    let mut init_seed = [0u8; 32];
    client.rng().fill_bytes(&mut init_seed);

    // Generate key pair
    let key_pair = SecretKey::with_rng(client.rng());

    // Anchor block
    let anchor_block = client.get_latest_epoch_block().await.unwrap();

    // Build the account
    let builder = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::RegularAccountUpdatableCode)
        .storage_mode(AccountStorageMode::Public)
        .with_component(RpoFalcon512::new(key_pair.public_key()))
        .with_component(BasicWallet);

    let (alice_account, seed) = builder.build().unwrap();

    // Add the account to the client
    client
        .add_account(
            &alice_account,
            Some(seed),
            &AuthSecretKey::RpoFalcon512(key_pair),
            false,
        )
        .await?;

    println!("Alice's account ID: {:?}", alice_account.id().to_hex());

    //------------------------------------------------------------
    // STEP 2: Deploy a fungible faucet
    //------------------------------------------------------------
    println!("\n[STEP 2] Deploying a new fungible faucet.");

    // Faucet seed
    let mut init_seed = [0u8; 32];
    client.rng().fill_bytes(&mut init_seed);

    // Faucet parameters
    let symbol = TokenSymbol::new("MID").unwrap();
    let decimals = 8;
    let max_supply = Felt::new(1_000_000);

    // Generate key pair
    let key_pair = SecretKey::with_rng(client.rng());

    // Build the account
    let builder = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::FungibleFaucet)
        .storage_mode(AccountStorageMode::Public)
        .with_component(RpoFalcon512::new(key_pair.public_key()))
        .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap());

    let (faucet_account, seed) = builder.build().unwrap();

    // Add the faucet to the client
    client
        .add_account(
            &faucet_account,
            Some(seed),
            &AuthSecretKey::RpoFalcon512(key_pair),
            false,
        )
        .await?;

    println!("Faucet account ID: {:?}", faucet_account.id().to_hex());

    // Resync to show newly deployed faucet
    client.sync_state().await?;
    tokio::time::sleep(Duration::from_secs(2)).await;

    //------------------------------------------------------------
    // STEP 3: Mint 5 notes of 100 tokens for Alice
    //------------------------------------------------------------
    println!("\n[STEP 3] Minting 5 notes of 100 tokens each for Alice.");

    let amount: u64 = 100;
    let fungible_asset = FungibleAsset::new(faucet_account.id(), amount).unwrap();

    for i in 1..=5 {
        let transaction_request = TransactionRequestBuilder::mint_fungible_asset(
            fungible_asset.clone(),
            alice_account.id(),
            NoteType::Public,
            client.rng(),
        )
        .unwrap()
        .build();
        let tx_execution_result = client
            .new_transaction(faucet_account.id(), transaction_request)
            .await?;

        client.submit_transaction(tx_execution_result).await?;
        println!("Minted note #{} of {} tokens for Alice.", i, amount);
    }
    println!("All 5 notes minted for Alice successfully!");

    // Re-sync so minted notes become visible
    client.sync_state().await?;

    //------------------------------------------------------------
    // STEP 4: Alice consumes all her notes
    //------------------------------------------------------------
    println!("\n[STEP 4] Alice will now consume all of her notes to consolidate them.");

    // Consume all minted notes in a single transaction
    loop {
        // Resync to get the latest data
        client.sync_state().await?;

        let consumable_notes = client
            .get_consumable_notes(Some(alice_account.id()))
            .await?;
        let list_of_note_ids: Vec<_> = consumable_notes.iter().map(|(note, _)| note.id()).collect();

        if list_of_note_ids.len() == 5 {
            println!("Found 5 consumable notes for Alice. Consuming them now...");
            let transaction_request =
                TransactionRequestBuilder::consume_notes(list_of_note_ids).build();
            let tx_execution_result = client
                .new_transaction(alice_account.id(), transaction_request)
                .await?;

            client.submit_transaction(tx_execution_result).await?;
            println!("All of Alice's notes consumed successfully.");
            break;
        } else {
            println!(
                "Currently, Alice has {} consumable notes. Waiting for 5...",
                list_of_note_ids.len()
            );
            tokio::time::sleep(Duration::from_secs(3)).await;
        }
    }

    //------------------------------------------------------------
    // STEP 5: Alice sends 5 notes of 50 tokens to 5 users
    //------------------------------------------------------------
    println!("\n[STEP 5] Alice sends 5 notes of 50 tokens each to 5 different users.");

    // Send 50 tokens to 4 accounts in one transaction
    println!("Creating multiple P2ID notes for 4 target accounts in one transaction...");
    let mut p2id_notes = vec![];
    for _ in 1..=4 {
        let init_seed = {
            let mut seed = [0u8; 15];
            rand::thread_rng().fill(&mut seed);
            seed[0] = 99u8;
            seed
        };
        let target_account_id = AccountId::dummy(
            init_seed,
            AccountIdVersion::Version0,
            AccountType::RegularAccountUpdatableCode,
            AccountStorageMode::Public,
        );

        let send_amount = 50;
        let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap();

        let p2id_note = create_p2id_note(
            alice_account.id(),
            target_account_id,
            vec![fungible_asset.into()],
            NoteType::Public,
            Felt::new(0),
            client.rng(),
        )?;
        p2id_notes.push(p2id_note);
    }
    let output_notes: Vec<OutputNote> = p2id_notes.into_iter().map(OutputNote::Full).collect();

    let transaction_request = TransactionRequestBuilder::new()
        .with_own_output_notes(output_notes)
        .unwrap()
        .build();

    let tx_execution_result = client
        .new_transaction(alice_account.id(), transaction_request)
        .await?;

    client.submit_transaction(tx_execution_result).await?;
    println!("Submitted a transaction with 4 P2ID notes.");

    // Send 50 tokens to 1 more account as a single P2ID transaction
    println!("Submitting one more single P2ID transaction...");
    let init_seed = {
        let mut seed = [0u8; 15];
        rand::thread_rng().fill(&mut seed);
        seed[0] = 99u8;
        seed
    };
    let target_account_id = AccountId::dummy(
        init_seed,
        AccountIdVersion::Version0,
        AccountType::RegularAccountUpdatableCode,
        AccountStorageMode::Public,
    );

    let send_amount = 50;
    let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap();

    let payment_transaction = PaymentTransactionData::new(
        vec![fungible_asset.into()],
        alice_account.id(),
        target_account_id,
    );
    let transaction_request = TransactionRequestBuilder::pay_to_id(
        payment_transaction,
        None,             // recall_height
        NoteType::Public, // note type
        client.rng(),     // rng
    )
    .unwrap()
    .build();
    let tx_execution_result = client
        .new_transaction(alice_account.id(), transaction_request)
        .await?;

    client.submit_transaction(tx_execution_result).await?;

    println!("\nAll steps completed successfully!");
    println!("Alice created a wallet, a faucet was deployed,");
    println!("5 notes of 100 tokens were minted to Alice, those notes were consumed,");
    println!("and then Alice sent 5 separate 50-token notes to 5 different users.");

    Ok(())
}

Let's run the src/main.rs program again:

cargo run --release 

The output will look like this:

Client initialized successfully.
Latest block number: 1519

[STEP 1] Creating a new account for Alice
Alice's account ID: "0xd0e8ba5acf2e83100000887188d2b9"

[STEP 2] Deploying a new fungible faucet.
Faucet account ID: "0xcdf877e221333a2000002e2b7ff0b2"

[STEP 3] Minting 5 notes of 100 tokens each for Alice.
Minted note #1 of 100 tokens for Alice.
Minted note #2 of 100 tokens for Alice.
Minted note #3 of 100 tokens for Alice.
Minted note #4 of 100 tokens for Alice.
Minted note #5 of 100 tokens for Alice.
All 5 notes minted for Alice successfully!

[STEP 4] Alice will now consume all of her notes to consolidate them.
Currently, Alice has 1 consumable notes. Waiting for 5...
Currently, Alice has 4 consumable notes. Waiting for 5...
Found 5 consumable notes for Alice. Consuming them now...
one or more warnings were emitted
All of Alice's notes consumed successfully.

[STEP 5] Alice sends 5 notes of 50 tokens each to 5 different users.
Creating multiple P2ID notes for 4 target accounts in one transaction...
Submitted a transaction with 4 P2ID notes.
Submitting one more single P2ID transaction...

All steps completed successfully!
Alice created a wallet, a faucet was deployed,
5 notes of 100 tokens were minted to Alice, those notes were consumed,
and then Alice sent 5 separate 50-token notes to 5 different users.

Running the example

To run a full working example navigate to the rust-client directory in the miden-tutorials repository and run this command:

cd rust-client
cargo run --release --bin create_mint_consume_send

Continue learning

Next tutorial: Deploying a Counter Contract

Deploying a Counter Contract

Using the Miden client in Rust to deploy and interact with a custom smart contract on Miden

Overview

In this tutorial, we will build a simple counter smart contract that maintains a count, deploy it to the Miden testnet, and interact with it by incrementing the count. You can also deploy the counter contract on a locally running Miden node, similar to previous tutorials.

Using a script, we will invoke the increment function within the counter contract to update the count. This tutorial provides a foundational understanding of developing and deploying custom smart contracts on Miden.

What we'll cover

  • Deploying a custom smart contract on Miden
  • Getting up to speed with the basics of Miden assembly
  • Calling procedures in an account
  • Pure vs state changing procedures

Prerequisites

This tutorial assumes you have a basic understanding of Miden assembly. To quickly get up to speed with Miden assembly (MASM), please play around with running Miden programs in the Miden playground.

Step 1: Initialize your repository

Create a new Rust repository for your Miden project and navigate to it with the following command:

cargo new miden-counter-contract
cd miden-counter-contract

Add the following dependencies to your Cargo.toml file:

[dependencies]
miden-client = { version = "0.7", features = ["testing", "concurrent", "tonic", "sqlite"] }
miden-lib = { version = "0.7", default-features = false }
miden-objects = { version = "0.7.2", default-features = false }
miden-crypto = { version = "0.13.2", features = ["executable"] }
rand = { version = "0.8" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
tokio = { version = "1.40", features = ["rt-multi-thread", "net", "macros"] }
rand_chacha = "0.3.1"

Set up your src/main.rs file

In the previous section, we explained how to instantiate the Miden client. We can reuse the same initialize_client function for our counter contract.

Copy and paste the following code into your src/main.rs file:

use std::{fs, path::Path, sync::Arc};

use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use tokio::time::Duration;

use miden_client::{
    account::{AccountStorageMode, AccountType},
    crypto::RpoRandomCoin,
    rpc::{Endpoint, TonicRpcClient},
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{TransactionKernel, TransactionRequestBuilder},
    Client, ClientError, Felt,
};

use miden_objects::{
    account::{AccountBuilder, AccountComponent, AuthSecretKey, StorageSlot},
    assembly::Assembler,
    crypto::{dsa::rpo_falcon512::SecretKey, hash::rpo::RpoDigest},
    Word,
};

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new(
        "https".to_string(),
        "rpc.testnet.miden.io".to_string(),
        Some(443),
    );
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate client (toggle debug mode as needed)
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

pub fn get_new_pk_and_authenticator() -> (Word, AuthSecretKey) {
    // Create a deterministic RNG with zeroed seed
    let seed = [0_u8; 32];
    let mut rng = ChaCha20Rng::from_seed(seed);

    // Generate Falcon-512 secret key
    let sec_key = SecretKey::with_rng(&mut rng);

    // Convert public key to `Word` (4xFelt)
    let pub_key: Word = sec_key.public_key().into();

    // Wrap secret key in `AuthSecretKey`
    let auth_secret_key = AuthSecretKey::RpoFalcon512(sec_key);

    (pub_key, auth_secret_key)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    let sync_summary = client.sync_state().await.unwrap();
    println!("Latest block: {}", sync_summary.block_num);

    Ok(())
}

When running the code above, there will be some unused imports, however, we will use these imports later on in the tutorial.

Step 2: Build the counter contract

For better code organization, we will separate the Miden assembly code from our Rust code.

Create a directory named masm at the root of your miden-counter-contract directory. This will contain our contract and script masm code.

Initialize the masm directory:

mkdir -p masm/accounts masm/scripts

This will create:

masm/
├── accounts/
└── scripts/

Custom Miden smart contract

Below is our counter contract. It has a two exported procedures: get_count and increment_count.

At the beginning of the MASM file, we define our imports. In this case, we import miden::account and std::sys.

The import miden::account contains useful procedures for interacting with a smart contract's state.

The import std::sys contains a useful procedure for truncating the operand stack at the end of a procedure.

Here's a breakdown of what the get_count procedure does:

  1. Pushes 0 onto the stack, representing the index of the storage slot to read.
  2. Calls account::get_item with the index of 0.
  3. Calls sys::truncate_stack to truncate the stack to size 16.
  4. The value returned from account::get_item is still on the stack and will be returned when this procedure is called.

Here's a breakdown of what the increment_count procedure does:

  1. Pushes 0 onto the stack, representing the index of the storage slot to read.
  2. Calls account::get_item with the index of 0.
  3. Pushes 1 onto the stack.
  4. Adds 1 to the count value returned from account::get_item.
  5. For demonstration purposes, calls debug.stack to see the state of the stack
  6. Pushes 0 onto the stack, which is the index of the storage slot we want to write to.
  7. Calls account::set_item which saves the incremented count to storage at index 0
  8. Calls sys::truncate_stack to truncate the stack to size 16.

Inside of the masm/accounts/ directory, create the counter.masm file:

use.miden::account
use.std::sys

export.get_count
    # => []
    push.0
    
    # => [index]
    exec.account::get_item

    # => [count]
    exec.sys::truncate_stack
end

export.increment_count
    # => []
    push.0

    # => [index]
    exec.account::get_item

    # => [count]
    push.1 add

    # debug statement with client
    debug.stack

    # => [count+1]
    push.0

    # [index, count+1]
    exec.account::set_item

    # => []
    push.1 exec.account::incr_nonce

    # => []
    exec.sys::truncate_stack
end

Note: It's a good habit to add comments above each line of MASM code with the expected stack state. This improves readability and helps with debugging.

Concept of function visibility and modifiers in Miden smart contracts

The increment_count function in our Miden smart contract behaves like an "external" Solidity function without a modifier, meaning any user can call it to increment the contract's count. This is because it calls account::incr_nonce during execution.

If the increment_count procedure did not call the account::incr_nonce procedure during its execution, only the deployer of the counter contract would be able to increment the count of the smart contract (if the RpoFalcon512 component was added to the account, in this case we didn't add it).

In essence, if a procedure performs a state change in the Miden smart contract, and does not call account::incr_nonce at some point during its execution, this function can be equated to having an onlyOwner Solidity modifer, meaning only the user with knowledge of the private key of the account can execute transactions that result in a state change.

Note: Adding the account::incr_nonce to a state changing procedure allows any user to call the procedure.

Custom script

This is a Miden assembly script that will call the increment_count procedure during the transaction.

The string {increment_count} will be replaced with the hash of the increment_count procedure in our rust program.

Inside of the masm/scripts/ directory, create the counter_script.masm file:

begin
    # => []
    call.{increment_count}
end

Step 3: Build the counter smart contract in Rust

To build the counter contract copy and paste the following code at the end of your src/main.rs file:

#![allow(unused)]
fn main() {
// -------------------------------------------------------------------------
// STEP 1: Create a basic counter contract
// -------------------------------------------------------------------------
println!("\n[STEP 1] Creating counter contract.");

// Load the MASM file for the counter contract
let file_path = Path::new("./masm/accounts/counter.masm");
let account_code = fs::read_to_string(file_path).unwrap();

// Prepare assembler (debug mode = true)
let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);

// Compile the account code into `AccountComponent` with one storage slot
let counter_component = AccountComponent::compile(
    account_code,
    assembler,
    vec![StorageSlot::Value(Word::default())],
)
.unwrap()
.with_supports_all_types();

// Init seed for the counter contract
let init_seed = ChaCha20Rng::from_entropy().gen();

// Anchor block of the account
let anchor_block = client.get_latest_epoch_block().await.unwrap();

// Build the new `Account` with the component
let (counter_contract, counter_seed) = AccountBuilder::new(init_seed)
    .anchor((&anchor_block).try_into().unwrap())
    .account_type(AccountType::RegularAccountImmutableCode)
    .storage_mode(AccountStorageMode::Public)
    .with_component(counter_component.clone())
    .build()
    .unwrap();

println!(
    "counter_contract hash: {:?}",
    counter_contract.hash().to_hex()
);
println!("contract id: {:?}", counter_contract.id().to_hex());

// Since anyone should be able to write to the counter contract, auth_secret_key is not required.
// However, to import to the client, we must generate a random value.
let (_counter_pub_key, auth_secret_key) = get_new_pk_and_authenticator();

client
    .add_account(
        &counter_contract.clone(),
        Some(counter_seed),
        &auth_secret_key,
        false,
    )
    .await
    .unwrap();
}

Run the following command to execute src/main.rs:

cargo run --release 

After the program executes, you should see the counter contract hash and contract id printed to the terminal, for example:

counter_contract hash: "0xd693494753f51cb73a436916077c7b71c680a6dddc64dc364c1fe68f16f0c087"
contract id: "0x082ed14c8ad9a866"

Step 4: Computing the prodedure hashes

Each Miden assembly procedure has an associated hash. When calling a procedure in a smart contract, we need to know the hash of the procedure. The hashes of the procedures form a Merkelized Abstract Syntax Tree (MAST).

To get the hash of the increment_count procedure, add this code snippet to the end of your main() function:

#![allow(unused)]
fn main() {
// Print the procedure root hash
let get_increment_export = counter_component
    .library()
    .exports()
    .find(|export| export.name.as_str() == "increment_count")
    .unwrap();

let get_increment_count_mast_id = counter_component
    .library()
    .get_export_node_id(get_increment_export);

let increment_count_hash = counter_component
    .library()
    .mast_forest()
    .get_node_by_id(get_increment_count_mast_id)
    .unwrap()
    .digest().to_hex();

println!("increment_count procedure hash: {:?}", increment_count_hash);
}

Run the following command to execute src/main.rs:

cargo run --release 

After the program executes, you should see the procedure hashes printed to the terminal, for example:

increment_count procedure hash: "0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54"

This is the hash of the increment_count procedure.

Step 4: Incrementing the count

Now that we know the hash of the increment_count procedure, we can call the procedure in the counter contract. In the Rust code below, we replace the {increment_count} string with the hash of the increment_count procedure.

Then we create a new transaction request with our custom script, and then pass the transaction request to the client.

Paste the following code at the end of your src/main.rs file:

#![allow(unused)]
fn main() {
// -------------------------------------------------------------------------
// STEP 2: Call the Counter Contract with a script
// -------------------------------------------------------------------------
println!("\n[STEP 2] Call Counter Contract With Script");

// Load the MASM script referencing the increment procedure
let file_path = Path::new("./masm/scripts/counter_script.masm");
let original_code = fs::read_to_string(file_path).unwrap();

// Replace the placeholder with the actual procedure call
let replaced_code = original_code.replace("{increment_count}", &increment_count_hash);
println!("Final script:\n{}", replaced_code);

// Compile the script referencing our procedure
let tx_script = client.compile_tx_script(vec![], &replaced_code).unwrap();

// Build a transaction request with the custom script
let tx_increment_request = TransactionRequestBuilder::new()
    .with_custom_script(tx_script)
    .unwrap()
    .build();

// Execute the transaction locally
let tx_result = client
    .new_transaction(counter_contract.id(), tx_increment_request)
    .await
    .unwrap();

let tx_id = tx_result.executed_transaction().id();
println!(
    "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
    tx_id
);

// Submit transaction to the network
let _ = client.submit_transaction(tx_result).await;

// Wait, then re-sync
tokio::time::sleep(Duration::from_secs(3)).await;
client.sync_state().await.unwrap();

// Retrieve updated contract data to see the incremented counter
let account = client.get_account(counter_contract.id()).await.unwrap();
println!(
    "counter contract storage: {:?}",
    account.unwrap().account().storage().get_item(0)
);
}

Note: Once our counter contract is deployed, other users can increment the count of the smart contract simply by knowing the account id of the contract and the procedure hash of the increment_count procedure.

Summary

The final src/main.rs file should look like this:

use std::{fs, path::Path, sync::Arc};

use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use tokio::time::Duration;

use miden_client::{
    account::{AccountStorageMode, AccountType},
    crypto::RpoRandomCoin,
    rpc::{Endpoint, TonicRpcClient},
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{TransactionKernel, TransactionRequestBuilder},
    Client, ClientError, Felt,
};

use miden_objects::{
    account::{AccountBuilder, AccountComponent, AuthSecretKey, StorageSlot},
    assembly::Assembler,
    crypto::dsa::rpo_falcon512::SecretKey,
    Word,
};

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new(
        "https".to_string(),
        "rpc.testnet.miden.io".to_string(),
        Some(443),
    );
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate client (toggle debug mode as needed)
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

pub fn get_new_pk_and_authenticator() -> (Word, AuthSecretKey) {
    // Create a deterministic RNG with zeroed seed
    let seed = [0_u8; 32];
    let mut rng = ChaCha20Rng::from_seed(seed);

    // Generate Falcon-512 secret key
    let sec_key = SecretKey::with_rng(&mut rng);

    // Convert public key to `Word` (4xFelt)
    let pub_key: Word = sec_key.public_key().into();

    // Wrap secret key in `AuthSecretKey`
    let auth_secret_key = AuthSecretKey::RpoFalcon512(sec_key);

    (pub_key, auth_secret_key)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    // Initialize client
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    // Fetch latest block from node
    let sync_summary = client.sync_state().await.unwrap();
    println!("Latest block: {}", sync_summary.block_num);

    // -------------------------------------------------------------------------
    // STEP 1: Create a basic counter contract
    // -------------------------------------------------------------------------
    println!("\n[STEP 1] Creating counter contract.");

    // Load the MASM file for the counter contract
    let file_path = Path::new("./masm/accounts/counter.masm");
    let account_code = fs::read_to_string(file_path).unwrap();

    // Prepare assembler (debug mode = true)
    let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);

    // Compile the account code into `AccountComponent` with one storage slot
    let counter_component = AccountComponent::compile(
        account_code,
        assembler,
        vec![StorageSlot::Value([
            Felt::new(0),
            Felt::new(0),
            Felt::new(0),
            Felt::new(0),
        ])],
    )
    .unwrap()
    .with_supports_all_types();

    // Init seed for the counter contract
    let init_seed = ChaCha20Rng::from_entropy().gen();

    // Anchor block of the account
    let anchor_block = client.get_latest_epoch_block().await.unwrap();

    // Build the new `Account` with the component
    let (counter_contract, counter_seed) = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::RegularAccountImmutableCode)
        .storage_mode(AccountStorageMode::Public)
        .with_component(counter_component.clone())
        .build()
        .unwrap();

    println!(
        "counter_contract hash: {:?}",
        counter_contract.hash().to_hex()
    );
    println!("contract id: {:?}", counter_contract.id().to_hex());
    println!("account_storage: {:?}", counter_contract.storage());

    // Since anyone should be able to write to the counter contract, auth_secret_key is not required.
    // However, to import to the client, we must generate a random value.
    let (_counter_pub_key, auth_secret_key) = get_new_pk_and_authenticator();

    client
        .add_account(
            &counter_contract.clone(),
            Some(counter_seed),
            &auth_secret_key,
            false,
        )
        .await
        .unwrap();

    // Print the procedure root hash
    let get_increment_export = counter_component
        .library()
        .exports()
        .find(|export| export.name.as_str() == "increment_count")
        .unwrap();

    let get_increment_count_mast_id = counter_component
        .library()
        .get_export_node_id(get_increment_export);

    let increment_count_hash = counter_component
        .library()
        .mast_forest()
        .get_node_by_id(get_increment_count_mast_id)
        .unwrap()
        .digest()
        .to_hex();

    println!("increment_count procedure hash: {:?}", increment_count_hash);

    // -------------------------------------------------------------------------
    // STEP 2: Call the Counter Contract with a script
    // -------------------------------------------------------------------------
    println!("\n[STEP 2] Call Counter Contract With Script");

    // Load the MASM script referencing the increment procedure
    let file_path = Path::new("./masm/scripts/counter_script.masm");
    let original_code = fs::read_to_string(file_path).unwrap();

    // Replace the placeholder with the actual procedure call
    let replaced_code = original_code.replace("{increment_count}", &increment_count_hash);
    println!("Final script:\n{}", replaced_code);

    // Compile the script referencing our procedure
    let tx_script = client.compile_tx_script(vec![], &replaced_code).unwrap();

    // Build a transaction request with the custom script
    let tx_increment_request = TransactionRequestBuilder::new()
        .with_custom_script(tx_script)
        .unwrap()
        .build();

    // Execute the transaction locally
    let tx_result = client
        .new_transaction(counter_contract.id(), tx_increment_request)
        .await
        .unwrap();

    let tx_id = tx_result.executed_transaction().id();
    println!(
        "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
        tx_id
    );

    // Submit transaction to the network
    let _ = client.submit_transaction(tx_result).await;

    // Wait, then re-sync
    tokio::time::sleep(Duration::from_secs(3)).await;
    client.sync_state().await.unwrap();

    // Retrieve updated contract data to see the incremented counter
    let account = client.get_account(counter_contract.id()).await.unwrap();
    println!(
        "counter contract storage: {:?}",
        account.unwrap().account().storage().get_item(0)
    );

    Ok(())
}

The output of our program will look something like this:

Client initialized successfully.
Latest block: 118178

[STEP 1] Creating counter contract.
counter_contract hash: "0xa1802c8cfba2bd9c1c0f0b10b875795445566bd61864a05103bdaff167775293"
contract id: "0x4eedb9db1bdcf90000036bcebfe53a"
account_storage: AccountStorage { slots: [Value([0, 0, 0, 0])] }
increment_count procedure hash: "0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54"

[STEP 2] Call Counter Contract With Script
Final script:
begin
    # => []
    call.0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54
end
Stack state before step 2384:
├──  0: 1
├──  1: 0
├──  2: 0
├──  3: 0
├──  4: 0
├──  5: 0
├──  6: 0
├──  7: 0
├──  8: 0
├──  9: 0
├── 10: 0
├── 11: 0
├── 12: 0
├── 13: 0
├── 14: 0
├── 15: 0
├── 16: 0
├── 17: 0
├── 18: 0
└── 19: 0

View transaction on MidenScan: https://testnet.midenscan.com/tx/0x4384619bba7e6c959a31769a52ce8c6c081ffab00be33e85f58f62cccfd32c21
counter contract storage: Ok(RpoDigest([0, 0, 0, 1]))

The line in the output Stack state before step 2384 ouputs the stack state when we call "debug.stack" in the counter.masm file.

To increment the count of the counter contract all you need is to know the account id of the counter and the procedure hash of the increment_count procedure. To increment the count without deploying the counter each time, you can modify the program above to hardcode the account id of the counter and the procedure hash of the increment_count prodedure in the masm script.

Running the example

To run the full example, navigate to the rust-client directory in the miden-tutorials repository and run this command:

cd rust-client
cargo run --release --bin counter_contract_deploy

Continue learning

Next tutorial: Interacting with Public Smart Contracts

Interacting with Public Smart Contracts

Using the Miden client in Rust to interact with public smart contracts on Miden

Overview

In the previous tutorial, we built a simple counter contract and deployed it to the Miden testnet. However, we only covered how the contract’s deployer could interact with it. Now, let’s explore how anyone can interact with a public smart contract on Miden.

We’ll retrieve the counter contract’s state from the chain and rebuild it locally so a local transaction can be executed against it. In the near future, Miden will support network transactions, making the process of submitting transactions to public smart contracts much more like traditional blockchains.

Just like in the previous tutorial, we will use a script to invoke the increment function within the counter contract to update the count. However, this tutorial demonstrates how to call a procedure in a smart contract that was deployed by a different user on Miden.

What we'll cover

  • Reading state from a public smart contract
  • Interacting with public smart contracts on Miden

Prerequisites

This tutorial assumes you have a basic understanding of Miden assembly and completed the previous tutorial on deploying the counter contract. Although not a requirement, it is recommended to complete the counter contract deployment tutorial before starting this tutorial.

Step 1: Initialize your repository

Create a new Rust repository for your Miden project and navigate to it with the following command:

cargo new miden-counter-contract
cd miden-counter-contract

Add the following dependencies to your Cargo.toml file:

[dependencies]
miden-client = { version = "0.7", features = ["testing", "concurrent", "tonic", "sqlite"] }
miden-lib = { version = "0.7", default-features = false }
miden-objects = { version = "0.7.2", default-features = false }
miden-crypto = { version = "0.13.2", features = ["executable"] }
rand = { version = "0.8" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
tokio = { version = "1.40", features = ["rt-multi-thread", "net", "macros"] }
rand_chacha = "0.3.1"

Step 2: Build the counter contract

For better code organization, we will separate the Miden assembly code from our Rust code.

Create a directory named masm at the root of your miden-counter-contract directory. This will contain our contract and script masm code.

Initialize the masm directory:

mkdir -p masm/accounts masm/scripts

This will create:

masm/
├── accounts/
└── scripts/

Inside of the masm/accounts/ directory, create the counter.masm file:

use.miden::account
use.std::sys

export.get_count
    # => []
    push.0
    
    # => [index]
    exec.account::get_item

    # => [count]
    exec.sys::truncate_stack
end

export.increment_count
    # => []
    push.0

    # => [index]
    exec.account::get_item

    # => [count]
    push.1 add

    # debug statement with client
    debug.stack

    # => [count+1]
    push.0

    # [index, count+1]
    exec.account::set_item

    # => []
    push.1 exec.account::incr_nonce

    # => []
    exec.sys::truncate_stack
end

Inside of the masm/scripts/ directory, create the counter_script.masm file:

begin
    # => []
    call.{increment_count}
end

Note: We explained in the previous counter contract tutorial what exactly happens at each step in the increment_count procedure.

Step 3: Set up your src/main.rs file

Copy and paste the following code into your src/main.rs file:

use std::{fs, path::Path, sync::Arc};

use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use tokio::time::Duration;

use miden_client::{
    account::{Account, AccountCode, AccountId, AccountType},
    asset::AssetVault,
    crypto::RpoRandomCoin,
    rpc::{domain::account::AccountDetails, Endpoint, TonicRpcClient},
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{TransactionKernel, TransactionRequestBuilder},
    Client, ClientError, Felt,
};

use miden_objects::{
    account::{AccountComponent, AccountStorage, AuthSecretKey, StorageSlot},
    assembly::Assembler,
    crypto::dsa::rpo_falcon512::SecretKey,
    Word,
};

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new(
        "https".to_string(),
        "rpc.testnet.miden.io".to_string(),
        Some(443),
    );
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate client (toggle debug mode as needed)
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

pub fn get_new_pk_and_authenticator() -> (Word, AuthSecretKey) {
    // Create a deterministic RNG with zeroed seed
    let seed = [0_u8; 32];
    let mut rng = ChaCha20Rng::from_seed(seed);

    // Generate Falcon-512 secret key
    let sec_key = SecretKey::with_rng(&mut rng);

    // Convert public key to `Word` (4xFelt)
    let pub_key: Word = sec_key.public_key().into();

    // Wrap secret key in `AuthSecretKey`
    let auth_secret_key = AuthSecretKey::RpoFalcon512(sec_key);

    (pub_key, auth_secret_key)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    // Initialize client
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    // Fetch latest block from node
    let sync_summary = client.sync_state().await.unwrap();
    println!("Latest block: {}", sync_summary.block_num);

    Ok(())
}

Step 4: Reading public state from a smart contract

To read the public storage state of a smart contract on Miden we either instantiate the TonicRpcClient by itself, or use the test_rpc_api() method on the Client instance. In this example, we will be using the test_rpc_api() method.

We will be reading the public storage state of the counter contract deployed on the testnet at address 0x303dd027d27adc0000012b07dbf1b4.

Add the following code snippet to the end of your src/main.rs function:

#![allow(unused)]
fn main() {
// -------------------------------------------------------------------------
// STEP 1: Read the Public State of the Counter Contract
// -------------------------------------------------------------------------
println!("\n[STEP 1] Reading data from public state");

// Define the AccountId of the account to read from
let counter_contract_id = AccountId::from_hex("0x4eedb9db1bdcf90000036bcebfe53a").unwrap();

let account_details = client
    .test_rpc_api()
    .get_account_update(counter_contract_id)
    .await
    .unwrap();

let AccountDetails::Public(counter_contract_details, _) = account_details else {
    panic!("counter contract must be public");
};

// Getting the value of the count from slot 0 and the nonce of the counter contract
let count_value = counter_contract_details.storage().slots().get(0).unwrap();
let counter_nonce = counter_contract_details.nonce();

println!("count val: {:?}", count_value.value());
println!("counter nonce: {:?}", counter_nonce);
}

Run the following command to execute src/main.rs:

cargo run --release 

After the program executes, you should see the counter contract count value and nonce printed to the terminal, for example:

count val: [0, 0, 0, 5]
counter nonce: 5

Step 5: Building an account from parts

Now that we know the storage state of the counter contract and its nonce, we can build the account from its parts. We know the account ID, asset vault value, the storage layout, account code, and nonce. We need the full account data to interact with it locally. From these values, we can build the counter contract from scratch.

Add the following code snippet to the end of your src/main.rs function:

#![allow(unused)]
fn main() {
// -------------------------------------------------------------------------
// STEP 2: Build the Counter Contract
// -------------------------------------------------------------------------
println!("\n[STEP 2] Building the counter contract");

// Load the MASM file for the counter contract
let file_path = Path::new("./masm/accounts/counter.masm");
let account_code = fs::read_to_string(file_path).unwrap();

// Prepare assembler (debug mode = true)
let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);

// Compile the account code into `AccountComponent` with the count value returned by the node 
let account_component = AccountComponent::compile(
    account_code,
    assembler,
    vec![StorageSlot::Value(count_value.value())],
)
.unwrap()
.with_supports_all_types();

// Initialize the AccountStorage with the count value returned by the node  
let account_storage =
    AccountStorage::new(vec![StorageSlot::Value(count_value.value())]).unwrap();

// Build AccountCode from components
let account_code = AccountCode::from_components(
    &[account_component],
    AccountType::RegularAccountImmutableCode,
)
.unwrap();

// The counter contract doesn't have any assets so we pass an empty vector
let vault = AssetVault::new(&[]).unwrap();

// Build the counter contract from parts
let counter_contract = Account::from_parts(
    counter_contract_id,
    vault,
    account_storage,
    account_code,
    counter_nonce,
);

// Since anyone should be able to write to the counter contract, auth_secret_key is not required.
// However, to import to the client, we must generate a random value.
let (_, _auth_secret_key) = get_new_pk_and_authenticator();

client
    .add_account(&counter_contract.clone(), None, &_auth_secret_key, true)
    .await
    .unwrap();
}

Step 6: Incrementing the count

This step is exactly the same as in the counter contract deploy tutorial, the only change being that we hardcode the increment_count procedure hash since this value will not change.

Add the following code snippet to the end of your src/main.rs function:

#![allow(unused)]
fn main() {
// -------------------------------------------------------------------------
// STEP 3: Call the Counter Contract with a script
// -------------------------------------------------------------------------
println!("\n[STEP 3] Call the increment_count procedure in the counter contract");

// The increment_count procedure hash is constant
let increment_procedure = "0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54";

// Load the MASM script referencing the increment procedure
let file_path = Path::new("./masm/scripts/counter_script.masm");
let original_code = fs::read_to_string(file_path).unwrap();

// Replace the placeholder with the actual procedure call
let replaced_code = original_code.replace("{increment_count}", &increment_procedure);
println!("Final script:\n{}", replaced_code);

// Compile the script
let tx_script = client.compile_tx_script(vec![], &replaced_code).unwrap();

// Build a transaction request with the custom script
let tx_increment_request = TransactionRequestBuilder::new()
    .with_custom_script(tx_script)
    .unwrap()
    .build();

// Execute the transaction locally
let tx_result = client
    .new_transaction(counter_contract.id(), tx_increment_request)
    .await
    .unwrap();

let tx_id = tx_result.executed_transaction().id();
println!(
    "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
    tx_id
);

// Submit transaction to the network
let _ = client.submit_transaction(tx_result).await;

// Wait, then re-sync
tokio::time::sleep(Duration::from_secs(3)).await;
client.sync_state().await.unwrap();

// Retrieve updated contract data to see the incremented counter
let account = client.get_account(counter_contract.id()).await.unwrap();
println!(
    "counter contract storage: {:?}",
    account.unwrap().account().storage().get_item(0)
);
}

Summary

The final src/main.rs file should look like this:

use std::{fs, path::Path, sync::Arc};

use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use tokio::time::Duration;

use miden_client::{
    account::{Account, AccountCode, AccountId, AccountType},
    asset::AssetVault,
    crypto::RpoRandomCoin,
    rpc::{domain::account::AccountDetails, Endpoint, TonicRpcClient},
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{TransactionKernel, TransactionRequestBuilder},
    Client, ClientError, Felt,
};

use miden_objects::{
    account::{AccountComponent, AccountStorage, AuthSecretKey, StorageSlot},
    assembly::Assembler,
    crypto::dsa::rpo_falcon512::SecretKey,
    Word,
};

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new(
        "https".to_string(),
        "rpc.testnet.miden.io".to_string(),
        Some(443),
    );
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate client (toggle debug mode as needed)
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

pub fn get_new_pk_and_authenticator() -> (Word, AuthSecretKey) {
    // Create a deterministic RNG with zeroed seed
    let seed = [0_u8; 32];
    let mut rng = ChaCha20Rng::from_seed(seed);

    // Generate Falcon-512 secret key
    let sec_key = SecretKey::with_rng(&mut rng);

    // Convert public key to `Word` (4xFelt)
    let pub_key: Word = sec_key.public_key().into();

    // Wrap secret key in `AuthSecretKey`
    let auth_secret_key = AuthSecretKey::RpoFalcon512(sec_key);

    (pub_key, auth_secret_key)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    // Initialize client
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    // Fetch latest block from node
    let sync_summary = client.sync_state().await.unwrap();
    println!("Latest block: {}", sync_summary.block_num);

    // -------------------------------------------------------------------------
    // STEP 1: Read the Public State of the Counter Contract
    // -------------------------------------------------------------------------
    println!("\n[STEP 1] Reading data from public state");

    // Define the Counter Contract account id from counter contract deploy
    let counter_contract_id = AccountId::from_hex("0x4eedb9db1bdcf90000036bcebfe53a").unwrap();

    let account_details = client
        .test_rpc_api()
        .get_account_update(counter_contract_id)
        .await
        .unwrap();

    let AccountDetails::Public(counter_contract_details, _) = account_details else {
        panic!("counter contract must be public");
    };

    // Getting the value of the count from slot 0 and the nonce of the counter contract
    let count_value = counter_contract_details.storage().slots().get(0).unwrap();
    let counter_nonce = counter_contract_details.nonce();

    println!("count val: {:?}", count_value.value());
    println!("counter nonce: {:?}", counter_nonce);

    // -------------------------------------------------------------------------
    // STEP 2: Build the Counter Contract
    // -------------------------------------------------------------------------
    println!("\n[STEP 2] Building the counter contract");

    // Load the MASM file for the counter contract
    let file_path = Path::new("./masm/accounts/counter.masm");
    let account_code = fs::read_to_string(file_path).unwrap();

    // Prepare assembler (debug mode = true)
    let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);

    // Compile the account code into `AccountComponent` with the count value returned by the node
    let account_component = AccountComponent::compile(
        account_code,
        assembler,
        vec![StorageSlot::Value(count_value.value())],
    )
    .unwrap()
    .with_supports_all_types();

    // Initialize the AccountStorage with the count value returned by the node
    let account_storage =
        AccountStorage::new(vec![StorageSlot::Value(count_value.value())]).unwrap();

    // Build AccountCode from components
    let account_code = AccountCode::from_components(
        &[account_component],
        AccountType::RegularAccountImmutableCode,
    )
    .unwrap();

    // The counter contract doesn't have any assets so we pass an empty vector
    let vault = AssetVault::new(&[]).unwrap();

    // Build the counter contract from parts
    let counter_contract = Account::from_parts(
        counter_contract_id,
        vault,
        account_storage,
        account_code,
        counter_nonce,
    );

    // Since anyone should be able to write to the counter contract, auth_secret_key is not required.
    // However, to import to the client, we must generate a random value.
    let (_, _auth_secret_key) = get_new_pk_and_authenticator();

    client
        .add_account(&counter_contract.clone(), None, &_auth_secret_key, true)
        .await
        .unwrap();

    // -------------------------------------------------------------------------
    // STEP 3: Call the Counter Contract with a script
    // -------------------------------------------------------------------------
    println!("\n[STEP 3] Call the increment_count procedure in the counter contract");

    // The increment_count procedure hash is constant
    let increment_procedure = "0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54";

    // Load the MASM script referencing the increment procedure
    let file_path = Path::new("./masm/scripts/counter_script.masm");
    let original_code = fs::read_to_string(file_path).unwrap();

    // Replace the placeholder with the actual procedure call
    let replaced_code = original_code.replace("{increment_count}", &increment_procedure);
    println!("Final script:\n{}", replaced_code);

    // Compile the script
    let tx_script = client.compile_tx_script(vec![], &replaced_code).unwrap();

    // Build a transaction request with the custom script
    let tx_increment_request = TransactionRequestBuilder::new()
        .with_custom_script(tx_script)
        .unwrap()
        .build();

    // Execute the transaction locally
    let tx_result = client
        .new_transaction(counter_contract.id(), tx_increment_request)
        .await
        .unwrap();

    let tx_id = tx_result.executed_transaction().id();
    println!(
        "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
        tx_id
    );

    // Submit transaction to the network
    let _ = client.submit_transaction(tx_result).await;

    // Wait, then re-sync
    tokio::time::sleep(Duration::from_secs(3)).await;
    client.sync_state().await.unwrap();

    // Retrieve updated contract data to see the incremented counter
    let account = client.get_account(counter_contract.id()).await.unwrap();
    println!(
        "counter contract storage: {:?}",
        account.unwrap().account().storage().get_item(0)
    );

    Ok(())
}

Run the following command to execute src/main.rs:

cargo run --release 

The output of our program will look something like this depending on the current count value in the smart contract:

Client initialized successfully.
Latest block: 242342

[STEP 1] Building counter contract from public state
count val: [0, 0, 0, 1]
counter nonce: 1

[STEP 2] Call the increment_count procedure in the counter contract
Procedure 1: "0x92495ca54d519eb5e4ba22350f837904d3895e48d74d8079450f19574bb84cb6"
Procedure 2: "0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54"
number of procedures: 2
Final script:
begin
    # => []
    call.0xecd7eb223a5524af0cc78580d96357b298bb0b3d33fe95aeb175d6dab9de2e54
end
Stack state before step 1812:
├──  0: 2
├──  1: 0
├──  2: 0
├──  3: 0
├──  4: 0
├──  5: 0
├──  6: 0
├──  7: 0
├──  8: 0
├──  9: 0
├── 10: 0
├── 11: 0
├── 12: 0
├── 13: 0
├── 14: 0
├── 15: 0
├── 16: 0
├── 17: 0
├── 18: 0
└── 19: 0

View transaction on MidenScan: https://testnet.midenscan.com/tx/0x8183aed150f20b9c26d4cb7840bfc92571ea45ece31116170b11cdff2649eb5c
counter contract storage: Ok(RpoDigest([0, 0, 0, 2]))

Running the example

To run the full example, navigate to the rust-client directory in the miden-tutorials repository and run this command:

cd rust-client
cargo run --release --bin counter_contract_increment

Continue learning

Next tutorial: Foreign Procedure Invocation

Foreign Procedure Invocation Tutorial

Using foreign procedure invocation to craft read-only cross-contract calls in the Miden VM

Overview

In previous tutorials we deployed a public counter contract and incremented the count from a different client instance.

In this tutorial we will cover the basics of "foreign procedure invocation" (FPI) in the Miden VM, by building a "Count Copy" smart contract that reads the count from our previously deployed counter contract and copies the count to its own local storage.

Foreign procedure invocation (FPI) is a powerful tool for building smart contracts in the Miden VM. FPI allows one smart contract to call "read-only" procedures in other smart contracts.

The term "foreign procedure invocation" might sound a bit verbose, but it is as simple as one smart contract calling a non-state modifying procedure in another smart contract. The "EVM equivalent" of foreign procedure invocation would be a smart contract calling a read-only function in another contract.

FPI is useful for developing smart contracts that extend the functionality of existing contracts on Miden. FPI is the core primitive used by price oracles on Miden.

What we'll cover

  • Foreign Procedure Invocation (FPI)
  • Building a "Count Copy" Smart Contract

Prerequisites

This tutorial assumes you have a basic understanding of Miden assembly and completed the previous tutorial on deploying the counter contract. We will be working within the same miden-counter-contract repository that we created in the Interacting with Public Smart Contracts tutorial.

Step 1: Set up your repository

We will be using the same repository used in the "Interacting with Public Smart Contracts" tutorial. To set up your repository for this tutorial, first follow up until step two here.

Step 2: Set up the "count reader" contract

Inside of the masm/accounts/ directory, create the count_reader.masm file. This is the smart contract that will read the "count" value from the counter contract.

masm/accounts/count_reader.masm:

use.miden::account
use.miden::tx
use.std::sys

# Reads the count from the counter contract 
# and then copies the value to storage
export.copy_count
  # => []
  push.{get_count_proc_hash}

  # => [GET_COUNT_HASH]
  push.{account_id_suffix}

  # => [account_id_suffix]
  push.{account_id_prefix}

  # => [account_id_prefix, account_id_suffix, GET_COUNT_HASH]
  exec.tx::execute_foreign_procedure

  # => [count]
  debug.stack

  # => [count]
  push.0 

  # [index, count]
  exec.account::set_item

  # => []
  push.1 exec.account::incr_nonce

  # => []
  exec.sys::truncate_stack
end

In the count reader smart contract we have a copy_count procedure that uses tx::execute_foreign_procedure to call the get_count procedure in the counter contract.

To call the get_count procedure, we push its hash along with the counter contract's ID suffix and prefix.

This is what the stack state should look like before we call tx::execute_foreign_procedure:

# => [account_id_prefix, account_id_suffix, GET_COUNT_HASH]

After calling the get_count procedure in the counter contract, we call debug.stack and then save the count of the counter contract to index 0 in storage.

Note: The bracket symbols used in the count copy contract are not valid MASM syntax. These are simply placeholder elements that we will replace with the actual values before compilation.

Inside the masm/scripts/ directory, create the reader_script.masm file:

begin
    # => []
    call.{copy_count}
end

Step 3: Set up your src/main.rs file:

use std::{fs, path::Path, sync::Arc};

use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use tokio::time::Duration;

use miden_client::{
    account::{Account, AccountCode, AccountId, AccountStorageMode, AccountType},
    asset::AssetVault,
    crypto::RpoRandomCoin,
    rpc::{
        domain::account::{AccountDetails, AccountStorageRequirements},
        Endpoint, TonicRpcClient,
    },
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{ForeignAccount, TransactionKernel, TransactionRequestBuilder},
    Client, ClientError, Felt,
};

use miden_objects::{
    account::{AccountBuilder, AccountComponent, AccountStorage, AuthSecretKey, StorageSlot},
    assembly::Assembler,
    crypto::dsa::rpo_falcon512::SecretKey,
    Word,
};

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new(
        "https".to_string(),
        "rpc.testnet.miden.io".to_string(),
        Some(443),
    );
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate client (toggle debug mode as needed)
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

pub fn get_new_pk_and_authenticator() -> (Word, AuthSecretKey) {
    // Create a deterministic RNG with zeroed seed
    let seed = [0_u8; 32];
    let mut rng = ChaCha20Rng::from_seed(seed);

    // Generate Falcon-512 secret key
    let sec_key = SecretKey::with_rng(&mut rng);

    // Convert public key to `Word` (4xFelt)
    let pub_key: Word = sec_key.public_key().into();

    // Wrap secret key in `AuthSecretKey`
    let auth_secret_key = AuthSecretKey::RpoFalcon512(sec_key);

    (pub_key, auth_secret_key)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    // Initialize client
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    // Fetch latest block from node
    let sync_summary = client.sync_state().await.unwrap();
    println!("Latest block: {}", sync_summary.block_num);

    // -------------------------------------------------------------------------
    // STEP 1: Create the Count Reader Contract
    // -------------------------------------------------------------------------
    println!("\n[STEP 1] Creating count reader contract.");

    // Load the MASM file for the counter contract
    let file_path = Path::new("./masm/accounts/count_reader.masm");
    let raw_account_code = fs::read_to_string(file_path).unwrap();

    // Define the counter contract account id and `get_count` procedure hash
    let counter_contract_id = AccountId::from_hex("0x4eedb9db1bdcf90000036bcebfe53a").unwrap();
    let get_count_hash = "0x92495ca54d519eb5e4ba22350f837904d3895e48d74d8079450f19574bb84cb6";

    let count_reader_code = raw_account_code
        .replace("{get_count_proc_hash}", &get_count_hash)
        .replace(
            "{account_id_prefix}",
            &counter_contract_id.prefix().to_string(),
        )
        .replace(
            "{account_id_suffix}",
            &counter_contract_id.suffix().to_string(),
        );

    // Initialize assembler (debug mode = true)
    let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);

    // Compile the account code into `AccountComponent` with one storage slot
    let count_reader_component = AccountComponent::compile(
        count_reader_code,
        assembler,
        vec![StorageSlot::Value(Word::default())],
    )
    .unwrap()
    .with_supports_all_types();

    // Init seed for the count reader contract
    let init_seed = ChaCha20Rng::from_entropy().gen();

    // Using latest block as the anchor block
    let anchor_block = client.get_latest_epoch_block().await.unwrap();

    // Build the count reader contract with the component
    let (count_reader_contract, counter_seed) = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::RegularAccountImmutableCode)
        .storage_mode(AccountStorageMode::Public)
        .with_component(count_reader_component.clone())
        .build()
        .unwrap();

    println!(
        "count reader contract id: {:?}",
        count_reader_contract.id().to_hex()
    );
    println!(
        "count reader  storage: {:?}",
        count_reader_contract.storage()
    );

    // Since anyone should be able to write to the counter contract, auth_secret_key is not required.
    // However, to import to the client, we must generate a random value.
    let (_counter_pub_key, auth_secret_key) = get_new_pk_and_authenticator();

    client
        .add_account(
            &count_reader_contract.clone(),
            Some(counter_seed),
            &auth_secret_key,
            false,
        )
        .await
        .unwrap();

    // Getting the hash of the `copy_count` procedure
    let get_proc_export = count_reader_component
        .library()
        .exports()
        .find(|export| export.name.as_str() == "copy_count")
        .unwrap();

    let get_proc_mast_id = count_reader_component
        .library()
        .get_export_node_id(get_proc_export);

    let copy_count_proc_hash = count_reader_component
        .library()
        .mast_forest()
        .get_node_by_id(get_proc_mast_id)
        .unwrap()
        .digest()
        .to_hex();

    println!("copy_count procedure hash: {:?}", copy_count_proc_hash);
    Ok(())
}

Run the following command to execute src/main.rs:

cargo run --release 

The output of our program will look something like this:

Client initialized successfully.
Latest block: 243826

[STEP 1] Creating count reader contract.
count reader contract id: "0xa47d7e5d8b1b90000003cd45a45a78"
count reader  storage: AccountStorage { slots: [Value([0, 0, 0, 0])] }
copy_count procedure hash: "0xa2ab9f6a150e9c598699741187589d0c61de12c35c1bbe591d658950f44ab743"

Step 4: Build and read the state of the counter contract deployed on testnet

Add this snippet to the end of your file in the main() function that we created in the previous step:

#![allow(unused)]
fn main() {
// -------------------------------------------------------------------------
// STEP 2: Build & Get State of the Counter Contract
// -------------------------------------------------------------------------
println!("\n[STEP 2] Building counter contract from public state");

// Define the Counter Contract account id from counter contract deploy
let account_details = client
    .test_rpc_api()
    .get_account_update(counter_contract_id)
    .await
    .unwrap();

let AccountDetails::Public(counter_contract_details, _) = account_details else {
    panic!("counter contract must be public");
};

// Getting the value of the count from slot 0 and the nonce of the counter contract
let count_value = counter_contract_details.storage().slots().get(0).unwrap();
let counter_nonce = counter_contract_details.nonce();

println!("count val: {:?}", count_value.value());
println!("counter nonce: {:?}", counter_nonce);

// Load the MASM file for the counter contract
let file_path = Path::new("./masm/accounts/counter.masm");
let account_code = fs::read_to_string(file_path).unwrap();

// Prepare assembler (debug mode = true)
let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);

// Compile the account code into `AccountComponent` with the count value returned by the node
let counter_component = AccountComponent::compile(
    account_code,
    assembler,
    vec![StorageSlot::Value(count_value.value())],
)
.unwrap()
.with_supports_all_types();

// Initialize the AccountStorage with the count value returned by the node
let counter_storage =
    AccountStorage::new(vec![StorageSlot::Value(count_value.value())]).unwrap();

// Build AccountCode from components
let counter_code = AccountCode::from_components(
    &[counter_component.clone()],
    AccountType::RegularAccountImmutableCode,
)
.unwrap();

// The counter contract doesn't have any assets so we pass an empty vector
let vault = AssetVault::new(&[]).unwrap();

// Build the counter contract from parts
let counter_contract = Account::from_parts(
    counter_contract_id,
    vault,
    counter_storage,
    counter_code,
    counter_nonce,
);

// Since anyone should be able to write to the counter contract, auth_secret_key is not required.
// However, to import to the client, we must generate a random value.
let (_, auth_secret_key) = get_new_pk_and_authenticator();

client
    .add_account(&counter_contract.clone(), None, &auth_secret_key, true)
    .await
    .unwrap();
}

This step uses the logic we explained in the Public Account Interaction Tutorial to read the state of the Counter contract and import it to the client locally.

Step 5: Call the counter contract via foreign procedure invocation

Add this snippet to the end of your file in the main() function:

#![allow(unused)]
fn main() {
// -------------------------------------------------------------------------
// STEP 3: Call the Counter Contract via Foreign Procedure Invocation (FPI)
// -------------------------------------------------------------------------
println!("\n[STEP 3] Call Counter Contract with FPI from Count Copy Contract");

// Load the MASM script referencing the increment procedure
let file_path = Path::new("./masm/scripts/reader_script.masm");
let original_code = fs::read_to_string(file_path).unwrap();

// Replace {get_count} and {account_id}
let replaced_code = original_code.replace("{copy_count}", &copy_count_proc_hash);

// Compile the script referencing our procedure
let tx_script = client.compile_tx_script(vec![], &replaced_code).unwrap();

let foreign_account =
    ForeignAccount::public(counter_contract_id, AccountStorageRequirements::default()).unwrap();

// Build a transaction request with the custom script
let tx_request = TransactionRequestBuilder::new()
    .with_foreign_accounts([foreign_account])
    .with_custom_script(tx_script)
    .unwrap()
    .build();

// Execute the transaction locally
let tx_result = client
    .new_transaction(count_reader_contract.id(), tx_request)
    .await
    .unwrap();

let tx_id = tx_result.executed_transaction().id();
println!(
    "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
    tx_id
);

// Submit transaction to the network
let _ = client.submit_transaction(tx_result).await;

// Wait, then re-sync
tokio::time::sleep(Duration::from_secs(3)).await;
client.sync_state().await.unwrap();

// Retrieve updated contract data to see the incremented counter
let account_1 = client.get_account(counter_contract.id()).await.unwrap();
println!(
    "counter contract storage: {:?}",
    account_1.unwrap().account().storage().get_item(0)
);

let account_2 = client
    .get_account(count_reader_contract.id())
    .await
    .unwrap();
println!(
    "count reader contract storage: {:?}",
    account_2.unwrap().account().storage().get_item(0)
);
}

The key here is the use of the .with_foreign_accounts() method on the TransactionRequestBuilder. Using this method, it is possible to create transactions with multiple foreign procedure calls.

Summary

In this tutorial created a smart contract that calls the get_count procedure in the counter contract using foreign procedure invocation, and then saves the returned value to its local storage.

The final src/main.rs file should look like this:

use std::{fs, path::Path, sync::Arc};

use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use tokio::time::Duration;

use miden_client::{
    account::{Account, AccountCode, AccountId, AccountStorageMode, AccountType},
    asset::AssetVault,
    crypto::RpoRandomCoin,
    rpc::{
        domain::account::{AccountDetails, AccountStorageRequirements},
        Endpoint, TonicRpcClient,
    },
    store::{sqlite_store::SqliteStore, StoreAuthenticator},
    transaction::{ForeignAccount, TransactionKernel, TransactionRequestBuilder},
    Client, ClientError, Felt,
};

use miden_objects::{
    account::{AccountBuilder, AccountComponent, AccountStorage, AuthSecretKey, StorageSlot},
    assembly::Assembler,
    crypto::dsa::rpo_falcon512::SecretKey,
    Word,
};

pub async fn initialize_client() -> Result<Client<RpoRandomCoin>, ClientError> {
    // RPC endpoint and timeout
    let endpoint = Endpoint::new(
        "https".to_string(),
        "rpc.testnet.miden.io".to_string(),
        Some(443),
    );
    let timeout_ms = 10_000;

    // Build RPC client
    let rpc_api = Box::new(TonicRpcClient::new(endpoint, timeout_ms));

    // Seed RNG
    let mut seed_rng = rand::thread_rng();
    let coin_seed: [u64; 4] = seed_rng.gen();

    // Create random coin instance
    let rng = RpoRandomCoin::new(coin_seed.map(Felt::new));

    // SQLite path
    let store_path = "store.sqlite3";

    // Initialize SQLite store
    let store = SqliteStore::new(store_path.into())
        .await
        .map_err(ClientError::StoreError)?;
    let arc_store = Arc::new(store);

    // Create authenticator referencing the store and RNG
    let authenticator = StoreAuthenticator::new_with_rng(arc_store.clone(), rng.clone());

    // Instantiate client (toggle debug mode as needed)
    let client = Client::new(rpc_api, rng, arc_store, Arc::new(authenticator), true);

    Ok(client)
}

pub fn get_new_pk_and_authenticator() -> (Word, AuthSecretKey) {
    // Create a deterministic RNG with zeroed seed
    let seed = [0_u8; 32];
    let mut rng = ChaCha20Rng::from_seed(seed);

    // Generate Falcon-512 secret key
    let sec_key = SecretKey::with_rng(&mut rng);

    // Convert public key to `Word` (4xFelt)
    let pub_key: Word = sec_key.public_key().into();

    // Wrap secret key in `AuthSecretKey`
    let auth_secret_key = AuthSecretKey::RpoFalcon512(sec_key);

    (pub_key, auth_secret_key)
}

#[tokio::main]
async fn main() -> Result<(), ClientError> {
    // Initialize client
    let mut client = initialize_client().await?;
    println!("Client initialized successfully.");

    // Fetch latest block from node
    let sync_summary = client.sync_state().await.unwrap();
    println!("Latest block: {}", sync_summary.block_num);

    // -------------------------------------------------------------------------
    // STEP 1: Create the Count Reader Contract
    // -------------------------------------------------------------------------
    println!("\n[STEP 1] Creating count reader contract.");

    // Load the MASM file for the counter contract
    let file_path = Path::new("./masm/accounts/count_reader.masm");
    let raw_account_code = fs::read_to_string(file_path).unwrap();

    // Define the counter contract account id and `get_count` procedure hash
    let counter_contract_id = AccountId::from_hex("0x4eedb9db1bdcf90000036bcebfe53a").unwrap();
    let get_count_hash = "0x92495ca54d519eb5e4ba22350f837904d3895e48d74d8079450f19574bb84cb6";

    let count_reader_code = raw_account_code
        .replace("{get_count_proc_hash}", &get_count_hash)
        .replace(
            "{account_id_prefix}",
            &counter_contract_id.prefix().to_string(),
        )
        .replace(
            "{account_id_suffix}",
            &counter_contract_id.suffix().to_string(),
        );

    // Initialize assembler (debug mode = true)
    let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);

    // Compile the account code into `AccountComponent` with one storage slot
    let count_reader_component = AccountComponent::compile(
        count_reader_code,
        assembler,
        vec![StorageSlot::Value(Word::default())],
    )
    .unwrap()
    .with_supports_all_types();

    // Init seed for the count reader contract
    let init_seed = ChaCha20Rng::from_entropy().gen();

    // Using latest block as the anchor block
    let anchor_block = client.get_latest_epoch_block().await.unwrap();

    // Build the count reader contract with the component
    let (count_reader_contract, counter_seed) = AccountBuilder::new(init_seed)
        .anchor((&anchor_block).try_into().unwrap())
        .account_type(AccountType::RegularAccountImmutableCode)
        .storage_mode(AccountStorageMode::Public)
        .with_component(count_reader_component.clone())
        .build()
        .unwrap();

    println!(
        "count reader contract id: {:?}",
        count_reader_contract.id().to_hex()
    );
    println!(
        "count reader  storage: {:?}",
        count_reader_contract.storage()
    );

    // Since anyone should be able to write to the counter contract, auth_secret_key is not required.
    // However, to import to the client, we must generate a random value.
    let (_counter_pub_key, auth_secret_key) = get_new_pk_and_authenticator();

    client
        .add_account(
            &count_reader_contract.clone(),
            Some(counter_seed),
            &auth_secret_key,
            false,
        )
        .await
        .unwrap();

    // Getting the hash of the `copy_count` procedure
    let get_proc_export = count_reader_component
        .library()
        .exports()
        .find(|export| export.name.as_str() == "copy_count")
        .unwrap();

    let get_proc_mast_id = count_reader_component
        .library()
        .get_export_node_id(get_proc_export);

    let copy_count_proc_hash = count_reader_component
        .library()
        .mast_forest()
        .get_node_by_id(get_proc_mast_id)
        .unwrap()
        .digest()
        .to_hex();

    println!("copy_count procedure hash: {:?}", copy_count_proc_hash);

    // -------------------------------------------------------------------------
    // STEP 2: Build & Get State of the Counter Contract
    // -------------------------------------------------------------------------
    println!("\n[STEP 2] Building counter contract from public state");

    // Define the Counter Contract account id from counter contract deploy
    let account_details = client
        .test_rpc_api()
        .get_account_update(counter_contract_id)
        .await
        .unwrap();

    let AccountDetails::Public(counter_contract_details, _) = account_details else {
        panic!("counter contract must be public");
    };

    // Getting the value of the count from slot 0 and the nonce of the counter contract
    let count_value = counter_contract_details.storage().slots().get(0).unwrap();
    let counter_nonce = counter_contract_details.nonce();

    println!("count val: {:?}", count_value.value());
    println!("counter nonce: {:?}", counter_nonce);

    // Load the MASM file for the counter contract
    let file_path = Path::new("./masm/accounts/counter.masm");
    let account_code = fs::read_to_string(file_path).unwrap();

    // Prepare assembler (debug mode = true)
    let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true);

    // Compile the account code into `AccountComponent` with the count value returned by the node
    let counter_component = AccountComponent::compile(
        account_code,
        assembler,
        vec![StorageSlot::Value(count_value.value())],
    )
    .unwrap()
    .with_supports_all_types();

    // Initialize the AccountStorage with the count value returned by the node
    let counter_storage =
        AccountStorage::new(vec![StorageSlot::Value(count_value.value())]).unwrap();

    // Build AccountCode from components
    let counter_code = AccountCode::from_components(
        &[counter_component.clone()],
        AccountType::RegularAccountImmutableCode,
    )
    .unwrap();

    // The counter contract doesn't have any assets so we pass an empty vector
    let vault = AssetVault::new(&[]).unwrap();

    // Build the counter contract from parts
    let counter_contract = Account::from_parts(
        counter_contract_id,
        vault,
        counter_storage,
        counter_code,
        counter_nonce,
    );

    // Since anyone should be able to write to the counter contract, auth_secret_key is not required.
    // However, to import to the client, we must generate a random value.
    let (_, auth_secret_key) = get_new_pk_and_authenticator();

    client
        .add_account(&counter_contract.clone(), None, &auth_secret_key, true)
        .await
        .unwrap();

    // -------------------------------------------------------------------------
    // STEP 3: Call the Counter Contract via Foreign Procedure Invocation (FPI)
    // -------------------------------------------------------------------------
    println!("\n[STEP 3] Call Counter Contract with FPI from Count Copy Contract");

    // Load the MASM script referencing the increment procedure
    let file_path = Path::new("./masm/scripts/reader_script.masm");
    let original_code = fs::read_to_string(file_path).unwrap();

    // Replace {get_count} and {account_id}
    let replaced_code = original_code.replace("{copy_count}", &copy_count_proc_hash);

    // Compile the script referencing our procedure
    let tx_script = client.compile_tx_script(vec![], &replaced_code).unwrap();

    let foreign_account =
        ForeignAccount::public(counter_contract_id, AccountStorageRequirements::default()).unwrap();

    // Build a transaction request with the custom script
    let tx_request = TransactionRequestBuilder::new()
        .with_foreign_accounts([foreign_account])
        .with_custom_script(tx_script)
        .unwrap()
        .build();

    // Execute the transaction locally
    let tx_result = client
        .new_transaction(count_reader_contract.id(), tx_request)
        .await
        .unwrap();

    let tx_id = tx_result.executed_transaction().id();
    println!(
        "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}",
        tx_id
    );

    // Submit transaction to the network
    let _ = client.submit_transaction(tx_result).await;

    // Wait, then re-sync
    tokio::time::sleep(Duration::from_secs(3)).await;
    client.sync_state().await.unwrap();

    // Retrieve updated contract data to see the incremented counter
    let account_1 = client.get_account(counter_contract.id()).await.unwrap();
    println!(
        "counter contract storage: {:?}",
        account_1.unwrap().account().storage().get_item(0)
    );

    let account_2 = client
        .get_account(count_reader_contract.id())
        .await
        .unwrap();
    println!(
        "count reader contract storage: {:?}",
        account_2.unwrap().account().storage().get_item(0)
    );

    Ok(())
}

The output of our program will look something like this:

Client initialized successfully.
Latest block: 242367

[STEP 1] Creating count reader contract.
count reader contract id: "0x95b00b4f410f5000000383ca114c9a"
count reader  storage: AccountStorage { slots: [Value([0, 0, 0, 0])] }
copy_count procedure hash: "0xa2ab9f6a150e9c598699741187589d0c61de12c35c1bbe591d658950f44ab743"

[STEP 2] Building counter contract from public state
count val: [0, 0, 0, 2]
counter nonce: 2

[STEP 3] Call Counter Contract with FPI from Count Copy Contract
Stack state before step 3351:
├──  0: 2
├──  1: 0
├──  2: 0
├──  3: 0
├──  4: 0
├──  5: 0
├──  6: 0
├──  7: 0
├──  8: 0
├──  9: 0
├── 10: 0
├── 11: 0
├── 12: 0
├── 13: 0
├── 14: 0
└── 15: 0

View transaction on MidenScan: https://testnet.midenscan.com/tx/0xe2fdb53926e7a11548863c2a85d81127094d6fe38f60509b4ef8ea38994f8cec
counter contract storage: Ok(RpoDigest([0, 0, 0, 2]))
count reader contract storage: Ok(RpoDigest([0, 0, 0, 2]))

Running the example

To run the full example, navigate to the rust-client directory in the miden-tutorials repository and run this command:

cd rust-client
cargo run --release --bin counter_contract_fpi

WebClient

TypeScript library, which can be used to programmatically interact with the Miden rollup.

The Miden WebClient can be used for a variety of things, including:

  • Deploying and creating transactions to interact with accounts and notes on Miden.
  • Storing the state of accounts and notes in the browser.
  • Generating and submitting proofs of transactions.
  • Submitting transactions to delegated proving services.

This section of the docs is an overview of the different things one can achieve using the WebClient, and how to implement them.

Keep in mind that both the WebClient and the documentation are works-in-progress!

Creating Accounts and Deploying Faucets

Using the Miden WebClient in TypeScript to create accounts and deploy faucets

Overview

In this tutorial, we will create a basic web application that interacts with Miden using the Miden WebClient.

Our web application will create a Miden account for Alice and then deploy a fungible faucet. In the next section we will mint tokens from the faucet to fund her account, and then send the tokens from Alice's account to other Miden accounts.

What we'll cover

  • Understanding the difference between public and private accounts & notes
  • Instantiating the Miden client
  • Creating new accounts (public or private)
  • Deploying a faucet to fund an account

Prerequisites

To begin, make sure you have a miden-node running locally in a separate terminal window. To get the Miden node running locally, you can follow the instructions on the Miden Node Setup page.

Note: In this tutorial we use pnpm which is a drop in replacement for npm.

Public vs. private accounts & notes

Before we dive into the coding, let's clarify the concepts of public and private accounts and notes on Miden:

  • Public accounts: The account's data and code are stored on-chain and are openly visible, including its assets.
  • Private accounts: The account's state and logic are off-chain, only known to its owner.
  • Public notes: The note's state is visible to anyone - perfect for scenarios where transparency is desired.
  • Private notes: The note's state is stored off-chain, you will need to share the note data with the relevant parties (via email or Telegram) for them to be able to consume the note.

Note: The term "account" can be used interchangeably with the term "smart contract" since account abstraction on Miden is handled natively.

It is useful to think of notes on Miden as "cryptographic cashier's checks" that allow users to send tokens. If the note is private, the note transfer is only known to the sender and receiver.

Step 1: Initialize your repository

Create a new React TypeScript repository for your Miden web application, navigate to it, and install the Miden WebClient using this command:

pnpm create vite miden-app --template react-ts

Navigate to the new repository:

cd miden-app

Install dependencies:

pnpm install

Install the Miden WebClient SDK:

pnpm i @demox-labs/miden-sdk@0.6.1-next.4

Save this as your vite.config.ts file:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    target: 'esnext',
  },
  optimizeDeps: {
    exclude: ['@demox-labs/miden-sdk'], // Exclude the SDK from optimization
  },
});

Note: ensure you are using Node version v20.12.0

Step 2: Initialize the client

Before we can interact with the Miden network, we need to instantiate the WebClient. In this step, we specify two parameters:

  • RPC endpoint - The URL of the Miden node to which we connect.
  • Delegated Prover Endpoint (optional) – The URL of the delegated prover which the client can connect to.

Create a webClient.ts file:

To instantiate the WebClient, pass in the endpoint of the Miden node. You can also instantiate the client with a delegated prover to speed up the proof generation time, however, in this example we will be instantiating the WebClient only with the endpoint of the Miden node since we will be handling proof generation locally within the browser.

Since we will be handling proof generation in the computationally constrained environment of the browser, it will be slower than proof generation handled by the Rust client. Currently, the Miden WebClient is thread-blocking when not used within a web worker.

Example of instantiating the WebClient with a delegated prover:

const nodeEndpoint = "http://localhost:57291";
const delegatedProver = 'http://18.118.151.210:8082'

let client = new WebClient();
await client.create_client(nodeEndpoint, delegatedProver);

In the src/ directory create a file named webClient.ts and paste the following into it:

// src/webClient.ts
import { WebClient } from "@demox-labs/miden-sdk";

const nodeEndpoint = "http://localhost:57291";

export async function webClient(): Promise<void> {
  try {
    // 1. Create client
    const client = new WebClient();
    await client.create_client(nodeEndpoint);

    // 2. Sync and log block
    const state = await client.sync_state();
    console.log("Latest block number:", state.block_num());
  } catch (error) {
    console.error("Error", error);
    throw error;
  }
}

Edit your App.tsx file:

Set this as your App.tsx file.

// src/App.tsx
import { useState } from "react";
import "./App.css";
import { webClient } from "./webClient";

function App() {
  const [clientStarted, setClientStarted] = useState(false);

  const handleClick = () => {
    webClient();
    setClientStarted(true);
  };

  return (
    <div className="App">
      <h1>Miden Web App</h1>

      <p>Open the console to view logs</p>

      {!clientStarted && <button onClick={handleClick}>Start WebClient</button>}
    </div>
  );
}

export default App;

Starting the frontend:

pnpm run dev

Open the frontend at:

http://localhost:5173/

Now open the browser console. Click the "Start the WebClient" button. Then in the console, you should see something like:

Latest block number: 123

Step 3: Creating a wallet

Now that we've initialized the WebClient, we can create a wallet for Alice.

To create a wallet for Alice using the Miden WebClient, we specify the account type by specifying if the account code is mutable or immutable and whether the account is public or private. A mutable wallet means you can change the account code after deployment.

A wallet on Miden is simply an account with standardized code.

In the example below we create a mutable public account for Alice.

Our src/webClient.ts file should now look something like this:

// src/webClient.ts
import {
  WebClient,
  AccountStorageMode,
  AccountId,
  NoteType,
} from "@demox-labs/miden-sdk";

const nodeEndpoint = "http://localhost:57291";

export async function webClient(): Promise<void> {
  try {
    // 1. Create client
    const client = new WebClient();
    await client.create_client(nodeEndpoint);

    // 2. Sync and log block
    const state = await client.sync_state();
    console.log("Latest block number:", state.block_num());

    // 3. Create Alice account (public, updatable)
    console.log("Creating account for Alice");
    const aliceAccount = await client.new_wallet(
      AccountStorageMode.public(), // account type
      true,                        // mutability
    );
    const aliceIdHex = aliceAccount.id().to_string();
    console.log("Alice's account ID:", aliceIdHex);

    await client.sync_state();
  } catch (error) {
    console.error("Error:", error);
    throw error;
  }
}

Step 4: Deploying a fungible faucet

For Alice to receive testnet assets, we first need to deploy a faucet. A faucet account on Miden mints fungible tokens.

We'll create a public faucet with a token symbol, decimals, and a max supply. We will use this faucet to mint tokens to Alice's account in the next section.

Add this snippet to the end of the webClient() function:

// 4. Create faucet
console.log("Creating faucet...");
const faucetAccount = await client.new_faucet(
  AccountStorageMode.public(), // account type
  false,                       // is fungible
  "MID",                       // symbol
  8,                           // decimals
  BigInt(1_000_000)            // max supply
);
const faucetIdHex = faucetAccount.id().to_string();
console.log("Faucet account ID:", faucetIdHex);

await client.sync_state();

When tokens are minted from this faucet, each token batch is represented as a "note" (UTXO). You can think of a Miden Note as a cryptographic cashier's check that has certain spend conditions attached to it.

Summary

Our new src/webClient.ts file should look something like this:

// src/webClient.ts
import {
  WebClient,
  AccountStorageMode,
  AccountId,
  NoteType,
} from "@demox-labs/miden-sdk";

const nodeEndpoint = "http://localhost:57291";

export async function webClient(): Promise<void> {
  try {
    // 1. Create client
    const client = new WebClient();
    await client.create_client(nodeEndpoint);

    // 2. Sync and log block
    const state = await client.sync_state();
    console.log("Latest block number:", state.block_num());

    // 3. Create Alice account (public, updatable)
    console.log("Creating account for Alice");
    const aliceAccount = await client.new_wallet(
      AccountStorageMode.public(),
      true,
    );
    const aliceIdHex = aliceAccount.id().to_string();
    console.log("Alice's account ID:", aliceIdHex);

    // 4. Create faucet
    console.log("Creating faucet...");
    const faucetAccount = await client.new_faucet(
      AccountStorageMode.public(), // account type
      false,                       // is fungible
      "MID",                       // symbol
      8,                           // decimals
      BigInt(1_000_000)            // max supply
    );
    const faucetIdHex = faucetAccount.id().to_string();
    console.log("Faucet account ID:", faucetIdHex);

    await client.sync_state();
  } catch (error) {
    console.error("Error", error);
    throw error;
  }
}

Let's run the src/main.rs program again:

pnpm run dev

The output will look like this:

Latest block number: 2247
Alice's account ID: 0xd70b2072c6495d100000869a8bacf2
Faucet account ID: 0x2d7e506fb88dde200000a1386efec8

In this section, we explained how to instantiate the Miden client, create a wallet, and deploy a faucet.

In the next section we will cover how to mint tokens from the faucet, consume notes, and send tokens to other accounts.

Running the example

To run a full working example navigate to the web-client directory in the miden-tutorials repository and run the web application example:

cd web-client
pnpm i
pnpm run dev

Mint, Consume, and Create Notes

Using the Miden WebClient in TypeScript to mint, consume, and create notes

Overview

In the previous section, we initialized our repository and covered how to create an account and deploy a faucet. In this section, we will mint tokens from the faucet for Alice, consume the newly created notes, and demonstrate how to send assets to other accounts.

What we'll cover

  • Minting assets from a faucet
  • Consuming notes to fund an account
  • Sending tokens to other users

Step 1: Minting tokens from the faucet

To mint notes with tokens from the faucet we created, Alice can use the WebClient's new_mint_transaction() function.

Below is an example of a transaction request minting tokens from the faucet for Alice.

Add this snippet to the end of the webClient function in the src/webClient.ts file that we created in the previous chapter:

await client.fetch_and_cache_account_auth_by_pub_key(
  AccountId.from_hex(faucetIdHex),
);
await client.sync_state();

console.log("Minting tokens to Alice...");
await client.new_mint_transaction(
  AccountId.from_hex(aliceIdHex),  // target wallet id
  AccountId.from_hex(faucetIdHex), // faucet id
  NoteType.public(),               // note type
  BigInt(1000),                    // amount
);

console.log("Waiting 15 seconds for transaction confirmation...");
await new Promise((resolve) => setTimeout(resolve, 15000));
await client.sync_state();

Step 2: Identifying consumable notes

Once Alice has minted a note from the faucet, she will eventually want to spend the tokens that she received in the note created by the mint transaction.

Minting a note from a faucet on Miden means a faucet account creates a new note targeted to the requesting account. The requesting account must consume this note for the assets to appear in their account.

To identify notes that are ready to consume, the Miden WebClient has a useful function get_consumable_notes. It is also important to sync the state of the client before calling the get_consumable_notes function.

Tip: If you know the expected number of notes after a transaction, use await or a loop condition to verify their availability before calling get_consumable_notes. This prevents unnecessary application idling.

Identifying which notes are available:

consumable_notes = await client.get_consumable_notes(accountId);

Step 3: Consuming multiple notes in a single transaction:

Now that we know how to identify notes ready to consume, let's consume the notes created by the faucet in a single transaction. After consuming the notes, Alice's wallet balance will be updated.

The following code snippet identifies and consumes notes in a single transaction.

Add this snippet to the end of the webClient function in the src/webClient.ts file:

await client.fetch_and_cache_account_auth_by_pub_key(
  AccountId.from_hex(aliceIdHex),
);

const mintedNotes = await client.get_consumable_notes(
  AccountId.from_hex(aliceIdHex),
);
const mintedNoteIds = mintedNotes.map((n) =>
  n.input_note_record().id().to_string(),
);
console.log("Minted note IDs:", mintedNoteIds);

console.log("Consuming minted notes...");
await client.new_consume_transaction(
  AccountId.from_hex(aliceIdHex), // account id
  mintedNoteIds,                  // array of note ids to consume
);
await client.sync_state();
console.log("Notes consumed.");

Step 4: Sending tokens to other accounts

After consuming the notes, Alice has tokens in her wallet. Now, she wants to send tokens to her friends. She has two options: create a separate transaction for each transfer or batch multiple notes in a single transaction.

The standard asset transfer note on Miden is the P2ID note (Pay to Id). There is also the P2IDR (Pay to Id Reclaimable) variant which allows the creator of the note to reclaim the note after a certain block height.

In our example, Alice will now send 50 tokens to a different account.

Basic P2ID transfer

Now as an example, Alice will send some tokens to an account in a single transaction.

Add this snippet to the end of your file in the main() function:

// send single P2ID note
const dummyIdHex = "0x599a54603f0cf9000000ed7a11e379";
console.log("Sending tokens to dummy account...");
await client.new_send_transaction(
  AccountId.from_hex(aliceIdHex),  // sender account id
  AccountId.from_hex(dummyIdHex),  // receiver account id
  AccountId.from_hex(faucetIdHex), // faucet account id
  NoteType.public(),               // note type
  BigInt(100),                     // amount
);
await client.sync_state();

Summary

Your src/webClient.ts function should now look like this:

import {
  WebClient,
  AccountStorageMode,
  AccountId,
  NoteType,
} from "@demox-labs/miden-sdk";

const nodeEndpoint = "http://localhost:57291";

export async function webClient(): Promise<void> {
  try {
    // 1. Create client
    const client = new WebClient();
    await client.create_client(nodeEndpoint);

    // 2. Sync and log block
    const state = await client.sync_state();
    console.log("Latest block number:", state.block_num());

    // 3. Create Alice account (public, updatable)
    console.log("Creating account for Alice");
    const aliceAccount = await client.new_wallet(
      AccountStorageMode.public(), // account type
      true                         // mutability
    );
    const aliceIdHex = aliceAccount.id().to_string();
    console.log("Alice's account ID:", aliceIdHex);

    // 4. Create faucet
    console.log("Creating faucet...");
    const faucetAccount = await client.new_faucet(
      AccountStorageMode.public(),  // account type
      false,                        // fungible
      "MID",                        // symbol
      8,                            // decimals
      BigInt(1_000_000)             // max supply
    );
    const faucetIdHex = faucetAccount.id().to_string();
    console.log("Faucet account ID:", faucetIdHex);

    // 5. Mint tokens to Alice
    await client.fetch_and_cache_account_auth_by_pub_key(
      AccountId.from_hex(faucetIdHex),
    );
    await client.sync_state();

    console.log("Minting tokens to Alice...");
    await client.new_mint_transaction(
      AccountId.from_hex(aliceIdHex),  // target wallet id
      AccountId.from_hex(faucetIdHex), // faucet id
      NoteType.public(),               // note type
      BigInt(1000),                    // amount
    );

    console.log("Waiting 15 seconds for transaction confirmation...");
    await new Promise((resolve) => setTimeout(resolve, 15000));
    await client.sync_state();

    // 6. Fetch minted notes
    await client.fetch_and_cache_account_auth_by_pub_key(
      AccountId.from_hex(aliceIdHex),
    );

    const mintedNotes = await client.get_consumable_notes(
      AccountId.from_hex(aliceIdHex),
    );
    const mintedNoteIds = mintedNotes.map((n) =>
      n.input_note_record().id().to_string(),
    );
    console.log("Minted note IDs:", mintedNoteIds);

    // 7. Consume minted notes
    console.log("Consuming minted notes...");
    await client.new_consume_transaction(
      AccountId.from_hex(aliceIdHex), // account id
      mintedNoteIds,                  // array of note ids to consume
    );
    await client.sync_state();
    console.log("Notes consumed.");

    // 8. Send tokens to a dummy account
    const dummyIdHex = "0x599a54603f0cf9000000ed7a11e379";
    console.log("Sending tokens to dummy account...");
    await client.new_send_transaction(
      AccountId.from_hex(aliceIdHex),  // sender account id
      AccountId.from_hex(dummyIdHex),  // receiver account id
      AccountId.from_hex(faucetIdHex), // faucet account id
      NoteType.public(),               // note type
      BigInt(100),                     // amount
    );
    await client.sync_state();
    console.log("Tokens sent.");
  } catch (error) {
    console.error("Error:", error);
    throw error;
  }
}

Let's run the src/webClient.ts function again. Reload the page and click "Start WebClient".

Note: Currently there is a minor bug in the WebClient that produces a warning message, "Error inserting code with root" when creating multiple accounts. This is currently being fixed.

The output will look like this:

Latest block number: 4807
Alice's account ID: 0x1a20f4d1321e681000005020e69b1a
Creating faucet...
Faucet account ID: 0xaa86a6f05ae40b2000000f26054d5d
Minting tokens to Alice...
Waiting 15 seconds for transaction confirmation...
Minted note IDs: ['0x4edbb3d5dbdf6944f229a4711533114e0602ad48b70cda400993925c61f5bfaa']
Consuming minted notes...
Notes consumed.
Sending tokens to dummy account...
Tokens sent.

Resetting the MidenClientDB

The Miden webclient stores account and note data in the browser. To clear the account and node data in the browser, paste this code snippet into the browser console:

(async () => {
  const dbs = await indexedDB.databases(); // Get all database names
  for (const db of dbs) {
    await indexedDB.deleteDatabase(db.name);
    console.log(`Deleted database: ${db.name}`);
  }
  console.log("All databases deleted.");
})();

Running the example

To run a full working example navigate to the web-client directory in the miden-tutorials repository and run the web application example:

cd web-client
pnpm i
pnpm run dev