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
31,415 changes: 31,415 additions & 0 deletions demo/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@epoch-protocol/epoch-intents-sdk": "^1.0.37",
"@epoch-protocol/epoch-intent-widget": "^0.1.3",
"@epoch-protocol/epoch-intents-sdk": "file:../../smallocator/sdk",
"@epoch-protocol/epoch-intent-widget": "file:../",
"@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",
Expand Down
89 changes: 20 additions & 69 deletions demo/src/earn/useEarnMidenAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,36 @@
import { useCallback, useMemo } from 'react';
import { SendTransaction } from '@miden-sdk/miden-wallet-adapter-base';
import { useMidenFiWallet } from '@miden-sdk/miden-wallet-adapter-react';
import { toast } from 'sonner';
import {
getMidenGraphTokens,
midenFaucetKey,
type EarnMidenAdapter,
} from '@epoch-protocol/epoch-intent-widget';
import { type EarnMidenAdapter } from '@epoch-protocol/epoch-intent-widget';
import { useMidenWalletAdapter } from '../miden/hooks/useMidenWalletAdapter';
import { useMidenP2IDNoteFactory } from '../miden/hooks/useMidenP2IDNoteFactory';

/**
* Bridges the demo's Miden wallet adapter into {@link EarnMidenAdapter}, shared by
* the earn and pay/swap flows. Surfaces every Miden testnet faucet from the Epoch
* graph with the wallet's balance overlaid by faucet id.
* the earn and pay/swap flows.
*/
export function useEarnMidenAdapter(): EarnMidenAdapter {
const midenWallet = useMidenWalletAdapter({ enabled: true });
const { requestSend, waitForTransaction } = useMidenFiWallet();

const assets = useMemo(() => {
// Every Miden testnet faucet from the graph, wallet balances overlaid by
// faucet id. The wallet can encode ids as bech32 or hex, so match on the
// normalized key rather than a raw string compare.
return getMidenGraphTokens(true).map((t) => {
const match = midenWallet.assets.find(
(a) => midenFaucetKey(a.assetId) === midenFaucetKey(t.faucetId),
);
return {
faucetId: t.faucetId,
symbol: t.symbol,
decimals: t.decimals,
balance: match?.amount ?? 0n,
};
});
// 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.
return midenWallet.assets.map((a) => ({
faucetId: a.assetId,
symbol: a.symbol ?? '',
decimals: a.decimals ?? 6,
balance: a.amount,
}));
}, [midenWallet.assets]);

const createP2IDNote = useCallback<EarnMidenAdapter['createP2IDNote']>(
async (faucetIdParam, amountParam, allocatorId) => {
try {
if (!midenWallet.accountId?.hex) {
throw new Error('Connect Miden wallet first');
}
if (!requestSend) {
throw new Error('Miden wallet adapter not available');
}
// Checked before the send, not after: requestSend broadcasts a real
// transaction, so bailing out afterwards would move funds and still
// throw, leaving the note unreadable.
if (!waitForTransaction) {
throw new Error('waitForTransaction not available in adapter');
}

const normalizedAmount = BigInt(amountParam);
if (normalizedAmount > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new Error('Amount too large for wallet adapter send');
}

const payload = new SendTransaction(
midenWallet.accountId.hex,
allocatorId,
faucetIdParam,
'public',
Number(normalizedAmount),
);
const txId = await requestSend(payload);
const finalized = await waitForTransaction(txId, 120_000);
const first = finalized.outputNotes?.[0];
const noteId = first ? first.id().toString() : '';
if (!noteId) {
throw new Error(`Could not read output note id for tx ${txId}`);
}
return { success: true, noteId };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : String(err),
};
}
},
[midenWallet.accountId?.hex, requestSend, waitForTransaction],
);
// The SDK also hands this callback a relative `recallBlocks` and the
// mandate-binding attachment felts; both have to make it into the note, so the
// minting lives in the shared factory rather than a plain wallet send.
const createP2IDNote = useMidenP2IDNoteFactory({
midenAccountId: midenWallet.accountId?.hex ?? null,
});

