From a342492fff0b95fa886dd6f2dfd4388d38add406 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:26:53 +0200 Subject: [PATCH] fix: update miniapp transactions spec implementation --- .changeset/real-crabs-heal.md | 6 + .../composer-form-action-dialog.tsx | 113 +++++++++++------- packages/debugger/app/frames/route.ts | 43 ++++--- .../transaction-miniapp/miniapp/page.tsx | 27 ++--- 4 files changed, 111 insertions(+), 78 deletions(-) create mode 100644 .changeset/real-crabs-heal.md diff --git a/.changeset/real-crabs-heal.md b/.changeset/real-crabs-heal.md new file mode 100644 index 000000000..32754d47d --- /dev/null +++ b/.changeset/real-crabs-heal.md @@ -0,0 +1,6 @@ +--- +"template-next-starter-with-examples": patch +"@frames.js/debugger": patch +--- + +fix: update miniapp transactions spec implementation diff --git a/packages/debugger/app/components/composer-form-action-dialog.tsx b/packages/debugger/app/components/composer-form-action-dialog.tsx index bc9692d41..abea404db 100644 --- a/packages/debugger/app/components/composer-form-action-dialog.tsx +++ b/packages/debugger/app/components/composer-form-action-dialog.tsx @@ -14,7 +14,7 @@ import { useCallback, useEffect, useRef } from "react"; import { Abi, TypedDataDomain } from "viem"; import { z } from "zod"; -const composerFormCreateCastMessageSchema = z.object({ +const createCastRequestSchemaLegacy = z.object({ type: z.literal("createCast"), data: z.object({ cast: z.object({ @@ -56,17 +56,35 @@ const ethSignTypedDataV4ActionSchema = z.object({ }), }); -const transactionRequestBodySchema = z.object({ - requestId: z.string(), - tx: z.union([ethSendTransactionActionSchema, ethSignTypedDataV4ActionSchema]), +const walletActionRequestSchema = z.object({ + jsonrpc: z.literal("2.0"), + id: z.string(), + method: z.literal("fc_requestWalletAction"), + params: z.object({ + action: z.union([ + ethSendTransactionActionSchema, + ethSignTypedDataV4ActionSchema, + ]), + }), }); -const composerActionMessageSchema = z.discriminatedUnion("type", [ - composerFormCreateCastMessageSchema, - z.object({ - type: z.literal("requestTransaction"), - data: transactionRequestBodySchema, +const createCastRequestSchema = z.object({ + jsonrpc: z.literal("2.0"), + id: z.union([z.string(), z.number(), z.null()]), + method: z.literal("fc_createCast"), + params: z.object({ + cast: z.object({ + parent: z.string().optional(), + text: z.string(), + embeds: z.array(z.string().min(1).url()).min(1), + }), }), +}); + +const composerActionMessageSchema = z.union([ + createCastRequestSchemaLegacy, + walletActionRequestSchema, + createCastRequestSchema, ]); type ComposerFormActionDialogProps = { @@ -106,6 +124,10 @@ export function ComposerFormActionDialog({ useEffect(() => { const handleMessage = (event: MessageEvent) => { + if (event.origin !== new URL(composerActionForm.url).origin) { + return; + } + const result = composerActionMessageSchema.safeParse(event.data); // on error is not called here because there can be different messages that don't have anything to do with composer form actions @@ -117,78 +139,77 @@ export function ComposerFormActionDialog({ const message = result.data; - if (message.type === "requestTransaction") { - if (message.data.tx.method === "eth_sendTransaction") { + if ("type" in message) { + // Handle legacy messages + onSaveRef.current({ + composerState: message.data.cast, + }); + } else if (message.method === "fc_requestWalletAction") { + if (message.params.action.method === "eth_sendTransaction") { onTransaction?.({ - transactionData: message.data.tx, + transactionData: message.params.action, }).then((txHash) => { if (txHash) { postMessageToIframe({ - type: "transactionResponse", - data: { - requestId: message.data.requestId, - success: true, - receipt: { - address: connectedAddress, - transactionId: txHash, - }, + jsonrpc: "2.0", + id: message.id, + result: { + address: connectedAddress, + transactionId: txHash, }, }); } else { postMessageToIframe({ - type: "transactionResponse", - data: { - requestId: message.data.requestId, - success: false, + jsonrpc: "2.0", + id: message.id, + error: { + code: -32000, message: "User rejected the request", }, }); } }); - } else if (message.data.tx.method === "eth_signTypedData_v4") { + } else if (message.params.action.method === "eth_signTypedData_v4") { onSignature?.({ signatureData: { - chainId: message.data.tx.chainId, - method: "eth_signTypedData_v4", + chainId: message.params.action.chainId, + method: message.params.action.method, params: { - domain: message.data.tx.params.domain, - types: message.data.tx.params.types as any, - primaryType: message.data.tx.params.primaryType, - message: message.data.tx.params.message, + domain: message.params.action.params.domain, + types: message.params.action.params.types as any, + primaryType: message.params.action.params.primaryType, + message: message.params.action.params.message, }, }, }).then((signature) => { if (signature) { postMessageToIframe({ - type: "signatureResponse", - data: { - requestId: message.data.requestId, - success: true, - receipt: { - address: connectedAddress, - transactionId: signature, - }, + jsonrpc: "2.0", + id: message.id, + result: { + address: connectedAddress, + transactionId: signature, }, }); } else { postMessageToIframe({ - type: "signatureResponse", - data: { - requestId: message.data.requestId, - success: false, + jsonrpc: "2.0", + id: message.id, + error: { + code: -32000, message: "User rejected the request", }, }); } }); } - } else if (message.type === "createCast") { - if (message.data.cast.embeds.length > 2) { + } else if (message.method === "fc_createCast") { + if (message.params.cast.embeds.length > 2) { console.warn("Only first 2 embeds are shown in the cast"); } onSaveRef.current({ - composerState: message.data.cast, + composerState: message.params.cast, }); } }; diff --git a/packages/debugger/app/frames/route.ts b/packages/debugger/app/frames/route.ts index 6d66a40fd..e7c4b9cce 100644 --- a/packages/debugger/app/frames/route.ts +++ b/packages/debugger/app/frames/route.ts @@ -23,23 +23,16 @@ const composerActionFormParser = z.object({ title: z.string().min(1), }); -const jsonResponseParser = z.preprocess( - (data) => { - if (typeof data === "object" && data !== null && !("type" in data)) { - return { - type: "message", - ...data, - }; - } +const jsonResponseParser = z.preprocess((data) => { + if (typeof data === "object" && data !== null && !("type" in data)) { + return { + type: "message", + ...data, + }; + } - return data; - }, - z.discriminatedUnion("type", [ - castActionFrameParser, - castActionMessageParser, - composerActionFormParser, - ]) -); + return data; +}, z.discriminatedUnion("type", [castActionFrameParser, castActionMessageParser, composerActionFormParser])); const errorResponseParser = z.object({ message: z.string().min(1), @@ -184,7 +177,14 @@ export async function POST(req: NextRequest): Promise { ); } - return r.clone(); + const headers = new Headers(r.headers); + // Proxied requests could have content-encoding set, which breaks the response + headers.delete("content-encoding"); + return new Response(r.body, { + headers, + status: r.status, + statusText: r.statusText, + }); } if (isPostRedirect && r.status !== 302) { @@ -211,7 +211,14 @@ export async function POST(req: NextRequest): Promise { throw new Error("Invalid frame response"); } - return r.clone(); + const headers = new Headers(r.headers); + // Proxied requests could have content-encoding set, which breaks the response + headers.delete("content-encoding"); + return new Response(r.body, { + headers, + status: r.status, + statusText: r.statusText, + }); } const htmlString = await r.text(); diff --git a/templates/next-starter-with-examples/app/examples/transaction-miniapp/miniapp/page.tsx b/templates/next-starter-with-examples/app/examples/transaction-miniapp/miniapp/page.tsx index 16eec5e72..9f81895d8 100644 --- a/templates/next-starter-with-examples/app/examples/transaction-miniapp/miniapp/page.tsx +++ b/templates/next-starter-with-examples/app/examples/transaction-miniapp/miniapp/page.tsx @@ -44,10 +44,11 @@ export default function MiniappPage({ // Handle form submission here windowObject?.parent.postMessage( { - type: "requestTransaction", - data: { - requestId: uuidv4(), - tx: { + jsonrpc: "2.0", + id: uuidv4(), + method: "fc_requestWalletAction", + params: { + action: { chainId: "eip155:10", method: "eth_sendTransaction", params: { @@ -67,10 +68,11 @@ export default function MiniappPage({ const handleRequestSignature = useCallback(() => { windowObject?.parent.postMessage( { - type: "requestTransaction", - data: { - requestId: uuidv4(), - tx: { + jsonrpc: "2.0", + id: uuidv4(), + method: "fc_requestWalletAction", + params: { + action: { chainId: "eip155:10", // OP Mainnet 10 method: "eth_signTypedData_v4", params: { @@ -152,13 +154,10 @@ export default function MiniappPage({ Request Signature - {message?.data?.success ? ( -
- Transaction sent successfully: {message?.data?.receipt?.transactionId}{" "} - sent from {message?.data?.receipt?.address} -
+ {message?.result ? ( +
{JSON.stringify(message?.result, null, 2)}
) : ( -
{message?.data?.message}
+
{message?.result?.message}
)} );