├── docs ├── masm ├── .gitignore ├── static │ └── img │ │ ├── logo.png │ │ ├── favicon.ico │ │ └── custom_caret.svg ├── src │ ├── img │ │ ├── note_creation_masm.png │ │ └── count_copy_fpi_diagram.png │ ├── rust-client │ │ ├── _category_.yml │ │ ├── index.md │ │ └── delegated_proving_tutorial.md │ ├── web-client │ │ ├── _category_.yml │ │ ├── .prettierrc │ │ ├── index.md │ │ ├── mint_consume_create_tutorial.md │ │ └── create_deploy_tutorial.md │ ├── _category_.yml │ ├── index.md │ ├── theme │ │ └── Admonition │ │ │ ├── Icon │ │ │ ├── Note.tsx │ │ │ ├── Info.tsx │ │ │ ├── Tip.tsx │ │ │ ├── Warning.tsx │ │ │ └── Danger.tsx │ │ │ ├── Layout │ │ │ ├── styles.module.css │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── Type │ │ │ ├── Tip.tsx │ │ │ ├── Info.tsx │ │ │ ├── Note.tsx │ │ │ ├── Danger.tsx │ │ │ ├── Warning.tsx │ │ │ └── Caution.tsx │ │ │ └── Types.tsx │ ├── lib.rs │ └── miden_node_setup.md ├── sidebars.ts ├── tsconfig.json ├── package.json ├── Cargo.toml └── docusaurus.config.ts ├── .gitignore ├── web-client ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── postcss.config.mjs ├── .prettierrc ├── package.json ├── .gitignore ├── next.config.ts ├── tsconfig.json ├── README.md └── lib │ ├── createMintConsume.ts │ ├── multiSendWithDelegatedProver.ts │ ├── incrementCounterContract.ts │ ├── unauthenticatedNoteTransfer.ts │ └── foreignProcedureInvocation.ts ├── masm ├── accounts │ ├── auth │ │ └── no_auth.masm │ ├── count_reader.masm │ ├── oracle_reader.masm │ ├── counter.masm │ └── mapping_example_contract.masm ├── scripts │ ├── oracle_reader_script.masm │ ├── counter_script.masm │ ├── reader_script.masm │ └── mapping_example_script.masm └── notes │ ├── network_increment_note.masm │ ├── hash_preimage_note.masm │ └── iterative_output_note.masm ├── .prettierrc ├── .github └── workflows │ ├── ci.yml │ ├── trigger-deploy-docs.yml │ ├── doc-tests.yml │ └── build-docs.yml ├── contributing.md ├── rust-client ├── Cargo.toml └── src │ └── bin │ ├── delegated_prover.rs │ ├── counter_contract_increment.rs │ ├── counter_contract_deploy.rs │ ├── mapping_example.rs │ ├── oracle_data_query.rs │ ├── counter_contract_fpi.rs │ ├── hash_preimage_note.rs │ ├── create_mint_consume_send.rs │ ├── note_creation_in_masm.rs │ ├── unauthenticated_note_transfer.rs │ └── network_notes_counter_contract.rs └── README.md /docs/masm: -------------------------------------------------------------------------------- 1 | ../masm -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .docusaurus/ 2 | build/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.dat 3 | node/storage/* 4 | docs/Cargo.lock 5 | 6 | keystore 7 | *.sqlite3 -------------------------------------------------------------------------------- /docs/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xMiden/miden-tutorials/HEAD/docs/static/img/logo.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xMiden/miden-tutorials/HEAD/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /web-client/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xMiden/miden-tutorials/HEAD/web-client/app/favicon.ico -------------------------------------------------------------------------------- /masm/accounts/auth/no_auth.masm: -------------------------------------------------------------------------------- 1 | use.miden::account 2 | export.auth__basic 3 | push.1 exec.account::incr_nonce 4 | end 5 | -------------------------------------------------------------------------------- /docs/src/img/note_creation_masm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xMiden/miden-tutorials/HEAD/docs/src/img/note_creation_masm.png -------------------------------------------------------------------------------- /web-client/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /docs/src/img/count_copy_fpi_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xMiden/miden-tutorials/HEAD/docs/src/img/count_copy_fpi_diagram.png -------------------------------------------------------------------------------- /web-client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /masm/scripts/oracle_reader_script.masm: -------------------------------------------------------------------------------- 1 | use.external_contract::oracle_reader 2 | 3 | begin 4 | exec.oracle_reader::get_price 5 | end 6 | -------------------------------------------------------------------------------- /masm/scripts/counter_script.masm: -------------------------------------------------------------------------------- 1 | use.external_contract::counter_contract 2 | 3 | begin 4 | call.counter_contract::increment_count 5 | end 6 | -------------------------------------------------------------------------------- /masm/notes/network_increment_note.masm: -------------------------------------------------------------------------------- 1 | use.external_contract::counter_contract 2 | 3 | begin 4 | call.counter_contract::increment_count 5 | end 6 | -------------------------------------------------------------------------------- /docs/src/rust-client/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Rust Client" 2 | # Determines where this documentation section appears relative to other sections in the parent folder 3 | position: 2 4 | collapsed: true 5 | -------------------------------------------------------------------------------- /docs/src/web-client/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Web Client" 2 | # Determines where this documentation section appears relative to other sections in the parent folder 3 | position: 3 4 | collapsed: true 5 | -------------------------------------------------------------------------------- /docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; 2 | 3 | const sidebars: SidebarsConfig = { 4 | docs: [{ type: "autogenerated", dirName: "." }], 5 | }; 6 | 7 | export default sidebars; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": false, 5 | "trailingComma": "all", 6 | "proseWrap": "preserve", 7 | "embeddedLanguageFormatting": "auto", 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /docs/src/web-client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "proseWrap": "preserve", 7 | "embeddedLanguageFormatting": "auto", 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /docs/src/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Tutorials" 2 | # Determines where this documentation section appears relative to other sections on the main documentation page (which is the parent of this folder in the miden-docs repository) 3 | position: 4 4 | collapsed: true 5 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | }, 7 | "exclude": [".docusaurus", "build"] 8 | } 9 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tutorials 3 | sidebar_position: 4 4 | --- 5 | 6 | # Introduction 7 | 8 | Basic tutorials and examples of how to build applications on Miden. 9 | 10 | The goal is to make getting up to speed with building on Miden as quick and simple as possible. 11 | 12 | All of the following tutorials are accompanied by code examples in Rust and TypeScript, which can be found in the [Miden Tutorials](https://github.com/0xMiden/miden-tutorials) repository. 13 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@miden/docs-dev", 3 | "private": true, 4 | "scripts": { 5 | "start:dev": "docusaurus start --port 3000 --host 0.0.0.0", 6 | "build:dev": "docusaurus build", 7 | "serve:dev": "docusaurus serve build" 8 | }, 9 | "devDependencies": { 10 | "@cmfcmf/docusaurus-search-local": "^2.0.0", 11 | "@docusaurus/core": "^3", 12 | "@docusaurus/preset-classic": "^3", 13 | "rehype-katex": "^7", 14 | "remark-math": "^6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /masm/accounts/count_reader.masm: -------------------------------------------------------------------------------- 1 | use.miden::active_account 2 | use miden::native_account 3 | use.miden::tx 4 | use.std::sys 5 | 6 | # => [account_id_prefix, account_id_suffix, get_count_proc_hash] 7 | export.copy_count 8 | exec.tx::execute_foreign_procedure 9 | # => [count] 10 | 11 | push.0 12 | # [index, count] 13 | 14 | debug.stack 15 | 16 | exec.native_account::set_item dropw 17 | # => [] 18 | 19 | exec.sys::truncate_stack 20 | # => [] 21 | end 22 | -------------------------------------------------------------------------------- /masm/scripts/reader_script.masm: -------------------------------------------------------------------------------- 1 | use.external_contract::count_reader_contract 2 | use.std::sys 3 | 4 | begin 5 | push.{get_count_proc_hash} 6 | # => [GET_COUNT_HASH] 7 | 8 | push.{account_id_suffix} 9 | # => [account_id_suffix, GET_COUNT_HASH] 10 | 11 | push.{account_id_prefix} 12 | # => [account_id_prefix, account_id_suffix, GET_COUNT_HASH] 13 | 14 | call.count_reader_contract::copy_count 15 | # => [] 16 | 17 | exec.sys::truncate_stack 18 | # => [] 19 | end 20 | -------------------------------------------------------------------------------- /masm/scripts/mapping_example_script.masm: -------------------------------------------------------------------------------- 1 | use.miden_by_example::mapping_example_contract 2 | use.std::sys 3 | 4 | begin 5 | push.1.2.3.4 6 | push.0.0.0.0 7 | # => [KEY, VALUE] 8 | 9 | call.mapping_example_contract::write_to_map 10 | # => [] 11 | 12 | push.0.0.0.0 13 | # => [KEY] 14 | 15 | call.mapping_example_contract::get_value_in_map 16 | # => [VALUE] 17 | 18 | dropw 19 | # => [] 20 | 21 | call.mapping_example_contract::get_current_map_root 22 | # => [CURRENT_ROOT] 23 | 24 | exec.sys::truncate_stack 25 | end 26 | -------------------------------------------------------------------------------- /docs/static/img/custom_caret.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /web-client/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #000000; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Format Markdown 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, reopened, synchronize] 8 | 9 | jobs: 10 | format-markdown: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '16' 20 | 21 | - name: Install Prettier 22 | run: npm install -g prettier 23 | 24 | - name: Check Markdown formatting 25 | run: prettier --check "**/*.md" 26 | -------------------------------------------------------------------------------- /web-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@demox-labs/miden-sdk": "0.12.3", 13 | "next": "15.3.2", 14 | "react": "^19.0.0", 15 | "react-dom": "^19.0.0" 16 | }, 17 | "devDependencies": { 18 | "@tailwindcss/postcss": "^4", 19 | "@types/node": "^20", 20 | "@types/react": "^19", 21 | "@types/react-dom": "^19", 22 | "tailwindcss": "^4", 23 | "typescript": "^5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web-client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /masm/accounts/oracle_reader.masm: -------------------------------------------------------------------------------- 1 | use.miden::tx 2 | 3 | # Fetches the current price from the `get_median` 4 | # procedure from the Pragma oracle 5 | # => [] 6 | export.get_price 7 | push.0.0.0.120195681 8 | # => [PAIR] 9 | 10 | # This is the procedure root of the `get_median` procedure 11 | push.0xb86237a8c9cd35acfef457e47282cc4da43df676df410c988eab93095d8fb3b9 12 | # => [GET_MEDIAN_HASH, PAIR] 13 | 14 | push.939716883672832.2172042075194638080 15 | # => [oracle_id_prefix, oracle_id_suffix, GET_MEDIAN_HASH, PAIR] 16 | 17 | exec.tx::execute_foreign_procedure 18 | # => [price] 19 | 20 | debug.stack 21 | # => [price] 22 | 23 | dropw dropw 24 | end 25 | -------------------------------------------------------------------------------- /web-client/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | output: "export", 5 | trailingSlash: true, 6 | skipTrailingSlashRedirect: true, 7 | experimental: { 8 | esmExternals: "loose", 9 | }, 10 | webpack: (config, { isServer }) => { 11 | // Handle WASM files 12 | config.experiments = { 13 | ...config.experiments, 14 | asyncWebAssembly: true, 15 | topLevelAwait: true, 16 | }; 17 | 18 | // Add WASM to asset rules 19 | config.module.rules.push({ 20 | test: /\.wasm$/, 21 | type: "asset/resource", 22 | }); 23 | 24 | return config; 25 | }, 26 | }; 27 | 28 | export default nextConfig; 29 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Icon/Note.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from "react"; 2 | import type { Props } from "@theme/Admonition/Icon/Note"; 3 | 4 | export default function AdmonitionIconNote(props: Props): ReactNode { 5 | return ( 6 | 14 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/trigger-deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Trigger Aggregator Docs Rebuild 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "docs/**" 8 | 9 | jobs: 10 | notify: 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | contents: read 15 | 16 | steps: 17 | - name: Send repository_dispatch to aggregator 18 | uses: peter-evans/repository-dispatch@a628c95fd17070f003ea24579a56e6bc89b25766 19 | with: 20 | # PAT (Personal Access Token) that grants permission to trigger the rebuild workflow at the docs repository 21 | token: ${{ secrets.DOCS_REPO_TOKEN }} 22 | repository: ${{ vars.DOCS_AGGREGATOR_REPO }} 23 | event-type: rebuild 24 | -------------------------------------------------------------------------------- /docs/src/rust-client/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Rust Client" 3 | sidebar_position: 1 4 | --- 5 | 6 | # Rust Client 7 | 8 | Rust library, which can be used to programmatically interact with the Miden rollup. 9 | 10 | The Miden Rust client can be used for a variety of things, including: 11 | 12 | - Deploying, testing, and creating transactions to interact with accounts and notes on Miden. 13 | - Storing the state of accounts and notes locally. 14 | - Generating and submitting proofs of transactions. 15 | 16 | This section of the docs is an overview of the different things one can achieve using the Rust client, and how to implement them. 17 | 18 | Keep in mind that both the Rust client and the documentation are works-in-progress! 19 | -------------------------------------------------------------------------------- /web-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /docs/src/web-client/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Web Client' 3 | sidebar_position: 1 4 | --- 5 | 6 | TypeScript library, which can be used to programmatically interact with the Miden rollup. 7 | 8 | The Miden WebClient can be used for a variety of things, including: 9 | 10 | - Deploying and creating transactions to interact with accounts and notes on Miden. 11 | - Storing the state of accounts and notes in the browser. 12 | - Generating and submitting proofs of transactions. 13 | - Submitting transactions to delegated proving services. 14 | 15 | This section of the docs is an overview of the different things one can achieve using the WebClient, and how to implement them. 16 | 17 | Keep in mind that both the WebClient and the documentation are works-in-progress! 18 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Miden Tutorials 2 | 3 | #### First off, thanks for taking the time to contribute! 4 | 5 | ## Markdown Formatting with Prettier 6 | 7 | We use [Prettier](https://prettier.io/) to ensure our Markdown files are consistently formatted. 8 | 9 | ### Installation 10 | 11 | - **Global Installation:** 12 | 13 | ```bash 14 | npm install -g prettier 15 | ``` 16 | 17 | - **Local (Dev Dependency) Installation:** 18 | 19 | ```bash 20 | npm install --save-dev prettier 21 | ``` 22 | 23 | ### Formatting Files 24 | 25 | From the root of the project, run: 26 | 27 | ```bash 28 | prettier --write "**/*.md" 29 | ``` 30 | 31 | Make sure to run this command before submitting pull requests. 32 | 33 | Thank you for contributing! 34 | 35 | --- 36 | -------------------------------------------------------------------------------- /docs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "miden-tutorials-docs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | miden-client = { version = "0.12", features = ["testing", "tonic"] } 8 | miden-client-sqlite-store = { version = "0.12", package = "miden-client-sqlite-store" } 9 | miden-lib = { version = "0.12", default-features = false } 10 | miden-objects = { version = "0.12", default-features = false, features = ["testing"] } 11 | miden-crypto = { version = "0.17.1", features = ["executable"] } 12 | miden-assembly = "0.18.3" 13 | rand = { version = "0.9" } 14 | serde = { version = "1", features = ["derive"] } 15 | serde_json = { version = "1.0", features = ["raw_value"] } 16 | tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] } 17 | rand_chacha = "0.9.0" -------------------------------------------------------------------------------- /rust-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | miden-client = { version = "0.12", features = ["testing", "tonic"] } 8 | miden-client-sqlite-store = { version = "0.12", package = "miden-client-sqlite-store" } 9 | miden-lib = { version = "0.12", default-features = false } 10 | miden-objects = { version = "0.12", default-features = false, features = ["testing"] } 11 | miden-crypto = { version = "0.17.1", features = ["executable"] } 12 | miden-assembly = "0.18.3" 13 | rand = { version = "0.9" } 14 | serde = { version = "1", features = ["derive"] } 15 | serde_json = { version = "1.0", features = ["raw_value"] } 16 | tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] } 17 | rand_chacha = "0.9.0" 18 | -------------------------------------------------------------------------------- /docs/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("rust-client/create_deploy_tutorial.md")] 2 | #![doc = include_str!("rust-client/mint_consume_create_tutorial.md")] 3 | #![doc = include_str!("rust-client/counter_contract_tutorial.md")] 4 | #![doc = include_str!("rust-client/custom_note_how_to.md")] 5 | #![doc = include_str!("rust-client/foreign_procedure_invocation_tutorial.md")] 6 | #![doc = include_str!("rust-client/public_account_interaction_tutorial.md")] 7 | #![doc = include_str!("rust-client/unauthenticated_note_how_to.md")] 8 | #![doc = include_str!("rust-client/mappings_in_masm_how_to.md")] 9 | #![doc = include_str!("rust-client/creating_notes_in_masm_tutorial.md")] 10 | #![doc = include_str!("rust-client/delegated_proving_tutorial.md")] 11 | #![doc = include_str!("rust-client/network_transactions_tutorial.md")] 12 | -------------------------------------------------------------------------------- /masm/accounts/counter.masm: -------------------------------------------------------------------------------- 1 | use.miden::active_account 2 | use miden::native_account 3 | use.std::sys 4 | 5 | const.COUNTER_SLOT=0 6 | 7 | #! Inputs: [] 8 | #! Outputs: [count] 9 | export.get_count 10 | push.COUNTER_SLOT 11 | # => [index] 12 | 13 | exec.active_account::get_item 14 | # => [count] 15 | 16 | # clean up stack 17 | movdn.4 dropw 18 | # => [count] 19 | end 20 | 21 | #! Inputs: [] 22 | #! Outputs: [] 23 | export.increment_count 24 | push.COUNTER_SLOT 25 | # => [index] 26 | 27 | exec.active_account::get_item 28 | # => [count] 29 | 30 | add.1 31 | # => [count+1] 32 | 33 | debug.stack 34 | 35 | push.COUNTER_SLOT 36 | # [index, count+1] 37 | 38 | exec.native_account::set_item 39 | # => [OLD_VALUE] 40 | 41 | dropw 42 | # => [] 43 | end 44 | -------------------------------------------------------------------------------- /web-client/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Layout/styles.module.css: -------------------------------------------------------------------------------- 1 | .admonition { 2 | margin-bottom: 1em; 3 | } 4 | 5 | .admonitionHeading { 6 | font: var(--ifm-heading-font-weight) var(--ifm-h5-font-size) / 7 | var(--ifm-heading-line-height) var(--ifm-heading-font-family); 8 | text-transform: uppercase; 9 | } 10 | 11 | /* Heading alone without content (does not handle fragment content) */ 12 | .admonitionHeading:not(:last-child) { 13 | margin-bottom: 0.3rem; 14 | } 15 | 16 | .admonitionHeading code { 17 | text-transform: none; 18 | } 19 | 20 | .admonitionIcon { 21 | display: inline-block; 22 | vertical-align: middle; 23 | margin-right: 0.4em; 24 | } 25 | 26 | .admonitionIcon svg { 27 | display: inline-block; 28 | height: 1.6em; 29 | width: 1.6em; 30 | fill: var(--ifm-alert-foreground-color); 31 | } 32 | 33 | .admonitionContent > :last-child { 34 | margin-bottom: 0; 35 | } 36 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {type ComponentType, type ReactNode} from 'react'; 2 | import {processAdmonitionProps} from '@docusaurus/theme-common'; 3 | import type {Props} from '@theme/Admonition'; 4 | import AdmonitionTypes from '@theme/Admonition/Types'; 5 | 6 | function getAdmonitionTypeComponent(type: string): ComponentType { 7 | const component = AdmonitionTypes[type]; 8 | if (component) { 9 | return component; 10 | } 11 | console.warn( 12 | `No admonition component found for admonition type "${type}". Using Info as fallback.`, 13 | ); 14 | return AdmonitionTypes.info!; 15 | } 16 | 17 | export default function Admonition(unprocessedProps: Props): ReactNode { 18 | const props = processAdmonitionProps(unprocessedProps); 19 | const AdmonitionTypeComponent = getAdmonitionTypeComponent(props.type); 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Type/Tip.tsx: -------------------------------------------------------------------------------- 1 | import React, {type ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Translate from '@docusaurus/Translate'; 4 | import type {Props} from '@theme/Admonition/Type/Tip'; 5 | import AdmonitionLayout from '@theme/Admonition/Layout'; 6 | import IconTip from '@theme/Admonition/Icon/Tip'; 7 | 8 | const infimaClassName = 'alert alert--success'; 9 | 10 | const defaultProps = { 11 | icon: , 12 | title: ( 13 | 16 | tip 17 | 18 | ), 19 | }; 20 | 21 | export default function AdmonitionTypeTip(props: Props): ReactNode { 22 | return ( 23 | 27 | {props.children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Type/Info.tsx: -------------------------------------------------------------------------------- 1 | import React, {type ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Translate from '@docusaurus/Translate'; 4 | import type {Props} from '@theme/Admonition/Type/Info'; 5 | import AdmonitionLayout from '@theme/Admonition/Layout'; 6 | import IconInfo from '@theme/Admonition/Icon/Info'; 7 | 8 | const infimaClassName = 'alert alert--info'; 9 | 10 | const defaultProps = { 11 | icon: , 12 | title: ( 13 | 16 | info 17 | 18 | ), 19 | }; 20 | 21 | export default function AdmonitionTypeInfo(props: Props): ReactNode { 22 | return ( 23 | 27 | {props.children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Type/Note.tsx: -------------------------------------------------------------------------------- 1 | import React, {type ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Translate from '@docusaurus/Translate'; 4 | import type {Props} from '@theme/Admonition/Type/Note'; 5 | import AdmonitionLayout from '@theme/Admonition/Layout'; 6 | import IconNote from '@theme/Admonition/Icon/Note'; 7 | 8 | const infimaClassName = 'alert alert--secondary'; 9 | 10 | const defaultProps = { 11 | icon: , 12 | title: ( 13 | 16 | note 17 | 18 | ), 19 | }; 20 | 21 | export default function AdmonitionTypeNote(props: Props): ReactNode { 22 | return ( 23 | 27 | {props.children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Icon/Info.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from "react"; 2 | import type { Props } from "@theme/Admonition/Icon/Info"; 3 | 4 | export default function AdmonitionIconInfo(props: Props): ReactNode { 5 | return ( 6 | 14 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Type/Danger.tsx: -------------------------------------------------------------------------------- 1 | import React, {type ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Translate from '@docusaurus/Translate'; 4 | import type {Props} from '@theme/Admonition/Type/Danger'; 5 | import AdmonitionLayout from '@theme/Admonition/Layout'; 6 | import IconDanger from '@theme/Admonition/Icon/Danger'; 7 | 8 | const infimaClassName = 'alert alert--danger'; 9 | 10 | const defaultProps = { 11 | icon: , 12 | title: ( 13 | 16 | danger 17 | 18 | ), 19 | }; 20 | 21 | export default function AdmonitionTypeDanger(props: Props): ReactNode { 22 | return ( 23 | 27 | {props.children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/doc-tests.yml: -------------------------------------------------------------------------------- 1 | name: Documentation Tests 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | push: 7 | branches: [main] 8 | pull_request: 9 | types: [opened, reopened, synchronize] 10 | schedule: 11 | - cron: '0 7 * * 1' 12 | 13 | jobs: 14 | doc-tests: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@main 19 | 20 | - uses: Swatinem/rust-cache@v2 21 | with: 22 | # Only update the cache on push onto the main branch. This strikes a nice balance between 23 | # cache hits and cache evictions (github has a 10GB cache limit). 24 | save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 25 | workspaces: docs 26 | 27 | - name: Run documentation tests 28 | run: | 29 | rustup update --no-self-update 30 | cargo test --doc --release -- --test-threads=1 31 | working-directory: docs 32 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Type/Warning.tsx: -------------------------------------------------------------------------------- 1 | import React, {type ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Translate from '@docusaurus/Translate'; 4 | import type {Props} from '@theme/Admonition/Type/Warning'; 5 | import AdmonitionLayout from '@theme/Admonition/Layout'; 6 | import IconWarning from '@theme/Admonition/Icon/Warning'; 7 | 8 | const infimaClassName = 'alert alert--warning'; 9 | 10 | const defaultProps = { 11 | icon: , 12 | title: ( 13 | 16 | warning 17 | 18 | ), 19 | }; 20 | 21 | export default function AdmonitionTypeWarning(props: Props): ReactNode { 22 | return ( 23 | 27 | {props.children} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /masm/accounts/mapping_example_contract.masm: -------------------------------------------------------------------------------- 1 | use.miden::active_account 2 | use.miden::native_account 3 | use.std::sys 4 | 5 | # Inputs: [KEY, VALUE] 6 | # Outputs: [] 7 | export.write_to_map 8 | # The storage map is in storage slot 1 9 | push.1 10 | # => [index, KEY, VALUE] 11 | 12 | # Setting the key value pair in the map 13 | exec.native_account::set_map_item 14 | # => [OLD_MAP_ROOT, OLD_MAP_VALUE] 15 | 16 | dropw dropw dropw dropw 17 | # => [] 18 | end 19 | 20 | # Inputs: [KEY] 21 | # Outputs: [VALUE] 22 | export.get_value_in_map 23 | # The storage map is in storage slot 1 24 | push.1 25 | # => [index] 26 | 27 | exec.active_account::get_map_item 28 | # => [VALUE] 29 | end 30 | 31 | # Inputs: [] 32 | # Outputs: [CURRENT_ROOT] 33 | export.get_current_map_root 34 | # Getting the current root from slot 1 35 | push.1 exec.active_account::get_item 36 | # => [CURRENT_ROOT] 37 | 38 | exec.sys::truncate_stack 39 | # => [CURRENT_ROOT] 40 | end 41 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Icon/Tip.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from "react"; 2 | import type { Props } from "@theme/Admonition/Icon/Tip"; 3 | 4 | export default function AdmonitionIconTip(props: Props): ReactNode { 5 | return ( 6 | 14 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Type/Caution.tsx: -------------------------------------------------------------------------------- 1 | import React, {type ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Translate from '@docusaurus/Translate'; 4 | import type {Props} from '@theme/Admonition/Type/Caution'; 5 | import AdmonitionLayout from '@theme/Admonition/Layout'; 6 | import IconWarning from '@theme/Admonition/Icon/Warning'; 7 | 8 | const infimaClassName = 'alert alert--warning'; 9 | 10 | const defaultProps = { 11 | icon: , 12 | title: ( 13 | 16 | caution 17 | 18 | ), 19 | }; 20 | 21 | // TODO remove before v4: Caution replaced by Warning 22 | // see https://github.com/facebook/docusaurus/issues/7558 23 | export default function AdmonitionTypeCaution(props: Props): ReactNode { 24 | return ( 25 | 29 | {props.children} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # miden-tutorials 2 | 3 | The goal of this repository is to provide clear and practical examples for interacting with the **Miden Rollup**. These examples are designed to ensure a smooth onboarding experience for developers exploring Miden's capabilities. 4 | 5 | This repository is organized into several parts: 6 | 7 | 1. **docs**, contains the README files for the tutorials and guides. 8 | 2. **masm**, contains the Miden assembly notes, accounts, and scripts used in the examples. 9 | 3. **rust-client**, contains examples for interacting with the Miden Rollup using **Rust**. 10 | 4. **web-client**, contains examples for interacting with the Miden Rollup in the browser. 11 | 12 | ## Documentation 13 | 14 | The documentation (tutorials) in the `docs` folder is built using Docusaurus and is automatically absorbed into the main [miden-docs](https://github.com/0xMiden/miden-docs) repository for the main documentation website. Changes to the `next` branch trigger an automated deployment workflow. The docs folder requires npm packages to be installed before building. 15 | 16 | The documentation folder is also a standalone Rust repository. The purpose of this is to be able to run `cargo doc test`, to test the Rust code inside of the tutorial markdowns. 17 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Icon/Warning.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from "react"; 2 | import type { Props } from "@theme/Admonition/Icon/Warning"; 3 | 4 | export default function AdmonitionIconCaution(props: Props): ReactNode { 5 | return ( 6 | 14 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: build-docs 2 | 3 | # Limits workflow concurrency to only the latest commit in the PR. 4 | concurrency: 5 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 6 | cancel-in-progress: true 7 | 8 | on: 9 | push: 10 | branches: [main] 11 | paths: 12 | - "docs/**" 13 | - ".github/workflows/build-docs.yml" 14 | pull_request: 15 | types: [opened, reopened, synchronize] 16 | paths: 17 | - "docs/**" 18 | - ".github/workflows/build-docs.yml" 19 | workflow_dispatch: 20 | 21 | permissions: 22 | contents: read 23 | 24 | jobs: 25 | build-docs: 26 | name: Build Documentation 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: "20" 37 | cache: "npm" 38 | cache-dependency-path: docs/package-lock.json 39 | 40 | - name: Install dependencies 41 | working-directory: ./docs 42 | run: npm ci 43 | 44 | - name: Build documentation 45 | working-directory: ./docs 46 | run: npm run build:dev 47 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Types.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AdmonitionTypeNote from '@theme/Admonition/Type/Note'; 3 | import AdmonitionTypeTip from '@theme/Admonition/Type/Tip'; 4 | import AdmonitionTypeInfo from '@theme/Admonition/Type/Info'; 5 | import AdmonitionTypeWarning from '@theme/Admonition/Type/Warning'; 6 | import AdmonitionTypeDanger from '@theme/Admonition/Type/Danger'; 7 | import AdmonitionTypeCaution from '@theme/Admonition/Type/Caution'; 8 | import type AdmonitionTypes from '@theme/Admonition/Types'; 9 | 10 | const admonitionTypes: typeof AdmonitionTypes = { 11 | note: AdmonitionTypeNote, 12 | tip: AdmonitionTypeTip, 13 | info: AdmonitionTypeInfo, 14 | warning: AdmonitionTypeWarning, 15 | danger: AdmonitionTypeDanger, 16 | }; 17 | 18 | // Undocumented legacy admonition type aliases 19 | // Provide hardcoded/untranslated retrocompatible label 20 | // See also https://github.com/facebook/docusaurus/issues/7767 21 | const admonitionAliases: typeof AdmonitionTypes = { 22 | secondary: (props) => , 23 | important: (props) => , 24 | success: (props) => , 25 | caution: AdmonitionTypeCaution, 26 | }; 27 | 28 | export default { 29 | ...admonitionTypes, 30 | ...admonitionAliases, 31 | }; 32 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Icon/Danger.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from "react"; 2 | import type { Props } from "@theme/Admonition/Icon/Danger"; 3 | 4 | export default function AdmonitionIconDanger(props: Props): ReactNode { 5 | return ( 6 | 13 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /web-client/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /docs/src/theme/Admonition/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from "react"; 2 | import clsx from "clsx"; 3 | import { ThemeClassNames } from "@docusaurus/theme-common"; 4 | 5 | import type { Props } from "@theme/Admonition/Layout"; 6 | 7 | import styles from "./styles.module.css"; 8 | 9 | function AdmonitionContainer({ 10 | type, 11 | className, 12 | children, 13 | }: Pick & { children: ReactNode }) { 14 | return ( 15 |
23 | {children} 24 |
25 | ); 26 | } 27 | 28 | function AdmonitionHeading({ icon, title }: Pick) { 29 | return ( 30 |
31 | {icon} 32 | {/* {title} */} 33 |
34 | ); 35 | } 36 | 37 | function AdmonitionContent({ children }: Pick) { 38 | return children ? ( 39 |
{children}
40 | ) : null; 41 | } 42 | 43 | export default function AdmonitionLayout(props: Props): ReactNode { 44 | const { type, icon, title, children, className } = props; 45 | return ( 46 | 47 | {title || icon ? : null} 48 | {children} 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /masm/notes/hash_preimage_note.masm: -------------------------------------------------------------------------------- 1 | use.miden::active_note 2 | use.miden::contracts::wallets::basic->wallet 3 | 4 | # CONSTANTS 5 | # ================================================================================================= 6 | 7 | const.EXPECTED_DIGEST_PTR=0 8 | const.ASSET_PTR=100 9 | 10 | # ERRORS 11 | # ================================================================================================= 12 | 13 | const.ERROR_DIGEST_MISMATCH="Expected digest does not match computed digest" 14 | 15 | #! Inputs (arguments): [HASH_PREIMAGE_SECRET] 16 | #! Outputs: [] 17 | #! 18 | #! Note inputs are assumed to be as follows: 19 | #! => EXPECTED_DIGEST 20 | begin 21 | # => HASH_PREIMAGE_SECRET 22 | # Hashing the secret number 23 | hash 24 | # => [DIGEST] 25 | 26 | # Writing the note inputs to memory 27 | push.EXPECTED_DIGEST_PTR exec.active_note::get_inputs drop drop 28 | 29 | # Pad stack and load expected digest from memory 30 | padw push.EXPECTED_DIGEST_PTR mem_loadw_be 31 | # => [EXPECTED_DIGEST, DIGEST] 32 | 33 | # Assert that the note input matches the digest 34 | # Will fail if the two hashes do not match 35 | assert_eqw.err=ERROR_DIGEST_MISMATCH 36 | # => [] 37 | 38 | # --------------------------------------------------------------------------------------------- 39 | # If the check is successful, we allow for the asset to be consumed 40 | # --------------------------------------------------------------------------------------------- 41 | 42 | # Write the asset in note to memory address ASSET_PTR 43 | push.ASSET_PTR exec.active_note::get_assets 44 | # => [num_assets, dest_ptr] 45 | 46 | drop 47 | # => [dest_ptr] 48 | 49 | # Load asset from memory 50 | mem_loadw_be 51 | # => [ASSET] 52 | 53 | # Call receive asset in wallet 54 | call.wallet::receive_asset 55 | # => [] 56 | end 57 | -------------------------------------------------------------------------------- /masm/notes/iterative_output_note.masm: -------------------------------------------------------------------------------- 1 | use.miden::active_note 2 | use.miden::tx 3 | use.miden::note 4 | use.miden::output_note 5 | use.std::sys 6 | use.std::crypto::hashes::rpo 7 | use.miden::contracts::wallets::basic->wallet 8 | 9 | # Memory Addresses 10 | const.ASSET=0 11 | const.ASSET_HALF=4 12 | const.ACCOUNT_ID_PREFIX=8 13 | const.ACCOUNT_ID_SUFFIX=9 14 | const.TAG=10 15 | 16 | # => [] 17 | begin 18 | # Drop word if user accidentally pushes note_args 19 | dropw 20 | # => [] 21 | 22 | # Get note inputs 23 | push.ACCOUNT_ID_PREFIX exec.active_note::get_inputs drop drop 24 | # => [] 25 | 26 | # Get asset contained in note 27 | push.ASSET exec.active_note::get_assets drop drop 28 | # => [] 29 | 30 | mem_loadw_be.ASSET 31 | # => [ASSET] 32 | 33 | # Compute half amount of asset 34 | swap.3 push.2 div swap.3 35 | # => [ASSET_HALF] 36 | 37 | mem_storew_be.ASSET_HALF dropw 38 | # => [] 39 | 40 | mem_loadw_be.ASSET 41 | # => [ASSET] 42 | 43 | # Receive the entire asset amount to the wallet 44 | call.wallet::receive_asset 45 | # => [] 46 | 47 | # Get note inputs commitment 48 | push.8.ACCOUNT_ID_PREFIX 49 | # => [memory_address_pointer, number_of_inputs] 50 | 51 | # Note: Must pad with 0s to nearest multiple of 8 52 | exec.rpo::hash_memory 53 | # => [INPUTS_COMMITMENT] 54 | 55 | # Push script hash 56 | exec.active_note::get_script_root 57 | # => [SCRIPT_HASH, INPUTS_COMMITMENT] 58 | 59 | # Get the current note serial number 60 | exec.active_note::get_serial_number 61 | # => [SERIAL_NUM, SCRIPT_HASH, INPUTS_COMMITMENT] 62 | 63 | # Increment serial number by 1 64 | push.1 add 65 | # => [SERIAL_NUM+1, SCRIPT_HASH, INPUTS_COMMITMENT] 66 | 67 | exec.note::build_recipient_hash 68 | # => [RECIPIENT] 69 | 70 | # Push hint, note type, and aux to stack 71 | push.1.1.0 72 | # => [aux, public_note, execution_hint_always, RECIPIENT] 73 | 74 | # Load tag from memory 75 | mem_load.TAG 76 | # => [tag, aux, note_type, execution_hint, RECIPIENT] 77 | 78 | call.output_note::create 79 | # => [note_idx, pad(15) ...] 80 | 81 | padw mem_loadw_be.ASSET_HALF 82 | # => [ASSET / 2, note_idx] 83 | 84 | call.wallet::move_asset_to_note 85 | # => [ASSET, note_idx, pad(11)] 86 | 87 | dropw drop 88 | # => [] 89 | 90 | exec.sys::truncate_stack 91 | # => [] 92 | end 93 | -------------------------------------------------------------------------------- /docs/src/miden_node_setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Miden Node Setup 3 | sidebar_position: 2 4 | --- 5 | 6 | # Miden Node Setup Tutorial 7 | 8 | To run the Miden tutorial examples, you will need to set up a test environment and connect to a Miden node. 9 | 10 | There are two ways to connect to a Miden node: 11 | 12 | 1. Run the Miden node locally 13 | 2. Connect to the Miden testnet 14 | 15 | ## Running the Miden node locally 16 | 17 | ### Step 1: Install the Miden node 18 | 19 | Next, install the miden-node crate using this command: 20 | 21 | ```bash 22 | cargo install miden-node --locked --version 0.12.3 23 | ``` 24 | 25 | ### Step 2: Initializing the node 26 | 27 | To start the node, we first need to generate the genesis file. Create the genesis file using this command: 28 | 29 | ```bash 30 | mkdir data 31 | mkdir accounts 32 | 33 | miden-node bundled bootstrap \ 34 | --data-directory data \ 35 | --accounts-directory accounts 36 | ``` 37 | 38 | Expected output: 39 | 40 | ``` 41 | 2025-04-16T18:05:30.049129Z INFO miden_node::commands::store: bin/node/src/commands/store.rs:145: Generating account, index: 0, total: 1 42 | ``` 43 | 44 | ### Step 3: Starting the node 45 | 46 | To start the node run this command: 47 | 48 | ```bash 49 | miden-node bundled start \ 50 | --data-directory data \ 51 | --rpc.url http://0.0.0.0:57291 52 | ``` 53 | 54 | Expected output: 55 | 56 | ``` 57 | 2025-01-17T12:14:55.432445Z INFO try_build_batches: miden-block-producer: /Users/username/.cargo/registry/src/index.crates.io-6f17d22bba15001f/miden-node-block-producer-0.6.0/src/txqueue/mod.rs:85: close, time.busy: 8.88µs, time.idle: 103µs 58 | 2025-01-17T12:14:57.433162Z INFO try_build_batches: miden-block-producer: /Users/username/.cargo/registry/src/index.crates.io-6f17d22bba15001f/miden-node-block-producer-0.6.0/src/txqueue/mod.rs:85: new 59 | 2025-01-17T12:14:57.433256Z INFO try_build_batches: miden-block-producer: /Users/username/.cargo/registry/src/index.crates.io-6f17d22bba15001f/miden-node-block-producer-0.6.0/src/txqueue/mod.rs:85: close, time.busy: 6.46µs, time.idle: 94.0µs 60 | ``` 61 | 62 | Congratulations, you now have a Miden node running locally. Now we can start creating a testing environment for building applications on Miden! 63 | 64 | The endpoint of the Miden node running locally is: 65 | 66 | ``` 67 | http://localhost:57291 68 | ``` 69 | 70 | ### Resetting the node 71 | 72 | _If you need to reset the local state of the node run this command:_ 73 | 74 | ```bash 75 | rm -r data 76 | rm -r accounts 77 | ``` 78 | 79 | After resetting the state of the node, follow steps 2 and 4 again. 80 | 81 | ## Connecting to the Miden testnet 82 | 83 | To run the tutorial examples using the Miden testnet, use this endpoint: 84 | 85 | ```bash 86 | https://rpc.testnet.miden.io:443 87 | ``` 88 | -------------------------------------------------------------------------------- /web-client/lib/createMintConsume.ts: -------------------------------------------------------------------------------- 1 | // lib/createMintConsume.ts 2 | export async function createMintConsume(): Promise { 3 | if (typeof window === 'undefined') { 4 | console.warn('webClient() can only run in the browser'); 5 | return; 6 | } 7 | 8 | // dynamic import → only in the browser, so WASM is loaded client‑side 9 | const { WebClient, AccountStorageMode, NoteType, Address } = await import( 10 | '@demox-labs/miden-sdk' 11 | ); 12 | 13 | const nodeEndpoint = 'https://rpc.testnet.miden.io'; 14 | const client = await WebClient.createClient(nodeEndpoint); 15 | 16 | // 1. Sync with the latest blockchain state 17 | const state = await client.syncState(); 18 | console.log('Latest block number:', state.blockNum()); 19 | 20 | // 2. Create Alice's account 21 | console.log('Creating account for Alice…'); 22 | const alice = await client.newWallet(AccountStorageMode.public(), true, 0); 23 | console.log('Alice ID:', alice.id().toString()); 24 | 25 | // 3. Deploy a fungible faucet 26 | console.log('Creating faucet…'); 27 | const faucet = await client.newFaucet( 28 | AccountStorageMode.public(), 29 | false, 30 | 'MID', 31 | 8, 32 | BigInt(1_000_000), 33 | 0, 34 | ); 35 | console.log('Faucet ID:', faucet.id().toString()); 36 | 37 | await client.syncState(); 38 | 39 | // 4. Mint tokens to Alice 40 | await client.syncState(); 41 | 42 | console.log('Minting tokens to Alice...'); 43 | const mintTxRequest = client.newMintTransactionRequest( 44 | alice.id(), 45 | faucet.id(), 46 | NoteType.Public, 47 | BigInt(1000), 48 | ); 49 | 50 | await client.submitNewTransaction(faucet.id(), mintTxRequest); 51 | 52 | console.log('Waiting 10 seconds for transaction confirmation...'); 53 | await new Promise((resolve) => setTimeout(resolve, 10000)); 54 | await client.syncState(); 55 | 56 | // 5. Fetch minted notes 57 | const mintedNotes = await client.getConsumableNotes(alice.id()); 58 | const mintedNoteIds = mintedNotes.map((n) => 59 | n.inputNoteRecord().id().toString(), 60 | ); 61 | console.log('Minted note IDs:', mintedNoteIds); 62 | 63 | // 6. Consume minted notes 64 | console.log('Consuming minted notes...'); 65 | const consumeTxRequest = client.newConsumeTransactionRequest(mintedNoteIds); 66 | 67 | await client.submitNewTransaction(alice.id(), consumeTxRequest); 68 | 69 | await client.syncState(); 70 | console.log('Notes consumed.'); 71 | 72 | // 7. Send tokens to Bob 73 | const bobAccountId = Address.fromBech32( 74 | 'mtst1apve54rq8ux0jqqqqrkh5y0r0y8cwza6_qruqqypuyph', 75 | ).accountId(); 76 | console.log("Sending tokens to Bob's account..."); 77 | const sendTxRequest = client.newSendTransactionRequest( 78 | alice.id(), 79 | bobAccountId, 80 | faucet.id(), 81 | NoteType.Public, 82 | BigInt(100), 83 | ); 84 | 85 | await client.submitNewTransaction(alice.id(), sendTxRequest); 86 | console.log('Tokens sent successfully!'); 87 | } 88 | -------------------------------------------------------------------------------- /web-client/lib/multiSendWithDelegatedProver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Demonstrates multi-send functionality using a delegated prover on the Miden Network 3 | * Creates multiple P2ID (Pay to ID) notes for different recipients 4 | * 5 | * @throws {Error} If the function cannot be executed in a browser environment 6 | */ 7 | export async function multiSendWithDelegatedProver(): Promise { 8 | // Ensure this runs only in a browser context 9 | if (typeof window === 'undefined') return console.warn('Run in browser'); 10 | 11 | const { 12 | WebClient, 13 | AccountStorageMode, 14 | Address, 15 | NoteType, 16 | TransactionProver, 17 | NetworkId, 18 | Note, 19 | NoteAssets, 20 | OutputNoteArray, 21 | Felt, 22 | FungibleAsset, 23 | TransactionRequestBuilder, 24 | OutputNote, 25 | } = await import('@demox-labs/miden-sdk'); 26 | 27 | const client = await WebClient.createClient('https://rpc.testnet.miden.io'); 28 | const prover = TransactionProver.newRemoteProver( 29 | 'https://tx-prover.testnet.miden.io', 30 | ); 31 | 32 | console.log('Latest block:', (await client.syncState()).blockNum()); 33 | 34 | // ── Creating new account ────────────────────────────────────────────────────── 35 | console.log('Creating account for Alice…'); 36 | const alice = await client.newWallet(AccountStorageMode.public(), true, 0); 37 | console.log('Alice accout ID:', alice.id().toString()); 38 | 39 | // ── Creating new faucet ────────────────────────────────────────────────────── 40 | const faucet = await client.newFaucet( 41 | AccountStorageMode.public(), 42 | false, 43 | 'MID', 44 | 8, 45 | BigInt(1_000_000), 46 | 0, 47 | ); 48 | console.log('Faucet ID:', faucet.id().toString()); 49 | 50 | // ── mint 10 000 MID to Alice ────────────────────────────────────────────────────── 51 | { 52 | const txResult = await client.executeTransaction( 53 | faucet.id(), 54 | client.newMintTransactionRequest( 55 | alice.id(), 56 | faucet.id(), 57 | NoteType.Public, 58 | BigInt(10_000), 59 | ), 60 | ); 61 | const proven = await client.proveTransaction(txResult, prover); 62 | const submissionHeight = await client.submitProvenTransaction( 63 | proven, 64 | txResult, 65 | ); 66 | await client.applyTransaction(txResult, submissionHeight); 67 | 68 | console.log('waiting for settlement'); 69 | await new Promise((r) => setTimeout(r, 7_000)); 70 | await client.syncState(); 71 | } 72 | 73 | // ── consume the freshly minted notes ────────────────────────────────────────────── 74 | const noteIds = (await client.getConsumableNotes(alice.id())).map((rec) => 75 | rec.inputNoteRecord().id().toString(), 76 | ); 77 | 78 | { 79 | const txResult = await client.executeTransaction( 80 | alice.id(), 81 | client.newConsumeTransactionRequest(noteIds), 82 | ); 83 | const proven = await client.proveTransaction(txResult, prover); 84 | await client.syncState(); 85 | const submissionHeight = await client.submitProvenTransaction( 86 | proven, 87 | txResult, 88 | ); 89 | await client.applyTransaction(txResult, submissionHeight); 90 | } 91 | 92 | // ── build 3 P2ID notes (100 MID each) ───────────────────────────────────────────── 93 | const recipientAddresses = [ 94 | 'mtst1aqezqc90x7dkzypr9m5fmlpp85w6cl04', 95 | 'mtst1apjg2ul76wrkxyr5qlcnczaskypa4ljn', 96 | 'mtst1arpee6y9cm8t7ypn33pc8fzj6gkzz7kd', 97 | ]; 98 | 99 | const assets = new NoteAssets([new FungibleAsset(faucet.id(), BigInt(100))]); 100 | 101 | const p2idNotes = recipientAddresses.map((addr) => { 102 | const receiverAccountId = Address.fromBech32(addr).accountId(); 103 | const note = Note.createP2IDNote( 104 | alice.id(), 105 | receiverAccountId, 106 | assets, 107 | NoteType.Public, 108 | new Felt(BigInt(0)), 109 | ); 110 | 111 | return OutputNote.full(note); 112 | }); 113 | 114 | // ── create all P2ID notes ─────────────────────────────────────────────────────────────── 115 | await client.submitNewTransaction( 116 | alice.id(), 117 | new TransactionRequestBuilder() 118 | .withOwnOutputNotes(new OutputNoteArray(p2idNotes)) 119 | .build(), 120 | ); 121 | 122 | console.log('All notes created ✅'); 123 | } 124 | -------------------------------------------------------------------------------- /rust-client/src/bin/delegated_prover.rs: -------------------------------------------------------------------------------- 1 | use miden_client::auth::AuthSecretKey; 2 | use miden_lib::account::auth::AuthRpoFalcon512; 3 | use rand::{rngs::StdRng, RngCore}; 4 | use std::sync::Arc; 5 | 6 | use miden_client::{ 7 | account::component::BasicWallet, 8 | builder::ClientBuilder, 9 | keystore::FilesystemKeyStore, 10 | rpc::{Endpoint, GrpcClient}, 11 | transaction::{TransactionProver, TransactionRequestBuilder}, 12 | ClientError, RemoteTransactionProver, 13 | }; 14 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 15 | use miden_objects::account::{AccountBuilder, AccountStorageMode, AccountType}; 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), ClientError> { 19 | // Initialize client 20 | let endpoint = Endpoint::testnet(); 21 | let timeout_ms = 10_000; 22 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 23 | 24 | // Initialize keystore 25 | let keystore_path = std::path::PathBuf::from("./keystore"); 26 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 27 | 28 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 29 | 30 | let mut client = ClientBuilder::new() 31 | .rpc(rpc_client) 32 | .sqlite_store(store_path) 33 | .authenticator(keystore.clone()) 34 | .in_debug_mode(true.into()) 35 | .build() 36 | .await?; 37 | 38 | let sync_summary = client.sync_state().await.unwrap(); 39 | println!("Latest block: {}", sync_summary.block_num); 40 | 41 | // Create Alice's account 42 | let mut init_seed = [0_u8; 32]; 43 | client.rng().fill_bytes(&mut init_seed); 44 | 45 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 46 | 47 | let alice_account = AccountBuilder::new(init_seed) 48 | .account_type(AccountType::RegularAccountImmutableCode) 49 | .storage_mode(AccountStorageMode::Private) 50 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 51 | .with_component(BasicWallet) 52 | .build() 53 | .unwrap(); 54 | 55 | client.add_account(&alice_account, false).await?; 56 | keystore.add_key(&key_pair).unwrap(); 57 | 58 | // ------------------------------------------------------------------------- 59 | // Setup the remote tx prover 60 | // ------------------------------------------------------------------------- 61 | let remote_tx_prover: RemoteTransactionProver = 62 | RemoteTransactionProver::new("https://tx-prover.testnet.miden.io"); 63 | let tx_prover: Arc = Arc::new(remote_tx_prover); 64 | 65 | // We use a dummy transaction request to showcase delegated proving. 66 | // The only effect of this tx should be increasing Alice's nonce. 67 | println!("Alice nonce initial: {:?}", alice_account.nonce()); 68 | let script_code = "begin push.1 drop end"; 69 | let tx_script = client 70 | .script_builder() 71 | .compile_tx_script(script_code) 72 | .unwrap(); 73 | 74 | let transaction_request = TransactionRequestBuilder::new() 75 | .custom_script(tx_script) 76 | .build() 77 | .unwrap(); 78 | 79 | // Step 1: Execute the transaction locally 80 | println!("Executing transaction..."); 81 | let tx_result = client 82 | .execute_transaction(alice_account.id(), transaction_request) 83 | .await?; 84 | 85 | // Step 2: Prove the transaction using the remote prover 86 | println!("Proving transaction with remote prover..."); 87 | let proven_transaction = client.prove_transaction_with(&tx_result, tx_prover).await?; 88 | 89 | // Step 3: Submit the proven transaction 90 | println!("Submitting proven transaction..."); 91 | let submission_height = client 92 | .submit_proven_transaction(proven_transaction, &tx_result) 93 | .await?; 94 | 95 | // Step 4: Apply the transaction to local store 96 | client 97 | .apply_transaction(&tx_result, submission_height) 98 | .await?; 99 | 100 | println!("Transaction submitted successfully using delegated prover!"); 101 | 102 | client.sync_state().await.unwrap(); 103 | 104 | let account = client 105 | .get_account(alice_account.id()) 106 | .await 107 | .unwrap() 108 | .unwrap(); 109 | 110 | println!("Alice nonce has increased: {:?}", account.account().nonce()); 111 | 112 | Ok(()) 113 | } 114 | -------------------------------------------------------------------------------- /web-client/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | import { createMintConsume } from "../lib/createMintConsume"; 4 | import { multiSendWithDelegatedProver } from "../lib/multiSendWithDelegatedProver"; 5 | import { incrementCounterContract } from "../lib/incrementCounterContract"; 6 | import { unauthenticatedNoteTransfer } from "../lib/unauthenticatedNoteTransfer"; 7 | import { foreignProcedureInvocation } from "../lib/foreignProcedureInvocation"; 8 | 9 | export default function Home() { 10 | const [isCreatingNotes, setIsCreatingNotes] = useState(false); 11 | const [isMultiSendNotes, setIsMultiSendNotes] = useState(false); 12 | const [isIncrementCounter, setIsIncrementCounter] = useState(false); 13 | const [isUnauthenticatedNoteTransfer, setIsUnauthenticatedNoteTransfer] = useState(false); 14 | const [isForeignProcedureInvocation, setIsForeignProcedureInvocation] = useState(false); 15 | 16 | const handleCreateMintConsume = async () => { 17 | setIsCreatingNotes(true); 18 | await createMintConsume(); 19 | setIsCreatingNotes(false); 20 | }; 21 | 22 | const handleMultiSendNotes = async () => { 23 | setIsMultiSendNotes(true); 24 | await multiSendWithDelegatedProver(); 25 | setIsMultiSendNotes(false); 26 | }; 27 | 28 | const handleIncrementCounterContract = async () => { 29 | setIsIncrementCounter(true); 30 | await incrementCounterContract(); 31 | setIsIncrementCounter(false); 32 | }; 33 | 34 | const handleUnauthenticatedNoteTransfer = async () => { 35 | setIsUnauthenticatedNoteTransfer(true); 36 | await unauthenticatedNoteTransfer(); 37 | setIsUnauthenticatedNoteTransfer(false); 38 | }; 39 | 40 | const handleForeignProcedureInvocation = async () => { 41 | setIsForeignProcedureInvocation(true); 42 | await foreignProcedureInvocation(); 43 | setIsForeignProcedureInvocation(false); 44 | }; 45 | 46 | return ( 47 |
48 |
49 |

Miden Web App

50 |

Open your browser console to see WebClient logs.

51 | 52 |
53 | 61 | 62 | 70 | 71 | 79 | 80 | 88 | 89 | 97 |
98 |
99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /web-client/lib/incrementCounterContract.ts: -------------------------------------------------------------------------------- 1 | // lib/incrementCounterContract.ts 2 | export async function incrementCounterContract(): Promise { 3 | if (typeof window === 'undefined') { 4 | console.warn('webClient() can only run in the browser'); 5 | return; 6 | } 7 | 8 | // dynamic import → only in the browser, so WASM is loaded client‑side 9 | const { 10 | Address, 11 | AccountBuilder, 12 | AccountComponent, 13 | AccountStorageMode, 14 | AccountType, 15 | SecretKey, 16 | StorageMap, 17 | StorageSlot, 18 | TransactionRequestBuilder, 19 | WebClient, 20 | } = await import('@demox-labs/miden-sdk'); 21 | 22 | const nodeEndpoint = 'https://rpc.testnet.miden.io'; 23 | const client = await WebClient.createClient(nodeEndpoint); 24 | console.log('Current block number: ', (await client.syncState()).blockNum()); 25 | 26 | // Counter contract code in Miden Assembly 27 | const counterContractCode = ` 28 | use.miden::active_account 29 | use.miden::native_account 30 | use.std::sys 31 | 32 | const.COUNTER_SLOT=0 33 | 34 | #! Inputs: [] 35 | #! Outputs: [count] 36 | export.get_count 37 | push.COUNTER_SLOT 38 | # => [index] 39 | 40 | exec.active_account::get_item 41 | # => [count] 42 | 43 | # clean up stack 44 | movdn.4 dropw 45 | # => [count] 46 | end 47 | 48 | #! Inputs: [] 49 | #! Outputs: [] 50 | export.increment_count 51 | push.COUNTER_SLOT 52 | # => [index] 53 | 54 | exec.active_account::get_item 55 | # => [count] 56 | 57 | add.1 58 | # => [count+1] 59 | 60 | debug.stack 61 | 62 | push.COUNTER_SLOT 63 | # [index, count+1] 64 | 65 | exec.native_account::set_item 66 | # => [OLD_VALUE] 67 | 68 | dropw 69 | # => [] 70 | end 71 | `; 72 | 73 | // Building the counter contract 74 | // Counter contract account id on testnet 75 | const counterContractId = Address.fromBech32( 76 | 'mtst1arjemrxne8lj5qz4mg9c8mtyxg954483', 77 | ).accountId(); 78 | 79 | // Reading the public state of the counter contract from testnet, 80 | // and importing it into the WebClient 81 | let counterContractAccount = await client.getAccount(counterContractId); 82 | if (!counterContractAccount) { 83 | await client.importAccountById(counterContractId); 84 | await client.syncState(); 85 | counterContractAccount = await client.getAccount(counterContractId); 86 | if (!counterContractAccount) { 87 | throw new Error(`Account not found after import: ${counterContractId}`); 88 | } 89 | } 90 | 91 | const builder = client.createScriptBuilder(); 92 | const storageMap = new StorageMap(); 93 | const storageSlotMap = StorageSlot.map(storageMap); 94 | 95 | const mappingAccountComponent = AccountComponent.compile( 96 | counterContractCode, 97 | builder, 98 | [storageSlotMap], 99 | ).withSupportsAllTypes(); 100 | 101 | const walletSeed = new Uint8Array(32); 102 | crypto.getRandomValues(walletSeed); 103 | 104 | const secretKey = SecretKey.rpoFalconWithRNG(walletSeed); 105 | const authComponent = AccountComponent.createAuthComponent(secretKey); 106 | 107 | const accountBuilderResult = new AccountBuilder(walletSeed) 108 | .accountType(AccountType.RegularAccountImmutableCode) 109 | .storageMode(AccountStorageMode.public()) 110 | .withAuthComponent(authComponent) 111 | .withComponent(mappingAccountComponent) 112 | .build(); 113 | 114 | await client.addAccountSecretKeyToWebStore(secretKey); 115 | await client.newAccount(accountBuilderResult.account, false); 116 | 117 | await client.syncState(); 118 | 119 | const accountCodeLib = builder.buildLibrary( 120 | 'external_contract::counter_contract', 121 | counterContractCode, 122 | ); 123 | 124 | builder.linkDynamicLibrary(accountCodeLib); 125 | 126 | // Building the transaction script which will call the counter contract 127 | const txScriptCode = ` 128 | use.external_contract::counter_contract 129 | begin 130 | call.counter_contract::increment_count 131 | end 132 | `; 133 | 134 | const txScript = builder.compileTxScript(txScriptCode); 135 | const txIncrementRequest = new TransactionRequestBuilder() 136 | .withCustomScript(txScript) 137 | .build(); 138 | 139 | // Executing the transaction script against the counter contract 140 | await client.submitNewTransaction( 141 | counterContractAccount.id(), 142 | txIncrementRequest, 143 | ); 144 | 145 | // Sync state 146 | await client.syncState(); 147 | 148 | // Logging the count of counter contract 149 | const counter = await client.getAccount(counterContractAccount.id()); 150 | 151 | // Here we get the first Word from storage of the counter contract 152 | // A word is comprised of 4 Felts, 2**64 - 2**32 + 1 153 | const count = counter?.storage().getItem(0); 154 | 155 | // Converting the Word represented as a hex to a single integer value 156 | const counterValue = Number( 157 | BigInt('0x' + count!.toHex().slice(-16).match(/../g)!.reverse().join('')), 158 | ); 159 | 160 | console.log('Count: ', counterValue); 161 | } 162 | -------------------------------------------------------------------------------- /docs/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@docusaurus/types"; 2 | import { themes as prismThemes } from "prism-react-renderer"; 3 | 4 | // If your content lives in docs/src, set DOCS_PATH='src'; else '.' 5 | const DOCS_PATH = 6 | process.env.DOCS_PATH || (require("fs").existsSync("src") ? "src" : "."); 7 | 8 | const config: Config = { 9 | title: "Docs Dev Preview", 10 | url: "http://localhost:3000", 11 | baseUrl: "/", 12 | trailingSlash: false, 13 | 14 | // Minimal classic preset: docs only, autogenerated sidebars, same math plugins as prod 15 | presets: [ 16 | [ 17 | "classic", 18 | { 19 | docs: { 20 | path: DOCS_PATH, // '../docs' is implied because we are already inside docs/ 21 | routeBasePath: "/", // mount docs at root for quick preview 22 | sidebarPath: "./sidebars.ts", 23 | remarkPlugins: [require("remark-math")], 24 | rehypePlugins: [require("rehype-katex")], 25 | versions: { 26 | current: { 27 | label: `stable`, 28 | }, 29 | }, 30 | }, 31 | blog: false, 32 | pages: false, 33 | theme: { 34 | customCss: "./styles.css", 35 | }, 36 | }, 37 | ], 38 | ], 39 | 40 | plugins: [ 41 | [ 42 | "@cmfcmf/docusaurus-search-local", 43 | { 44 | // whether to index docs pages 45 | indexDocs: true, 46 | 47 | // whether to index blog pages 48 | indexBlog: false, 49 | 50 | // whether to index static pages 51 | indexPages: false, 52 | 53 | // language of your documentation, see next section 54 | language: "en", 55 | 56 | // setting this to "none" will prevent the default CSS to be included. The default CSS 57 | // comes from autocomplete-theme-classic, which you can read more about here: 58 | // https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-theme-classic/ 59 | style: undefined, 60 | 61 | // lunr.js-specific settings 62 | lunr: { 63 | // When indexing your documents, their content is split into "tokens". 64 | // Text entered into the search box is also tokenized. 65 | // This setting configures the separator used to determine where to split the text into tokens. 66 | // By default, it splits the text at whitespace and dashes. 67 | // 68 | // Note: Does not work for "ja" and "th" languages, since these use a different tokenizer. 69 | tokenizerSeparator: /[\s\-]+/, 70 | // https://lunrjs.com/guides/customising.html#similarity-tuning 71 | // 72 | // This parameter controls the importance given to the length of a document and its fields. This 73 | // value must be between 0 and 1, and by default it has a value of 0.75. Reducing this value 74 | // reduces the effect of different length documents on a term's importance to that document. 75 | b: 0.75, 76 | // This controls how quickly the boost given by a common word reaches saturation. Increasing it 77 | // will slow down the rate of saturation and lower values result in quicker saturation. The 78 | // default value is 1.2. If the collection of documents being indexed have high occurrences 79 | // of words that are not covered by a stop word filter, these words can quickly dominate any 80 | // similarity calculation. In these cases, this value can be reduced to get more balanced results. 81 | k1: 1.2, 82 | // By default, we rank pages where the search term appears in the title higher than pages where 83 | // the search term appears in just the text. This is done by "boosting" title matches with a 84 | // higher value than content matches. The concrete boosting behavior can be controlled by changing 85 | // the following settings. 86 | titleBoost: 5, 87 | contentBoost: 1, 88 | tagsBoost: 3, 89 | parentCategoriesBoost: 2, // Only used when indexing is enabled for categories 90 | }, 91 | }, 92 | ], 93 | ], 94 | 95 | themeConfig: 96 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 97 | { 98 | colorMode: { 99 | defaultMode: "light", 100 | disableSwitch: true, 101 | }, 102 | prism: { 103 | theme: prismThemes.oneLight, 104 | darkTheme: prismThemes.oneDark, 105 | additionalLanguages: ["rust", "solidity", "toml", "yaml"], 106 | }, 107 | navbar: { 108 | logo: { 109 | src: "img/logo.png", 110 | alt: "Miden Logo", 111 | height: 240, 112 | }, 113 | title: "MIDEN", 114 | items: [ 115 | { 116 | type: "docsVersionDropdown", 117 | position: "left", 118 | dropdownActiveClassDisabled: true, 119 | }, 120 | { 121 | href: "https://github.com/0xMiden/", 122 | label: "GitHub", 123 | position: "right", 124 | }, 125 | ], 126 | }, 127 | }, 128 | }; 129 | export default config; 130 | -------------------------------------------------------------------------------- /rust-client/src/bin/counter_contract_increment.rs: -------------------------------------------------------------------------------- 1 | use miden_lib::transaction::TransactionKernel; 2 | use rand::rngs::StdRng; 3 | use std::{fs, path::Path, sync::Arc}; 4 | 5 | use miden_client::{ 6 | account::AccountId, 7 | assembly::{Assembler, DefaultSourceManager, LibraryPath, Module, ModuleKind}, 8 | builder::ClientBuilder, 9 | keystore::FilesystemKeyStore, 10 | rpc::{Endpoint, GrpcClient}, 11 | transaction::TransactionRequestBuilder, 12 | ClientError, 13 | }; 14 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 15 | 16 | fn create_library( 17 | assembler: Assembler, 18 | library_path: &str, 19 | source_code: &str, 20 | ) -> Result> { 21 | let source_manager = Arc::new(DefaultSourceManager::default()); 22 | let module = Module::parser(ModuleKind::Library).parse_str( 23 | LibraryPath::new(library_path)?, 24 | source_code, 25 | &source_manager, 26 | )?; 27 | let library = assembler.clone().assemble_library([module])?; 28 | Ok(library) 29 | } 30 | 31 | #[tokio::main] 32 | async fn main() -> Result<(), ClientError> { 33 | // Initialize client 34 | let endpoint = Endpoint::testnet(); 35 | let timeout_ms = 10_000; 36 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 37 | 38 | // Initialize keystore 39 | let keystore_path = std::path::PathBuf::from("./keystore"); 40 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 41 | 42 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 43 | 44 | let mut client = ClientBuilder::new() 45 | .rpc(rpc_client) 46 | .sqlite_store(store_path) 47 | .authenticator(keystore.clone()) 48 | .in_debug_mode(true.into()) 49 | .build() 50 | .await?; 51 | 52 | let sync_summary = client.sync_state().await.unwrap(); 53 | println!("Latest block: {}", sync_summary.block_num); 54 | 55 | // ------------------------------------------------------------------------- 56 | // STEP 1: Read the Public State of the Counter Contract 57 | // ------------------------------------------------------------------------- 58 | println!("\n[STEP 1] Reading data from public state"); 59 | 60 | // Define the Counter Contract account id from counter contract deploy 61 | let (_, counter_contract_id) = 62 | AccountId::from_bech32("mtst1arjemrxne8lj5qz4mg9c8mtyxg954483").unwrap(); 63 | 64 | client 65 | .import_account_by_id(counter_contract_id) 66 | .await 67 | .unwrap(); 68 | 69 | let counter_contract_details = client.get_account(counter_contract_id).await.unwrap(); 70 | 71 | let counter_contract = if let Some(account_record) = counter_contract_details { 72 | // Clone the account to get an owned instance 73 | let account = account_record.account().clone(); 74 | println!( 75 | "Account details: {:?}", 76 | account.storage().slots().first().unwrap() 77 | ); 78 | account // Now returns an owned account 79 | } else { 80 | panic!("Counter contract not found!"); 81 | }; 82 | 83 | // ------------------------------------------------------------------------- 84 | // STEP 2: Call the Counter Contract with a script 85 | // ------------------------------------------------------------------------- 86 | println!("\n[STEP 2] Call the increment_count procedure in the counter contract"); 87 | 88 | // Load the MASM script referencing the increment procedure 89 | let script_path = Path::new("../masm/scripts/counter_script.masm"); 90 | let script_code = fs::read_to_string(script_path).unwrap(); 91 | 92 | let counter_path = Path::new("../masm/accounts/counter.masm"); 93 | let counter_code = fs::read_to_string(counter_path).unwrap(); 94 | 95 | let assembler = TransactionKernel::assembler().with_debug_mode(true); 96 | let account_component_lib = create_library( 97 | assembler.clone(), 98 | "external_contract::counter_contract", 99 | &counter_code, 100 | ) 101 | .unwrap(); 102 | 103 | let tx_script = client 104 | .script_builder() 105 | .with_dynamically_linked_library(&account_component_lib) 106 | .unwrap() 107 | .compile_tx_script(&script_code) 108 | .unwrap(); 109 | 110 | // Build a transaction request with the custom script 111 | let tx_increment_request = TransactionRequestBuilder::new() 112 | .custom_script(tx_script) 113 | .build() 114 | .unwrap(); 115 | 116 | // Execute and submit the transaction 117 | let tx_id = client 118 | .submit_new_transaction(counter_contract.id(), tx_increment_request) 119 | .await 120 | .unwrap(); 121 | 122 | println!( 123 | "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", 124 | tx_id 125 | ); 126 | 127 | client.sync_state().await.unwrap(); 128 | 129 | // Retrieve updated contract data to see the incremented counter 130 | let account = client.get_account(counter_contract.id()).await.unwrap(); 131 | println!( 132 | "counter contract storage: {:?}", 133 | account.unwrap().account().storage().get_item(0) 134 | ); 135 | Ok(()) 136 | } 137 | -------------------------------------------------------------------------------- /rust-client/src/bin/counter_contract_deploy.rs: -------------------------------------------------------------------------------- 1 | use miden_lib::account::auth::NoAuth; 2 | use miden_lib::transaction::TransactionKernel; 3 | use rand::{rngs::StdRng, RngCore}; 4 | use std::{fs, path::Path, sync::Arc}; 5 | 6 | use miden_client::{ 7 | address::NetworkId, 8 | assembly::{Assembler, DefaultSourceManager, LibraryPath, Module, ModuleKind}, 9 | builder::ClientBuilder, 10 | keystore::FilesystemKeyStore, 11 | rpc::{Endpoint, GrpcClient}, 12 | transaction::TransactionRequestBuilder, 13 | ClientError, 14 | }; 15 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 16 | use miden_objects::{ 17 | account::{AccountBuilder, AccountComponent, AccountStorageMode, AccountType, StorageSlot}, 18 | Word, 19 | }; 20 | 21 | fn create_library( 22 | assembler: Assembler, 23 | library_path: &str, 24 | source_code: &str, 25 | ) -> Result> { 26 | let source_manager = Arc::new(DefaultSourceManager::default()); 27 | let module = Module::parser(ModuleKind::Library).parse_str( 28 | LibraryPath::new(library_path)?, 29 | source_code, 30 | &source_manager, 31 | )?; 32 | let library = assembler.clone().assemble_library([module])?; 33 | Ok(library) 34 | } 35 | 36 | #[tokio::main] 37 | async fn main() -> Result<(), ClientError> { 38 | // Initialize client 39 | let endpoint = Endpoint::testnet(); 40 | let timeout_ms = 10_000; 41 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 42 | 43 | // Initialize keystore 44 | let keystore_path = std::path::PathBuf::from("./keystore"); 45 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 46 | 47 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 48 | 49 | let mut client = ClientBuilder::new() 50 | .rpc(rpc_client) 51 | .sqlite_store(store_path) 52 | .authenticator(keystore.clone()) 53 | .in_debug_mode(true.into()) 54 | .build() 55 | .await?; 56 | 57 | let sync_summary = client.sync_state().await.unwrap(); 58 | println!("Latest block: {}", sync_summary.block_num); 59 | 60 | // ------------------------------------------------------------------------- 61 | // STEP 1: Create a basic counter contract 62 | // ------------------------------------------------------------------------- 63 | println!("\n[STEP 1] Creating counter contract."); 64 | 65 | // Load the MASM file for the counter contract 66 | let counter_path = Path::new("../masm/accounts/counter.masm"); 67 | let counter_code = fs::read_to_string(counter_path).unwrap(); 68 | 69 | // Compile the account code into `AccountComponent` with one storage slot 70 | let counter_component = AccountComponent::compile( 71 | &counter_code, 72 | TransactionKernel::assembler(), 73 | vec![StorageSlot::Value(Word::default())], 74 | ) 75 | .unwrap() 76 | .with_supports_all_types(); 77 | 78 | // Init seed for the counter contract 79 | let mut seed = [0_u8; 32]; 80 | client.rng().fill_bytes(&mut seed); 81 | 82 | // Build the new `Account` with the component 83 | let counter_contract = AccountBuilder::new(seed) 84 | .account_type(AccountType::RegularAccountImmutableCode) 85 | .storage_mode(AccountStorageMode::Public) 86 | .with_component(counter_component.clone()) 87 | .with_auth_component(NoAuth) 88 | .build() 89 | .unwrap(); 90 | 91 | println!( 92 | "counter_contract commitment: {:?}", 93 | counter_contract.commitment() 94 | ); 95 | println!("counter_contract id: {:?}", counter_contract.id()); 96 | println!("counter_contract storage: {:?}", counter_contract.storage()); 97 | 98 | client.add_account(&counter_contract, false).await.unwrap(); 99 | 100 | // ------------------------------------------------------------------------- 101 | // STEP 2: Call the Counter Contract with a script 102 | // ------------------------------------------------------------------------- 103 | println!("\n[STEP 2] Call Counter Contract With Script"); 104 | 105 | // Load the MASM script referencing the increment procedure 106 | let script_path = Path::new("../masm/scripts/counter_script.masm"); 107 | let script_code = fs::read_to_string(script_path).unwrap(); 108 | 109 | // Create a library from the counter contract code 110 | let assembler = TransactionKernel::assembler().with_debug_mode(true); 111 | let account_component_lib = create_library( 112 | assembler.clone(), 113 | "external_contract::counter_contract", 114 | &counter_code, 115 | ) 116 | .unwrap(); 117 | 118 | let tx_script = client 119 | .script_builder() 120 | .with_dynamically_linked_library(&account_component_lib) 121 | .unwrap() 122 | .compile_tx_script(&script_code) 123 | .unwrap(); 124 | 125 | // Build a transaction request with the custom script 126 | let tx_increment_request = TransactionRequestBuilder::new() 127 | .custom_script(tx_script) 128 | .build() 129 | .unwrap(); 130 | 131 | // Execute and submit the transaction 132 | let tx_id = client 133 | .submit_new_transaction(counter_contract.id(), tx_increment_request) 134 | .await 135 | .unwrap(); 136 | 137 | println!( 138 | "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", 139 | tx_id 140 | ); 141 | 142 | println!( 143 | "Counter contract id: {:?}", 144 | counter_contract.id().to_bech32(NetworkId::Testnet) 145 | ); 146 | 147 | client.sync_state().await.unwrap(); 148 | 149 | // Retrieve updated contract data to see the incremented counter 150 | let account = client.get_account(counter_contract.id()).await.unwrap(); 151 | println!( 152 | "counter contract storage: {:?}", 153 | account.unwrap().account().storage().get_item(0) 154 | ); 155 | 156 | Ok(()) 157 | } 158 | -------------------------------------------------------------------------------- /web-client/lib/unauthenticatedNoteTransfer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Demonstrates unauthenticated note transfer chain using a delegated prover on the Miden Network 3 | * Creates a chain of P2ID (Pay to ID) notes: Alice → wallet 1 → wallet 2 → wallet 3 → wallet 4 4 | * 5 | * @throws {Error} If the function cannot be executed in a browser environment 6 | */ 7 | export async function unauthenticatedNoteTransfer(): Promise { 8 | // Ensure this runs only in a browser context 9 | if (typeof window === 'undefined') return console.warn('Run in browser'); 10 | 11 | const { 12 | WebClient, 13 | AccountStorageMode, 14 | NoteType, 15 | TransactionProver, 16 | Note, 17 | NoteAssets, 18 | OutputNoteArray, 19 | Felt, 20 | FungibleAsset, 21 | NoteAndArgsArray, 22 | NoteAndArgs, 23 | TransactionRequestBuilder, 24 | OutputNote, 25 | } = await import('@demox-labs/miden-sdk'); 26 | 27 | const client = await WebClient.createClient('https://rpc.testnet.miden.io'); 28 | const prover = TransactionProver.newRemoteProver( 29 | 'https://tx-prover.testnet.miden.io', 30 | ); 31 | 32 | console.log('Latest block:', (await client.syncState()).blockNum()); 33 | 34 | // ── Creating new account ────────────────────────────────────────────────────── 35 | console.log('Creating accounts'); 36 | 37 | console.log('Creating account for Alice…'); 38 | const alice = await client.newWallet(AccountStorageMode.public(), true, 0); 39 | console.log('Alice accout ID:', alice.id().toString()); 40 | 41 | const wallets = []; 42 | for (let i = 0; i < 5; i++) { 43 | const wallet = await client.newWallet(AccountStorageMode.public(), true, 0); 44 | wallets.push(wallet); 45 | console.log('wallet ', i.toString(), wallet.id().toString()); 46 | } 47 | 48 | // ── Creating new faucet ────────────────────────────────────────────────────── 49 | const faucet = await client.newFaucet( 50 | AccountStorageMode.public(), 51 | false, 52 | 'MID', 53 | 8, 54 | BigInt(1_000_000), 55 | 0, 56 | ); 57 | console.log('Faucet ID:', faucet.id().toString()); 58 | 59 | // ── mint 10 000 MID to Alice ────────────────────────────────────────────────────── 60 | { 61 | const txResult = await client.executeTransaction( 62 | faucet.id(), 63 | client.newMintTransactionRequest( 64 | alice.id(), 65 | faucet.id(), 66 | NoteType.Public, 67 | BigInt(10_000), 68 | ), 69 | ); 70 | const proven = await client.proveTransaction(txResult, prover); 71 | const submissionHeight = await client.submitProvenTransaction( 72 | proven, 73 | txResult, 74 | ); 75 | await client.applyTransaction(txResult, submissionHeight); 76 | } 77 | 78 | console.log('Waiting for settlement'); 79 | await new Promise((r) => setTimeout(r, 7_000)); 80 | await client.syncState(); 81 | 82 | // ── Consume the freshly minted note ────────────────────────────────────────────── 83 | const noteIds = (await client.getConsumableNotes(alice.id())).map((rec) => 84 | rec.inputNoteRecord().id().toString(), 85 | ); 86 | 87 | { 88 | const txResult = await client.executeTransaction( 89 | alice.id(), 90 | client.newConsumeTransactionRequest(noteIds), 91 | ); 92 | const proven = await client.proveTransaction(txResult, prover); 93 | const submissionHeight = await client.submitProvenTransaction( 94 | proven, 95 | txResult, 96 | ); 97 | await client.applyTransaction(txResult, submissionHeight); 98 | await client.syncState(); 99 | } 100 | 101 | // ── Create unauthenticated note transfer chain ───────────────────────────────────────────── 102 | // Alice → wallet 1 → wallet 2 → wallet 3 → wallet 4 103 | for (let i = 0; i < wallets.length; i++) { 104 | console.log(`\nUnauthenticated tx ${i + 1}`); 105 | 106 | // Determine sender and receiver for this iteration 107 | const sender = i === 0 ? alice : wallets[i - 1]; 108 | const receiver = wallets[i]; 109 | 110 | console.log('Sender:', sender.id().toString()); 111 | console.log('Receiver:', receiver.id().toString()); 112 | 113 | const assets = new NoteAssets([new FungibleAsset(faucet.id(), BigInt(50))]); 114 | const p2idNote = Note.createP2IDNote( 115 | sender.id(), 116 | receiver.id(), 117 | assets, 118 | NoteType.Public, 119 | new Felt(BigInt(0)), // aux value 120 | ); 121 | 122 | const outputP2ID = OutputNote.full(p2idNote); 123 | 124 | console.log('Creating P2ID note...'); 125 | { 126 | const txResult = await client.executeTransaction( 127 | sender.id(), 128 | new TransactionRequestBuilder() 129 | .withOwnOutputNotes(new OutputNoteArray([outputP2ID])) 130 | .build(), 131 | ); 132 | const proven = await client.proveTransaction(txResult, prover); 133 | const submissionHeight = await client.submitProvenTransaction( 134 | proven, 135 | txResult, 136 | ); 137 | await client.applyTransaction(txResult, submissionHeight); 138 | } 139 | 140 | console.log('Consuming P2ID note...'); 141 | 142 | const noteIdAndArgs = new NoteAndArgs(p2idNote, null); 143 | 144 | const consumeRequest = new TransactionRequestBuilder() 145 | .withUnauthenticatedInputNotes(new NoteAndArgsArray([noteIdAndArgs])) 146 | .build(); 147 | 148 | { 149 | const txResult = await client.executeTransaction( 150 | receiver.id(), 151 | consumeRequest, 152 | ); 153 | const proven = await client.proveTransaction(txResult, prover); 154 | const submissionHeight = await client.submitProvenTransaction( 155 | proven, 156 | txResult, 157 | ); 158 | const txExecutionResult = await client.applyTransaction( 159 | txResult, 160 | submissionHeight, 161 | ); 162 | 163 | const txId = txExecutionResult 164 | .executedTransaction() 165 | .id() 166 | .toHex() 167 | .toString(); 168 | 169 | console.log( 170 | `Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/${txId}`, 171 | ); 172 | } 173 | } 174 | 175 | console.log('Asset transfer chain completed ✅'); 176 | } 177 | -------------------------------------------------------------------------------- /rust-client/src/bin/mapping_example.rs: -------------------------------------------------------------------------------- 1 | use miden_lib::account::auth::NoAuth; 2 | use miden_lib::transaction::TransactionKernel; 3 | use rand::{rngs::StdRng, RngCore}; 4 | use std::{fs, path::Path, sync::Arc}; 5 | 6 | use miden_client::{ 7 | assembly::{Assembler, DefaultSourceManager, LibraryPath, Module, ModuleKind}, 8 | builder::ClientBuilder, 9 | keystore::FilesystemKeyStore, 10 | rpc::{Endpoint, GrpcClient}, 11 | transaction::TransactionRequestBuilder, 12 | ClientError, 13 | }; 14 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 15 | use miden_objects::{ 16 | account::{ 17 | AccountBuilder, AccountComponent, AccountStorageMode, AccountType, StorageMap, StorageSlot, 18 | }, 19 | Felt, Word, 20 | }; 21 | 22 | fn create_library( 23 | assembler: Assembler, 24 | library_path: &str, 25 | source_code: &str, 26 | ) -> Result> { 27 | let source_manager = Arc::new(DefaultSourceManager::default()); 28 | let module = Module::parser(ModuleKind::Library).parse_str( 29 | LibraryPath::new(library_path)?, 30 | source_code, 31 | &source_manager, 32 | )?; 33 | let library = assembler.clone().assemble_library([module])?; 34 | Ok(library) 35 | } 36 | 37 | #[tokio::main] 38 | async fn main() -> Result<(), ClientError> { 39 | // Initialize client 40 | let endpoint = Endpoint::testnet(); 41 | let timeout_ms = 10_000; 42 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 43 | 44 | // Initialize keystore 45 | let keystore_path = std::path::PathBuf::from("./keystore"); 46 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 47 | 48 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 49 | 50 | let mut client = ClientBuilder::new() 51 | .rpc(rpc_client) 52 | .sqlite_store(store_path) 53 | .authenticator(keystore.clone()) 54 | .in_debug_mode(true.into()) 55 | .build() 56 | .await?; 57 | 58 | let sync_summary = client.sync_state().await.unwrap(); 59 | println!("Latest block: {}", sync_summary.block_num); 60 | 61 | // ------------------------------------------------------------------------- 62 | // STEP 1: Deploy a smart contract with a mapping 63 | // ------------------------------------------------------------------------- 64 | println!("\n[STEP 1] Deploy a smart contract with a mapping"); 65 | 66 | // Load the MASM file for the counter contract 67 | let file_path = Path::new("../masm/accounts/mapping_example_contract.masm"); 68 | let account_code = fs::read_to_string(file_path).unwrap(); 69 | 70 | // Prepare assembler (debug mode = true) 71 | let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true); 72 | 73 | // Using an empty storage value in slot 0 since this is usually reserved 74 | // for the account pub_key and metadata 75 | let empty_storage_slot = StorageSlot::Value(Word::default()); 76 | 77 | // initialize storage map 78 | let storage_map = StorageMap::new(); 79 | let storage_slot_map = StorageSlot::Map(storage_map.clone()); 80 | 81 | // Compile the account code into `AccountComponent` with one storage slot 82 | let mapping_contract_component = AccountComponent::compile( 83 | &account_code, 84 | assembler.clone(), 85 | vec![empty_storage_slot, storage_slot_map], 86 | ) 87 | .unwrap() 88 | .with_supports_all_types(); 89 | 90 | // Init seed for the counter contract 91 | let mut init_seed = [0_u8; 32]; 92 | client.rng().fill_bytes(&mut init_seed); 93 | 94 | // Build the new `Account` with the component 95 | let mapping_example_contract = AccountBuilder::new(init_seed) 96 | .account_type(AccountType::RegularAccountImmutableCode) 97 | .storage_mode(AccountStorageMode::Public) 98 | .with_component(mapping_contract_component.clone()) 99 | .with_auth_component(NoAuth) 100 | .build() 101 | .unwrap(); 102 | 103 | client 104 | .add_account(&mapping_example_contract, false) 105 | .await 106 | .unwrap(); 107 | 108 | // ------------------------------------------------------------------------- 109 | // STEP 2: Call the Mapping Contract with a Script 110 | // ------------------------------------------------------------------------- 111 | println!("\n[STEP 2] Call Mapping Contract With Script"); 112 | 113 | let script_code = 114 | fs::read_to_string(Path::new("../masm/scripts/mapping_example_script.masm")).unwrap(); 115 | 116 | // Create the library from the account source code using the helper function. 117 | let account_component_lib = create_library( 118 | assembler.clone(), 119 | "miden_by_example::mapping_example_contract", 120 | &account_code, 121 | ) 122 | .unwrap(); 123 | 124 | // Compile the transaction script with the library. 125 | let tx_script = client 126 | .script_builder() 127 | .with_dynamically_linked_library(&account_component_lib) 128 | .unwrap() 129 | .compile_tx_script(&script_code) 130 | .unwrap(); 131 | 132 | // Build a transaction request with the custom script 133 | let tx_increment_request = TransactionRequestBuilder::new() 134 | .custom_script(tx_script) 135 | .build() 136 | .unwrap(); 137 | 138 | // Execute and submit the transaction 139 | let tx_id = client 140 | .submit_new_transaction(mapping_example_contract.id(), tx_increment_request) 141 | .await 142 | .unwrap(); 143 | 144 | println!( 145 | "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", 146 | tx_id 147 | ); 148 | 149 | client.sync_state().await.unwrap(); 150 | 151 | let account = client 152 | .get_account(mapping_example_contract.id()) 153 | .await 154 | .unwrap(); 155 | let index = 1; 156 | let key = [Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(0)].into(); 157 | println!( 158 | "Mapping state\n Index: {:?}\n Key: {:?}\n Value: {:?}", 159 | index, 160 | key, 161 | account 162 | .unwrap() 163 | .account() 164 | .storage() 165 | .get_map_item(index, key) 166 | ); 167 | 168 | Ok(()) 169 | } 170 | -------------------------------------------------------------------------------- /rust-client/src/bin/oracle_data_query.rs: -------------------------------------------------------------------------------- 1 | use miden_client::{ 2 | assembly::{Assembler, DefaultSourceManager, LibraryPath, Module, ModuleKind}, 3 | builder::ClientBuilder, 4 | keystore::FilesystemKeyStore, 5 | rpc::{ 6 | domain::account::{AccountStorageRequirements, StorageMapKey}, 7 | Endpoint, GrpcClient, 8 | }, 9 | transaction::{ForeignAccount, TransactionRequestBuilder}, 10 | Client, ClientError, 11 | }; 12 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 13 | use miden_lib::{account::auth::NoAuth, transaction::TransactionKernel}; 14 | use miden_objects::{ 15 | account::{AccountComponent, AccountId, AccountStorageMode, AccountType, StorageSlot}, 16 | Felt, Word, ZERO, 17 | }; 18 | use rand::{rngs::StdRng, RngCore}; 19 | use std::{fs, path::Path, sync::Arc}; 20 | 21 | /// Import the oracle + its publishers and return the ForeignAccount list 22 | /// Due to Pragma's decentralized oracle architecture, we need to get the 23 | /// list of all data publisher accounts to read price from via a nested FPI call 24 | pub async fn get_oracle_foreign_accounts( 25 | client: &mut Client>, 26 | oracle_account_id: AccountId, 27 | trading_pair: u64, 28 | ) -> Result, ClientError> { 29 | client.import_account_by_id(oracle_account_id).await?; 30 | 31 | let oracle_record = client 32 | .get_account(oracle_account_id) 33 | .await 34 | .expect("RPC failed") 35 | .expect("oracle account not found"); 36 | 37 | let storage = oracle_record.account().storage(); 38 | let publisher_count = storage.get_item(1).unwrap()[0].as_int(); 39 | 40 | let publisher_ids: Vec = (1..publisher_count.saturating_sub(1)) 41 | .map(|i| { 42 | let digest = storage.get_item(2 + i as u8).unwrap(); 43 | let words: Word = digest.into(); 44 | AccountId::new_unchecked([words[3], words[2]]) 45 | }) 46 | .collect(); 47 | 48 | let mut foreign_accounts = Vec::with_capacity(publisher_ids.len() + 1); 49 | 50 | for pid in publisher_ids { 51 | client.import_account_by_id(pid).await?; 52 | 53 | foreign_accounts.push(ForeignAccount::public( 54 | pid, 55 | AccountStorageRequirements::new([( 56 | 1u8, 57 | &[StorageMapKey::from([ 58 | ZERO, 59 | ZERO, 60 | ZERO, 61 | Felt::new(trading_pair), 62 | ])], 63 | )]), 64 | )?); 65 | } 66 | 67 | foreign_accounts.push(ForeignAccount::public( 68 | oracle_account_id, 69 | AccountStorageRequirements::default(), 70 | )?); 71 | 72 | Ok(foreign_accounts) 73 | } 74 | 75 | fn create_library( 76 | assembler: Assembler, 77 | library_path: &str, 78 | source_code: &str, 79 | ) -> Result> { 80 | let source_manager = Arc::new(DefaultSourceManager::default()); 81 | let module = Module::parser(ModuleKind::Library).parse_str( 82 | LibraryPath::new(library_path)?, 83 | source_code, 84 | &source_manager, 85 | )?; 86 | let library = assembler.clone().assemble_library([module])?; 87 | Ok(library) 88 | } 89 | 90 | #[tokio::main] 91 | async fn main() -> Result<(), ClientError> { 92 | // ------------------------------------------------------------------------- 93 | // Initialize Client 94 | // ------------------------------------------------------------------------- 95 | let endpoint = Endpoint::testnet(); 96 | let timeout_ms = 10_000; 97 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 98 | 99 | let keystore_path = std::path::PathBuf::from("./keystore"); 100 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 101 | 102 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 103 | 104 | let mut client = ClientBuilder::new() 105 | .rpc(rpc_client) 106 | .sqlite_store(store_path) 107 | .authenticator(keystore.clone()) 108 | .in_debug_mode(true.into()) 109 | .build() 110 | .await?; 111 | 112 | println!("Latest block: {}", client.sync_state().await?.block_num); 113 | 114 | // ------------------------------------------------------------------------- 115 | // Get all foreign accounts for oracle data 116 | // ------------------------------------------------------------------------- 117 | let oracle_bech32 = "mtst1qq0zffxzdykm7qqqqdt24cc2du5ghx99"; 118 | let (_, oracle_account_id) = AccountId::from_bech32(oracle_bech32).unwrap(); 119 | let btc_usd_pair_id = 120195681; 120 | let foreign_accounts: Vec = 121 | get_oracle_foreign_accounts(&mut client, oracle_account_id, btc_usd_pair_id).await?; 122 | 123 | println!( 124 | "Oracle accountId prefix: {:?} suffix: {:?}", 125 | oracle_account_id.prefix(), 126 | oracle_account_id.suffix() 127 | ); 128 | 129 | // ------------------------------------------------------------------------- 130 | // Create Oracle Reader contract 131 | // ------------------------------------------------------------------------- 132 | let contract_code = 133 | fs::read_to_string(Path::new("../masm/accounts/oracle_reader.masm")).unwrap(); 134 | 135 | let assembler = TransactionKernel::assembler().with_debug_mode(true); 136 | 137 | let contract_component = AccountComponent::compile( 138 | &contract_code, 139 | assembler, 140 | vec![StorageSlot::Value(Word::default())], 141 | ) 142 | .unwrap() 143 | .with_supports_all_types(); 144 | 145 | let mut seed = [0_u8; 32]; 146 | client.rng().fill_bytes(&mut seed); 147 | 148 | let oracle_reader_contract = miden_objects::account::AccountBuilder::new(seed) 149 | .account_type(AccountType::RegularAccountImmutableCode) 150 | .storage_mode(AccountStorageMode::Public) 151 | .with_component(contract_component.clone()) 152 | .with_auth_component(NoAuth) 153 | .build() 154 | .unwrap(); 155 | 156 | client 157 | .add_account(&oracle_reader_contract, false) 158 | .await 159 | .unwrap(); 160 | 161 | // ------------------------------------------------------------------------- 162 | // Build the script that calls our `get_price` procedure 163 | // ------------------------------------------------------------------------- 164 | let script_path = Path::new("../masm/scripts/oracle_reader_script.masm"); 165 | let script_code = fs::read_to_string(script_path).unwrap(); 166 | 167 | let assembler = TransactionKernel::assembler().with_debug_mode(true); 168 | let library_path = "external_contract::oracle_reader"; 169 | let account_component_lib = 170 | create_library(assembler.clone(), library_path, &contract_code).unwrap(); 171 | 172 | let tx_script = client 173 | .script_builder() 174 | .with_dynamically_linked_library(&account_component_lib) 175 | .unwrap() 176 | .compile_tx_script(&script_code) 177 | .unwrap(); 178 | 179 | let tx_increment_request = TransactionRequestBuilder::new() 180 | .foreign_accounts(foreign_accounts) 181 | .custom_script(tx_script) 182 | .build() 183 | .unwrap(); 184 | 185 | let tx_id = client 186 | .submit_new_transaction(oracle_reader_contract.id(), tx_increment_request) 187 | .await 188 | .unwrap(); 189 | 190 | println!( 191 | "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", 192 | tx_id 193 | ); 194 | 195 | client.sync_state().await.unwrap(); 196 | 197 | Ok(()) 198 | } 199 | -------------------------------------------------------------------------------- /web-client/lib/foreignProcedureInvocation.ts: -------------------------------------------------------------------------------- 1 | // lib/foreignProcedureInvocation.ts 2 | export async function foreignProcedureInvocation(): Promise { 3 | if (typeof window === 'undefined') { 4 | console.warn('foreignProcedureInvocation() can only run in the browser'); 5 | return; 6 | } 7 | 8 | // dynamic import → only in the browser, so WASM is loaded client‑side 9 | const { 10 | AccountBuilder, 11 | AccountComponent, 12 | Address, 13 | AccountType, 14 | MidenArrays, 15 | SecretKey, 16 | StorageSlot, 17 | TransactionRequestBuilder, 18 | ForeignAccount, 19 | AccountStorageRequirements, 20 | WebClient, 21 | AccountStorageMode, 22 | } = await import('@demox-labs/miden-sdk'); 23 | 24 | const nodeEndpoint = 'https://rpc.testnet.miden.io'; 25 | const client = await WebClient.createClient(nodeEndpoint); 26 | console.log('Current block number: ', (await client.syncState()).blockNum()); 27 | 28 | // ------------------------------------------------------------------------- 29 | // STEP 1: Create the Count Reader Contract 30 | // ------------------------------------------------------------------------- 31 | console.log('\n[STEP 1] Creating count reader contract.'); 32 | 33 | // Count reader contract code in Miden Assembly (exactly from count_reader.masm) 34 | const countReaderCode = ` 35 | use.miden::active_account 36 | use.miden::native_account 37 | use.miden::tx 38 | use.std::sys 39 | 40 | # => [account_id_prefix, account_id_suffix, get_count_proc_hash] 41 | export.copy_count 42 | exec.tx::execute_foreign_procedure 43 | # => [count] 44 | 45 | push.0 46 | # [index, count] 47 | 48 | debug.stack 49 | 50 | exec.native_account::set_item dropw 51 | # => [] 52 | 53 | exec.sys::truncate_stack 54 | # => [] 55 | end 56 | `; 57 | 58 | const builder = client.createScriptBuilder(); 59 | const countReaderComponent = AccountComponent.compile( 60 | countReaderCode, 61 | builder, 62 | [StorageSlot.emptyValue()], 63 | ).withSupportsAllTypes(); 64 | 65 | const walletSeed = new Uint8Array(32); 66 | crypto.getRandomValues(walletSeed); 67 | 68 | const secretKey = SecretKey.rpoFalconWithRNG(walletSeed); 69 | const authComponent = AccountComponent.createAuthComponent(secretKey); 70 | 71 | const countReaderContract = new AccountBuilder(walletSeed) 72 | .accountType(AccountType.RegularAccountImmutableCode) 73 | .storageMode(AccountStorageMode.public()) 74 | .withAuthComponent(authComponent) 75 | .withComponent(countReaderComponent) 76 | .build(); 77 | 78 | await client.addAccountSecretKeyToWebStore(secretKey); 79 | await client.syncState(); 80 | 81 | // Create the count reader contract account (using available WebClient API) 82 | console.log('Creating count reader contract account...'); 83 | console.log( 84 | 'Count reader contract ID:', 85 | countReaderContract.account.id().toString(), 86 | ); 87 | 88 | await client.newAccount(countReaderContract.account, false); 89 | 90 | // ------------------------------------------------------------------------- 91 | // STEP 2: Build & Get State of the Counter Contract 92 | // ------------------------------------------------------------------------- 93 | console.log('\n[STEP 2] Building counter contract from public state'); 94 | 95 | // Define the Counter Contract account id from counter contract deploy (same as Rust) 96 | const counterContractId = Address.fromBech32( 97 | 'mtst1arjemrxne8lj5qz4mg9c8mtyxg954483', 98 | ).accountId(); 99 | 100 | // Import the counter contract 101 | let counterContractAccount = await client.getAccount(counterContractId); 102 | if (!counterContractAccount) { 103 | await client.importAccountById(counterContractId); 104 | await client.syncState(); 105 | counterContractAccount = await client.getAccount(counterContractId); 106 | if (!counterContractAccount) { 107 | throw new Error(`Account not found after import: ${counterContractId}`); 108 | } 109 | } 110 | console.log( 111 | 'Account storage slot 0:', 112 | counterContractAccount.storage().getItem(0)?.toHex(), 113 | ); 114 | 115 | // ------------------------------------------------------------------------- 116 | // STEP 3: Call the Counter Contract via Foreign Procedure Invocation (FPI) 117 | // ------------------------------------------------------------------------- 118 | console.log( 119 | '\n[STEP 3] Call counter contract with FPI from count reader contract', 120 | ); 121 | 122 | // Counter contract code (exactly from counter.masm) 123 | const counterContractCode = ` 124 | use.miden::active_account 125 | use miden::native_account 126 | use.std::sys 127 | 128 | const.COUNTER_SLOT=0 129 | 130 | #! Inputs: [] 131 | #! Outputs: [count] 132 | export.get_count 133 | push.COUNTER_SLOT 134 | # => [index] 135 | 136 | exec.active_account::get_item 137 | # => [count] 138 | 139 | # clean up stack 140 | movdn.4 dropw 141 | # => [count] 142 | end 143 | 144 | #! Inputs: [] 145 | #! Outputs: [] 146 | export.increment_count 147 | push.COUNTER_SLOT 148 | # => [index] 149 | 150 | exec.active_account::get_item 151 | # => [count] 152 | 153 | add.1 154 | # => [count+1] 155 | 156 | debug.stack 157 | 158 | push.COUNTER_SLOT 159 | # [index, count+1] 160 | 161 | exec.native_account::set_item 162 | # => [OLD_VALUE] 163 | 164 | dropw 165 | # => [] 166 | end 167 | `; 168 | 169 | // Create the counter contract component to get the procedure hash (following Rust pattern) 170 | const counterContractComponent = AccountComponent.compile( 171 | counterContractCode, 172 | builder, 173 | [StorageSlot.emptyValue()], 174 | ).withSupportsAllTypes(); 175 | 176 | const getCountProcHash = 177 | counterContractComponent.getProcedureHash('get_count'); 178 | 179 | // Build the script that calls the count reader contract (exactly from reader_script.masm with replacements) 180 | const fpiScriptCode = ` 181 | use.external_contract::count_reader_contract 182 | use.std::sys 183 | 184 | begin 185 | push.${getCountProcHash} 186 | # => [GET_COUNT_HASH] 187 | 188 | push.${counterContractAccount.id().suffix()} 189 | # => [account_id_suffix, GET_COUNT_HASH] 190 | 191 | push.${counterContractAccount.id().prefix()} 192 | # => [account_id_prefix, account_id_suffix, GET_COUNT_HASH] 193 | 194 | call.count_reader_contract::copy_count 195 | # => [] 196 | 197 | exec.sys::truncate_stack 198 | # => [] 199 | 200 | end 201 | `; 202 | 203 | // Create the library for the count reader contract 204 | const countReaderLib = builder.buildLibrary( 205 | 'external_contract::count_reader_contract', 206 | countReaderCode, 207 | ); 208 | builder.linkDynamicLibrary(countReaderLib); 209 | 210 | // Compile the transaction script with the count reader library 211 | const txScript = builder.compileTxScript(fpiScriptCode); 212 | 213 | // foreign account 214 | const storageRequirements = new AccountStorageRequirements(); 215 | const foreignAccount = ForeignAccount.public( 216 | counterContractId, 217 | storageRequirements, 218 | ); 219 | 220 | // Build a transaction request with the custom script 221 | const txRequest = new TransactionRequestBuilder() 222 | .withCustomScript(txScript) 223 | .withForeignAccounts(new MidenArrays.ForeignAccountArray([foreignAccount])) 224 | .build(); 225 | 226 | // Execute the transaction on the count reader contract and send it to the network (following Rust pattern) 227 | const txResult = await client.submitNewTransaction( 228 | countReaderContract.account.id(), 229 | txRequest, 230 | ); 231 | 232 | console.log( 233 | 'View transaction on MidenScan: https://testnet.midenscan.com/tx/' + 234 | txResult.toHex(), 235 | ); 236 | 237 | await client.syncState(); 238 | 239 | // Retrieve updated contract data to see the results (following Rust pattern) 240 | const updatedCounterContract = await client.getAccount( 241 | counterContractAccount.id(), 242 | ); 243 | console.log( 244 | 'counter contract storage:', 245 | updatedCounterContract?.storage().getItem(0)?.toHex(), 246 | ); 247 | 248 | const updatedCountReaderContract = await client.getAccount( 249 | countReaderContract.account.id(), 250 | ); 251 | console.log( 252 | 'count reader contract storage:', 253 | updatedCountReaderContract?.storage().getItem(0)?.toHex(), 254 | ); 255 | 256 | // Log the count value copied via FPI 257 | const countReaderStorage = updatedCountReaderContract?.storage().getItem(0); 258 | if (countReaderStorage) { 259 | const countValue = Number( 260 | BigInt( 261 | '0x' + 262 | countReaderStorage 263 | .toHex() 264 | .slice(-16) 265 | .match(/../g)! 266 | .reverse() 267 | .join(''), 268 | ), 269 | ); 270 | console.log('Count copied via Foreign Procedure Invocation:', countValue); 271 | } 272 | 273 | console.log('\nForeign Procedure Invocation Transaction completed!'); 274 | } 275 | -------------------------------------------------------------------------------- /docs/src/rust-client/delegated_proving_tutorial.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Delegated Proving" 3 | sidebar_position: 12 4 | --- 5 | 6 | # Delegated Proving 7 | 8 | _Using delegated proving to minimize transaction proving times on computationally constrained devices_ 9 | 10 | ## Overview 11 | 12 | In this tutorial we will cover how to use delegated proving with the Miden Rust client to minimize the time it takes to generate a valid transaction proof. In the code below, we will create an account, mint tokens from a faucet, then send the tokens to another account using delegated proving. 13 | 14 | ## Prerequisites 15 | 16 | This tutorial assumes you have basic familiarity with the Miden Rust client. 17 | 18 | ## What we'll cover 19 | 20 | - Explaining what "delegated proving" is and its pros and cons 21 | - How to use delegated proving with the Rust client 22 | 23 | ## What is Delegated Proving? 24 | 25 | Before diving into our code example, let's clarify what "delegated proving" means. 26 | 27 | Delegated proving is the process of outsourcing the ZK proof generation of your transaction to a third party. For certain computationally constrained devices such as mobile phones and web browser environments, generating ZK proofs might take too long to ensure an acceptable user experience. Devices that do not have the computational resources to generate Miden proofs in under 1-2 seconds can use delegated proving to provide a more responsive user experience. 28 | 29 | _How does it work?_ When a user choses to use delegated proving, they send off their locally executed transaction to a dedicated server. This dedicated server generates the ZK proof for the executed transaction and sends the proof back to the user. Proving a transaction with delegated proving is trustless, meaning if the delegated prover is malicious, they could not compromise the security of the account that is submitting a transaction to be processed by the delegated prover. 30 | 31 | The only downside of using delegated proving is that it reduces the privacy of the account that uses delegated proving, because the delegated prover would have knowledge of the inputs to the transaction that is being proven. For example, it would not be advisable to use delegated proving in the case of our "How to Create a Custom Note" tutorial, since the note we create requires knowledge of a hash preimage to redeem the assets in the note. Using delegated proving would reveal the hash preimage to the server running the delegated proving service. 32 | 33 | 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-remote-prover. 34 | 35 | ## Step 1: Initialize your repository 36 | 37 | Create a new Rust repository for your Miden project and navigate to it with the following command: 38 | 39 | ```bash 40 | cargo new miden-delegated-proving-app 41 | cd miden-delegated-proving-app 42 | ``` 43 | 44 | Add the following dependencies to your `Cargo.toml` file: 45 | 46 | ```toml 47 | [dependencies] 48 | miden-client = { version = "0.12", features = ["testing", "tonic"] } 49 | miden-client-sqlite-store = { version = "0.12", package = "miden-client-sqlite-store" } 50 | miden-lib = { version = "0.12", default-features = false } 51 | miden-objects = { version = "0.12", default-features = false, features = ["testing"] } 52 | miden-crypto = { version = "0.17.1", features = ["executable"] } 53 | miden-assembly = "0.18.3" 54 | rand = { version = "0.9" } 55 | serde = { version = "1", features = ["derive"] } 56 | serde_json = { version = "1.0", features = ["raw_value"] } 57 | tokio = { version = "1.46", features = ["rt-multi-thread", "net", "macros", "fs"] } 58 | rand_chacha = "0.9.0" 59 | ``` 60 | 61 | ## Step 2: Initialize the client and delegated prover endpoint and construct transactions 62 | 63 | Similarly to previous tutorials, we must instantiate the client. 64 | We construct a `RemoteTransactionProver` that points to our delegated-proving service running at https://tx-prover.testnet.miden.io. 65 | 66 | ```rust 67 | use miden_client::auth::AuthSecretKey; 68 | use miden_lib::account::auth::AuthRpoFalcon512; 69 | use rand::{rngs::StdRng, RngCore}; 70 | use std::sync::Arc; 71 | 72 | use miden_client::{ 73 | account::component::BasicWallet, 74 | builder::ClientBuilder, 75 | keystore::FilesystemKeyStore, 76 | rpc::{Endpoint, GrpcClient}, 77 | transaction::{TransactionProver, TransactionRequestBuilder}, 78 | ClientError, RemoteTransactionProver, 79 | }; 80 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 81 | use miden_objects::account::{AccountBuilder, AccountStorageMode, AccountType}; 82 | 83 | #[tokio::main] 84 | async fn main() -> Result<(), ClientError> { 85 | // Initialize client 86 | let endpoint = Endpoint::testnet(); 87 | let timeout_ms = 10_000; 88 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 89 | 90 | // Initialize keystore 91 | let keystore_path = std::path::PathBuf::from("./keystore"); 92 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 93 | 94 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 95 | 96 | let mut client = ClientBuilder::new() 97 | .rpc(rpc_client) 98 | .sqlite_store(store_path) 99 | .authenticator(keystore.clone()) 100 | .in_debug_mode(true.into()) 101 | .build() 102 | .await?; 103 | 104 | let sync_summary = client.sync_state().await.unwrap(); 105 | println!("Latest block: {}", sync_summary.block_num); 106 | 107 | // Create Alice's account 108 | let mut init_seed = [0_u8; 32]; 109 | client.rng().fill_bytes(&mut init_seed); 110 | 111 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 112 | 113 | let alice_account = AccountBuilder::new(init_seed) 114 | .account_type(AccountType::RegularAccountImmutableCode) 115 | .storage_mode(AccountStorageMode::Private) 116 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 117 | .with_component(BasicWallet) 118 | .build() 119 | .unwrap(); 120 | 121 | client.add_account(&alice_account, false).await?; 122 | keystore.add_key(&key_pair).unwrap(); 123 | 124 | // ------------------------------------------------------------------------- 125 | // Setup the remote tx prover 126 | // ------------------------------------------------------------------------- 127 | let remote_tx_prover: RemoteTransactionProver = 128 | RemoteTransactionProver::new("https://tx-prover.testnet.miden.io"); 129 | let tx_prover: Arc = Arc::new(remote_tx_prover); 130 | 131 | // We use a dummy transaction request to showcase delegated proving. 132 | // The only effect of this tx should be increasing Alice's nonce. 133 | println!("Alice nonce initial: {:?}", alice_account.nonce()); 134 | let script_code = "begin push.1 drop end"; 135 | let tx_script = client 136 | .script_builder() 137 | .compile_tx_script(script_code) 138 | .unwrap(); 139 | 140 | let transaction_request = TransactionRequestBuilder::new() 141 | .custom_script(tx_script) 142 | .build() 143 | .unwrap(); 144 | 145 | // Step 1: Execute the transaction locally 146 | println!("Executing transaction..."); 147 | let tx_result = client 148 | .execute_transaction(alice_account.id(), transaction_request) 149 | .await?; 150 | 151 | // Step 2: Prove the transaction using the remote prover 152 | println!("Proving transaction with remote prover..."); 153 | let proven_transaction = client.prove_transaction_with(&tx_result, tx_prover).await?; 154 | 155 | // Step 3: Submit the proven transaction 156 | println!("Submitting proven transaction..."); 157 | let submission_height = client 158 | .submit_proven_transaction(proven_transaction, &tx_result) 159 | .await?; 160 | 161 | // Step 4: Apply the transaction to local store 162 | client 163 | .apply_transaction(&tx_result, submission_height) 164 | .await?; 165 | 166 | println!("Transaction submitted successfully using delegated prover!"); 167 | 168 | client.sync_state().await.unwrap(); 169 | 170 | let account = client 171 | .get_account(alice_account.id()) 172 | .await 173 | .unwrap() 174 | .unwrap(); 175 | 176 | println!("Alice nonce has increased: {:?}", account.account().nonce()); 177 | 178 | Ok(()) 179 | } 180 | ``` 181 | 182 | Now let's run the `src/main.rs` program: 183 | 184 | ```bash 185 | cargo run --release 186 | ``` 187 | 188 | The output will look like this: 189 | 190 | ```text 191 | Latest block: 226954 192 | Alice initial account balance: Ok(1000) 193 | Alice final account balance: Ok(900) 194 | ``` 195 | 196 | ### Running the example 197 | 198 | To run a full working example navigate to the `rust-client` directory in the [miden-tutorials](https://github.com/0xMiden/miden-tutorials/) repository and run this command: 199 | 200 | ```bash 201 | cd rust-client 202 | cargo run --release --bin delegated_prover 203 | ``` 204 | 205 | ### Continue learning 206 | 207 | Next tutorial: [Consuming On-Chain Price Data from the Pragma Oracle](oracle_tutorial.md) 208 | -------------------------------------------------------------------------------- /rust-client/src/bin/counter_contract_fpi.rs: -------------------------------------------------------------------------------- 1 | use miden_crypto::Felt; 2 | use miden_lib::account::auth::NoAuth; 3 | use miden_lib::transaction::TransactionKernel; 4 | use rand::{rngs::StdRng, RngCore}; 5 | use std::{fs, path::Path, sync::Arc, time::Duration}; 6 | use tokio::time::sleep; 7 | 8 | use miden_client::{ 9 | assembly::{Assembler, DefaultSourceManager, LibraryPath, Module, ModuleKind}, 10 | builder::ClientBuilder, 11 | keystore::FilesystemKeyStore, 12 | rpc::{domain::account::AccountStorageRequirements, Endpoint, GrpcClient}, 13 | transaction::{ForeignAccount, TransactionRequestBuilder}, 14 | ClientError, 15 | }; 16 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 17 | use miden_objects::{ 18 | account::{ 19 | AccountBuilder, AccountComponent, AccountId, AccountStorageMode, AccountType, StorageSlot, 20 | }, 21 | assembly::mast::MastNodeExt, 22 | Word, 23 | }; 24 | 25 | fn create_library( 26 | assembler: Assembler, 27 | library_path: &str, 28 | source_code: &str, 29 | ) -> Result> { 30 | let source_manager = Arc::new(DefaultSourceManager::default()); 31 | let module = Module::parser(ModuleKind::Library).parse_str( 32 | LibraryPath::new(library_path)?, 33 | source_code, 34 | &source_manager, 35 | )?; 36 | let library = assembler.clone().assemble_library([module])?; 37 | Ok(library) 38 | } 39 | 40 | #[tokio::main] 41 | async fn main() -> Result<(), ClientError> { 42 | // Initialize client 43 | let endpoint = Endpoint::testnet(); 44 | let timeout_ms = 10_000; 45 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 46 | 47 | // Initialize keystore 48 | let keystore_path = std::path::PathBuf::from("./keystore"); 49 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 50 | 51 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 52 | 53 | let mut client = ClientBuilder::new() 54 | .rpc(rpc_client) 55 | .sqlite_store(store_path) 56 | .authenticator(keystore.clone()) 57 | .in_debug_mode(true.into()) 58 | .build() 59 | .await?; 60 | 61 | let sync_summary = client.sync_state().await.unwrap(); 62 | println!("Latest block: {}", sync_summary.block_num); 63 | 64 | // ------------------------------------------------------------------------- 65 | // STEP 1: Create the Count Reader Contract 66 | // ------------------------------------------------------------------------- 67 | println!("\n[STEP 1] Creating count reader contract."); 68 | 69 | // Load the MASM file for the counter contract 70 | let count_reader_path = Path::new("../masm/accounts/count_reader.masm"); 71 | let count_reader_code = fs::read_to_string(count_reader_path).unwrap(); 72 | 73 | // Prepare assembler (debug mode = true) 74 | let assembler = TransactionKernel::assembler().with_debug_mode(true); 75 | 76 | // Compile the account code into `AccountComponent` with one storage slot 77 | let count_reader_component = AccountComponent::compile( 78 | &count_reader_code, 79 | TransactionKernel::assembler(), 80 | vec![StorageSlot::Value(Word::default())], 81 | ) 82 | .unwrap() 83 | .with_supports_all_types(); 84 | 85 | // Init seed for the counter contract 86 | let mut init_seed = [0_u8; 32]; 87 | client.rng().fill_bytes(&mut init_seed); 88 | 89 | // Build the new `Account` with the component 90 | let count_reader_contract = AccountBuilder::new(init_seed) 91 | .account_type(AccountType::RegularAccountImmutableCode) 92 | .storage_mode(AccountStorageMode::Public) 93 | .with_component(count_reader_component.clone()) 94 | .with_auth_component(NoAuth) 95 | .build() 96 | .unwrap(); 97 | 98 | println!( 99 | "count_reader hash: {:?}", 100 | count_reader_contract.commitment() 101 | ); 102 | println!("contract id: {:?}", count_reader_contract.id()); 103 | 104 | client 105 | .add_account(&count_reader_contract, false) 106 | .await 107 | .unwrap(); 108 | 109 | // ------------------------------------------------------------------------- 110 | // STEP 2: Build & Get State of the Counter Contract 111 | // ------------------------------------------------------------------------- 112 | println!("\n[STEP 2] Building counter contract from public state"); 113 | 114 | // Define the Counter Contract account id from counter contract deploy 115 | let (_, counter_contract_id) = 116 | AccountId::from_bech32("mtst1arjemrxne8lj5qz4mg9c8mtyxg954483").unwrap(); 117 | 118 | println!("counter contract id: {:?}", counter_contract_id); 119 | 120 | client 121 | .import_account_by_id(counter_contract_id) 122 | .await 123 | .unwrap(); 124 | 125 | let counter_contract_details = client.get_account(counter_contract_id).await.unwrap(); 126 | 127 | let counter_contract = if let Some(account_record) = counter_contract_details { 128 | // Clone the account to get an owned instance 129 | let account = account_record.account().clone(); 130 | println!( 131 | "Account details: {:?}", 132 | account.storage().slots().first().unwrap() 133 | ); 134 | account // Now returns an owned account 135 | } else { 136 | panic!("Counter contract not found!"); 137 | }; 138 | 139 | // ------------------------------------------------------------------------- 140 | // STEP 3: Call the Counter Contract via Foreign Procedure Invocation (FPI) 141 | // ------------------------------------------------------------------------- 142 | println!("\n[STEP 3] Call counter contract with FPI from count copy contract"); 143 | 144 | let counter_contract_path = Path::new("../masm/accounts/counter.masm"); 145 | let counter_contract_code = fs::read_to_string(counter_contract_path).unwrap(); 146 | 147 | let counter_contract_component = AccountComponent::compile( 148 | &counter_contract_code, 149 | TransactionKernel::assembler(), 150 | vec![], 151 | ) 152 | .unwrap() 153 | .with_supports_all_types(); 154 | 155 | // Getting the hash of the `get_count` procedure 156 | let get_proc_export = counter_contract_component 157 | .library() 158 | .exports() 159 | .find(|export| export.name.name.as_str() == "get_count") 160 | .unwrap(); 161 | 162 | let get_proc_mast_id = counter_contract_component 163 | .library() 164 | .get_export_node_id(&get_proc_export.name); 165 | 166 | let get_count_hash = counter_contract_component 167 | .library() 168 | .mast_forest() 169 | .get_node_by_id(get_proc_mast_id) 170 | .unwrap() 171 | .digest() 172 | .as_elements() 173 | .iter() 174 | .map(|f: &Felt| format!("{}", f.as_int())) 175 | .collect::>() 176 | .join("."); 177 | 178 | println!("get count hash: {:?}", get_count_hash); 179 | println!("counter id prefix: {:?}", counter_contract.id().prefix()); 180 | println!("suffix: {:?}", counter_contract.id().suffix()); 181 | 182 | // Build the script that calls the count_copy_contract 183 | let script_path = Path::new("../masm/scripts/reader_script.masm"); 184 | let script_code_original = fs::read_to_string(script_path).unwrap(); 185 | let script_code = script_code_original 186 | .replace("{get_count_proc_hash}", &get_count_hash) 187 | .replace( 188 | "{account_id_suffix}", 189 | &counter_contract.id().suffix().to_string(), 190 | ) 191 | .replace( 192 | "{account_id_prefix}", 193 | &counter_contract.id().prefix().to_string(), 194 | ); 195 | 196 | let account_component_lib = create_library( 197 | assembler.clone(), 198 | "external_contract::count_reader_contract", 199 | &count_reader_code, 200 | ) 201 | .unwrap(); 202 | 203 | let tx_script = client 204 | .script_builder() 205 | .with_dynamically_linked_library(&account_component_lib) 206 | .unwrap() 207 | .compile_tx_script(&script_code) 208 | .unwrap(); 209 | 210 | let foreign_account = 211 | ForeignAccount::public(counter_contract_id, AccountStorageRequirements::default()).unwrap(); 212 | 213 | // Build a transaction request with the custom script 214 | let tx_request = TransactionRequestBuilder::new() 215 | .foreign_accounts([foreign_account]) 216 | .custom_script(tx_script) 217 | .build() 218 | .unwrap(); 219 | 220 | // Execute and submit the transaction 221 | let tx_id = client 222 | .submit_new_transaction(count_reader_contract.id(), tx_request) 223 | .await 224 | .unwrap(); 225 | 226 | println!( 227 | "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", 228 | tx_id 229 | ); 230 | 231 | client.sync_state().await.unwrap(); 232 | 233 | sleep(Duration::from_secs(5)).await; 234 | 235 | client.sync_state().await.unwrap(); 236 | 237 | // Retrieve updated contract data to see the incremented counter 238 | let account_1 = client.get_account(counter_contract.id()).await.unwrap(); 239 | println!( 240 | "counter contract storage: {:?}", 241 | account_1.unwrap().account().storage().get_item(0) 242 | ); 243 | 244 | let account_2 = client 245 | .get_account(count_reader_contract.id()) 246 | .await 247 | .unwrap(); 248 | println!( 249 | "count reader contract storage: {:?}", 250 | account_2.unwrap().account().storage().get_item(0) 251 | ); 252 | 253 | Ok(()) 254 | } 255 | -------------------------------------------------------------------------------- /rust-client/src/bin/hash_preimage_note.rs: -------------------------------------------------------------------------------- 1 | use miden_lib::account::auth::AuthRpoFalcon512; 2 | use rand::{rngs::StdRng, RngCore}; 3 | use std::{fs, path::Path, sync::Arc}; 4 | use tokio::time::{sleep, Duration}; 5 | 6 | use miden_client::{ 7 | account::{ 8 | component::{BasicFungibleFaucet, BasicWallet}, 9 | Account, 10 | }, 11 | address::NetworkId, 12 | auth::AuthSecretKey, 13 | builder::ClientBuilder, 14 | crypto::FeltRng, 15 | keystore::FilesystemKeyStore, 16 | note::{ 17 | Note, NoteAssets, NoteExecutionHint, NoteExecutionMode, NoteInputs, NoteMetadata, 18 | NoteRecipient, NoteTag, NoteType, 19 | }, 20 | rpc::{Endpoint, GrpcClient}, 21 | store::TransactionFilter, 22 | transaction::{OutputNote, TransactionId, TransactionRequestBuilder, TransactionStatus}, 23 | Client, ClientError, Felt, ScriptBuilder, 24 | }; 25 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 26 | use miden_objects::{ 27 | account::{AccountBuilder, AccountStorageMode, AccountType}, 28 | asset::{FungibleAsset, TokenSymbol}, 29 | Hasher, 30 | }; 31 | 32 | // Helper to create a basic account 33 | async fn create_basic_account( 34 | client: &mut Client>, 35 | keystore: &Arc>, 36 | ) -> Result { 37 | let mut init_seed = [0_u8; 32]; 38 | client.rng().fill_bytes(&mut init_seed); 39 | 40 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 41 | 42 | let account = AccountBuilder::new(init_seed) 43 | .account_type(AccountType::RegularAccountUpdatableCode) 44 | .storage_mode(AccountStorageMode::Public) 45 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 46 | .with_component(BasicWallet) 47 | .build() 48 | .unwrap(); 49 | 50 | client.add_account(&account, false).await?; 51 | keystore.add_key(&key_pair).unwrap(); 52 | 53 | Ok(account) 54 | } 55 | 56 | async fn create_basic_faucet( 57 | client: &mut Client>, 58 | keystore: &Arc>, 59 | ) -> Result { 60 | let mut init_seed = [0u8; 32]; 61 | client.rng().fill_bytes(&mut init_seed); 62 | 63 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 64 | let symbol = TokenSymbol::new("MID").unwrap(); 65 | let decimals = 8; 66 | let max_supply = Felt::new(1_000_000); 67 | 68 | let account = AccountBuilder::new(init_seed) 69 | .account_type(AccountType::FungibleFaucet) 70 | .storage_mode(AccountStorageMode::Public) 71 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 72 | .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap()) 73 | .build() 74 | .unwrap(); 75 | 76 | client.add_account(&account, false).await?; 77 | keystore.add_key(&key_pair).unwrap(); 78 | 79 | Ok(account) 80 | } 81 | 82 | /// Waits for a specific transaction to be committed. 83 | async fn wait_for_tx( 84 | client: &mut Client>, 85 | tx_id: TransactionId, 86 | ) -> Result<(), ClientError> { 87 | loop { 88 | client.sync_state().await?; 89 | 90 | // Check transaction status 91 | let txs = client 92 | .get_transactions(TransactionFilter::Ids(vec![tx_id])) 93 | .await?; 94 | let tx_committed = if !txs.is_empty() { 95 | matches!(txs[0].status, TransactionStatus::Committed { .. }) 96 | } else { 97 | false 98 | }; 99 | 100 | if tx_committed { 101 | println!("✅ transaction {} committed", tx_id.to_hex()); 102 | break; 103 | } 104 | 105 | println!( 106 | "Transaction {} not yet committed. Waiting...", 107 | tx_id.to_hex() 108 | ); 109 | sleep(Duration::from_secs(2)).await; 110 | } 111 | Ok(()) 112 | } 113 | 114 | #[tokio::main] 115 | async fn main() -> Result<(), ClientError> { 116 | // Initialize client 117 | let endpoint = Endpoint::testnet(); 118 | let timeout_ms = 10_000; 119 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 120 | 121 | // Initialize keystore 122 | let keystore_path = std::path::PathBuf::from("./keystore"); 123 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 124 | 125 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 126 | 127 | let mut client = ClientBuilder::new() 128 | .rpc(rpc_client) 129 | .sqlite_store(store_path) 130 | .authenticator(keystore.clone()) 131 | .in_debug_mode(true.into()) 132 | .build() 133 | .await?; 134 | 135 | let sync_summary = client.sync_state().await.unwrap(); 136 | println!("Latest block: {}", sync_summary.block_num); 137 | 138 | // ------------------------------------------------------------------------- 139 | // STEP 1: Create accounts and deploy faucet 140 | // ------------------------------------------------------------------------- 141 | println!("\n[STEP 1] Creating new accounts"); 142 | let alice_account = create_basic_account(&mut client, &keystore).await?; 143 | println!( 144 | "Alice's account ID: {:?}", 145 | alice_account.id().to_bech32(NetworkId::Testnet) 146 | ); 147 | let bob_account = create_basic_account(&mut client, &keystore).await?; 148 | println!( 149 | "Bob's account ID: {:?}", 150 | bob_account.id().to_bech32(NetworkId::Testnet) 151 | ); 152 | 153 | println!("\nDeploying a new fungible faucet."); 154 | let faucet = create_basic_faucet(&mut client, &keystore).await?; 155 | println!( 156 | "Faucet account ID: {:?}", 157 | faucet.id().to_bech32(NetworkId::Testnet) 158 | ); 159 | client.sync_state().await?; 160 | 161 | // ------------------------------------------------------------------------- 162 | // STEP 2: Mint tokens with P2ID 163 | // ------------------------------------------------------------------------- 164 | println!("\n[STEP 2] Mint tokens with P2ID"); 165 | let faucet_id = faucet.id(); 166 | let amount: u64 = 100; 167 | let mint_amount = FungibleAsset::new(faucet_id, amount).unwrap(); 168 | let tx_request = TransactionRequestBuilder::new() 169 | .build_mint_fungible_asset( 170 | mint_amount, 171 | alice_account.id(), 172 | NoteType::Public, 173 | client.rng(), 174 | ) 175 | .unwrap(); 176 | 177 | let tx_id = client 178 | .submit_new_transaction(faucet.id(), tx_request) 179 | .await?; 180 | println!("Minted tokens. TX: {:?}", tx_id); 181 | 182 | // Wait for the note to be available 183 | client.sync_state().await?; 184 | wait_for_tx(&mut client, tx_id).await?; 185 | 186 | // Consume the minted note 187 | let consumable_notes = client 188 | .get_consumable_notes(Some(alice_account.id())) 189 | .await?; 190 | 191 | if let Some((note_record, _)) = consumable_notes.first() { 192 | let consume_request = TransactionRequestBuilder::new() 193 | .build_consume_notes(vec![note_record.id()]) 194 | .unwrap(); 195 | 196 | let tx_id = client 197 | .submit_new_transaction(alice_account.id(), consume_request) 198 | .await?; 199 | println!("Consumed minted note. TX: {:?}", tx_id); 200 | } 201 | 202 | client.sync_state().await?; 203 | 204 | // ------------------------------------------------------------------------- 205 | // STEP 3: Create custom note 206 | // ------------------------------------------------------------------------- 207 | println!("\n[STEP 3] Create custom note"); 208 | let secret_vals = vec![Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]; 209 | let digest = Hasher::hash_elements(&secret_vals); 210 | println!("digest: {:?}", digest); 211 | 212 | let code = fs::read_to_string(Path::new("../masm/notes/hash_preimage_note.masm")).unwrap(); 213 | let serial_num = client.rng().draw_word(); 214 | 215 | let note_script = ScriptBuilder::new(true).compile_note_script(code).unwrap(); 216 | let note_inputs = NoteInputs::new(digest.to_vec()).unwrap(); 217 | let recipient = NoteRecipient::new(serial_num, note_script, note_inputs); 218 | let tag = NoteTag::for_public_use_case(0, 0, NoteExecutionMode::Local).unwrap(); 219 | let metadata = NoteMetadata::new( 220 | alice_account.id(), 221 | NoteType::Public, 222 | tag, 223 | NoteExecutionHint::always(), 224 | Felt::new(0), 225 | )?; 226 | let vault = NoteAssets::new(vec![mint_amount.into()])?; 227 | let custom_note = Note::new(vault, metadata, recipient); 228 | println!("note hash: {:?}", custom_note.id().to_hex()); 229 | 230 | let note_request = TransactionRequestBuilder::new() 231 | .own_output_notes(vec![OutputNote::Full(custom_note.clone())]) 232 | .build() 233 | .unwrap(); 234 | 235 | let tx_id = client 236 | .submit_new_transaction(alice_account.id(), note_request) 237 | .await?; 238 | println!( 239 | "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", 240 | tx_id 241 | ); 242 | 243 | client.sync_state().await?; 244 | 245 | // ------------------------------------------------------------------------- 246 | // STEP 4: Consume the Custom Note 247 | // ------------------------------------------------------------------------- 248 | println!("\n[STEP 4] Bob consumes the Custom Note with Correct Secret"); 249 | 250 | let secret = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]; 251 | let consume_custom_request = TransactionRequestBuilder::new() 252 | .unauthenticated_input_notes([(custom_note, Some(secret.into()))]) 253 | .build() 254 | .unwrap(); 255 | 256 | let tx_id = client 257 | .submit_new_transaction(bob_account.id(), consume_custom_request) 258 | .await?; 259 | println!( 260 | "Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/{:?} \n", 261 | tx_id 262 | ); 263 | 264 | Ok(()) 265 | } 266 | -------------------------------------------------------------------------------- /docs/src/web-client/mint_consume_create_tutorial.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Mint, Consume, and Create Notes' 3 | sidebar_position: 3 4 | --- 5 | 6 | _Using the Miden WebClient in TypeScript to mint, consume, and transfer assets_ 7 | 8 | ## Overview 9 | 10 | In the previous tutorial, we set up the foundation - creating Alice's wallet and deploying a faucet. Now we'll put these to use by minting and transferring assets. 11 | 12 | ## What we'll cover 13 | 14 | - Minting assets from a faucet 15 | - Consuming notes to fund an account 16 | - Sending tokens to other users 17 | 18 | ## Prerequisites 19 | 20 | This tutorial builds directly on the previous one. Make sure you have: 21 | 22 | - Completed the "Creating Accounts and Deploying Faucets" tutorial 23 | - Your Next.js app with the Miden WebClient set up 24 | 25 | ## Understanding Notes in Miden 26 | 27 | Before we start coding, it's important to understand **notes**: 28 | 29 | - Minting a note from a faucet does not automatically add the tokens to your account balance. It creates a note addressed to you. 30 | - You must **consume** a note to add its tokens to your account balance. 31 | - Until consumed, tokens exist in the note but aren't in your account yet. 32 | 33 | ## Step 1: Mint tokens from the faucet 34 | 35 | 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. 36 | 37 | Add this to the end of your `createMintConsume` function in `lib/createMintConsume.ts`: 38 | 39 | 40 | 41 | ```ts 42 | // 4. Mint tokens from the faucet to Alice 43 | await client.syncState(); 44 | 45 | console.log("Minting tokens to Alice..."); 46 | const mintTxRequest = client.newMintTransactionRequest( 47 | alice.id(), // Target account (who receives the tokens) 48 | faucet.id(), // Faucet account (who mints the tokens) 49 | NoteType.Public, // Note visibility (public = onchain) 50 | BigInt(1000), // Amount to mint (in base units) 51 | ); 52 | 53 | await client.submitNewTransaction(faucet.id(), mintTxRequest); 54 | 55 | // Wait for the transaction to be processed 56 | console.log("Waiting 10 seconds for transaction confirmation..."); 57 | await new Promise((resolve) => setTimeout(resolve, 10000)); 58 | await client.syncState(); 59 | ``` 60 | 61 | 62 | 63 | ### What's happening here? 64 | 65 | 1. **newMintTransactionRequest**: Creates a request to mint tokens to Alice. Note that this is only possible to submit transactions on the faucets' behalf if the user controls the faucet (i.e. its keys are stored in the client). 66 | 2. **newTransaction**: Locally executes and proves the transaction. 67 | 3. **submitTransaction**: Sends the transaction to the network. 68 | 4. Wait 10 seconds for the transaction to be included in a block. 69 | 70 | ## Step 2: Find consumable notes 71 | 72 | After minting, Alice has a note waiting for her but the tokens aren't in her account yet. 73 | To identify notes that are ready to consume, the Miden WebClient provides the `getConsumableNotes` function: 74 | 75 | ```ts 76 | // 5. Find notes available for consumption 77 | const consumableNotes = await client.getConsumableNotes(alice.id()); 78 | console.log(`Found ${consumableNotes.length} note(s) to consume`); 79 | 80 | const noteIds = consumableNotes.map((note) => 81 | note.inputNoteRecord().id().toString(), 82 | ); 83 | console.log('Consumable note IDs:', noteIds); 84 | ``` 85 | 86 | ## Step 3: Consume notes in a single transaction 87 | 88 | Now let's consume the notes to add the tokens to Alice's account balance: 89 | 90 | ```ts 91 | // 6. Consume the notes to add tokens to Alice's balance 92 | console.log('Consuming minted notes...'); 93 | const consumeTxRequest = client.newConsumeTransactionRequest(mintedNoteIds); 94 | 95 | await client.submitNewTransaction(alice.id(), consumeTxRequest); 96 | 97 | await client.syncState(); 98 | console.log('Notes consumed.'); 99 | ``` 100 | 101 | ## Step 4: Sending tokens to other accounts 102 | 103 | After consuming the notes, Alice has tokens in her wallet. Now, she wants to send tokens to her friends. She has two options: create a separate transaction for each transfer or batch multiple notes in a single transaction. 104 | 105 | _The standard asset transfer note on Miden is the P2ID note (Pay-to-Id). There is also the P2IDE (Pay-to-Id Extended) variant which allows for both timelocking the note (target can only spend the note after a certain block height) and for the note to be reclaimable (the creator of the note can reclaim the note after a certain block height)._ 106 | 107 | Now that Alice has tokens in her account, she can send some to Bob: 108 | 109 | 110 | ```ts 111 | // Add this import at the top of the file 112 | import { NoteType } from "@demox-labs/miden-sdk"; 113 | // ... 114 | 115 | // 7. Send tokens from Alice to Bob 116 | const bobAccountId = Address.fromBech32( 117 | 'mtst1apve54rq8ux0jqqqqrkh5y0r0y8cwza6_qruqqypuyph', 118 | ).accountId(); 119 | console.log("Sending tokens to Bob's account..."); 120 | 121 | const sendTxRequest = client.newSendTransactionRequest( 122 | alice.id(), // Sender account ID 123 | bobAccountId, // Recipient account ID 124 | faucet.id(), // Asset ID (faucet that created the tokens) 125 | NoteType.Public, // Note visibility 126 | BigInt(100), // Amount to send 127 | ); 128 | 129 | await client.submitNewTransaction(alice.id(), sendTxRequest); 130 | 131 | console.log('Tokens sent successfully!'); 132 | ``` 133 | 134 | 135 | 136 | ### Understanding P2ID notes 137 | 138 | The transaction creates a **P2ID (Pay-to-ID)** note: 139 | 140 | - It's the standard way to transfer assets in Miden 141 | - The note is "locked" to Bob's account ID, i.e. only Bob can consume this note to receive the tokens 142 | - Public notes are visible onchain; private notes would need to be shared offchain (e.g. via a private channel) 143 | 144 | ## Summary 145 | 146 | Here's the complete `lib/createMintConsume.ts` file: 147 | 148 | ```ts 149 | // lib/createMintConsume.ts 150 | export async function createMintConsume(): Promise { 151 | if (typeof window === 'undefined') { 152 | console.warn('webClient() can only run in the browser'); 153 | return; 154 | } 155 | 156 | // dynamic import → only in the browser, so WASM is loaded client‑side 157 | const { WebClient, AccountStorageMode, NoteType, Address } = 158 | await import('@demox-labs/miden-sdk'); 159 | 160 | const nodeEndpoint = 'https://rpc.testnet.miden.io'; 161 | const client = await WebClient.createClient(nodeEndpoint); 162 | 163 | // 1. Sync with the latest blockchain state 164 | const state = await client.syncState(); 165 | console.log('Latest block number:', state.blockNum()); 166 | 167 | // 2. Create Alice's account 168 | console.log('Creating account for Alice…'); 169 | const alice = await client.newWallet(AccountStorageMode.public(), true, 0); 170 | console.log('Alice ID:', alice.id().toString()); 171 | 172 | // 3. Deploy a fungible faucet 173 | console.log('Creating faucet…'); 174 | const faucet = await client.newFaucet( 175 | AccountStorageMode.public(), 176 | false, 177 | 'MID', 178 | 8, 179 | BigInt(1_000_000), 180 | 0, 181 | ); 182 | console.log('Faucet ID:', faucet.id().toString()); 183 | 184 | await client.syncState(); 185 | 186 | // 4. Mint tokens to Alice 187 | await client.syncState(); 188 | 189 | console.log('Minting tokens to Alice...'); 190 | const mintTxRequest = client.newMintTransactionRequest( 191 | alice.id(), 192 | faucet.id(), 193 | NoteType.Public, 194 | BigInt(1000), 195 | ); 196 | 197 | await client.submitNewTransaction(faucet.id(), mintTxRequest); 198 | 199 | console.log('Waiting 10 seconds for transaction confirmation...'); 200 | await new Promise((resolve) => setTimeout(resolve, 10000)); 201 | await client.syncState(); 202 | 203 | // 5. Fetch minted notes 204 | const mintedNotes = await client.getConsumableNotes(alice.id()); 205 | const mintedNoteIds = mintedNotes.map((n) => 206 | n.inputNoteRecord().id().toString(), 207 | ); 208 | console.log('Minted note IDs:', mintedNoteIds); 209 | 210 | // 6. Consume minted notes 211 | console.log('Consuming minted notes...'); 212 | const consumeTxRequest = client.newConsumeTransactionRequest(mintedNoteIds); 213 | 214 | await client.submitNewTransaction(alice.id(), consumeTxRequest); 215 | 216 | await client.syncState(); 217 | console.log('Notes consumed.'); 218 | 219 | // 7. Send tokens to Bob 220 | const bobAccountId = Address.fromBech32( 221 | 'mtst1apve54rq8ux0jqqqqrkh5y0r0y8cwza6_qruqqypuyph', 222 | ).accountId(); 223 | console.log("Sending tokens to Bob's account..."); 224 | const sendTxRequest = client.newSendTransactionRequest( 225 | alice.id(), 226 | bobAccountId, 227 | faucet.id(), 228 | NoteType.Public, 229 | BigInt(100), 230 | ); 231 | 232 | await client.submitNewTransaction(alice.id(), sendTxRequest); 233 | console.log('Tokens sent successfully!'); 234 | } 235 | ``` 236 | 237 | Let's run the `lib/createMintConsume.ts` function again. Reload the page and click "Start WebClient". 238 | 239 | The output will look like this: 240 | 241 | ``` 242 | Latest block number: 4807 243 | Creating account for Alice... 244 | Alice ID: 0x1a20f4d1321e681000005020e69b1a 245 | Creating faucet... 246 | Faucet ID: 0xaa86a6f05ae40b2000000f26054d5d 247 | Minting 1000 tokens to Alice... 248 | Waiting 10 seconds for transaction confirmation... 249 | Consumable note IDs: ['0x4edbb3d5dbdf694...'] 250 | Consuming notes... 251 | Notes consumed. 252 | Sending tokens to Bob's account... 253 | Tokens sent successfully! 254 | ``` 255 | 256 | ### Resetting the `MidenClientDB` 257 | 258 | The Miden webclient stores account and note data in the browser. To clear the account and note data in the browser, paste this code snippet into the browser console: 259 | 260 | ```javascript 261 | (async () => { 262 | const dbs = await indexedDB.databases(); // Get all database names 263 | for (const db of dbs) { 264 | await indexedDB.deleteDatabase(db.name); 265 | console.log(`Deleted database: ${db.name}`); 266 | } 267 | console.log('All databases deleted.'); 268 | })(); 269 | ``` 270 | 271 | ## What's next? 272 | 273 | You've now learned the complete note lifecycle in Miden: 274 | 275 | 1. **Minting** - Creating new tokens from a faucet (issued in notes) 276 | 2. **Consuming** - Adding tokens from notes to an account 277 | 3. **Transferring** - Sending tokens to other accounts 278 | 279 | In the next tutorials, we'll explore: 280 | 281 | - Creating multiple notes in a single transaction 282 | - Delegated proving 283 | -------------------------------------------------------------------------------- /rust-client/src/bin/create_mint_consume_send.rs: -------------------------------------------------------------------------------- 1 | use miden_lib::account::auth::AuthRpoFalcon512; 2 | use rand::{rngs::StdRng, RngCore}; 3 | use std::sync::Arc; 4 | use tokio::time::Duration; 5 | 6 | use miden_client::{ 7 | account::{ 8 | component::{BasicFungibleFaucet, BasicWallet}, 9 | AccountId, 10 | }, 11 | address::NetworkId, 12 | auth::AuthSecretKey, 13 | builder::ClientBuilder, 14 | keystore::FilesystemKeyStore, 15 | note::{create_p2id_note, NoteType}, 16 | rpc::{Endpoint, GrpcClient}, 17 | transaction::{OutputNote, TransactionRequestBuilder}, 18 | ClientError, 19 | }; 20 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 21 | use miden_objects::{ 22 | account::{AccountBuilder, AccountIdVersion, AccountStorageMode, AccountType}, 23 | asset::{FungibleAsset, TokenSymbol}, 24 | Felt, 25 | }; 26 | 27 | #[tokio::main] 28 | async fn main() -> Result<(), ClientError> { 29 | // Initialize client 30 | let endpoint = Endpoint::testnet(); 31 | let timeout_ms = 10_000; 32 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 33 | 34 | // Initialize keystore 35 | let keystore_path = std::path::PathBuf::from("./keystore"); 36 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 37 | 38 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 39 | 40 | let mut client = ClientBuilder::new() 41 | .rpc(rpc_client) 42 | .sqlite_store(store_path) 43 | .authenticator(keystore.clone()) 44 | .in_debug_mode(true.into()) 45 | .build() 46 | .await?; 47 | 48 | let sync_summary = client.sync_state().await.unwrap(); 49 | println!("Latest block: {}", sync_summary.block_num); 50 | 51 | //------------------------------------------------------------ 52 | // STEP 1: Create a basic wallet for Alice 53 | //------------------------------------------------------------ 54 | println!("\n[STEP 1] Creating a new account for Alice"); 55 | 56 | // Account seed 57 | let mut init_seed = [0_u8; 32]; 58 | client.rng().fill_bytes(&mut init_seed); 59 | 60 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 61 | 62 | // Build the account 63 | let alice_account = AccountBuilder::new(init_seed) 64 | .account_type(AccountType::RegularAccountUpdatableCode) 65 | .storage_mode(AccountStorageMode::Public) 66 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 67 | .with_component(BasicWallet) 68 | .build() 69 | .unwrap(); 70 | 71 | // Add the account to the client 72 | client.add_account(&alice_account, false).await?; 73 | 74 | // Add the key pair to the keystore 75 | keystore.add_key(&key_pair).unwrap(); 76 | 77 | let alice_account_id_bech32 = alice_account.id().to_bech32(NetworkId::Testnet); 78 | println!("Alice's account ID: {:?}", alice_account_id_bech32); 79 | 80 | //------------------------------------------------------------ 81 | // STEP 2: Deploy a fungible faucet 82 | //------------------------------------------------------------ 83 | println!("\n[STEP 2] Deploying a new fungible faucet."); 84 | 85 | // Faucet seed 86 | let mut init_seed = [0u8; 32]; 87 | client.rng().fill_bytes(&mut init_seed); 88 | 89 | // Faucet parameters 90 | let symbol = TokenSymbol::new("MID").unwrap(); 91 | let decimals = 8; 92 | let max_supply = Felt::new(1_000_000); 93 | 94 | // Generate key pair 95 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 96 | 97 | // Build the faucet account 98 | let faucet_account = AccountBuilder::new(init_seed) 99 | .account_type(AccountType::FungibleFaucet) 100 | .storage_mode(AccountStorageMode::Public) 101 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 102 | .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap()) 103 | .build() 104 | .unwrap(); 105 | 106 | // Add the faucet to the client 107 | client.add_account(&faucet_account, false).await?; 108 | 109 | // Add the key pair to the keystore 110 | keystore.add_key(&key_pair).unwrap(); 111 | 112 | let faucet_account_id_bech32 = faucet_account.id().to_bech32(NetworkId::Testnet); 113 | println!("Faucet account ID: {:?}", faucet_account_id_bech32); 114 | 115 | // Resync to show newly deployed faucet 116 | client.sync_state().await?; 117 | tokio::time::sleep(Duration::from_secs(2)).await; 118 | 119 | //------------------------------------------------------------ 120 | // STEP 3: Mint 5 notes of 100 tokens for Alice 121 | //------------------------------------------------------------ 122 | println!("\n[STEP 3] Minting 5 notes of 100 tokens each for Alice."); 123 | 124 | let amount: u64 = 100; 125 | let fungible_asset = FungibleAsset::new(faucet_account.id(), amount).unwrap(); 126 | 127 | for i in 1..=5 { 128 | let transaction_request = TransactionRequestBuilder::new() 129 | .build_mint_fungible_asset( 130 | fungible_asset, 131 | alice_account.id(), 132 | NoteType::Public, 133 | client.rng(), 134 | ) 135 | .unwrap(); 136 | 137 | println!("tx request built"); 138 | 139 | let tx_id = client 140 | .submit_new_transaction(faucet_account.id(), transaction_request) 141 | .await?; 142 | println!( 143 | "Minted note #{} of {} tokens for Alice. TX: {:?}", 144 | i, amount, tx_id 145 | ); 146 | } 147 | println!("All 5 notes minted for Alice successfully!"); 148 | 149 | // Re-sync so minted notes become visible 150 | client.sync_state().await?; 151 | 152 | //------------------------------------------------------------ 153 | // STEP 4: Alice consumes all her notes 154 | //------------------------------------------------------------ 155 | println!("\n[STEP 4] Alice will now consume all of her notes to consolidate them."); 156 | 157 | // Consume all minted notes in a single transaction 158 | loop { 159 | // Resync to get the latest data 160 | client.sync_state().await?; 161 | 162 | let consumable_notes = client 163 | .get_consumable_notes(Some(alice_account.id())) 164 | .await?; 165 | let list_of_note_ids: Vec<_> = consumable_notes.iter().map(|(note, _)| note.id()).collect(); 166 | 167 | if list_of_note_ids.len() == 5 { 168 | println!("Found 5 consumable notes for Alice. Consuming them now..."); 169 | let transaction_request = TransactionRequestBuilder::new() 170 | .build_consume_notes(list_of_note_ids) 171 | .unwrap(); 172 | 173 | let tx_id = client 174 | .submit_new_transaction(alice_account.id(), transaction_request) 175 | .await?; 176 | println!( 177 | "All of Alice's notes consumed successfully. TX: {:?}", 178 | tx_id 179 | ); 180 | break; 181 | } else { 182 | println!( 183 | "Currently, Alice has {} consumable notes. Waiting...", 184 | list_of_note_ids.len() 185 | ); 186 | tokio::time::sleep(Duration::from_secs(3)).await; 187 | } 188 | } 189 | 190 | //------------------------------------------------------------ 191 | // STEP 5: Alice sends 5 notes of 50 tokens to 5 users 192 | //------------------------------------------------------------ 193 | println!("\n[STEP 5] Alice sends 5 notes of 50 tokens each to 5 different users."); 194 | 195 | // Send 50 tokens to 4 accounts in one transaction 196 | println!("Creating multiple P2ID notes for 4 target accounts in one transaction..."); 197 | let mut p2id_notes = vec![]; 198 | 199 | // Creating 4 P2ID notes to 4 'dummy' AccountIds 200 | for _ in 1..=4 { 201 | let init_seed: [u8; 15] = { 202 | let mut init_seed = [0_u8; 15]; 203 | client.rng().fill_bytes(&mut init_seed); 204 | init_seed 205 | }; 206 | let target_account_id = AccountId::dummy( 207 | init_seed, 208 | AccountIdVersion::Version0, 209 | AccountType::RegularAccountUpdatableCode, 210 | AccountStorageMode::Public, 211 | ); 212 | 213 | let send_amount = 50; 214 | let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); 215 | 216 | let p2id_note = create_p2id_note( 217 | alice_account.id(), 218 | target_account_id, 219 | vec![fungible_asset.into()], 220 | NoteType::Public, 221 | Felt::new(0), 222 | client.rng(), 223 | )?; 224 | p2id_notes.push(p2id_note); 225 | } 226 | 227 | // Specifying output notes and creating a tx request to create them 228 | let output_notes: Vec = p2id_notes.into_iter().map(OutputNote::Full).collect(); 229 | let transaction_request = TransactionRequestBuilder::new() 230 | .own_output_notes(output_notes) 231 | .build() 232 | .unwrap(); 233 | 234 | let tx_id = client 235 | .submit_new_transaction(alice_account.id(), transaction_request) 236 | .await?; 237 | 238 | println!("Submitted a transaction with 4 P2ID notes. TX: {:?}", tx_id); 239 | 240 | println!("Submitting one more single P2ID transaction..."); 241 | let init_seed: [u8; 15] = { 242 | let mut init_seed = [0_u8; 15]; 243 | client.rng().fill_bytes(&mut init_seed); 244 | init_seed 245 | }; 246 | let target_account_id = AccountId::dummy( 247 | init_seed, 248 | AccountIdVersion::Version0, 249 | AccountType::RegularAccountUpdatableCode, 250 | AccountStorageMode::Public, 251 | ); 252 | 253 | let send_amount = 50; 254 | let fungible_asset = FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); 255 | 256 | let p2id_note = create_p2id_note( 257 | alice_account.id(), 258 | target_account_id, 259 | vec![fungible_asset.into()], 260 | NoteType::Public, 261 | Felt::new(0), 262 | client.rng(), 263 | )?; 264 | 265 | let transaction_request = TransactionRequestBuilder::new() 266 | .own_output_notes(vec![OutputNote::Full(p2id_note)]) 267 | .build() 268 | .unwrap(); 269 | 270 | let tx_id = client 271 | .submit_new_transaction(alice_account.id(), transaction_request) 272 | .await?; 273 | 274 | println!("Submitted final P2ID transaction. TX: {:?}", tx_id); 275 | 276 | println!("\nAll steps completed successfully!"); 277 | println!("Alice created a wallet, a faucet was deployed,"); 278 | println!("5 notes of 100 tokens were minted to Alice, those notes were consumed,"); 279 | println!("and then Alice sent 5 separate 50-token notes to 5 different users."); 280 | 281 | Ok(()) 282 | } 283 | -------------------------------------------------------------------------------- /rust-client/src/bin/note_creation_in_masm.rs: -------------------------------------------------------------------------------- 1 | use miden_lib::account::auth::AuthRpoFalcon512; 2 | use miden_lib::transaction::TransactionKernel; 3 | use rand::{rngs::StdRng, RngCore}; 4 | use std::{fs, path::Path, sync::Arc}; 5 | use tokio::time::{sleep, Duration}; 6 | 7 | use miden_client::{ 8 | account::{ 9 | component::{BasicFungibleFaucet, BasicWallet}, 10 | Account, 11 | }, 12 | address::NetworkId, 13 | asset::{FungibleAsset, TokenSymbol}, 14 | auth::AuthSecretKey, 15 | builder::ClientBuilder, 16 | crypto::FeltRng, 17 | keystore::FilesystemKeyStore, 18 | note::{ 19 | Note, NoteAssets, NoteExecutionHint, NoteExecutionMode, NoteInputs, NoteMetadata, 20 | NoteRecipient, NoteScript, NoteTag, NoteType, 21 | }, 22 | rpc::{Endpoint, GrpcClient}, 23 | transaction::{OutputNote, TransactionRequestBuilder}, 24 | Client, ClientError, Felt, 25 | }; 26 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 27 | use miden_objects::{ 28 | account::{AccountBuilder, AccountStorageMode, AccountType}, 29 | note::NoteDetails, 30 | }; 31 | 32 | // Helper to create a basic account 33 | async fn create_basic_account( 34 | client: &mut Client>, 35 | keystore: &Arc>, 36 | ) -> Result { 37 | let mut init_seed = [0u8; 32]; 38 | client.rng().fill_bytes(&mut init_seed); 39 | 40 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 41 | 42 | let account = AccountBuilder::new(init_seed) 43 | .account_type(AccountType::RegularAccountUpdatableCode) 44 | .storage_mode(AccountStorageMode::Public) 45 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 46 | .with_component(BasicWallet) 47 | .build() 48 | .unwrap(); 49 | 50 | client.add_account(&account, false).await?; 51 | keystore.add_key(&key_pair).unwrap(); 52 | 53 | Ok(account) 54 | } 55 | 56 | async fn create_basic_faucet( 57 | client: &mut Client>, 58 | keystore: &Arc>, 59 | ) -> Result { 60 | let mut init_seed = [0u8; 32]; 61 | client.rng().fill_bytes(&mut init_seed); 62 | 63 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 64 | let symbol = TokenSymbol::new("MID").unwrap(); 65 | let decimals = 8; 66 | let max_supply = Felt::new(1_000_000); 67 | 68 | let account = AccountBuilder::new(init_seed) 69 | .account_type(AccountType::FungibleFaucet) 70 | .storage_mode(AccountStorageMode::Public) 71 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 72 | .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap()) 73 | .build() 74 | .unwrap(); 75 | 76 | client.add_account(&account, false).await?; 77 | keystore.add_key(&key_pair).unwrap(); 78 | 79 | Ok(account) 80 | } 81 | 82 | // Helper to wait until an account has the expected number of consumable notes 83 | async fn wait_for_notes( 84 | client: &mut Client>, 85 | account_id: &Account, 86 | expected: usize, 87 | ) -> Result<(), ClientError> { 88 | loop { 89 | client.sync_state().await?; 90 | let notes = client.get_consumable_notes(Some(account_id.id())).await?; 91 | if notes.len() >= expected { 92 | break; 93 | } 94 | println!( 95 | "{} consumable notes found for account {}. Waiting...", 96 | notes.len(), 97 | account_id.id().to_bech32(NetworkId::Testnet) 98 | ); 99 | sleep(Duration::from_secs(3)).await; 100 | } 101 | Ok(()) 102 | } 103 | 104 | #[tokio::main] 105 | async fn main() -> Result<(), ClientError> { 106 | // Initialize client 107 | let endpoint = Endpoint::testnet(); 108 | let timeout_ms = 10_000; 109 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 110 | 111 | // Initialize keystore 112 | let keystore_path = std::path::PathBuf::from("./keystore"); 113 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 114 | 115 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 116 | 117 | let mut client = ClientBuilder::new() 118 | .rpc(rpc_client) 119 | .sqlite_store(store_path) 120 | .authenticator(keystore.clone()) 121 | .in_debug_mode(true.into()) 122 | .build() 123 | .await?; 124 | 125 | let sync_summary = client.sync_state().await.unwrap(); 126 | println!("Latest block: {}", sync_summary.block_num); 127 | 128 | // ------------------------------------------------------------------------- 129 | // STEP 1: Create accounts and deploy faucet 130 | // ------------------------------------------------------------------------- 131 | println!("\n[STEP 1] Creating new accounts"); 132 | let alice_account = create_basic_account(&mut client, &keystore).await?; 133 | println!( 134 | "Alice's account ID: {:?}", 135 | alice_account.id().to_bech32(NetworkId::Testnet) 136 | ); 137 | let bob_account = create_basic_account(&mut client, &keystore).await?; 138 | println!( 139 | "Bob's account ID: {:?}", 140 | bob_account.id().to_bech32(NetworkId::Testnet) 141 | ); 142 | 143 | println!("\nDeploying a new fungible faucet."); 144 | let faucet = create_basic_faucet(&mut client, &keystore).await?; 145 | println!( 146 | "Faucet account ID: {:?}", 147 | faucet.id().to_bech32(NetworkId::Testnet) 148 | ); 149 | client.sync_state().await?; 150 | 151 | // ------------------------------------------------------------------------- 152 | // STEP 2: Mint tokens with P2ID 153 | // ------------------------------------------------------------------------- 154 | println!("\n[STEP 2] Mint tokens with P2ID"); 155 | let faucet_id = faucet.id(); 156 | let amount: u64 = 100; 157 | let mint_amount = FungibleAsset::new(faucet_id, amount).unwrap(); 158 | 159 | let tx_req = TransactionRequestBuilder::new() 160 | .build_mint_fungible_asset( 161 | mint_amount, 162 | alice_account.id(), 163 | NoteType::Public, 164 | client.rng(), 165 | ) 166 | .unwrap(); 167 | 168 | let tx_id = client.submit_new_transaction(faucet.id(), tx_req).await?; 169 | println!("Minted tokens. TX: {:?}", tx_id); 170 | 171 | wait_for_notes(&mut client, &alice_account, 1).await?; 172 | 173 | // Consume the minted note 174 | let consumable_notes = client 175 | .get_consumable_notes(Some(alice_account.id())) 176 | .await?; 177 | 178 | if let Some((note_record, _)) = consumable_notes.first() { 179 | let consume_req = TransactionRequestBuilder::new() 180 | .build_consume_notes(vec![note_record.id()]) 181 | .unwrap(); 182 | 183 | let tx_id = client 184 | .submit_new_transaction(alice_account.id(), consume_req) 185 | .await?; 186 | println!("Consumed minted note. TX: {:?}", tx_id); 187 | } 188 | 189 | client.sync_state().await?; 190 | 191 | // ------------------------------------------------------------------------- 192 | // STEP 3: Create iterative output note 193 | // ------------------------------------------------------------------------- 194 | println!("\n[STEP 3] Create iterative output note"); 195 | 196 | let assembler = TransactionKernel::assembler().with_debug_mode(true); 197 | let code = fs::read_to_string(Path::new("../masm/notes/iterative_output_note.masm")).unwrap(); 198 | let serial_num = client.rng().draw_word(); 199 | 200 | // Create note metadata and tag 201 | let tag = NoteTag::for_public_use_case(0, 0, NoteExecutionMode::Local).unwrap(); 202 | let metadata = NoteMetadata::new( 203 | alice_account.id(), 204 | NoteType::Public, 205 | tag, 206 | NoteExecutionHint::always(), 207 | Felt::new(0), 208 | )?; 209 | let program = assembler.clone().assemble_program(&code).unwrap(); 210 | let note_script = NoteScript::new(program); 211 | let note_inputs = NoteInputs::new(vec![ 212 | alice_account.id().prefix().as_felt(), 213 | alice_account.id().suffix(), 214 | tag.into(), 215 | Felt::new(0), 216 | ]) 217 | .unwrap(); 218 | 219 | let recipient = NoteRecipient::new(serial_num, note_script.clone(), note_inputs.clone()); 220 | let vault = NoteAssets::new(vec![mint_amount.into()])?; 221 | let custom_note = Note::new(vault, metadata, recipient); 222 | 223 | let note_req = TransactionRequestBuilder::new() 224 | .own_output_notes(vec![OutputNote::Full(custom_note.clone())]) 225 | .build() 226 | .unwrap(); 227 | 228 | let tx_id = client 229 | .submit_new_transaction(alice_account.id(), note_req) 230 | .await?; 231 | println!( 232 | "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", 233 | tx_id 234 | ); 235 | 236 | client.sync_state().await?; 237 | 238 | // ------------------------------------------------------------------------- 239 | // STEP 4: Consume the iterative output note 240 | // ------------------------------------------------------------------------- 241 | println!("\n[STEP 4] Bob consumes the note and creates a copy"); 242 | 243 | // Increment the serial number for the new note 244 | let serial_num_1 = [ 245 | serial_num[0], 246 | serial_num[1], 247 | serial_num[2], 248 | Felt::new(serial_num[3].as_int() + 1), 249 | ] 250 | .into(); 251 | 252 | // Reuse the note_script and note_inputs 253 | let recipient = NoteRecipient::new(serial_num_1, note_script, note_inputs); 254 | 255 | // Note: Change metadata to include Bob's account as the creator 256 | let metadata = NoteMetadata::new( 257 | bob_account.id(), 258 | NoteType::Public, 259 | tag, 260 | NoteExecutionHint::always(), 261 | Felt::new(0), 262 | )?; 263 | 264 | let asset_amount_1 = FungibleAsset::new(faucet_id, 50).unwrap(); 265 | let vault = NoteAssets::new(vec![asset_amount_1.into()])?; 266 | let output_note = Note::new(vault, metadata, recipient); 267 | 268 | let consume_custom_req = TransactionRequestBuilder::new() 269 | .unauthenticated_input_notes([(custom_note, None)]) 270 | .expected_future_notes(vec![( 271 | NoteDetails::from(output_note.clone()), 272 | output_note.metadata().tag(), 273 | ) 274 | .clone()]) 275 | .expected_output_recipients(vec![output_note.recipient().clone()]) 276 | .build() 277 | .unwrap(); 278 | 279 | let tx_id = client 280 | .submit_new_transaction(bob_account.id(), consume_custom_req) 281 | .await?; 282 | println!( 283 | "Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/{:?}", 284 | tx_id 285 | ); 286 | 287 | Ok(()) 288 | } 289 | -------------------------------------------------------------------------------- /rust-client/src/bin/unauthenticated_note_transfer.rs: -------------------------------------------------------------------------------- 1 | use miden_lib::account::auth::AuthRpoFalcon512; 2 | use rand::{rngs::StdRng, RngCore}; 3 | use std::sync::Arc; 4 | use tokio::time::{sleep, Duration, Instant}; 5 | 6 | use miden_client::{ 7 | account::component::{BasicFungibleFaucet, BasicWallet}, 8 | address::NetworkId, 9 | asset::{FungibleAsset, TokenSymbol}, 10 | auth::AuthSecretKey, 11 | builder::ClientBuilder, 12 | keystore::FilesystemKeyStore, 13 | note::{create_p2id_note, Note, NoteType}, 14 | rpc::{Endpoint, GrpcClient}, 15 | store::TransactionFilter, 16 | transaction::{OutputNote, TransactionId, TransactionRequestBuilder, TransactionStatus}, 17 | utils::{Deserializable, Serializable}, 18 | Client, ClientError, Felt, 19 | }; 20 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 21 | use miden_objects::account::{AccountBuilder, AccountStorageMode, AccountType}; 22 | 23 | /// Waits for a specific transaction to be committed. 24 | async fn wait_for_tx( 25 | client: &mut Client>, 26 | tx_id: TransactionId, 27 | ) -> Result<(), ClientError> { 28 | loop { 29 | client.sync_state().await?; 30 | 31 | // Check transaction status 32 | let txs = client 33 | .get_transactions(TransactionFilter::Ids(vec![tx_id])) 34 | .await?; 35 | let tx_committed = if !txs.is_empty() { 36 | matches!(txs[0].status, TransactionStatus::Committed { .. }) 37 | } else { 38 | false 39 | }; 40 | 41 | if tx_committed { 42 | println!("✅ transaction {} committed", tx_id.to_hex()); 43 | break; 44 | } 45 | 46 | println!( 47 | "Transaction {} not yet committed. Waiting...", 48 | tx_id.to_hex() 49 | ); 50 | sleep(Duration::from_secs(2)).await; 51 | } 52 | Ok(()) 53 | } 54 | 55 | #[tokio::main] 56 | async fn main() -> Result<(), ClientError> { 57 | // Initialize client 58 | let endpoint = Endpoint::testnet(); 59 | let timeout_ms = 10_000; 60 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 61 | 62 | // Initialize keystore 63 | let keystore_path = std::path::PathBuf::from("./keystore"); 64 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 65 | 66 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 67 | 68 | let mut client = ClientBuilder::new() 69 | .rpc(rpc_client) 70 | .sqlite_store(store_path) 71 | .authenticator(keystore.clone()) 72 | .in_debug_mode(true.into()) 73 | .build() 74 | .await?; 75 | 76 | let sync_summary = client.sync_state().await.unwrap(); 77 | println!("Latest block: {}", sync_summary.block_num); 78 | 79 | //------------------------------------------------------------ 80 | // STEP 1: Deploy a fungible faucet 81 | //------------------------------------------------------------ 82 | println!("\n[STEP 1] Deploying a new fungible faucet."); 83 | 84 | // Faucet seed 85 | let mut init_seed = [0_u8; 32]; 86 | client.rng().fill_bytes(&mut init_seed); 87 | 88 | // Generate key pair 89 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 90 | 91 | // Faucet parameters 92 | let symbol = TokenSymbol::new("MID").unwrap(); 93 | let decimals = 8; 94 | let max_supply = Felt::new(1_000_000); 95 | 96 | // Build the account 97 | let faucet_account = AccountBuilder::new(init_seed) 98 | .account_type(AccountType::FungibleFaucet) 99 | .storage_mode(AccountStorageMode::Public) 100 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 101 | .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply).unwrap()) 102 | .build() 103 | .unwrap(); 104 | 105 | // Add the faucet to the client 106 | client.add_account(&faucet_account, false).await?; 107 | 108 | println!( 109 | "Faucet account ID: {}", 110 | faucet_account.id().to_bech32(NetworkId::Testnet) 111 | ); 112 | 113 | // Add the key pair to the keystore 114 | keystore.add_key(&key_pair).unwrap(); 115 | 116 | // Resync to show newly deployed faucet 117 | tokio::time::sleep(Duration::from_secs(2)).await; 118 | client.sync_state().await?; 119 | 120 | //------------------------------------------------------------ 121 | // STEP 2: Create basic wallet accounts 122 | //------------------------------------------------------------ 123 | println!("\n[STEP 2] Creating new accounts"); 124 | 125 | let mut accounts = vec![]; 126 | let number_of_accounts = 10; 127 | 128 | for i in 0..number_of_accounts { 129 | let mut init_seed = [0_u8; 32]; 130 | client.rng().fill_bytes(&mut init_seed); 131 | 132 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 133 | 134 | let account = AccountBuilder::new(init_seed) 135 | .account_type(AccountType::RegularAccountUpdatableCode) 136 | .storage_mode(AccountStorageMode::Public) 137 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 138 | .with_component(BasicWallet) 139 | .build() 140 | .unwrap(); 141 | 142 | accounts.push(account.clone()); 143 | println!( 144 | "account id {:?}: {}", 145 | i, 146 | account.id().to_bech32(NetworkId::Testnet) 147 | ); 148 | client.add_account(&account, true).await?; 149 | 150 | // Add the key pair to the keystore 151 | keystore.add_key(&key_pair).unwrap(); 152 | } 153 | 154 | // For demo purposes, Alice is the first account. 155 | let alice = &accounts[0]; 156 | 157 | //------------------------------------------------------------ 158 | // STEP 3: Mint and consume tokens for Alice 159 | //------------------------------------------------------------ 160 | println!("\n[STEP 3] Mint tokens"); 161 | println!("Minting tokens for Alice..."); 162 | let amount: u64 = 100; 163 | let fungible_asset_mint_amount = FungibleAsset::new(faucet_account.id(), amount).unwrap(); 164 | let transaction_request = TransactionRequestBuilder::new() 165 | .build_mint_fungible_asset( 166 | fungible_asset_mint_amount, 167 | alice.id(), 168 | NoteType::Public, 169 | client.rng(), 170 | ) 171 | .unwrap(); 172 | 173 | let tx_id = client 174 | .submit_new_transaction(faucet_account.id(), transaction_request) 175 | .await?; 176 | println!("Minted tokens. TX: {:?}", tx_id); 177 | 178 | // Wait for mint transaction to be committed 179 | wait_for_tx(&mut client, tx_id).await?; 180 | 181 | // Get the minted note and consume it 182 | let consumable_notes = client.get_consumable_notes(Some(alice.id())).await?; 183 | 184 | if let Some((note_record, _)) = consumable_notes.first() { 185 | let transaction_request = TransactionRequestBuilder::new() 186 | .build_consume_notes(vec![note_record.id()]) 187 | .unwrap(); 188 | 189 | let consume_tx_id = client 190 | .submit_new_transaction(alice.id(), transaction_request) 191 | .await?; 192 | println!("Consumed minted note. TX: {:?}", consume_tx_id); 193 | 194 | // Wait for consumption to complete 195 | wait_for_tx(&mut client, consume_tx_id).await?; 196 | } 197 | 198 | //------------------------------------------------------------ 199 | // STEP 4: Create unauthenticated note tx chain 200 | //------------------------------------------------------------ 201 | println!("\n[STEP 4] Create unauthenticated note tx chain"); 202 | let start = Instant::now(); 203 | 204 | for i in 0..number_of_accounts - 1 { 205 | let loop_start = Instant::now(); 206 | println!("\nunauthenticated tx {:?}", i + 1); 207 | println!("sender: {}", accounts[i].id().to_bech32(NetworkId::Testnet)); 208 | println!( 209 | "target: {}", 210 | accounts[i + 1].id().to_bech32(NetworkId::Testnet) 211 | ); 212 | 213 | // Time the creation of the p2id note 214 | let send_amount = 20; 215 | let fungible_asset_send_amount = 216 | FungibleAsset::new(faucet_account.id(), send_amount).unwrap(); 217 | 218 | // for demo purposes, unauthenticated notes can be public or private 219 | let note_type = if i % 2 == 0 { 220 | NoteType::Private 221 | } else { 222 | NoteType::Public 223 | }; 224 | 225 | let p2id_note = create_p2id_note( 226 | accounts[i].id(), 227 | accounts[i + 1].id(), 228 | vec![fungible_asset_send_amount.into()], 229 | note_type, 230 | Felt::new(0), 231 | client.rng(), 232 | ) 233 | .unwrap(); 234 | 235 | let output_note = OutputNote::Full(p2id_note.clone()); 236 | 237 | // Time transaction request building 238 | let transaction_request = TransactionRequestBuilder::new() 239 | .own_output_notes(vec![output_note]) 240 | .build() 241 | .unwrap(); 242 | 243 | let tx_id = client 244 | .submit_new_transaction(accounts[i].id(), transaction_request) 245 | .await?; 246 | println!("Created note. TX: {:?}", tx_id); 247 | 248 | // Note serialization/deserialization 249 | // This demonstrates how you could send the serialized note to another client instance 250 | let serialized = p2id_note.to_bytes(); 251 | let deserialized_p2id_note = Note::read_from_bytes(&serialized).unwrap(); 252 | 253 | // Time consume note request building 254 | let consume_note_request = TransactionRequestBuilder::new() 255 | .unauthenticated_input_notes([(deserialized_p2id_note, None)]) 256 | .build() 257 | .unwrap(); 258 | 259 | let tx_id = client 260 | .submit_new_transaction(accounts[i + 1].id(), consume_note_request) 261 | .await?; 262 | 263 | println!( 264 | "Consumed Note Tx on MidenScan: https://testnet.midenscan.com/tx/{:?}", 265 | tx_id 266 | ); 267 | println!( 268 | "Total time for loop iteration {}: {:?}", 269 | i, 270 | loop_start.elapsed() 271 | ); 272 | } 273 | 274 | println!( 275 | "\nTotal execution time for unauthenticated note txs: {:?}", 276 | start.elapsed() 277 | ); 278 | 279 | // Final resync and display account balances 280 | tokio::time::sleep(Duration::from_secs(3)).await; 281 | client.sync_state().await?; 282 | for account in accounts.clone() { 283 | let new_account = client.get_account(account.id()).await.unwrap().unwrap(); 284 | let balance = new_account 285 | .account() 286 | .vault() 287 | .get_balance(faucet_account.id()) 288 | .unwrap(); 289 | println!( 290 | "Account: {} balance: {}", 291 | account.id().to_bech32(NetworkId::Testnet), 292 | balance 293 | ); 294 | } 295 | 296 | Ok(()) 297 | } 298 | -------------------------------------------------------------------------------- /rust-client/src/bin/network_notes_counter_contract.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::Path, sync::Arc}; 2 | 3 | use miden_client::account::component::BasicWallet; 4 | use miden_client::{ 5 | address::NetworkId, 6 | auth::AuthSecretKey, 7 | builder::ClientBuilder, 8 | crypto::FeltRng, 9 | keystore::FilesystemKeyStore, 10 | note::{ 11 | Note, NoteAssets, NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, NoteTag, 12 | NoteType, 13 | }, 14 | rpc::{Endpoint, GrpcClient}, 15 | store::TransactionFilter, 16 | transaction::{OutputNote, TransactionId, TransactionRequestBuilder, TransactionStatus}, 17 | Client, ClientError, Felt, Word, 18 | }; 19 | use miden_client_sqlite_store::ClientBuilderSqliteExt; 20 | use miden_lib::account::auth::{self, AuthRpoFalcon512}; 21 | use miden_lib::transaction::TransactionKernel; 22 | use miden_objects::{ 23 | account::{AccountBuilder, AccountComponent, AccountStorageMode, AccountType, StorageSlot}, 24 | assembly::{Assembler, DefaultSourceManager, Library, LibraryPath, Module, ModuleKind}, 25 | }; 26 | use rand::{rngs::StdRng, RngCore}; 27 | use tokio::time::{sleep, Duration}; 28 | 29 | /// Waits for a specific transaction to be committed. 30 | async fn wait_for_tx( 31 | client: &mut Client>, 32 | tx_id: TransactionId, 33 | ) -> Result<(), ClientError> { 34 | loop { 35 | client.sync_state().await?; 36 | 37 | // Check transaction status 38 | let txs = client 39 | .get_transactions(TransactionFilter::Ids(vec![tx_id])) 40 | .await?; 41 | let tx_committed = if !txs.is_empty() { 42 | matches!(txs[0].status, TransactionStatus::Committed { .. }) 43 | } else { 44 | false 45 | }; 46 | 47 | if tx_committed { 48 | println!("✅ transaction {} committed", tx_id.to_hex()); 49 | break; 50 | } 51 | 52 | println!( 53 | "Transaction {} not yet committed. Waiting...", 54 | tx_id.to_hex() 55 | ); 56 | sleep(Duration::from_secs(2)).await; 57 | } 58 | Ok(()) 59 | } 60 | 61 | /// Creates a Miden library from the provided account code and library path. 62 | fn create_library( 63 | account_code: String, 64 | library_path: &str, 65 | ) -> Result> { 66 | let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true); 67 | let source_manager = Arc::new(DefaultSourceManager::default()); 68 | let module = Module::parser(ModuleKind::Library).parse_str( 69 | LibraryPath::new(library_path)?, 70 | account_code, 71 | &source_manager, 72 | )?; 73 | let library = assembler.clone().assemble_library([module])?; 74 | Ok(library) 75 | } 76 | 77 | #[tokio::main] 78 | async fn main() -> Result<(), Box> { 79 | // Initialize client 80 | let endpoint = Endpoint::testnet(); 81 | let timeout_ms = 10_000; 82 | let rpc_client = Arc::new(GrpcClient::new(&endpoint, timeout_ms)); 83 | 84 | // Initialize keystore 85 | let keystore_path = std::path::PathBuf::from("./keystore"); 86 | let keystore = Arc::new(FilesystemKeyStore::::new(keystore_path).unwrap()); 87 | 88 | let store_path = std::path::PathBuf::from("./store.sqlite3"); 89 | 90 | let mut client = ClientBuilder::new() 91 | .rpc(rpc_client) 92 | .sqlite_store(store_path) 93 | .authenticator(keystore.clone()) 94 | .in_debug_mode(true.into()) 95 | .build() 96 | .await?; 97 | 98 | let sync_summary = client.sync_state().await.unwrap(); 99 | println!("Latest block: {}", sync_summary.block_num); 100 | 101 | // ------------------------------------------------------------------------- 102 | // STEP 1: Create Basic User Account 103 | // ------------------------------------------------------------------------- 104 | println!("\n[STEP 1] Creating a new account for Alice"); 105 | 106 | // Account seed 107 | let mut init_seed = [0_u8; 32]; 108 | client.rng().fill_bytes(&mut init_seed); 109 | 110 | let key_pair = AuthSecretKey::new_rpo_falcon512(); 111 | 112 | // Build the account 113 | let alice_account = AccountBuilder::new(init_seed) 114 | .account_type(AccountType::RegularAccountUpdatableCode) 115 | .storage_mode(AccountStorageMode::Public) 116 | .with_auth_component(AuthRpoFalcon512::new(key_pair.public_key().to_commitment())) 117 | .with_component(BasicWallet) 118 | .build() 119 | .unwrap(); 120 | 121 | // Add the account to the client 122 | client.add_account(&alice_account, false).await?; 123 | 124 | // Add the key pair to the keystore 125 | keystore.add_key(&key_pair).unwrap(); 126 | 127 | println!( 128 | "Alice's account ID: {:?}", 129 | alice_account.id().to_bech32(NetworkId::Testnet) 130 | ); 131 | 132 | // ------------------------------------------------------------------------- 133 | // STEP 2: Create Network Counter Smart Contract 134 | // ------------------------------------------------------------------------- 135 | println!("\n[STEP 2] Creating a network counter smart contract"); 136 | 137 | let counter_code = fs::read_to_string(Path::new("../masm/accounts/counter.masm")).unwrap(); 138 | 139 | // Create the network counter smart contract account 140 | // First, compile the MASM code into an account component 141 | let assembler: Assembler = TransactionKernel::assembler().with_debug_mode(true); 142 | let counter_component = AccountComponent::compile( 143 | &counter_code, 144 | assembler.clone(), 145 | vec![StorageSlot::Value([Felt::new(0); 4].into())], // Initialize counter storage to 0 146 | ) 147 | .unwrap() 148 | .with_supports_all_types(); 149 | 150 | // Generate a random seed for the account 151 | let mut init_seed = [0_u8; 32]; 152 | client.rng().fill_bytes(&mut init_seed); 153 | 154 | // Build the immutable network account with no authentication 155 | let counter_contract = AccountBuilder::new(init_seed) 156 | .account_type(AccountType::RegularAccountImmutableCode) // Immutable code 157 | .storage_mode(AccountStorageMode::Network) // Stored on network 158 | .with_auth_component(auth::NoAuth) // No authentication required 159 | .with_component(counter_component) 160 | .build() 161 | .unwrap(); 162 | 163 | client.add_account(&counter_contract, false).await.unwrap(); 164 | 165 | println!( 166 | "contract id: {:?}", 167 | counter_contract.id().to_bech32(NetworkId::Testnet) 168 | ); 169 | 170 | // ------------------------------------------------------------------------- 171 | // STEP 3: Deploy Network Account with Transaction Script 172 | // ------------------------------------------------------------------------- 173 | println!("\n[STEP 3] Deploy network counter smart contract"); 174 | 175 | let script_code = fs::read_to_string(Path::new("../masm/scripts/counter_script.masm")).unwrap(); 176 | 177 | let account_code = fs::read_to_string(Path::new("../masm/accounts/counter.masm")).unwrap(); 178 | let library_path = "external_contract::counter_contract"; 179 | 180 | let library = create_library(account_code, library_path).unwrap(); 181 | 182 | let tx_script = client 183 | .script_builder() 184 | .with_dynamically_linked_library(&library)? 185 | .compile_tx_script(&script_code)?; 186 | 187 | let tx_increment_request = TransactionRequestBuilder::new() 188 | .custom_script(tx_script) 189 | .build() 190 | .unwrap(); 191 | 192 | let tx_id = client 193 | .submit_new_transaction(counter_contract.id(), tx_increment_request) 194 | .await 195 | .unwrap(); 196 | 197 | println!( 198 | "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", 199 | tx_id 200 | ); 201 | 202 | // Wait for the transaction to be committed 203 | wait_for_tx(&mut client, tx_id).await.unwrap(); 204 | 205 | // ------------------------------------------------------------------------- 206 | // STEP 4: Prepare & Create the Network Note 207 | // ------------------------------------------------------------------------- 208 | println!("\n[STEP 4] Creating a network note for network counter contract"); 209 | 210 | let network_note_code = 211 | fs::read_to_string(Path::new("../masm/notes/network_increment_note.masm")).unwrap(); 212 | let account_code = fs::read_to_string(Path::new("../masm/accounts/counter.masm")).unwrap(); 213 | 214 | let library_path = "external_contract::counter_contract"; 215 | let library = create_library(account_code, library_path).unwrap(); 216 | 217 | // Create and submit the network note that will increment the counter 218 | // Generate a random serial number for the note 219 | let serial_num = client.rng().draw_word(); 220 | 221 | // Compile the note script with the counter contract library 222 | let note_script = client 223 | .script_builder() 224 | .with_dynamically_linked_library(&library)? 225 | .compile_note_script(&network_note_code)?; 226 | 227 | // Create note recipient with empty inputs 228 | let note_inputs = NoteInputs::new([].to_vec())?; 229 | let recipient = NoteRecipient::new(serial_num, note_script, note_inputs); 230 | 231 | // Set up note metadata - tag it with the counter contract ID so it gets consumed 232 | let tag = NoteTag::from_account_id(counter_contract.id()); 233 | let metadata = NoteMetadata::new( 234 | alice_account.id(), 235 | NoteType::Public, 236 | tag, 237 | NoteExecutionHint::none(), 238 | Felt::new(0), 239 | )?; 240 | 241 | // Create the complete note 242 | let increment_note = Note::new(NoteAssets::default(), metadata, recipient); 243 | 244 | // Build and submit the transaction containing the note 245 | let note_req = TransactionRequestBuilder::new() 246 | .own_output_notes(vec![OutputNote::Full(increment_note)]) 247 | .build()?; 248 | 249 | let note_tx_id = client 250 | .submit_new_transaction(alice_account.id(), note_req) 251 | .await?; 252 | 253 | println!( 254 | "View transaction on MidenScan: https://testnet.midenscan.com/tx/{:?}", 255 | note_tx_id 256 | ); 257 | 258 | client.sync_state().await?; 259 | 260 | println!("network increment note creation tx submitted, waiting for onchain commitment"); 261 | 262 | // Wait for the note transaction to be committed 263 | wait_for_tx(&mut client, note_tx_id).await.unwrap(); 264 | 265 | // Waiting for network note to be picked up by the network transaction builder 266 | sleep(Duration::from_secs(6)).await; 267 | 268 | client.sync_state().await?; 269 | 270 | // Checking updated state 271 | let new_account_state = client.get_account(counter_contract.id()).await.unwrap(); 272 | 273 | if let Some(account) = new_account_state.as_ref() { 274 | let count: Word = account.account().storage().get_item(0).unwrap().into(); 275 | let val = count.get(3).unwrap().as_int(); 276 | assert_eq!(val, 2); 277 | println!("🔢 Final counter value: {}", val); 278 | } 279 | 280 | Ok(()) 281 | } 282 | -------------------------------------------------------------------------------- /docs/src/web-client/create_deploy_tutorial.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Creating Accounts and Deploying Faucets' 3 | sidebar_position: 2 4 | --- 5 | 6 | _Using the Miden WebClient in TypeScript to create accounts and deploy faucets_ 7 | 8 | ## Overview 9 | 10 | In this tutorial, we'll build a simple Next.js application that demonstrates the fundamentals of interacting with the Miden blockchain using the WebClient SDK. We'll walk through creating a Miden account for Alice and deploying a fungible faucet contract that can mint tokens. This sets the foundation for more complex operations like issuing assets and transferring them between accounts. 11 | 12 | ## What we'll cover 13 | 14 | - Understanding the difference between public and private accounts & notes 15 | - Instantiating the Miden client 16 | - Creating new accounts (public or private) 17 | - Deploying a faucet to fund an account 18 | 19 | ## Prerequisites 20 | 21 | - Node `v20` or greater 22 | - Familiarity with TypeScript 23 | - `yarn` 24 | 25 | ## Public vs. private accounts & notes 26 | 27 | Before we dive into code, a quick refresher: 28 | 29 | - **Public accounts**: The account's data and code are stored on-chain and are openly visible, including its assets. 30 | - **Private accounts**: The account's state and logic are off-chain, only known to its owner. 31 | - **Public notes**: The note's state is visible to anyone - perfect for scenarios where transparency is desired. 32 | - **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. 33 | 34 | > **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. 35 | 36 | It is useful to think of notes on Miden as "cryptographic cashier's checks" that allow users to send tokens. If the note is private, the note transfer is only known to the sender and receiver. 37 | 38 | ## Step 1: Initialize your Next.js project 39 | 40 | 1. Create a new Next.js app with TypeScript: 41 | 42 | ```bash 43 | yarn create next-app@latest miden-web-app --typescript 44 | ``` 45 | 46 | Hit enter for all terminal prompts. 47 | 48 | 2. Change into the project directory: 49 | 50 | ```bash 51 | cd miden-web-app 52 | ``` 53 | 54 | 3. Install the Miden WebClient SDK: 55 | ```bash 56 | yarn add @demox-labs/miden-sdk@0.12.3 57 | ``` 58 | 59 | **NOTE!**: Be sure to add the `--webpack` command to your `package.json` when running the `dev script`. The dev script should look like this: 60 | 61 | `package.json` 62 | 63 | ```json 64 | "scripts": { 65 | "dev": "next dev --webpack", 66 | ... 67 | } 68 | ``` 69 | 70 | ## Step 2: Set up the WebClient 71 | 72 | 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. 73 | 74 | ### Create `lib/createMintConsume.ts` 75 | 76 | First, we'll create a separate file for our blockchain logic. In the project root, create a folder `lib/` and inside it `createMintConsume.ts`: 77 | 78 | ```bash 79 | mkdir -p lib 80 | touch lib/createMintConsume.ts 81 | ``` 82 | 83 | ```ts 84 | // lib/createMintConsume.ts 85 | export async function createMintConsume(): Promise { 86 | if (typeof window === 'undefined') { 87 | console.warn('webClient() can only run in the browser'); 88 | return; 89 | } 90 | 91 | // dynamic import → only in the browser, so WASM is loaded client‑side 92 | const { WebClient, AccountStorageMode, AccountId, NoteType } = 93 | await import('@demox-labs/miden-sdk'); 94 | 95 | // Connect to Miden testnet RPC endpoint 96 | const nodeEndpoint = 'https://rpc.testnet.miden.io'; 97 | const client = await WebClient.createClient(nodeEndpoint); 98 | 99 | // 1. Sync with the latest blockchain state 100 | // This fetches the latest block header and state commitments 101 | const state = await client.syncState(); 102 | console.log('Latest block number:', state.blockNum()); 103 | 104 | // At this point, your client is connected and synchronized 105 | // Ready to create accounts and deploy contracts! 106 | } 107 | ``` 108 | 109 | > 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. 110 | 111 | ## Step 3: Create the User Interface 112 | 113 | 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. 114 | 115 | Edit `app/page.tsx` to call `createMintConsume()` on a button click: 116 | 117 | ```tsx 118 | 'use client'; 119 | import { useState } from 'react'; 120 | import { createMintConsume } from '../lib/createMintConsume'; 121 | 122 | export default function Home() { 123 | const [isCreatingNotes, setIsCreatingNotes] = useState(false); 124 | 125 | const handleCreateMintConsume = async () => { 126 | setIsCreatingNotes(true); 127 | await createMintConsume(); 128 | setIsCreatingNotes(false); 129 | }; 130 | 131 | return ( 132 |
133 |
134 |

Miden Web App

135 |

Open your browser console to see WebClient logs.

136 | 137 |
138 | 146 |
147 |
148 |
149 | ); 150 | } 151 | ``` 152 | 153 | ## Step 4: Create Alice's Wallet Account 154 | 155 | Now we'll create Alice's account. Let's create a **public** account so we can easily track her transactions. 156 | 157 | Back in `lib/createMintConsume.ts`, extend the `createMintConsume()` function: 158 | 159 | 160 | ```ts 161 | // lib/createMintConsume.ts 162 | export async function createMintConsume(): Promise { 163 | if (typeof window === 'undefined') { 164 | console.warn('webClient() can only run in the browser'); 165 | return; 166 | } 167 | 168 | const { WebClient, AccountStorageMode } = await import( 169 | "@demox-labs/miden-sdk" 170 | ); 171 | 172 | const nodeEndpoint = 'https://rpc.testnet.miden.io'; 173 | const client = await WebClient.createClient(nodeEndpoint); 174 | 175 | // 1. Sync with the latest blockchain state 176 | const state = await client.syncState(); 177 | console.log('Latest block number:', state.blockNum()); 178 | 179 | // 2. Create Alice's account 180 | console.log('Creating account for Alice…'); 181 | const alice = await client.newWallet( 182 | AccountStorageMode.public(), // Public: account state is visible on-chain 183 | true, // Mutable: account code can be upgraded later 184 | 0 // Auth Scheme: 0 for RPO Falcon 512, 1 for ECDSA 256 Keccak 185 | ); 186 | console.log('Alice ID:', alice.id().toString()); 187 | } 188 | ``` 189 | 190 | 191 | ## Step 5: Deploy a Fungible Faucet 192 | 193 | A faucet in Miden is a special type of account that can mint new tokens. Think of it as your own token factory. Let's deploy one that will create our custom "MID" tokens. 194 | 195 | Add this code after creating Alice's account: 196 | 197 | 198 | ```ts 199 | // 3. Deploy a fungible faucet 200 | // A faucet is an account that can mint new tokens 201 | console.log('Creating faucet…'); 202 | const faucetAccount = await client.newFaucet( 203 | AccountStorageMode.public(), // Public: faucet operations are transparent 204 | false, // Immutable: faucet rules cannot be changed 205 | "MID", // Token symbol (like ETH, BTC, etc.) 206 | 8, // Decimals (8 means 1 MID = 100,000,000 base units) 207 | BigInt(1_000_000), // Max supply: total tokens that can ever be minted 208 | 0 // Auth Scheme: 0 for RPO Falcon 512, 1 for ECDSA 256 Keccak 209 | ); 210 | console.log('Faucet account ID:', faucetAccount.id().toString()); 211 | 212 | console.log('Setup complete.'); 213 | ``` 214 | 215 | 216 | ### Understanding Faucet Parameters: 217 | 218 | - **Storage Mode**: We use `public()` so anyone can verify the faucet's minting operations 219 | - **Mutability**: Set to `false` to ensure the faucet rules can't be changed after deployment 220 | - **Token Symbol**: A short identifier for your token (e.g., "MID", "USDC", "DAI") 221 | - **Decimals**: Determines the smallest unit of your token. With 8 decimals, 1 MID = 10^8 base units 222 | - **Max Supply**: The maximum number of tokens that can ever exist 223 | 224 | > **Note**: When tokens are minted from a faucet, they're created as "notes" - Miden's version of UTXOs. Each note contains tokens and can have specific spending conditions. 225 | 226 | ## Summary 227 | 228 | In this tutorial, we've successfully: 229 | 230 | 1. Set up a Next.js application with the Miden WebClient SDK 231 | 2. Connected to the Miden testnet 232 | 3. Created a wallet account for Alice 233 | 4. Deployed a fungible faucet that can mint custom tokens 234 | 235 | Your final `lib/createMintConsume.ts` should look like: 236 | 237 | ```ts 238 | // lib/createMintConsume.ts 239 | export async function createMintConsume(): Promise { 240 | if (typeof window === 'undefined') { 241 | console.warn('webClient() can only run in the browser'); 242 | return; 243 | } 244 | 245 | // dynamic import → only in the browser, so WASM is loaded client‑side 246 | const { WebClient, AccountStorageMode, NoteType, Address } = 247 | await import('@demox-labs/miden-sdk'); 248 | 249 | const nodeEndpoint = 'https://rpc.testnet.miden.io'; 250 | const client = await WebClient.createClient(nodeEndpoint); 251 | 252 | // 1. Sync with the latest blockchain state 253 | const state = await client.syncState(); 254 | console.log('Latest block number:', state.blockNum()); 255 | 256 | // 2. Create Alice's account 257 | console.log('Creating account for Alice…'); 258 | const alice = await client.newWallet(AccountStorageMode.public(), true, 0); 259 | console.log('Alice ID:', alice.id().toString()); 260 | 261 | // 3. Deploy a fungible faucet 262 | console.log('Creating faucet…'); 263 | const faucet = await client.newFaucet( 264 | AccountStorageMode.public(), 265 | false, 266 | 'MID', 267 | 8, 268 | BigInt(1_000_000), 269 | 0, 270 | ); 271 | console.log('Faucet ID:', faucet.id().toString()); 272 | 273 | console.log('Setup complete.'); 274 | } 275 | ``` 276 | 277 | ### Running the example 278 | 279 | ```bash 280 | cd miden-web-app 281 | yarn install 282 | yarn dev 283 | ``` 284 | 285 | Open [http://localhost:3000](http://localhost:3000) in your browser, click **Tutorial #1: Create a wallet and deploy a faucet**, and check the browser console (F12 or right-click → Inspect → Console): 286 | 287 | ``` 288 | Latest block: 2247 289 | Creating account for Alice… 290 | Alice ID: 0xd70b2072c6495d100000869a8bacf2 291 | Creating faucet… 292 | Faucet ID: 0x2d7e506fb88dde200000a1386efec8 293 | Setup complete. 294 | ``` 295 | 296 | ## What's Next? 297 | 298 | Now that you have: 299 | 300 | - A wallet account for Alice that can hold tokens 301 | - A faucet that can mint new MID tokens 302 | 303 | In the next tutorial, we'll: 304 | 305 | 1. Mint tokens from the faucet to Alice's account 306 | 2. Consume notes 307 | 3. Transfer tokens between accounts 308 | --------------------------------------------------------------------------------