Skip to content

Commit ac0af6c

Browse files
authored
feat: handle transactions from composer actions in debugger (#513)
* feat: handle transactions from composer actions in debugger * feat: update types, support signatures * chore: changeset * feat: miniapp transaction example * fix: build
1 parent bd4f30f commit ac0af6c

File tree

13 files changed

+480
-36
lines changed

13 files changed

+480
-36
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

.changeset/lazy-deers-laugh.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"template-next-starter-with-examples": patch
3+
"create-frames": patch
4+
---
5+
6+
feat: miniapp transaction example
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@frames.js/debugger": patch
3+
---
4+
5+
feat: composer action transaction support

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

Lines changed: 3 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);
@@ -447,6 +448,8 @@ export const ActionDebugger = React.forwardRef<
447448
composerActionState: composerState,
448449
});
449450
}}
451+
onTransaction={farcasterFrameConfig.onTransaction}
452+
onSignature={farcasterFrameConfig.onSignature}
450453
/>
451454
)}
452455
</TabsContent>

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

Lines changed: 148 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import {
22
Dialog,
3+
DialogContent,
4+
DialogFooter,
35
DialogHeader,
46
DialogTitle,
5-
DialogFooter,
6-
DialogContent,
77
} from "@/components/ui/dialog";
8+
import { OnSignatureFunc, OnTransactionFunc } from "@frames.js/render";
89
import type {
910
ComposerActionFormResponse,
1011
ComposerActionState,
1112
} from "frames.js/types";
12-
import { useEffect, useRef } from "react";
13+
import { useCallback, useEffect, useRef } from "react";
14+
import { Abi, TypedDataDomain } from "viem";
1315
import { z } from "zod";
1416

1517
const composerFormCreateCastMessageSchema = z.object({
@@ -23,38 +25,172 @@ const composerFormCreateCastMessageSchema = z.object({
2325
}),
2426
});
2527

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]),
62+
});
63+
64+
const composerActionMessageSchema = z.discriminatedUnion("type", [
65+
composerFormCreateCastMessageSchema,
66+
z.object({
67+
type: z.literal("requestTransaction"),
68+
data: transactionRequestBodySchema,
69+
}),
70+
]);
71+
2672
type ComposerFormActionDialogProps = {
2773
composerActionForm: ComposerActionFormResponse;
2874
onClose: () => void;
2975
onSave: (arg: { composerState: ComposerActionState }) => void;
76+
onTransaction?: OnTransactionFunc;
77+
onSignature?: OnSignatureFunc;
78+
// TODO: Consider moving this into return value of onTransaction
79+
connectedAddress?: `0x${string}`;
3080
};
3181

3282
export function ComposerFormActionDialog({
3383
composerActionForm,
3484
onClose,
3585
onSave,
86+
onTransaction,
87+
onSignature,
88+
connectedAddress,
3689
}: ComposerFormActionDialogProps) {
3790
const onSaveRef = useRef(onSave);
3891
onSaveRef.current = onSave;
3992

93+
const iframeRef = useRef<HTMLIFrameElement>(null);
94+
95+
const postMessageToIframe = useCallback(
96+
(message: any) => {
97+
if (iframeRef.current && iframeRef.current.contentWindow) {
98+
iframeRef.current.contentWindow.postMessage(
99+
message,
100+
new URL(composerActionForm.url).origin
101+
);
102+
}
103+
},
104+
[composerActionForm.url]
105+
);
106+
40107
useEffect(() => {
41108
const handleMessage = (event: MessageEvent) => {
42-
const result = composerFormCreateCastMessageSchema.safeParse(event.data);
109+
const result = composerActionMessageSchema.safeParse(event.data);
43110

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

51-
if (result.data.data.cast.embeds.length > 2) {
52-
console.warn("Only first 2 embeds are shown in the cast");
53-
}
118+
const message = result.data;
54119

55-
onSaveRef.current({
56-
composerState: result.data.data.cast,
57-
});
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+
}
189+
190+
onSaveRef.current({
191+
composerState: message.data.cast,
192+
});
193+
}
58194
};
59195

60196
window.addEventListener("message", handleMessage);
@@ -80,6 +216,7 @@ export function ComposerFormActionDialog({
80216
<div>
81217
<iframe
82218
className="h-[600px] w-full opacity-100 transition-opacity duration-300"
219+
ref={iframeRef}
83220
src={composerActionForm.url}
84221
sandbox="allow-forms allow-scripts allow-same-origin"
85222
></iframe>

0 commit comments

Comments
 (0)