Skip to content

Commit

Permalink
feat: update types, support signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
stephancill committed Oct 23, 2024
1 parent 50567af commit 58369ce
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 72 deletions.
5 changes: 5 additions & 0 deletions .changeset/gentle-beers-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@frames.js/render": patch
---

fix: allow onTransaction/onSignature to be called from contexts outside of frame e.g. miniapp
2 changes: 2 additions & 0 deletions packages/debugger/app/components/action-debugger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ export const ActionDebugger = React.forwardRef<

{!!composeFormActionDialogSignal && (
<ComposerFormActionDialog
connectedAddress={farcasterFrameConfig.connectedAddress}
composerActionForm={composeFormActionDialogSignal.data}
onClose={() => {
composeFormActionDialogSignal.resolve(undefined);
Expand All @@ -448,6 +449,7 @@ export const ActionDebugger = React.forwardRef<
});
}}
onTransaction={farcasterFrameConfig.onTransaction}
onSignature={farcasterFrameConfig.onSignature}
/>
)}
</TabsContent>
Expand Down
183 changes: 139 additions & 44 deletions packages/debugger/app/components/composer-form-action-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,91 @@
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<Abi>(),
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<TypedDataDomain>(),
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({
composerActionForm,
onClose,
onSave,
onTransaction,
onSignature,
connectedAddress,
}: ComposerFormActionDialogProps) {
const onSaveRef = useRef(onSave);
onSaveRef.current = onSave;
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 58369ce

Please sign in to comment.