Creating Multiple Notes in a Single Transaction
Using the Miden WebClient in TypeScript to create several P2ID notes in a single transaction
Overview
In the previous sections we learned how to create accounts, deploy faucets, and mint tokens. In this tutorial we will:
- Mint test tokens from a faucet to Alice
- Consume the minted notes so the assets appear in Alice’s wallet
- Create three P2ID notes in a single transaction using a custom note‑script and delegated proving
The entire flow is wrapped in a helper called multiSendWithDelegatedProver()
that you can call from any browser page.
What we’ll cover
- Setting‑up the WebClient and initializing a remote prover
- Building three P2ID notes worth 100
MID
each - Submitting the transaction using delegated proving
Prerequisites
- Node
v20
or greater - Familiarity with TypeScript
pnpm
What is Delegated Proving?
Before diving into our code example, let's clarify what in the world "delegated proving" actually is.
Delegated proving is the process of outsourcing a part of the ZK proof generation of your transaction to a third party. For certain computationally constrained devices such as mobile phones and web browser environments, generating ZK proofs might take too long to ensure an acceptable user experience. Devices that do not have the computational resources to generate Miden proofs in under 1-2 seconds can use delegated proving to provide a more responsive user experience.
How does it work? When a user choses to use delegated proving, they send off a portion of the zk proof of their transaction to a dedicated server. This dedicated server generates the remainder of the ZK proof of the transaction and submits it to the network. Submitting a transaction with delegated proving is trustless, meaning if the delegated prover is malicious, the could not compromise the security of the account that is submitting a transaction to be processed by the delegated prover. The downside of using delegated proving is that it reduces the privacy of the account that uses delegated proving, because the delegated prover would have knowledge of the transaction that is being proven. Additionally, transactions that require sensitive data such as the knowledge of a hash preimage or a secret, should not use delegated proving as this data will be shared with the delegated prover for proof generation.
Anyone can run their own delegated prover server. If you are building a product on Miden, it may make sense to run your own delegated prover server for your users. To run your own delegated proving server, follow the instructions here: https://crates.io/crates/miden-proving-service
Step 1: Initialize your Next.js project
-
Create a new Next.js app with TypeScript:
npx create-next-app@latest miden-web-app --typescript
Hit enter for all terminal prompts.
-
Change into the project directory:
cd miden-web-app
-
Install the Miden WebClient SDK:
pnpm install @demox-labs/miden-sdk@0.9.4
NOTE!: Be sure to remove the --turbopack
command from your package.json
when running the dev script
. The dev script should look like this:
package.json
"scripts": {
"dev": "next dev",
...
}
Step 2: Edit the app/page.tsx
file:
Add the following code to the app/page.tsx
file:
"use client";
import { useState } from "react";
import { multiSendWithDelegatedProver } from "../lib/multiSendWithDelegatedProver";
export default function Home() {
const [isMultiSendNotes, setIsMultiSendNotes] = useState(false);
const handleMultiSendNotes = async () => {
setIsMultiSendNotes(true);
await multiSendWithDelegatedProver();
setIsMultiSendNotes(false);
};
return (
<main className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 via-gray-800 to-black text-slate-800 dark:text-slate-100">
<div className="text-center">
<h1 className="text-4xl font-semibold mb-4">Miden Web App</h1>
<p className="mb-6">Open your browser console to see WebClient logs.</p>
<div className="max-w-sm w-full bg-gray-800/20 border border-gray-600 rounded-2xl p-6 mx-auto flex flex-col gap-4">
<button
onClick={handleMultiSendNotes}
className="w-full px-6 py-3 text-lg cursor-pointer bg-transparent border-2 border-orange-600 text-white rounded-lg transition-all hover:bg-orange-600 hover:text-white"
>
{isMultiSendNotes
? "Working..."
: "Tutorial #2: Send 1 to N P2ID Notes with Delegated Proving"}
</button>
</div>
</div>
</main>
);
}
Step 3 — Initalize the WebClient & Define the Note Script
Create the file lib/multiSendWithDelegatedProver.ts
and add the following code. This snippet defines the P2ID note script, implements the function multiSendWithDelegatedProver
, and initializes the WebClient along with the delegated prover endpoint.
mkdir -p lib
touch lib/multiSendWithDelegatedProver.ts
/**
* P2ID (Pay to ID) Note Script for Miden Network
* Enables creating notes that can be received by specific account IDs
*/
const P2ID_NOTE_SCRIPT = `
use.miden::account
use.miden::note
use.miden::contracts::wallets::basic->wallet
const.ERR_P2ID_WRONG_NUMBER_OF_INPUTS="P2ID note expects exactly 2 note inputs"
const.ERR_P2ID_TARGET_ACCT_MISMATCH="P2ID's target account address and transaction address do not match"
proc.add_note_assets_to_account
push.0 exec.note::get_assets
mul.4 dup.1 add
padw movup.5
dup dup.6 neq
while.true
dup movdn.5
mem_loadw
padw swapw padw padw swapdw
call.wallet::receive_asset
dropw dropw dropw
movup.4 add.4 dup dup.6 neq
end
drop dropw drop
end
begin
push.0 exec.note::get_inputs
eq.2 assert.err=ERR_P2ID_WRONG_NUMBER_OF_INPUTS
padw movup.4 mem_loadw drop drop
exec.account::get_id
exec.account::is_id_equal assert.err=ERR_P2ID_TARGET_ACCT_MISMATCH
exec.add_note_assets_to_account
end
`;
export async function multiSendWithDelegatedProver(): Promise<void> {
// Ensure this runs only in a browser context
if (typeof window === "undefined") return console.warn("Run in browser");
const {
WebClient,
AccountStorageMode,
AccountId,
NoteType,
TransactionProver,
NoteInputs,
Note,
NoteAssets,
NoteRecipient,
Word,
OutputNotesArray,
NoteExecutionHint,
NoteTag,
NoteExecutionMode,
NoteMetadata,
FeltArray,
Felt,
FungibleAsset,
TransactionRequestBuilder,
OutputNote,
} = await import("@demox-labs/miden-sdk");
const client = await WebClient.createClient(
"https://rpc.testnet.miden.io:443",
);
const prover = TransactionProver.newRemoteProver(
"https://tx-prover.testnet.miden.io",
);
console.log("Latest block:", (await client.syncState()).blockNum());
}
Step 4 — Create an account, deploy a faucet, mint and consume tokens
Add the code snippet below to the multiSendWithDelegatedProver
function. This code creates a wallet and faucet, mints tokens from the faucet for the wallet, and then consumes the minted tokens.
// ── Creating new account ──────────────────────────────────────────────────────
console.log("Creating account for Alice…");
const alice = await client.newWallet(AccountStorageMode.public(), true);
console.log("Alice accout ID:", alice.id().toString());
// ── Creating new faucet ──────────────────────────────────────────────────────
const faucet = await client.newFaucet(
AccountStorageMode.public(),
false,
"MID",
8,
BigInt(1_000_000),
);
console.log("Faucet ID:", faucet.id().toString());
// ── mint 10 000 MID to Alice ──────────────────────────────────────────────────────
await client.submitTransaction(
await client.newTransaction(
faucet.id(),
client.newMintTransactionRequest(
alice.id(),
faucet.id(),
NoteType.Public,
BigInt(10_000),
),
),
prover,
);
console.log("waiting for settlement");
await new Promise((r) => setTimeout(r, 7_000));
await client.syncState();
// ── consume the freshly minted notes ──────────────────────────────────────────────
const noteIds = (await client.getConsumableNotes(alice.id())).map((rec) =>
rec.inputNoteRecord().id().toString(),
);
await client.submitTransaction(
await client.newTransaction(
alice.id(),
client.newConsumeTransactionRequest(noteIds),
),
prover,
);
await client.syncState();
Step 5 — Build and Create P2ID notes
Add the following code to the multiSendWithDelegatedProver
function. This code defines three recipient addresses, builds three P2ID notes with 100 MID
each, and then creates all three notes in the same transaction.
// ── build 3 P2ID notes (100 MID each) ─────────────────────────────────────────────
const recipientAddresses = [
"0xbf1db1694c83841000008cefd4fce0",
"0xee1a75244282c32000010a29bed5f4",
"0x67dc56bd0cbe629000006f36d81029",
];
const script = client.compileNoteScript(P2ID_NOTE_SCRIPT);
const assets = new NoteAssets([new FungibleAsset(faucet.id(), BigInt(100))]);
const metadata = new NoteMetadata(
alice.id(),
NoteType.Public,
NoteTag.fromAccountId(alice.id(), NoteExecutionMode.newLocal()),
NoteExecutionHint.always(),
);
const p2idNotes = recipientAddresses.map((addr) => {
let serialNumber = Word.newFromFelts([
new Felt(BigInt(Math.floor(Math.random() * 0x1_0000_0000))),
new Felt(BigInt(Math.floor(Math.random() * 0x1_0000_0000))),
new Felt(BigInt(Math.floor(Math.random() * 0x1_0000_0000))),
new Felt(BigInt(Math.floor(Math.random() * 0x1_0000_0000))),
]);
const acct = AccountId.fromHex(addr);
const inputs = new NoteInputs(new FeltArray([acct.suffix(), acct.prefix()]));
let note = new Note(
assets,
metadata,
new NoteRecipient(serialNumber, script, inputs),
);
return OutputNote.full(note);
});
// ── create all P2ID notes ───────────────────────────────────────────────────────────────
let transaction = await client.newTransaction(
alice.id(),
new TransactionRequestBuilder()
.withOwnOutputNotes(new OutputNotesArray(p2idNotes))
.build(),
);
// ── submit tx ───────────────────────────────────────────────────────────────
await client.submitTransaction(transaction, prover);
console.log("All notes created ✅");
Summary
Your lib/multiSendWithDelegatedProver.ts
file sould now look like this:
/**
* P2ID (Pay to ID) Note Script for Miden Network
* Enables creating notes that can be received by specific account IDs
*/
const P2ID_NOTE_SCRIPT = `
use.miden::account
use.miden::note
use.miden::contracts::wallets::basic->wallet
const.ERR_P2ID_WRONG_NUMBER_OF_INPUTS="P2ID note expects exactly 2 note inputs"
const.ERR_P2ID_TARGET_ACCT_MISMATCH="P2ID's target account address and transaction address do not match"
proc.add_note_assets_to_account
push.0 exec.note::get_assets
mul.4 dup.1 add
padw movup.5
dup dup.6 neq
while.true
dup movdn.5
mem_loadw
padw swapw padw padw swapdw
call.wallet::receive_asset
dropw dropw dropw
movup.4 add.4 dup dup.6 neq
end
drop dropw drop
end
begin
push.0 exec.note::get_inputs
eq.2 assert.err=ERR_P2ID_WRONG_NUMBER_OF_INPUTS
padw movup.4 mem_loadw drop drop
exec.account::get_id
exec.account::is_id_equal assert.err=ERR_P2ID_TARGET_ACCT_MISMATCH
exec.add_note_assets_to_account
end
`;
export async function multiSendWithDelegatedProver(): Promise<void> {
// Ensure this runs only in a browser context
if (typeof window === "undefined") return console.warn("Run in browser");
const {
WebClient,
AccountStorageMode,
AccountId,
NoteType,
TransactionProver,
NoteInputs,
Note,
NoteAssets,
NoteRecipient,
Word,
OutputNotesArray,
NoteExecutionHint,
NoteTag,
NoteExecutionMode,
NoteMetadata,
FeltArray,
Felt,
FungibleAsset,
TransactionRequestBuilder,
OutputNote,
} = await import("@demox-labs/miden-sdk");
const client = await WebClient.createClient(
"https://rpc.testnet.miden.io:443",
);
const prover = TransactionProver.newRemoteProver(
"https://tx-prover.testnet.miden.io",
);
console.log("Latest block:", (await client.syncState()).blockNum());
// ── Creating new account ──────────────────────────────────────────────────────
console.log("Creating account for Alice…");
const alice = await client.newWallet(AccountStorageMode.public(), true);
console.log("Alice accout ID:", alice.id().toString());
// ── Creating new faucet ──────────────────────────────────────────────────────
const faucet = await client.newFaucet(
AccountStorageMode.public(),
false,
"MID",
8,
BigInt(1_000_000),
);
console.log("Faucet ID:", faucet.id().toString());
// ── mint 10 000 MID to Alice ──────────────────────────────────────────────────────
await client.submitTransaction(
await client.newTransaction(
faucet.id(),
client.newMintTransactionRequest(
alice.id(),
faucet.id(),
NoteType.Public,
BigInt(10_000),
),
),
prover,
);
console.log("waiting for settlement");
await new Promise((r) => setTimeout(r, 7_000));
await client.syncState();
// ── consume the freshly minted notes ──────────────────────────────────────────────
const noteIds = (await client.getConsumableNotes(alice.id())).map((rec) =>
rec.inputNoteRecord().id().toString(),
);
await client.submitTransaction(
await client.newTransaction(
alice.id(),
client.newConsumeTransactionRequest(noteIds),
),
prover,
);
await client.syncState();
// ── build 3 P2ID notes (100 MID each) ─────────────────────────────────────────────
const recipientAddresses = [
"0xbf1db1694c83841000008cefd4fce0",
"0xee1a75244282c32000010a29bed5f4",
"0x67dc56bd0cbe629000006f36d81029",
];
const script = client.compileNoteScript(P2ID_NOTE_SCRIPT);
const assets = new NoteAssets([new FungibleAsset(faucet.id(), BigInt(100))]);
const metadata = new NoteMetadata(
alice.id(),
NoteType.Public,
NoteTag.fromAccountId(alice.id(), NoteExecutionMode.newLocal()),
NoteExecutionHint.always(),
);
const p2idNotes = recipientAddresses.map((addr) => {
let serialNumber = Word.newFromFelts([
new Felt(BigInt(Math.floor(Math.random() * 0x1_0000_0000))),
new Felt(BigInt(Math.floor(Math.random() * 0x1_0000_0000))),
new Felt(BigInt(Math.floor(Math.random() * 0x1_0000_0000))),
new Felt(BigInt(Math.floor(Math.random() * 0x1_0000_0000))),
]);
const acct = AccountId.fromHex(addr);
const inputs = new NoteInputs(
new FeltArray([acct.suffix(), acct.prefix()]),
);
let note = new Note(
assets,
metadata,
new NoteRecipient(serialNumber, script, inputs),
);
return OutputNote.full(note);
});
// ── create all P2ID notes ───────────────────────────────────────────────────────────────
let transaction = await client.newTransaction(
alice.id(),
new TransactionRequestBuilder()
.withOwnOutputNotes(new OutputNotesArray(p2idNotes))
.build(),
);
// ── submit tx ───────────────────────────────────────────────────────────────
await client.submitTransaction(transaction, prover);
console.log("All notes created ✅");
}
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 start
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.");
})();