@farcaster/snap 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react/catalog-renderer.d.ts +5 -5
- package/dist/react/catalog-renderer.js +16 -4
- package/dist/react/components/action-button.js +23 -5
- package/dist/react/index.d.ts +2 -1
- package/dist/react/snap-view-core.js +90 -25
- package/dist/react/v1/snap-view.js +1 -1
- package/dist/react/v2/snap-view.js +1 -1
- package/dist/react-native/components/snap-action-button.js +6 -1
- package/dist/react-native/snap-view-core.js +77 -24
- package/dist/react-native/types.d.ts +2 -1
- package/dist/render-state.d.ts +9 -0
- package/dist/render-state.js +27 -0
- package/dist/schemas.d.ts +123 -3
- package/dist/schemas.js +53 -2
- package/dist/server/parseRequest.js +19 -3
- package/dist/ui/button.d.ts +1 -0
- package/dist/ui/button.js +1 -0
- package/dist/ui/catalog.d.ts +13 -0
- package/dist/ui/catalog.js +15 -8
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/react/catalog-renderer.tsx +57 -3
- package/src/react/components/action-button.tsx +32 -3
- package/src/react/index.tsx +4 -1
- package/src/react/snap-view-core.tsx +144 -27
- package/src/react/v1/snap-view.tsx +1 -0
- package/src/react/v2/snap-view.tsx +1 -0
- package/src/react-native/components/snap-action-button.tsx +6 -1
- package/src/react-native/snap-view-core.tsx +114 -27
- package/src/react-native/types.ts +4 -1
- package/src/render-state.ts +46 -0
- package/src/schemas.ts +73 -2
- package/src/server/parseRequest.ts +37 -6
- package/src/ui/button.ts +1 -0
- package/src/ui/catalog.ts +16 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Spec } from "@json-render/core";
|
|
2
|
+
import { createStateStore } from "@json-render/react-native";
|
|
2
3
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
4
|
import { SnapCatalogView } from "./catalog-renderer";
|
|
4
5
|
import { ConfettiOverlay } from "./confetti-overlay";
|
|
@@ -21,10 +22,11 @@ import {
|
|
|
21
22
|
type PaletteColor,
|
|
22
23
|
} from "@farcaster/snap";
|
|
23
24
|
import {
|
|
24
|
-
|
|
25
|
+
buildActionActivityStateChanges,
|
|
25
26
|
buildInitialRenderState,
|
|
26
27
|
cloneSnapRenderState,
|
|
27
28
|
getUnpresentedSnapEffects,
|
|
29
|
+
hasPendingSnapAction,
|
|
28
30
|
markSnapEffectsPresented,
|
|
29
31
|
type SnapRenderState,
|
|
30
32
|
} from "../render-state";
|
|
@@ -40,6 +42,12 @@ function optionalString(value: unknown): string | undefined {
|
|
|
40
42
|
return value ? String(value) : undefined;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
46
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
47
|
+
? (value as Record<string, unknown>)
|
|
48
|
+
: undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
function withDefaultElementProps(spec: Spec): Spec {
|
|
44
52
|
if (!spec || typeof spec !== "object" || !("elements" in spec)) return spec;
|
|
45
53
|
const elements = spec.elements as unknown as Record<
|
|
@@ -114,12 +122,34 @@ export function SnapViewCoreInner({
|
|
|
114
122
|
[initialRenderState, spec.state, snap.theme?.accent],
|
|
115
123
|
);
|
|
116
124
|
|
|
125
|
+
const stateStore = useMemo(() => createStateStore(initialState), [
|
|
126
|
+
initialState,
|
|
127
|
+
]);
|
|
117
128
|
const stateRef = useRef<Record<string, unknown>>(initialState);
|
|
129
|
+
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
130
|
+
const pendingActionCountRef = useRef(0);
|
|
131
|
+
const [hasPendingAction, setHasPendingAction] = useState(false);
|
|
132
|
+
const [actionActivityVersion, setActionActivityVersion] = useState(0);
|
|
118
133
|
|
|
119
134
|
useEffect(() => {
|
|
120
135
|
stateRef.current = cloneSnapRenderState(initialState);
|
|
121
136
|
}, [initialState]);
|
|
122
137
|
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
onRenderStateChangeRef.current = onRenderStateChange;
|
|
140
|
+
}, [onRenderStateChange]);
|
|
141
|
+
|
|
142
|
+
useEffect(
|
|
143
|
+
() =>
|
|
144
|
+
stateStore.subscribe(() => {
|
|
145
|
+
const snapshot = cloneSnapRenderState(stateStore.getSnapshot());
|
|
146
|
+
stateRef.current = snapshot;
|
|
147
|
+
setHasPendingAction(hasPendingSnapAction(snapshot));
|
|
148
|
+
onRenderStateChangeRef.current?.(snapshot);
|
|
149
|
+
}),
|
|
150
|
+
[stateStore],
|
|
151
|
+
);
|
|
152
|
+
|
|
123
153
|
useEffect(() => {
|
|
124
154
|
const catalogResult = snapJsonRenderCatalog.validate(spec);
|
|
125
155
|
if (!catalogResult.success) {
|
|
@@ -144,10 +174,6 @@ export function SnapViewCoreInner({
|
|
|
144
174
|
confetti: 0,
|
|
145
175
|
fireworks: 0,
|
|
146
176
|
});
|
|
147
|
-
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
onRenderStateChangeRef.current = onRenderStateChange;
|
|
150
|
-
}, [onRenderStateChange]);
|
|
151
177
|
useEffect(() => {
|
|
152
178
|
const effectsToPresent = getUnpresentedSnapEffects(
|
|
153
179
|
stateRef.current,
|
|
@@ -169,7 +195,10 @@ export function SnapViewCoreInner({
|
|
|
169
195
|
}
|
|
170
196
|
|
|
171
197
|
if (markSnapEffectsPresented(stateRef.current, effectsToPresent)) {
|
|
172
|
-
|
|
198
|
+
const meta = recordValue(stateRef.current.__snapRender);
|
|
199
|
+
stateStore.update({
|
|
200
|
+
"/__snapRender/presentedEffects": meta?.presentedEffects ?? [],
|
|
201
|
+
});
|
|
173
202
|
}
|
|
174
203
|
|
|
175
204
|
setEffectRunKeys((current) => ({
|
|
@@ -184,49 +213,92 @@ export function SnapViewCoreInner({
|
|
|
184
213
|
? current.fireworks
|
|
185
214
|
: 0,
|
|
186
215
|
}));
|
|
187
|
-
}, [initialState, showConfetti, showFireworks, snapEffects]);
|
|
216
|
+
}, [initialState, showConfetti, showFireworks, snapEffects, stateStore]);
|
|
188
217
|
|
|
189
218
|
const handlersRef = useRef(handlers);
|
|
190
219
|
handlersRef.current = handlers;
|
|
191
220
|
|
|
221
|
+
const applyActionActivityState = useCallback(
|
|
222
|
+
(name: unknown, params: Record<string, unknown>, pending: boolean) => {
|
|
223
|
+
stateStore.update(
|
|
224
|
+
Object.fromEntries(
|
|
225
|
+
buildActionActivityStateChanges({
|
|
226
|
+
actionName: name,
|
|
227
|
+
params,
|
|
228
|
+
pending,
|
|
229
|
+
}).map(({ path, value }) => [path, value]),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
[stateStore],
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const setActionPending = useCallback(
|
|
237
|
+
(name: unknown, params: Record<string, unknown>) => {
|
|
238
|
+
pendingActionCountRef.current += 1;
|
|
239
|
+
setHasPendingAction(true);
|
|
240
|
+
setActionActivityVersion((version) => version + 1);
|
|
241
|
+
applyActionActivityState(name, params, true);
|
|
242
|
+
},
|
|
243
|
+
[applyActionActivityState],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const setActionSettled = useCallback(
|
|
247
|
+
(name: unknown, params: Record<string, unknown>) => {
|
|
248
|
+
pendingActionCountRef.current = Math.max(
|
|
249
|
+
0,
|
|
250
|
+
pendingActionCountRef.current - 1,
|
|
251
|
+
);
|
|
252
|
+
applyActionActivityState(name, params, false);
|
|
253
|
+
if (pendingActionCountRef.current === 0) {
|
|
254
|
+
setHasPendingAction(false);
|
|
255
|
+
}
|
|
256
|
+
setActionActivityVersion((version) => version + 1);
|
|
257
|
+
},
|
|
258
|
+
[applyActionActivityState],
|
|
259
|
+
);
|
|
260
|
+
|
|
192
261
|
const handleAction = useCallback((name: unknown, params: unknown) => {
|
|
193
262
|
const inputs = (stateRef.current.inputs ?? {}) as Record<string, JsonValue>;
|
|
194
263
|
const p = (params ?? {}) as Record<string, unknown>;
|
|
195
264
|
const h = handlersRef.current;
|
|
265
|
+
let result: unknown;
|
|
266
|
+
setActionPending(name, p);
|
|
267
|
+
|
|
196
268
|
switch (name) {
|
|
197
269
|
case "submit":
|
|
198
|
-
h.submit(String(p.target ?? ""), inputs);
|
|
270
|
+
result = h.submit(String(p.target ?? ""), inputs);
|
|
199
271
|
break;
|
|
200
272
|
case "open_url":
|
|
201
|
-
h.open_url(String(p.target ?? ""));
|
|
273
|
+
result = h.open_url(String(p.target ?? ""));
|
|
202
274
|
break;
|
|
203
275
|
case "open_snap":
|
|
204
|
-
h.open_snap(String(p.target ?? ""));
|
|
276
|
+
result = h.open_snap(String(p.target ?? ""));
|
|
205
277
|
break;
|
|
206
278
|
case "open_mini_app":
|
|
207
|
-
h.open_mini_app(String(p.target ?? ""));
|
|
279
|
+
result = h.open_mini_app(String(p.target ?? ""));
|
|
208
280
|
break;
|
|
209
281
|
case "view_cast":
|
|
210
|
-
h.view_cast({ hash: String(p.hash ?? "") });
|
|
282
|
+
result = h.view_cast({ hash: String(p.hash ?? "") });
|
|
211
283
|
break;
|
|
212
284
|
case "view_profile":
|
|
213
|
-
h.view_profile({ fid: Number(p.fid ?? 0) });
|
|
285
|
+
result = h.view_profile({ fid: Number(p.fid ?? 0) });
|
|
214
286
|
break;
|
|
215
287
|
case "view_channel":
|
|
216
|
-
h.view_channel({ channelKey: String(p.channelKey ?? "") });
|
|
288
|
+
result = h.view_channel({ channelKey: String(p.channelKey ?? "") });
|
|
217
289
|
break;
|
|
218
290
|
case "compose_cast":
|
|
219
|
-
h.compose_cast({
|
|
291
|
+
result = h.compose_cast({
|
|
220
292
|
text: p.text ? String(p.text) : undefined,
|
|
221
293
|
channelKey: p.channelKey ? String(p.channelKey) : undefined,
|
|
222
294
|
embeds: Array.isArray(p.embeds) ? (p.embeds as string[]) : undefined,
|
|
223
295
|
});
|
|
224
296
|
break;
|
|
225
297
|
case "view_token":
|
|
226
|
-
h.view_token({ token: String(p.token ?? "") });
|
|
298
|
+
result = h.view_token({ token: String(p.token ?? "") });
|
|
227
299
|
break;
|
|
228
300
|
case "send_token":
|
|
229
|
-
h.send_token({
|
|
301
|
+
result = h.send_token({
|
|
230
302
|
token: String(p.token ?? ""),
|
|
231
303
|
amount: p.amount ? String(p.amount) : undefined,
|
|
232
304
|
recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
|
|
@@ -236,13 +308,13 @@ export function SnapViewCoreInner({
|
|
|
236
308
|
});
|
|
237
309
|
break;
|
|
238
310
|
case "swap_token":
|
|
239
|
-
h.swap_token({
|
|
311
|
+
result = h.swap_token({
|
|
240
312
|
sellToken: p.sellToken ? String(p.sellToken) : undefined,
|
|
241
313
|
buyToken: p.buyToken ? String(p.buyToken) : undefined,
|
|
242
314
|
});
|
|
243
315
|
break;
|
|
244
316
|
case "send_transaction":
|
|
245
|
-
h.send_transaction?.({
|
|
317
|
+
result = h.send_transaction?.({
|
|
246
318
|
chainId: String(p.chainId ?? ""),
|
|
247
319
|
to: String(p.to ?? ""),
|
|
248
320
|
data: optionalString(p.data),
|
|
@@ -256,11 +328,30 @@ export function SnapViewCoreInner({
|
|
|
256
328
|
default:
|
|
257
329
|
break;
|
|
258
330
|
}
|
|
259
|
-
|
|
331
|
+
|
|
332
|
+
if (result instanceof Promise) {
|
|
333
|
+
void result.finally(() => {
|
|
334
|
+
setActionSettled(name, p);
|
|
335
|
+
}).catch(() => {});
|
|
336
|
+
} else {
|
|
337
|
+
setActionSettled(name, p);
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}, [setActionPending, setActionSettled]);
|
|
341
|
+
|
|
342
|
+
const showLoadingOverlay =
|
|
343
|
+
loading ||
|
|
344
|
+
hasPendingAction ||
|
|
345
|
+
(actionActivityVersion >= 0 && pendingActionCountRef.current > 0);
|
|
260
346
|
|
|
261
347
|
return (
|
|
262
|
-
<View
|
|
263
|
-
{
|
|
348
|
+
<View
|
|
349
|
+
style={styles.container}
|
|
350
|
+
onStartShouldSetResponderCapture={() =>
|
|
351
|
+
hasPendingSnapAction(stateRef.current)
|
|
352
|
+
}
|
|
353
|
+
>
|
|
354
|
+
{showLoadingOverlay ? (
|
|
264
355
|
loadingOverlay === undefined ? (
|
|
265
356
|
<SnapLoadingOverlay appearance={mode} accentHex={accentHex} />
|
|
266
357
|
) : (
|
|
@@ -271,12 +362,8 @@ export function SnapViewCoreInner({
|
|
|
271
362
|
<SnapCatalogView
|
|
272
363
|
key={pageKey}
|
|
273
364
|
spec={spec}
|
|
274
|
-
|
|
365
|
+
store={stateStore}
|
|
275
366
|
loading={false}
|
|
276
|
-
onStateChange={(changes) => {
|
|
277
|
-
applyStatePaths(stateRef.current, changes);
|
|
278
|
-
onRenderStateChange?.(cloneSnapRenderState(stateRef.current));
|
|
279
|
-
}}
|
|
280
367
|
onAction={handleAction}
|
|
281
368
|
/>
|
|
282
369
|
</SnapVersionProvider>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Spec } from "@json-render/core";
|
|
2
2
|
import type { SnapRenderState } from "../render-state";
|
|
3
|
+
import type { SnapTransactionResult } from "../schemas";
|
|
3
4
|
|
|
4
5
|
export type { SnapRenderState };
|
|
5
6
|
|
|
@@ -50,5 +51,7 @@ export type SnapActionHandlers = {
|
|
|
50
51
|
recipientAddress?: string;
|
|
51
52
|
}) => void;
|
|
52
53
|
swap_token: (params: { sellToken?: string; buyToken?: string }) => void;
|
|
53
|
-
send_transaction?: (
|
|
54
|
+
send_transaction?: (
|
|
55
|
+
params: SnapSendTransactionParams,
|
|
56
|
+
) => void | Promise<void | SnapTransactionResult>;
|
|
54
57
|
};
|
package/src/render-state.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type SnapRenderStateChanges =
|
|
|
7
7
|
| undefined;
|
|
8
8
|
|
|
9
9
|
const SNAP_RENDER_STATE_META_KEY = "__snapRender";
|
|
10
|
+
const ACTION_ACTIVITY_KEY_MAX_LENGTH = 64;
|
|
10
11
|
|
|
11
12
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
12
13
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -121,6 +122,51 @@ export function applyStatePaths(
|
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
function sanitizeActionActivityKey(value: string): string {
|
|
126
|
+
const sanitized = value
|
|
127
|
+
.trim()
|
|
128
|
+
.slice(0, ACTION_ACTIVITY_KEY_MAX_LENGTH)
|
|
129
|
+
.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
130
|
+
return sanitized || "action";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getActionActivityKey(
|
|
134
|
+
actionName: unknown,
|
|
135
|
+
params: Record<string, unknown>,
|
|
136
|
+
): string {
|
|
137
|
+
const explicitKey = params.activityKey;
|
|
138
|
+
return sanitizeActionActivityKey(
|
|
139
|
+
typeof explicitKey === "string" && explicitKey.trim()
|
|
140
|
+
? explicitKey
|
|
141
|
+
: String(actionName || "action"),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildActionActivityStateChanges({
|
|
146
|
+
actionName,
|
|
147
|
+
params,
|
|
148
|
+
pending,
|
|
149
|
+
}: {
|
|
150
|
+
actionName: unknown;
|
|
151
|
+
params: Record<string, unknown>;
|
|
152
|
+
pending: boolean;
|
|
153
|
+
}): { path: string; value: unknown }[] {
|
|
154
|
+
const key = getActionActivityKey(actionName, params);
|
|
155
|
+
return [
|
|
156
|
+
{ path: `/actions/${key}/name`, value: String(actionName || "action") },
|
|
157
|
+
{ path: `/actions/${key}/pending`, value: pending },
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function hasPendingSnapAction(model: SnapRenderState): boolean {
|
|
162
|
+
const actions = model.actions;
|
|
163
|
+
if (!isRecord(actions)) return false;
|
|
164
|
+
|
|
165
|
+
return Object.values(actions).some(
|
|
166
|
+
(action) => isRecord(action) && action.pending === true,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
124
170
|
export function getUnpresentedSnapEffects(
|
|
125
171
|
model: SnapRenderState,
|
|
126
172
|
effects: readonly string[] | undefined,
|
package/src/schemas.ts
CHANGED
|
@@ -107,10 +107,9 @@ const surfaceSchema = z.discriminatedUnion("type", [
|
|
|
107
107
|
const fidSchema = z.number().int().nonnegative();
|
|
108
108
|
const userSchema = z.object({ fid: fidSchema });
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
const basePayloadSchema = z
|
|
111
111
|
.object({
|
|
112
112
|
fid: fidSchema.optional(), // deprecated in favor of user.fid
|
|
113
|
-
inputs: z.record(z.string(), postInputValueSchema).default({}),
|
|
114
113
|
timestamp: z.number().int(),
|
|
115
114
|
audience: z.string(),
|
|
116
115
|
user: userSchema,
|
|
@@ -118,6 +117,58 @@ export const payloadSchema = z
|
|
|
118
117
|
})
|
|
119
118
|
.strip();
|
|
120
119
|
|
|
120
|
+
const snapSendTransactionParamsSchema = z
|
|
121
|
+
.object({
|
|
122
|
+
chainId: z.string(),
|
|
123
|
+
to: z.string(),
|
|
124
|
+
data: z.string().optional(),
|
|
125
|
+
value: z.string().optional(),
|
|
126
|
+
gas: z.string().optional(),
|
|
127
|
+
gasPrice: z.string().optional(),
|
|
128
|
+
maxFeePerGas: z.string().optional(),
|
|
129
|
+
maxPriorityFeePerGas: z.string().optional(),
|
|
130
|
+
})
|
|
131
|
+
.strict();
|
|
132
|
+
|
|
133
|
+
export type SnapSendTransactionParams = z.infer<
|
|
134
|
+
typeof snapSendTransactionParamsSchema
|
|
135
|
+
>;
|
|
136
|
+
|
|
137
|
+
const snapTransactionSuccessSchema = z
|
|
138
|
+
.object({
|
|
139
|
+
success: z.literal(true),
|
|
140
|
+
transactionHash: z.string(),
|
|
141
|
+
})
|
|
142
|
+
.strict();
|
|
143
|
+
|
|
144
|
+
const snapTransactionFailureSchema = z
|
|
145
|
+
.object({
|
|
146
|
+
success: z.literal(false),
|
|
147
|
+
reason: z
|
|
148
|
+
.enum(["rejected_by_user", "failed", "unknown"])
|
|
149
|
+
.optional()
|
|
150
|
+
.default("unknown"),
|
|
151
|
+
message: z.string().optional(),
|
|
152
|
+
code: z.union([z.string(), z.number()]).optional(),
|
|
153
|
+
transactionHash: z.string().optional(),
|
|
154
|
+
})
|
|
155
|
+
.strict();
|
|
156
|
+
|
|
157
|
+
export const snapTransactionResultSchema = z.discriminatedUnion("success", [
|
|
158
|
+
snapTransactionSuccessSchema,
|
|
159
|
+
snapTransactionFailureSchema,
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
export type SnapTransactionResult = z.infer<
|
|
163
|
+
typeof snapTransactionResultSchema
|
|
164
|
+
>;
|
|
165
|
+
|
|
166
|
+
export const payloadSchema = basePayloadSchema
|
|
167
|
+
.extend({
|
|
168
|
+
inputs: z.record(z.string(), postInputValueSchema).default({}),
|
|
169
|
+
})
|
|
170
|
+
.strip();
|
|
171
|
+
|
|
121
172
|
export type SnapPayload = z.infer<typeof payloadSchema>;
|
|
122
173
|
|
|
123
174
|
/** JFS payload shape for POST minus deprecated `fid`; used for GET auth via payload header. */
|
|
@@ -127,6 +178,7 @@ export type SnapGetPayload = z.infer<typeof getPayloadSchema>;
|
|
|
127
178
|
|
|
128
179
|
export const ACTION_TYPE_GET = "get" as const;
|
|
129
180
|
export const ACTION_TYPE_POST = "post" as const;
|
|
181
|
+
export const ACTION_TYPE_TRANSACTION_RESULT = "transaction_result" as const;
|
|
130
182
|
|
|
131
183
|
const snapGetActionSchema = z.object({
|
|
132
184
|
type: z.literal(ACTION_TYPE_GET),
|
|
@@ -144,9 +196,28 @@ const snapPostActionSchema = payloadSchema.extend({
|
|
|
144
196
|
|
|
145
197
|
export type SnapPostAction = z.infer<typeof snapPostActionSchema>;
|
|
146
198
|
|
|
199
|
+
export const transactionResultPayloadSchema = basePayloadSchema
|
|
200
|
+
.extend({
|
|
201
|
+
type: z.literal(ACTION_TYPE_TRANSACTION_RESULT),
|
|
202
|
+
transaction: z
|
|
203
|
+
.object({
|
|
204
|
+
request: snapSendTransactionParamsSchema,
|
|
205
|
+
result: snapTransactionResultSchema,
|
|
206
|
+
})
|
|
207
|
+
.strict(),
|
|
208
|
+
})
|
|
209
|
+
.strip();
|
|
210
|
+
|
|
211
|
+
export type SnapTransactionResultPayload = z.infer<
|
|
212
|
+
typeof transactionResultPayloadSchema
|
|
213
|
+
>;
|
|
214
|
+
|
|
215
|
+
export type SnapTransactionResultAction = SnapTransactionResultPayload;
|
|
216
|
+
|
|
147
217
|
export const snapActionSchema = z.discriminatedUnion("type", [
|
|
148
218
|
snapGetActionSchema,
|
|
149
219
|
snapPostActionSchema,
|
|
220
|
+
transactionResultPayloadSchema,
|
|
150
221
|
]);
|
|
151
222
|
|
|
152
223
|
export type SnapAction = z.infer<typeof snapActionSchema>;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import {
|
|
3
|
+
ACTION_TYPE_TRANSACTION_RESULT,
|
|
3
4
|
ACTION_TYPE_GET,
|
|
4
5
|
ACTION_TYPE_POST,
|
|
5
6
|
getPayloadSchema,
|
|
6
7
|
payloadSchema,
|
|
8
|
+
transactionResultPayloadSchema,
|
|
7
9
|
type SnapAction,
|
|
8
10
|
type SnapPayload,
|
|
9
11
|
type SnapGetPayload,
|
|
12
|
+
type SnapTransactionResultPayload,
|
|
10
13
|
} from "../schemas";
|
|
11
14
|
import { decodePayload, parseJfs, verifyJFS } from "./verify";
|
|
12
15
|
import { SNAP_PAYLOAD_HEADER } from "../constants";
|
|
@@ -121,9 +124,14 @@ async function parsePostRequest(
|
|
|
121
124
|
request: Request,
|
|
122
125
|
options: ParseRequestOptions,
|
|
123
126
|
): Promise<ParseRequestResult> {
|
|
124
|
-
const result = await validateJfsPayload
|
|
127
|
+
const result = await validateJfsPayload<
|
|
128
|
+
SnapPayload | SnapTransactionResultPayload
|
|
129
|
+
>({
|
|
125
130
|
jfsText: await request.text(),
|
|
126
|
-
schema:
|
|
131
|
+
schema: (decodedPayload) =>
|
|
132
|
+
isTransactionResultPayload(decodedPayload)
|
|
133
|
+
? transactionResultPayloadSchema
|
|
134
|
+
: payloadSchema,
|
|
127
135
|
request,
|
|
128
136
|
options,
|
|
129
137
|
});
|
|
@@ -142,12 +150,30 @@ async function parsePostRequest(
|
|
|
142
150
|
};
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
if (isTransactionResultPayload(payload)) {
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
action: payload,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
145
160
|
return {
|
|
146
161
|
success: true,
|
|
147
162
|
action: { type: ACTION_TYPE_POST, ...payload },
|
|
148
163
|
};
|
|
149
164
|
}
|
|
150
165
|
|
|
166
|
+
function isTransactionResultPayload(
|
|
167
|
+
payload: unknown,
|
|
168
|
+
): payload is { type: typeof ACTION_TYPE_TRANSACTION_RESULT } {
|
|
169
|
+
return (
|
|
170
|
+
payload !== null &&
|
|
171
|
+
typeof payload === "object" &&
|
|
172
|
+
"type" in payload &&
|
|
173
|
+
payload.type === ACTION_TYPE_TRANSACTION_RESULT
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
151
177
|
/**
|
|
152
178
|
* Shared pipeline for authenticated snap requests: parse the JFS envelope,
|
|
153
179
|
* decode and schema-validate the payload, optionally verify the JFS signature
|
|
@@ -156,7 +182,9 @@ async function parsePostRequest(
|
|
|
156
182
|
*
|
|
157
183
|
* Both GET (payload header) and POST (request body) feed into this.
|
|
158
184
|
*/
|
|
159
|
-
async function validateJfsPayload<
|
|
185
|
+
async function validateJfsPayload<
|
|
186
|
+
T extends SnapPayload | SnapGetPayload | SnapTransactionResultPayload,
|
|
187
|
+
>({
|
|
160
188
|
jfsText,
|
|
161
189
|
schema,
|
|
162
190
|
request,
|
|
@@ -164,7 +192,7 @@ async function validateJfsPayload<T extends SnapPayload | SnapGetPayload>({
|
|
|
164
192
|
invalidJsonMessage,
|
|
165
193
|
}: {
|
|
166
194
|
jfsText: string;
|
|
167
|
-
schema: z.ZodType
|
|
195
|
+
schema: z.ZodType | ((decodedPayload: unknown) => z.ZodType);
|
|
168
196
|
request: Request;
|
|
169
197
|
options: ParseRequestOptions;
|
|
170
198
|
invalidJsonMessage?: string;
|
|
@@ -183,14 +211,17 @@ async function validateJfsPayload<T extends SnapPayload | SnapGetPayload>({
|
|
|
183
211
|
}
|
|
184
212
|
const jfs = parsed.jfs;
|
|
185
213
|
|
|
186
|
-
const
|
|
214
|
+
const decodedPayload = decodePayload(jfs.payload);
|
|
215
|
+
const selectedSchema =
|
|
216
|
+
typeof schema === "function" ? schema(decodedPayload) : schema;
|
|
217
|
+
const payloadParsed = selectedSchema.safeParse(decodedPayload);
|
|
187
218
|
if (!payloadParsed.success) {
|
|
188
219
|
return {
|
|
189
220
|
ok: false,
|
|
190
221
|
error: { type: "validation", issues: payloadParsed.error.issues },
|
|
191
222
|
};
|
|
192
223
|
}
|
|
193
|
-
const payload = payloadParsed.data;
|
|
224
|
+
const payload = payloadParsed.data as T;
|
|
194
225
|
|
|
195
226
|
if (!options.skipJFSVerification) {
|
|
196
227
|
const verified = await verifyJFS(jfs);
|
package/src/ui/button.ts
CHANGED
|
@@ -8,6 +8,7 @@ export const buttonProps = z.object({
|
|
|
8
8
|
label: z.string().min(1).max(BUTTON_MAX_LABEL_CHARS),
|
|
9
9
|
variant: z.enum(BUTTON_VARIANTS).optional(),
|
|
10
10
|
icon: z.enum(ICON_NAMES).optional(),
|
|
11
|
+
disabled: z.boolean().optional(),
|
|
11
12
|
});
|
|
12
13
|
|
|
13
14
|
export type ButtonProps = z.infer<typeof buttonProps>;
|
package/src/ui/catalog.ts
CHANGED
|
@@ -23,6 +23,10 @@ const snapClientParams = z.object({
|
|
|
23
23
|
client_action: z.record(z.string(), z.unknown()),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
const activityParams = {
|
|
27
|
+
activityKey: z.string().min(1).max(64).optional(),
|
|
28
|
+
};
|
|
29
|
+
|
|
26
30
|
/**
|
|
27
31
|
* json-render catalog for snap elements.
|
|
28
32
|
*
|
|
@@ -121,32 +125,32 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
121
125
|
submit: {
|
|
122
126
|
description:
|
|
123
127
|
"POST to snap server with signed body (fid, inputs, timestamp, signature); response is next snap page.",
|
|
124
|
-
params: z.object({ target: z.string() }),
|
|
128
|
+
params: z.object({ target: z.string(), ...activityParams }),
|
|
125
129
|
},
|
|
126
130
|
open_url: {
|
|
127
131
|
description: "Open external URL in browser.",
|
|
128
|
-
params: z.object({ target: z.string() }),
|
|
132
|
+
params: z.object({ target: z.string(), ...activityParams }),
|
|
129
133
|
},
|
|
130
134
|
open_snap: {
|
|
131
135
|
description:
|
|
132
136
|
"Open a snap URL inline. The client renders the target as a snap rather than opening a browser.",
|
|
133
|
-
params: z.object({ target: z.string() }),
|
|
137
|
+
params: z.object({ target: z.string(), ...activityParams }),
|
|
134
138
|
},
|
|
135
139
|
open_mini_app: {
|
|
136
140
|
description: "Open target URL as a Farcaster mini app.",
|
|
137
|
-
params: z.object({ target: z.string() }),
|
|
141
|
+
params: z.object({ target: z.string(), ...activityParams }),
|
|
138
142
|
},
|
|
139
143
|
view_cast: {
|
|
140
144
|
description: "Navigate to a cast by hash.",
|
|
141
|
-
params: z.object({ hash: z.string() }),
|
|
145
|
+
params: z.object({ hash: z.string(), ...activityParams }),
|
|
142
146
|
},
|
|
143
147
|
view_profile: {
|
|
144
148
|
description: "Navigate to a user profile by FID.",
|
|
145
|
-
params: z.object({ fid: z.number() }),
|
|
149
|
+
params: z.object({ fid: z.number(), ...activityParams }),
|
|
146
150
|
},
|
|
147
151
|
view_channel: {
|
|
148
152
|
description: "Navigate to a Farcaster channel by channel key.",
|
|
149
|
-
params: z.object({ channelKey: z.string() }),
|
|
153
|
+
params: z.object({ channelKey: z.string(), ...activityParams }),
|
|
150
154
|
},
|
|
151
155
|
compose_cast: {
|
|
152
156
|
description: "Open the cast composer with optional pre-filled content.",
|
|
@@ -154,11 +158,12 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
154
158
|
text: z.string().optional(),
|
|
155
159
|
channelKey: z.string().optional(),
|
|
156
160
|
embeds: z.array(z.string()).optional(),
|
|
161
|
+
...activityParams,
|
|
157
162
|
}),
|
|
158
163
|
},
|
|
159
164
|
view_token: {
|
|
160
165
|
description: "View a token in the wallet. Token is a CAIP-19 identifier.",
|
|
161
|
-
params: z.object({ token: z.string() }),
|
|
166
|
+
params: z.object({ token: z.string(), ...activityParams }),
|
|
162
167
|
},
|
|
163
168
|
send_token: {
|
|
164
169
|
description: "Open send flow for a token. Token is CAIP-19.",
|
|
@@ -167,6 +172,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
167
172
|
amount: z.string().optional(),
|
|
168
173
|
recipientFid: z.number().optional(),
|
|
169
174
|
recipientAddress: z.string().optional(),
|
|
175
|
+
...activityParams,
|
|
170
176
|
}),
|
|
171
177
|
},
|
|
172
178
|
swap_token: {
|
|
@@ -174,6 +180,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
174
180
|
params: z.object({
|
|
175
181
|
sellToken: z.string().optional(),
|
|
176
182
|
buyToken: z.string().optional(),
|
|
183
|
+
...activityParams,
|
|
177
184
|
}),
|
|
178
185
|
},
|
|
179
186
|
send_transaction: {
|
|
@@ -188,6 +195,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
188
195
|
gasPrice: z.string().optional(),
|
|
189
196
|
maxFeePerGas: z.string().optional(),
|
|
190
197
|
maxPriorityFeePerGas: z.string().optional(),
|
|
198
|
+
...activityParams,
|
|
191
199
|
}),
|
|
192
200
|
},
|
|
193
201
|
paginator_next: {
|