From 58369cec7cbe629f93941b1ae7d1946cb51fc512 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:40:33 +0200 Subject: [PATCH] feat: update types, support signatures --- .changeset/gentle-beers-jog.md | 5 + .../app/components/action-debugger.tsx | 2 + .../composer-form-action-dialog.tsx | 183 +++++++++++++----- packages/render/src/types.ts | 50 ++--- .../transaction-miniapp/miniapp/page.tsx | 63 +++++- .../next-starter-with-examples/package.json | 6 +- yarn.lock | 10 + 7 files changed, 247 insertions(+), 72 deletions(-) create mode 100644 .changeset/gentle-beers-jog.md diff --git a/.changeset/gentle-beers-jog.md b/.changeset/gentle-beers-jog.md new file mode 100644 index 000000000..6ffda0ec4 --- /dev/null +++ b/.changeset/gentle-beers-jog.md @@ -0,0 +1,5 @@ +--- +"@frames.js/render": patch +--- + +fix: allow onTransaction/onSignature to be called from contexts outside of frame e.g. miniapp diff --git a/packages/debugger/app/components/action-debugger.tsx b/packages/debugger/app/components/action-debugger.tsx index 360bab76a..c6835584b 100644 --- a/packages/debugger/app/components/action-debugger.tsx +++ b/packages/debugger/app/components/action-debugger.tsx @@ -438,6 +438,7 @@ export const ActionDebugger = React.forwardRef< {!!composeFormActionDialogSignal && ( { composeFormActionDialogSignal.resolve(undefined); @@ -448,6 +449,7 @@ export const ActionDebugger = React.forwardRef< }); }} onTransaction={farcasterFrameConfig.onTransaction} + onSignature={farcasterFrameConfig.onSignature} /> )} diff --git a/packages/debugger/app/components/composer-form-action-dialog.tsx b/packages/debugger/app/components/composer-form-action-dialog.tsx index a0b98013d..bc9692d41 100644 --- a/packages/debugger/app/components/composer-form-action-dialog.tsx +++ b/packages/debugger/app/components/composer-form-action-dialog.tsx @@ -1,28 +1,82 @@ import { Dialog, + DialogContent, + DialogFooter, DialogHeader, DialogTitle, - DialogFooter, - DialogContent, } from "@/components/ui/dialog"; -import { OnTransactionFunc } from "@frames.js/render"; +import { OnSignatureFunc, OnTransactionFunc } from "@frames.js/render"; import type { ComposerActionFormResponse, ComposerActionState, } from "frames.js/types"; import { useCallback, useEffect, useRef } from "react"; +import { Abi, TypedDataDomain } from "viem"; import { z } from "zod"; -const miniappMessageSchema = z.object({ - type: z.string(), - data: z.record(z.unknown()), +const composerFormCreateCastMessageSchema = z.object({ + type: z.literal("createCast"), + data: z.object({ + cast: z.object({ + parent: z.string().optional(), + text: z.string(), + embeds: z.array(z.string().min(1).url()).min(1), + }), + }), +}); + +const ethSendTransactionActionSchema = z.object({ + chainId: z.string(), + method: z.literal("eth_sendTransaction"), + attribution: z.boolean().optional(), + params: z.object({ + abi: z.custom(), + to: z.custom<`0x${string}`>( + (val): val is `0x${string}` => + typeof val === "string" && val.startsWith("0x") + ), + value: z.string().optional(), + data: z + .custom<`0x${string}`>( + (val): val is `0x${string}` => + typeof val === "string" && val.startsWith("0x") + ) + .optional(), + }), +}); + +const ethSignTypedDataV4ActionSchema = z.object({ + chainId: z.string(), + method: z.literal("eth_signTypedData_v4"), + params: z.object({ + domain: z.custom(), + types: z.unknown(), + primaryType: z.string(), + message: z.record(z.unknown()), + }), +}); + +const transactionRequestBodySchema = z.object({ + requestId: z.string(), + tx: z.union([ethSendTransactionActionSchema, ethSignTypedDataV4ActionSchema]), }); +const composerActionMessageSchema = z.discriminatedUnion("type", [ + composerFormCreateCastMessageSchema, + z.object({ + type: z.literal("requestTransaction"), + data: transactionRequestBodySchema, + }), +]); + type ComposerFormActionDialogProps = { composerActionForm: ComposerActionFormResponse; onClose: () => void; onSave: (arg: { composerState: ComposerActionState }) => void; onTransaction?: OnTransactionFunc; + onSignature?: OnSignatureFunc; + // TODO: Consider moving this into return value of onTransaction + connectedAddress?: `0x${string}`; }; export function ComposerFormActionDialog({ @@ -30,6 +84,8 @@ export function ComposerFormActionDialog({ onClose, onSave, onTransaction, + onSignature, + connectedAddress, }: ComposerFormActionDialogProps) { const onSaveRef = useRef(onSave); onSaveRef.current = onSave; @@ -50,52 +106,91 @@ export function ComposerFormActionDialog({ useEffect(() => { const handleMessage = (event: MessageEvent) => { - const result = miniappMessageSchema.parse(event.data); - - if (result?.type === "requestTransaction") { - onTransaction?.({ - transactionData: result.data.tx as any, - }).then((txHash) => { - if (txHash) { - postMessageToIframe({ - type: "transactionResponse", - data: { - requestId: result.data.requestId, - success: true, - receipt: { - // address: farcasterFrameConfig.connectedAddress, - transactionId: txHash, - }, - }, - }); - } else { - postMessageToIframe({ - type: "transactionResponse", - data: { - requestId: result.data.requestId, - success: false, - message: "User rejected the request", - }, - }); - } - }); - 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 // instead we are just waiting for the correct message if (!result.success) { - console.warn("Invalid message received", event.data); + console.warn("Invalid message received", event.data, result.error); return; } - if (result.data.data.cast.embeds.length > 2) { - console.warn("Only first 2 embeds are shown in the cast"); - } + const message = result.data; + + if (message.type === "requestTransaction") { + if (message.data.tx.method === "eth_sendTransaction") { + onTransaction?.({ + transactionData: message.data.tx, + }).then((txHash) => { + if (txHash) { + postMessageToIframe({ + type: "transactionResponse", + data: { + requestId: message.data.requestId, + success: true, + receipt: { + address: connectedAddress, + transactionId: txHash, + }, + }, + }); + } else { + postMessageToIframe({ + type: "transactionResponse", + data: { + requestId: message.data.requestId, + success: false, + message: "User rejected the request", + }, + }); + } + }); + } else if (message.data.tx.method === "eth_signTypedData_v4") { + onSignature?.({ + signatureData: { + chainId: message.data.tx.chainId, + method: "eth_signTypedData_v4", + 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, + }, + }, + }).then((signature) => { + if (signature) { + postMessageToIframe({ + type: "signatureResponse", + data: { + requestId: message.data.requestId, + success: true, + receipt: { + address: connectedAddress, + transactionId: signature, + }, + }, + }); + } else { + postMessageToIframe({ + type: "signatureResponse", + data: { + requestId: message.data.requestId, + success: false, + message: "User rejected the request", + }, + }); + } + }); + } + } else if (message.type === "createCast") { + if (message.data.cast.embeds.length > 2) { + console.warn("Only first 2 embeds are shown in the cast"); + } - onSaveRef.current({ - composerState: result.data.data.cast, - }); + onSaveRef.current({ + composerState: message.data.cast, + }); + } }; window.addEventListener("message", handleMessage); diff --git a/packages/render/src/types.ts b/packages/render/src/types.ts index 16b39b520..7ad0310b6 100644 --- a/packages/render/src/types.ts +++ b/packages/render/src/types.ts @@ -22,8 +22,10 @@ import type { FrameStackAPI } from "./use-frame-stack"; export type OnTransactionArgs = { transactionData: TransactionTargetResponseSendTransaction; - frameButton: FrameButton; - frame: Frame; + /** If the transaction was triggered by a frame button, this will be the frame that it was from */ + frame?: Frame; + /** If the transaction was triggered by a frame button, this will be the frame button that triggered it */ + frameButton?: FrameButton; }; export type OnTransactionFunc = ( @@ -32,8 +34,10 @@ export type OnTransactionFunc = ( export type OnSignatureArgs = { signatureData: TransactionTargetResponseSignTypedDataV4; - frameButton: FrameButton; - frame: Frame; + /** If the signature was triggered by a frame button, this will be the frame that it was from */ + frame?: Frame; + /** If the signature was triggered by a frame button, this will be the frame button that triggered it */ + frameButton?: FrameButton; }; export type OnSignatureFunc = ( @@ -74,7 +78,7 @@ export type OnConnectWalletFunc = () => void; export type SignFrameActionFunc< TSignerStorageType = Record, TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = ( actionContext: SignerStateActionContext ) => Promise>; @@ -84,7 +88,7 @@ export type UseFetchFrameSignFrameActionFunction< unknown, Record >, - TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload > = (arg: { actionContext: TSignerStateActionContext; /** @@ -96,7 +100,7 @@ export type UseFetchFrameSignFrameActionFunction< export type UseFetchFrameOptions< TSignerStorageType = Record, TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { stackAPI: FrameStackAPI; stackDispatch: React.Dispatch; @@ -212,7 +216,7 @@ export type UseFetchFrameOptions< export type UseFrameOptions< TSignerStorageType = Record, TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { /** skip frame signing, for frames that don't verify signatures */ dangerousSkipSigning?: boolean; @@ -286,7 +290,7 @@ export type UseFrameOptions< type SignerStateActionSharedContext< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { target?: string; frameButton: FrameButton; @@ -303,14 +307,14 @@ type SignerStateActionSharedContext< export type SignerStateDefaultActionContext< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { type?: "default"; } & SignerStateActionSharedContext; export type SignerStateTransactionDataActionContext< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { type: "tx-data"; /** Wallet address used to create the transaction, available only for "tx" button actions */ @@ -319,7 +323,7 @@ export type SignerStateTransactionDataActionContext< export type SignerStateTransactionPostActionContext< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { type: "tx-post"; /** Wallet address used to create the transaction, available only for "tx" button actions */ @@ -329,7 +333,7 @@ export type SignerStateTransactionPostActionContext< export type SignerStateActionContext< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = | SignerStateDefaultActionContext | SignerStateTransactionDataActionContext< @@ -342,7 +346,7 @@ export type SignerStateActionContext< >; export type SignedFrameAction< - TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload > = { body: TFrameActionBodyType; searchParams: URLSearchParams; @@ -353,7 +357,7 @@ export type SignFrameActionFunction< unknown, Record > = SignerStateActionContext, - TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload > = ( actionContext: TSignerStateActionContext ) => Promise>; @@ -361,7 +365,7 @@ export type SignFrameActionFunction< export interface SignerStateInstance< TSignerStorageType = Record, TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > { signer: TSignerStorageType | null; /** @@ -388,7 +392,7 @@ export type FramePOSTRequest< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - > = SignerStateActionContext, + > = SignerStateActionContext > = | { method: "POST"; @@ -414,7 +418,7 @@ export type FrameRequest< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - > = SignerStateActionContext, + > = SignerStateActionContext > = FrameGETRequest | FramePOSTRequest; export type FrameStackBase = { @@ -527,7 +531,7 @@ type ButtonPressFunction< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - >, + > > = ( frame: Frame, frameButton: FrameButton, @@ -570,7 +574,7 @@ export type CastActionRequest< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - > = SignerStateActionContext, + > = SignerStateActionContext > = Omit< FramePOSTRequest, "method" | "frameButton" | "sourceFrame" | "signerStateActionContext" @@ -589,7 +593,7 @@ export type ComposerActionRequest< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - > = SignerStateActionContext, + > = SignerStateActionContext > = Omit< FramePOSTRequest, "method" | "frameButton" | "sourceFrame" | "signerStateActionContext" @@ -609,7 +613,7 @@ export type FetchFrameFunction< TSignerStateActionContext extends SignerStateActionContext< unknown, Record - > = SignerStateActionContext, + > = SignerStateActionContext > = ( request: | FrameRequest @@ -625,7 +629,7 @@ export type FetchFrameFunction< export type FrameState< TSignerStorageType = Record, - TFrameContextType extends FrameContext = FarcasterFrameContext, + TFrameContextType extends FrameContext = FarcasterFrameContext > = { fetchFrame: FetchFrameFunction< SignerStateActionContext 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 8022753ff..5d2598e57 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 @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; // pass state from frame message export default function MiniappPage({ @@ -43,7 +44,7 @@ export default function MiniappPage({ { type: "requestTransaction", data: { - requestId: "01ef6570-5a51-48fa-910c-f419400a6d0d", + requestId: uuidv4(), tx: { chainId: "eip155:10", method: "eth_sendTransaction", @@ -61,8 +62,52 @@ export default function MiniappPage({ [window?.parent] ); + const handleRequestSignature = useCallback(() => { + window.parent.postMessage( + { + type: "requestTransaction", + data: { + requestId: uuidv4(), + tx: { + chainId: "eip155:10", // OP Mainnet 10 + method: "eth_signTypedData_v4", + params: { + domain: { + chainId: 10, + }, + types: { + Person: [ + { name: "name", type: "string" }, + { name: "wallet", type: "address" }, + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person" }, + { name: "contents", type: "string" }, + ], + }, + primaryType: "Mail", + message: { + from: { + name: "Cow", + wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + }, + to: { + name: "Bob", + wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + }, + contents: "Hello, Bob!", + }, + }, + }, + }, + }, + "*" + ); + }, [window?.parent]); + return ( -
+