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
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# Optional. The app uses https://rpc.testnet.miden.io by default.
# VITE_MIDEN_RPC_URL=https://rpc.testnet.miden.io
# Optional public endpoints. Do not place invitation codes, private keys, or
# other credentials in Vite variables: they are compiled into the client bundle.
# VITE_EPOCH_API_BASE_URL=https://miden-integration-example.epochprotocol.xyz/allocator
# VITE_MIDEN_RPC_URL=https://rpc.cedar.miden.io
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"test": "tsx --test test/**/*.test.ts",
"dev": "vite",
"build": "tsc -b && vite build",
"typecheck": "tsc -b",
Expand All @@ -15,13 +16,14 @@
"prepare": "husky"
},
"dependencies": {
"@epoch-protocol/epoch-commons-sdk": "github:epochprotocol/epoch-commons-sdk#36226c5d1fd640cc8497dd1c3976ddfe47429aa8",
"@epoch-protocol/epoch-intents-sdk": "1.0.39",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@metamask/sdk": "^0.34.0",
"@miden-sdk/miden-sdk": "0.17.3",
"@miden-sdk/miden-wallet-adapter-base": "0.17.3",
"@miden-sdk/miden-wallet-adapter-react": "0.17.3",
"@miden-sdk/react": "0.17.3",
"@miden-sdk/miden-sdk": "0.17.5",
"@miden-sdk/miden-wallet-adapter-base": "0.17.5",
"@miden-sdk/miden-wallet-adapter-react": "0.17.5",
"@miden-sdk/react": "0.17.5",
"@phosphor-icons/react": "^2.1.10",
"@rainbow-me/rainbowkit": "^2.2.10",
"@tanstack/react-query": "^5.90.20",
Expand Down Expand Up @@ -53,6 +55,7 @@
"postcss": "^8.5.6",
"prettier": "^3.9.4",
"tailwindcss": "3.4.17",
"tsx": "4.20.6",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1",
Expand Down
744 changes: 505 additions & 239 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
allowBuilds:
"@epoch-protocol/epoch-commons-sdk@git+ssh://[email protected]/epochprotocol/epoch-commons-sdk.git#36226c5d1fd640cc8497dd1c3976ddfe47429aa8": true
"@swc/core": true
bufferutil: true
esbuild: true
keccak: true
msw: true
utf-8-validate: true
minimumReleaseAgeExclude:
- '@epoch-protocol/[email protected]'
- '@epoch-protocol/[email protected]'
- "@epoch-protocol/[email protected]"
- "@epoch-protocol/[email protected]"
4 changes: 2 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ function App() {
Wallets
</div>
<div className="text-sm text-neutral-700">
Connect both to run end-to-end flows. EVM pays gas on any
Epoch testnet chain; Miden provides/receives notes.
Connect both to run end-to-end flows. EVM uses Base mainnet;
Miden provides or receives Cedar notes.
</div>
</div>
</div>
Expand Down
3 changes: 1 addition & 2 deletions src/components/crosschain/EVMWalletConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ export function EVMWalletConnect() {
<div className="ui-card">
<h2 className="text-lg font-semibold text-neutral-900">EVM wallet</h2>
<p className="mt-2 text-sm leading-relaxed text-neutral-600">
Required for Epoch intents: signing and funding on an Epoch testnet EVM
chain. Connect the wallet that should pay gas and hold
Required for Epoch intents: signing and funding USDC on Base mainnet.
</p>
<div className="mt-4">
<ConnectButton />
Expand Down
83 changes: 61 additions & 22 deletions src/components/crosschain/IntentForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useAccount, useChainId } from "wagmi";
import { toast } from "sonner";
import type { SolveIntentParams } from "@epoch-protocol/epoch-intents-sdk";
Expand All @@ -8,22 +8,24 @@ import type {
} from "../../types/miden";
import type { IntentQuotePhase } from "../../hooks/useEpochIntent";
import {
DEFAULT_TESTNET_CHAIN_ID_STR,
isSupportedTestnetEvmChain,
DEFAULT_EVM_CHAIN_ID_STR,
isSupportedEvmChain,
} from "../../constants/chains";
import {
getMidenFaucetDecimals,
isPragmaSupportedMidenAsset,
} from "../../constants/miden-tokens";
import { EPOCH_TESTNET_TOKENS } from "../../constants/evm-tokens";
import { EPOCH_EVM_TOKENS } from "../../constants/evm-tokens";
import { midenscanNoteUrl } from "../../lib/explorers";
import {
extractIntentIdentity,
readIntentError,
} from "../../lib/intent-result";
import { useIntentSettlementView } from "../../hooks/useIntentSettlementView";
import { useMidenP2IDNoteFactory } from "../../hooks/useMidenP2IDNoteFactory";
import type { CachedMidenNote } from "../../lib/miden-note-retry";
import { useMidenNetwork } from "../../hooks/useMidenNetwork";
import { getMidenPublicAccountError } from "../../lib/miden-account-validation";
import { Button } from "@/components/ui/button";
import { IntentSourceAssetField } from "./intent/IntentSourceAssetField";
import {
Expand All @@ -39,9 +41,10 @@ interface Props {
midenAssets: MidenAssetOption[];
isLoadingMidenAssets: boolean;
onFetchQuote: (params: CrossChainIntentParams) => Promise<void>;
onConfirmIntent: (
createMidenP2IDENote: SolveIntentParams["createMidenP2IDENote"],
) => Promise<unknown>;
onConfirmIntent: (input: {
createMidenP2IDENote: SolveIntentParams["createMidenP2IDENote"];
currentMidenAccountId: string;
}) => Promise<unknown>;
onClearQuote: () => void;
quotePhase: IntentQuotePhase;
isSDKReady: boolean;
Expand Down Expand Up @@ -82,13 +85,13 @@ export function IntentForm({
// useState defaults: the wallet connects after this mounts.
const [edits, setEdits] = useState<Partial<IntentDestination>>({});
const destination: IntentDestination = {
outputToken: edits.outputToken ?? EPOCH_TESTNET_TOKENS[0].address,
minTokenOut: edits.minTokenOut ?? "1000000000000000000",
outputToken: edits.outputToken ?? EPOCH_EVM_TOKENS[0].address,
minTokenOut: edits.minTokenOut ?? "1000000",
chainId:
edits.chainId ??
(isSupportedTestnetEvmChain(walletChainId)
(isSupportedEvmChain(walletChainId)
? String(walletChainId)
: DEFAULT_TESTNET_CHAIN_ID_STR),
: DEFAULT_EVM_CHAIN_ID_STR),
evmAddress: edits.evmAddress ?? address ?? "",
};
const [confirmStatus, setConfirmStatus] = useState("");
Expand All @@ -98,13 +101,27 @@ export function IntentForm({
const [localIntentUserAddress, setLocalIntentUserAddress] = useState<
string | undefined
>(undefined);
const [localMidenNoteId, setLocalMidenNoteId] = useState<string | undefined>(
undefined,
);
const [localMidenNote, setLocalMidenNote] = useState<
CachedMidenNote | undefined
>(undefined);
const previousMidenAccountId = useRef(midenAccountId);

useEffect(() => {
if (previousMidenAccountId.current !== midenAccountId) {
setLocalMidenNote(undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/no-adjust-state-on-prop-change (warning)

This effect adjusts state after a prop changes, so users briefly see the stale value.

Fix → Remove the adjustment effect by deriving values during render, resetting the component with a key, or updating related state in the event that changes the prop. Avoid tracking the previous prop in more state, which preserves the duplication. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes

Docs

onClearQuote();
previousMidenAccountId.current = midenAccountId;
}
}, [midenAccountId, onClearQuote]);

const clearQuote = () => {
setLocalMidenNote(undefined);
onClearQuote();
};

const editDestination = (key: keyof IntentDestination, value: string) => {
setEdits((prev) => ({ ...prev, [key]: value }));
onClearQuote();
clearQuote();
};

const destinationChainIdNum = Number.parseInt(destination.chainId, 10);
Expand All @@ -114,6 +131,10 @@ export function IntentForm({
destination.evmAddress.trim(),
);

const midenAccountError = midenAccountId
? getMidenPublicAccountError(midenAccountId, "Miden source account")
: "Connect Miden wallet first";

const selectedAsset = supportedMidenAssets.find(
(a) => a.assetId.toLowerCase() === resolvedSelectedAssetId.toLowerCase(),
);
Expand All @@ -132,8 +153,9 @@ export function IntentForm({

const createMidenP2IDENote = useMidenP2IDNoteFactory({
midenAccountId,
existingNote: localMidenNote,
onStatus: setConfirmStatus,
onNoteCreated: setLocalMidenNoteId,
onNoteCreated: setLocalMidenNote,
});

const buildParams = (): CrossChainIntentParams => {
Expand All @@ -151,6 +173,9 @@ export function IntentForm({
if (!midenAccountId) {
throw new Error("Connect Miden wallet first");
}
if (midenAccountError) {
throw new Error(midenAccountError);
}
return {
midenAccountId,
midenFaucetId: resolvedSelectedAssetId,
Expand All @@ -164,6 +189,7 @@ export function IntentForm({
const canFetch =
isSDKReady &&
!!midenAccountId &&
!midenAccountError &&
!!resolvedSelectedAssetId &&
hasValidEvmRecipient &&
!!destination.outputToken &&
Expand All @@ -186,11 +212,18 @@ export function IntentForm({

const handleConfirm = () => {
if (quotePhase.status !== "ready") return;
if (!midenAccountId || midenAccountError) {
setConfirmStatus(`Error: ${midenAccountError}`);
return;
}

void toast.promise(
(async () => {
setConfirmStatus("Submitting intent…");
const result = await onConfirmIntent(createMidenP2IDENote);
const result = await onConfirmIntent({
createMidenP2IDENote,
currentMidenAccountId: midenAccountId,
});

const solverError = readIntentError(result);
if (solverError) throw new Error(solverError);
Expand Down Expand Up @@ -245,10 +278,16 @@ export function IntentForm({
unsupportedAssetCount={unsupportedAssetCount}
onSelect={(assetId) => {
setSelectedAssetId(assetId);
onClearQuote();
clearQuote();
}}
/>

{midenAccountError && (
<p className="text-xs text-red-600" role="alert">
{midenAccountError}
</p>
)}

<IntentDestinationFields
values={destination}
onChange={editDestination}
Expand All @@ -259,7 +298,7 @@ export function IntentForm({
quote={activeQuote}
faucetDecimals={midenFaucetDecimals}
assetSymbol={selectedAsset?.symbol}
onRequote={onClearQuote}
onRequote={clearQuote}
/>
)}

Expand Down Expand Up @@ -299,11 +338,11 @@ export function IntentForm({
</p>
)}

{localMidenNoteId && (
{localMidenNote && (
<ExplorerHashCard
label="Miden note id (P2IDE)"
value={localMidenNoteId}
href={midenscanNoteUrl(localMidenNoteId)}
value={localMidenNote.noteId}
href={midenscanNoteUrl(localMidenNote.noteId)}
linkLabel="View on Midenscan"
tone="neutral"
/>
Expand Down
19 changes: 19 additions & 0 deletions src/components/crosschain/WithdrawForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { truncateHash } from "../../lib/explorers";
import { readDepositHash, readIntentError } from "../../lib/intent-result";
import { findEvmToken } from "../../constants/evm-tokens";
import { getMidenNetworkConfig } from "../../config/miden";
import { getMidenPublicAccountError } from "../../lib/miden-account-validation";
import { Button } from "@/components/ui/button";
import {
CUSTOM_TOKEN_DEFAULT_DECIMALS,
Expand Down Expand Up @@ -56,6 +57,9 @@ export function WithdrawForm({

// Defaults to the connected wallet until the user types their own.
const midenRecipientId = midenRecipientInput || (accounts[0]?.id ?? "");
const midenRecipientError = midenRecipientId
? getMidenPublicAccountError(midenRecipientId, "Miden destination account")
: "Enter a Miden destination account";

const resolvedFaucetId = midenFaucetId.trim();
const finalToken = customToken || evmToken;
Expand All @@ -71,6 +75,9 @@ export function WithdrawForm({
if (!connectedAddress) {
throw new Error("Connect EVM wallet first");
}
if (midenRecipientError) {
throw new Error(midenRecipientError);
}
return {
sourceChainId: walletChainId,
destinationChainId: MIDEN_VIRTUAL_CHAIN_ID,
Expand All @@ -88,6 +95,7 @@ export function WithdrawForm({
!!connectedAddress &&
walletChainId > 0 &&
!!midenRecipientId &&
!midenRecipientError &&
!!resolvedFaucetId &&
!!finalToken &&
minTokenOut.trim() !== "" &&
Expand Down Expand Up @@ -121,6 +129,11 @@ export function WithdrawForm({
};

const handleConfirm = async () => {
if (midenRecipientError) {
setStatus(`Error: ${midenRecipientError}`);
toast.error(midenRecipientError, { id: WITHDRAW_DEPOSIT_TOAST_ID });
return;
}
// Two-stage toast: (1) Compact deposit signature/confirmation,
// (2) SIO Miden settlement — dismissed by parent WithdrawTab once the
// synthetic Miden row appears in the status poll.
Expand Down Expand Up @@ -209,6 +222,12 @@ export function WithdrawForm({
}}
/>

{midenRecipientError && (
<p className="text-xs text-red-600" role="alert">
{midenRecipientError}
</p>
)}

{pendingQuote && (
<WithdrawQuoteSummary
quote={pendingQuote}
Expand Down
13 changes: 5 additions & 8 deletions src/components/crosschain/intent/IntentDestinationFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
EPOCH_TESTNET_EVM_CHAINS,
getTestnetChainName,
} from "../../../constants/chains";
import { EPOCH_TESTNET_TOKENS } from "../../../constants/evm-tokens";
import { EPOCH_EVM_CHAINS, getEvmChainName } from "../../../constants/chains";
import { EPOCH_EVM_TOKENS } from "../../../constants/evm-tokens";

export interface IntentDestination {
outputToken: string;
Expand All @@ -30,7 +27,7 @@ export function IntentDestinationFields({ values, onChange }: Props) {
<>
<div className="grid grid-cols-2 gap-3">
<div>
<Label>Output token ({getTestnetChainName(values.chainId)})</Label>
<Label>Output token ({getEvmChainName(values.chainId)})</Label>
<SelectRoot
value={values.outputToken}
onValueChange={(v) => onChange("outputToken", v)}
Expand All @@ -39,7 +36,7 @@ export function IntentDestinationFields({ values, onChange }: Props) {
<SelectValue placeholder="Token" />
</SelectTrigger>
<SelectContent>
{EPOCH_TESTNET_TOKENS.map((token) => (
{EPOCH_EVM_TOKENS.map((token) => (
<SelectItem key={token.symbol} value={token.address}>
{token.symbol}
{token.address ? ` · ${token.address.slice(0, 10)}…` : ""}
Expand Down Expand Up @@ -70,7 +67,7 @@ export function IntentDestinationFields({ values, onChange }: Props) {
<SelectValue placeholder="Chain" />
</SelectTrigger>
<SelectContent>
{EPOCH_TESTNET_EVM_CHAINS.map((chain) => (
{EPOCH_EVM_CHAINS.map((chain) => (
<SelectItem key={chain.id} value={String(chain.id)}>
{chain.name} ({chain.id})
</SelectItem>
Expand Down
4 changes: 2 additions & 2 deletions src/components/crosschain/withdraw/WithdrawTokenFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { getTestnetChainName } from "../../../constants/chains";
import { getEvmChainName } from "../../../constants/chains";
import { TOKEN_CUSTOM, WITHDRAW_TOKENS } from "./withdraw-tokens";

interface Props {
Expand Down Expand Up @@ -35,7 +35,7 @@ export function WithdrawTokenFields({
return (
<>
<div>
<Label>Source EVM token ({getTestnetChainName(walletChainId)})</Label>
<Label>Source EVM token ({getEvmChainName(walletChainId)})</Label>
<SelectRoot
value={isCustom ? TOKEN_CUSTOM : evmToken}
onValueChange={(v) => onEvmTokenChange(v === TOKEN_CUSTOM ? "" : v)}
Expand Down
Loading
Loading