Skip to content

Commit 58369ce

Browse files
committed
feat: update types, support signatures
1 parent 50567af commit 58369ce

File tree

7 files changed

+247
-72
lines changed

7 files changed

+247
-72
lines changed

.changeset/gentle-beers-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@frames.js/render": patch
3+
---
4+
5+
fix: allow onTransaction/onSignature to be called from contexts outside of frame e.g. miniapp

packages/debugger/app/components/action-debugger.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ export const ActionDebugger = React.forwardRef<
438438

439439
{!!composeFormActionDialogSignal && (
440440
<ComposerFormActionDialog
441+
connectedAddress={farcasterFrameConfig.connectedAddress}
441442
composerActionForm={composeFormActionDialogSignal.data}
442443
onClose={() => {
443444
composeFormActionDialogSignal.resolve(undefined);
@@ -448,6 +449,7 @@ export const ActionDebugger = React.forwardRef<
448449
});
449450
}}
450451
onTransaction={farcasterFrameConfig.onTransaction}
452+
onSignature={farcasterFrameConfig.onSignature}
451453
/>
452454
)}
453455
</TabsContent>

packages/debugger/app/components/composer-form-action-dialog.tsx

Lines changed: 139 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,91 @@
11
import {
22
Dialog,
3+
DialogContent,
4+
DialogFooter,
35
DialogHeader,
46
DialogTitle,
5-
DialogFooter,
6-
DialogContent,
77
} from "@/components/ui/dialog";
8-
import { OnTransactionFunc } from "@frames.js/render";
8+
import { OnSignatureFunc, OnTransactionFunc } from "@frames.js/render";
99
import type {
1010
ComposerActionFormResponse,
1111
ComposerActionState,
1212
} from "frames.js/types";
1313
import { useCallback, useEffect, useRef } from "react";
14+
import { Abi, TypedDataDomain } from "viem";
1415
import { z } from "zod";
1516

