@farcaster/snap 2.8.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.
Files changed (37) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/react/catalog-renderer.d.ts +5 -5
  4. package/dist/react/catalog-renderer.js +16 -4
  5. package/dist/react/components/action-button.js +23 -5
  6. package/dist/react/index.d.ts +2 -13
  7. package/dist/react/snap-view-core.js +90 -45
  8. package/dist/react/v1/snap-view.js +1 -1
  9. package/dist/react/v2/snap-view.js +1 -1
  10. package/dist/react-native/components/snap-action-button.js +6 -1
  11. package/dist/react-native/snap-view-core.js +77 -44
  12. package/dist/react-native/types.d.ts +2 -13
  13. package/dist/render-state.d.ts +9 -0
  14. package/dist/render-state.js +27 -0
  15. package/dist/schemas.d.ts +123 -3
  16. package/dist/schemas.js +53 -2
  17. package/dist/server/parseRequest.js +19 -3
  18. package/dist/ui/button.d.ts +1 -0
  19. package/dist/ui/button.js +1 -0
  20. package/dist/ui/catalog.d.ts +13 -14
  21. package/dist/ui/catalog.js +15 -22
  22. package/package.json +1 -1
  23. package/src/index.ts +7 -0
  24. package/src/react/catalog-renderer.tsx +57 -3
  25. package/src/react/components/action-button.tsx +32 -3
  26. package/src/react/index.tsx +4 -14
  27. package/src/react/snap-view-core.tsx +144 -48
  28. package/src/react/v1/snap-view.tsx +1 -0
  29. package/src/react/v2/snap-view.tsx +1 -0
  30. package/src/react-native/components/snap-action-button.tsx +6 -1
  31. package/src/react-native/snap-view-core.tsx +114 -48
  32. package/src/react-native/types.ts +4 -14
  33. package/src/render-state.ts +46 -0
  34. package/src/schemas.ts +73 -2
  35. package/src/server/parseRequest.ts +37 -6
  36. package/src/ui/button.ts +1 -0
  37. package/src/ui/catalog.ts +16 -25
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createStateStore } from "@json-render/react-native";
2
3
  import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
4
  import { SnapCatalogView } from "./catalog-renderer.js";
4
5
  import { ConfettiOverlay } from "./confetti-overlay.js";
@@ -8,7 +9,7 @@ import { SnapVersionProvider } from "./snap-version-context.js";
8
9
  import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
9
10
  import { ActivityIndicator, StyleSheet, View } from "react-native";
10
11
  import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "@farcaster/snap";
11
- import { applyStatePaths, buildInitialRenderState, cloneSnapRenderState, getUnpresentedSnapEffects, markSnapEffectsPresented, } from "../render-state.js";
12
+ import { buildActionActivityStateChanges, buildInitialRenderState, cloneSnapRenderState, getUnpresentedSnapEffects, hasPendingSnapAction, markSnapEffectsPresented, } from "../render-state.js";
12
13
  function asRecord(value) {
13
14
  return value && typeof value === "object"
14
15
  ? value
@@ -17,6 +18,11 @@ function asRecord(value) {
17
18
  function optionalString(value) {
18
19
  return value ? String(value) : undefined;
19
20
  }
21
+ function recordValue(value) {
22
+ return value && typeof value === "object" && !Array.isArray(value)
23
+ ? value
24
+ : undefined;
25
+ }
20
26
  function withDefaultElementProps(spec) {
21
27
  if (!spec || typeof spec !== "object" || !("elements" in spec))
22
28
  return spec;
@@ -54,10 +60,26 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
54
60
  initialRenderState,
55
61
  themeAccent: snap.theme?.accent,
56
62
  }), [initialRenderState, spec.state, snap.theme?.accent]);
63
+ const stateStore = useMemo(() => createStateStore(initialState), [
64
+ initialState,
65
+ ]);
57
66
  const stateRef = useRef(initialState);
67
+ const onRenderStateChangeRef = useRef(onRenderStateChange);
68
+ const pendingActionCountRef = useRef(0);
69
+ const [hasPendingAction, setHasPendingAction] = useState(false);
70
+ const [actionActivityVersion, setActionActivityVersion] = useState(0);
58
71
  useEffect(() => {
59
72
  stateRef.current = cloneSnapRenderState(initialState);
60
73
  }, [initialState]);
