Skip to content
Merged
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
9 changes: 7 additions & 2 deletions demo/.env
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions demo/.env.example
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 9 additions & 8 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@epoch-protocol/epoch-intents-sdk": "1.0.39",
"@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-commons-sdk": "file:../../epoch-commons-sdk",
"@epoch-protocol/epoch-intents-sdk": "^1.0.38",
"@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",
Expand All @@ -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"
}
}
7 changes: 2 additions & 5 deletions demo/src/earn/useEarnMidenAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '',
Expand Down
26 changes: 14 additions & 12 deletions demo/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.StrictMode>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
<MidenFiSignerProvider
network={WalletAdapterNetwork.Testnet}
appName="Epoch Intent Widget Demo"
allowedPrivateData={AllowedPrivateData.Assets}
>
{/* 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. */}
<MidenProvider config={{ rpcUrl: 'testnet' }}>
<MidenProvider config={midenConfig}>
<MidenFiSignerProvider
network={WalletAdapterNetwork.Testnet}
appName="Epoch Intent Widget Demo"
allowedPrivateData={AllowedPrivateData.Assets}
>
<App />
</MidenProvider>
<Toaster position="bottom-right" closeButton duration={5000} />
</MidenFiSignerProvider>
</MidenFiSignerProvider>
</MidenProvider>
<Toaster position="bottom-right" closeButton duration={5000} />
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
Expand Down
12 changes: 5 additions & 7 deletions demo/src/miden/constants/miden-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ import { normalizeMidenIdToHex } from "../services/epoch-bridge";
* (e.g. `mtst1qxxxxxxxxxxxxx_xxxxxx`) resolve correctly.
*/
const MIDEN_FAUCET_DECIMALS: Record<string, number> = {
// 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 {
Expand Down
21 changes: 19 additions & 2 deletions demo/src/miden/hooks/useMidenP2IDNoteFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
NoteAttachment,
NoteType,
TransactionRequestBuilder,
Word,
} from "@miden-sdk/miden-sdk";
import type { EarnMidenCreateP2IDNote } from "@epoch-protocol/epoch-intent-widget";

Expand All @@ -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).
Expand All @@ -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<EarnMidenCreateP2IDNote>(
async (
Expand All @@ -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",
);
}

Expand Down Expand Up @@ -113,6 +127,7 @@ export function useMidenP2IDNoteFactory({
}

const txRequest = new TransactionRequestBuilder()
.withFeeConversionSalt(createFeeConversionSalt())
.withOwnOutputNotes(new NoteArray([note]))
.build();

Expand Down Expand Up @@ -149,6 +164,8 @@ export function useMidenP2IDNoteFactory({
waitForTransaction,
client,
isReady,
isInitializing,
initError,
onStatus,
onNoteCreated,
],
Expand Down
94 changes: 45 additions & 49 deletions demo/src/miden/hooks/useMidenWalletAdapter.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -96,13 +96,13 @@ export function useMidenWalletAdapter(
connect: adapterConnect,
address,
requestAssets,
connecting,
select,
wallet,
wallets,
} = useMidenFiWallet();

const [accountId, setAccountId] = useState<NormalizedMidenAccountId | null>(null);
const [rawAssets, setRawAssets] = useState<Asset[]>([]);

// AccountId parsing touches WASM and often fails on first render. Keep the raw
// wallet address usable immediately, then upgrade to canonical hex once ready.
Expand Down Expand Up @@ -141,28 +141,33 @@ export function useMidenWalletAdapter(
cancelled = true;
};
}, [address, connected]);
const [isLoadingAssets, setIsLoadingAssets] = useState(false);
const [assetsError, setAssetsError] = useState<string | null>(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);
Expand All @@ -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,
Expand All @@ -243,4 +240,3 @@ export function useMidenWalletAdapter(
refreshAssets,
};
}

2 changes: 2 additions & 0 deletions demo/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion demo/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
2 changes: 1 addition & 1 deletion docs/INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
Loading