diff --git a/package.json b/package.json index a609ba0..d5ff015 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f73f90..fb5fe15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@epoch-protocol/epoch-intents-sdk': - specifier: ^1.0.35 - version: 1.0.35(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.48.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2))(zod@4.4.2) + specifier: file:../smallocator/sdk + version: file:../smallocator/sdk(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.48.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2))(zod@4.4.2) '@fontsource-variable/jetbrains-mono': specifier: ^5.2.8 version: 5.2.8 @@ -315,11 +315,11 @@ packages: '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - '@epoch-protocol/epoch-commons-sdk@0.1.18': - resolution: {integrity: sha512-FJ15pOddF5ndbkRUO6ZJr67zHQe1YfVw3sQgtrxzsgsv8mc+MPKaWCkvaTQ9uSvNKJcvwqQmgqn4gcayejJtug==} + '@epoch-protocol/epoch-commons-sdk@0.1.19': + resolution: {integrity: sha512-M1ZSzK5LQyTWFPx4gHLVVOaAFHT23DKKP3c2G3cPsavmnyckxFayoIXIS9UyMTDn1g0QPeoL5R1YmKBmP7HE8Q==} - '@epoch-protocol/epoch-intents-sdk@1.0.35': - resolution: {integrity: sha512-69rFadIgAFKVgYgnZW3zyFKEYbpRUS3GXF7gAUEUqbojJzBWEVwCv/EwAQfd9tNVT6EVWFtJd2m29QZUE3sijQ==} + '@epoch-protocol/epoch-intents-sdk@file:../smallocator/sdk': + resolution: {directory: ../smallocator/sdk, type: directory} peerDependencies: viem: ^2.x @@ -5730,7 +5730,7 @@ snapshots: '@emotion/hash@0.9.2': {} - '@epoch-protocol/epoch-commons-sdk@0.1.18(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2)': + '@epoch-protocol/epoch-commons-sdk@0.1.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2)': dependencies: ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) viem: 2.42.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2) @@ -5740,9 +5740,9 @@ snapshots: - utf-8-validate - zod - '@epoch-protocol/epoch-intents-sdk@1.0.35(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.48.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2))(zod@4.4.2)': + '@epoch-protocol/epoch-intents-sdk@file:../smallocator/sdk(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.48.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2))(zod@4.4.2)': dependencies: - '@epoch-protocol/epoch-commons-sdk': 0.1.18(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2) + '@epoch-protocol/epoch-commons-sdk': 0.1.19(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2) '@metamask/delegation-toolkit': 0.13.0(viem@2.48.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2)) viem: 2.48.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.4.2) transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 1b45529..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Supply-chain hardening (pnpm reads settings from this file as of pnpm 10). -# Wait 7 days before installing a newly published version, so hijacked releases -# have time to be reported and unpublished before they reach this project. -minimumReleaseAge: 10080 -# Refuse transitive deps that resolve outside the registry (git/file/tarball URLs). -blockExoticSubdeps: true -# Reject packages whose trust signals (provenance, signatures) weaken between updates. -trustPolicy: no-downgrade diff --git a/src/hooks/useMidenP2IDNoteFactory.ts b/src/hooks/useMidenP2IDNoteFactory.ts index 3465f89..535b155 100644 --- a/src/hooks/useMidenP2IDNoteFactory.ts +++ b/src/hooks/useMidenP2IDNoteFactory.ts @@ -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>( - 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) { @@ -70,6 +139,14 @@ export function useMidenP2IDNoteFactory({ }; } }, - [midenAccountId, requestSend, waitForTransaction, onStatus, onNoteCreated], + [ + midenAccountId, + requestTransaction, + waitForTransaction, + client, + isReady, + onStatus, + onNoteCreated, + ], ); } diff --git a/src/main.tsx b/src/main.tsx index 44d8abf..2666aa6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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, @@ -34,7 +35,12 @@ createRoot(document.getElementById("root")!).render( appName="Miden Integration Example" allowedPrivateData={AllowedPrivateData.Assets} > - + {/* MidenProvider powers the @miden-sdk/react hooks (useSend / + useSyncState) that the P2IDE note factory uses to write the F-01 + recipient-binding attachment. */} + + + diff --git a/src/services/epoch-bridge.ts b/src/services/epoch-bridge.ts index 2019ddd..193c4b1 100644 --- a/src/services/epoch-bridge.ts +++ b/src/services/epoch-bridge.ts @@ -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 { diff --git a/vite.config.ts b/vite.config.ts index b79e2ee..67910c2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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",