74
+ useEffect(() => {
75
+ onRenderStateChangeRef.current = onRenderStateChange;
76
+ }, [onRenderStateChange]);
77
+ useEffect(() => stateStore.subscribe(() => {
78
+ const snapshot = cloneSnapRenderState(stateStore.getSnapshot());
79
+ stateRef.current = snapshot;
80
+ setHasPendingAction(hasPendingSnapAction(snapshot));
81
+ onRenderStateChangeRef.current?.(snapshot);
82
+ }), [stateStore]);
61
83
  useEffect(() => {
62
84
  const catalogResult = snapJsonRenderCatalog.validate(spec);
63
85
  if (!catalogResult.success) {
@@ -77,10 +99,6 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
77
99
  confetti: 0,
78
100
  fireworks: 0,
79
101
  });
80
- const onRenderStateChangeRef = useRef(onRenderStateChange);
81
- useEffect(() => {
82
- onRenderStateChangeRef.current = onRenderStateChange;
83
- }, [onRenderStateChange]);
84
102
  useEffect(() => {
85
103
  const effectsToPresent = getUnpresentedSnapEffects(stateRef.current, snapEffects);
86
104
  if (effectsToPresent.length === 0) {
@@ -97,7 +115,10 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
97
115
  return;
98
116
  }
99
117
  if (markSnapEffectsPresented(stateRef.current, effectsToPresent)) {
100
- onRenderStateChangeRef.current?.(cloneSnapRenderState(stateRef.current));
118
+ const meta = recordValue(stateRef.current.__snapRender);
119
+ stateStore.update({
120
+ "/__snapRender/presentedEffects": meta?.presentedEffects ?? [],
121
+ });
101
122
  }
102
123
  setEffectRunKeys((current) => ({
103
124
  confetti: effectsToPresent.includes("confetti")
@@ -111,47 +132,70 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
111
132
  ? current.fireworks
112
133
  : 0,
113
134
  }));
114
- }, [initialState, showConfetti, showFireworks, snapEffects]);
135
+ }, [initialState, showConfetti, showFireworks, snapEffects, stateStore]);
115
136
  const handlersRef = useRef(handlers);
116
137
  handlersRef.current = handlers;
