@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
@@ -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
- export const payloadSchema = z
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: payloadSchema,
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<T extends SnapPayload | SnapGetPayload>({
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<T>;
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 payloadParsed = schema.safeParse(decodePayload(jfs.payload));
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,23 +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(),
191
- }),
192
- },
193
- send_calls: {
194
- description:
195
- "Request one or more EVM calls through the host wallet using wallet_sendCalls.",
196
- params: z.object({
197
- version: z.literal("1.0").optional(),
198
- chainId: z.string(),
199
- atomicRequired: z.boolean().optional(),
200
- id: z.string().optional(),
201
- calls: z.array(
202
- z.object({
203
- to: z.string().optional(),
204
- data: z.string().optional(),
205
- value: z.string().optional(),
206
- }),
207
- ),
198
+ ...activityParams,
208
199
  }),
209
200
  },
210
201
  paginator_next: {