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.
- 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/Bridge/common/token-query.js +4 -1
- package/dist/cjs/react/web/ui/Bridge/common/token-query.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/Bridge/common/token-query.js +4 -1
- package/dist/esm/react/web/ui/Bridge/common/token-query.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 +3 -3
- 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/Bridge/common/token-query.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/Bridge/common/token-query.test.tsx +117 -0
- package/src/react/web/ui/Bridge/common/token-query.ts +4 -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
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { allowance } from "../extensions/erc20/__generated__/IERC20/read/allowance.js";
|
|
3
|
+
import { nonces } from "../extensions/erc20/__generated__/IERC20Permit/read/nonces.js";
|
|
4
|
+
import { getAddress } from "../utils/address.js";
|
|
5
|
+
import type { AsyncStorage } from "../utils/storage/AsyncStorage.js";
|
|
6
|
+
import { webLocalStorage } from "../utils/storage/webStorage.js";
|
|
2
7
|
import { safeBase64Decode, safeBase64Encode } from "./encode.js";
|
|
3
8
|
import { wrapFetchWithPayment } from "./fetchWithPayment.js";
|
|
4
9
|
import { getPaymentRequestHeader } from "./headers.js";
|
|
10
|
+
import {
|
|
11
|
+
getPermitSignatureFromCache,
|
|
12
|
+
savePermitSignatureToCache,
|
|
13
|
+
} from "./permitSignatureStorage.js";
|
|
5
14
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
vi.mock("./common.js", async (importOriginal) => {
|
|
16
|
+
const actual = await importOriginal<typeof import("./common.js")>();
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
getSupportedSignatureType: vi.fn(
|
|
20
|
+
async (args: { eip712Extras?: { primaryType?: string } }) =>
|
|
21
|
+
args.eip712Extras?.primaryType ?? "TransferWithAuthorization",
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
vi.mock("../extensions/erc20/__generated__/IERC20/read/allowance.js", () => ({
|
|
27
|
+
allowance: vi.fn(),
|
|
9
28
|
}));
|
|
10
29
|
|
|
30
|
+
vi.mock(
|
|
31
|
+
"../extensions/erc20/__generated__/IERC20Permit/read/nonces.js",
|
|
32
|
+
() => ({
|
|
33
|
+
nonces: vi.fn(),
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
11
37
|
// Mock webLocalStorage
|
|
12
38
|
vi.mock("../utils/storage/webStorage.js", () => ({
|
|
13
39
|
webLocalStorage: {
|
|
@@ -17,6 +43,34 @@ vi.mock("../utils/storage/webStorage.js", () => ({
|
|
|
17
43
|
},
|
|
18
44
|
}));
|
|
19
45
|
|
|
46
|
+
const SIGNATURE = `0x${"11".repeat(65)}`;
|
|
47
|
+
const CACHED_SIGNATURE = `0x${"22".repeat(65)}`;
|
|
48
|
+
const OWNER = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd";
|
|
49
|
+
const PAY_TO = "0x1234567890123456789012345678901234567890";
|
|
50
|
+
const OTHER_ADDRESS = "0x9999999999999999999999999999999999999999";
|
|
51
|
+
const ASSET = "0x0000000000000000000000000000000000000001";
|
|
52
|
+
|
|
53
|
+
function base64Utf8(data: unknown): string {
|
|
54
|
+
return Buffer.from(JSON.stringify(data), "utf-8").toString("base64");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function paymentRequiredResponse(
|
|
58
|
+
data: unknown,
|
|
59
|
+
mode: "header" | "body",
|
|
60
|
+
): Response {
|
|
61
|
+
if (mode === "header") {
|
|
62
|
+
return new Response(null, {
|
|
63
|
+
status: 402,
|
|
64
|
+
headers: { "payment-required": base64Utf8(data) },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return new Response(JSON.stringify(data), { status: 402 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function successResponse(): Response {
|
|
71
|
+
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
|
72
|
+
}
|
|
73
|
+
|
|
20
74
|
describe("wrapFetchWithPayment", () => {
|
|
21
75
|
const mockPaymentRequirements = {
|
|
22
76
|
scheme: "exact",
|
|
@@ -25,9 +79,9 @@ describe("wrapFetchWithPayment", () => {
|
|
|
25
79
|
resource: "https://api.example.com/resource",
|
|
26
80
|
description: "Test payment",
|
|
27
81
|
mimeType: "application/json",
|
|
28
|
-
payTo:
|
|
82
|
+
payTo: PAY_TO,
|
|
29
83
|
maxTimeoutSeconds: 300,
|
|
30
|
-
asset:
|
|
84
|
+
asset: ASSET,
|
|
31
85
|
extra: {
|
|
32
86
|
name: "Test Token",
|
|
33
87
|
version: "1",
|
|
@@ -40,12 +94,31 @@ describe("wrapFetchWithPayment", () => {
|
|
|
40
94
|
error: undefined,
|
|
41
95
|
};
|
|
42
96
|
|
|
97
|
+
const canonicalV2Requirement = {
|
|
98
|
+
scheme: "exact",
|
|
99
|
+
network: "eip155:1",
|
|
100
|
+
amount: "12345",
|
|
101
|
+
payTo: PAY_TO,
|
|
102
|
+
maxTimeoutSeconds: 300,
|
|
103
|
+
asset: ASSET,
|
|
104
|
+
extra: {
|
|
105
|
+
name: "Test Token",
|
|
106
|
+
version: "1",
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const canonicalV2Resource = {
|
|
111
|
+
url: "https://api.example.com/resource",
|
|
112
|
+
description: "Test payment",
|
|
113
|
+
mimeType: "application/json",
|
|
114
|
+
};
|
|
115
|
+
|
|
43
116
|
const mockClient = {
|
|
44
117
|
clientId: "test-client-id",
|
|
45
118
|
} as Parameters<typeof wrapFetchWithPayment>[1];
|
|
46
119
|
|
|
47
120
|
const mockAccount = {
|
|
48
|
-
address:
|
|
121
|
+
address: OWNER,
|
|
49
122
|
signTypedData: vi.fn(),
|
|
50
123
|
};
|
|
51
124
|
|
|
@@ -55,8 +128,36 @@ describe("wrapFetchWithPayment", () => {
|
|
|
55
128
|
switchChain: vi.fn(),
|
|
56
129
|
} as unknown as Parameters<typeof wrapFetchWithPayment>[2];
|
|
57
130
|
|
|
131
|
+
function sentPayment(
|
|
132
|
+
mockFetch: ReturnType<typeof vi.fn>,
|
|
133
|
+
x402Version: number,
|
|
134
|
+
) {
|
|
135
|
+
const init = mockFetch.mock.calls[1]?.[1] as RequestInit;
|
|
136
|
+
const headers = init.headers as Record<string, string>;
|
|
137
|
+
const header = headers[getPaymentRequestHeader(x402Version)];
|
|
138
|
+
expect(header).toBeDefined();
|
|
139
|
+
return JSON.parse(
|
|
140
|
+
Buffer.from(header as string, "base64").toString("utf-8"),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function signedValues() {
|
|
145
|
+
return mockAccount.signTypedData.mock.calls.map(
|
|
146
|
+
(call) => (call[0] as { message: { value: bigint } }).message.value,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function mockFetchWith(...responses: Response[]) {
|
|
151
|
+
const mockFetch = vi.fn();
|
|
152
|
+
for (const response of responses) {
|
|
153
|
+
mockFetch.mockResolvedValueOnce(response);
|
|
154
|
+
}
|
|
155
|
+
return mockFetch;
|
|
156
|
+
}
|
|
157
|
+
|
|
58
158
|
beforeEach(() => {
|
|
59
159
|
vi.clearAllMocks();
|
|
160
|
+
mockAccount.signTypedData.mockResolvedValue(SIGNATURE);
|
|
60
161
|
});
|
|
61
162
|
|
|
62
163
|
it("should pass through non-402 responses unchanged", async () => {
|
|
@@ -77,29 +178,11 @@ describe("wrapFetchWithPayment", () => {
|
|
|
77
178
|
});
|
|
78
179
|
|
|
79
180
|
it("should parse payment requirements from payment-required header when present", async () => {
|
|
80
|
-
const
|
|
81
|
-
|
|
181
|
+
const mockFetch = mockFetchWith(
|
|
182
|
+
paymentRequiredResponse(mock402ResponseData, "header"),
|
|
183
|
+
successResponse(),
|
|
82
184
|
);
|
|
83
185
|
|
|
84
|
-
const mock402Response = new Response(null, {
|
|
85
|
-
status: 402,
|
|
86
|
-
headers: {
|
|
87
|
-
"payment-required": encodedPaymentInfo,
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const mockSuccessResponse = new Response(
|
|
92
|
-
JSON.stringify({ success: true }),
|
|
93
|
-
{
|
|
94
|
-
status: 200,
|
|
95
|
-
},
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const mockFetch = vi
|
|
99
|
-
.fn()
|
|
100
|
-
.mockResolvedValueOnce(mock402Response)
|
|
101
|
-
.mockResolvedValueOnce(mockSuccessResponse);
|
|
102
|
-
|
|
103
186
|
const wrappedFetch = wrapFetchWithPayment(
|
|
104
187
|
mockFetch,
|
|
105
188
|
mockClient,
|
|
@@ -118,22 +201,11 @@ describe("wrapFetchWithPayment", () => {
|
|
|
118
201
|
});
|
|
119
202
|
|
|
120
203
|
it("should parse payment requirements from JSON body when payment-required header is absent", async () => {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const mockSuccessResponse = new Response(
|
|
126
|
-
JSON.stringify({ success: true }),
|
|
127
|
-
{
|
|
128
|
-
status: 200,
|
|
129
|
-
},
|
|
204
|
+
const mockFetch = mockFetchWith(
|
|
205
|
+
paymentRequiredResponse(mock402ResponseData, "body"),
|
|
206
|
+
successResponse(),
|
|
130
207
|
);
|
|
131
208
|
|
|
132
|
-
const mockFetch = vi
|
|
133
|
-
.fn()
|
|
134
|
-
.mockResolvedValueOnce(mock402Response)
|
|
135
|
-
.mockResolvedValueOnce(mockSuccessResponse);
|
|
136
|
-
|
|
137
209
|
const wrappedFetch = wrapFetchWithPayment(
|
|
138
210
|
mockFetch,
|
|
139
211
|
mockClient,
|
|
@@ -178,17 +250,7 @@ describe("wrapFetchWithPayment", () => {
|
|
|
178
250
|
},
|
|
179
251
|
});
|
|
180
252
|
|
|
181
|
-
const
|
|
182
|
-
JSON.stringify({ success: true }),
|
|
183
|
-
{
|
|
184
|
-
status: 200,
|
|
185
|
-
},
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
const mockFetch = vi
|
|
189
|
-
.fn()
|
|
190
|
-
.mockResolvedValueOnce(mock402Response)
|
|
191
|
-
.mockResolvedValueOnce(mockSuccessResponse);
|
|
253
|
+
const mockFetch = mockFetchWith(mock402Response, successResponse());
|
|
192
254
|
|
|
193
255
|
// Use maxValue to verify which payment requirements are used
|
|
194
256
|
// If header is used (500000), it should pass
|
|
@@ -206,47 +268,7 @@ describe("wrapFetchWithPayment", () => {
|
|
|
206
268
|
|
|
207
269
|
// Should succeed because header value (500000) is under maxValue (1000000)
|
|
208
270
|
expect(response.status).toBe(200);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
it("should parse payment requirements from payment-required header", async () => {
|
|
212
|
-
const encodedPaymentInfo = safeBase64Encode(
|
|
213
|
-
JSON.stringify(mock402ResponseData),
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
const mock402Response = new Response(null, {
|
|
217
|
-
status: 402,
|
|
218
|
-
headers: {
|
|
219
|
-
"payment-required": encodedPaymentInfo,
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
const mockSuccessResponse = new Response(
|
|
224
|
-
JSON.stringify({ success: true }),
|
|
225
|
-
{
|
|
226
|
-
status: 200,
|
|
227
|
-
},
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
const mockFetch = vi
|
|
231
|
-
.fn()
|
|
232
|
-
.mockResolvedValueOnce(mock402Response)
|
|
233
|
-
.mockResolvedValueOnce(mockSuccessResponse);
|
|
234
|
-
|
|
235
|
-
const wrappedFetch = wrapFetchWithPayment(
|
|
236
|
-
mockFetch,
|
|
237
|
-
mockClient,
|
|
238
|
-
mockWallet,
|
|
239
|
-
);
|
|
240
|
-
const response = await wrappedFetch("https://api.example.com/resource");
|
|
241
|
-
|
|
242
|
-
expect(response.status).toBe(200);
|
|
243
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
244
|
-
|
|
245
|
-
// Verify the second call includes the payment header for the version
|
|
246
|
-
const secondCallInit = mockFetch.mock.calls[1]?.[1] as RequestInit;
|
|
247
|
-
expect(secondCallInit.headers).toHaveProperty(
|
|
248
|
-
getPaymentRequestHeader(mock402ResponseData.x402Version),
|
|
249
|
-
);
|
|
271
|
+
expect(signedValues()).toEqual([500000n]);
|
|
250
272
|
});
|
|
251
273
|
|
|
252
274
|
it("should correctly decode a raw base64 encoded payment-required header", async () => {
|
|
@@ -275,17 +297,7 @@ describe("wrapFetchWithPayment", () => {
|
|
|
275
297
|
},
|
|
276
298
|
});
|
|
277
299
|
|
|
278
|
-
const
|
|
279
|
-
JSON.stringify({ success: true }),
|
|
280
|
-
{
|
|
281
|
-
status: 200,
|
|
282
|
-
},
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
const mockFetch = vi
|
|
286
|
-
.fn()
|
|
287
|
-
.mockResolvedValueOnce(mock402Response)
|
|
288
|
-
.mockResolvedValueOnce(mockSuccessResponse);
|
|
300
|
+
const mockFetch = mockFetchWith(mock402Response, successResponse());
|
|
289
301
|
|
|
290
302
|
// Use a wallet on Base (chain 8453) to match the payment requirements
|
|
291
303
|
const baseWallet = {
|
|
@@ -310,4 +322,1061 @@ describe("wrapFetchWithPayment", () => {
|
|
|
310
322
|
getPaymentRequestHeader(parsed.x402Version),
|
|
311
323
|
);
|
|
312
324
|
});
|
|
325
|
+
|
|
326
|
+
it("does not sign when a payment was already attempted for the request", async () => {
|
|
327
|
+
const mockFetch = mockFetchWith(
|
|
328
|
+
paymentRequiredResponse(mock402ResponseData, "header"),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
332
|
+
mockFetch,
|
|
333
|
+
mockClient,
|
|
334
|
+
mockWallet,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
await expect(
|
|
338
|
+
wrappedFetch("https://api.example.com/resource", {
|
|
339
|
+
__is402Retry: true,
|
|
340
|
+
} as RequestInit),
|
|
341
|
+
).rejects.toThrow("Payment already attempted");
|
|
342
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
343
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe("x402 v2 payment requirements", () => {
|
|
347
|
+
it("pays a canonical v2 header requirement that only specifies amount", async () => {
|
|
348
|
+
const paymentRequired = {
|
|
349
|
+
x402Version: 2,
|
|
350
|
+
resource: canonicalV2Resource,
|
|
351
|
+
accepts: [canonicalV2Requirement],
|
|
352
|
+
};
|
|
353
|
+
const mockFetch = mockFetchWith(
|
|
354
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
355
|
+
successResponse(),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
359
|
+
mockFetch,
|
|
360
|
+
mockClient,
|
|
361
|
+
mockWallet,
|
|
362
|
+
{ maxValue: 12345n },
|
|
363
|
+
);
|
|
364
|
+
const response = await wrappedFetch("https://api.example.com/resource");
|
|
365
|
+
|
|
366
|
+
expect(response.status).toBe(200);
|
|
367
|
+
expect(signedValues()).toEqual([12345n]);
|
|
368
|
+
|
|
369
|
+
const typedData = mockAccount.signTypedData.mock.calls[0]?.[0];
|
|
370
|
+
expect(typedData.primaryType).toBe("TransferWithAuthorization");
|
|
371
|
+
expect(typedData.domain.chainId).toBe(1);
|
|
372
|
+
expect(typedData.domain.verifyingContract).toBe(getAddress(ASSET));
|
|
373
|
+
expect(typedData.message.from).toBe(getAddress(OWNER));
|
|
374
|
+
expect(typedData.message.to).toBe(getAddress(PAY_TO));
|
|
375
|
+
|
|
376
|
+
const init = mockFetch.mock.calls[1]?.[1] as RequestInit;
|
|
377
|
+
expect(init.headers).toHaveProperty("PAYMENT-SIGNATURE");
|
|
378
|
+
expect(init.headers).not.toHaveProperty("X-PAYMENT");
|
|
379
|
+
|
|
380
|
+
const payment = sentPayment(mockFetch, 2);
|
|
381
|
+
expect(payment.x402Version).toBe(2);
|
|
382
|
+
expect(payment.payload.authorization.value).toBe("12345");
|
|
383
|
+
expect(payment.payload.signature).toBe(SIGNATURE);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("includes the raw accepted requirement and top-level scheme/network in the v2 payload", async () => {
|
|
387
|
+
const paymentRequired = {
|
|
388
|
+
x402Version: 2,
|
|
389
|
+
resource: canonicalV2Resource,
|
|
390
|
+
accepts: [canonicalV2Requirement],
|
|
391
|
+
};
|
|
392
|
+
const mockFetch = mockFetchWith(
|
|
393
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
394
|
+
successResponse(),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
398
|
+
mockFetch,
|
|
399
|
+
mockClient,
|
|
400
|
+
mockWallet,
|
|
401
|
+
);
|
|
402
|
+
await wrappedFetch("https://api.example.com/resource");
|
|
403
|
+
|
|
404
|
+
const payment = sentPayment(mockFetch, 2);
|
|
405
|
+
expect(payment.scheme).toBe("exact");
|
|
406
|
+
expect(payment.network).toBe("eip155:1");
|
|
407
|
+
expect(payment.accepted).toEqual(canonicalV2Requirement);
|
|
408
|
+
expect(payment.accepted).not.toHaveProperty("maxAmountRequired");
|
|
409
|
+
expect(payment.resource).toEqual(canonicalV2Resource);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("pays a canonical v2 JSON body requirement", async () => {
|
|
413
|
+
const paymentRequired = {
|
|
414
|
+
x402Version: 2,
|
|
415
|
+
accepts: [canonicalV2Requirement],
|
|
416
|
+
};
|
|
417
|
+
const mockFetch = mockFetchWith(
|
|
418
|
+
paymentRequiredResponse(paymentRequired, "body"),
|
|
419
|
+
successResponse(),
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
423
|
+
mockFetch,
|
|
424
|
+
mockClient,
|
|
425
|
+
mockWallet,
|
|
426
|
+
);
|
|
427
|
+
const response = await wrappedFetch("https://api.example.com/paid");
|
|
428
|
+
|
|
429
|
+
expect(response.status).toBe(200);
|
|
430
|
+
expect(signedValues()).toEqual([12345n]);
|
|
431
|
+
|
|
432
|
+
const payment = sentPayment(mockFetch, 2);
|
|
433
|
+
expect(payment.payload.authorization.value).toBe("12345");
|
|
434
|
+
expect(payment.accepted).toEqual(canonicalV2Requirement);
|
|
435
|
+
expect(payment.resource.url).toBe("https://api.example.com/paid");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it.each(["header", "body"] as const)(
|
|
439
|
+
"pays a v2 %s requirement with non-Latin-1 text",
|
|
440
|
+
async (mode) => {
|
|
441
|
+
const requirement = {
|
|
442
|
+
...canonicalV2Requirement,
|
|
443
|
+
description: "Premium — content ✓",
|
|
444
|
+
};
|
|
445
|
+
const resource = {
|
|
446
|
+
...canonicalV2Resource,
|
|
447
|
+
description: "Données 日本語",
|
|
448
|
+
};
|
|
449
|
+
const paymentRequired = {
|
|
450
|
+
x402Version: 2,
|
|
451
|
+
resource,
|
|
452
|
+
accepts: [requirement],
|
|
453
|
+
};
|
|
454
|
+
const mockFetch = mockFetchWith(
|
|
455
|
+
paymentRequiredResponse(paymentRequired, mode),
|
|
456
|
+
successResponse(),
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
460
|
+
mockFetch,
|
|
461
|
+
mockClient,
|
|
462
|
+
mockWallet,
|
|
463
|
+
);
|
|
464
|
+
const response = await wrappedFetch("https://api.example.com/resource");
|
|
465
|
+
|
|
466
|
+
expect(response.status).toBe(200);
|
|
467
|
+
const payment = sentPayment(mockFetch, 2);
|
|
468
|
+
expect(payment.accepted).toEqual(requirement);
|
|
469
|
+
expect(payment.resource).toEqual(resource);
|
|
470
|
+
expect(signedValues()).toEqual([12345n]);
|
|
471
|
+
},
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
it("pays a v2 envelope with v1-shaped requirements", async () => {
|
|
475
|
+
const paymentRequired = {
|
|
476
|
+
x402Version: 2,
|
|
477
|
+
error: "Payment required",
|
|
478
|
+
resource: { url: "https://api.example.com/resource" },
|
|
479
|
+
accepts: [mockPaymentRequirements],
|
|
480
|
+
};
|
|
481
|
+
const mockFetch = mockFetchWith(
|
|
482
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
483
|
+
successResponse(),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
487
|
+
mockFetch,
|
|
488
|
+
mockClient,
|
|
489
|
+
mockWallet,
|
|
490
|
+
);
|
|
491
|
+
const response = await wrappedFetch("https://api.example.com/resource");
|
|
492
|
+
|
|
493
|
+
expect(response.status).toBe(200);
|
|
494
|
+
expect(signedValues()).toEqual([1000000n]);
|
|
495
|
+
|
|
496
|
+
const payment = sentPayment(mockFetch, 2);
|
|
497
|
+
expect(payment.payload.authorization.value).toBe("1000000");
|
|
498
|
+
expect(payment.accepted).toEqual(mockPaymentRequirements);
|
|
499
|
+
expect(payment.resource).toEqual({
|
|
500
|
+
url: "https://api.example.com/resource",
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("pays a v1 body requirement with the v1 header and payload", async () => {
|
|
505
|
+
const paymentRequired = {
|
|
506
|
+
x402Version: 1,
|
|
507
|
+
accepts: [mockPaymentRequirements],
|
|
508
|
+
};
|
|
509
|
+
const mockFetch = mockFetchWith(
|
|
510
|
+
paymentRequiredResponse(paymentRequired, "body"),
|
|
511
|
+
successResponse(),
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
515
|
+
mockFetch,
|
|
516
|
+
mockClient,
|
|
517
|
+
mockWallet,
|
|
518
|
+
);
|
|
519
|
+
const response = await wrappedFetch("https://api.example.com/resource");
|
|
520
|
+
|
|
521
|
+
expect(response.status).toBe(200);
|
|
522
|
+
const init = mockFetch.mock.calls[1]?.[1] as RequestInit;
|
|
523
|
+
expect(init.headers).toHaveProperty("X-PAYMENT");
|
|
524
|
+
expect(init.headers).not.toHaveProperty("PAYMENT-SIGNATURE");
|
|
525
|
+
|
|
526
|
+
const payment = sentPayment(mockFetch, 1);
|
|
527
|
+
expect(Object.keys(payment).sort()).toEqual(
|
|
528
|
+
["network", "payload", "scheme", "x402Version"].sort(),
|
|
529
|
+
);
|
|
530
|
+
expect(payment.x402Version).toBe(1);
|
|
531
|
+
expect(payment.payload.authorization.value).toBe("1000000");
|
|
532
|
+
expect(signedValues()).toEqual([1000000n]);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("defaults to x402 v2 when the response has no x402Version", async () => {
|
|
536
|
+
const mockFetch = mockFetchWith(
|
|
537
|
+
paymentRequiredResponse(
|
|
538
|
+
{ resource: canonicalV2Resource, accepts: [canonicalV2Requirement] },
|
|
539
|
+
"header",
|
|
540
|
+
),
|
|
541
|
+
successResponse(),
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
545
|
+
mockFetch,
|
|
546
|
+
mockClient,
|
|
547
|
+
mockWallet,
|
|
548
|
+
);
|
|
549
|
+
await wrappedFetch("https://api.example.com/resource");
|
|
550
|
+
|
|
551
|
+
const init = mockFetch.mock.calls[1]?.[1] as RequestInit;
|
|
552
|
+
expect(init.headers).toHaveProperty("PAYMENT-SIGNATURE");
|
|
553
|
+
const payment = sentPayment(mockFetch, 2);
|
|
554
|
+
expect(payment.x402Version).toBe(2);
|
|
555
|
+
expect(payment.accepted).toEqual(canonicalV2Requirement);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe.each(["header", "body"] as const)("%s", (mode) => {
|
|
559
|
+
it.each([
|
|
560
|
+
[1, { amount: "1", maxAmountRequired: "1000000000" }],
|
|
561
|
+
[1, { amount: "1000000000", maxAmountRequired: "1" }],
|
|
562
|
+
[2, { amount: "1", maxAmountRequired: "1000000000" }],
|
|
563
|
+
[2, { amount: "1000000000", maxAmountRequired: "1" }],
|
|
564
|
+
])(
|
|
565
|
+
"rejects before signing when amounts differ (x402Version %i, %j)",
|
|
566
|
+
async (x402Version, amounts) => {
|
|
567
|
+
const paymentRequired = {
|
|
568
|
+
x402Version,
|
|
569
|
+
accepts: [{ ...mockPaymentRequirements, ...amounts }],
|
|
570
|
+
};
|
|
571
|
+
const mockFetch = mockFetchWith(
|
|
572
|
+
paymentRequiredResponse(paymentRequired, mode),
|
|
573
|
+
successResponse(),
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
577
|
+
mockFetch,
|
|
578
|
+
mockClient,
|
|
579
|
+
mockWallet,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
await expect(
|
|
583
|
+
wrappedFetch("https://api.example.com/resource"),
|
|
584
|
+
).rejects.toThrow("do not match");
|
|
585
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
586
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
587
|
+
},
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("rejects before signing when amounts differ with maxValue set", async () => {
|
|
592
|
+
const paymentRequired = {
|
|
593
|
+
x402Version: 2,
|
|
594
|
+
resource: canonicalV2Resource,
|
|
595
|
+
accepts: [
|
|
596
|
+
{
|
|
597
|
+
...canonicalV2Requirement,
|
|
598
|
+
amount: "1",
|
|
599
|
+
maxAmountRequired: "1000000000",
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
};
|
|
603
|
+
const mockFetch = mockFetchWith(
|
|
604
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
605
|
+
successResponse(),
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
609
|
+
mockFetch,
|
|
610
|
+
mockClient,
|
|
611
|
+
mockWallet,
|
|
612
|
+
{ maxValue: 10n },
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
await expect(
|
|
616
|
+
wrappedFetch("https://api.example.com/resource"),
|
|
617
|
+
).rejects.toThrow("do not match");
|
|
618
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
619
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("rejects when a requirement that would not be selected has mismatched amounts", async () => {
|
|
623
|
+
const paymentRequired = {
|
|
624
|
+
x402Version: 2,
|
|
625
|
+
resource: canonicalV2Resource,
|
|
626
|
+
accepts: [
|
|
627
|
+
canonicalV2Requirement,
|
|
628
|
+
{
|
|
629
|
+
...canonicalV2Requirement,
|
|
630
|
+
network: "eip155:8453",
|
|
631
|
+
amount: "1",
|
|
632
|
+
maxAmountRequired: "1000000000",
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
};
|
|
636
|
+
const mockFetch = mockFetchWith(
|
|
637
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
638
|
+
successResponse(),
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
642
|
+
mockFetch,
|
|
643
|
+
mockClient,
|
|
644
|
+
mockWallet,
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
await expect(
|
|
648
|
+
wrappedFetch("https://api.example.com/resource"),
|
|
649
|
+
).rejects.toThrow("do not match");
|
|
650
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
651
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it("accepts amount and maxAmountRequired when they are equal", async () => {
|
|
655
|
+
const paymentRequired = {
|
|
656
|
+
x402Version: 2,
|
|
657
|
+
resource: canonicalV2Resource,
|
|
658
|
+
accepts: [{ ...canonicalV2Requirement, maxAmountRequired: "12345" }],
|
|
659
|
+
};
|
|
660
|
+
const mockFetch = mockFetchWith(
|
|
661
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
662
|
+
successResponse(),
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
666
|
+
mockFetch,
|
|
667
|
+
mockClient,
|
|
668
|
+
mockWallet,
|
|
669
|
+
);
|
|
670
|
+
const response = await wrappedFetch("https://api.example.com/resource");
|
|
671
|
+
|
|
672
|
+
expect(response.status).toBe(200);
|
|
673
|
+
expect(signedValues()).toEqual([12345n]);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it("signs the canonical form of a zero-padded amount", async () => {
|
|
677
|
+
const requirement = {
|
|
678
|
+
...canonicalV2Requirement,
|
|
679
|
+
amount: "0100",
|
|
680
|
+
maxAmountRequired: "100",
|
|
681
|
+
};
|
|
682
|
+
const mockFetch = mockFetchWith(
|
|
683
|
+
paymentRequiredResponse(
|
|
684
|
+
{
|
|
685
|
+
x402Version: 2,
|
|
686
|
+
resource: canonicalV2Resource,
|
|
687
|
+
accepts: [requirement],
|
|
688
|
+
},
|
|
689
|
+
"header",
|
|
690
|
+
),
|
|
691
|
+
successResponse(),
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
695
|
+
mockFetch,
|
|
696
|
+
mockClient,
|
|
697
|
+
mockWallet,
|
|
698
|
+
{ maxValue: 100n },
|
|
699
|
+
);
|
|
700
|
+
await wrappedFetch("https://api.example.com/resource");
|
|
701
|
+
|
|
702
|
+
expect(signedValues()).toEqual([100n]);
|
|
703
|
+
const payment = sentPayment(mockFetch, 2);
|
|
704
|
+
expect(payment.payload.authorization.value).toBe("100");
|
|
705
|
+
expect(payment.accepted).toEqual(requirement);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("pays the maximum uint256 amount", async () => {
|
|
709
|
+
const max = (2n ** 256n - 1n).toString();
|
|
710
|
+
const mockFetch = mockFetchWith(
|
|
711
|
+
paymentRequiredResponse(
|
|
712
|
+
{
|
|
713
|
+
x402Version: 2,
|
|
714
|
+
resource: canonicalV2Resource,
|
|
715
|
+
accepts: [{ ...canonicalV2Requirement, amount: max }],
|
|
716
|
+
},
|
|
717
|
+
"header",
|
|
718
|
+
),
|
|
719
|
+
successResponse(),
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
723
|
+
mockFetch,
|
|
724
|
+
mockClient,
|
|
725
|
+
mockWallet,
|
|
726
|
+
);
|
|
727
|
+
await wrappedFetch("https://api.example.com/resource");
|
|
728
|
+
|
|
729
|
+
expect(signedValues()).toEqual([2n ** 256n - 1n]);
|
|
730
|
+
expect(sentPayment(mockFetch, 2).payload.authorization.value).toBe(max);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it.each([
|
|
734
|
+
["canonical v2", { x402Version: 2, accepts: [canonicalV2Requirement] }],
|
|
735
|
+
["v1-shaped", mock402ResponseData],
|
|
736
|
+
])(
|
|
737
|
+
"blocks a %s payment when maxValue is 0n",
|
|
738
|
+
async (_name, paymentRequired) => {
|
|
739
|
+
const mockFetch = mockFetchWith(
|
|
740
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
741
|
+
successResponse(),
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
745
|
+
mockFetch,
|
|
746
|
+
mockClient,
|
|
747
|
+
mockWallet,
|
|
748
|
+
{ maxValue: 0n },
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
await expect(
|
|
752
|
+
wrappedFetch("https://api.example.com/resource"),
|
|
753
|
+
).rejects.toThrow("Payment amount exceeds maximum allowed");
|
|
754
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
755
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
756
|
+
},
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
it("blocks a canonical v2 amount above maxValue", async () => {
|
|
760
|
+
const paymentRequired = {
|
|
761
|
+
x402Version: 2,
|
|
762
|
+
resource: canonicalV2Resource,
|
|
763
|
+
accepts: [canonicalV2Requirement],
|
|
764
|
+
};
|
|
765
|
+
const mockFetch = mockFetchWith(
|
|
766
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
770
|
+
mockFetch,
|
|
771
|
+
mockClient,
|
|
772
|
+
mockWallet,
|
|
773
|
+
{ maxValue: 12344n },
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
await expect(
|
|
777
|
+
wrappedFetch("https://api.example.com/resource"),
|
|
778
|
+
).rejects.toThrow("Payment amount exceeds maximum allowed");
|
|
779
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it.each([
|
|
783
|
+
["amount", { amount: "1e3" }],
|
|
784
|
+
["maxAmountRequired", { amount: undefined, maxAmountRequired: "1e3" }],
|
|
785
|
+
["amount", { amount: "-1" }],
|
|
786
|
+
["amount", { amount: "1.5" }],
|
|
787
|
+
["amount", { amount: "" }],
|
|
788
|
+
["amount", { amount: 1000 }],
|
|
789
|
+
["amount", { amount: (2n ** 256n).toString() }],
|
|
790
|
+
["amount", { amount: "0x10" }],
|
|
791
|
+
["maxAmountRequired", { amount: undefined, maxAmountRequired: "0x10" }],
|
|
792
|
+
["amount", { amount: " 100" }],
|
|
793
|
+
["amount", { amount: "100\n" }],
|
|
794
|
+
["amount", { amount: "+100" }],
|
|
795
|
+
["amount", { amount: "1".repeat(79) }],
|
|
796
|
+
["maxAmountRequired", { maxAmountRequired: "" }],
|
|
797
|
+
])("rejects a malformed %s (%j)", async (_field, override) => {
|
|
798
|
+
const paymentRequired = {
|
|
799
|
+
x402Version: 2,
|
|
800
|
+
resource: canonicalV2Resource,
|
|
801
|
+
accepts: [{ ...canonicalV2Requirement, ...override }],
|
|
802
|
+
};
|
|
803
|
+
const mockFetch = mockFetchWith(
|
|
804
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
808
|
+
mockFetch,
|
|
809
|
+
mockClient,
|
|
810
|
+
mockWallet,
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
await expect(
|
|
814
|
+
wrappedFetch("https://api.example.com/resource"),
|
|
815
|
+
).rejects.toThrow("Invalid payment requirements");
|
|
816
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
817
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("rejects a requirement with no amount", async () => {
|
|
821
|
+
const { amount: _amount, ...withoutAmount } = canonicalV2Requirement;
|
|
822
|
+
const paymentRequired = {
|
|
823
|
+
x402Version: 2,
|
|
824
|
+
resource: canonicalV2Resource,
|
|
825
|
+
accepts: [withoutAmount],
|
|
826
|
+
};
|
|
827
|
+
const mockFetch = mockFetchWith(
|
|
828
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
832
|
+
mockFetch,
|
|
833
|
+
mockClient,
|
|
834
|
+
mockWallet,
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
await expect(
|
|
838
|
+
wrappedFetch("https://api.example.com/resource"),
|
|
839
|
+
).rejects.toThrow("missing amount or maxAmountRequired");
|
|
840
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
describe("resource resolution", () => {
|
|
844
|
+
it("keeps the requirement resource and echoes the top-level resource", async () => {
|
|
845
|
+
const requirement = {
|
|
846
|
+
...mockPaymentRequirements,
|
|
847
|
+
resource: "https://api.example.com/a",
|
|
848
|
+
};
|
|
849
|
+
const topLevelResource = { url: "https://api.example.com/b" };
|
|
850
|
+
const mockFetch = mockFetchWith(
|
|
851
|
+
paymentRequiredResponse(
|
|
852
|
+
{
|
|
853
|
+
x402Version: 2,
|
|
854
|
+
resource: topLevelResource,
|
|
855
|
+
accepts: [requirement],
|
|
856
|
+
},
|
|
857
|
+
"header",
|
|
858
|
+
),
|
|
859
|
+
successResponse(),
|
|
860
|
+
);
|
|
861
|
+
const selector = vi.fn(
|
|
862
|
+
(requirements: Array<{ resource: string }>) => requirements[0],
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
866
|
+
mockFetch,
|
|
867
|
+
mockClient,
|
|
868
|
+
mockWallet,
|
|
869
|
+
{ paymentRequirementsSelector: selector as never },
|
|
870
|
+
);
|
|
871
|
+
await wrappedFetch("https://api.example.com/c");
|
|
872
|
+
|
|
873
|
+
expect(selector.mock.calls[0]?.[0]?.[0]?.resource).toBe(
|
|
874
|
+
"https://api.example.com/a",
|
|
875
|
+
);
|
|
876
|
+
const payment = sentPayment(mockFetch, 2);
|
|
877
|
+
expect(payment.resource).toEqual(topLevelResource);
|
|
878
|
+
expect(payment.accepted).toEqual(requirement);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it.each([
|
|
882
|
+
[
|
|
883
|
+
"a Request",
|
|
884
|
+
() => new Request("https://api.example.com/request"),
|
|
885
|
+
"https://api.example.com/request",
|
|
886
|
+
],
|
|
887
|
+
[
|
|
888
|
+
"a URL",
|
|
889
|
+
() => new URL("https://api.example.com/url"),
|
|
890
|
+
"https://api.example.com/url",
|
|
891
|
+
],
|
|
892
|
+
])(
|
|
893
|
+
"falls back to the URL of %s",
|
|
894
|
+
async (_name, createInput, expectedUrl) => {
|
|
895
|
+
const mockFetch = mockFetchWith(
|
|
896
|
+
paymentRequiredResponse(
|
|
897
|
+
{ x402Version: 2, accepts: [canonicalV2Requirement] },
|
|
898
|
+
"body",
|
|
899
|
+
),
|
|
900
|
+
successResponse(),
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
904
|
+
mockFetch,
|
|
905
|
+
mockClient,
|
|
906
|
+
mockWallet,
|
|
907
|
+
);
|
|
908
|
+
await wrappedFetch(createInput() as unknown as RequestInfo);
|
|
909
|
+
|
|
910
|
+
expect(sentPayment(mockFetch, 2).resource.url).toBe(expectedUrl);
|
|
911
|
+
},
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
it("rejects before signing when no resource URL can be resolved", async () => {
|
|
915
|
+
const mockFetch = mockFetchWith(
|
|
916
|
+
paymentRequiredResponse(
|
|
917
|
+
{ x402Version: 2, accepts: [canonicalV2Requirement] },
|
|
918
|
+
"body",
|
|
919
|
+
),
|
|
920
|
+
successResponse(),
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
924
|
+
mockFetch,
|
|
925
|
+
mockClient,
|
|
926
|
+
mockWallet,
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
await expect(wrappedFetch("/paid")).rejects.toThrow();
|
|
930
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
931
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
932
|
+
});
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
describe("custom selector", () => {
|
|
936
|
+
const paymentRequired = {
|
|
937
|
+
x402Version: 2,
|
|
938
|
+
resource: canonicalV2Resource,
|
|
939
|
+
accepts: [canonicalV2Requirement],
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
it("validates requirements returned by a custom selector", async () => {
|
|
943
|
+
const mockFetch = mockFetchWith(
|
|
944
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
948
|
+
mockFetch,
|
|
949
|
+
mockClient,
|
|
950
|
+
mockWallet,
|
|
951
|
+
{
|
|
952
|
+
paymentRequirementsSelector: (requirements) => {
|
|
953
|
+
const [first] = requirements;
|
|
954
|
+
return first ? { ...first, maxAmountRequired: "1e3" } : undefined;
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
await expect(
|
|
960
|
+
wrappedFetch("https://api.example.com/resource"),
|
|
961
|
+
).rejects.toThrow("Invalid payment requirements");
|
|
962
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
it("validates a parsed requirement modified in place by a custom selector", async () => {
|
|
966
|
+
const mockFetch = mockFetchWith(
|
|
967
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
971
|
+
mockFetch,
|
|
972
|
+
mockClient,
|
|
973
|
+
mockWallet,
|
|
974
|
+
{
|
|
975
|
+
paymentRequirementsSelector: (requirements) => {
|
|
976
|
+
const [first] = requirements;
|
|
977
|
+
if (first) {
|
|
978
|
+
first.maxAmountRequired = "0x10";
|
|
979
|
+
}
|
|
980
|
+
return first;
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
await expect(
|
|
986
|
+
wrappedFetch("https://api.example.com/resource"),
|
|
987
|
+
).rejects.toThrow("Invalid payment requirements");
|
|
988
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it("caps the amount of a copy returned by a custom selector", async () => {
|
|
992
|
+
const mockFetch = mockFetchWith(
|
|
993
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
994
|
+
);
|
|
995
|
+
|
|
996
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
997
|
+
mockFetch,
|
|
998
|
+
mockClient,
|
|
999
|
+
mockWallet,
|
|
1000
|
+
{
|
|
1001
|
+
maxValue: 15000n,
|
|
1002
|
+
paymentRequirementsSelector: (requirements) => {
|
|
1003
|
+
const [first] = requirements;
|
|
1004
|
+
return first
|
|
1005
|
+
? { ...first, maxAmountRequired: "20000" }
|
|
1006
|
+
: undefined;
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
await expect(
|
|
1012
|
+
wrappedFetch("https://api.example.com/resource"),
|
|
1013
|
+
).rejects.toThrow("Payment amount exceeds maximum allowed");
|
|
1014
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it("signs and echoes a copy returned by a custom selector", async () => {
|
|
1018
|
+
const mockFetch = mockFetchWith(
|
|
1019
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
1020
|
+
successResponse(),
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
1024
|
+
mockFetch,
|
|
1025
|
+
mockClient,
|
|
1026
|
+
mockWallet,
|
|
1027
|
+
{
|
|
1028
|
+
paymentRequirementsSelector: (requirements) => {
|
|
1029
|
+
const [first] = requirements;
|
|
1030
|
+
return first
|
|
1031
|
+
? { ...first, maxAmountRequired: "20000" }
|
|
1032
|
+
: undefined;
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
);
|
|
1036
|
+
await wrappedFetch("https://api.example.com/resource");
|
|
1037
|
+
|
|
1038
|
+
expect(signedValues()).toEqual([20000n]);
|
|
1039
|
+
const payment = sentPayment(mockFetch, 2);
|
|
1040
|
+
expect(payment.payload.authorization.value).toBe("20000");
|
|
1041
|
+
expect(payment.accepted.maxAmountRequired).toBe("20000");
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it("exposes the normalised amount as maxAmountRequired to selectors", async () => {
|
|
1045
|
+
const mockFetch = mockFetchWith(
|
|
1046
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
1047
|
+
successResponse(),
|
|
1048
|
+
);
|
|
1049
|
+
const selector = vi.fn(
|
|
1050
|
+
(requirements: Array<{ maxAmountRequired: string }>) =>
|
|
1051
|
+
requirements[0],
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
1055
|
+
mockFetch,
|
|
1056
|
+
mockClient,
|
|
1057
|
+
mockWallet,
|
|
1058
|
+
{
|
|
1059
|
+
paymentRequirementsSelector: selector as never,
|
|
1060
|
+
},
|
|
1061
|
+
);
|
|
1062
|
+
await wrappedFetch("https://api.example.com/resource");
|
|
1063
|
+
|
|
1064
|
+
expect(selector.mock.calls[0]?.[0]).toEqual([
|
|
1065
|
+
expect.objectContaining({
|
|
1066
|
+
maxAmountRequired: "12345",
|
|
1067
|
+
resource: canonicalV2Resource.url,
|
|
1068
|
+
description: "",
|
|
1069
|
+
mimeType: "",
|
|
1070
|
+
}),
|
|
1071
|
+
]);
|
|
1072
|
+
expect(signedValues()).toEqual([12345n]);
|
|
1073
|
+
expect(sentPayment(mockFetch, 2).accepted).toEqual(
|
|
1074
|
+
canonicalV2Requirement,
|
|
1075
|
+
);
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
describe("upto permit cache", () => {
|
|
1081
|
+
const uptoRequirement = {
|
|
1082
|
+
...canonicalV2Requirement,
|
|
1083
|
+
scheme: "upto",
|
|
1084
|
+
amount: "1000",
|
|
1085
|
+
extra: {
|
|
1086
|
+
name: "Test Token",
|
|
1087
|
+
version: "1",
|
|
1088
|
+
primaryType: "Permit",
|
|
1089
|
+
},
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
const cacheParams = {
|
|
1093
|
+
chainId: 1,
|
|
1094
|
+
asset: ASSET,
|
|
1095
|
+
owner: OWNER,
|
|
1096
|
+
spender: PAY_TO,
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
function createMemoryStorage(): AsyncStorage {
|
|
1100
|
+
const store = new Map<string, string>();
|
|
1101
|
+
return {
|
|
1102
|
+
getItem: async (key) => store.get(key) ?? null,
|
|
1103
|
+
setItem: async (key, value) => {
|
|
1104
|
+
store.set(key, value);
|
|
1105
|
+
},
|
|
1106
|
+
removeItem: async (key) => {
|
|
1107
|
+
store.delete(key);
|
|
1108
|
+
},
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
async function seedCachedPermit(
|
|
1113
|
+
storage: AsyncStorage,
|
|
1114
|
+
value: string,
|
|
1115
|
+
overrides: {
|
|
1116
|
+
deadlineOffset?: number;
|
|
1117
|
+
from?: string;
|
|
1118
|
+
to?: string;
|
|
1119
|
+
envelope?: Record<string, unknown>;
|
|
1120
|
+
} = {},
|
|
1121
|
+
) {
|
|
1122
|
+
const deadline = String(
|
|
1123
|
+
Math.floor(Date.now() / 1000) + (overrides.deadlineOffset ?? 3600),
|
|
1124
|
+
);
|
|
1125
|
+
await savePermitSignatureToCache(
|
|
1126
|
+
storage,
|
|
1127
|
+
cacheParams,
|
|
1128
|
+
{
|
|
1129
|
+
x402Version: 1,
|
|
1130
|
+
scheme: "upto",
|
|
1131
|
+
network: "eip155:1",
|
|
1132
|
+
...overrides.envelope,
|
|
1133
|
+
payload: {
|
|
1134
|
+
signature: CACHED_SIGNATURE,
|
|
1135
|
+
authorization: {
|
|
1136
|
+
from: overrides.from ?? OWNER,
|
|
1137
|
+
to: overrides.to ?? PAY_TO,
|
|
1138
|
+
value,
|
|
1139
|
+
validAfter: "0",
|
|
1140
|
+
validBefore: deadline,
|
|
1141
|
+
nonce: `0x${"00".repeat(32)}`,
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
} as Parameters<typeof savePermitSignatureToCache>[2],
|
|
1145
|
+
deadline,
|
|
1146
|
+
value,
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
beforeEach(() => {
|
|
1151
|
+
vi.mocked(allowance).mockResolvedValue(10n ** 18n);
|
|
1152
|
+
vi.mocked(nonces).mockResolvedValue(0n);
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it("reuses a cached permit that covers the amount, in the current envelope", async () => {
|
|
1156
|
+
const storage = createMemoryStorage();
|
|
1157
|
+
await seedCachedPermit(storage, "5000");
|
|
1158
|
+
|
|
1159
|
+
const paymentRequired = {
|
|
1160
|
+
x402Version: 2,
|
|
1161
|
+
resource: canonicalV2Resource,
|
|
1162
|
+
accepts: [uptoRequirement],
|
|
1163
|
+
};
|
|
1164
|
+
const mockFetch = mockFetchWith(
|
|
1165
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
1166
|
+
successResponse(),
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
1170
|
+
mockFetch,
|
|
1171
|
+
mockClient,
|
|
1172
|
+
mockWallet,
|
|
1173
|
+
{ storage },
|
|
1174
|
+
);
|
|
1175
|
+
const response = await wrappedFetch("https://api.example.com/resource");
|
|
1176
|
+
|
|
1177
|
+
expect(response.status).toBe(200);
|
|
1178
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
1179
|
+
|
|
1180
|
+
const payment = sentPayment(mockFetch, 2);
|
|
1181
|
+
expect(payment.x402Version).toBe(2);
|
|
1182
|
+
expect(payment.scheme).toBe("upto");
|
|
1183
|
+
expect(payment.network).toBe("eip155:1");
|
|
1184
|
+
expect(payment.accepted).toEqual(uptoRequirement);
|
|
1185
|
+
expect(payment.resource).toEqual(canonicalV2Resource);
|
|
1186
|
+
expect(payment.payload.signature).toBe(CACHED_SIGNATURE);
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it("signs a new permit when the cached permit is below the amount", async () => {
|
|
1190
|
+
const storage = createMemoryStorage();
|
|
1191
|
+
await seedCachedPermit(storage, "999");
|
|
1192
|
+
|
|
1193
|
+
const paymentRequired = {
|
|
1194
|
+
x402Version: 2,
|
|
1195
|
+
resource: canonicalV2Resource,
|
|
1196
|
+
accepts: [uptoRequirement],
|
|
1197
|
+
};
|
|
1198
|
+
const mockFetch = mockFetchWith(
|
|
1199
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
1200
|
+
successResponse(),
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
1204
|
+
mockFetch,
|
|
1205
|
+
mockClient,
|
|
1206
|
+
mockWallet,
|
|
1207
|
+
{ storage },
|
|
1208
|
+
);
|
|
1209
|
+
const response = await wrappedFetch("https://api.example.com/resource");
|
|
1210
|
+
|
|
1211
|
+
expect(response.status).toBe(200);
|
|
1212
|
+
expect(signedValues()).toEqual([1000n]);
|
|
1213
|
+
|
|
1214
|
+
const payment = sentPayment(mockFetch, 2);
|
|
1215
|
+
expect(payment.payload.signature).toBe(SIGNATURE);
|
|
1216
|
+
expect(payment.payload.authorization.value).toBe("1000");
|
|
1217
|
+
expect(payment.accepted).toEqual(uptoRequirement);
|
|
1218
|
+
|
|
1219
|
+
const cached = await getPermitSignatureFromCache(storage, cacheParams);
|
|
1220
|
+
expect(cached?.maxAmount).toBe("1000");
|
|
1221
|
+
expect(cached?.payload.x402Version).toBe(2);
|
|
1222
|
+
expect(cached?.payload.payload).toMatchObject({
|
|
1223
|
+
signature: SIGNATURE,
|
|
1224
|
+
authorization: { value: "1000" },
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
it.each([
|
|
1229
|
+
{ name: "a cached value equal to the amount", reused: true },
|
|
1230
|
+
{
|
|
1231
|
+
name: "a minAmountRequired covered by the allowance",
|
|
1232
|
+
allowance: 600n,
|
|
1233
|
+
minAmountRequired: "500",
|
|
1234
|
+
reused: true,
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
name: "a cached value above maxValue",
|
|
1238
|
+
maxValue: 1000n,
|
|
1239
|
+
cachedValue: "5000",
|
|
1240
|
+
reused: false,
|
|
1241
|
+
},
|
|
1242
|
+
{ name: "an expired deadline", deadlineOffset: -60, reused: false },
|
|
1243
|
+
{ name: "an allowance below the amount", allowance: 999n, reused: false },
|
|
1244
|
+
{
|
|
1245
|
+
name: "a minAmountRequired of zero",
|
|
1246
|
+
allowance: 0n,
|
|
1247
|
+
minAmountRequired: "0",
|
|
1248
|
+
reused: false,
|
|
1249
|
+
},
|
|
1250
|
+
{ name: "a malformed cached value", cachedValue: "1e3", reused: false },
|
|
1251
|
+
{
|
|
1252
|
+
name: "a permit for another spender",
|
|
1253
|
+
to: OTHER_ADDRESS,
|
|
1254
|
+
reused: false,
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
name: "a permit from another owner",
|
|
1258
|
+
from: OTHER_ADDRESS,
|
|
1259
|
+
reused: false,
|
|
1260
|
+
},
|
|
1261
|
+
])("handles $name", async (testCase) => {
|
|
1262
|
+
const storage = createMemoryStorage();
|
|
1263
|
+
await seedCachedPermit(storage, testCase.cachedValue ?? "1000", {
|
|
1264
|
+
deadlineOffset: testCase.deadlineOffset,
|
|
1265
|
+
from: testCase.from,
|
|
1266
|
+
to: testCase.to,
|
|
1267
|
+
});
|
|
1268
|
+
if (testCase.allowance !== undefined) {
|
|
1269
|
+
vi.mocked(allowance).mockResolvedValue(testCase.allowance);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const requirement = testCase.minAmountRequired
|
|
1273
|
+
? {
|
|
1274
|
+
...uptoRequirement,
|
|
1275
|
+
extra: {
|
|
1276
|
+
...uptoRequirement.extra,
|
|
1277
|
+
minAmountRequired: testCase.minAmountRequired,
|
|
1278
|
+
},
|
|
1279
|
+
}
|
|
1280
|
+
: uptoRequirement;
|
|
1281
|
+
const mockFetch = mockFetchWith(
|
|
1282
|
+
paymentRequiredResponse(
|
|
1283
|
+
{
|
|
1284
|
+
x402Version: 2,
|
|
1285
|
+
resource: canonicalV2Resource,
|
|
1286
|
+
accepts: [requirement],
|
|
1287
|
+
},
|
|
1288
|
+
"header",
|
|
1289
|
+
),
|
|
1290
|
+
successResponse(),
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
1294
|
+
mockFetch,
|
|
1295
|
+
mockClient,
|
|
1296
|
+
mockWallet,
|
|
1297
|
+
{ storage, maxValue: testCase.maxValue },
|
|
1298
|
+
);
|
|
1299
|
+
await wrappedFetch("https://api.example.com/resource");
|
|
1300
|
+
|
|
1301
|
+
const payment = sentPayment(mockFetch, 2);
|
|
1302
|
+
if (testCase.reused) {
|
|
1303
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
1304
|
+
expect(payment.payload.signature).toBe(CACHED_SIGNATURE);
|
|
1305
|
+
} else {
|
|
1306
|
+
expect(signedValues()).toEqual([1000n]);
|
|
1307
|
+
expect(payment.payload.signature).toBe(SIGNATURE);
|
|
1308
|
+
expect(payment.payload.authorization.value).toBe("1000");
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
it("reuses a v2 cached permit in a v1 envelope for a v1 requirement", async () => {
|
|
1313
|
+
const storage = createMemoryStorage();
|
|
1314
|
+
await seedCachedPermit(storage, "5000", {
|
|
1315
|
+
envelope: {
|
|
1316
|
+
x402Version: 2,
|
|
1317
|
+
accepted: uptoRequirement,
|
|
1318
|
+
resource: canonicalV2Resource,
|
|
1319
|
+
},
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
const v1Requirement = {
|
|
1323
|
+
...mockPaymentRequirements,
|
|
1324
|
+
scheme: "upto",
|
|
1325
|
+
maxAmountRequired: "1000",
|
|
1326
|
+
extra: uptoRequirement.extra,
|
|
1327
|
+
};
|
|
1328
|
+
const mockFetch = mockFetchWith(
|
|
1329
|
+
paymentRequiredResponse(
|
|
1330
|
+
{ x402Version: 1, accepts: [v1Requirement] },
|
|
1331
|
+
"body",
|
|
1332
|
+
),
|
|
1333
|
+
successResponse(),
|
|
1334
|
+
);
|
|
1335
|
+
|
|
1336
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
1337
|
+
mockFetch,
|
|
1338
|
+
mockClient,
|
|
1339
|
+
mockWallet,
|
|
1340
|
+
{ storage },
|
|
1341
|
+
);
|
|
1342
|
+
await wrappedFetch("https://api.example.com/resource");
|
|
1343
|
+
|
|
1344
|
+
expect(mockAccount.signTypedData).not.toHaveBeenCalled();
|
|
1345
|
+
const payment = sentPayment(mockFetch, 1);
|
|
1346
|
+
expect(Object.keys(payment).sort()).toEqual(
|
|
1347
|
+
["network", "payload", "scheme", "x402Version"].sort(),
|
|
1348
|
+
);
|
|
1349
|
+
expect(payment.x402Version).toBe(1);
|
|
1350
|
+
expect(payment.payload.signature).toBe(CACHED_SIGNATURE);
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
it("writes and clears the permit with the default storage when payment is rejected", async () => {
|
|
1354
|
+
const paymentRequired = {
|
|
1355
|
+
x402Version: 2,
|
|
1356
|
+
resource: canonicalV2Resource,
|
|
1357
|
+
accepts: [uptoRequirement],
|
|
1358
|
+
};
|
|
1359
|
+
const mockFetch = mockFetchWith(
|
|
1360
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
1361
|
+
paymentRequiredResponse(paymentRequired, "header"),
|
|
1362
|
+
);
|
|
1363
|
+
|
|
1364
|
+
const wrappedFetch = wrapFetchWithPayment(
|
|
1365
|
+
mockFetch,
|
|
1366
|
+
mockClient,
|
|
1367
|
+
mockWallet,
|
|
1368
|
+
);
|
|
1369
|
+
const response = await wrappedFetch("https://api.example.com/resource");
|
|
1370
|
+
|
|
1371
|
+
expect(response.status).toBe(402);
|
|
1372
|
+
const expectedKey = `x402:permit:1:${ASSET.toLowerCase()}:${OWNER.toLowerCase()}:${PAY_TO.toLowerCase()}`;
|
|
1373
|
+
expect(vi.mocked(webLocalStorage.setItem).mock.calls[0]?.[0]).toBe(
|
|
1374
|
+
expectedKey,
|
|
1375
|
+
);
|
|
1376
|
+
expect(vi.mocked(webLocalStorage.removeItem)).toHaveBeenCalledTimes(1);
|
|
1377
|
+
expect(vi.mocked(webLocalStorage.removeItem).mock.calls[0]?.[0]).toBe(
|
|
1378
|
+
expectedKey,
|
|
1379
|
+
);
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
313
1382
|
});
|