138
+ const applyActionActivityState = useCallback((name, params, pending) => {
139
+ stateStore.update(Object.fromEntries(buildActionActivityStateChanges({
140
+ actionName: name,
141
+ params,
142
+ pending,
143
+ }).map(({ path, value }) => [path, value])));
144
+ }, [stateStore]);
145
+ const setActionPending = useCallback((name, params) => {
146
+ pendingActionCountRef.current += 1;
147
+ setHasPendingAction(true);
148
+ setActionActivityVersion((version) => version + 1);
149
+ applyActionActivityState(name, params, true);
150
+ }, [applyActionActivityState]);
151
+ const setActionSettled = useCallback((name, params) => {
152
+ pendingActionCountRef.current = Math.max(0, pendingActionCountRef.current - 1);
153
+ applyActionActivityState(name, params, false);
154
+ if (pendingActionCountRef.current === 0) {
155
+ setHasPendingAction(false);
156
+ }
157
+ setActionActivityVersion((version) => version + 1);
158
+ }, [applyActionActivityState]);
117
159
  const handleAction = useCallback((name, params) => {
118
160
  const inputs = (stateRef.current.inputs ?? {});
119
161
  const p = (params ?? {});
120
162
  const h = handlersRef.current;
163
+ let result;
164
+ setActionPending(name, p);
121
165
  switch (name) {
122
166
  case "submit":
123
- h.submit(String(p.target ?? ""), inputs);
167
+ result = h.submit(String(p.target ?? ""), inputs);
124
168
  break;
125
169
  case "open_url":
126
- h.open_url(String(p.target ?? ""));
170
+ result = h.open_url(String(p.target ?? ""));
127
171
  break;
128
172
  case "open_snap":
129
- h.open_snap(String(p.target ?? ""));
173
+ result = h.open_snap(String(p.target ?? ""));
130
174
  break;
131
175
  case "open_mini_app":
132
- h.open_mini_app(String(p.target ?? ""));
176
+ result = h.open_mini_app(String(p.target ?? ""));
133
177
  break;
134
178
  case "view_cast":
135
- h.view_cast({ hash: String(p.hash ?? "") });
179
+ result = h.view_cast({ hash: String(p.hash ?? "") });
136
180
  break;
137
181
  case "view_profile":
138
- h.view_profile({ fid: Number(p.fid ?? 0) });
182
+ result = h.view_profile({ fid: Number(p.fid ?? 0) });
139
183
  break;
140
184
  case "view_channel":
141
- h.view_channel({ channelKey: String(p.channelKey ?? "") });
185
+ result = h.view_channel({ channelKey: String(p.channelKey ?? "") });
142
186
  break;
143
187
  case "compose_cast":
144
- h.compose_cast({
188
+ result = h.compose_cast({
145
189
  text: p.text ? String(p.text) : undefined,
146
190
  channelKey: p.channelKey ? String(p.channelKey) : undefined,
147
191
  embeds: Array.isArray(p.embeds) ? p.embeds : undefined,
148
192
  });
149
193
  break;
150
194
  case "view_token":
151
- h.view_token({ token: String(p.token ?? "") });
195
+ result = h.view_token({ token: String(p.token ?? "") });
152
196
  break;
153
197
  case "send_token":
154
- h.send_token({
198
+ result = h.send_token({
155
199
  token: String(p.token ?? ""),
156
200
  amount: p.amount ? String(p.amount) : undefined,
157
201
  recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
@@ -161,13 +205,13 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
161
205
  });
162
206
  break;
163
207
  case "swap_token":
164
- h.swap_token({
208
+ result = h.swap_token({
165
209
  sellToken: p.sellToken ? String(p.sellToken) : undefined,
166
210
  buyToken: p.buyToken ? String(p.buyToken) : undefined,
167
211
  });
168
212
  break;
169
213
  case "send_transaction":
170
- h.send_transaction?.({
214
+ result = h.send_transaction?.({
171
215
  chainId: String(p.chainId ?? ""),
172
216
  to: String(p.to ?? ""),
173
217
  data: optionalString(p.data),
@@ -178,34 +222,23 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
178
222
  maxPriorityFeePerGas: optionalString(p.maxPriorityFeePerGas),
179
223
  });
180
224
  break;
181
- case "send_calls":
182
- h.send_calls?.({
183
- version: p.version === "1.0" ? "1.0" : undefined,
184
- chainId: String(p.chainId ?? ""),
185
- atomicRequired: typeof p.atomicRequired === "boolean"
186
- ? p.atomicRequired
187
- : undefined,
188
- id: optionalString(p.id),
189
- calls: Array.isArray(p.calls)
190
- ? p.calls.map((call) => {
191
- const c = asRecord(call);
192
- return {
193
- to: optionalString(c.to),
194
- data: optionalString(c.data),
195
- value: optionalString(c.value),
196
- };
197
- })
198
- : [],
199
- });
200
- break;
201
225
  default:
202
226
  break;
203
227
  }
204
- }, []);
205
- return (_jsxs(View, { style: styles.container, children: [loading ? (loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })) : (loadingOverlay)) : null, _jsx(SnapVersionProvider, { value: snap.version === "2.0" ? "2.0" : "1.0", children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
206
- applyStatePaths(stateRef.current, changes);
207
- onRenderStateChange?.(cloneSnapRenderState(stateRef.current));
208
- }, onAction: handleAction }, pageKey) }), showConfetti && effectRunKeys.confetti > 0 && (_jsx(ConfettiOverlay, {}, effectRunKeys.confetti)), showFireworks && effectRunKeys.fireworks > 0 && (_jsx(FireworksOverlay, {}, effectRunKeys.fireworks))] }));
228
+ if (result instanceof Promise) {
229
+ void result.finally(() => {
230
+ setActionSettled(name, p);
231
+ }).catch(() => { });
232
+ }
233
+ else {
234
+ setActionSettled(name, p);
235
+ }
236
+ return result;
237
+ }, [setActionPending, setActionSettled]);
238
+ const showLoadingOverlay = loading ||
239
+ hasPendingAction ||
240
+ (actionActivityVersion >= 0 && pendingActionCountRef.current > 0);
241
+ return (_jsxs(View, { style: styles.container, onStartShouldSetResponderCapture: () => hasPendingSnapAction(stateRef.current), children: [showLoadingOverlay ? (loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })) : (loadingOverlay)) : null, _jsx(SnapVersionProvider, { value: snap.version === "2.0" ? "2.0" : "1.0", children: _jsx(SnapCatalogView, { spec: spec, store: stateStore, loading: false, onAction: handleAction }, pageKey) }), showConfetti && effectRunKeys.confetti > 0 && (_jsx(ConfettiOverlay, {}, effectRunKeys.confetti)), showFireworks && effectRunKeys.fireworks > 0 && (_jsx(FireworksOverlay, {}, effectRunKeys.fireworks))] }));
209
242
  }
