diff --git a/.gitignore b/.gitignore index 82293f30..b09721ee 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ web-client/.next/ web-client/playwright-report/ web-client/test-results/ **/.DS_Store -.DS_Store \ No newline at end of file +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..8c199ec7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,110 @@ +# Contributing to Miden Tutorials + +#### First off, thanks for taking the time to contribute! + +## Markdown Formatting with Prettier + +We use [Prettier](https://prettier.io/) to ensure our Markdown files are consistently formatted. + +### Installation + +- **Global Installation:** + + ```bash + npm install -g prettier + ``` + +- **Local (Dev Dependency) Installation:** + + ```bash + npm install --save-dev prettier + ``` + +### Formatting Files + +From the root of the project, run: + +```bash +prettier --write "**/*.md" +``` + +Make sure to run this command before submitting pull requests. + +## CodeSdkTabs: Dot-Indentation Convention + +The `CodeSdkTabs` component (in `docs/src/components/CodeSdkTabs.tsx`) lets tutorials show React and TypeScript code side-by-side. Code is passed as template literals inside MDX props: + +```mdx + +``` + +**Problem:** MDX/webpack strips leading whitespace from template literals, so indented code renders flush-left. + +**Solution:** Use leading dots (`.`) to represent indentation. Each dot equals one indent level (2 spaces). The `preserveIndent()` function in `CodeSdkTabs.tsx` converts dots to spaces at render time. + +### Example + +Source in `.md` file: + +``` +typescript: { code: `export function foo() { +.const x = 1; +.if (x) { +..console.log(x); +.} +}` } +``` + +Renders as: + +```ts +export function foo() { + const x = 1; + if (x) { + console.log(x); + } +} +``` + +### Rules + +| Context | Dots | Spaces | +| ------------------------------------------- | ---- | ------ | +| Top-level (`export`, `import`, closing `}`) | 0 | 0 | +| Inside function body | 1 | 2 | +| Inside nested block (`if`, `for`, etc.) | 2 | 4 | +| Function call arguments (continuation) | +1 | +2 | +| Deeper nesting | 3+ | 6+ | + +### Limitation: Leading-Dot Method Chains + +Because `preserveIndent()` converts **all** leading dots to spaces, TypeScript method-chaining syntax (`.withFoo()`, `.build()`) breaks when it appears at the start of a line. The leading `.` is consumed as indentation. + +**Workaround:** Assign the builder to a variable so the `.` appears mid-line: + +``` +// BAD – the leading dots get eaten: +..new TransactionRequestBuilder() +...withOwnOutputNotes(notes) +...build() + +// GOOD – dots are mid-line, preserveIndent ignores them: +..const builder = new TransactionRequestBuilder(); +..const request = builder.withOwnOutputNotes(notes).build(); +``` + +### Tips + +- Standalone code blocks (` ```ts...``` `) outside `CodeSdkTabs` use normal space indentation -- the dot convention only applies inside `CodeSdkTabs` template literals. +- The React and TypeScript code snippets both follow this convention. If you add or edit snippets, apply the same pattern. + +--- + +Thank you for contributing! diff --git a/contributing.md b/contributing.md deleted file mode 100644 index 9a299249..00000000 --- a/contributing.md +++ /dev/null @@ -1,35 +0,0 @@ -# Contributing to Miden Tutorials - -#### First off, thanks for taking the time to contribute! - -## Markdown Formatting with Prettier - -We use [Prettier](https://prettier.io/) to ensure our Markdown files are consistently formatted. - -### Installation - -- **Global Installation:** - - ```bash - npm install -g prettier - ``` - -- **Local (Dev Dependency) Installation:** - - ```bash - npm install --save-dev prettier - ``` - -### Formatting Files - -From the root of the project, run: - -```bash -prettier --write "**/*.md" -``` - -Make sure to run this command before submitting pull requests. - -Thank you for contributing! - ---- diff --git a/docs/src/components/CodeSdkTabs.module.css b/docs/src/components/CodeSdkTabs.module.css new file mode 100644 index 00000000..391d6d32 --- /dev/null +++ b/docs/src/components/CodeSdkTabs.module.css @@ -0,0 +1,82 @@ +/* CodeSdkTabs Component Styles */ + +.codeContainer { + margin: 1rem 0; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: var(--ifm-global-radius); + overflow: hidden; + background: var(--ifm-background-color); +} + +.tabContainer { + background: var(--ifm-color-emphasis-100); + border-bottom: 1px solid var(--ifm-color-emphasis-300); +} + +.tabButtons { + display: flex; + gap: 0; +} + +.tabButton { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + border: none; + background: transparent; + color: var(--ifm-color-content-secondary); + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + transition: all 0.2s ease; + border-bottom: 2px solid transparent; +} + +.tabButton:hover { + color: var(--ifm-color-primary); + background: var(--ifm-color-emphasis-200); +} + +.tabButton.active { + color: var(--ifm-color-primary); + background: var(--ifm-color-emphasis-200); + border-bottom: 2px solid var(--ifm-color-primary); +} + +.codeSection { + position: relative; + margin: 0; +} + +.codeSection pre { + margin: 0; + border-radius: 0; + border: none; +} + +/* Remove any extra bottom spacing from the theme code block */ +.codeSection :global(.theme-code-block) { + margin-bottom: 0 !important; +} + +.outputSection { + border-top: 1px solid var(--ifm-color-emphasis-300); + background: var(--ifm-color-emphasis-50); +} + +.outputHeader { + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 600; + color: var(--ifm-color-content-secondary); + background: var(--ifm-color-emphasis-100); + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.outputSection pre { + margin: 0; + border-radius: 0; + border: none; + background: var(--ifm-color-emphasis-50) !important; +} diff --git a/docs/src/components/CodeSdkTabs.tsx b/docs/src/components/CodeSdkTabs.tsx new file mode 100644 index 00000000..48b36ba2 --- /dev/null +++ b/docs/src/components/CodeSdkTabs.tsx @@ -0,0 +1,138 @@ +import React, { useState } from "react"; +import CodeBlock from "@theme/CodeBlock"; +import styles from "./CodeSdkTabs.module.css"; + +interface CodeExample { + react?: { + code: string; + output?: string; + }; + typescript?: { + code: string; + output?: string; + }; +} + +interface CodeSdkTabsProps { + example: CodeExample; + reactFilename?: string; + tsFilename?: string; +} + +// Dot-indentation convention for CodeSdkTabs +// ───────────────────────────────────────────── +// MDX/webpack strips leading whitespace from template literals inside JSX props. +// To preserve indentation in code snippets, use leading dots in the markdown +// source. Each dot represents one indent level (2 spaces). +// +// Example in a .md file: +// typescript: { code: `export function foo() { +// .const x = 1; +// .if (x) { +// ..console.log(x); +// .} +// }` } +// +// Renders as: +// export function foo() { +// const x = 1; +// if (x) { +// console.log(x); +// } +// } +// +// Rules: +// 0 dots → top-level declarations (export, import, closing braces) +// 1 dot → first level inside a function/block body +// 2 dots → second level (nested blocks, function call arguments) +// 3+ dots → deeper nesting +function preserveIndent(code: string): string { + return code.replace(/^(\.+)/gm, (match) => ' '.repeat(match.length)); +} + +export default function CodeSdkTabs({ + example, + reactFilename = "index.tsx", + tsFilename = "index.ts", +}: CodeSdkTabsProps): JSX.Element { + const [activeTab, setActiveTab] = useState<"react" | "typescript">( + example.react ? "react" : "typescript" + ); + + const hasReact = !!example.react; + const hasTypeScript = !!example.typescript; + + // Infer syntax language from filename extension (.tsx → tsx, .ts → ts) + const langFor = (filename: string, fallback: string) => + filename.endsWith(".tsx") ? "tsx" : filename.endsWith(".ts") ? "ts" : fallback; + + // Don't show tabs if there's only one language + if (!hasReact || !hasTypeScript) { + const singleLang = hasReact ? "react" : "typescript"; + const singleExample = example[singleLang]; + const filename = singleLang === "react" ? reactFilename : tsFilename; + + return ( +
+
+ + {preserveIndent(singleExample!.code)} + +
+ {singleExample!.output && ( +
+
Output
+ {singleExample.output} +
+ )} +
+ ); + } + + const currentExample = example[activeTab]; + const activeFilename = activeTab === "react" ? reactFilename : tsFilename; + + return ( +
+
+
+ + +
+
+ +
+ + {preserveIndent(currentExample!.code)} + +
+ + {currentExample!.output && ( +
+
Output
+ {currentExample.output} +
+ )} +
+ ); +} diff --git a/docs/src/components/index.ts b/docs/src/components/index.ts new file mode 100644 index 00000000..33f91fd9 --- /dev/null +++ b/docs/src/components/index.ts @@ -0,0 +1 @@ +export { default as CodeSdkTabs } from './CodeSdkTabs'; diff --git a/docs/src/web-client/create_deploy_tutorial.md b/docs/src/web-client/create_deploy_tutorial.md index c350229d..80666c29 100644 --- a/docs/src/web-client/create_deploy_tutorial.md +++ b/docs/src/web-client/create_deploy_tutorial.md @@ -3,6 +3,8 @@ title: 'Creating Accounts and Deploying Faucets' sidebar_position: 2 --- +import { CodeSdkTabs } from '@site/src/components'; + _Using the Miden WebClient in TypeScript to create accounts and deploy faucets_ ## Overview @@ -28,7 +30,7 @@ Before we dive into code, a quick refresher: - **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. +- **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. > **Important**: In Miden, "accounts" and "smart contracts" can be used interchangeably due to native account abstraction. Every account is programmable and can contain custom logic. @@ -51,10 +53,12 @@ It is useful to think of notes on Miden as "cryptographic cashier's checks" that cd miden-web-app ``` -3. Install the Miden WebClient SDK: - ```bash - yarn add @miden-sdk/miden-sdk@0.13.0 - ``` +3. Install the Miden SDK: + + **NOTE!**: Be sure to add the `--webpack` command to your `package.json` when running the `dev script`. The dev script should look like this: @@ -71,50 +75,95 @@ It is useful to think of notes on Miden as "cryptographic cashier's checks" that The WebClient is your gateway to interact with the Miden blockchain. It handles state synchronization, transaction creation, and proof generation. Let's set it up. -### Create `lib/createMintConsume.ts` +### Create the library file -First, we'll create a separate file for our blockchain logic. In the project root, create a folder `lib/` and inside it `createMintConsume.ts`: +First, we'll create a separate file for our blockchain logic. In the project root, create a folder `lib/` and inside it `lib/react/createMintConsume.tsx` (React) or `lib/createMintConsume.ts` (TypeScript): ```bash mkdir -p lib -touch lib/createMintConsume.ts ``` -```ts -// lib/createMintConsume.ts + { +..// We'll add our logic here +..console.log('Ready to go!'); +.}; + +.return ( +..
+... +..
+.); +} + +export default function CreateMintConsume() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`// lib/createMintConsume.ts export async function createMintConsume(): Promise { - if (typeof window === 'undefined') { - console.warn('webClient() can only run in the browser'); - return; - } +.if (typeof window === 'undefined') { +..console.warn('webClient() can only run in the browser'); +..return; +.} - // dynamic import → only in the browser, so WASM is loaded client‑side - const { WebClient, AccountStorageMode, AccountId, NoteType } = - await import('@miden-sdk/miden-sdk'); +.// dynamic import → only in the browser, so WASM is loaded client‑side +.const { WebClient } = +..await import('@miden-sdk/miden-sdk'); - // Connect to Miden testnet RPC endpoint - const nodeEndpoint = 'https://rpc.testnet.miden.io'; - const client = await WebClient.createClient(nodeEndpoint); +.// Connect to Miden testnet RPC endpoint +.const nodeEndpoint = 'https://rpc.testnet.miden.io'; +.const client = await WebClient.createClient(nodeEndpoint); - // 1. Sync with the latest blockchain state - // This fetches the latest block header and state commitments - const state = await client.syncState(); - console.log('Latest block number:', state.blockNum()); +.// 1. Sync with the latest blockchain state +.// This fetches the latest block header and state commitments +.const state = await client.syncState(); +.console.log('Latest block number:', state.blockNum()); - // At this point, your client is connected and synchronized - // Ready to create accounts and deploy contracts! -} -``` +.// At this point, your client is connected and synchronized +.// Ready to create accounts and deploy contracts! +}` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> > Since we will be handling proof generation in the browser, it will be slower than proof generation handled by the Rust client. Check out the [tutorial on delegated proving](./creating_multiple_notes_tutorial.md#what-is-delegated-proving) to speed up proof generation in the browser. ## Step 3: Create the User Interface -Now let's create a simple UI that will trigger our blockchain interactions. We'll replace the default Next.js page with a button that calls our `createMintConsume()` function. +Now let's create a simple UI that will trigger our blockchain interactions. We'll replace the default Next.js page with a button that calls our function. -Edit `app/page.tsx` to call `createMintConsume()` on a button click: +Edit `app/page.tsx`: + +If you're using the **React SDK**, the page simply renders your self-contained component: + +```tsx +// app/page.tsx +'use client'; +import CreateMintConsume from '../lib/react/createMintConsume'; + +export default function Home() { + return ; +} +``` + +If you're using the **TypeScript SDK**, the page manages state and calls the library function directly: ```tsx +// app/page.tsx 'use client'; import { useState } from 'react'; import { createMintConsume } from '../lib/createMintConsume'; @@ -154,39 +203,43 @@ export default function Home() { Now we'll create Alice's account. Let's create a **public** account so we can easily track her transactions. -Back in `lib/createMintConsume.ts`, extend the `createMintConsume()` function: +Back in your library file, extend the function: - -```ts -// lib/createMintConsume.ts + { +.// 1. Create Alice's wallet (public, mutable) +.console.log('Creating account for Alice…'); +.const alice = await createWallet({ storageMode: 'public' }); +.console.log('Alice ID:', alice.id().toString()); +};` }, +typescript: { code: `// lib/createMintConsume.ts export async function createMintConsume(): Promise { - if (typeof window === 'undefined') { - console.warn('webClient() can only run in the browser'); - return; - } - - const { WebClient, AccountStorageMode, AuthScheme } = await import( - "@miden-sdk/miden-sdk" - ); - - const nodeEndpoint = 'https://rpc.testnet.miden.io'; - const client = await WebClient.createClient(nodeEndpoint); - - // 1. Sync with the latest blockchain state - const state = await client.syncState(); - console.log('Latest block number:', state.blockNum()); - - // 2. Create Alice's account - console.log('Creating account for Alice…'); - const alice = await client.newWallet( - AccountStorageMode.public(), // Public: account state is visible on-chain - true, // Mutable: account code can be upgraded later - AuthScheme.AuthRpoFalcon512 // Auth Scheme: RPO Falcon 512 - ); - console.log('Alice ID:', alice.id().toString()); -} -``` - +.if (typeof window === 'undefined') { +..console.warn('webClient() can only run in the browser'); +..return; +.} + +.const { WebClient, AccountStorageMode, AuthScheme } = await import( +.."@miden-sdk/miden-sdk" +.); + +.const nodeEndpoint = 'https://rpc.testnet.miden.io'; +.const client = await WebClient.createClient(nodeEndpoint); + +.// 1. Sync with the latest blockchain state +.const state = await client.syncState(); +.console.log('Latest block number:', state.blockNum()); + +.// 2. Create Alice's account +.console.log('Creating account for Alice…'); +.const alice = await client.newWallet( +..AccountStorageMode.public(), // Public: account state is visible on-chain +..true, // Mutable: account code can be upgraded later +..AuthScheme.AuthRpoFalcon512 // Auth Scheme: RPO Falcon 512 +.); +.console.log('Alice ID:', alice.id().toString()); +}` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> ## Step 5: Deploy a Fungible Faucet @@ -194,24 +247,33 @@ A faucet in Miden is a special type of account that can mint new tokens. Think o Add this code after creating Alice's account: - -```ts -// 3. Deploy a fungible faucet + +console.log('Setup complete.');` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> ### Understanding Faucet Parameters: @@ -232,51 +294,95 @@ In this tutorial, we've successfully: 3. Created a wallet account for Alice 4. Deployed a fungible faucet that can mint custom tokens -Your final `lib/createMintConsume.ts` should look like: +Your final `lib/react/createMintConsume.tsx` (React) or `lib/createMintConsume.ts` (TypeScript) should look like: + + { +..// 1. Create Alice's wallet (public, mutable) +..console.log('Creating account for Alice…'); +..const alice = await createWallet({ storageMode: 'public' }); +..console.log('Alice ID:', alice.id().toString()); + +..// 2. Deploy a fungible faucet +..console.log('Creating faucet…'); +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: 'public', +..}); +..console.log('Faucet ID:', faucet.id().toString()); + +..console.log('Setup complete.'); +.}; + +.return ( +..
+... +..
+.); +} -```ts -// lib/createMintConsume.ts +export default function CreateMintConsume() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`// lib/createMintConsume.ts export async function createMintConsume(): Promise { - if (typeof window === 'undefined') { - console.warn('webClient() can only run in the browser'); - return; - } - - // dynamic import → only in the browser, so WASM is loaded client‑side - const { WebClient, AccountStorageMode, AuthScheme, NoteType, Address } = - await import('@miden-sdk/miden-sdk'); - - const nodeEndpoint = 'https://rpc.testnet.miden.io'; - const client = await WebClient.createClient(nodeEndpoint); - - // 1. Sync with the latest blockchain state - const state = await client.syncState(); - console.log('Latest block number:', state.blockNum()); - - // 2. Create Alice's account - console.log('Creating account for Alice…'); - const alice = await client.newWallet( - AccountStorageMode.public(), - true, - AuthScheme.AuthRpoFalcon512, - ); - console.log('Alice ID:', alice.id().toString()); - - // 3. Deploy a fungible faucet - console.log('Creating faucet…'); - const faucet = await client.newFaucet( - AccountStorageMode.public(), - false, - 'MID', - 8, - BigInt(1_000_000), - AuthScheme.AuthRpoFalcon512, - ); - console.log('Faucet ID:', faucet.id().toString()); - - console.log('Setup complete.'); -} -``` +.if (typeof window === 'undefined') { +..console.warn('webClient() can only run in the browser'); +..return; +.} + +.// dynamic import → only in the browser, so WASM is loaded client‑side +.const { WebClient, AccountStorageMode, AuthScheme } = +..await import('@miden-sdk/miden-sdk'); + +.const nodeEndpoint = 'https://rpc.testnet.miden.io'; +.const client = await WebClient.createClient(nodeEndpoint); + +.// 1. Sync with the latest blockchain state +.const state = await client.syncState(); +.console.log('Latest block number:', state.blockNum()); + +.// 2. Create Alice's account +.console.log('Creating account for Alice…'); +.const alice = await client.newWallet( +..AccountStorageMode.public(), +..true, +..AuthScheme.AuthRpoFalcon512, +.); +.console.log('Alice ID:', alice.id().toString()); + +.// 3. Deploy a fungible faucet +.console.log('Creating faucet…'); +.const faucet = await client.newFaucet( +..AccountStorageMode.public(), +..false, +..'MID', +..8, +..BigInt(1_000_000), +..AuthScheme.AuthRpoFalcon512, +.); +.console.log('Faucet ID:', faucet.id().toString()); + +.console.log('Setup complete.'); +}` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> ### Running the example diff --git a/docs/src/web-client/creating_multiple_notes_tutorial.md b/docs/src/web-client/creating_multiple_notes_tutorial.md index b673c316..08ed894c 100644 --- a/docs/src/web-client/creating_multiple_notes_tutorial.md +++ b/docs/src/web-client/creating_multiple_notes_tutorial.md @@ -3,22 +3,24 @@ title: 'Creating Multiple Notes in a Single Transaction' sidebar_position: 4 --- +import { CodeSdkTabs } from '@site/src/components'; + _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: +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 +- **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 +## What we'll cover -1. Setting‑up the WebClient and initializing a local prover -2. Building three P2ID notes worth 100 `MID` each +1. Setting‑up the WebClient +2. Building three P2ID notes worth 100 `MID` each 3. Submitting the transaction _using delegated proving_ ## Prerequisites @@ -37,8 +39,8 @@ _How does it work?_ When a user choses to use delegated proving, they send off a 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 -To keep this tutorial runnable without external services, the code below uses a local prover. You -can switch to delegated proving later by swapping in `TransactionProver.newRemoteProver(...)`. +The code below uses `submitNewTransaction`, which handles proving via the network's delegated +proving service. This means your browser never has to generate the full ZK proof locally. ## Step 1: Initialize your Next.js project @@ -56,10 +58,12 @@ can switch to delegated proving later by swapping in `TransactionProver.newRemot cd miden-web-app ``` -3. Install the Miden WebClient SDK: - ```bash - yarn add @miden-sdk/miden-sdk@0.13.0 - ``` +3. Install the Miden SDK: + + **NOTE!**: Be sure to add the `--webpack` command to your `package.json` when running the `dev script`. The dev script should look like this: @@ -76,7 +80,22 @@ can switch to delegated proving later by swapping in `TransactionProver.newRemot Add the following code to the `app/page.tsx` file: +If you're using the **React SDK**, the page simply renders your self-contained component: + +```tsx +// app/page.tsx +'use client'; +import MultiSendWithDelegatedProver from '../lib/react/multiSendWithDelegatedProver'; + +export default function Home() { + return ; +} +``` + +If you're using the **TypeScript SDK**, the page manages state and calls the library function directly: + ```tsx +// app/page.tsx 'use client'; import { useState } from 'react'; import { multiSendWithDelegatedProver } from '../lib/multiSendWithDelegatedProver'; @@ -112,280 +131,391 @@ export default function Home() { } ``` -## Step 3 — Initalize the WebClient +## Step 3 — Initialize the WebClient -Create the file `lib/multiSendWithDelegatedProver.ts` and add the following code. This snippet implements the function `multiSendWithDelegatedProver`, and initializes the WebClient along with a local prover. +Create `lib/react/multiSendWithDelegatedProver.tsx` (React) or `lib/multiSendWithDelegatedProver.ts` (TypeScript) and add the following code. This snippet initializes the WebClient. ``` mkdir -p lib -touch lib/multiSendWithDelegatedProver.ts ``` -```ts -export async function multiSendWithDelegatedProver(): Promise { - // Ensure this runs only in a browser context - if (typeof window === 'undefined') return console.warn('Run in browser'); - - const { - WebClient, - AccountStorageMode, - AuthScheme, - Address, - NoteType, - TransactionProver, - Note, - NoteAssets, - OutputNoteArray, - NoteAttachment, - FungibleAsset, - TransactionRequestBuilder, - OutputNote, - } = await import('@miden-sdk/miden-sdk'); - - const client = await WebClient.createClient('https://rpc.testnet.miden.io'); - const prover = TransactionProver.newLocalProver(); - - console.log('Latest block:', (await client.syncState()).blockNum()); + { +..// We'll add our logic here +.}; + +.return ( +..
+... +..
+.); } -``` -## Step 4 — Create an account, deploy a faucet, mint and consume tokens  +export default function MultiSendWithDelegatedProver() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`export async function multiSendWithDelegatedProver(): Promise { +.// Ensure this runs only in a browser context +.if (typeof window === 'undefined') return console.warn('Run in browser'); + +.const { +..WebClient, +..AccountStorageMode, +..AuthScheme, +..Address, +..NoteType, +..Note, +..NoteAssets, +..OutputNoteArray, +..NoteAttachment, +..FungibleAsset, +..TransactionRequestBuilder, +..OutputNote, +.} = await import('@miden-sdk/miden-sdk'); + +.const client = await WebClient.createClient('https://rpc.testnet.miden.io'); + +.console.log('Latest block:', (await client.syncState()).blockNum()); +}` }, +}} reactFilename="lib/react/multiSendWithDelegatedProver.tsx" tsFilename="lib/multiSendWithDelegatedProver.ts" /> + +## Step 4 — Create an account, deploy a faucet, mint and consume tokens + +Add the code snippet below to the function. This code creates a wallet and faucet, mints tokens from the faucet for the wallet, and then consumes the minted tokens. + + n.inputNoteRecord().id().toString()); +await consume({ accountId: aliceId, noteIds });`}, + typescript: { code:`// ── Creating new account ────────────────────────────────────────────────────── console.log('Creating account for Alice…'); const alice = await client.newWallet( - AccountStorageMode.public(), - true, - AuthScheme.AuthRpoFalcon512, +.AccountStorageMode.public(), +.true, +.AuthScheme.AuthRpoFalcon512, ); -console.log('Alice accout ID:', alice.id().toString()); +console.log('Alice account ID:', alice.id().toString()); // ── Creating new faucet ────────────────────────────────────────────────────── const faucet = await client.newFaucet( - AccountStorageMode.public(), - false, - 'MID', - 8, - BigInt(1_000_000), - AuthScheme.AuthRpoFalcon512, +.AccountStorageMode.public(), +.false, +.'MID', +.8, +.BigInt(1_000_000), +.AuthScheme.AuthRpoFalcon512, ); console.log('Faucet ID:', faucet.id().toString()); // ── mint 10 000 MID to Alice ────────────────────────────────────────────────────── -{ - const txResult = await client.executeTransaction( - faucet.id(), - client.newMintTransactionRequest( - alice.id(), - faucet.id(), - NoteType.Public, - BigInt(10_000), - ), - ); - const proven = await client.proveTransaction(txResult, prover); - const submissionHeight = await client.submitProvenTransaction( - proven, - txResult, - ); - await client.applyTransaction(txResult, submissionHeight); +await client.submitNewTransaction( +.faucet.id(), +.client.newMintTransactionRequest( +..alice.id(), +..faucet.id(), +..NoteType.Public, +..BigInt(10_000), +.), +); - console.log('waiting for settlement'); - await new Promise((r) => setTimeout(r, 7_000)); - await client.syncState(); -} +console.log('waiting for settlement'); +await new Promise((r) => setTimeout(r, 7_000)); +await client.syncState(); // ── consume the freshly minted notes ────────────────────────────────────────────── const noteList = (await client.getConsumableNotes(alice.id())).map((rec) => - rec.inputNoteRecord().toNote(), +.rec.inputNoteRecord().toNote(), ); -{ - const txResult = await client.executeTransaction( - alice.id(), - client.newConsumeTransactionRequest(noteList), - ); - const proven = await client.proveTransaction(txResult, prover); - await client.syncState(); - const submissionHeight = await client.submitProvenTransaction( - proven, - txResult, - ); - await client.applyTransaction(txResult, submissionHeight); -} -``` - -## 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. +await client.submitNewTransaction( +.alice.id(), +.client.newConsumeTransactionRequest(noteList), +);` }, +}} reactFilename="lib/react/multiSendWithDelegatedProver.tsx" tsFilename="lib/multiSendWithDelegatedProver.ts" /> + +## Step 5 — Build and Create P2ID notes + +Add the following code to the 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. + + { - const receiverAccountId = Address.fromBech32(addr).accountId(); - const note = Note.createP2IDNote( - alice.id(), - receiverAccountId, - assets, - NoteType.Public, - new NoteAttachment(), - ); - - return OutputNote.full(note); +.const receiverAccountId = Address.fromBech32(addr).accountId(); +.const note = Note.createP2IDNote( +..alice.id(), +..receiverAccountId, +..assets, +..NoteType.Public, +..new NoteAttachment(), +.); + +.return OutputNote.full(note); }); // ── create all P2ID notes ─────────────────────────────────────────────────────────────── -await client.submitNewTransaction( - alice.id(), - new TransactionRequestBuilder() - .withOwnOutputNotes(new OutputNoteArray(p2idNotes)) - .build(), -); +const builder = new TransactionRequestBuilder(); +const txRequest = builder.withOwnOutputNotes(new OutputNoteArray(p2idNotes)).build(); +await client.submitNewTransaction(alice.id(), txRequest); -console.log('All notes created ✅'); -``` +console.log('All notes created ✅');` }, +}} reactFilename="lib/react/multiSendWithDelegatedProver.tsx" tsFilename="lib/multiSendWithDelegatedProver.ts" /> ## Summary -Your `lib/multiSendWithDelegatedProver.ts` file sould now look like this: +Your library file should now look like this: + + { +..// 1. Create Alice's wallet +..console.log('Creating account for Alice…'); +..const alice = await createWallet({ storageMode: 'public' }); +..const aliceId = alice.id().toString(); +..console.log('Alice account ID:', aliceId); + +..// 2. Deploy a fungible faucet +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: 'public', +..}); +..const faucetId = faucet.id().toString(); +..console.log('Faucet ID:', faucetId); + +..// 3. Mint 10,000 MID to Alice +..const mintResult = await mint({ +...faucetId, +...targetAccountId: aliceId, +...amount: BigInt(10_000), +...noteType: 'public', +..}); + +..console.log('Waiting for settlement…'); +..await waitForCommit(mintResult.transactionId); + +..// 4. Consume the freshly minted notes +..const notes = await waitForConsumableNotes({ accountId: aliceId }); +..const noteIds = notes.map((n) => n.inputNoteRecord().id().toString()); +..await consume({ accountId: aliceId, noteIds }); + +..// 5. Send 100 MID to three recipients in a single transaction +..await sendMany({ +...from: aliceId, +...assetId: faucetId, +...recipients: [ +....{ to: 'mtst1aqezqc90x7dkzypr9m5fmlpp85w6cl04', amount: BigInt(100) }, +....{ to: 'mtst1apjg2ul76wrkxyr5qlcnczaskypa4ljn', amount: BigInt(100) }, +....{ to: 'mtst1arpee6y9cm8t7ypn33pc8fzj6gkzz7kd', amount: BigInt(100) }, +...], +...noteType: 'public', +..}); + +..console.log('All notes created ✅'); +.}; + +.return ( +..
+... +..
+.); +} -```ts -/** - * Demonstrates multi-send functionality using a local prover on the Miden Network - * Creates multiple P2ID (Pay to ID) notes for different recipients - * - * @throws {Error} If the function cannot be executed in a browser environment - */ +export default function MultiSendWithDelegatedProver() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`/\*\* +.\* Demonstrates multi-send functionality with delegated proving on the Miden Network +.\* Creates multiple P2ID (Pay to ID) notes for different recipients +.\* +.\* @throws {Error} If the function cannot be executed in a browser environment +.\*/ export async function multiSendWithDelegatedProver(): Promise { - // Ensure this runs only in a browser context - if (typeof window === 'undefined') return console.warn('Run in browser'); - - const { - WebClient, - AccountStorageMode, - AuthScheme, - Address, - NoteType, - TransactionProver, - Note, - NoteAssets, - OutputNoteArray, - FungibleAsset, - NoteAttachment, - TransactionRequestBuilder, - OutputNote, - } = await import('@miden-sdk/miden-sdk'); - - const client = await WebClient.createClient('https://rpc.testnet.miden.io'); - const prover = TransactionProver.newLocalProver(); - - 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, - AuthScheme.AuthRpoFalcon512, - ); - 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), - AuthScheme.AuthRpoFalcon512, - ); - console.log('Faucet ID:', faucet.id().toString()); - - // ── mint 10 000 MID to Alice ────────────────────────────────────────────────────── - { - const txResult = await client.executeTransaction( - faucet.id(), - client.newMintTransactionRequest( - alice.id(), - faucet.id(), - NoteType.Public, - BigInt(10_000), - ), - ); - const proven = await client.proveTransaction(txResult, prover); - const submissionHeight = await client.submitProvenTransaction( - proven, - txResult, - ); - await client.applyTransaction(txResult, submissionHeight); - - console.log('waiting for settlement'); - await new Promise((r) => setTimeout(r, 7_000)); - await client.syncState(); - } - - // ── consume the freshly minted notes ────────────────────────────────────────────── - const noteList = (await client.getConsumableNotes(alice.id())).map((rec) => - rec.inputNoteRecord().toNote(), - ); - - { - const txResult = await client.executeTransaction( - alice.id(), - client.newConsumeTransactionRequest(noteList), - ); - const proven = await client.proveTransaction(txResult, prover); - await client.syncState(); - const submissionHeight = await client.submitProvenTransaction( - proven, - txResult, - ); - await client.applyTransaction(txResult, submissionHeight); - } - - // ── build 3 P2ID notes (100 MID each) ───────────────────────────────────────────── - const recipientAddresses = [ - 'mtst1aqezqc90x7dkzypr9m5fmlpp85w6cl04', - 'mtst1apjg2ul76wrkxyr5qlcnczaskypa4ljn', - 'mtst1arpee6y9cm8t7ypn33pc8fzj6gkzz7kd', - ]; - - const assets = new NoteAssets([new FungibleAsset(faucet.id(), BigInt(100))]); - - const p2idNotes = recipientAddresses.map((addr) => { - const receiverAccountId = Address.fromBech32(addr).accountId(); - const note = Note.createP2IDNote( - alice.id(), - receiverAccountId, - assets, - NoteType.Public, - new NoteAttachment(), - ); - - return OutputNote.full(note); - }); - - // ── create all P2ID notes ─────────────────────────────────────────────────────────────── - await client.submitNewTransaction( - alice.id(), - new TransactionRequestBuilder() - .withOwnOutputNotes(new OutputNoteArray(p2idNotes)) - .build(), - ); - - console.log('All notes created ✅'); -} -``` +.// Ensure this runs only in a browser context +.if (typeof window === 'undefined') return console.warn('Run in browser'); + +.const { +..WebClient, +..AccountStorageMode, +..AuthScheme, +..Address, +..NoteType, +..Note, +..NoteAssets, +..OutputNoteArray, +..FungibleAsset, +..NoteAttachment, +..TransactionRequestBuilder, +..OutputNote, +.} = await import('@miden-sdk/miden-sdk'); + +.const client = await WebClient.createClient('https://rpc.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, +..AuthScheme.AuthRpoFalcon512, +.); +.console.log('Alice account ID:', alice.id().toString()); + +.// ── Creating new faucet ────────────────────────────────────────────────────── +.const faucet = await client.newFaucet( +..AccountStorageMode.public(), +..false, +..'MID', +..8, +..BigInt(1_000_000), +..AuthScheme.AuthRpoFalcon512, +.); +.console.log('Faucet ID:', faucet.id().toString()); + +.// ── mint 10 000 MID to Alice ────────────────────────────────────────────────────── +.await client.submitNewTransaction( +..faucet.id(), +..client.newMintTransactionRequest( +...alice.id(), +...faucet.id(), +...NoteType.Public, +...BigInt(10_000), +..), +.); + +.console.log('waiting for settlement'); +.await new Promise((r) => setTimeout(r, 7_000)); +.await client.syncState(); + +.// ── consume the freshly minted notes ────────────────────────────────────────────── +.const noteList = (await client.getConsumableNotes(alice.id())).map((rec) => +..rec.inputNoteRecord().toNote(), +.); + +.await client.submitNewTransaction( +..alice.id(), +..client.newConsumeTransactionRequest(noteList), +.); + +.// ── build 3 P2ID notes (100 MID each) ───────────────────────────────────────────── +.const recipientAddresses = [ +..'mtst1aqezqc90x7dkzypr9m5fmlpp85w6cl04', +..'mtst1apjg2ul76wrkxyr5qlcnczaskypa4ljn', +..'mtst1arpee6y9cm8t7ypn33pc8fzj6gkzz7kd', +.]; + +.const assets = new NoteAssets([new FungibleAsset(faucet.id(), BigInt(100))]); + +.const p2idNotes = recipientAddresses.map((addr) => { +..const receiverAccountId = Address.fromBech32(addr).accountId(); +..const note = Note.createP2IDNote( +...alice.id(), +...receiverAccountId, +...assets, +...NoteType.Public, +...new NoteAttachment(), +..); + +..return OutputNote.full(note); +.}); + +.// ── create all P2ID notes ─────────────────────────────────────────────────────────────── +.const builder = new TransactionRequestBuilder(); +.const txRequest = builder.withOwnOutputNotes(new OutputNoteArray(p2idNotes)).build(); +.await client.submitNewTransaction(alice.id(), txRequest); + +.console.log('All notes created ✅'); +}` }, +}} reactFilename="lib/react/multiSendWithDelegatedProver.tsx" tsFilename="lib/multiSendWithDelegatedProver.ts" /> ### Running the example diff --git a/docs/src/web-client/mint_consume_create_tutorial.md b/docs/src/web-client/mint_consume_create_tutorial.md index 7b8c12b1..81c03c7b 100644 --- a/docs/src/web-client/mint_consume_create_tutorial.md +++ b/docs/src/web-client/mint_consume_create_tutorial.md @@ -3,6 +3,8 @@ title: 'Mint, Consume, and Create Notes' sidebar_position: 3 --- +import { CodeSdkTabs } from '@site/src/components'; + _Using the Miden WebClient in TypeScript to mint, consume, and transfer assets_ ## Overview @@ -34,20 +36,30 @@ Before we start coding, it's important to understand **notes**: Let's mint some tokens for Alice. When we mint from a faucet, it creates a note containing the specified amount of tokens targeted to Alice's account. -Add this to the end of your `createMintConsume` function in `lib/createMintConsume.ts`: - - - -```ts -// 4. Mint tokens from the faucet to Alice +Add this to the end of your `createMintConsume` function: + + setTimeout(resolve, 10000)); -await client.syncState(); -``` - - +await client.syncState();` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> ### What's happening here? @@ -72,31 +82,40 @@ await client.syncState(); After minting, Alice has a note waiting for her but the tokens aren't in her account yet. To identify notes that are ready to consume, the Miden WebClient provides the `getConsumableNotes` function: -```ts -// 5. Find notes available for consumption -const consumableNotes = await client.getConsumableNotes(alice.id()); -console.log(`Found ${consumableNotes.length} note(s) to consume`); - -const noteIds = consumableNotes.map((note) => - note.inputNoteRecord().id().toString(), -); -console.log('Consumable note IDs:', noteIds); -``` + n.inputNoteRecord().id().toString()); +console.log('Consumable notes:', noteIds);` }, +typescript: { code: `// 5. Find notes available for consumption +const mintedNotes = await client.getConsumableNotes(alice.id()); +console.log(\`Found \${mintedNotes.length} note(s) to consume\`); + +const mintedNoteList = mintedNotes.map((n) => n.inputNoteRecord().toNote()); +console.log( +.'Minted notes:', +.mintedNoteList.map((note) => note.id().toString()), +);` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> ## Step 3: Consume notes in a single transaction Now let's consume the notes to add the tokens to Alice's account balance: -```ts -// 6. Consume the notes to add tokens to Alice's balance + ## Step 4: Sending tokens to other accounts @@ -106,32 +125,36 @@ _The standard asset transfer note on Miden is the P2ID note (Pay-to-Id). There i Now that Alice has tokens in her account, she can send some to Bob: - -```ts -// Add this import at the top of the file -import { NoteType } from "@miden-sdk/miden-sdk"; -// ... - -// 7. Send tokens from Alice to Bob + +console.log('Tokens sent successfully!');` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> ### Understanding P2ID notes @@ -143,106 +166,190 @@ The transaction creates a **P2ID (Pay-to-ID)** note: ## Summary -Here's the complete `lib/createMintConsume.ts` file: - -```ts -// lib/createMintConsume.ts -export async function createMintConsume(): Promise { - if (typeof window === 'undefined') { - console.warn('webClient() can only run in the browser'); - return; - } - - // dynamic import → only in the browser, so WASM is loaded client‑side - const { WebClient, AccountStorageMode, AuthScheme, NoteType, Address } = - await import('@miden-sdk/miden-sdk'); - - const nodeEndpoint = 'https://rpc.testnet.miden.io'; - const client = await WebClient.createClient(nodeEndpoint); - - // 1. Sync with the latest blockchain state - const state = await client.syncState(); - console.log('Latest block number:', state.blockNum()); - - // 2. Create Alice's account - console.log('Creating account for Alice…'); - const aliceSeed = new Uint8Array(32); - crypto.getRandomValues(aliceSeed); - const alice = await client.newWallet( - AccountStorageMode.public(), - true, - AuthScheme.AuthRpoFalcon512, - aliceSeed, - ); - console.log('Alice ID:', alice.id().toString()); - - // 3. Deploy a fungible faucet - console.log('Creating faucet…'); - const faucet = await client.newFaucet( - AccountStorageMode.public(), - false, - 'MID', - 8, - BigInt(1_000_000), - AuthScheme.AuthRpoFalcon512, - ); - console.log('Faucet ID:', faucet.id().toString()); - - await client.syncState(); - - // 4. Mint tokens to Alice - await client.syncState(); - - console.log('Minting tokens to Alice...'); - const mintTxRequest = client.newMintTransactionRequest( - alice.id(), - faucet.id(), - NoteType.Public, - BigInt(1000), - ); - - await client.submitNewTransaction(faucet.id(), mintTxRequest); - - console.log('Waiting 10 seconds for transaction confirmation...'); - await new Promise((resolve) => setTimeout(resolve, 10000)); - await client.syncState(); - - // 5. Fetch minted notes - const mintedNotes = await client.getConsumableNotes(alice.id()); - const mintedNoteList = mintedNotes.map((n) => n.inputNoteRecord().toNote()); - console.log( - 'Minted notes:', - mintedNoteList.map((note) => note.id().toString()), - ); - - // 6. Consume minted notes - console.log('Consuming minted notes...'); - const consumeTxRequest = client.newConsumeTransactionRequest(mintedNoteList); - - await client.submitNewTransaction(alice.id(), consumeTxRequest); - - await client.syncState(); - console.log('Notes consumed.'); - - // 7. Send tokens to Bob - const bobAccountId = Address.fromBech32( - 'mtst1apve54rq8ux0jqqqqrkh5y0r0y8cwza6_qruqqypuyph', - ).accountId(); - console.log("Sending tokens to Bob's account..."); - const sendTxRequest = client.newSendTransactionRequest( - alice.id(), - bobAccountId, - faucet.id(), - NoteType.Public, - BigInt(100), - ); - - await client.submitNewTransaction(alice.id(), sendTxRequest); - console.log('Tokens sent successfully!'); +Here's the complete `lib/react/createMintConsume.tsx` (React) or `lib/createMintConsume.ts` (TypeScript): + + { +..// 1. Create Alice's wallet (public, mutable) +..console.log('Creating account for Alice…'); +..const alice = await createWallet({ storageMode: 'public' }); +..const aliceId = alice.id().toString(); +..console.log('Alice ID:', aliceId); + +..// 2. Deploy a fungible faucet +..console.log('Creating faucet…'); +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: 'public', +..}); +..const faucetId = faucet.id().toString(); +..console.log('Faucet ID:', faucetId); + +..// 3. Mint 1000 tokens to Alice +..console.log('Minting tokens to Alice...'); +..const mintResult = await mint({ +...faucetId, +...targetAccountId: aliceId, +...amount: BigInt(1000), +...noteType: 'public', +..}); +..console.log('Mint tx:', mintResult.transactionId); + +..// 4. Wait for the mint transaction to be committed +..await waitForCommit(mintResult.transactionId); + +..// 5. Wait for consumable notes to appear +..const notes = await waitForConsumableNotes({ accountId: aliceId }); +..const noteIds = notes.map((n) => n.inputNoteRecord().id().toString()); +..console.log('Consumable notes:', noteIds); + +..// 6. Consume minted notes +..console.log('Consuming minted notes...'); +..await consume({ accountId: aliceId, noteIds }); +..console.log('Notes consumed.'); + +..// 7. Send 100 tokens to Bob +..const bobAddress = 'mtst1apve54rq8ux0jqqqqrkh5y0r0y8cwza6_qruqqypuyph'; +..console.log("Sending tokens to Bob's account..."); +..await send({ +...from: aliceId, +...to: bobAddress, +...assetId: faucetId, +...amount: BigInt(100), +...noteType: 'public', +..}); +..console.log('Tokens sent successfully!'); +.}; + +.return ( +..
+... +..
+.); } -``` -Let's run the `lib/createMintConsume.ts` function again. Reload the page and click "Start WebClient". +export default function CreateMintConsume() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`// lib/createMintConsume.ts +export async function createMintConsume(): Promise { +.if (typeof window === 'undefined') { +..console.warn('webClient() can only run in the browser'); +..return; +.} + +.// dynamic import → only in the browser, so WASM is loaded client‑side +.const { WebClient, AccountStorageMode, AuthScheme, NoteType, Address } = +..await import('@miden-sdk/miden-sdk'); + +.const nodeEndpoint = 'https://rpc.testnet.miden.io'; +.const client = await WebClient.createClient(nodeEndpoint); + +.// 1. Sync with the latest blockchain state +.const state = await client.syncState(); +.console.log('Latest block number:', state.blockNum()); + +.// 2. Create Alice's account +.console.log('Creating account for Alice…'); +.const aliceSeed = new Uint8Array(32); +.crypto.getRandomValues(aliceSeed); +.const alice = await client.newWallet( +..AccountStorageMode.public(), +..true, +..AuthScheme.AuthRpoFalcon512, +..aliceSeed, +.); +.console.log('Alice ID:', alice.id().toString()); + +.// 3. Deploy a fungible faucet +.console.log('Creating faucet…'); +.const faucet = await client.newFaucet( +..AccountStorageMode.public(), +..false, +..'MID', +..8, +..BigInt(1_000_000), +..AuthScheme.AuthRpoFalcon512, +.); +.console.log('Faucet ID:', faucet.id().toString()); + +.await client.syncState(); + +.// 4. Mint tokens to Alice +.await client.syncState(); + +.console.log('Minting tokens to Alice...'); +.const mintTxRequest = client.newMintTransactionRequest( +..alice.id(), +..faucet.id(), +..NoteType.Public, +..BigInt(1000), +.); + +.await client.submitNewTransaction(faucet.id(), mintTxRequest); + +.console.log('Waiting 10 seconds for transaction confirmation...'); +.await new Promise((resolve) => setTimeout(resolve, 10000)); +.await client.syncState(); + +.// 5. Fetch minted notes +.const mintedNotes = await client.getConsumableNotes(alice.id()); +.const mintedNoteList = mintedNotes.map((n) => n.inputNoteRecord().toNote()); +.console.log( +..'Minted notes:', +..mintedNoteList.map((note) => note.id().toString()), +.); + +.// 6. Consume minted notes +.console.log('Consuming minted notes...'); +.const consumeTxRequest = client.newConsumeTransactionRequest(mintedNoteList); + +.await client.submitNewTransaction(alice.id(), consumeTxRequest); + +.await client.syncState(); +.console.log('Notes consumed.'); + +.// 7. Send tokens to Bob +.const bobAccountId = Address.fromBech32( +..'mtst1apve54rq8ux0jqqqqrkh5y0r0y8cwza6_qruqqypuyph', +.).accountId(); +.console.log("Sending tokens to Bob's account..."); +.const sendTxRequest = client.newSendTransactionRequest( +..alice.id(), +..bobAccountId, +..faucet.id(), +..NoteType.Public, +..BigInt(100), +.); + +.await client.submitNewTransaction(alice.id(), sendTxRequest); +.console.log('Tokens sent successfully!'); +}` }, +}} reactFilename="lib/react/createMintConsume.tsx" tsFilename="lib/createMintConsume.ts" /> + +Let's run the function again. Reload the page and click "Start WebClient". The output will look like this: diff --git a/docs/src/web-client/react_wallet_tutorial.md b/docs/src/web-client/react_wallet_tutorial.md new file mode 100644 index 00000000..24bfc0b5 --- /dev/null +++ b/docs/src/web-client/react_wallet_tutorial.md @@ -0,0 +1,930 @@ +--- +title: 'Building a React Wallet' +sidebar_position: 8 +--- + +# Building a React Wallet + +_Using the Miden React SDK to build a complete wallet UI with account management, token transfers, and note claiming_ + +## Overview + +In this tutorial we will build a complete wallet application using the `@miden-sdk/react` package. The Miden React SDK provides a set of hooks and utilities that make it easy to integrate Miden functionality into React applications. + +By the end of this tutorial, you will have a working wallet that can: + +- Create new accounts +- Display account balances +- List and claim unclaimed notes +- Send tokens to other accounts + +## What we'll cover + +- Setting up a React project with the Miden React SDK +- Using the `MidenProvider` to configure the client +- Managing accounts with `useAccounts`, `useAccount`, and `useCreateWallet` +- Displaying and claiming notes with `useNotes` and `useConsume` +- Sending tokens with `useSend` +- Formatting utilities for assets and notes +- External signer integration patterns + +## Prerequisites + +- Node `v20` or greater +- Familiarity with React and TypeScript +- `yarn` + +--- + +## Step 1: Project Setup and MidenProvider + +First, create a new Vite + React project and install the Miden React SDK. + +1. Create a new Vite project with React and TypeScript: + + ```bash + yarn create vite miden-wallet --template react-ts + cd miden-wallet + ``` + +2. Install the Miden React SDK: + + ```bash + yarn add @miden-sdk/react + ``` + +3. Configure the `MidenProvider` in your `main.tsx` file. The provider initializes the Miden client and makes it available to all child components: + +```tsx +// main.tsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { MidenProvider } from '@miden-sdk/react'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +); +``` + +The `MidenProvider` accepts a `config` object with the following options: + +- `rpcUrl`: The RPC endpoint to connect to (`"testnet"`, `"devnet"`, or a custom URL) +- `prover`: The prover to use (`"testnet"` for delegated proving, or `"local"` for local proving) + +--- + +## Step 2: App Shell with useMiden + +The `useMiden()` hook provides access to the client's initialization state. Use it to show loading and error states while the client initializes. + +```tsx +// App.tsx +import { useMiden } from '@miden-sdk/react'; + +export default function App() { + const { isReady, error } = useMiden(); + + if (error) return
Error: {error.message}
; + if (!isReady) return
Initializing...
; + + return
Wallet ready!
; +} +``` + +The `useMiden()` hook returns: + +- `isReady`: `true` when the client has finished initializing +- `error`: An error object if initialization failed + +--- + +## Step 3: Listing Accounts with useAccounts + +The `useAccounts()` hook provides access to all accounts stored in the client. Use it to check if the user has any existing wallets. + +```tsx +import { useMiden, useAccounts } from '@miden-sdk/react'; + +export default function App() { + const { isReady, error } = useMiden(); + const { wallets, isLoading } = useAccounts(); + + if (error) return
Error: {error.message}
; + if (!isReady || isLoading) return
Loading...
; + + const accountId = wallets[0]?.id().toString(); + + if (!accountId) { + return
No wallet found. Create one!
; + } + + return
Account: {accountId}
; +} +``` + +The `useAccounts()` hook returns: + +- `wallets`: Array of wallet accounts +- `faucets`: Array of faucet accounts +- `isLoading`: `true` while accounts are being fetched + +--- + +## Step 4: Creating a Wallet with useCreateWallet + +The `useCreateWallet()` hook provides a function to create new wallet accounts. + +```tsx +import { useMiden, useAccounts, useCreateWallet } from '@miden-sdk/react'; + +export default function App() { + const { isReady, error } = useMiden(); + const { wallets, isLoading } = useAccounts(); + const { createWallet, isCreating } = useCreateWallet(); + + if (error) return
Error: {error.message}
; + if (!isReady || isLoading) return
Loading...
; + + const accountId = wallets[0]?.id().toString(); + + if (!accountId) { + return ( +
+

Wallet

+ +
+ ); + } + + return ; +} + +function Wallet({ accountId }: { accountId: string }) { + return
Wallet: {accountId}
; +} +``` + +The `useCreateWallet()` hook returns: + +- `createWallet(options?)`: Function to create a new wallet +- `isCreating`: `true` while a wallet is being created + +--- + +## Step 5: Displaying Account Details with useAccount + +The `useAccount(accountId)` hook provides detailed information about a specific account, including its assets and balances. + +```tsx +import { useAccount, formatAssetAmount } from '@miden-sdk/react'; + +function Wallet({ accountId }: { accountId: string }) { + const { account, assets } = useAccount(accountId); + + return ( +
+

Wallet

+ +
+

Address

+
{account?.bech32id?.() ?? 'Loading...'}
+
+ +
+

Balances

+ {assets.length === 0 ? ( +
No assets
+ ) : ( +
    + {assets.map((asset) => ( +
  • + {asset.symbol ?? asset.assetId} + {formatAssetAmount(asset.amount, asset.decimals)} +
  • + ))} +
+ )} +
+
+ ); +} +``` + +The `useAccount(accountId)` hook returns: + +- `account`: The account object with methods like `bech32id()` +- `assets`: Array of asset objects with `assetId`, `symbol`, `amount`, and `decimals` +- `isLoading`: `true` while account data is being fetched + +The `formatAssetAmount(amount, decimals)` utility formats a raw amount with the correct decimal places. + +--- + +## Step 6: Listing Unclaimed Notes with useNotes + +The `useNotes({ accountId })` hook provides access to notes that can be consumed by the account. + +```tsx +import { useNotes, formatNoteSummary } from '@miden-sdk/react'; + +function UnclaimedNotes({ accountId }: { accountId: string }) { + const { consumableNoteSummaries } = useNotes({ accountId }); + + return ( +
+

Unclaimed Notes

+ {consumableNoteSummaries.length === 0 ? ( +
No unclaimed notes
+ ) : ( +
    + {consumableNoteSummaries.map((summary) => ( +
  • {formatNoteSummary(summary)}
  • + ))} +
+ )} +
+ ); +} +``` + +The `useNotes({ accountId })` hook returns: + +- `consumableNoteSummaries`: Array of note summaries that can be consumed +- `isLoading`: `true` while notes are being fetched + +The `formatNoteSummary(summary)` utility formats a note summary for display. + +--- + +## Step 7: Claiming Notes with useConsume + +The `useConsume()` hook provides a function to consume (claim) notes and add their assets to the account. + +```tsx +import { useConsume, formatNoteSummary } from '@miden-sdk/react'; + +function UnclaimedNotes({ + accountId, + consumableNoteSummaries, +}: { + accountId: string; + consumableNoteSummaries: Array<{ id: string }>; +}) { + const { consume, isLoading: isConsuming } = useConsume(); + + const claimNote = (id: string) => () => { + consume({ accountId, noteIds: [id] }); + }; + + return ( +
+

Unclaimed Notes

+ {consumableNoteSummaries.length === 0 ? ( +
No unclaimed notes
+ ) : ( +
    + {consumableNoteSummaries.map((summary) => ( +
  • + {formatNoteSummary(summary)} + +
  • + ))} +
+ )} +
+ ); +} +``` + +The `useConsume()` hook returns: + +- `consume({ accountId, noteIds })`: Function to consume one or more notes +- `isLoading`: `true` while notes are being consumed + +--- + +## Step 8: Sending Tokens with useSend + +The `useSend()` hook provides a function to send tokens to other accounts. + +```tsx +import { useState, type ChangeEvent } from 'react'; +import { useSend, parseAssetAmount } from '@miden-sdk/react'; + +function SendForm({ + accountId, + assets, +}: { + accountId: string; + assets: Array<{ assetId: string; symbol?: string; decimals?: number }>; +}) { + const { send, isLoading: isSending } = useSend(); + const [to, setTo] = useState(''); + const [assetId, setAssetId] = useState(assets[0]?.assetId ?? ''); + const [amount, setAmount] = useState(''); + const [noteType, setNoteType] = useState<'private' | 'public'>('private'); + + const selectedAsset = assets.find((asset) => asset.assetId === assetId); + const selectedDecimals = selectedAsset?.decimals; + const hasAssets = assets.length > 0; + const canSend = Boolean(hasAssets && to && assetId && amount); + + const handleSend = async () => { + try { + if (!assetId) return; + const amt = parseAssetAmount(amount, selectedDecimals); + await send({ from: accountId, to, assetId, amount: amt, noteType }); + setAmount(''); + } catch (error) { + console.error(error); + } + }; + + const onAssetChange = (e: ChangeEvent) => + setAssetId(e.target.value); + const onNoteTypeChange = (e: ChangeEvent) => + setNoteType(e.target.value as 'private' | 'public'); + const onToChange = (e: ChangeEvent) => + setTo(e.target.value); + const onAmountChange = (e: ChangeEvent) => + setAmount(e.target.value); + + return ( +
+

Send

+ + + + + +
+ ); +} +``` + +The `useSend()` hook returns: + +- `send({ from, to, assetId, amount, noteType })`: Function to send tokens +- `isLoading`: `true` while the transaction is being processed + +Parameters: + +- `from`: The sender's account ID +- `to`: The recipient's address (bech32 format) +- `assetId`: The asset/faucet ID to send +- `amount`: The amount to send (as a BigInt) +- `noteType`: Either `"private"` or `"public"` + +The `parseAssetAmount(amount, decimals)` utility converts a string amount to a BigInt with the correct decimal places. + +--- + +## Summary: Complete Code + +Here is the complete wallet application combining all the features we've covered. + +**main.tsx** + +```tsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { MidenProvider } from '@miden-sdk/react'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +); +``` + +**App.tsx** + +```tsx +import { useEffect, useState, type ChangeEvent, type ReactNode } from 'react'; +import { + formatAssetAmount, + formatNoteSummary, + parseAssetAmount, +} from '@miden-sdk/react'; +import { + useMiden, + useAccounts, + useAccount, + useNotes, + useCreateWallet, + useConsume, + useSend, +} from '@miden-sdk/react'; + +const Panel = ({ title, children }: { title: string; children: ReactNode }) => ( +
+
{title}
+ {children} +
+); + +export default function App() { + const { isReady, error } = useMiden(); + const { wallets, isLoading } = useAccounts(); + const { createWallet, isCreating } = useCreateWallet(); + const handleCreate = () => createWallet(); + const createLabel = isCreating ? 'Creating...' : 'Create wallet'; + + if (error) return
Error: {error.message}
; + if (!isReady || isLoading) + return ( +
+ {!isReady ? 'Initializing...' : 'Loading...'} +
+ ); + + const accountId = wallets[0]?.id().toString(); + if (!accountId) + return ( +
+

Wallet

+ +
+ ); + + return ; +} + +function Wallet({ accountId }: { accountId: string }) { + const { account, assets } = useAccount(accountId); + const { consumableNoteSummaries } = useNotes({ accountId }); + const { consume, isLoading: isConsuming } = useConsume(); + const { send, isLoading: isSending } = useSend(); + const [to, setTo] = useState(''); + const [assetId, setAssetId] = useState(''); + const [amount, setAmount] = useState(''); + const [noteType, setNoteType] = useState<'private' | 'public'>('private'); + const defaultAssetId = assets[0]?.assetId; + const selectedAsset = assets.find((asset) => asset.assetId === assetId); + const selectedDecimals = selectedAsset?.decimals; + const hasAssets = assets.length > 0; + + useEffect(() => { + if (!assetId && defaultAssetId) setAssetId(defaultAssetId); + }, [assetId, defaultAssetId]); + + const handleSend = async () => { + try { + if (!assetId) return; + const amt = parseAssetAmount(amount, selectedDecimals); + await send({ from: accountId, to, assetId, amount: amt, noteType }); + setAmount(''); + } catch (error) { + console.error(error); + } + }; + + const claimNote = (id: string) => () => consume({ accountId, noteIds: [id] }); + const onAssetChange = (event: ChangeEvent) => + setAssetId(event.target.value); + const onNoteTypeChange = (event: ChangeEvent) => + setNoteType(event.target.value as 'private' | 'public'); + const onToChange = (event: ChangeEvent) => + setTo(event.target.value); + const onAmountChange = (event: ChangeEvent) => + setAmount(event.target.value); + const canSend = Boolean(hasAssets && to && assetId && amount); + const sendLabel = isSending ? 'Sending...' : 'Send'; + + return ( +
+

Wallet

+ +
{account?.bech32id?.() ?? 'Loading...'}
+
+ + {assets.length === 0 ? ( +
None
+ ) : ( +
+ {assets.map((asset) => ( +
+ {asset.symbol ?? asset.assetId} + {formatAssetAmount(asset.amount, asset.decimals)} +
+ ))} +
+ )} +
+ + {consumableNoteSummaries.length === 0 ? ( +
None
+ ) : ( +
+ {consumableNoteSummaries.map((summary) => { + const id = summary.id; + const label = formatNoteSummary(summary); + return ( +
+ {label} + +
+ ); + })} +
+ )} +
+ +
+ + + + + +
+
+
+ ); +} +``` + +--- + +## Running the Example + +To run a full working example, navigate to the `packages/react-sdk/examples/wallet` directory in the [miden-client](https://github.com/0xMiden/miden-client/) repository: + +```bash +git clone https://github.com/0xMiden/miden-client.git +cd miden-client/packages/react-sdk/examples/wallet +yarn install +yarn dev +``` + +### Resetting the MidenClientDB + +The Miden client stores account and note data in the browser's IndexedDB. To clear this data, paste the following into your browser console: + +```javascript +(async () => { + const dbs = await indexedDB.databases(); + for (const db of dbs) { + await indexedDB.deleteDatabase(db.name); + console.log(`Deleted database: ${db.name}`); + } + console.log('All databases deleted.'); +})(); +``` + +--- + +## External Signer Integration + +By default, the Miden React SDK manages keys internally using the browser's IndexedDB. However, for production applications you may want to integrate with external signers that provide enhanced security, key management, or authentication features. + +### The useSigner Hook + +The `useSigner()` hook from `@miden-sdk/react` provides a unified interface for interacting with any signer provider. When you wrap your app with a signer provider (Para, Turnkey, MidenFi, etc.), the hook returns the signer context with connection state and methods. + +```tsx +import { useSigner } from '@miden-sdk/react'; + +function ConnectButton() { + const signer = useSigner(); + + // Returns null if no signer provider is present (local keystore mode) + if (!signer) return null; + + const { isConnected, connect, disconnect, name } = signer; + + return isConnected ? ( + + ) : ( + + ); +} +``` + +The `useSigner()` hook returns: + +- `isConnected`: Whether the signer is connected and ready +- `connect()`: Triggers the authentication flow +- `disconnect()`: Disconnects from the signer +- `name`: Display name of the signer (e.g., "Para", "Turnkey", "MidenFi") + +This unified interface means your wallet UI code works the same regardless of which signer provider is used. + +--- + +### Para: EVM Wallet Integration + +[Para](https://para.space/) provides a modal-based authentication flow that allows users to sign in with their EVM wallets (MetaMask, WalletConnect, etc.). + +**Installation:** + +```bash +yarn add @miden-sdk/use-miden-para-react +``` + +**Usage:** + +```tsx +import { ParaSignerProvider } from '@miden-sdk/use-miden-para-react'; +import { MidenProvider, useSigner } from '@miden-sdk/react'; + +function App() { + return ( + + + + + + ); +} + +function Wallet() { + const signer = useSigner(); + + return ( +
+ {signer?.isConnected ? ( + + ) : ( + + )} +
+ ); +} +``` + +**ParaSignerProvider Props:** + +| Prop | Type | Description | +| ----------------------- | -------------------------------------------- | ------------------------------------------ | +| `apiKey` | `string` | Your Para API key | +| `environment` | `"PRODUCTION" \| "DEVELOPMENT" \| "SANDBOX"` | Para environment | +| `showSigningModal` | `boolean` | Whether to show signing confirmation modal | +| `customSignConfirmStep` | `ReactNode` | Custom signing confirmation UI | + +--- + +### Turnkey: App-Controlled Authentication + +[Turnkey](https://turnkey.com/) provides programmatic key management, giving your application full control over the authentication flow. + +**Installation:** + +```bash +yarn add @miden-sdk/miden-turnkey-react @turnkey/sdk-browser +``` + +**Usage:** + +```tsx +import { TurnkeySignerProvider } from '@miden-sdk/miden-turnkey-react'; +import { MidenProvider, useSigner } from '@miden-sdk/react'; + +function App() { + return ( + + + + + + ); +} + +function Wallet() { + const signer = useSigner(); + + return ( +
+ {signer?.isConnected ? ( + + ) : ( + + )} +
+ ); +} +``` + +Calling `connect()` handles the full Turnkey authentication flow: passkey login, wallet discovery, and account selection. No manual setup is needed. + +**TurnkeySignerProvider Props:** + +| Prop | Type | Description | +| -------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `config` | `Partial` | Optional. Defaults to `apiBaseUrl: "https://api.turnkey.com"` and `defaultOrganizationId` from `VITE_TURNKEY_ORG_ID` env var. | + +The `useTurnkeySigner()` hook is available for advanced use cases where you need direct access to the Turnkey `client`, the selected `account`, or the `setAccount()` method to manually control account selection. + +--- + +### MidenFi: Wallet Adapter + +[MidenFi](https://miden.fi/) provides a wallet adapter pattern similar to Solana's wallet-adapter, enabling integration with the MidenFi ecosystem. + +**Installation:** + +```bash +yarn add @miden-sdk/miden-wallet-adapter-react +``` + +**Usage:** + +```tsx +import { MidenFiSignerProvider } from '@miden-sdk/miden-wallet-adapter-react'; +import { MidenProvider, useSigner } from '@miden-sdk/react'; + +function App() { + return ( + + + + + + ); +} + +function Wallet() { + const signer = useSigner(); + + return ( +
+ {signer?.isConnected ? ( + + ) : ( + + )} +
+ ); +} +``` + +**MidenFiSignerProvider Props:** + +| Prop | Type | Description | +| ----------------------- | ------------------------ | -------------------------------------- | +| `network` | `"Testnet" \| "Mainnet"` | Target network | +| `privateDataPermission` | `boolean` | Whether to request private data access | +| `allowedPrivateData` | `string[]` | List of allowed private data types | + +--- + +### Building a Custom Signer Provider + +If you need to integrate with a different signing service, you can build your own signer provider by implementing the `SignerContextValue` interface and providing it via `SignerContext.Provider`. + +```tsx +import { useState, useCallback, type ReactNode } from 'react'; +import { SignerContext, type SignerContextValue } from '@miden-sdk/react'; +import { AccountStorageMode } from '@miden-sdk/miden-sdk'; + +interface CustomSignerProviderProps { + children: ReactNode; + // Your provider-specific config +} + +export function CustomSignerProvider({ children }: CustomSignerProviderProps) { + const [isConnected, setIsConnected] = useState(false); + const [signerContext, setSignerContext] = useState( + null, + ); + + const connect = useCallback(async () => { + // 1. Initialize your signing service and get credentials + const { publicKeyCommitment, signMessage } = + await initializeYourSigningService(); + + // 2. Build the signer context + const context: SignerContextValue = { + signCb: async (pubKey, signingInputs) => { + // Sign the message using your service + return signMessage(signingInputs); + }, + accountConfig: { + publicKeyCommitment, + accountType: 'RegularAccountImmutableCode', + storageMode: AccountStorageMode.public(), + }, + storeName: 'custom_signer', + name: 'CustomSigner', + isConnected: true, + connect, + disconnect, + }; + + setSignerContext(context); + setIsConnected(true); + }, []); + + const disconnect = useCallback(async () => { + setSignerContext(null); + setIsConnected(false); + }, []); + + return ( + + {children} + + ); +} +``` + +The `SignerContextValue` interface requires: + +| Field | Type | Description | +| --------------- | ------------------------------------------------ | --------------------------------------------------------------- | +| `signCb` | `(pubKey, signingInputs) => Promise` | Signs transaction inputs and returns the signature | +| `accountConfig` | `SignerAccountConfig` | Public key commitment, account type, and storage mode | +| `storeName` | `string` | Unique suffix for IndexedDB isolation (e.g., "custom_walletId") | +| `name` | `string` | Display name for UI (e.g., "CustomSigner") | +| `isConnected` | `boolean` | Whether the signer is connected and ready | +| `connect` | `() => Promise` | Triggers the authentication flow | +| `disconnect` | `() => Promise` | Disconnects from the signer | + +--- + +## Continue Learning + +Now that you've built a React wallet, explore these related topics: + +- [Creating Multiple Notes in a Single Transaction](./creating_multiple_notes_tutorial.md) - Learn about batch operations +- [Miden React SDK Reference](https://github.com/0xMiden/miden-client/tree/main/packages/react-sdk) - Full API documentation +- [Miden Documentation](https://docs.miden.io/) - Core Miden concepts diff --git a/docs/src/web-client/unauthenticated_note_how_to.md b/docs/src/web-client/unauthenticated_note_how_to.md index 8e661ef6..c5743552 100644 --- a/docs/src/web-client/unauthenticated_note_how_to.md +++ b/docs/src/web-client/unauthenticated_note_how_to.md @@ -3,6 +3,8 @@ title: 'How to Use Unauthenticated Notes' sidebar_position: 6 --- +import { CodeSdkTabs } from '@site/src/components'; + _Using unauthenticated notes for optimistic note consumption with the Miden WebClient_ ## Overview @@ -74,10 +76,12 @@ This tutorial assumes you have a basic understanding of Miden assembly. To quick cd miden-web-app ``` -3. Install the Miden WebClient SDK: - ```bash - yarn add @miden-sdk/miden-sdk@0.13.0 - ``` +3. Install the Miden SDK: + + **NOTE!**: Be sure to add the `--webpack` command to your `package.json` when running the `dev script`. The dev script should look like this: @@ -94,7 +98,22 @@ This tutorial assumes you have a basic understanding of Miden assembly. To quick Add the following code to the `app/page.tsx` file. This code defines the main page of our web application: +If you're using the **React SDK**, the page simply renders your self-contained component: + ```tsx +// app/page.tsx +'use client'; +import UnauthenticatedNoteTransfer from '../lib/react/unauthenticatedNoteTransfer'; + +export default function Home() { + return ; +} +``` + +If you're using the **TypeScript SDK**, the page manages state and calls the library function directly: + +```tsx +// app/page.tsx 'use client'; import { useState } from 'react'; import { unauthenticatedNoteTransfer } from '../lib/unauthenticatedNoteTransfer'; @@ -132,200 +151,289 @@ export default function Home() { ## Step 3: Create the Unauthenticated Note Transfer Implementation -Create the file `lib/unauthenticatedNoteTransfer.ts` and add the following code: +Create the library file and add the following code: ```bash mkdir -p lib -touch lib/unauthenticatedNoteTransfer.ts ``` -Copy and paste the following code into the `lib/unauthenticatedNoteTransfer.ts` file: +Copy and paste the following code into `lib/react/unauthenticatedNoteTransfer.tsx` (React) or `lib/unauthenticatedNoteTransfer.ts` (TypeScript): + + { +..// 1. Create Alice and 5 wallets for the transfer chain +..console.log('Creating accounts…'); +..const alice = await createWallet({ storageMode: 'public' }); +..const aliceId = alice.id().toString(); +..console.log('Alice account ID:', aliceId); + +..const walletIds: string[] = []; +..for (let i = 0; i < 5; i++) { +...const wallet = await createWallet({ storageMode: 'public' }); +...walletIds.push(wallet.id().toString()); +...console.log(\`Wallet \${i}:\`, walletIds[i]); +..} + +..// 2. Deploy a fungible faucet +..const faucet = await createFaucet({ +...tokenSymbol: 'MID', +...decimals: 8, +...maxSupply: BigInt(1_000_000), +...storageMode: 'public', +..}); +..const faucetId = faucet.id().toString(); +..console.log('Faucet ID:', faucetId); + +..// 3. Mint 10,000 MID to Alice +..const mintResult = await mint({ +...faucetId, +...targetAccountId: aliceId, +...amount: BigInt(10_000), +...noteType: 'public', +..}); + +..console.log('Waiting for settlement…'); +..await waitForCommit(mintResult.transactionId); + +..// 4. Consume the freshly minted notes +..const notes = await waitForConsumableNotes({ accountId: aliceId }); +..const noteIds = notes.map((n) => n.inputNoteRecord().id().toString()); +..await consume({ accountId: aliceId, noteIds }); + +..// 5. Create the unauthenticated note transfer chain: +..// Alice → Wallet 0 → Wallet 1 → Wallet 2 → Wallet 3 → Wallet 4 +..console.log('Starting unauthenticated transfer chain…'); +..const results = await transferChain({ +...from: aliceId, +...recipients: walletIds, +...assetId: faucetId, +...amount: BigInt(50), +...noteType: 'public', +..}); + +..results.forEach((r, i) => { +...console.log( +....\`Transfer \${i + 1}: https://testnet.midenscan.com/tx/\${r.consumeTransactionId}\`, +...); +..}); + +..console.log('Asset transfer chain completed ✅'); +.}; + +.return ( +..
+... +..
+.); +} -```ts -/** - * Demonstrates unauthenticated note transfer chain using a local prover on the Miden Network - * Creates a chain of P2ID (Pay to ID) notes: Alice → wallet 1 → wallet 2 → wallet 3 → wallet 4 - * - * @throws {Error} If the function cannot be executed in a browser environment - */ +export default function UnauthenticatedNoteTransfer() { +.return ( +.. +... +.. +.); +}`}, + typescript: { code:`/\*\* +.\* Demonstrates unauthenticated note transfer chain using a local prover on the Miden Network +.\* Creates a chain of P2ID (Pay to ID) notes: Alice → wallet 1 → wallet 2 → wallet 3 → wallet 4 +.\* +.\* @throws {Error} If the function cannot be executed in a browser environment +.\*/ export async function unauthenticatedNoteTransfer(): Promise { - // Ensure this runs only in a browser context - if (typeof window === 'undefined') return console.warn('Run in browser'); - - const { - WebClient, - AccountStorageMode, - AuthScheme, - NoteType, - TransactionProver, - Note, - NoteAssets, - OutputNoteArray, - FungibleAsset, - NoteAndArgsArray, - NoteAndArgs, - NoteAttachment, - TransactionRequestBuilder, - OutputNote, - } = await import('@miden-sdk/miden-sdk'); - - const client = await WebClient.createClient('https://rpc.testnet.miden.io'); - const prover = TransactionProver.newLocalProver(); - - console.log('Latest block:', (await client.syncState()).blockNum()); - - // ── Creating new account ────────────────────────────────────────────────────── - console.log('Creating accounts'); - - console.log('Creating account for Alice…'); - const alice = await client.newWallet( - AccountStorageMode.public(), - true, - AuthScheme.AuthRpoFalcon512, - ); - console.log('Alice accout ID:', alice.id().toString()); - - const wallets = []; - for (let i = 0; i < 5; i++) { - const wallet = await client.newWallet( - AccountStorageMode.public(), - true, - AuthScheme.AuthRpoFalcon512, - ); - wallets.push(wallet); - console.log('wallet ', i.toString(), wallet.id().toString()); - } - - // ── Creating new faucet ────────────────────────────────────────────────────── - const faucet = await client.newFaucet( - AccountStorageMode.public(), - false, - 'MID', - 8, - BigInt(1_000_000), - AuthScheme.AuthRpoFalcon512, - ); - console.log('Faucet ID:', faucet.id().toString()); - - // ── mint 10 000 MID to Alice ────────────────────────────────────────────────────── - { - const txResult = await client.executeTransaction( - faucet.id(), - client.newMintTransactionRequest( - alice.id(), - faucet.id(), - NoteType.Public, - BigInt(10_000), - ), - ); - const proven = await client.proveTransaction(txResult, prover); - const submissionHeight = await client.submitProvenTransaction( - proven, - txResult, - ); - await client.applyTransaction(txResult, submissionHeight); - } - - console.log('Waiting for settlement'); - await new Promise((r) => setTimeout(r, 7_000)); - await client.syncState(); - - // ── Consume the freshly minted note ────────────────────────────────────────────── - const noteList = (await client.getConsumableNotes(alice.id())).map((rec) => - rec.inputNoteRecord().toNote(), - ); - - { - const txResult = await client.executeTransaction( - alice.id(), - client.newConsumeTransactionRequest(noteList), - ); - const proven = await client.proveTransaction(txResult, prover); - const submissionHeight = await client.submitProvenTransaction( - proven, - txResult, - ); - await client.applyTransaction(txResult, submissionHeight); - await client.syncState(); - } - - // ── Create unauthenticated note transfer chain ───────────────────────────────────────────── - // Alice → wallet 1 → wallet 2 → wallet 3 → wallet 4 - for (let i = 0; i < wallets.length; i++) { - console.log(`\nUnauthenticated tx ${i + 1}`); - - // Determine sender and receiver for this iteration - const sender = i === 0 ? alice : wallets[i - 1]; - const receiver = wallets[i]; - - console.log('Sender:', sender.id().toString()); - console.log('Receiver:', receiver.id().toString()); - - const assets = new NoteAssets([new FungibleAsset(faucet.id(), BigInt(50))]); - const p2idNote = Note.createP2IDNote( - sender.id(), - receiver.id(), - assets, - NoteType.Public, - new NoteAttachment(), - ); - - const outputP2ID = OutputNote.full(p2idNote); - - console.log('Creating P2ID note...'); - { - const txResult = await client.executeTransaction( - sender.id(), - new TransactionRequestBuilder() - .withOwnOutputNotes(new OutputNoteArray([outputP2ID])) - .build(), - ); - const proven = await client.proveTransaction(txResult, prover); - const submissionHeight = await client.submitProvenTransaction( - proven, - txResult, - ); - await client.applyTransaction(txResult, submissionHeight); - } - - console.log('Consuming P2ID note...'); - - const noteIdAndArgs = new NoteAndArgs(p2idNote, null); - - const consumeRequest = new TransactionRequestBuilder() - .withInputNotes(new NoteAndArgsArray([noteIdAndArgs])) - .build(); - - { - const txResult = await client.executeTransaction( - receiver.id(), - consumeRequest, - ); - const proven = await client.proveTransaction(txResult, prover); - const submissionHeight = await client.submitProvenTransaction( - proven, - txResult, - ); - const txExecutionResult = await client.applyTransaction( - txResult, - submissionHeight, - ); - - const txId = txExecutionResult - .executedTransaction() - .id() - .toHex() - .toString(); - - console.log( - `Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/${txId}`, - ); - } - } - - console.log('Asset transfer chain completed ✅'); -} -``` +.// Ensure this runs only in a browser context +.if (typeof window === 'undefined') return console.warn('Run in browser'); + +.const { +..WebClient, +..AccountStorageMode, +..AuthScheme, +..NoteType, +..TransactionProver, +..Note, +..NoteAssets, +..OutputNoteArray, +..FungibleAsset, +..NoteAndArgsArray, +..NoteAndArgs, +..NoteAttachment, +..TransactionRequestBuilder, +..OutputNote, +.} = await import('@miden-sdk/miden-sdk'); + +.const client = await WebClient.createClient('https://rpc.testnet.miden.io'); +.const prover = TransactionProver.newLocalProver(); + +.console.log('Latest block:', (await client.syncState()).blockNum()); + +.// ── Creating new account ────────────────────────────────────────────────────── +.console.log('Creating accounts'); + +.console.log('Creating account for Alice…'); +.const alice = await client.newWallet( +..AccountStorageMode.public(), +..true, +..AuthScheme.AuthRpoFalcon512, +.); +.console.log('Alice account ID:', alice.id().toString()); + +.const wallets = []; +.for (let i = 0; i < 5; i++) { +..const wallet = await client.newWallet( +...AccountStorageMode.public(), +...true, +...AuthScheme.AuthRpoFalcon512, +..); +..wallets.push(wallet); +..console.log('wallet ', i.toString(), wallet.id().toString()); +.} + +.// ── Creating new faucet ────────────────────────────────────────────────────── +.const faucet = await client.newFaucet( +..AccountStorageMode.public(), +..false, +..'MID', +..8, +..BigInt(1_000_000), +..AuthScheme.AuthRpoFalcon512, +.); +.console.log('Faucet ID:', faucet.id().toString()); + +.// ── mint 10 000 MID to Alice ────────────────────────────────────────────────────── +.{ +..const txResult = await client.executeTransaction( +...faucet.id(), +...client.newMintTransactionRequest( +....alice.id(), +....faucet.id(), +....NoteType.Public, +....BigInt(10_000), +...), +..); +..const proven = await client.proveTransaction(txResult, prover); +..const submissionHeight = await client.submitProvenTransaction( +...proven, +...txResult, +..); +..await client.applyTransaction(txResult, submissionHeight); +.} + +.console.log('Waiting for settlement'); +.await new Promise((r) => setTimeout(r, 7_000)); +.await client.syncState(); + +.// ── Consume the freshly minted note ────────────────────────────────────────────── +.const noteList = (await client.getConsumableNotes(alice.id())).map((rec) => +..rec.inputNoteRecord().toNote(), +.); + +.{ +..const txResult = await client.executeTransaction( +...alice.id(), +...client.newConsumeTransactionRequest(noteList), +..); +..const proven = await client.proveTransaction(txResult, prover); +..const submissionHeight = await client.submitProvenTransaction( +...proven, +...txResult, +..); +..await client.applyTransaction(txResult, submissionHeight); +..await client.syncState(); +.} + +.// ── Create unauthenticated note transfer chain ───────────────────────────────────────────── +.// Alice → wallet 1 → wallet 2 → wallet 3 → wallet 4 +.for (let i = 0; i < wallets.length; i++) { +..console.log(\`\\nUnauthenticated tx \${i + 1}\`); + +..// Determine sender and receiver for this iteration +..const sender = i === 0 ? alice : wallets[i - 1]; +..const receiver = wallets[i]; + +..console.log('Sender:', sender.id().toString()); +..console.log('Receiver:', receiver.id().toString()); + +..const assets = new NoteAssets([new FungibleAsset(faucet.id(), BigInt(50))]); +..const p2idNote = Note.createP2IDNote( +...sender.id(), +...receiver.id(), +...assets, +...NoteType.Public, +...new NoteAttachment(), +..); + +..const outputP2ID = OutputNote.full(p2idNote); + +..console.log('Creating P2ID note...'); +..{ +...const builder = new TransactionRequestBuilder(); +...const request = builder.withOwnOutputNotes(new OutputNoteArray([outputP2ID])).build(); +...const txResult = await client.executeTransaction( +....sender.id(), +....request, +...); +...const proven = await client.proveTransaction(txResult, prover); +...const submissionHeight = await client.submitProvenTransaction( +....proven, +....txResult, +...); +...await client.applyTransaction(txResult, submissionHeight); +..} + +..console.log('Consuming P2ID note...'); + +..const noteIdAndArgs = new NoteAndArgs(p2idNote, null); + +..const consumeBuilder = new TransactionRequestBuilder(); +..const consumeRequest = consumeBuilder.withInputNotes(new NoteAndArgsArray([noteIdAndArgs])).build(); + +..{ +...const txResult = await client.executeTransaction( +....receiver.id(), +....consumeRequest, +...); +...const proven = await client.proveTransaction(txResult, prover); +...const submissionHeight = await client.submitProvenTransaction( +....proven, +....txResult, +...); +...const txExecutionResult = await client.applyTransaction( +....txResult, +....submissionHeight, +...); + +...const txId = txExecutionResult +....executedTransaction() +....id() +....toHex() +....toString(); + +...console.log( +....\`Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/\${txId}\`, +...); +..} + +.} + +.console.log('Asset transfer chain completed ✅'); +}` }, +}} reactFilename="lib/react/unauthenticatedNoteTransfer.tsx" tsFilename="lib/unauthenticatedNoteTransfer.ts" /> ## Key Concepts: Unauthenticated Notes @@ -418,7 +526,7 @@ Wallet 5 balance: 50 MID Unauthenticated notes on Miden offer a powerful mechanism for achieving faster asset settlements by allowing notes to be both created and consumed within the same block. In this guide, we walked through: -- **Setting up the Miden WebClient** with delegated proving for optimal performance +- **Setting up the Miden WebClient** with a local prover - **Creating P2ID Notes** for targeted asset transfers between specific accounts - **Building Transaction Chains** using unauthenticated input notes for sub-blocktime settlement - **Performance Observations** demonstrating how unauthenticated notes enable faster-than-blocktime transfers diff --git a/rust-client/src/bin/counter_contract_deploy.rs b/rust-client/src/bin/counter_contract_deploy.rs index 73d65568..012ee032 100644 --- a/rust-client/src/bin/counter_contract_deploy.rs +++ b/rust-client/src/bin/counter_contract_deploy.rs @@ -39,7 +39,7 @@ fn create_library( #[tokio::main] async fn main() -> Result<(), ClientError> { // Initialize client - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/rust-client/src/bin/counter_contract_fpi.rs b/rust-client/src/bin/counter_contract_fpi.rs index 8fa69675..34e26a50 100644 --- a/rust-client/src/bin/counter_contract_fpi.rs +++ b/rust-client/src/bin/counter_contract_fpi.rs @@ -39,7 +39,7 @@ fn create_library( #[tokio::main] async fn main() -> Result<(), ClientError> { // Initialize client - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/rust-client/src/bin/counter_contract_increment.rs b/rust-client/src/bin/counter_contract_increment.rs index 732fdf6c..5fbd7996 100644 --- a/rust-client/src/bin/counter_contract_increment.rs +++ b/rust-client/src/bin/counter_contract_increment.rs @@ -32,7 +32,7 @@ fn create_library( #[tokio::main] async fn main() -> Result<(), ClientError> { // Initialize client - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/rust-client/src/bin/create_mint_consume_send.rs b/rust-client/src/bin/create_mint_consume_send.rs index 5995673e..270d0d49 100644 --- a/rust-client/src/bin/create_mint_consume_send.rs +++ b/rust-client/src/bin/create_mint_consume_send.rs @@ -23,7 +23,7 @@ use miden_protocol::account::AccountIdVersion; #[tokio::main] async fn main() -> Result<(), ClientError> { // Initialize client - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/rust-client/src/bin/delegated_prover.rs b/rust-client/src/bin/delegated_prover.rs index 83666361..43163f92 100644 --- a/rust-client/src/bin/delegated_prover.rs +++ b/rust-client/src/bin/delegated_prover.rs @@ -18,7 +18,7 @@ use miden_client_sqlite_store::ClientBuilderSqliteExt; #[tokio::main] async fn main() -> Result<(), ClientError> { // Initialize client - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/rust-client/src/bin/hash_preimage_note.rs b/rust-client/src/bin/hash_preimage_note.rs index 6039426a..3d52b2bc 100644 --- a/rust-client/src/bin/hash_preimage_note.rs +++ b/rust-client/src/bin/hash_preimage_note.rs @@ -107,7 +107,7 @@ async fn wait_for_tx( #[tokio::main] async fn main() -> Result<(), ClientError> { // Initialize client - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/rust-client/src/bin/mapping_example.rs b/rust-client/src/bin/mapping_example.rs index 6523ba27..d0ebc984 100644 --- a/rust-client/src/bin/mapping_example.rs +++ b/rust-client/src/bin/mapping_example.rs @@ -38,7 +38,7 @@ fn create_library( #[tokio::main] async fn main() -> Result<(), ClientError> { // Initialize client - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/rust-client/src/bin/network_notes_counter_contract.rs b/rust-client/src/bin/network_notes_counter_contract.rs index c18ebc20..669765c1 100644 --- a/rust-client/src/bin/network_notes_counter_contract.rs +++ b/rust-client/src/bin/network_notes_counter_contract.rs @@ -80,7 +80,7 @@ fn create_library( #[tokio::main] async fn main() -> Result<(), Box> { // Initialize client - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/rust-client/src/bin/note_creation_in_masm.rs b/rust-client/src/bin/note_creation_in_masm.rs index 68160616..0b705d63 100644 --- a/rust-client/src/bin/note_creation_in_masm.rs +++ b/rust-client/src/bin/note_creation_in_masm.rs @@ -97,7 +97,7 @@ async fn wait_for_notes( #[tokio::main] async fn main() -> Result<(), ClientError> { // Initialize client - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/rust-client/src/bin/oracle_data_query.rs b/rust-client/src/bin/oracle_data_query.rs index e45598f8..7b3688df 100644 --- a/rust-client/src/bin/oracle_data_query.rs +++ b/rust-client/src/bin/oracle_data_query.rs @@ -138,7 +138,7 @@ async fn main() -> Result<(), ClientError> { // ------------------------------------------------------------------------- // Initialize Client // ------------------------------------------------------------------------- - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/rust-client/src/bin/unauthenticated_note_transfer.rs b/rust-client/src/bin/unauthenticated_note_transfer.rs index dc2d0d4e..5e64609e 100644 --- a/rust-client/src/bin/unauthenticated_note_transfer.rs +++ b/rust-client/src/bin/unauthenticated_note_transfer.rs @@ -56,7 +56,7 @@ async fn wait_for_tx( #[tokio::main] async fn main() -> Result<(), ClientError> { // Initialize client - let endpoint = Endpoint::devnet(); + let endpoint = Endpoint::testnet(); let timeout_ms = 10_000; let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); diff --git a/web-client/lib/createMintConsume.ts b/web-client/lib/createMintConsume.ts index 75c9123c..a7612fd0 100644 --- a/web-client/lib/createMintConsume.ts +++ b/web-client/lib/createMintConsume.ts @@ -14,7 +14,7 @@ export async function createMintConsume(): Promise { Address, } = await import('@miden-sdk/miden-sdk'); - const nodeEndpoint = 'https://rpc.devnet.miden.io'; + const nodeEndpoint = 'https://rpc.testnet.miden.io'; const client = await WebClient.createClient(nodeEndpoint); // 1. Sync with the latest blockchain state diff --git a/web-client/lib/foreignProcedureInvocation.ts b/web-client/lib/foreignProcedureInvocation.ts index 91b0efe9..79c2eda2 100644 --- a/web-client/lib/foreignProcedureInvocation.ts +++ b/web-client/lib/foreignProcedureInvocation.ts @@ -21,7 +21,7 @@ export async function foreignProcedureInvocation(): Promise { AccountStorageMode, } = await import('@miden-sdk/miden-sdk'); - const nodeEndpoint = 'https://rpc.devnet.miden.io'; + const nodeEndpoint = 'https://rpc.testnet.miden.io'; const client = await WebClient.createClient(nodeEndpoint); console.log('Current block number: ', (await client.syncState()).blockNum()); diff --git a/web-client/lib/incrementCounterContract.ts b/web-client/lib/incrementCounterContract.ts index 4cfb6480..1ed7b7e2 100644 --- a/web-client/lib/incrementCounterContract.ts +++ b/web-client/lib/incrementCounterContract.ts @@ -18,7 +18,7 @@ export async function incrementCounterContract(): Promise { WebClient, } = await import('@miden-sdk/miden-sdk'); - const nodeEndpoint = 'https://rpc.devnet.miden.io'; + const nodeEndpoint = 'https://rpc.testnet.miden.io'; const client = await WebClient.createClient(nodeEndpoint); console.log('Current block number: ', (await client.syncState()).blockNum()); diff --git a/web-client/lib/mintTestnetToAddress.ts b/web-client/lib/mintTestnetToAddress.ts new file mode 100644 index 00000000..6ed18233 --- /dev/null +++ b/web-client/lib/mintTestnetToAddress.ts @@ -0,0 +1,70 @@ +/** + * Mint 100 MIDEN tokens on testnet to a fixed recipient using a local prover. + */ +export async function mintTestnetToAddress(): Promise { + if (typeof window === 'undefined') { + console.warn('Run in browser'); + return; + } + + const { + WebClient, + AccountStorageMode, + AuthScheme, + Address, + NoteType, + TransactionProver, + } = await import('@miden-sdk/miden-sdk'); + + const client = await WebClient.createClient('https://rpc.testnet.miden.io'); + const prover = TransactionProver.newLocalProver(); + + console.log('Latest block:', (await client.syncState()).blockNum()); + + // ── Create a faucet ──────────────────────────────────────────────────────── + console.log('Creating faucet...'); + const faucet = await client.newFaucet( + AccountStorageMode.public(), + false, + 'MID', + 8, + BigInt(1_000_000), + AuthScheme.AuthRpoFalcon512, + ); + console.log('Faucet ID:', faucet.id().toString()); + await client.syncState(); + + // ── Mint to recipient ─────────────────────────────────────────────────────── + const recipientAddress = + 'mtst1apve54rq8ux0jqqqqrkh5y0r0y8cwza6_qruqqypuyph'; + const recipientAccountId = Address.fromBech32(recipientAddress).accountId(); + console.log('Recipient account ID:', recipientAccountId.toString()); + + console.log('Minting 100 MIDEN tokens...'); + const txResult = await client.executeTransaction( + faucet.id(), + client.newMintTransactionRequest( + recipientAccountId, + faucet.id(), + NoteType.Public, + BigInt(100), + ), + ); + const proven = await client.proveTransaction(txResult, prover); + const submissionHeight = await client.submitProvenTransaction( + proven, + txResult, + ); + const txExecutionResult = await client.applyTransaction( + txResult, + submissionHeight, + ); + + console.log('Waiting for settlement...'); + await new Promise((resolve) => setTimeout(resolve, 7_000)); + await client.syncState(); + + const txId = txExecutionResult.executedTransaction().id().toHex().toString(); + console.log('Mint tx id:', txId); + console.log('Mint complete.'); +} diff --git a/web-client/lib/multiSendWithDelegatedProver.ts b/web-client/lib/multiSendWithDelegatedProver.ts index 1f28a2de..08a2badb 100644 --- a/web-client/lib/multiSendWithDelegatedProver.ts +++ b/web-client/lib/multiSendWithDelegatedProver.ts @@ -1,5 +1,5 @@ /** - * Demonstrates multi-send functionality using a local prover on the Miden Network + * Demonstrates multi-send functionality with delegated proving on the Miden Network * Creates multiple P2ID (Pay to ID) notes for different recipients * * @throws {Error} If the function cannot be executed in a browser environment @@ -14,7 +14,6 @@ export async function multiSendWithDelegatedProver(): Promise { AuthScheme, Address, NoteType, - TransactionProver, Note, NoteAssets, OutputNoteArray, @@ -24,8 +23,7 @@ export async function multiSendWithDelegatedProver(): Promise { OutputNote, } = await import('@miden-sdk/miden-sdk'); - const client = await WebClient.createClient('https://rpc.devnet.miden.io'); - const prover = TransactionProver.newLocalProver(); + const client = await WebClient.createClient('https://rpc.testnet.miden.io'); console.log('Latest block:', (await client.syncState()).blockNum()); @@ -36,7 +34,7 @@ export async function multiSendWithDelegatedProver(): Promise { true, AuthScheme.AuthRpoFalcon512, ); - console.log('Alice accout ID:', alice.id().toString()); + console.log('Alice account ID:', alice.id().toString()); // ── Creating new faucet ────────────────────────────────────────────────────── const faucet = await client.newFaucet( @@ -50,46 +48,29 @@ export async function multiSendWithDelegatedProver(): Promise { console.log('Faucet ID:', faucet.id().toString()); // ── mint 10 000 MID to Alice ────────────────────────────────────────────────────── - { - const txResult = await client.executeTransaction( + await client.submitNewTransaction( + faucet.id(), + client.newMintTransactionRequest( + alice.id(), faucet.id(), - client.newMintTransactionRequest( - alice.id(), - faucet.id(), - NoteType.Public, - BigInt(10_000), - ), - ); - const proven = await client.proveTransaction(txResult, prover); - const submissionHeight = await client.submitProvenTransaction( - proven, - txResult, - ); - await client.applyTransaction(txResult, submissionHeight); + NoteType.Public, + BigInt(10_000), + ), + ); - console.log('waiting for settlement'); - await new Promise((r) => setTimeout(r, 7_000)); - await client.syncState(); - } + console.log('waiting for settlement'); + await new Promise((r) => setTimeout(r, 7_000)); + await client.syncState(); // ── consume the freshly minted notes ────────────────────────────────────────────── const noteList = (await client.getConsumableNotes(alice.id())).map((rec) => rec.inputNoteRecord().toNote(), ); - { - const txResult = await client.executeTransaction( - alice.id(), - client.newConsumeTransactionRequest(noteList), - ); - const proven = await client.proveTransaction(txResult, prover); - await client.syncState(); - const submissionHeight = await client.submitProvenTransaction( - proven, - txResult, - ); - await client.applyTransaction(txResult, submissionHeight); - } + await client.submitNewTransaction( + alice.id(), + client.newConsumeTransactionRequest(noteList), + ); // ── build 3 P2ID notes (100 MID each) ───────────────────────────────────────────── const recipientAddresses = [ @@ -114,12 +95,9 @@ export async function multiSendWithDelegatedProver(): Promise { }); // ── create all P2ID notes ─────────────────────────────────────────────────────────────── - await client.submitNewTransaction( - alice.id(), - new TransactionRequestBuilder() - .withOwnOutputNotes(new OutputNoteArray(p2idNotes)) - .build(), - ); + const builder = new TransactionRequestBuilder(); + const txRequest = builder.withOwnOutputNotes(new OutputNoteArray(p2idNotes)).build(); + await client.submitNewTransaction(alice.id(), txRequest); console.log('All notes created ✅'); } diff --git a/web-client/lib/react/createMintConsume.tsx b/web-client/lib/react/createMintConsume.tsx new file mode 100644 index 00000000..29775121 --- /dev/null +++ b/web-client/lib/react/createMintConsume.tsx @@ -0,0 +1,88 @@ +// Documentation-only example for the "Mint, Consume, and Create Notes" tutorial. +// This component is embedded in docs via CodeSdkTabs and is not wired into the +// test harness (app/page.tsx). The TypeScript equivalent in lib/createMintConsume.ts +// is used for Playwright tests instead. +'use client'; + +import { MidenProvider, useMiden, useCreateWallet, useCreateFaucet, useMint, useConsume, useSend, useWaitForCommit, useWaitForNotes } from '@miden-sdk/react'; + +function CreateMintConsumeInner() { + const { isReady } = useMiden(); + const { createWallet } = useCreateWallet(); + const { createFaucet } = useCreateFaucet(); + const { mint } = useMint(); + const { consume } = useConsume(); + const { send } = useSend(); + const { waitForCommit } = useWaitForCommit(); + const { waitForConsumableNotes } = useWaitForNotes(); + + const run = async () => { + // 1. Create Alice's wallet (public, mutable) + console.log('Creating account for Alice…'); + const alice = await createWallet({ storageMode: 'public' }); + const aliceId = alice.id().toString(); + console.log('Alice ID:', aliceId); + + // 2. Deploy a fungible faucet + console.log('Creating faucet…'); + const faucet = await createFaucet({ + tokenSymbol: 'MID', + decimals: 8, + maxSupply: BigInt(1_000_000), + storageMode: 'public', + }); + const faucetId = faucet.id().toString(); + console.log('Faucet ID:', faucetId); + + // 3. Mint 1000 tokens to Alice + console.log('Minting tokens to Alice...'); + const mintResult = await mint({ + faucetId, + targetAccountId: aliceId, + amount: BigInt(1000), + noteType: 'public', + }); + console.log('Mint tx:', mintResult.transactionId); + + // 4. Wait for the mint transaction to be committed + await waitForCommit(mintResult.transactionId); + + // 5. Wait for consumable notes to appear + const notes = await waitForConsumableNotes({ accountId: aliceId }); + const noteIds = notes.map((n) => n.inputNoteRecord().id().toString()); + console.log('Consumable notes:', noteIds); + + // 6. Consume minted notes + console.log('Consuming minted notes...'); + await consume({ accountId: aliceId, noteIds }); + console.log('Notes consumed.'); + + // 7. Send 100 tokens to Bob + const bobAddress = 'mtst1apve54rq8ux0jqqqqrkh5y0r0y8cwza6_qruqqypuyph'; + console.log("Sending tokens to Bob's account..."); + await send({ + from: aliceId, + to: bobAddress, + assetId: faucetId, + amount: BigInt(100), + noteType: 'public', + }); + console.log('Tokens sent successfully!'); + }; + + return ( +
+ +
+ ); +} + +export default function CreateMintConsume() { + return ( + + + + ); +} diff --git a/web-client/lib/react/multiSendWithDelegatedProver.tsx b/web-client/lib/react/multiSendWithDelegatedProver.tsx new file mode 100644 index 00000000..4226acb8 --- /dev/null +++ b/web-client/lib/react/multiSendWithDelegatedProver.tsx @@ -0,0 +1,82 @@ +// Documentation-only example for the "Creating Multiple Notes" tutorial. +// This component is embedded in docs via CodeSdkTabs and is not wired into the +// test harness (app/page.tsx). The TypeScript equivalent in +// lib/multiSendWithDelegatedProver.ts is used for Playwright tests instead. +'use client'; + +import { MidenProvider, useMiden, useCreateWallet, useCreateFaucet, useMint, useConsume, useMultiSend, useWaitForCommit, useWaitForNotes } from '@miden-sdk/react'; + +function MultiSendInner() { + const { isReady } = useMiden(); + const { createWallet } = useCreateWallet(); + const { createFaucet } = useCreateFaucet(); + const { mint } = useMint(); + const { consume } = useConsume(); + const { sendMany } = useMultiSend(); + const { waitForCommit } = useWaitForCommit(); + const { waitForConsumableNotes } = useWaitForNotes(); + + const run = async () => { + // 1. Create Alice's wallet + console.log('Creating account for Alice…'); + const alice = await createWallet({ storageMode: 'public' }); + const aliceId = alice.id().toString(); + console.log('Alice account ID:', aliceId); + + // 2. Deploy a fungible faucet + const faucet = await createFaucet({ + tokenSymbol: 'MID', + decimals: 8, + maxSupply: BigInt(1_000_000), + storageMode: 'public', + }); + const faucetId = faucet.id().toString(); + console.log('Faucet ID:', faucetId); + + // 3. Mint 10,000 MID to Alice + const mintResult = await mint({ + faucetId, + targetAccountId: aliceId, + amount: BigInt(10_000), + noteType: 'public', + }); + + console.log('Waiting for settlement…'); + await waitForCommit(mintResult.transactionId); + + // 4. Consume the freshly minted notes + const notes = await waitForConsumableNotes({ accountId: aliceId }); + const noteIds = notes.map((n) => n.inputNoteRecord().id().toString()); + await consume({ accountId: aliceId, noteIds }); + + // 5. Send 100 MID to three recipients in a single transaction + await sendMany({ + from: aliceId, + assetId: faucetId, + recipients: [ + { to: 'mtst1aqezqc90x7dkzypr9m5fmlpp85w6cl04', amount: BigInt(100) }, + { to: 'mtst1apjg2ul76wrkxyr5qlcnczaskypa4ljn', amount: BigInt(100) }, + { to: 'mtst1arpee6y9cm8t7ypn33pc8fzj6gkzz7kd', amount: BigInt(100) }, + ], + noteType: 'public', + }); + + console.log('All notes created ✅'); + }; + + return ( +
+ +
+ ); +} + +export default function MultiSendWithDelegatedProver() { + return ( + + + + ); +} diff --git a/web-client/lib/react/unauthenticatedNoteTransfer.tsx b/web-client/lib/react/unauthenticatedNoteTransfer.tsx new file mode 100644 index 00000000..e3c28dba --- /dev/null +++ b/web-client/lib/react/unauthenticatedNoteTransfer.tsx @@ -0,0 +1,94 @@ +// Documentation-only example for the "Unauthenticated Note Transfer" tutorial. +// This component is embedded in docs via CodeSdkTabs and is not wired into the +// test harness (app/page.tsx). The TypeScript equivalent in +// lib/unauthenticatedNoteTransfer.ts is used for Playwright tests instead. +'use client'; + +import { MidenProvider, useMiden, useCreateWallet, useCreateFaucet, useMint, useConsume, useInternalTransfer, useWaitForCommit, useWaitForNotes } from '@miden-sdk/react'; + +function UnauthenticatedNoteTransferInner() { + const { isReady } = useMiden(); + const { createWallet } = useCreateWallet(); + const { createFaucet } = useCreateFaucet(); + const { mint } = useMint(); + const { consume } = useConsume(); + const { transferChain } = useInternalTransfer(); + const { waitForCommit } = useWaitForCommit(); + const { waitForConsumableNotes } = useWaitForNotes(); + + const run = async () => { + // 1. Create Alice and 5 wallets for the transfer chain + console.log('Creating accounts…'); + const alice = await createWallet({ storageMode: 'public' }); + const aliceId = alice.id().toString(); + console.log('Alice account ID:', aliceId); + + const walletIds: string[] = []; + for (let i = 0; i < 5; i++) { + const wallet = await createWallet({ storageMode: 'public' }); + walletIds.push(wallet.id().toString()); + console.log(`Wallet ${i}:`, walletIds[i]); + } + + // 2. Deploy a fungible faucet + const faucet = await createFaucet({ + tokenSymbol: 'MID', + decimals: 8, + maxSupply: BigInt(1_000_000), + storageMode: 'public', + }); + const faucetId = faucet.id().toString(); + console.log('Faucet ID:', faucetId); + + // 3. Mint 10,000 MID to Alice + const mintResult = await mint({ + faucetId, + targetAccountId: aliceId, + amount: BigInt(10_000), + noteType: 'public', + }); + + console.log('Waiting for settlement…'); + await waitForCommit(mintResult.transactionId); + + // 4. Consume the freshly minted notes + const notes = await waitForConsumableNotes({ accountId: aliceId }); + const noteIds = notes.map((n) => n.inputNoteRecord().id().toString()); + await consume({ accountId: aliceId, noteIds }); + + // 5. Create the unauthenticated note transfer chain: + // Alice → Wallet 0 → Wallet 1 → Wallet 2 → Wallet 3 → Wallet 4 + console.log('Starting unauthenticated transfer chain…'); + const results = await transferChain({ + from: aliceId, + recipients: walletIds, + assetId: faucetId, + amount: BigInt(50), + noteType: 'public', + }); + + results.forEach((r, i) => { + console.log( + `Transfer ${i + 1}: https://testnet.midenscan.com/tx/${r.consumeTransactionId}`, + ); + }); + + console.log('Asset transfer chain completed ✅'); + }; + + return ( +
+ +
+ ); +} + +export default function UnauthenticatedNoteTransfer() { + return ( + + + + ); +} diff --git a/web-client/lib/unauthenticatedNoteTransfer.ts b/web-client/lib/unauthenticatedNoteTransfer.ts index 4f71ae14..fb7fdc98 100644 --- a/web-client/lib/unauthenticatedNoteTransfer.ts +++ b/web-client/lib/unauthenticatedNoteTransfer.ts @@ -25,7 +25,7 @@ export async function unauthenticatedNoteTransfer(): Promise { OutputNote, } = await import('@miden-sdk/miden-sdk'); - const client = await WebClient.createClient('https://rpc.devnet.miden.io'); + const client = await WebClient.createClient('https://rpc.testnet.miden.io'); const prover = TransactionProver.newLocalProver(); console.log('Latest block:', (await client.syncState()).blockNum()); @@ -39,7 +39,7 @@ export async function unauthenticatedNoteTransfer(): Promise { true, AuthScheme.AuthRpoFalcon512, ); - console.log('Alice accout ID:', alice.id().toString()); + console.log('Alice account ID:', alice.id().toString()); const wallets = []; for (let i = 0; i < 5; i++) { @@ -130,11 +130,11 @@ export async function unauthenticatedNoteTransfer(): Promise { console.log('Creating P2ID note...'); { + const builder = new TransactionRequestBuilder(); + const request = builder.withOwnOutputNotes(new OutputNoteArray([outputP2ID])).build(); const txResult = await client.executeTransaction( sender.id(), - new TransactionRequestBuilder() - .withOwnOutputNotes(new OutputNoteArray([outputP2ID])) - .build(), + request, ); const proven = await client.proveTransaction(txResult, prover); const submissionHeight = await client.submitProvenTransaction( @@ -148,9 +148,8 @@ export async function unauthenticatedNoteTransfer(): Promise { const noteIdAndArgs = new NoteAndArgs(p2idNote, null); - const consumeRequest = new TransactionRequestBuilder() - .withInputNotes(new NoteAndArgsArray([noteIdAndArgs])) - .build(); + const consumeBuilder = new TransactionRequestBuilder(); + const consumeRequest = consumeBuilder.withInputNotes(new NoteAndArgsArray([noteIdAndArgs])).build(); { const txResult = await client.executeTransaction( diff --git a/web-client/package.json b/web-client/package.json index 63fe0c14..2a78ff23 100644 --- a/web-client/package.json +++ b/web-client/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@miden-sdk/miden-sdk": "0.13.0", + "@miden-sdk/react": "0.13.2", "next": "15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0"