From 4aecd91bc2c112e59125e370eba9dcfa7786634c Mon Sep 17 00:00:00 2001 From: jasspreetbawa13 Date: Fri, 11 Sep 2026 14:35:14 +0530 Subject: [PATCH] feat(miden): migrate widget demo to 0.16 testnet --- demo/.env | 9 +- demo/.env.example | 8 ++ demo/README.md | 4 + demo/package.json | 15 +-- demo/src/earn/useEarnMidenAdapter.ts | 7 +- demo/src/main.tsx | 26 ++--- demo/src/miden/constants/miden-tokens.ts | 12 +-- .../miden/hooks/useMidenP2IDNoteFactory.ts | 21 ++++- demo/src/miden/hooks/useMidenWalletAdapter.ts | 94 +++++++++---------- demo/src/vite-env.d.ts | 2 + demo/vite.config.ts | 9 +- docs/INTEGRATION.md | 2 +- src/earn/miden.ts | 2 +- src/earn/use-earn-miden.ts | 17 +--- 14 files changed, 129 insertions(+), 99 deletions(-) create mode 100644 demo/.env.example diff --git a/demo/.env b/demo/.env index 03efdb5..a1bc921 100644 --- a/demo/.env +++ b/demo/.env @@ -1,4 +1,9 @@ -VITE_EPOCH_API_BASE_URL=https://testnet-dev.epochprotocol.xyz -# VITE_TESTNET_API_BASE_URL=https://testnet-dev.epochprotocol.xyz +# VITE_EPOCH_API_BASE_URL=https://testnet-dev.epochprotocol.xyz +VITE_TESTNET_API_BASE_URL=https://testnet-dev.epochprotocol.xyz VITE_TESTNET_API_BASE_URL=http://localhost:3000 +VITE_POSITIONS_API_BASE_URL=http://positions-testnet-dev.epochprotocol.xyz VITE_TESTNET_POSITIONS_API_BASE_URL=http://localhost:4024 + +# Miden testnet endpoints. +VITE_MIDEN_RPC_URL=https://rpc.testnet.miden.io +VITE_MIDEN_NOTE_TRANSPORT_URL=https://transport.miden.io diff --git a/demo/.env.example b/demo/.env.example new file mode 100644 index 0000000..b216497 --- /dev/null +++ b/demo/.env.example @@ -0,0 +1,8 @@ +# Epoch services for the local demo. +VITE_EPOCH_API_BASE_URL=http://localhost:3000 +VITE_TESTNET_API_BASE_URL=http://localhost:3000 +VITE_TESTNET_POSITIONS_API_BASE_URL=http://localhost:4024 + +# Miden testnet endpoints. +VITE_MIDEN_RPC_URL=https://rpc.testnet.miden.io +VITE_MIDEN_NOTE_TRANSPORT_URL=https://transport.miden.io diff --git a/demo/README.md b/demo/README.md index fa9e95e..f43a15f 100644 --- a/demo/README.md +++ b/demo/README.md @@ -17,10 +17,14 @@ pnpm run typecheck ## Environment +Copy `.env.example` to `.env`, then set the local service URLs as needed. + | Variable | Purpose | |----------|---------| | `VITE_EPOCH_API_BASE_URL` | Epoch allocator / API base URL (default `http://0.0.0.0:3000`) | | `VITE_ALLOCATOR_URL` | Fallback if `VITE_EPOCH_API_BASE_URL` is unset | +| `VITE_MIDEN_RPC_URL` | Miden testnet RPC URL (`https://rpc.testnet.miden.io`) | +| `VITE_MIDEN_NOTE_TRANSPORT_URL` | Miden testnet note transport URL (`https://transport.miden.io`) | | `VITE_MIDENSCAN_URL` | Optional Miden explorer base (Advanced tab) | ## Where to copy from diff --git a/demo/package.json b/demo/package.json index d14c7e9..9023c72 100644 --- a/demo/package.json +++ b/demo/package.json @@ -9,12 +9,13 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@epoch-protocol/epoch-commons-sdk": "file:../../epoch-commons-sdk", "@epoch-protocol/epoch-intents-sdk": "^1.0.38", - "@epoch-protocol/epoch-intent-widget": "^0.1.4", - "@miden-sdk/miden-sdk": "^0.15.2", - "@miden-sdk/miden-wallet-adapter-base": "^0.15.1", - "@miden-sdk/miden-wallet-adapter-react": "^0.15.1", - "@miden-sdk/react": "^0.15.2", + "@epoch-protocol/epoch-intent-widget": "file:../", + "@miden-sdk/miden-sdk": "0.16.0", + "@miden-sdk/miden-wallet-adapter-base": "0.16.0", + "@miden-sdk/miden-wallet-adapter-react": "0.16.0", + "@miden-sdk/react": "0.16.0", "@rainbow-me/rainbowkit": "^2.2.9", "@tanstack/react-query": "^5.90.10", "react": "^18.3.1", @@ -27,12 +28,12 @@ "@tailwindcss/vite": "^4.3.0", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react": "^5.1.1", "autoprefixer": "^10.5.0", "postcss": "^8.5.14", "tailwindcss": "^4.3.0", "typescript": "^5.5.0", - "vite": "^5.4.0", + "vite": "^7.3.1", "vite-plugin-wasm": "^3.5.0" } } diff --git a/demo/src/earn/useEarnMidenAdapter.ts b/demo/src/earn/useEarnMidenAdapter.ts index f19da8b..f7f77fe 100644 --- a/demo/src/earn/useEarnMidenAdapter.ts +++ b/demo/src/earn/useEarnMidenAdapter.ts @@ -12,11 +12,8 @@ export function useEarnMidenAdapter(): EarnMidenAdapter { const midenWallet = useMidenWalletAdapter({ enabled: true }); const assets = useMemo(() => { - // What the wallet actually holds, passed through as-is. The widget decides - // which faucets it can offer (graph tokens) and overlays these balances by - // faucet id, falling back to symbol. Pre-mapping to the graph here would - // drop the wallet's symbol — and with it that fallback — so a faucet id the - // widget couldn't match would silently read as a zero balance. + // Keep the wallet's exact faucet ids. The widget matches these against graph + // faucets after normalizing hex and bech32 forms; symbols are not identities. return midenWallet.assets.map((a) => ({ faucetId: a.assetId, symbol: a.symbol ?? '', diff --git a/demo/src/main.tsx b/demo/src/main.tsx index 90630b1..995387f 100644 --- a/demo/src/main.tsx +++ b/demo/src/main.tsx @@ -49,24 +49,26 @@ const config = getDefaultConfig({ const queryClient = new QueryClient(); +const midenConfig = { + rpcUrl: import.meta.env.VITE_MIDEN_RPC_URL?.trim(), + noteTransportUrl: import.meta.env.VITE_MIDEN_NOTE_TRANSPORT_URL?.trim(), +}; + ReactDOM.createRoot(document.getElementById('root')!).render( - - {/* Powers the @miden-sdk/react hooks the P2IDE note factory needs — - without a client it cannot read the synced chain tip, and the - mandate-binding note can't be minted. */} - + + - - - + + + diff --git a/demo/src/miden/constants/miden-tokens.ts b/demo/src/miden/constants/miden-tokens.ts index 770bd6d..5a0eb9d 100644 --- a/demo/src/miden/constants/miden-tokens.ts +++ b/demo/src/miden/constants/miden-tokens.ts @@ -13,13 +13,11 @@ import { normalizeMidenIdToHex } from "../services/epoch-bridge"; * (e.g. `mtst1qxxxxxxxxxxxxx_xxxxxx`) resolve correctly. */ const MIDEN_FAUCET_DECIMALS: Record = { - // USDC on Miden testnet — canonical earn / bridge faucet for now - fc90f0f4da30e51168453b60eafed7: 6, // USDC - "176275876f2fd41103257e341832b9": 6, // DAI - "7725b0e9bb9406912d2ebeaeb05f4d": 6, // USDT - a54717f6bd3210d128aeeaa8a2b7f3: 6, // WETH - "151823cde4b7bd91352617729d7614": 6, // WBTC - "2458e5446128e6b150b75b8ebd9ce1": 6, // MIDEN + "537c15a622074e91188aa894456c52": 6, // USDC + d17976f0809a8191412f2a126625df: 6, // DAI + "6500ca8c2dd69e9147ab7eafad162c": 6, // USDT + "4a09f13153d9cd114c078bfb62a7ec": 6, // WETH + "5fd2e6fd17712c51404d09c2b847f7": 6, // WBTC }; function toMapKey(faucetId: string): string { diff --git a/demo/src/miden/hooks/useMidenP2IDNoteFactory.ts b/demo/src/miden/hooks/useMidenP2IDNoteFactory.ts index cf3a074..cbf0bef 100644 --- a/demo/src/miden/hooks/useMidenP2IDNoteFactory.ts +++ b/demo/src/miden/hooks/useMidenP2IDNoteFactory.ts @@ -11,6 +11,7 @@ import { NoteAttachment, NoteType, TransactionRequestBuilder, + Word, } from "@miden-sdk/miden-sdk"; import type { EarnMidenCreateP2IDNote } from "@epoch-protocol/epoch-intent-widget"; @@ -29,6 +30,12 @@ function toAccountId(id: string): AccountId { return s.startsWith("0x") ? AccountId.fromHex(s) : AccountId.fromBech32(s); } +function createFeeConversionSalt(): Word { + return new Word( + BigUint64Array.from(crypto.getRandomValues(new Uint32Array(4)), BigInt), + ); +} + /** * Mints the reclaimable P2IDE collateral note, binding it to the intent's mandate * via the attachment felts the SDK computed (Compact-equivalent witness hash). @@ -53,7 +60,7 @@ export function useMidenP2IDNoteFactory({ const { requestTransaction, waitForTransaction } = useMidenFiWallet(); // useMiden() is non-throwing (unlike useMidenClient, which throws before the // client initializes); readiness is gated inside the callback instead. - const { client, isReady } = useMiden(); + const { client, isReady, isInitializing, error: initError } = useMiden(); return useCallback( async ( @@ -75,8 +82,15 @@ export function useMidenP2IDNoteFactory({ throw new Error("Wallet does not support custom transactions"); } if (!isReady || !client) { + if (initError) { + throw new Error( + `Miden client initialization failed: ${initError.message}`, + ); + } throw new Error( - "Miden client not ready yet — retry once it initializes", + isInitializing + ? "Miden client is still initializing — retry in a moment" + : "Miden client is not initialized", ); } @@ -113,6 +127,7 @@ export function useMidenP2IDNoteFactory({ } const txRequest = new TransactionRequestBuilder() + .withFeeConversionSalt(createFeeConversionSalt()) .withOwnOutputNotes(new NoteArray([note])) .build(); @@ -149,6 +164,8 @@ export function useMidenP2IDNoteFactory({ waitForTransaction, client, isReady, + isInitializing, + initError, onStatus, onNoteCreated, ], diff --git a/demo/src/miden/hooks/useMidenWalletAdapter.ts b/demo/src/miden/hooks/useMidenWalletAdapter.ts index cfce7b4..8b4df1a 100644 --- a/demo/src/miden/hooks/useMidenWalletAdapter.ts +++ b/demo/src/miden/hooks/useMidenWalletAdapter.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react"; import { AllowedPrivateData, @@ -8,7 +9,6 @@ import { } from "@miden-sdk/miden-wallet-adapter-base"; import { useAssetMetadata, toBech32AccountId } from "@miden-sdk/react"; import { AccountId, Address } from "@miden-sdk/miden-sdk"; -import type { Asset } from "@miden-sdk/miden-wallet-adapter-base"; export interface NormalizedMidenAccountId { hex: string; @@ -96,13 +96,13 @@ export function useMidenWalletAdapter( connect: adapterConnect, address, requestAssets, + connecting, select, wallet, wallets, } = useMidenFiWallet(); const [accountId, setAccountId] = useState(null); - const [rawAssets, setRawAssets] = useState([]); // AccountId parsing touches WASM and often fails on first render. Keep the raw // wallet address usable immediately, then upgrade to canonical hex once ready. @@ -141,28 +141,33 @@ export function useMidenWalletAdapter( cancelled = true; }; }, [address, connected]); - const [isLoadingAssets, setIsLoadingAssets] = useState(false); - const [assetsError, setAssetsError] = useState(null); + const { + data: rawAssets = [], + isFetching: isLoadingAssets, + error: assetsRequestError, + refetch, + } = useQuery({ + queryKey: ["miden-assets", address], + queryFn: async () => (await requestAssets!()) ?? [], + enabled: enabled && connected && !!address && !!requestAssets, + staleTime: Infinity, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + }); + + const assetsError = + enabled && connected && !requestAssets + ? "Connected wallet does not support requestAssets()" + : assetsRequestError instanceof Error + ? assetsRequestError.message + : assetsRequestError + ? "Failed to load assets" + : null; const refreshAssets = useCallback(async () => { - if (!enabled || !connected) return; - if (!requestAssets) { - setRawAssets([]); - setAssetsError("Connected wallet does not support requestAssets()"); - return; - } - setIsLoadingAssets(true); - setAssetsError(null); - try { - const raw = await requestAssets(); - setRawAssets(raw ?? []); - } catch (err) { - setRawAssets([]); - setAssetsError(err instanceof Error ? err.message : "Failed to load assets"); - } finally { - setIsLoadingAssets(false); - } - }, [enabled, connected, requestAssets]); + await refetch(); + }, [refetch]); const faucetIds = useMemo(() => rawAssets.map((a) => a.faucetId), [rawAssets]); const { assetMetadata } = useAssetMetadata(faucetIds); @@ -189,48 +194,40 @@ export function useMidenWalletAdapter( ); const connect = useCallback(async () => { - if (connected) { - await refreshAssets(); + if (connected || connecting) { return; } - // MidenFiSignerProvider auto-selects a single wallet on the next render, - // but connect() throws WalletNotSelectedError if called before that lands. - // Connect through the adapter directly when the context wallet isn't ready. - const target = wallet ?? wallets[0] ?? null; + const target = wallet ?? wallets.find( + ({ readyState }) => + readyState === WalletReadyState.Installed || + readyState === WalletReadyState.Loadable, + ); if (!target) { - throw new Error('No Miden wallet adapter available'); + throw new Error("No Miden wallet adapter is available"); } if (!wallet) { + // The provider selects its sole extension on the next render. Connect the + // detected adapter once so the first click cannot fail with WalletNotSelected. select(target.adapter.name); - } - - if ( - target.readyState === WalletReadyState.Installed || - target.readyState === WalletReadyState.Loadable - ) { await target.adapter.connect( PrivateDataPermission.UponRequest, WalletAdapterNetwork.Testnet, AllowedPrivateData.Assets, ); - } else { - await adapterConnect(); + return; } - await refreshAssets(); - }, [connected, adapterConnect, refreshAssets, wallet, wallets, select]); - - useEffect(() => { - if (!enabled || !connected) { - setRawAssets([]); - setAssetsError(null); - setIsLoadingAssets(false); - return; + if ( + wallet.readyState !== WalletReadyState.Installed && + wallet.readyState !== WalletReadyState.Loadable + ) { + throw new Error("Miden wallet is initializing. Wait a moment and try again."); } - void refreshAssets(); - }, [enabled, connected, refreshAssets]); + + await adapterConnect(); + }, [adapterConnect, connected, connecting, select, wallet, wallets]); return { connected, @@ -243,4 +240,3 @@ export function useMidenWalletAdapter( refreshAssets, }; } - diff --git a/demo/src/vite-env.d.ts b/demo/src/vite-env.d.ts index 3500b76..16ec363 100644 --- a/demo/src/vite-env.d.ts +++ b/demo/src/vite-env.d.ts @@ -6,6 +6,8 @@ interface ImportMetaEnv { readonly VITE_TESTNET_API_BASE_URL?: string; readonly VITE_TESTNET_POSITIONS_API_BASE_URL?: string; readonly VITE_MIDENSCAN_URL?: string; + readonly VITE_MIDEN_RPC_URL?: string; + readonly VITE_MIDEN_NOTE_TRANSPORT_URL?: string; } interface ImportMeta { diff --git a/demo/vite.config.ts b/demo/vite.config.ts index 2cdb2b2..dc332e5 100644 --- a/demo/vite.config.ts +++ b/demo/vite.config.ts @@ -10,7 +10,14 @@ export default defineConfig({ format: 'es', }, resolve: { - dedupe: ['react', 'react-dom', 'wagmi', 'viem', '@tanstack/react-query'], + dedupe: [ + 'react', + 'react-dom', + 'wagmi', + 'viem', + '@tanstack/react-query', + '@epoch-protocol/epoch-commons-sdk', + ], }, optimizeDeps: { exclude: ['@miden-sdk/miden-sdk'], diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index ed8caec..cb5454e 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -383,7 +383,7 @@ Caveats: **Earn has no real testnet** (1delta doesn't index testnet pools — du - **Dedupe singletons.** Ensure a single copy of `react`, `react-dom`, `wagmi`, `viem`, `@tanstack/react-query`. Duplicate copies break wagmi's hooks/context once the widget and your wallet UI both pull them in. In Vite: `resolve.dedupe: ['react','react-dom','wagmi','viem','@tanstack/react-query']`. - **Portalled modal + theme.** The modal mounts into `document.body`; project `--epoch-*` onto `:root` (Step 2) if your own chrome shares those tokens. -- **Miden earn only.** Miden funding needs WASM + top-level-await support from your bundler (e.g. `vite-plugin-wasm` + `vite-plugin-top-level-await`) and the Miden SDK excluded from dep pre-bundling. Skip all of it unless you integrate Miden — it's not needed for EVM pay/swap/earn. +- **Miden earn only.** Miden funding needs WASM support from your bundler (for Vite, `vite-plugin-wasm`) and the Miden SDK excluded from dep pre-bundling. Skip all of it unless you integrate Miden — it's not needed for EVM pay/swap/earn. --- diff --git a/src/earn/miden.ts b/src/earn/miden.ts index 279d013..5337c66 100644 --- a/src/earn/miden.ts +++ b/src/earn/miden.ts @@ -18,7 +18,7 @@ export { EVM_ZERO_ADDRESS, MIDEN_VIRTUAL_CHAIN_ID }; * USDC — SIO resolves faucets against the graph, so that id NO_QUOTE'd.) */ export const DEFAULT_MIDEN_FAUCET = { - faucetId: "0xfc90f0f4da30e51168453b60eafed7", + faucetId: "0x537c15a622074e91188aa894456c52", symbol: "USDC", decimals: 6, } as const; diff --git a/src/earn/use-earn-miden.ts b/src/earn/use-earn-miden.ts index 89f9b64..79ccd67 100644 --- a/src/earn/use-earn-miden.ts +++ b/src/earn/use-earn-miden.ts @@ -74,11 +74,8 @@ export interface EarnMiden { * funding payload, and the Smart Withdraw delivery target. * * Faucet ids are canonical to the Epoch graph, never to the wallet. The host - * adapter supplies *balances* only, overlaid here by faucet id and falling back - * to symbol, since wallets may encode the same id as bech32 or hex. An earlier - * version filtered adapter assets by the bundled default faucet — whose id had - * gone stale against the graph — so deposits quoted against an unresolvable - * faucet and failed with NO_QUOTE. + * adapter supplies balances overlaid by faucet id; `midenFaucetKey` normalizes + * hex and bech32 representations of the same asset. Symbols are not asset ids. */ export function useEarnMiden({ earnMiden, @@ -101,13 +98,9 @@ export function useEarnMiden({ // a dash. Before connecting, balances stay undefined. const connected = !!earnMiden?.connected; const mapped = graphTokens.map((t) => { - const match = - adapterAssets.find( - (a) => midenFaucetKey(a.faucetId) === midenFaucetKey(t.faucetId), - ) ?? - adapterAssets.find( - (a) => (a.symbol ?? '').toUpperCase() === t.symbol.toUpperCase(), - ); + const match = adapterAssets.find( + (a) => midenFaucetKey(a.faucetId) === midenFaucetKey(t.faucetId), + ); return { faucetId: t.faucetId, symbol: t.symbol,