const connect = useCallback(async () => {
try {
Expand Down
8 changes: 7 additions & 1 deletion demo/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
WalletAdapterNetwork,
} from '@miden-sdk/miden-wallet-adapter-base';
import { MidenFiSignerProvider } from '@miden-sdk/miden-wallet-adapter-react';
import { MidenProvider } from '@miden-sdk/react';
import { themeToCssVars, LIGHT_THEME } from '@epoch-protocol/epoch-intent-widget';
import App from './app/App';

Expand Down Expand Up @@ -58,7 +59,12 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
appName="Epoch Intent Widget Demo"
allowedPrivateData={AllowedPrivateData.Assets}
>
<App />
{/* 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' }}>
<App />
</MidenProvider>
<Toaster position="bottom-right" closeButton duration={5000} />
</MidenFiSignerProvider>
</RainbowKitProvider>
Expand Down
54 changes: 9 additions & 45 deletions demo/src/miden/hooks/useMidenBridgeIntent.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useState } from "react";
import { toast } from "sonner";
import { SendTransaction } from "@miden-sdk/miden-wallet-adapter-base";
import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
import type { SolveIntentParams } from "@epoch-protocol/epoch-intents-sdk/dist/types";
import type { CrossChainIntentParams } from "../types/miden";
import { useMidenP2IDNoteFactory } from "./useMidenP2IDNoteFactory";

interface UseMidenBridgeIntentOptions {
epoch: {
Expand Down Expand Up @@ -61,7 +60,6 @@ export function useMidenBridgeIntent({
outputToken,
resolvedEvmRecipient,
}: UseMidenBridgeIntentOptions): MidenBridgeIntent {
const { requestSend, waitForTransaction } = useMidenFiWallet();
const [confirmStatus, setConfirmStatus] = useState("");
const [localMidenNoteId, setLocalMidenNoteId] = useState<string>();
const [localIntentNonce, setLocalIntentNonce] = useState<string>();
Expand All @@ -83,48 +81,14 @@ export function useMidenBridgeIntent({
});
};

const createMidenP2IDNote: SolveIntentParams["createMidenP2IDNote"] = async (
faucetIdParam,
amountParam,
allocatorId,
) => {
setConfirmStatus("Creating P2IDE note on Miden…");
try {
if (!midenAccountIdHex) throw new Error("Missing Miden account id");
if (!requestSend) throw new Error("Miden wallet adapter not available");
// Checked before the send, not after: requestSend broadcasts a real
// transaction, so bailing out afterwards would move funds and still
// throw, leaving the note unreadable.
if (!waitForTransaction)
throw new Error("waitForTransaction not available in adapter");

const normalizedAmount = BigInt(amountParam);
if (normalizedAmount > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new Error("Amount too large for wallet adapter send");
}

const payload = new SendTransaction(
midenAccountIdHex,
allocatorId,
faucetIdParam,
"public",
Number(normalizedAmount),
);
const txId = await requestSend(payload);
const finalized = await waitForTransaction(txId, 120_000);
const first = finalized.outputNotes?.[0];
const noteId = first ? first.id().toString() : "";
if (!noteId)
throw new Error(`Could not read output note id for tx ${txId}`);
setLocalMidenNoteId(noteId);
return { success: true, noteId };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : String(err),
};
}
};
// Mints the P2IDE note *with* its reclaim height and mandate-binding
// attachment — a plain wallet send carries neither, and the allocator rejects
// such a note as "not bound to the intent mandate".
const createMidenP2IDNote = useMidenP2IDNoteFactory({
midenAccountId: midenAccountIdHex ?? null,
onStatus: setConfirmStatus,
onNoteCreated: setLocalMidenNoteId,
});

const confirm = () => {
if (!epoch.pendingQuote) return;
Expand Down
156 changes: 156 additions & 0 deletions demo/src/miden/hooks/useMidenP2IDNoteFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { useCallback } from "react";
import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
import { Transaction } from "@miden-sdk/miden-wallet-adapter-base";
import { useMiden } from "@miden-sdk/react";
import {
AccountId,
FungibleAsset,
Note,
NoteArray,
NoteAssets,
NoteAttachment,
NoteType,
TransactionRequestBuilder,
} from "@miden-sdk/miden-sdk";
import type { EarnMidenCreateP2IDNote } from "@epoch-protocol/epoch-intent-widget";

interface Options {
midenAccountId: string | null;
/** Progress copy for the host UI (toast / status line). Optional. */
onStatus?: (message: string) => void;
onNoteCreated?: (noteId: string) => void;
}

const WAIT_FOR_TRANSACTION_TIMEOUT_MS = 120_000;

/** Epoch Miden ids are 0x-hex; fall back to bech32 for wallet-formatted ids. */
function toAccountId(id: string): AccountId {
const s = id.trim();
return s.startsWith("0x") ? AccountId.fromHex(s) : AccountId.fromBech32(s);
}

/**
* Mints the reclaimable P2IDE collateral note, binding it to the intent's mandate
* via the attachment felts the SDK computed (Compact-equivalent witness hash).
*
* Submitted through the WALLET (`requestTransaction` + `createCustomTransaction`)
* rather than the SDK client's own transaction hooks: the wallet holds the
* account's state, whereas the SDK client's local store may not. The wallet's
* `SendTransaction` cannot carry an attachment, so the note is built as a custom
* `TransactionRequest` around `Note.createP2IDENote(…, reclaim, …, attachment)`
* — the one API that supports reclaim + attachment together. A note minted with
* plain `SendTransaction` has neither, and the allocator rejects it with "Miden
* note is not bound to the intent mandate".
*
* Shared by every Miden-funded flow in the demo (earn deposits and the bridge
* panel) so the binding can't drift back out of one of them.
*/
export function useMidenP2IDNoteFactory({
midenAccountId,
onStatus,
onNoteCreated,
}: Options): EarnMidenCreateP2IDNote {
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();

return useCallback<EarnMidenCreateP2IDNote>(
async (
faucetIdParam,
amountParam,
allocatorId,
recallBlocks,
bindingAttachmentFelts,
) => {
onStatus?.("Resource lock required — creating P2IDE note on Miden…");
try {
if (!midenAccountId) {
throw new Error("Missing Miden account id");
}
if (!bindingAttachmentFelts?.length) {
throw new Error("Missing mandate-binding attachment felts from SDK");
}
if (!requestTransaction) {
throw new Error("Wallet does not support custom transactions");
}
if (!isReady || !client) {
throw new Error(
"Miden client not ready yet — retry once it initializes",
);
}

const assets = new NoteAssets([
new FungibleAsset(toAccountId(faucetIdParam), BigInt(amountParam)),
]);
// Mandate binding: the witness hash the SDK computed, written verbatim as
// the note attachment (part of the note commitment, so tamper-proof).
const attachment = new NoteAttachment(
BigUint64Array.from(bindingAttachmentFelts),
);

// P2IDE reclaim height is ABSOLUTE; the SDK hands over a RELATIVE
// recallBlocks (allocator minimum + buffer). Convert against the client's
// synced chain tip (getSyncHeight needs the chain, not the account).
const currentBlock = await client.getSyncHeight();
if (!Number.isFinite(currentBlock) || currentBlock <= 0) {
throw new Error(
"Miden client not synced yet — retry once the block height is available",
);
}
const note = Note.createP2IDENote(
toAccountId(midenAccountId),
toAccountId(allocatorId),
assets,
currentBlock + recallBlocks,
undefined, // no time-lock
NoteType.Public,
attachment,
);
const noteId = note.id().toString();
if (!noteId) {
throw new Error("Could not compute note id for the minted note");
}

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

// Submit through the wallet (holds the account + signs).
const txId = await requestTransaction(
Transaction.createCustomTransaction(
midenAccountId,
allocatorId,
txRequest,
),
);

// Wait for finalization before returning, so the note is committed and
// queryable when the allocator fetches it during intent validation.
// Without this the intent can race ahead of the note and be rejected
// "not found on-chain".
if (waitForTransaction) {
onStatus?.("P2IDE note created — waiting for finalization on Miden…");
await waitForTransaction(txId, WAIT_FOR_TRANSACTION_TIMEOUT_MS);
}

onNoteCreated?.(noteId);
return { success: true, noteId };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : String(err),
};
}
},
[
midenAccountId,
requestTransaction,
waitForTransaction,
client,
isReady,
onStatus,
onNoteCreated,
],
);
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
"test": "node --import tsx --test test/*.test.ts test/*.test.tsx"
},
"dependencies": {
"@epoch-protocol/epoch-commons-sdk": "^0.1.17",
"@epoch-protocol/epoch-commons-sdk": "file:../epoch-commons-sdk",
"@epoch-protocol/epoch-flows-sdk": "^0.1.7",
"@epoch-protocol/epoch-intents-sdk": "^1.0.37",
"@epoch-protocol/epoch-intents-sdk": "file:../smallocator/sdk",
"clsx": "^2.1.1",
"tailwind-merge": "^2.6.1"
},
Expand Down
Loading