Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 294 additions & 0 deletions docs/src/web-client/bridging_with_epoch_tutorial.md

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions examples/bridging-app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Miden SDK network configuration
# Supported values: devnet | testnet | local | https://your-rpc-url
VITE_MIDEN_RPC_URL=testnet

# Prover configuration
# Supported values: devnet | testnet | local | https://your-prover-url
VITE_MIDEN_PROVER=testnet

# Epoch allocator service URL (Sepolia <-> Miden testnet bridging).
VITE_ALLOCATOR_URL=https://testnet-dev.epochprotocol.xyz

# Optional: override the Miden testnet explorer used for tx links.
# Default if unset: https://testnet.midenscan.com
# VITE_MIDENSCAN_URL=https://testnet.midenscan.com

# RainbowKit WalletConnect project ID. Get one at https://cloud.walletconnect.com/.
# The Epoch reference shipped a hardcoded project ID; we env-drive it here so each
# bridging-app instance uses its own WalletConnect Cloud project.
VITE_RAINBOWKIT_PROJECT_ID=
39 changes: 39 additions & 0 deletions examples/bridging-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# Dependencies
node_modules

# Build output
dist
dist-ssr
*.local
*.tsbuildinfo

# Editor / OS
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Tooling
.playwright-mcp
.claude
.mcp.json
CLAUDE.md

# Environment (commit .env.example, not .env)
.env
.env.local
.env.*.local
63 changes: 63 additions & 0 deletions examples/bridging-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Bridging app — Sepolia ↔ Miden via Epoch

```bash
yarn create miden-app bridging-app
cd bridging-app
yarn install
```

Run the reference app:

```bash
git clone https://github.com/0xMiden/tutorials.git
cd tutorials/examples/bridging-app
yarn install
yarn dev
```