210
243
  export function SnapLoadingOverlay({ appearance, accentHex, }) {
211
244
  return (_jsx(View, { style: [
@@ -1,5 +1,6 @@
1
1
  import type { Spec } from "@json-render/core";
2
2
  import type { SnapRenderState } from "../render-state.js";
3
+ import type { SnapTransactionResult } from "../schemas.js";
3
4
  export type { SnapRenderState };
4
5
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
5
6
  [key: string]: JsonValue;
@@ -22,17 +23,6 @@ export type SnapSendTransactionParams = {
22
23
  maxFeePerGas?: string;
23
24
  maxPriorityFeePerGas?: string;
24
25
  };
25
- export type SnapSendCallsParams = {
26
- version?: "1.0";
27
- chainId: string;
28
- atomicRequired?: boolean;
29
- id?: string;
30
- calls: Array<{
31
- to?: string;
32
- data?: string;
33
- value?: string;
34
- }>;
35
- };
36
26
  export type SnapActionHandlers = {
37
27
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
38
28
  open_url: (target: string) => void;
@@ -65,6 +55,5 @@ export type SnapActionHandlers = {
65
55
  sellToken?: string;
66
56
  buyToken?: string;
67
57
  }) => void;
68
- send_transaction?: (params: SnapSendTransactionParams) => void;
69
- send_calls?: (params: SnapSendCallsParams) => void;
58
+ send_transaction?: (params: SnapSendTransactionParams) => void | Promise<void | SnapTransactionResult>;
70
59
  };
@@ -5,6 +5,15 @@ export type SnapRenderStateChanges = {
5
5
  }[] | Record<string, unknown> | null | undefined;
6
6
  export declare function cloneSnapRenderState<T>(value: T): T;
7
7
  export declare function applyStatePaths(model: Record<string, unknown>, changes: SnapRenderStateChanges): void;
8
+ export declare function buildActionActivityStateChanges({ actionName, params, pending, }: {
9
+ actionName: unknown;
10
+ params: Record<string, unknown>;
11
+ pending: boolean;
12
+ }): {
13
+ path: string;
14
+ value: unknown;
15
+ }[];
16
+ export declare function hasPendingSnapAction(model: SnapRenderState): boolean;
8
17
  export declare function getUnpresentedSnapEffects(model: SnapRenderState, effects: readonly string[] | undefined): string[];
9
18
  export declare function markSnapEffectsPresented(model: SnapRenderState, effects: readonly string[] | undefined): boolean;
10
19
  export declare function buildInitialRenderState({ specState, initialRenderState, themeAccent, }: {
@@ -1,4 +1,5 @@
1
1
  const SNAP_RENDER_STATE_META_KEY = "__snapRender";
2
+ const ACTION_ACTIVITY_KEY_MAX_LENGTH = 64;
2
3
  function isRecord(value) {
3
4
  return typeof value === "object" && value !== null && !Array.isArray(value);
4
5
  }
@@ -75,6 +76,32 @@ export function applyStatePaths(model, changes) {
75
76
  setStateValue(model, parts, value);
76
77
  }
77
78
  }
79
+ function sanitizeActionActivityKey(value) {
80
+ const sanitized = value
81
+ .trim()
82
+ .slice(0, ACTION_ACTIVITY_KEY_MAX_LENGTH)
83
+ .replace(/[^A-Za-z0-9_.-]/g, "_");
84
+ return sanitized || "action";
85
+ }
86
+ function getActionActivityKey(actionName, params) {
87
+ const explicitKey = params.activityKey;
88
+ return sanitizeActionActivityKey(typeof explicitKey === "string" && explicitKey.trim()
89
+ ? explicitKey
90
+ : String(actionName || "action"));
91
+ }
92
+ export function buildActionActivityStateChanges({ actionName, params, pending, }) {
93
+ const key = getActionActivityKey(actionName, params);
94
+ return [
95
+ { path: `/actions/${key}/name`, value: String(actionName || "action") },
96
+ { path: `/actions/${key}/pending`, value: pending },
97
+ ];
98
+ }
99
+ export function hasPendingSnapAction(model) {
100
+ const actions = model.actions;
101
+ if (!isRecord(actions))
102
+ return false;
103
+ return Object.values(actions).some((action) => isRecord(action) && action.pending === true);
104
+ }
78
105
  export function getUnpresentedSnapEffects(model, effects) {
79
106
  const presentedEffects = getPresentedSnapEffects(model);
80
107
  return normalizeEffects(effects).filter((effect) => !presentedEffects.has(effect));
package/dist/schemas.d.ts CHANGED
@@ -69,9 +69,34 @@ export type SnapHandlerResult = {
69
69
  effects?: z.input<typeof snapResponseSchema>["effects"];
70
70
  ui: SnapSpecInput;
71
71
  };
72
+ declare const snapSendTransactionParamsSchema: z.ZodObject<{
73
+ chainId: z.ZodString;
74
+ to: z.ZodString;
75
+ data: z.ZodOptional<z.ZodString>;
76
+ value: z.ZodOptional<z.ZodString>;
77
+ gas: z.ZodOptional<z.ZodString>;
78
+ gasPrice: z.ZodOptional<z.ZodString>;
79
+ maxFeePerGas: z.ZodOptional<z.ZodString>;
80
+ maxPriorityFeePerGas: z.ZodOptional<z.ZodString>;
81
+ }, z.core.$strict>;
82
+ export type SnapSendTransactionParams = z.infer<typeof snapSendTransactionParamsSchema>;
83
+ export declare const snapTransactionResultSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
84
+ success: z.ZodLiteral<true>;
85
+ transactionHash: z.ZodString;
86
+ }, z.core.$strict>, z.ZodObject<{
87
+ success: z.ZodLiteral<false>;
88
+ reason: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
89
+ unknown: "unknown";
90
+ rejected_by_user: "rejected_by_user";
91
+ failed: "failed";
92
+ }>>>;
93
+ message: z.ZodOptional<z.ZodString>;
94
+ code: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
95
+ transactionHash: z.ZodOptional<z.ZodString>;
96
+ }, z.core.$strict>], "success">;
97
+ export type SnapTransactionResult = z.infer<typeof snapTransactionResultSchema>;
72
98
  export declare const payloadSchema: z.ZodObject<{
73
99
  fid: z.ZodOptional<z.ZodNumber>;
74
- inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
75
100
  timestamp: z.ZodNumber;
76
101
  audience: z.ZodString;
77
102
  user: z.ZodObject<{
@@ -88,6 +113,7 @@ export declare const payloadSchema: z.ZodObject<{
88
113
  }, z.core.$strip>, z.ZodObject<{
89
114
  type: z.ZodLiteral<"standalone">;
90
115
  }, z.core.$strip>], "type">;
116
+ inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
91
117
  }, z.core.$strip>;
92
118
  export type SnapPayload = z.infer<typeof payloadSchema>;
93
119
  /** JFS payload shape for POST minus deprecated `fid`; used for GET auth via payload header. */
@@ -112,6 +138,7 @@ export declare const getPayloadSchema: z.ZodObject<{
112
138
  export type SnapGetPayload = z.infer<typeof getPayloadSchema>;
113
139
  export declare const ACTION_TYPE_GET: "get";
114
140
  export declare const ACTION_TYPE_POST: "post";
141
+ export declare const ACTION_TYPE_TRANSACTION_RESULT: "transaction_result";
115
142
  declare const snapGetActionSchema: z.ZodObject<{
116
143
  type: z.ZodLiteral<"get">;
117
144
  user: z.ZodOptional<z.ZodObject<{
@@ -134,7 +161,6 @@ declare const snapGetActionSchema: z.ZodObject<{
134
161
  export type SnapGetAction = z.infer<typeof snapGetActionSchema>;
135
162
  declare const snapPostActionSchema: z.ZodObject<{
136
163
  fid: z.ZodOptional<z.ZodNumber>;
137
- inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
138
164
  timestamp: z.ZodNumber;
139
165
  audience: z.ZodString;
140
166
  user: z.ZodObject<{
@@ -151,9 +177,58 @@ declare const snapPostActionSchema: z.ZodObject<{
151
177
  }, z.core.$strip>, z.ZodObject<{
152
178
  type: z.ZodLiteral<"standalone">;
153
179
  }, z.core.$strip>], "type">;
180
+ inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
154
181
  type: z.ZodLiteral<"post">;
155
182
  }, z.core.$strip>;
156
183
  export type SnapPostAction = z.infer<typeof snapPostActionSchema>;
184
+ export declare const transactionResultPayloadSchema: z.ZodObject<{
185
+ fid: z.ZodOptional<z.ZodNumber>;
186
+ timestamp: z.ZodNumber;
187
+ audience: z.ZodString;
188
+ user: z.ZodObject<{
189
+ fid: z.ZodNumber;
190
+ }, z.core.$strip>;
191
+ surface: z.ZodDiscriminatedUnion<[z.ZodObject<{
192
+ type: z.ZodLiteral<"cast">;
193
+ cast: z.ZodObject<{
194
+ hash: z.ZodString;
195
+ author: z.ZodObject<{
196
+ fid: z.ZodNumber;
197
+ }, z.core.$strip>;
198
+ }, z.core.$strip>;
199
+ }, z.core.$strip>, z.ZodObject<{
200
+ type: z.ZodLiteral<"standalone">;
201
+ }, z.core.$strip>], "type">;
202
+ type: z.ZodLiteral<"transaction_result">;
203
+ transaction: z.ZodObject<{
204
+ request: z.ZodObject<{
205
+ chainId: z.ZodString;
206
+ to: z.ZodString;
207
+ data: z.ZodOptional<z.ZodString>;
208
+ value: z.ZodOptional<z.ZodString>;
209
+ gas: z.ZodOptional<z.ZodString>;
210
+ gasPrice: z.ZodOptional<z.ZodString>;
211
+ maxFeePerGas: z.ZodOptional<z.ZodString>;
212
+ maxPriorityFeePerGas: z.ZodOptional<z.ZodString>;
213
+ }, z.core.$strict>;
214
+ result: z.ZodDiscriminatedUnion<[z.ZodObject<{
215
+ success: z.ZodLiteral<true>;
216
+ transactionHash: z.ZodString;
217
+ }, z.core.$strict>, z.ZodObject<{
218
+ success: z.ZodLiteral<false>;
219
+ reason: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
220
+ unknown: "unknown";
221
+ rejected_by_user: "rejected_by_user";
222
+ failed: "failed";
223
+ }>>>;
224
+ message: z.ZodOptional<z.ZodString>;
225
+ code: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
226
+ transactionHash: z.ZodOptional<z.ZodString>;
227
+ }, z.core.$strict>], "success">;
228
+ }, z.core.$strict>;
229
+ }, z.core.$strip>;
230
+ export type SnapTransactionResultPayload = z.infer<typeof transactionResultPayloadSchema>;
231
+ export type SnapTransactionResultAction = SnapTransactionResultPayload;
157
232
  export declare const snapActionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
158
233
  type: z.ZodLiteral<"get">;
159
234
  user: z.ZodOptional<z.ZodObject<{
@@ -174,7 +249,6 @@ export declare const snapActionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
174
249
  }, z.core.$strip>], "type">>;
175
250
  }, z.core.$strip>, z.ZodObject<{
176
251
  fid: z.ZodOptional<z.ZodNumber>;
177
- inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
178
252
  timestamp: z.ZodNumber;
179
253
  audience: z.ZodString;
180
254
  user: z.ZodObject<{
@@ -191,7 +265,53 @@ export declare const snapActionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
191
265
  }, z.core.$strip>, z.ZodObject<{
192
266
  type: z.ZodLiteral<"standalone">;
193
267
  }, z.core.$strip>], "type">;
268
+ inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>>>;
194
269
  type: z.ZodLiteral<"post">;
270
+ }, z.core.$strip>, z.ZodObject<{
271
+ fid: z.ZodOptional<z.ZodNumber>;
272
+ timestamp: z.ZodNumber;
273
+ audience: z.ZodString;
274
+ user: z.ZodObject<{
275
+ fid: z.ZodNumber;
276
+ }, z.core.$strip>;
277
+ surface: z.ZodDiscriminatedUnion<[z.ZodObject<{
278
+ type: z.ZodLiteral<"cast">;
279
+ cast: z.ZodObject<{
280
+ hash: z.ZodString;
281
+ author: z.ZodObject<{
282
+ fid: z.ZodNumber;
283
+ }, z.core.$strip>;
284
+ }, z.core.$strip>;
285
+ }, z.core.$strip>, z.ZodObject<{
286
+ type: z.ZodLiteral<"standalone">;
287
+ }, z.core.$strip>], "type">;
288
+ type: z.ZodLiteral<"transaction_result">;
289
+ transaction: z.ZodObject<{
290
+ request: z.ZodObject<{
291
+ chainId: z.ZodString;
292
+ to: z.ZodString;
293
+ data: z.ZodOptional<z.ZodString>;
294
+ value: z.ZodOptional<z.ZodString>;
295
+ gas: z.ZodOptional<z.ZodString>;
296
+ gasPrice: z.ZodOptional<z.ZodString>;
297
+ maxFeePerGas: z.ZodOptional<z.ZodString>;
298
+ maxPriorityFeePerGas: z.ZodOptional<z.ZodString>;
299
+ }, z.core.$strict>;
300
+ result: z.ZodDiscriminatedUnion<[z.ZodObject<{
301
+ success: z.ZodLiteral<true>;
302
+ transactionHash: z.ZodString;
303
+ }, z.core.$strict>, z.ZodObject<{
304
+ success: z.ZodLiteral<false>;
305
+ reason: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
306
+ unknown: "unknown";
307
+ rejected_by_user: "rejected_by_user";
308
+ failed: "failed";
309
+ }>>>;
310
+ message: z.ZodOptional<z.ZodString>;
311
+ code: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
312
+ transactionHash: z.ZodOptional<z.ZodString>;
313
+ }, z.core.$strict>], "success">;
314
+ }, z.core.$strict>;
195
315
  }, z.core.$strip>], "type">;
196
316
  export type SnapAction = z.infer<typeof snapActionSchema>;
197
317
  export type SnapContext = {
package/dist/schemas.js CHANGED
@@ -49,20 +49,59 @@ const surfaceSchema = z.discriminatedUnion("type", [
49
49
  ]);
50
50
  const fidSchema = z.number().int().nonnegative();
51
51
  const userSchema = z.object({ fid: fidSchema });
52
- export const payloadSchema = z
52
+ const basePayloadSchema = z
53
53
  .object({
54
54
  fid: fidSchema.optional(), // deprecated in favor of user.fid
55
- inputs: z.record(z.string(), postInputValueSchema).default({}),
56
55
  timestamp: z.number().int(),
57
56
  audience: z.string(),
58
57
  user: userSchema,
59
58
  surface: surfaceSchema,
60
59
  })
61
60
  .strip();
61
+ const snapSendTransactionParamsSchema = z
62
+ .object({
63
+ chainId: z.string(),
64
+ to: z.string(),
65
+ data: z.string().optional(),
66
+ value: z.string().optional(),
67
+ gas: z.string().optional(),
68
+ gasPrice: z.string().optional(),
69
+ maxFeePerGas: z.string().optional(),
70
+ maxPriorityFeePerGas: z.string().optional(),
71
+ })
72
+ .strict();
73
+ const snapTransactionSuccessSchema = z
74
+ .object({
75
+ success: z.literal(true),
76
+ transactionHash: z.string(),
77
+ })
78
+ .strict();
79
+ const snapTransactionFailureSchema = z
80
+ .object({
81
+ success: z.literal(false),
82
+ reason: z
83
+ .enum(["rejected_by_user", "failed", "unknown"])
84
+ .optional()
85
+ .default("unknown"),
86
+ message: z.string().optional(),
87
+ code: z.union([z.string(), z.number()]).optional(),
88
+ transactionHash: z.string().optional(),
89
+ })
90
+ .strict();
91
+ export const snapTransactionResultSchema = z.discriminatedUnion("success", [
92
+ snapTransactionSuccessSchema,
93
+ snapTransactionFailureSchema,
94
+ ]);
95
+ export const payloadSchema = basePayloadSchema
96
+ .extend({
97
+ inputs: z.record(z.string(), postInputValueSchema).default({}),
98
+ })
99
+ .strip();
62
100
  /** JFS payload shape for POST minus deprecated `fid`; used for GET auth via payload header. */
63
101
  export const getPayloadSchema = payloadSchema.omit({ inputs: true, fid: true });
64
102
  export const ACTION_TYPE_GET = "get";
65
103
  export const ACTION_TYPE_POST = "post";
104
+ export const ACTION_TYPE_TRANSACTION_RESULT = "transaction_result";
66
105
  const snapGetActionSchema = z.object({
67
106
  type: z.literal(ACTION_TYPE_GET),
68
107
  user: userSchema.optional(),
@@ -73,7 +112,19 @@ const snapGetActionSchema = z.object({
73
112
  const snapPostActionSchema = payloadSchema.extend({
74
113
  type: z.literal(ACTION_TYPE_POST),
75
114
  });
115
+ export const transactionResultPayloadSchema = basePayloadSchema
116
+ .extend({
117
+ type: z.literal(ACTION_TYPE_TRANSACTION_RESULT),
118
+ transaction: z
119
+ .object({
120
+ request: snapSendTransactionParamsSchema,
121
+ result: snapTransactionResultSchema,
122
+ })
123
+ .strict(),
124
+ })
125
+ .strip();
76
126
  export const snapActionSchema = z.discriminatedUnion("type", [
77
127
  snapGetActionSchema,
78
128
  snapPostActionSchema,
129
+ transactionResultPayloadSchema,
79
130
  ]);
@@ -1,4 +1,4 @@
1
- import { ACTION_TYPE_GET, ACTION_TYPE_POST, getPayloadSchema, payloadSchema, } from "../schemas.js";
1
+ import { ACTION_TYPE_TRANSACTION_RESULT, ACTION_TYPE_GET, ACTION_TYPE_POST, getPayloadSchema, payloadSchema, transactionResultPayloadSchema, } from "../schemas.js";
2
2
  import { decodePayload, parseJfs, verifyJFS } from "./verify.js";
3
3
  import { SNAP_PAYLOAD_HEADER } from "../constants.js";
4
4
  const DEFAULT_SNAP_POST_MAX_SKEW_SECONDS = 300;
@@ -46,7 +46,9 @@ async function parseGetRequest(request, options) {
46
46
  async function parsePostRequest(request, options) {
47
47
  const result = await validateJfsPayload({
48
48
  jfsText: await request.text(),
49
- schema: payloadSchema,
49
+ schema: (decodedPayload) => isTransactionResultPayload(decodedPayload)
50
+ ? transactionResultPayloadSchema
51
+ : payloadSchema,
50
52
  request,
51
53
  options,
52
54
  });
@@ -63,11 +65,23 @@ async function parsePostRequest(request, options) {
63
65
  },
64
66
  };
65
67
  }
68
+ if (isTransactionResultPayload(payload)) {
69
+ return {
70
+ success: true,
71
+ action: payload,
72
+ };
73
+ }
66
74
  return {
67
75
  success: true,
68
76
  action: { type: ACTION_TYPE_POST, ...payload },
69
77
  };
70
78
  }
79
+ function isTransactionResultPayload(payload) {
80
+ return (payload !== null &&
81
+ typeof payload === "object" &&
82
+ "type" in payload &&
83
+ payload.type === ACTION_TYPE_TRANSACTION_RESULT);
84
+ }
71
85
  /**
72
86
  * Shared pipeline for authenticated snap requests: parse the JFS envelope,
73
87
  * decode and schema-validate the payload, optionally verify the JFS signature
@@ -88,7 +102,9 @@ async function validateJfsPayload({ jfsText, schema, request, options, invalidJs
88
102
  };
89
103
  }
90
104
  const jfs = parsed.jfs;
91
- const payloadParsed = schema.safeParse(decodePayload(jfs.payload));
105
+ const decodedPayload = decodePayload(jfs.payload);
106
+ const selectedSchema = typeof schema === "function" ? schema(decodedPayload) : schema;
107
+ const payloadParsed = selectedSchema.safeParse(decodedPayload);
92
108
  if (!payloadParsed.success) {
93
109
  return {
94
110
  ok: false,
@@ -42,5 +42,6 @@ export declare const buttonProps: z.ZodObject<{
42
42
  "trending-up": "trending-up";
43
43
  "trending-down": "trending-down";
44
44
  }>>;
45
+ disabled: z.ZodOptional<z.ZodBoolean>;
45
46
  }, z.core.$strip>;
46
47
  export type ButtonProps = z.infer<typeof buttonProps>;
package/dist/ui/button.js CHANGED
@@ -6,4 +6,5 @@ export const buttonProps = z.object({
6
6
  label: z.string().min(1).max(BUTTON_MAX_LABEL_CHARS),
7
7
  variant: z.enum(BUTTON_VARIANTS).optional(),
8
8
  icon: z.enum(ICON_NAMES).optional(),
9
+ disabled: z.boolean().optional(),
9
10
  });