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
43 changes: 38 additions & 5 deletions src/components/crosschain/IntentForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useState } from "react";
import { useAccount, useChainId } from "wagmi";
import { toast } from "sonner";
import type { SolveIntentParams } from "@epoch-protocol/epoch-intents-sdk";
import type {
MidenNoteVisibility,
SolveIntentParams,
} from "@epoch-protocol/epoch-intents-sdk";
import type {
CrossChainIntentParams,
MidenAssetOption,
Expand All @@ -20,12 +23,14 @@ import {
} from "../../lib/intent-result";
import { useIntentSettlementView } from "../../hooks/useIntentSettlementView";
import { useMidenP2IDNoteFactory } from "../../hooks/useMidenP2IDNoteFactory";
import { useMidenPrivateNotesSupport } from "../../hooks/useMidenPrivateNotesSupport";
import { Button } from "@/components/ui/button";
import { IntentSourceAssetField } from "./intent/IntentSourceAssetField";
import {
IntentDestinationFields,
type IntentDestination,
} from "./intent/IntentDestinationFields";
import { IntentNoteVisibilityField } from "./intent/IntentNoteVisibilityField";
import { IntentQuoteSummary } from "./intent/IntentQuoteSummary";
import { SettlementPendingCard } from "./intent/SettlementPendingCard";
import { ExplorerHashCard } from "./intent/ExplorerHashCard";
Expand All @@ -35,9 +40,10 @@ interface Props {
midenAssets: MidenAssetOption[];
isLoadingMidenAssets: boolean;
onFetchQuote: (params: CrossChainIntentParams) => Promise<void>;
onConfirmIntent: (
createMidenP2IDNote: SolveIntentParams["createMidenP2IDNote"],
) => Promise<unknown>;
onConfirmIntent: (args: {
createMidenP2IDNote: SolveIntentParams["createMidenP2IDNote"];
midenNoteVisibility: SolveIntentParams["midenNoteVisibility"];
}) => Promise<unknown>;
onClearQuote: () => void;
quotePhase: IntentQuotePhase;
isSDKReady: boolean;
Expand Down Expand Up @@ -120,6 +126,17 @@ export function IntentForm({
onNoteCreated: setLocalMidenNoteId,
});

const { isSupported: isPrivateSupported, isLoading: isLoadingSupport } =
useMidenPrivateNotesSupport();
const [noteVisibility, setNoteVisibility] =
useState<MidenNoteVisibility>("public");
// The select disables "private" when unsupported, but that state can go stale
// (allocator config change, a different backend). Block the submit rather than
// quietly downgrading: silently publishing the account and amount someone
// asked to keep off-chain is worse than refusing.
const privateUnavailable =
noteVisibility === "private" && !isPrivateSupported;

const buildParams = (): CrossChainIntentParams => {
if (!destination.evmAddress) {
throw new Error("Connect EVM wallet first");
Expand Down Expand Up @@ -176,11 +193,20 @@ export function IntentForm({

const handleConfirm = () => {
if (quotePhase.status !== "ready") return;
if (privateUnavailable) {
toast.error(
"This allocator cannot accept private notes yet. Switch to public, or point at an allocator that advertises midenPrivateNotesSupported.",
);
return;
}

void toast.promise(
(async () => {
setConfirmStatus("Submitting intent…");
const result = await onConfirmIntent(createMidenP2IDNote);
const result = await onConfirmIntent({
createMidenP2IDNote,
midenNoteVisibility: noteVisibility,
});

const solverError = readIntentError(result);
if (solverError) throw new Error(solverError);
Expand Down Expand Up @@ -243,6 +269,13 @@ export function IntentForm({
onChange={editDestination}
/>

<IntentNoteVisibilityField
value={noteVisibility}
onSelect={setNoteVisibility}
isPrivateSupported={isPrivateSupported}
isLoadingSupport={isLoadingSupport}
/>

{activeQuote && (
<IntentQuoteSummary
quote={activeQuote}
Expand Down
2 changes: 2 additions & 0 deletions src/components/crosschain/IntentStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface IntentFlowStatus {
midenTxId?: string;
midenStatus?: string;
midenNoteId?: string;
/** Base64 NoteFile — private EVM→Miden payouts only. */
midenNoteBytes?: string;
latestStatusLabel?: string;
latestChainId?: string;
statusCount?: number;
Expand Down
6 changes: 6 additions & 0 deletions src/components/crosschain/QuoteSummaryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import type { ReactNode } from "react";

interface Props {
amountText: string;
/** "Miden" or "EVM" — which wallet must hold the funds. */
walletNoun: string;
clearLabel: string;
onClear: () => void;
/** Rendered under the amount, above the reminder. */
detail?: ReactNode;
}

export function QuoteSummaryCard({
amountText,
walletNoun,
clearLabel,
onClear,
detail,
}: Props) {
return (
<div className="rounded-xl border border-primary/30 bg-primary/5 p-4 space-y-3">
Expand All @@ -32,6 +37,7 @@ export function QuoteSummaryCard({
{amountText}
</p>
</div>
{detail}
<p className="text-xs text-neutral-500 italic">
Keep at least this amount in your {walletNoun} wallet before confirming.
</p>
Expand Down
15 changes: 15 additions & 0 deletions src/components/crosschain/WithdrawForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react";
import type { MidenNoteVisibility } from "@epoch-protocol/epoch-intents-sdk";
import { useAccount, useChainId } from "wagmi";
import { toast } from "sonner";
import { MIDEN_VIRTUAL_CHAIN_ID } from "@epoch-protocol/epoch-intents-sdk";
Expand All @@ -17,6 +18,7 @@ import {
WITHDRAW_SETTLE_TOAST_ID,
} from "./withdraw/withdraw-toasts";
import { WithdrawTokenFields } from "./withdraw/WithdrawTokenFields";
import { WithdrawNoteVisibilityField } from "./withdraw/WithdrawNoteVisibilityField";
import { WithdrawAccountFields } from "./withdraw/WithdrawAccountFields";
import { WithdrawQuoteSummary } from "./withdraw/WithdrawQuoteSummary";

Expand Down Expand Up @@ -49,6 +51,8 @@ export function WithdrawForm({
"0xfc90f0f4da30e51168453b60eafed7",
);
const [status, setStatus] = useState("");
const [noteVisibility, setNoteVisibility] =
useState<MidenNoteVisibility>("public");

const { address: connectedAddress } = useAccount();
const walletChainId = useChainId();
Expand Down Expand Up @@ -79,6 +83,7 @@ export function WithdrawForm({
midenRecipientId,
midenFaucetId: resolvedFaucetId,
minTokenOut: minTokenOut.trim(),
midenNoteVisibility: noteVisibility,
};
};

Expand Down Expand Up @@ -194,6 +199,16 @@ export function WithdrawForm({
}}
/>

<WithdrawNoteVisibilityField
value={noteVisibility}
onSelect={(v) => {
setNoteVisibility(v);
// The choice is part of the signed mandate, so a stale quote would
// register a claim hash for the OTHER visibility.
onClearQuote();
}}
/>

<WithdrawAccountFields
connectedAddress={connectedAddress}
midenRecipientId={midenRecipientId}
Expand Down
64 changes: 64 additions & 0 deletions src/components/crosschain/intent/IntentNoteVisibilityField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Label } from "@/components/ui/label";
import {
SelectContent,
SelectItem,
SelectRoot,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { MidenNoteVisibility } from "@epoch-protocol/epoch-intents-sdk";

interface Props {
value: MidenNoteVisibility;
onSelect: (value: MidenNoteVisibility) => void;
/** Allocator advertises `midenPrivateNotesSupported` — see useMidenPrivateNotesSupport. */
isPrivateSupported: boolean;
isLoadingSupport: boolean;
}

export function IntentNoteVisibilityField({
value,
onSelect,
isPrivateSupported,
isLoadingSupport,
}: Props) {
return (
<div className="space-y-2">
<Label>Collateral note visibility</Label>
<SelectRoot
value={value}
onValueChange={(v) => onSelect(v as MidenNoteVisibility)}
>
<SelectTrigger aria-label="Select collateral note visibility">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="public">Public — full note on Miden</SelectItem>
<SelectItem value="private" disabled={!isPrivateSupported}>
Private — only the commitment on Miden
{isPrivateSupported ? "" : " (allocator unsupported)"}
</SelectItem>
</SelectContent>
</SelectRoot>
<p className="text-xs text-neutral-500">
{value === "private" ? (
<>
Your Miden account, the faucet and the amount stay off-chain. The
note body is sent to the allocator with the intent — it is the only
copy, so a failed submission strands the collateral.
</>
) : (
<>
The note&apos;s target account, faucet and amount are published to
Miden&apos;s note database and readable by anyone.
{isLoadingSupport
? " Checking whether the allocator supports private notes…"
: isPrivateSupported
? ""
: " This allocator cannot accept private notes yet."}
</>
)}
</p>
</div>
);
}
Loading