thirdweb 5.121.1 → 5.121.3

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 (78) hide show
  1. package/dist/cjs/react/core/hooks/x402/useFetchWithPaymentCore.js +1 -0
  2. package/dist/cjs/react/core/hooks/x402/useFetchWithPaymentCore.js.map +1 -1
  3. package/dist/cjs/react/native/hooks/x402/useFetchWithPayment.js +1 -1
  4. package/dist/cjs/react/web/hooks/x402/useFetchWithPayment.js +2 -2
  5. package/dist/cjs/react/web/hooks/x402/useFetchWithPayment.js.map +1 -1
  6. package/dist/cjs/react/web/ui/Bridge/common/token-query.js +4 -1
  7. package/dist/cjs/react/web/ui/Bridge/common/token-query.js.map +1 -1
  8. package/dist/cjs/react/web/ui/x402/PaymentErrorModal.js +4 -4
  9. package/dist/cjs/react/web/ui/x402/PaymentErrorModal.js.map +1 -1
  10. package/dist/cjs/version.js +1 -1
  11. package/dist/cjs/x402/encode.js +41 -2
  12. package/dist/cjs/x402/encode.js.map +1 -1
  13. package/dist/cjs/x402/fetchWithPayment.js +56 -33
  14. package/dist/cjs/x402/fetchWithPayment.js.map +1 -1
  15. package/dist/cjs/x402/schemas.js +134 -2
  16. package/dist/cjs/x402/schemas.js.map +1 -1
  17. package/dist/cjs/x402/sign.js +56 -15
  18. package/dist/cjs/x402/sign.js.map +1 -1
  19. package/dist/esm/react/core/hooks/x402/useFetchWithPaymentCore.js +2 -1
  20. package/dist/esm/react/core/hooks/x402/useFetchWithPaymentCore.js.map +1 -1
  21. package/dist/esm/react/native/hooks/x402/useFetchWithPayment.js +1 -1
  22. package/dist/esm/react/web/hooks/x402/useFetchWithPayment.js +2 -2
  23. package/dist/esm/react/web/hooks/x402/useFetchWithPayment.js.map +1 -1
  24. package/dist/esm/react/web/ui/Bridge/common/token-query.js +4 -1
  25. package/dist/esm/react/web/ui/Bridge/common/token-query.js.map +1 -1
  26. package/dist/esm/react/web/ui/x402/PaymentErrorModal.js +5 -5
  27. package/dist/esm/react/web/ui/x402/PaymentErrorModal.js.map +1 -1
  28. package/dist/esm/version.js +1 -1
  29. package/dist/esm/x402/encode.js +40 -2
  30. package/dist/esm/x402/encode.js.map +1 -1
  31. package/dist/esm/x402/fetchWithPayment.js +57 -35
  32. package/dist/esm/x402/fetchWithPayment.js.map +1 -1
  33. package/dist/esm/x402/schemas.js +129 -1
  34. package/dist/esm/x402/schemas.js.map +1 -1
  35. package/dist/esm/x402/sign.js +57 -16
  36. package/dist/esm/x402/sign.js.map +1 -1
  37. package/dist/scripts/bridge-widget.js +3 -3
  38. package/dist/types/react/core/hooks/x402/useFetchWithPaymentCore.d.ts +1 -0
  39. package/dist/types/react/core/hooks/x402/useFetchWithPaymentCore.d.ts.map +1 -1
  40. package/dist/types/react/native/hooks/x402/useFetchWithPayment.d.ts +1 -1
  41. package/dist/types/react/web/hooks/x402/useFetchWithPayment.d.ts +1 -1
  42. package/dist/types/react/web/hooks/x402/useFetchWithPayment.d.ts.map +1 -1
  43. package/dist/types/react/web/ui/Bridge/common/token-query.d.ts.map +1 -1
  44. package/dist/types/react/web/ui/x402/PaymentErrorModal.d.ts +1 -0
  45. package/dist/types/react/web/ui/x402/PaymentErrorModal.d.ts.map +1 -1
  46. package/dist/types/version.d.ts +1 -1
  47. package/dist/types/x402/encode.d.ts +7 -0
  48. package/dist/types/x402/encode.d.ts.map +1 -1
  49. package/dist/types/x402/fetchWithPayment.d.ts +10 -1
  50. package/dist/types/x402/fetchWithPayment.d.ts.map +1 -1
  51. package/dist/types/x402/schemas.d.ts +60 -2
  52. package/dist/types/x402/schemas.d.ts.map +1 -1
  53. package/dist/types/x402/sign.d.ts +13 -1
  54. package/dist/types/x402/sign.d.ts.map +1 -1
  55. package/package.json +3 -3
  56. package/src/contract/actions/resolve-abi.test.ts +3 -5
  57. package/src/extensions/airdrop/write/airdropERC721WithSignature.test.ts +4 -5
  58. package/src/extensions/erc1155/drop1155.test.ts +1 -1
  59. package/src/extensions/erc1155/write/sigMint1155.test.ts +189 -187
  60. package/src/extensions/erc20/drop20.test.ts +1 -1
  61. package/src/extensions/erc721/write/sigMint721.test.ts +4 -6
  62. package/src/extensions/modules/ClaimableERC1155/claimableERC1155.test.ts +180 -178
  63. package/src/extensions/modules/ClaimableERC20/claimableERC20.test.ts +1 -1
  64. package/src/extensions/modules/ClaimableERC721/claimableERC721.test.ts +1 -1
  65. package/src/extensions/prebuilts/process-ref-deployments.test.ts +1 -1
  66. package/src/react/core/hooks/x402/useFetchWithPaymentCore.ts +6 -1
  67. package/src/react/native/hooks/x402/useFetchWithPayment.ts +1 -1
  68. package/src/react/web/hooks/x402/useFetchWithPayment.tsx +3 -1
  69. package/src/react/web/ui/Bridge/common/token-query.test.tsx +117 -0
  70. package/src/react/web/ui/Bridge/common/token-query.ts +4 -1
  71. package/src/react/web/ui/x402/PaymentErrorModal.tsx +9 -4
  72. package/src/version.ts +1 -1
  73. package/src/x402/encode.ts +46 -2
  74. package/src/x402/fetchWithPayment.test.ts +1173 -104
  75. package/src/x402/fetchWithPayment.ts +72 -54
  76. package/src/x402/schemas.test.ts +123 -0
  77. package/src/x402/schemas.ts +199 -6
  78. package/src/x402/sign.ts +86 -10
