Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Setting‑up the WebClient and initializing a remote prover
  2. Building three P2ID notes worth 100 MID each
  3. 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

  1. Create a new Next.js app with TypeScript:

    npx create-next-app@latest miden-web-app --typescript
    

    Hit enter for all terminal prompts.

  2. Change into the project directory:

    cd miden-web-app
    
  3. 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.");
})();