Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/src/web-client/.prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "preserve",
"embeddedLanguageFormatting": "auto",
"embeddedLanguageFormatting": "off",
"endOfLine": "lf"
}
289 changes: 289 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
64 changes: 64 additions & 0 deletions examples/bridging-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# 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, called out by a preceding
`<!-- source: examples/bridging-app/<file>:<line range> -->` comment.

## 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.
46 changes: 46 additions & 0 deletions examples/bridging-app/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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([
// `.playwright-mcp/extensions/**` is generated browser-extension state from
// the local Playwright MCP harness — see the bridging-app README's
// "Playwright MCP wallet extensions" section. Those files are unpacked
// third-party wallet builds (MetaMask, MidenFi) and are not part of this
// project's source code, so they must not be linted.
globalIgnores(['dist', '.playwright-mcp/**']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
rules: {
// The Epoch SDK + viem/wagmi + Miden web SDK each expose loosely-typed
// surfaces (notably `walletClient`, intent task data, and IntentResult
// shapes) where the application code routinely casts to `any` for
// interop. The tutorial-app code is small and deliberately readable;
// fully replacing every interop `any` with a precise type would be a
// dependent-types refactor against three external packages. Allowed
// here; auditor reference: AUDIT-final-implementation.md → MEDIUM lint
// finding.
'@typescript-eslint/no-explicit-any': 'off',
// The reference app co-locates small utility exports (`fallbackMidenNoteId`,
// a `buttonVariants` helper, the `MidenWalletAdapterProvider` + its
// consumer hook) with their related React components. This is a common
// pattern in shadcn/ui-derived UI kits and in shared-context modules;
// splitting one helper into its own file per consumer would obscure
// the tutorial's structure for little real HMR benefit.
'react-refresh/only-export-components': 'off',
},
},
])
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 x Epoch Bridge</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.
110 changes: 110 additions & 0 deletions examples/bridging-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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 {
MidenWalletAdapterProvider,
useMidenWalletAdapterContext,
} from './hooks/MidenWalletAdapterProvider';
import { Button } from '@/components/ui/button';
import { ConnectButton } from '@rainbow-me/rainbowkit';

function App() {
// Hoist the Miden wallet adapter into a single shared context so the three
// consumers (App, CrosschainTab, WithdrawTab) share one `requestAssets()`
// popup on page load.
return (
<MidenWalletAdapterProvider enabled>
<AppContent />
</MidenWalletAdapterProvider>
);
}

function AppContent() {
const [activeTab, setActiveTab] = useState('crosschain');
// Lazy-mount tabs but keep them mounted once visited, so in-flight bridge
// state (quote, intent submission, withdraw result, consume status) isn't
// discarded if the user tab-switches mid-flow. Active tab is shown; visited-
// but-inactive tabs stay in the DOM with `display:none`.
const [visitedTabs, setVisitedTabs] = useState<Set<string>>(() => new Set(['crosschain']));
const handleTabChange = (tab: string) => {
setActiveTab(tab);
setVisitedTabs((prev) => (prev.has(tab) ? prev : new Set(prev).add(tab)));
};
const midenWallet = useMidenWalletAdapterContext();


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={handleTabChange}
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>

{visitedTabs.has('crosschain') && (
<div className={activeTab === 'crosschain' ? '' : 'hidden'}>
<CrosschainTab />
</div>
)}
{visitedTabs.has('withdraw') && (
<div className={activeTab === 'withdraw' ? '' : 'hidden'}>
<WithdrawTab />
</div>
)}
</main>
</div>
);
}

export default App;
Loading
Loading