@@ -4,7 +4,7 @@ import { getAddress } from "../utils/address.js";
4
4
  import type { AsyncStorage } from "../utils/storage/AsyncStorage.js";
5
5
  import { webLocalStorage } from "../utils/storage/webStorage.js";
6
6
  import type { Wallet } from "../wallets/interfaces/wallet.js";
7
- import { safeBase64Decode } from "./encode.js";
7
+ import { base64DecodeUtf8 } from "./encode.js";
8
8
  import {
9
9
  getPaymentRequestHeader,
10
10
  getPaymentResponseHeader,
@@ -13,8 +13,9 @@ import { clearPermitSignatureFromCache } from "./permitSignatureStorage.js";
13
13
  import {
14
14
  extractEvmChainId,
15
15
  networkToCaip2ChainId,
16
+ normalizePaymentRequirements,
17
+ parsePaymentRequired,
16
18
  type RequestedPaymentRequirements,
17
- RequestedPaymentRequirementsSchema,
18
19
  } from "./schemas.js";
19
20
  import { createPaymentHeader } from "./sign.js";
20
21
  import { x402Version as defaultX402Version } from "./types.js";
@@ -33,7 +34,7 @@ import { x402Version as defaultX402Version } from "./types.js";
33
34
  * @param fetch - The fetch function to wrap (typically globalThis.fetch)
34
35
  * @param client - The thirdweb client used to access RPC infrastructure
35
36
  * @param wallet - The wallet used to sign payment messages
36
- * @param maxValue - The maximum allowed payment amount in base units
37
+ * @param maxValue - The maximum allowed payment amount in base units. `0n` only allows zero-amount payments
37
38
  * @returns A wrapped fetch function that handles 402 responses automatically
38
39
  *
39
40
  * @example
@@ -64,6 +65,10 @@ export function wrapFetchWithPayment(
64
65
  wallet: Wallet,
65
66
  options?: {
66
67
  maxValue?: bigint;
68
+ /**
69
+ * Selects the payment requirement to pay. Return one of the provided requirements;
70
+ * the returned requirement is validated again before payment.
71
+ */
67
72
  paymentRequirementsSelector?: (
68
73
  paymentRequirements: RequestedPaymentRequirements[],
69
74
  ) => RequestedPaymentRequirements | undefined;
@@ -81,49 +86,25 @@ export function wrapFetchWithPayment(
81
86
  return response;
82
87
  }
83
88
 
84
- let x402Version: number;
85
- let parsedPaymentRequirements: RequestedPaymentRequirements[];
86
- let error: string | undefined;
89
+ const initParams = init || {};
90
+
91
+ if ((initParams as { __is402Retry?: boolean }).__is402Retry) {
92
+ throw new Error("Payment already attempted");
93
+ }
87
94
 
88
95
  // Check payment-required header first before falling back to JSON body
89
96
  const paymentRequiredHeader = response.headers.get("payment-required");
90
- if (paymentRequiredHeader) {
91
- const decoded = safeBase64Decode(paymentRequiredHeader);
92
- const parsed = JSON.parse(decoded) as {
93
- x402Version: number;
94
- accepts: unknown[];
95
- error?: string;
96
- };
97
- if (!Array.isArray(parsed.accepts)) {
98
- throw new Error(
99
- `402 response has no usable x402 payment requirements. ${parsed.error ?? ""}`,
100
- );
101
- }
102
-
103
- x402Version = parsed.x402Version ?? defaultX402Version;
104
- parsedPaymentRequirements = parsed.accepts.map((x) =>
105
- RequestedPaymentRequirementsSchema.parse(x),
106
- );
107
- error = parsed.error;
108
- } else {
109
- const body = (await response.json()) as {
110
- x402Version: number;
111
- accepts: unknown[];
112
- error?: string;
113
- };
114
-
115
- if (!Array.isArray(body.accepts)) {
116
- throw new Error(
117
- `402 response has no usable x402 payment requirements. ${body.error ?? ""}`,
118
- );
119
- }
97
+ const paymentRequired: unknown = paymentRequiredHeader
98
+ ? JSON.parse(base64DecodeUtf8(paymentRequiredHeader))
99
+ : await response.json();
120
100
 
121
- x402Version = body.x402Version ?? defaultX402Version;
122
- parsedPaymentRequirements = body.accepts.map((x) =>
123
- RequestedPaymentRequirementsSchema.parse(x),
124
- );
125
- error = body.error;
126
- }
101
+ const parsed = parsePaymentRequired(paymentRequired, getRequestUrl(input));
102
+ const x402Version = parsed.x402Version ?? defaultX402Version;
103
+ const error = parsed.error;
104
+ const normalizedPaymentRequirements = parsed.accepts;
105
+ const parsedPaymentRequirements = normalizedPaymentRequirements.map(
106
+ (x) => x.requirements,
107
+ );
127
108
 
128
109
  const account = wallet.getAccount();
129
110
  let chain = wallet.getChain();
@@ -133,7 +114,7 @@ export function wrapFetchWithPayment(
133
114
  "Wallet not connected. Please connect your wallet to continue.",
134
115
  );
135
116
  }
136
- const selectedPaymentRequirements = options?.paymentRequirementsSelector
117
+ const selected = options?.paymentRequirementsSelector
137
118
  ? options.paymentRequirementsSelector(parsedPaymentRequirements)
138
119
  : defaultPaymentRequirementsSelector(
139
120
  parsedPaymentRequirements,
@@ -141,14 +122,22 @@ export function wrapFetchWithPayment(
141
122
  error,
142
123
  );
143
124
 
144
- if (!selectedPaymentRequirements) {
125
+ if (!selected) {
145
126
  throw new Error(
146
127
  `No suitable payment requirements found for chain ${chain.id}. ${error}`,
147
128
  );
148
129
  }
149
130
 
131
+ const { requirements: selectedPaymentRequirements } =
132
+ normalizePaymentRequirements(selected, {
133
+ resourceUrl: parsed.resourceUrl,
134
+ });
135
+ const acceptedRequirement =
136
+ normalizedPaymentRequirements.find((x) => x.requirements === selected)
137
+ ?.raw ?? selected;
138
+
150
139
  if (
151
- options?.maxValue &&
140
+ options?.maxValue !== undefined &&
152
141
  BigInt(selectedPaymentRequirements.maxAmountRequired) > options.maxValue
153
142
  ) {
154
143
  throw new Error(
@@ -176,23 +165,27 @@ export function wrapFetchWithPayment(
176
165
  }
177
166
  }
178
167
 
168
+ const storage = options?.storage ?? webLocalStorage;
179
169
  const paymentHeader = await createPaymentHeader(
180
170
  client,
181
171
  account,
182
172
  selectedPaymentRequirements,
183
173
  x402Version,
184
- options?.storage ?? webLocalStorage,
174
+ storage,
175
+ {
176
+ accepted: acceptedRequirement,
177
+ resource: parsed.resource ?? {
178
+ url: selectedPaymentRequirements.resource,
179
+ description: selectedPaymentRequirements.description,
180
+ mimeType: selectedPaymentRequirements.mimeType,
181
+ },
182
+ },
183
+ options?.maxValue,
185
184
  );
186
185
 
187
186
  const paymentRequestHeaderName = getPaymentRequestHeader(x402Version);
188
187
  const paymentResponseHeaderName = getPaymentResponseHeader(x402Version);
189
188
 
190
- const initParams = init || {};
191
-
192
- if ((initParams as { __is402Retry?: boolean }).__is402Retry) {
193
- throw new Error("Payment already attempted");
194
- }
195
-
196
189
  const newInit = {
197
190
  ...initParams,
198
191
  headers: {
@@ -206,8 +199,8 @@ export function wrapFetchWithPayment(
206
199
  const secondResponse = await fetch(input, newInit);
207
200
 
208
201
  // If payment was rejected (still 402), clear cached signature
209
- if (secondResponse.status === 402 && options?.storage) {
210
- await clearPermitSignatureFromCache(options.storage, {
202
+ if (secondResponse.status === 402) {
203
+ await clearPermitSignatureFromCache(storage, {
211
204
  chainId: paymentChainId,
212
205
  asset: selectedPaymentRequirements.asset,
213
206
  owner: getAddress(account.address),
@@ -219,6 +212,31 @@ export function wrapFetchWithPayment(
219
212
  };
220
213
  }
221
214
 
215
+ /**
216
+ * Resolves the absolute URL of a fetch input, if it can be determined.
217
+ * @internal
218
+ */
219
+ export function getRequestUrl(input: RequestInfo | URL): string | undefined {
220
+ const url =
221
+ typeof input === "string"
222
+ ? input
223
+ : input instanceof URL
224
+ ? input.href
225
+ : (input as Partial<Request> | undefined)?.url;
226
+ if (typeof url !== "string") {
227
+ return undefined;
228
+ }
229
+ try {
230
+ const base =
231
+ typeof globalThis.location !== "undefined"
232
+ ? globalThis.location.href
233
+ : undefined;
234
+ return new URL(url, base).toString();
235
+ } catch {
236
+ return undefined;
237
+ }
238
+ }
239
+
222
240
  function defaultPaymentRequirementsSelector(
223
241
  paymentRequirements: RequestedPaymentRequirements[],
224
242
  chainId: number,
@@ -0,0 +1,123 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ normalizePaymentRequirements,
4
+ parsePaymentRequirementsForDisplay,
5
+ } from "./schemas.js";
6
+
7
+ const PAY_TO = "0x1234567890123456789012345678901234567890";
8
+ const ASSET = "0x0000000000000000000000000000000000000001";
9
+ const REQUEST_URL = "https://api.example.com/paid";
10
+
11
+ const canonicalV2Requirement = {
12
+ scheme: "exact",
13
+ network: "eip155:8453",
14
+ amount: "1000",
15
+ payTo: PAY_TO,
16
+ maxTimeoutSeconds: 300,
17
+ asset: ASSET,
18
+ extra: { name: "Test Token", version: "1" },
19
+ };
20
+
21
+ describe("normalizePaymentRequirements", () => {
22
+ it("canonicalises the amount", () => {
23
+ const { requirements } = normalizePaymentRequirements(
24
+ { ...canonicalV2Requirement, amount: "0100" },
25
+ { resourceUrl: REQUEST_URL },
26
+ );
27
+ expect(requirements.maxAmountRequired).toBe("100");
28
+ });
29
+
30
+ it("accepts amount and maxAmountRequired with the same value", () => {
31
+ const { requirements } = normalizePaymentRequirements(
32
+ { ...canonicalV2Requirement, amount: "0100", maxAmountRequired: "100" },
33
+ { resourceUrl: REQUEST_URL },
34
+ );
35
+ expect(requirements.maxAmountRequired).toBe("100");
36
+ });
37
+
38
+ it.each([
39
+ { amount: "1", maxAmountRequired: "1000000000" },
40
+ { amount: "1000000000", maxAmountRequired: "1" },
41
+ ])("rejects mismatched amounts (%j)", (amounts) => {
42
+ expect(() =>
43
+ normalizePaymentRequirements(
44
+ { ...canonicalV2Requirement, ...amounts },
45
+ { resourceUrl: REQUEST_URL },
46
+ ),
47
+ ).toThrow("do not match");
48
+ });
49
+
50
+ it("keeps the raw requirement", () => {
51
+ const raw = { ...canonicalV2Requirement };
52
+ expect(
53
+ normalizePaymentRequirements(raw, { resourceUrl: REQUEST_URL }).raw,
54
+ ).toBe(raw);
55
+ });
56
+ });
57
+
58
+ describe("parsePaymentRequirementsForDisplay", () => {
59
+ it("parses a canonical v2 requirement without a resource using the request URL", () => {
60
+ expect(
61
+ parsePaymentRequirementsForDisplay(
62
+ { x402Version: 2, accepts: [canonicalV2Requirement] },
63
+ REQUEST_URL,
64
+ ),
65
+ ).toEqual([
66
+ expect.objectContaining({
67
+ network: "eip155:8453",
68
+ asset: ASSET,
69
+ maxAmountRequired: "1000",
70
+ resource: REQUEST_URL,
71
+ }),
72
+ ]);
73
+ });
74
+
75
+ it("prefers the top-level resource URL", () => {
76
+ expect(
77
+ parsePaymentRequirementsForDisplay(
78
+ {
79
+ x402Version: 2,
80
+ resource: { url: "https://api.example.com/resource" },
81
+ accepts: [canonicalV2Requirement],
82
+ },
83
+ REQUEST_URL,
84
+ ),
85
+ ).toEqual([
86
+ expect.objectContaining({ resource: "https://api.example.com/resource" }),
87
+ ]);
88
+ });
89
+
90
+ it("skips invalid entries", () => {
91
+ const requirements = parsePaymentRequirementsForDisplay(
92
+ {
93
+ x402Version: 2,
94
+ accepts: [
95
+ { ...canonicalV2Requirement, amount: "1", maxAmountRequired: "2" },
96
+ { ...canonicalV2Requirement, amount: "1e3" },
97
+ { ...canonicalV2Requirement, network: "eip155:1" },
98
+ ],
99
+ },
100
+ REQUEST_URL,
101
+ );
102
+ expect(requirements).toHaveLength(1);
103
+ expect(requirements[0]?.network).toBe("eip155:1");
104
+ });
105
+
106
+ it.each([[undefined], [null], [{}], [{ accepts: "invalid" }]])(
107
+ "returns no requirements for %j",
108
+ (paymentRequired) => {
109
+ expect(
110
+ parsePaymentRequirementsForDisplay(paymentRequired, REQUEST_URL),
111
+ ).toEqual([]);
112
+ },
113
+ );
114
+
115
+ it("returns no requirements when no resource URL can be resolved", () => {
116
+ expect(
117
+ parsePaymentRequirementsForDisplay({
118
+ x402Version: 2,
119
+ accepts: [canonicalV2Requirement],
120
+ }),
121
+ ).toEqual([]);
122
+ });
123
+ });
@@ -23,7 +23,12 @@ const RequestedPaymentPayloadSchema = PaymentPayloadSchema.extend({
23
23
 
24
24
  export type RequestedPaymentPayload = z.infer<
25
25
  typeof RequestedPaymentPayloadSchema
26
- >;
26
+ > & {
27
+ /** x402 v2: the payment requirement accepted by the client, as sent by the server */
28
+ accepted?: Record<string, unknown>;
29
+ /** x402 v2: the resource being paid for */
30
+ resource?: Record<string, unknown>;
31
+ };
27
32
  export type UnsignedPaymentPayload = Omit<
28
33
  RequestedPaymentPayload,
29
34
  "payload"
@@ -31,16 +36,204 @@ export type UnsignedPaymentPayload = Omit<
31
36
  payload: Omit<ExactEvmPayload, "signature"> & { signature: undefined };
32
37
  };
33
38
 
34
- export const RequestedPaymentRequirementsSchema =
35
- PaymentRequirementsSchema.extend({
36
- network: FacilitatorNetworkSchema,
37
- scheme: PaymentSchemeSchema,
38
- });
39
+ const RequestedPaymentRequirementsSchema = PaymentRequirementsSchema.extend({
40
+ network: FacilitatorNetworkSchema,
41
+ scheme: PaymentSchemeSchema,
42
+ });
39
43
 
40
44
  export type RequestedPaymentRequirements = z.infer<
41
45
  typeof RequestedPaymentRequirementsSchema
42
46
  >;
43
47
 
48
+ const MAX_UINT256 = 2n ** 256n - 1n;
49
+ const ATOMIC_AMOUNT_REGEX = /^\d{1,78}$/;
50
+
51
+ /**
52
+ * Returns true if the value is a base-10 integer string that fits in a uint256.
53
+ * @internal
54
+ */
55
+ export function isAtomicAmount(value: unknown): value is string {
56
+ return (
57
+ typeof value === "string" &&
58
+ ATOMIC_AMOUNT_REGEX.test(value) &&
59
+ BigInt(value) <= MAX_UINT256
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Reads an amount field and returns it in canonical form (no leading zeros).
65
+ */
66
+ function readAtomicAmount(
67
+ requirement: Record<string, unknown>,
68
+ field: "amount" | "maxAmountRequired",
69
+ ): string | undefined {
70
+ const value = requirement[field];
71
+ if (value === undefined) {
72
+ return undefined;
73
+ }
74
+ if (!isAtomicAmount(value)) {
75
+ throw new Error(
76
+ `Invalid payment requirements: ${field} must be an integer string in base units`,
77
+ );
78
+ }
79
+ return BigInt(value).toString();
80
+ }
81
+
82
+ /**
83
+ * Payment requirements normalised from a server 402 response.
84
+ * @internal
85
+ */
86
+ type NormalizedPaymentRequirements = {
87
+ /** The normalised requirement */
88
+ requirements: RequestedPaymentRequirements;
89
+ /** The requirement exactly as sent by the server */
90
+ raw: Record<string, unknown>;
91
+ };
92
+
93
+ /**
94
+ * Extracts the top-level resource object of a 402 payment required response, if any.
95
+ */
96
+ function getPaymentRequiredResource(
97
+ paymentRequired: unknown,
98
+ ): Record<string, unknown> | undefined {
99
+ if (!isRecord(paymentRequired)) {
100
+ return undefined;
101
+ }
102
+ const resource = paymentRequired.resource;
103
+ if (isRecord(resource) && typeof resource.url === "string") {
104
+ return resource;
105
+ }
106
+ return undefined;
107
+ }
108
+
109
+ /**
110
+ * Normalises a payment requirement from a 402 response (x402 v1, v2, or v2 envelopes with v1 requirements).
111
+ *
112
+ * @param requirement - The raw payment requirement
113
+ * @param context - Fallback values for fields that v2 moves out of the requirement
114
+ * @returns The normalised requirement and the raw requirement
115
+ * @throws If the amount is missing, malformed, or ambiguous, or the requirement is otherwise invalid
116
+ * @internal
117
+ */
118
+ export function normalizePaymentRequirements(
119
+ requirement: unknown,
120
+ context: { resourceUrl?: string } = {},
121
+ ): NormalizedPaymentRequirements {
122
+ if (!isRecord(requirement)) {
123
+ throw new Error("Invalid payment requirements: expected an object");
124
+ }
125
+
126
+ const amount = readAtomicAmount(requirement, "amount");
127
+ const maxAmountRequired = readAtomicAmount(requirement, "maxAmountRequired");
128
+ if (
129
+ amount !== undefined &&
130
+ maxAmountRequired !== undefined &&
131
+ amount !== maxAmountRequired
132
+ ) {
133
+ throw new Error(
134
+ `Invalid payment requirements: amount (${amount}) and maxAmountRequired (${maxAmountRequired}) do not match`,
135
+ );
136
+ }
137
+ const normalizedAmount = amount ?? maxAmountRequired;
138
+ if (normalizedAmount === undefined) {
139
+ throw new Error(
140
+ "Invalid payment requirements: missing amount or maxAmountRequired",
141
+ );
142
+ }
143
+
144
+ const requirements = RequestedPaymentRequirementsSchema.parse({
145
+ ...requirement,
146
+ maxAmountRequired: normalizedAmount,
147
+ resource:
148
+ typeof requirement.resource === "string"
149
+ ? requirement.resource
150
+ : context.resourceUrl,
151
+ description:
152
+ typeof requirement.description === "string"
153
+ ? requirement.description
154
+ : "",
155
+ mimeType:
156
+ typeof requirement.mimeType === "string" ? requirement.mimeType : "",
157
+ });
158
+
159
+ return { requirements, raw: requirement };
160
+ }
161
+
162
+ /**
163
+ * Parses a decoded 402 payment required object (PAYMENT-REQUIRED header or JSON body).
164
+ * Every entry of `accepts` is normalised with {@link normalizePaymentRequirements}.
165
+ *
166
+ * @param paymentRequired - The decoded payment required object
167
+ * @param requestUrl - The URL of the request, used when the response carries no resource URL
168
+ * @returns The x402 version, error, top-level resource, resolved resource URL and normalised requirements
169
+ * @throws If the object has no accepts array or any requirement is invalid
170
+ * @internal
171
+ */
172
+ export function parsePaymentRequired(
173
+ paymentRequired: unknown,
174
+ requestUrl?: string,
175
+ ): {
176
+ x402Version: number | undefined;
177
+ error: string | undefined;
178
+ resource: Record<string, unknown> | undefined;
179
+ resourceUrl: string | undefined;
180
+ accepts: NormalizedPaymentRequirements[];
181
+ } {
182
+ const data = isRecord(paymentRequired) ? paymentRequired : {};
183
+ const error = typeof data.error === "string" ? data.error : undefined;
184
+ if (!Array.isArray(data.accepts)) {
185
+ throw new Error(
186
+ `402 response has no usable x402 payment requirements. ${error ?? ""}`,
187
+ );
188
+ }
189
+ const resource = getPaymentRequiredResource(data);
190
+ const resourceUrl =
191
+ typeof resource?.url === "string" ? resource.url : requestUrl;
192
+ return {
193
+ x402Version:
194
+ typeof data.x402Version === "number" ? data.x402Version : undefined,
195
+ error,
196
+ resource,
197
+ resourceUrl,
198
+ accepts: data.accepts.map((requirement) =>
199
+ normalizePaymentRequirements(requirement, { resourceUrl }),
200
+ ),
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Parses the payment requirements of a 402 response for display only, skipping invalid entries.
206
+ *
207
+ * @param paymentRequired - The decoded payment required object
208
+ * @param requestUrl - The URL of the request, used when the response carries no resource URL
209
+ * @returns The normalised requirements that passed validation
210
+ * @internal
211
+ */
212
+ export function parsePaymentRequirementsForDisplay(
213
+ paymentRequired: unknown,
214
+ requestUrl?: string,
215
+ ): RequestedPaymentRequirements[] {
216
+ if (!isRecord(paymentRequired) || !Array.isArray(paymentRequired.accepts)) {
217
+ return [];
218
+ }
219
+ const resource = getPaymentRequiredResource(paymentRequired);
220
+ const resourceUrl =
221
+ typeof resource?.url === "string" ? resource.url : requestUrl;
222
+ return paymentRequired.accepts.flatMap((requirement) => {
223
+ try {
224
+ return [
225
+ normalizePaymentRequirements(requirement, { resourceUrl }).requirements,
226
+ ];
227
+ } catch {
228
+ return [];
229
+ }
230
+ });
231
+ }
232
+
233
+ function isRecord(value: unknown): value is Record<string, unknown> {
234
+ return typeof value === "object" && value !== null && !Array.isArray(value);
235
+ }
236
+
44
237
  const FacilitatorSettleResponseSchema = SettleResponseSchema.extend({
45
238
  network: FacilitatorNetworkSchema,
46
239
  errorMessage: z.string().optional(),