thirdweb 5.121.2 → 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.
- package/dist/cjs/react/core/hooks/x402/useFetchWithPaymentCore.js +1 -0
- package/dist/cjs/react/core/hooks/x402/useFetchWithPaymentCore.js.map +1 -1
- package/dist/cjs/react/native/hooks/x402/useFetchWithPayment.js +1 -1
- package/dist/cjs/react/web/hooks/x402/useFetchWithPayment.js +2 -2
- package/dist/cjs/react/web/hooks/x402/useFetchWithPayment.js.map +1 -1
- package/dist/cjs/react/web/ui/x402/PaymentErrorModal.js +4 -4
- package/dist/cjs/react/web/ui/x402/PaymentErrorModal.js.map +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/x402/encode.js +41 -2
- package/dist/cjs/x402/encode.js.map +1 -1
- package/dist/cjs/x402/fetchWithPayment.js +56 -33
- package/dist/cjs/x402/fetchWithPayment.js.map +1 -1
- package/dist/cjs/x402/schemas.js +134 -2
- package/dist/cjs/x402/schemas.js.map +1 -1
- package/dist/cjs/x402/sign.js +56 -15
- package/dist/cjs/x402/sign.js.map +1 -1
- package/dist/esm/react/core/hooks/x402/useFetchWithPaymentCore.js +2 -1
- package/dist/esm/react/core/hooks/x402/useFetchWithPaymentCore.js.map +1 -1
- package/dist/esm/react/native/hooks/x402/useFetchWithPayment.js +1 -1
- package/dist/esm/react/web/hooks/x402/useFetchWithPayment.js +2 -2
- package/dist/esm/react/web/hooks/x402/useFetchWithPayment.js.map +1 -1
- package/dist/esm/react/web/ui/x402/PaymentErrorModal.js +5 -5
- package/dist/esm/react/web/ui/x402/PaymentErrorModal.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/x402/encode.js +40 -2
- package/dist/esm/x402/encode.js.map +1 -1
- package/dist/esm/x402/fetchWithPayment.js +57 -35
- package/dist/esm/x402/fetchWithPayment.js.map +1 -1
- package/dist/esm/x402/schemas.js +129 -1
- package/dist/esm/x402/schemas.js.map +1 -1
- package/dist/esm/x402/sign.js +57 -16
- package/dist/esm/x402/sign.js.map +1 -1
- package/dist/scripts/bridge-widget.js +2 -2
- package/dist/types/react/core/hooks/x402/useFetchWithPaymentCore.d.ts +1 -0
- package/dist/types/react/core/hooks/x402/useFetchWithPaymentCore.d.ts.map +1 -1
- package/dist/types/react/native/hooks/x402/useFetchWithPayment.d.ts +1 -1
- package/dist/types/react/web/hooks/x402/useFetchWithPayment.d.ts +1 -1
- package/dist/types/react/web/hooks/x402/useFetchWithPayment.d.ts.map +1 -1
- package/dist/types/react/web/ui/x402/PaymentErrorModal.d.ts +1 -0
- package/dist/types/react/web/ui/x402/PaymentErrorModal.d.ts.map +1 -1
- package/dist/types/version.d.ts +1 -1
- package/dist/types/x402/encode.d.ts +7 -0
- package/dist/types/x402/encode.d.ts.map +1 -1
- package/dist/types/x402/fetchWithPayment.d.ts +10 -1
- package/dist/types/x402/fetchWithPayment.d.ts.map +1 -1
- package/dist/types/x402/schemas.d.ts +60 -2
- package/dist/types/x402/schemas.d.ts.map +1 -1
- package/dist/types/x402/sign.d.ts +13 -1
- package/dist/types/x402/sign.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/contract/actions/resolve-abi.test.ts +3 -5
- package/src/extensions/airdrop/write/airdropERC721WithSignature.test.ts +4 -5
- package/src/extensions/erc1155/drop1155.test.ts +1 -1
- package/src/extensions/erc1155/write/sigMint1155.test.ts +189 -187
- package/src/extensions/erc20/drop20.test.ts +1 -1
- package/src/extensions/erc721/write/sigMint721.test.ts +4 -6
- package/src/extensions/modules/ClaimableERC1155/claimableERC1155.test.ts +180 -178
- package/src/extensions/modules/ClaimableERC20/claimableERC20.test.ts +1 -1
- package/src/extensions/modules/ClaimableERC721/claimableERC721.test.ts +1 -1
- package/src/extensions/prebuilts/process-ref-deployments.test.ts +1 -1
- package/src/react/core/hooks/x402/useFetchWithPaymentCore.ts +6 -1
- package/src/react/native/hooks/x402/useFetchWithPayment.ts +1 -1
- package/src/react/web/hooks/x402/useFetchWithPayment.tsx +3 -1
- package/src/react/web/ui/x402/PaymentErrorModal.tsx +9 -4
- package/src/version.ts +1 -1
- package/src/x402/encode.ts +46 -2
- package/src/x402/fetchWithPayment.test.ts +1173 -104
- package/src/x402/fetchWithPayment.ts +72 -54
- package/src/x402/schemas.test.ts +123 -0
- package/src/x402/schemas.ts +199 -6
- 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 {
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
210
|
-
await clearPermitSignatureFromCache(
|
|
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
|
+
});
|
package/src/x402/schemas.ts
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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(),
|