Open [http://localhost:5173](http://localhost:5173). The app exposes two tabs — `Bridge to EVM` (Miden → Sepolia) and `Withdraw to Miden` (Sepolia → Miden) — wired to the Epoch testnet allocator (`testnet-dev.epochprotocol.xyz`).

## Environment

Copy `.env.example` to `.env` and supply the required values:

| Variable | Required | Description |
|---|---|---|
| `VITE_RAINBOWKIT_PROJECT_ID` | yes | WalletConnect Cloud project id from <https://cloud.walletconnect.com/>. |
| `VITE_ALLOCATOR_URL` | yes | Epoch allocator endpoint (default `https://testnet-dev.epochprotocol.xyz`). |
| `VITE_MIDEN_RPC_URL` | no | Miden RPC; defaults to `testnet`. |
| `VITE_MIDEN_PROVER` | no | Miden prover; defaults to `testnet`. |
| `VITE_MIDENSCAN_URL` | no | Override block-explorer base; defaults to `https://testnet.midenscan.com`. |

## Prerequisites

- An EVM wallet supported by [RainbowKit](https://www.rainbowkit.com/) (MetaMask, Rabby, Coinbase Wallet, …).
- The [MidenFi browser extension](https://chromewebstore.google.com/detail/miden-wallet/ablmompanofnodfdkgchkpmphailefpb) to sign the P2IDE note on Miden.
- A small Sepolia ETH balance for gas; grab some from the [pk910 PoW faucet](https://sepolia-faucet.pk910.de/) or the [Google Cloud Sepolia faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia).

## Scripts

```bash
yarn dev # Vite dev server (http://localhost:5173)
yarn build # tsc -b && vite build
yarn preview # Serve the production build locally
yarn test # Vitest (scaffold-inherited tests)
yarn lint # ESLint
```

## Tutorial

The accompanying single-page tutorial lives at
[`docs/src/web-client/bridging_with_epoch_tutorial.md`](../../docs/src/web-client/bridging_with_epoch_tutorial.md).
Every fenced code block in the tutorial is byte-identical to a slice of this
app's source — `verify_snippets.py` enforces it as a CI gate.

## Forked from

`epochprotocol/miden-integration-example@efc3a690` with the following bridging-specific adaptations:

- `'1000'` reclaim-height literal replaced with `String(currentMidenBlock + 1000)` computed at the call site (`IntentForm.tsx`).
- Dead `defineChain({ id: 0 })` Miden placeholder and the no-op `midenClient` removed from `src/config/wagmi.ts`.
- RainbowKit `projectId` is env-driven via `VITE_RAINBOWKIT_PROJECT_ID`; a missing value renders a setup screen instead of crashing to a blank page.
- `WithdrawForm` `SEPOLIA_TOKENS` decimals corrected to `18` for USDC/USDT — the Epoch test ERC-20s are 18-decimal on Sepolia, matching the same addresses in `IntentForm` (the upstream reference had them as `6`).
- The general-purpose Miden wallet UI (`BalancePanel`, `NotesInboxPanel`, `TransferPanel`, `MidenStatus`, `PersistenceControls`, `BalanceAccountRow`, `AllocatorDebugPanel`) is omitted to keep the example focused on bridging.
23 changes: 23 additions & 0 deletions examples/bridging-app/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
13 changes: 13 additions & 0 deletions examples/bridging-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Miden Template</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
61 changes: 61 additions & 0 deletions examples/bridging-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "bridging-app",
"description": "Reference app for the Miden Epoch bridging tutorial — Sepolia <-> Miden via Epoch intents",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest --run",
"test:watch": "vitest",
"test:coverage": "vitest --run --coverage"
},
"dependencies": {
"@epoch-protocol/epoch-intents-sdk": "^1.0.23",
"@miden-sdk/miden-sdk": "0.14.4",
"@miden-sdk/miden-wallet-adapter-base": "0.14.3",
"@miden-sdk/miden-wallet-adapter-react": "0.14.3",
"@miden-sdk/react": "0.14.4",
"@phosphor-icons/react": "^2.1.10",
"@rainbow-me/rainbowkit": "^2.2.10",
"@tanstack/react-query": "^5.90.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"radix-ui": "^1.4.3",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.5.0",
"viem": "^2.45.2",
"wagmi": "^2.14.0"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@miden-sdk/vite-plugin": "0.14.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.24",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
"jsdom": "^28.1.0",
"postcss": "^8.5.6",
"tailwindcss": "3.4.17",
"typescript": "~5.7.0",
"typescript-eslint": "^8.45.0",
"vite": "^6.0.0",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0",
"vitest": "^4.0.18"
}
}
6 changes: 6 additions & 0 deletions examples/bridging-app/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
1 change: 1 addition & 0 deletions examples/bridging-app/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions examples/bridging-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useState } from 'react';
import { Header } from './components/layout/Header';
import { TabNav } from './components/layout/TabNav';
import { CrosschainTab } from './components/tabs/CrosschainTab';
import { WithdrawTab } from './components/tabs/WithdrawTab';
import { useMidenWalletAdapter } from './hooks/useMidenWalletAdapter';
import { Button } from '@/components/ui/button';
import { ConnectButton } from '@rainbow-me/rainbowkit';

function App() {
const [activeTab, setActiveTab] = useState('crosschain');
const midenWallet = useMidenWalletAdapter({ enabled: true });


return (
<div className="min-h-screen">
<Header />
<main className="mx-auto max-w-5xl space-y-8 px-6 py-8 pb-14">
<div className="flex flex-col gap-4">
<TabNav
activeTab={activeTab}
onTabChange={setActiveTab}
disabledTabs={
midenWallet.connected
? undefined
: {
withdraw: 'Connect Miden wallet to enable Withdraw.',
}
}
/>

<section className="ui-card p-4 sm:p-5">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<div className="text-xs font-semibold uppercase tracking-wide text-neutral-500">Wallets</div>
<div className="text-sm text-neutral-700">
Connect both to run end-to-end flows. EVM pays gas; Miden provides/receives notes.
</div>
</div>
</div>

<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-3">
<div className="text-xs font-semibold uppercase tracking-wide text-neutral-500">EVM wallet</div>
<div className="mt-2">
<ConnectButton />
</div>
</div>

<div className="rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-xs font-semibold uppercase tracking-wide text-neutral-500">Miden wallet</div>
<div className="mt-1 font-mono text-[12px] text-neutral-700 break-all">
{midenWallet.connected ? (midenWallet.accountId?.hex ?? 'connected') : 'not connected'}
</div>
</div>
<Button type="button" onClick={() => void midenWallet.connect()} disabled={midenWallet.connected}>
{midenWallet.connected ? 'Connected' : 'Connect'}
</Button>
</div>
{!midenWallet.connected && (
<div className="mt-2 text-xs text-neutral-500">
Required for Withdraw and for creating P2IDE notes on Cross-chain.
</div>
)}
</div>
</div>
</section>
</div>

{activeTab === 'crosschain' && <CrosschainTab />}
{activeTab === 'withdraw' && <WithdrawTab />}
</main>
</div>
);
}

export default App;
68 changes: 68 additions & 0 deletions examples/bridging-app/src/__tests__/fixtures/accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Test fixtures for Miden frontend tests.
* Uses hex IDs (network-agnostic) and BigInt amounts matching real SDK shapes.
* These are mock values — never parsed by the real SDK at runtime.
*/

export const WALLET_ID_1 = "0x0a00000000000001";
export const WALLET_ID_2 = "0x0a00000000000002";
export const FAUCET_ID = "0x0a00000000000003";
export const COUNTER_ID = "0x0a00000000000004";

export const MOCK_WALLET_HEADER = {
id: WALLET_ID_1,
nonce: 1n,
storageCommitment: "0x0000000000000000000000000000000000000000000000000000000000000001",
};

export const MOCK_WALLET_HEADER_2 = {
id: WALLET_ID_2,
nonce: 0n,
storageCommitment: "0x0000000000000000000000000000000000000000000000000000000000000000",
};

export const MOCK_FAUCET_HEADER = {
id: FAUCET_ID,
nonce: 5n,
storageCommitment: "0x0000000000000000000000000000000000000000000000000000000000000005",
};

export const MOCK_ASSET_BALANCE = {
assetId: FAUCET_ID,
amount: 1000000000n, // 10.0 tokens with 8 decimals
symbol: "TEST",
decimals: 8,
};

export const MOCK_ASSET_BALANCE_EMPTY = {
assetId: FAUCET_ID,
amount: 0n,
symbol: "TEST",
decimals: 8,
};

export const MOCK_ACCOUNT = {
id: WALLET_ID_1,
nonce: 1n,
bech32id: () => WALLET_ID_1,
};

export const MOCK_ASSET_METADATA = {
assetId: FAUCET_ID,
symbol: "TEST",
decimals: 8,
};

// TransactionResult shape — matches @miden-sdk/react types for useMint / useConsume /
// useSwap / useMultiSend / useTransaction result payloads.
export const MOCK_TRANSACTION_RESULT = {
transactionId: "0xabc123def456789012345678901234567890123456789012345678901234abcd",
};

// SendResult shape — distinct from TransactionResult. @miden-sdk/react's useSend
// returns { txId, note }, not { transactionId }. Keep these fixtures separate so
// mocks for useSend don't bleed into mocks for TransactionResult-typed hooks.
export const MOCK_SEND_RESULT = {
txId: "0xabc123def456789012345678901234567890123456789012345678901234abcd",
note: null,
};
Loading
Loading