16-
const miniappMessageSchema = z.object({
17-
type: z.string(),
18-
data: z.record(z.unknown()),
17+
const composerFormCreateCastMessageSchema = z.object({
18+
type: z.literal("createCast"),
19+
data: z.object({
20+
cast: z.object({
21+
parent: z.string().optional(),
22+
text: z.string(),
23+
embeds: z.array(z.string().min(1).url()).min(1),
24+
}),
25+
}),
26+
});
27+
28+
const ethSendTransactionActionSchema = z.object({
29+
chainId: z.string(),
30+
method: z.literal("eth_sendTransaction"),
31+
attribution: z.boolean().optional(),
32+
params: z.object({
33+
abi: z.custom<Abi>(),
34+
to: z.custom<`0x${string}`>(
35+
(val): val is `0x${string}` =>
36+
typeof val === "string" && val.startsWith("0x")
37+
),
38+
value: z.string().optional(),
39+
data: z
40+
.custom<`0x${string}`>(
41+
(val): val is `0x${string}` =>
42+
typeof val === "string" && val.startsWith("0x")
43+
)
44+
.optional(),
45+
}),
46+
});
47+
48+
const ethSignTypedDataV4ActionSchema = z.object({
49+
chainId: z.string(),
50+
method: z.literal("eth_signTypedData_v4"),
51+
params: z.object({
52+
domain: z.custom<TypedDataDomain>(),
53+
types: z.unknown(),
54+
primaryType: z.string(),
55+
message: z.record(z.unknown()),
56+
}),
57+
});
58+
59+
const transactionRequestBodySchema = z.object({
60+
requestId: z.string(),
61+
tx: z.union([ethSendTransactionActionSchema, ethSignTypedDataV4ActionSchema]),
1962
});
2063

64+
const composerActionMessageSchema = z.discriminatedUnion("type", [
65+
composerFormCreateCastMessageSchema,
66+
z.object({
67+
type: z.literal("requestTransaction"),
68+
data: transactionRequestBodySchema,
69+
}),
70+
]);
71+
2172
type ComposerFormActionDialogProps = {
2273
composerActionForm: ComposerActionFormResponse;
2374
onClose: () => void;
2475
onSave: (arg: { composerState: ComposerActionState }) => void;
2576
onTransaction?: OnTransactionFunc;
77+
onSignature?: OnSignatureFunc;
78+
// TODO: Consider moving this into return value of onTransaction
79+
connectedAddress?: `0x${string}`;
2680
};
2781

2882
export function ComposerFormActionDialog({
2983
composerActionForm,
3084
onClose,
3185
onSave,
3286
onTransaction,
87+
onSignature,
88+
connectedAddress,
3389
}: ComposerFormActionDialogProps) {
3490
const onSaveRef = useRef(onSave);
3591
onSaveRef.current = onSave;
@@ -50,52 +106,91 @@ export function ComposerFormActionDialog({
50106

51107
useEffect(() => {
52108
const handleMessage = (event: MessageEvent) => {
53-
const result = miniappMessageSchema.parse(event.data);
54-
55-
if (result?.type === "requestTransaction") {
56-
onTransaction?.({
57-
transactionData: result.data.tx as any,
58-
}).then((txHash) => {
59-
if (txHash) {
60-
postMessageToIframe({
61-
type: "transactionResponse",
62-
data: {
63-
requestId: result.data.requestId,
64-
success: true,
65-
receipt: {
66-
// address: farcasterFrameConfig.connectedAddress,
67-
transactionId: txHash,
68-
},
69-
},
70-
});
71-
} else {
72-
postMessageToIframe({
73-
type: "transactionResponse",
74-
data: {
75-
requestId: result.data.requestId,
76-
success: false,
77-
message: "User rejected the request",
78-
},
79-
});
80-
}
81-
});
82-
return;
83-
}
109+
const result = composerActionMessageSchema.safeParse(event.data);
84110

85111
// on error is not called here because there can be different messages that don't have anything to do with composer form actions
86112
// instead we are just waiting for the correct message
87113
if (!result.success) {
88-
console.warn("Invalid message received", event.data);
114+
console.warn("Invalid message received", event.data, result.error);
89115
return;
90116
}
91117

92-
if (result.data.data.cast.embeds.length > 2) {
93-
console.warn("Only first 2 embeds are shown in the cast");
94-
}
118+
const message = result.data;
119+
120+
if (message.type === "requestTransaction") {
121+
if (message.data.tx.method === "eth_sendTransaction") {
122+
onTransaction?.({
123+
transactionData: message.data.tx,
124+
}).then((txHash) => {
125+
if (txHash) {
126+
postMessageToIframe({
127+
type: "transactionResponse",
128+
data: {
129+
requestId: message.data.requestId,
130+
success: true,
131+
receipt: {
132+
address: connectedAddress,
133+
transactionId: txHash,
134+
},
135+
},
136+
});
137+
} else {
138+
postMessageToIframe({
139+
type: "transactionResponse",
140+
data: {
141+
requestId: message.data.requestId,
142+
success: false,
143+
message: "User rejected the request",
144+
},
145+
});
146+
}
147+
});
148+
} else if (message.data.tx.method === "eth_signTypedData_v4") {
149+
onSignature?.({
150+
signatureData: {
151+
chainId: message.data.tx.chainId,
152+
method: "eth_signTypedData_v4",
153+
params: {
154+
domain: message.data.tx.params.domain,
155+
types: message.data.tx.params.types as any,
156+
primaryType: message.data.tx.params.primaryType,
157+
message: message.data.tx.params.message,
158+
},
159+
},
160+
}).then((signature) => {
161+
if (signature) {
162+
postMessageToIframe({
163+
type: "signatureResponse",
164+
data: {
165+
requestId: message.data.requestId,
166+
success: true,
167+
receipt: {
168+
address: connectedAddress,
169+
transactionId: signature,
170+
},
171+
},
172+
});
173+
} else {
174+
postMessageToIframe({
175+
type: "signatureResponse",
176+
data: {
177+
requestId: message.data.requestId,
178+
success: false,
179+
message: "User rejected the request",
180+
},
181+
});
182+
}
183+
});
184+
}
185+
} else if (message.type === "createCast") {
186+
if (message.data.cast.embeds.length > 2) {
187+
console.warn("Only first 2 embeds are shown in the cast");
188+
}
95189

96-
onSaveRef.current({
97-
composerState: result.data.data.cast,
98-
});
190+
onSaveRef.current({
191+
composerState: message.data.cast,
192+
});
193+
}
99194
};
100195

101196
window.addEventListener("message", handleMessage);

0 commit comments

Comments
 (0)