Integration Guide

updated with @miden-sdk 0.14.5

A general, repo-agnostic guide to integrating the Epoch Intent SDK with Miden in any TypeScript dapp. Two flows are supported:

  • Miden → EVM — user spends a Miden token, receives an EVM token.

  • EVM → Miden — user spends an EVM token, receives a Miden token.


1. Install

pnpm add @epoch-protocol/epoch-intents-sdk
pnpm add wagmi viem @rainbow-me/rainbowkit @tanstack/react-query
pnpm add @miden-sdk/miden-sdk @miden-sdk/miden-wallet-adapter-base \\\\
        @miden-sdk/miden-wallet-adapter-react @miden-sdk/react

Both SDKs ship WASM. With Vite, add vite-plugin-wasm and vite-plugin-top-level-await, and do not set COOP/COEP headers on the dev server (they break Miden's gRPC-Web transport).


2. Initialize the SDK

The SDK needs an allocator URL and a viem-compatible walletClient from wagmi.

2.1 Cross-chain (Miden → EVM)

When the input is a Miden token, override the wallet client's chain id to the Miden virtual chain id (999999999) so the SDK does not try to EVM-route the input:

import { useEffect, useState } from 'react';
import { useWalletClient } from 'wagmi';

const MIDEN_VIRTUAL_CHAIN_ID = 999999999;

export function useMidenSourceSDK() {
  const { data: walletClient } = useWalletClient();
  const [sdk, setSdk] = useState<any>(null);

  useEffect(() => {
    if (!walletClient) {
      setSdk(null);
      return;
    }
    let cancelled = false;
    import('@epoch-protocol/epoch-intents-sdk').then(({ EpochIntentSDK }) => {
      if (cancelled) return;
      const midenWalletClient = {
        ...walletClient,
        chain: { ...(walletClient.chain ?? {}), id: MIDEN_VIRTUAL_CHAIN_ID },
      };
      setSdk(new EpochIntentSDK({
        apiBaseUrl: import.meta.env.VITE_ALLOCATOR_URL ?? '<http://localhost:3000>',
        walletClient: midenWalletClient,
      }));
    });
    return () => { cancelled = true; };
  }, [walletClient]);

  return { sdk, isReady: !!sdk };
}

2.2 Withdraw (EVM → Miden)

Pass the wagmi walletClient unchanged — the input is on a real EVM chain, so its real chain id must reach the SDK.

2.3 Why useEffect and not useMemo

The dynamic import() and the resulting setState are side effects. React Strict Mode may skip a useMemo body during the second render of a development double-mount, leaving sdk undefined.

Field
Cross-chain
Withdraw

apiBaseUrl

Allocator base URL

Same

walletClient.chain.id

999999999

Real EVM chain id


3. Intent lifecycle

Both flows go through the same four-stage pipeline:

Step
Input
Output
Side effects

getTaskData

Encoded params object

{ taskTypeString, intentData }

None

getIntentQuote

taskTypeString, intentData, sponsorAddress

IntentQuoteResult (tokenIn, tokenOut, success)

None

solveIntent

quote + collateralType (+ createMidenP2IDNote for Miden source)

solveResult with intentNonce, transactions, etc.

Locks Miden note (Miden→EVM) or returns EVM transactions to sign (EVM→Miden)

pollStatus

userAddress, intentNonce

{ evmCompleted, midenConsumed, … }

None

Recommended UX: quote-then-confirm. Show the user the required deposit (quote.tokenIn) and only lock funds when they click confirm. Pass the cached quote into solveIntent via quoteResult to skip a redundant getTaskData round-trip.


4. Miden → EVM

User holds a Miden token, wants an ERC-20 on an EVM chain. The user creates a public P2IDE note targeting the allocator. The solver pays the EVM recipient. The allocator consumes the note. P2IDE is recallable after midenReclaimHeightblocks if the intent stalls.

4.1 Encode task data

Important. depositTokenAddress must be the zero address. The Miden source identity belongs in extraData.midenSourceAccount. Putting the source account in depositTokenAddress will be rejected by the allocator.

Reclaim window — enforced. The allocator validates the on-chain P2IDE reclaim height and rejects the intent if the remaining window is too small. The minimum is configured by the allocator via MIDEN_MIN_RECLAIM_BLOCKS and defaults to 1000 blocks. Compute the absolute reclaim height as currentMidenBlock + N where N >= 1000. A literal value like 1000 is interpreted as an absolute block and will fail validation once the chain passes that height (or be skipped, leaving the user no recall window). Always derive from the latest Miden block height at note-creation time.

4.2 Quote

4.3 Solve

The SDK invokes createMidenP2IDNote(faucetId, amount, allocatorId) mid-flight (see §6). The callback creates the P2IDE note via your wallet adapter and returns { success: true, noteId }.

4.4 Field reference

Field
Value
Meaning

depositTokenAddress

Zero address

Miden source — actual identity is in extraData

tokenInAmount

'0' (reverse) or base units (forward)

See §3

outputTokenAddress

ERC-20 on destination chain

What the user receives

minTokenOut

Base units, decimal string

Floor on the output the user accepts

destinationChainId

EVM chain id as string

e.g. '11155111' for Sepolia

protocolHashIdentifier

Zero hash unless using a named protocol

32 bytes

recipient

EVM address

Receives the output token

extraData.midenSourceAccount

Canonical hex

User's Miden account

extraData.midenFaucetId

Canonical hex

Miden faucet / asset id

extraData.midenNoteType

'P2IDE'

Encumbered, recallable

extraData.midenReclaimHeight

Absolute Miden block number, must satisfy reclaimHeight - currentBlock >= 1000 (allocator default MIDEN_MIN_RECLAIM_BLOCKS)

After this block, the user can reclaim a stuck note. Allocator rejects intents whose on-chain reclaim window is below the minimum.

collateralType

CollateralType.Miden

Routes through the P2IDE callback


5. EVM → Miden

User spends an ERC-20 on an EVM chain to receive a Miden P2ID note. No Miden-side signing — the solver writes the destination note. P2ID is final (non-recallable).

5.1 Encode task data

5.2 Quote + solve

solveResult contains the EVM transactions the user needs to sign (typically approve + Compact deposit, depending on the token). Submit them via your wallet client.

5.3 Field reference

Field
Value
Meaning

depositTokenAddress

ERC-20 address

Token the user spends

outputTokenAddress

Zero address

Output is on Miden

destinationChainId

'999999999'

Miden virtual chain id

recipient

evmSourceAddress

Refund target on the EVM side

collateralType

CollateralType.EVM

SDK handles Compact deposit

extraData.midenRecipientAccount

Canonical hex

Miden account that receives the P2ID note

extraData.midenNoteType

'P2ID'

Final, non-recallable


6. createMidenP2IDNote callback

When collateralType === CollateralType.Miden, the SDK delegates resource-locking back to your dapp.

Reference implementation using @miden-sdk/miden-wallet-adapter-base:

Hard requirements:

  • 'public' note type. Private notes cannot be consumed by the allocator.

  • Output-note id is non-optional. The allocator binds the intent to this note id.

  • Settle before returning. waitForTransaction blocks until finalization on Miden. Returning early breaks allocator verification.

  • Reject amounts above Number.MAX_SAFE_INTEGER. SendTransaction accepts a JS number for amount. Above 2^53 − 1, low bits drop — never silently truncate.

  • Return, do not throw, on Miden-side failures. Throwing aborts the whole intent without giving the SDK a chance to clean up.


7. Track intent status

After solveIntent returns, poll the allocator:

  • Cadence: every 5 seconds.

  • Stop: when evmCompleted && midenConsumed.

  • Tear down: clear the interval on unmount; do not leak polling across navigation.

  • midenConsumeError after evmCompleted: true is a settlement edge case the allocator is retrying — surface but do not treat as terminal.

  • userAddress is the EVM address: the recipient in Miden→EVM, the source in EVM→Miden. In both cases this is the value of intentData.recipient.


8. Identifier normalization

Miden ids appear in four forms; the allocator expects canonical hex:

Normalize before assembling intentData and extraData. The allocator does not auto-detect bech32.


9. Decimals & amounts

Pass amounts to the SDK in base units (decimal strings):

Field
Format
Source

tokenInAmount

Base units, or '0' for reverse quote

Caller

minTokenOut

Base units

Caller

quoteResult.tokenIn

Base units

Backend

quoteResult.tokenOut

Base units

Backend

createMidenP2IDNote amount

Base units

SDK

Use useAssetMetadata(faucetId) from @miden-sdk/react for canonical Miden faucet decimals. The backend may echo midenFaucetDecimals on quoteResult — treat it as advisory only.

Never parseUnits an integer string. parseUnits('1099993', 6) returns 1099993000000 — a 1,000,000× error. Integer strings are already base units:

Last updated