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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"prepare": "husky"
},
"dependencies": {
"@epoch-protocol/epoch-intents-sdk": "^1.0.35",
"@epoch-protocol/epoch-intents-sdk": "file:../smallocator/sdk",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@metamask/sdk": "^0.34.0",
"@miden-sdk/miden-sdk": "^0.15.2",
Expand Down
18 changes: 9 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 0 additions & 8 deletions pnpm-workspace.yaml

This file was deleted.

141 changes: 109 additions & 32 deletions src/hooks/useMidenP2IDNoteFactory.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,135 @@
import { useCallback } from "react";
import { useMidenFiWallet } from "@miden-sdk/miden-wallet-adapter-react";
import { SendTransaction } from "@miden-sdk/miden-wallet-adapter-base";
import { Transaction } from "@miden-sdk/miden-wallet-adapter-base";
import { useMiden } from "@miden-sdk/react";
import {
Note,
NoteType,
AccountId,
NoteAssets,
FungibleAsset,
NoteArray,
NoteAttachment,
TransactionRequestBuilder,
} from "@miden-sdk/miden-sdk";
import type { SolveIntentParams } from "@epoch-protocol/epoch-intents-sdk";

const WAIT_FOR_TRANSACTION_TIMEOUT_MS = 120_000;

interface Options {
midenAccountId: string | null;
onStatus: (message: string) => void;
onNoteCreated: (noteId: string) => void;
}

/** Mints the recallable P2IDE note the SDK asks for when an intent needs a resource lock. */
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 recallable 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 `useTransaction`: the wallet holds the account's
* state, whereas the SDK client's local store may not (which caused
* "account data wasn't found"). The wallet's `SendTransaction` can't carry an
* attachment, so we build a custom `TransactionRequest` whose output note is a
* P2IDE note created with `Note.createP2IDENote(..., reclaim, type, attachment)`
* — the one API that supports reclaim + attachment together.
*/
export function useMidenP2IDNoteFactory({
midenAccountId,
onStatus,
onNoteCreated,
}: Options): SolveIntentParams["createMidenP2IDNote"] {
const { requestSend, waitForTransaction } = useMidenFiWallet();
const { requestTransaction, waitForTransaction } = useMidenFiWallet();
// useMiden() is non-throwing (unlike useMidenClient, which throws before the
// client initializes); we gate on readiness inside the callback instead.
const { client, isReady } = useMiden();

return useCallback<NonNullable<SolveIntentParams["createMidenP2IDNote"]>>(
async (faucetIdParam, amountParam, allocatorId, recallBlocks) => {
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 (!requestSend) {
throw new Error("Miden wallet adapter not available");
if (!bindingAttachmentFelts?.length) {
throw new Error("Missing mandate-binding attachment felts from SDK");
}
if (!requestTransaction) {
throw new Error("Wallet does not support custom transactions");
}
// Before requestSend: broadcasting first would strand the user's funds.
if (!waitForTransaction) {
throw new Error("waitForTransaction not available in adapter");
if (!isReady || !client) {
throw new Error(
"Miden client not ready yet — retry once it initializes",
);
}

const normalizedAmount = BigInt(amountParam);
if (normalizedAmount > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new Error("Amount too large for wallet adapter send");
const assets = new NoteAssets([
new FungibleAsset(toAccountId(faucetIdParam), BigInt(amountParam)),
]);
// Mandate binding: witness hash the SDK computed, written verbatim as the
// note attachment (part of the note commitment, tamper-proof).
const attachment = new NoteAttachment(
BigUint64Array.from(bindingAttachmentFelts),
);

// P2IDE reclaim height is ABSOLUTE; the SDK gives a RELATIVE recallBlocks
// (allocator min + 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 reclaimHeight = currentBlock + recallBlocks;
const note = Note.createP2IDENote(
toAccountId(midenAccountId),
toAccountId(allocatorId),
assets,
reclaimHeight,
undefined, // no time-lock
NoteType.Public,
attachment,
);
const noteId = note.id().toString();

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

if (!noteId) {
throw new Error("Could not compute note id for the minted note");
}

// recallBlocks comes from the SDK (allocator's published reclaim minimum
// + buffer). Without it the note mints as a plain P2ID with no recall
// window, so a failed intent would strand the funds.
const payload = new SendTransaction(
// Submit through the wallet (holds the account + signs).
const customTx = Transaction.createCustomTransaction(
midenAccountId,
allocatorId,
faucetIdParam,
"public",
Number(normalizedAmount),
recallBlocks,
txRequest,
);
const txId = await requestSend(payload);
const txId = await requestTransaction(customTx);

const finalized = await waitForTransaction(
txId,
WAIT_FOR_TRANSACTION_TIMEOUT_MS,
);
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}`);
// 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) {
Expand All @@ -70,6 +139,14 @@ export function useMidenP2IDNoteFactory({
};
}
},
[midenAccountId, requestSend, waitForTransaction, onStatus, onNoteCreated],
[
midenAccountId,
requestTransaction,
waitForTransaction,
client,
isReady,
onStatus,
onNoteCreated,
],
);
}
8 changes: 7 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Toaster } from "sonner";
import App from "./App";
import { config } from "./config/wagmi";
import { MidenFiSignerProvider } from "@miden-sdk/miden-wallet-adapter-react";
import { MidenProvider } from "@miden-sdk/react";
import {
AllowedPrivateData,
WalletAdapterNetwork,
Expand All @@ -34,7 +35,12 @@ createRoot(document.getElementById("root")!).render(
appName="Miden Integration Example"
allowedPrivateData={AllowedPrivateData.Assets}
>
<App />
{/* MidenProvider powers the @miden-sdk/react hooks (useSend /
useSyncState) that the P2IDE note factory uses to write the F-01
recipient-binding attachment. */}
<MidenProvider config={{ rpcUrl: "testnet" }}>
<App />
</MidenProvider>
<Toaster position="bottom-right" closeButton duration={5_000} />
</MidenFiSignerProvider>
</RainbowKitProvider>
Expand Down
2 changes: 1 addition & 1 deletion src/services/epoch-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function formatQuoteTokenIn(
* intent is fulfilled — privacy-preserving on the Miden side, trustless on EVM side.
*/

const ZERO_HASH =
export const ZERO_HASH =
"0x0000000000000000000000000000000000000000000000000000000000000000";

export function normalizeMidenIdToHex(id: string): string {
Expand Down
5 changes: 5 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export default defineConfig({
},
optimizeDeps: {
exclude: ["@miden-sdk/miden-sdk"],
// Force Vite to pre-bundle the linked local SDK. Linked (file:) deps are not
// optimized by default, so its CommonJS transitive deps (e.g. lodash) would
// lack an ESM default export and fail at runtime. Pre-bundling interops them.
// After rebuilding the SDK, restart the dev server with `--force` to refresh.
include: ["@epoch-protocol/epoch-intents-sdk"],
},
build: {
target: "esnext",
Expand Down