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
@@ -20,234 +20,236 @@ import { getInstalledModules } from "../__generated__/IModularCore/read/getInsta
20
20
  import * as BatchMetadataERC1155 from "../BatchMetadataERC1155/index.js";
21
21
  import * as ClaimableERC1155 from "./index.js";
22
22
 
23
- describe.runIf(process.env.TW_SECRET_KEY)("ModularClaimableERC1155", () => {
24
- let contract: ThirdwebContract;
25
- beforeAll(async () => {
26
- const address = await deployModularContract({
27
- account: TEST_ACCOUNT_A,
28
- chain: ANVIL_CHAIN,
29
- client: TEST_CLIENT,
30
- core: "ERC1155",
31
- modules: [
32
- ClaimableERC1155.module({
33
- primarySaleRecipient: TEST_ACCOUNT_A.address,
23
+ describe
24
+ .runIf(process.env.TW_SECRET_KEY)
25
+ .skip("ModularClaimableERC1155", () => {
26
+ let contract: ThirdwebContract;
27
+ beforeAll(async () => {
28
+ const address = await deployModularContract({
29
+ account: TEST_ACCOUNT_A,
30
+ chain: ANVIL_CHAIN,
31
+ client: TEST_CLIENT,
32
+ core: "ERC1155",
33
+ modules: [
34
+ ClaimableERC1155.module({
35
+ primarySaleRecipient: TEST_ACCOUNT_A.address,
36
+ }),
37
+ BatchMetadataERC1155.module(),
38
+ ],
39
+ params: {
40
+ contractURI: TEST_CONTRACT_URI,
41
+ name: "TestDropERC1155",
42
+ },
43
+ });
44
+ contract = getContract({
45
+ address,
46
+ chain: ANVIL_CHAIN,
47
+ client: TEST_CLIENT,
48
+ });
49
+ }, 120000);
50
+
51
+ it("should have erc1155 module", async () => {
52
+ const modules = await getInstalledModules({ contract });
53
+ expect(modules.length).toBe(2);
54
+ });
55
+
56
+ it("should upload metadata", async () => {
57
+ const transaction = BatchMetadataERC1155.uploadMetadata({
58
+ contract,
59
+ metadatas: Array.from(
60
+ { length: 10 },
61
+ (_, index) =>
62
+ `ipfs://QmP4JFzBhTGvb27GnJ9eL9vZGYpNBGjHnPudWndruiNERm/${index}`,
63
+ ),
64
+ });
65
+
66
+ await sendAndConfirmTransaction({
67
+ account: TEST_ACCOUNT_A,
68
+ transaction,
69
+ });
70
+ });
71
+
72
+ it("should not claim without claim conditions", async () => {
73
+ // should throw
74
+ await expect(
75
+ sendAndConfirmTransaction({
76
+ account: TEST_ACCOUNT_A,
77
+ transaction: ClaimableERC1155.mint({
78
+ contract,
79
+ quantity: 1,
80
+ to: TEST_ACCOUNT_A.address,
81
+ tokenId: 123123123213n,
82
+ }),
34
83
  }),
35
- BatchMetadataERC1155.module(),
36
- ],
37
- params: {
38
- contractURI: TEST_CONTRACT_URI,
39
- name: "TestDropERC1155",
40
- },
41
- });
42
- contract = getContract({
43
- address,
44
- chain: ANVIL_CHAIN,
45
- client: TEST_CLIENT,
46
- });
47
- }, 120000);
48
-
49
- it("should have erc1155 module", async () => {
50
- const modules = await getInstalledModules({ contract });
51
- expect(modules.length).toBe(2);
52
- });
53
-
54
- it("should upload metadata", async () => {
55
- const transaction = BatchMetadataERC1155.uploadMetadata({
56
- contract,
57
- metadatas: Array.from(
58
- { length: 10 },
59
- (_, index) =>
60
- `ipfs://QmP4JFzBhTGvb27GnJ9eL9vZGYpNBGjHnPudWndruiNERm/${index}`,
61
- ),
62
- });
63
-
64
- await sendAndConfirmTransaction({
65
- account: TEST_ACCOUNT_A,
66
- transaction,
84
+ ).rejects.toThrowError(/ClaimableOutOfTimeWindow/);
67
85
  });
68
- });
69
86
 
70
- it("should not claim without claim conditions", async () => {
71
- // should throw
72
- await expect(
73
- sendAndConfirmTransaction({
87
+ it("should claim tokens with claim conditions", async () => {
88
+ await sendAndConfirmTransaction({
74
89
  account: TEST_ACCOUNT_A,
75
- transaction: ClaimableERC1155.mint({
90
+ transaction: ClaimableERC1155.setClaimCondition({
76
91
  contract,
77
- quantity: 1,
78
- to: TEST_ACCOUNT_A.address,
79
- tokenId: 123123123213n,
92
+ maxClaimableSupply: "1",
93
+ pricePerToken: "0.1",
94
+ tokenId: 0n,
80
95
  }),
81
- }),
82
- ).rejects.toThrowError(/ClaimableOutOfTimeWindow/);
83
- });
96
+ });
97
+
98
+ // should throw if claiming more than supply
99
+ await expect(
100
+ sendAndConfirmTransaction({
101
+ account: TEST_ACCOUNT_A,
102
+ transaction: ClaimableERC1155.mint({
103
+ contract,
104
+ quantity: 1000,
105
+ to: TEST_ACCOUNT_A.address,
106
+ tokenId: 0n,
107
+ }),
108
+ }),
109
+ ).rejects.toThrowError(/ClaimableOutOfSupply/);
84
110
 
85
- it("should claim tokens with claim conditions", async () => {
86
- await sendAndConfirmTransaction({
87
- account: TEST_ACCOUNT_A,
88
- transaction: ClaimableERC1155.setClaimCondition({
111
+ let balance = await balanceOf({
89
112
  contract,
90
- maxClaimableSupply: "1",
91
- pricePerToken: "0.1",
113
+ owner: TEST_ACCOUNT_A.address,
92
114
  tokenId: 0n,
93
- }),
94
- });
115
+ });
95
116
 
96
- // should throw if claiming more than supply
97
- await expect(
98
- sendAndConfirmTransaction({
117
+ expect(balance).toBe(0n);
118
+
119
+ await sendAndConfirmTransaction({
99
120
  account: TEST_ACCOUNT_A,
100
121
  transaction: ClaimableERC1155.mint({
101
122
  contract,
102
- quantity: 1000,
123
+ quantity: 1,
103
124
  to: TEST_ACCOUNT_A.address,
104
125
  tokenId: 0n,
105
126
  }),
106
- }),
107
- ).rejects.toThrowError(/ClaimableOutOfSupply/);
127
+ });
108
128
 
109
- let balance = await balanceOf({
110
- contract,
111
- owner: TEST_ACCOUNT_A.address,
112
- tokenId: 0n,
113
- });
114
-
115
- expect(balance).toBe(0n);
116
-
117
- await sendAndConfirmTransaction({
118
- account: TEST_ACCOUNT_A,
119
- transaction: ClaimableERC1155.mint({
129
+ balance = await balanceOf({
120
130
  contract,
121
- quantity: 1,
122
- to: TEST_ACCOUNT_A.address,
131
+ owner: TEST_ACCOUNT_A.address,
123
132
  tokenId: 0n,
124
- }),
125
- });
133
+ });
134
+ expect(balance).toBe(1n);
126
135
 
127
- balance = await balanceOf({
128
- contract,
129
- owner: TEST_ACCOUNT_A.address,
130
- tokenId: 0n,
131
- });
132
- expect(balance).toBe(1n);
133
-
134
- const all = await getNFTs({
135
- contract,
136
- });
137
- expect(all.length).toBe(10);
138
-
139
- const owned = await getOwnedNFTs({
140
- address: TEST_ACCOUNT_A.address,
141
- contract,
142
- });
143
- expect(owned.length).toBe(1);
144
- expect(owned?.[0]?.metadata.name).toBe("Test 0");
145
- expect(owned?.[0]?.quantityOwned).toBe(1n);
146
- });
136
+ const all = await getNFTs({
137
+ contract,
138
+ });
139
+ expect(all.length).toBe(10);
147
140
 
148
- it("should claim tokens with allowlist", async () => {
149
- await sendAndConfirmTransaction({
150
- account: TEST_ACCOUNT_A,
151
- transaction: ClaimableERC1155.setClaimCondition({
152
- allowList: [TEST_ACCOUNT_A.address, TEST_ACCOUNT_B.address],
141
+ const owned = await getOwnedNFTs({
142
+ address: TEST_ACCOUNT_A.address,
153
143
  contract,
154
- maxClaimableSupply: "2",
155
- pricePerToken: "0.1",
156
- tokenId: 0n,
157
- }),
144
+ });
145
+ expect(owned.length).toBe(1);
146
+ expect(owned?.[0]?.metadata.name).toBe("Test 0");
147
+ expect(owned?.[0]?.quantityOwned).toBe(1n);
158
148
  });
159
149
 
160
- // should throw if not in allowlist
161
- await expect(
162
- sendAndConfirmTransaction({
163
- account: TEST_ACCOUNT_C,
164
- transaction: ClaimableERC1155.mint({
150
+ it("should claim tokens with allowlist", async () => {
151
+ await sendAndConfirmTransaction({
152
+ account: TEST_ACCOUNT_A,
153
+ transaction: ClaimableERC1155.setClaimCondition({
154
+ allowList: [TEST_ACCOUNT_A.address, TEST_ACCOUNT_B.address],
165
155
  contract,
166
- quantity: 1,
167
- to: TEST_ACCOUNT_C.address,
156
+ maxClaimableSupply: "2",
157
+ pricePerToken: "0.1",
168
158
  tokenId: 0n,
169
159
  }),
170
- }),
171
- ).rejects.toThrowError(/ClaimableNotInAllowlist/);
160
+ });
161
+
162
+ // should throw if not in allowlist
163
+ await expect(
164
+ sendAndConfirmTransaction({
165
+ account: TEST_ACCOUNT_C,
166
+ transaction: ClaimableERC1155.mint({
167
+ contract,
168
+ quantity: 1,
169
+ to: TEST_ACCOUNT_C.address,
170
+ tokenId: 0n,
171
+ }),
172
+ }),
173
+ ).rejects.toThrowError(/ClaimableNotInAllowlist/);
174
+
175
+ // should throw if in allowlist but over supply
176
+ await expect(
177
+ sendAndConfirmTransaction({
178
+ account: TEST_ACCOUNT_C,
179
+ transaction: ClaimableERC1155.mint({
180
+ contract,
181
+ quantity: 3,
182
+ to: TEST_ACCOUNT_C.address,
183
+ tokenId: 0n,
184
+ }),
185
+ }),
186
+ ).rejects.toThrowError(/ClaimableOutOfSupply/);
172
187
 
173
- // should throw if in allowlist but over supply
174
- await expect(
175
- sendAndConfirmTransaction({
188
+ // can claim to address in allowlist (regardless of sender)
189
+ await sendAndConfirmTransaction({
176
190
  account: TEST_ACCOUNT_C,
177
191
  transaction: ClaimableERC1155.mint({
178
192
  contract,
179
- quantity: 3,
180
- to: TEST_ACCOUNT_C.address,
193
+ quantity: 2,
194
+ to: TEST_ACCOUNT_B.address,
181
195
  tokenId: 0n,
182
196
  }),
183
- }),
184
- ).rejects.toThrowError(/ClaimableOutOfSupply/);
197
+ });
185
198
 
186
- // can claim to address in allowlist (regardless of sender)
187
- await sendAndConfirmTransaction({
188
- account: TEST_ACCOUNT_C,
189
- transaction: ClaimableERC1155.mint({
199
+ const balance = await balanceOf({
190
200
  contract,
191
- quantity: 2,
192
- to: TEST_ACCOUNT_B.address,
201
+ owner: TEST_ACCOUNT_B.address,
193
202
  tokenId: 0n,
194
- }),
203
+ });
204
+ expect(balance).toBe(2n);
195
205
  });
196
206
 
197
- const balance = await balanceOf({
198
- contract,
199
- owner: TEST_ACCOUNT_B.address,
200
- tokenId: 0n,
201
- });
202
- expect(balance).toBe(2n);
203
- });
207
+ it("should claim tokens with max per wallet", async () => {
208
+ await sendAndConfirmTransaction({
209
+ account: TEST_ACCOUNT_A,
210
+ transaction: ClaimableERC1155.setClaimCondition({
211
+ contract,
212
+ maxClaimablePerWallet: 1,
213
+ maxClaimableSupply: 10,
214
+ tokenId: 0n,
215
+ }),
216
+ });
204
217
 
205
- it("should claim tokens with max per wallet", async () => {
206
- await sendAndConfirmTransaction({
207
- account: TEST_ACCOUNT_A,
208
- transaction: ClaimableERC1155.setClaimCondition({
218
+ let balance = await balanceOf({
209
219
  contract,
210
- maxClaimablePerWallet: 1,
211
- maxClaimableSupply: 10,
220
+ owner: TEST_ACCOUNT_C.address,
212
221
  tokenId: 0n,
213
- }),
214
- });
215
-
216
- let balance = await balanceOf({
217
- contract,
218
- owner: TEST_ACCOUNT_C.address,
219
- tokenId: 0n,
220
- });
221
- expect(balance).toBe(0n);
222
+ });
223
+ expect(balance).toBe(0n);
224
+
225
+ // should throw if max per wallet is reached
226
+ await expect(
227
+ sendAndConfirmTransaction({
228
+ account: TEST_ACCOUNT_C,
229
+ transaction: ClaimableERC1155.mint({
230
+ contract,
231
+ quantity: 4,
232
+ to: TEST_ACCOUNT_C.address,
233
+ tokenId: 0n,
234
+ }),
235
+ }),
236
+ ).rejects.toThrowError(/ClaimableMaxMintPerWalletExceeded/);
222
237
 
223
- // should throw if max per wallet is reached
224
- await expect(
225
- sendAndConfirmTransaction({
238
+ await sendAndConfirmTransaction({
226
239
  account: TEST_ACCOUNT_C,
227
240
  transaction: ClaimableERC1155.mint({
228
241
  contract,
229
- quantity: 4,
242
+ quantity: 1,
230
243
  to: TEST_ACCOUNT_C.address,
231
244
  tokenId: 0n,
232
245
  }),
233
- }),
234
- ).rejects.toThrowError(/ClaimableMaxMintPerWalletExceeded/);
246
+ });
235
247
 
236
- await sendAndConfirmTransaction({
237
- account: TEST_ACCOUNT_C,
238
- transaction: ClaimableERC1155.mint({
248
+ balance = await balanceOf({
239
249
  contract,
240
- quantity: 1,
241
- to: TEST_ACCOUNT_C.address,
250
+ owner: TEST_ACCOUNT_C.address,
242
251
  tokenId: 0n,
243
- }),
244
- });
245
-
246
- balance = await balanceOf({
247
- contract,
248
- owner: TEST_ACCOUNT_C.address,
249
- tokenId: 0n,
252
+ });
253
+ expect(balance).toBe(1n);
250
254
  });
251
- expect(balance).toBe(1n);
252
255
  });
253
- });
@@ -17,7 +17,7 @@ import { deployModularContract } from "../../prebuilts/deploy-modular.js";
17
17
  import { getInstalledModules } from "../__generated__/IModularCore/read/getInstalledModules.js";
18
18
  import * as ClaimableERC20 from "./index.js";
19
19
 
20
- describe.runIf(process.env.TW_SECRET_KEY)("ModularDropERC20", () => {
20
+ describe.runIf(process.env.TW_SECRET_KEY).skip("ModularDropERC20", () => {
21
21
  let contract: ThirdwebContract;
22
22
  beforeAll(async () => {
23
23
  const address = await deployModularContract({
@@ -18,7 +18,7 @@ import { getInstalledModules } from "../__generated__/IModularCore/read/getInsta
18
18
  import * as BatchMetadataERC721 from "../BatchMetadataERC721/index.js";
19
19
  import * as ClaimableERC721 from "./index.js";
20
20
 
21
- describe.runIf(process.env.TW_SECRET_KEY)("ModularClaimableERC721", () => {
21
+ describe.runIf(process.env.TW_SECRET_KEY).skip("ModularClaimableERC721", () => {
22
22
  let contract: ThirdwebContract;
23
23
  beforeAll(async () => {
24
24
  const address = await deployModularContract({
@@ -8,7 +8,7 @@ import { readContract } from "../../transaction/read-contract.js";
8
8
  import { getInstalledModules } from "../modules/__generated__/IModularCore/read/getInstalledModules.js";
9
9
  import { deployPublishedContract } from "./deploy-published.js";
10
10
 
11
- describe.runIf(process.env.TW_SECRET_KEY)(
11
+ describe.runIf(process.env.TW_SECRET_KEY).skip(
12
12
  "deployref",
13
13
  {
14
14
  timeout: 120000,
@@ -4,7 +4,10 @@ import { useMutation } from "@tanstack/react-query";
4
4
  import type { ThirdwebClient } from "../../../../client/client.js";
5
5
  import type { AsyncStorage } from "../../../../utils/storage/AsyncStorage.js";
6
6
  import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
7
- import { wrapFetchWithPayment } from "../../../../x402/fetchWithPayment.js";
7
+ import {
8
+ getRequestUrl,
9
+ wrapFetchWithPayment,
10
+ } from "../../../../x402/fetchWithPayment.js";
8
11
  import type { RequestedPaymentRequirements } from "../../../../x402/schemas.js";
9
12
  import type { PaymentRequiredResult } from "../../../../x402/types.js";
10
13
  import { useActiveWallet } from "../wallets/useActiveWallet.js";
@@ -24,6 +27,7 @@ export type UseFetchWithPaymentOptions = {
24
27
 
25
28
  type ShowErrorModalCallback = (data: {
26
29
  errorData: PaymentRequiredResult["responseBody"];
30
+ requestUrl?: string;
27
31
  onRetry: () => void;
28
32
  onCancel: () => void;
29
33
  }) => void;
@@ -107,6 +111,7 @@ export function useFetchWithPaymentCore(
107
111
  return new Promise<unknown>((resolve, reject) => {
108
112
  showErrorModal({
109
113
  errorData: errorBody,
114
+ requestUrl: getRequestUrl(input),
110
115
  onRetry: async () => {
111
116
  // Retry the entire fetch+error handling logic recursively
112
117
  // Pass currentWallet to avoid re-showing connect modal with stale wallet state
@@ -27,7 +27,7 @@ export type { UseFetchWithPaymentOptions };
27
27
  *
28
28
  * @param client - The thirdweb client used to access RPC infrastructure
29
29
  * @param options - Optional configuration for payment handling
30
- * @param options.maxValue - The maximum allowed payment amount in base units
30
+ * @param options.maxValue - The maximum allowed payment amount in base units. `0n` only allows zero-amount payments
31
31
  * @param options.paymentRequirementsSelector - Custom function to select payment requirements from available options
32
32
  * @param options.parseAs - How to parse the response: "json" (default), "text", or "raw"
33
33
  * @param options.storage - Storage for caching permit signatures (for "upto" scheme). Provide your own AsyncStorage implementation for React Native.
@@ -78,7 +78,7 @@ type UseFetchWithPaymentConfig = UseFetchWithPaymentOptions & {
78
78
  *
79
79
  * @param client - The thirdweb client used to access RPC infrastructure
80
80
  * @param options - Optional configuration for payment handling
81
- * @param options.maxValue - The maximum allowed payment amount in base units
81
+ * @param options.maxValue - The maximum allowed payment amount in base units. `0n` only allows zero-amount payments
82
82
  * @param options.paymentRequirementsSelector - Custom function to select payment requirements from available options
83
83
  * @param options.parseAs - How to parse the response: "json" (default), "text", or "raw"
84
84
  * @param options.uiEnabled - Whether to show the UI for connection, funding or payment retries (defaults to true). Set to false to handle errors yourself
@@ -196,6 +196,7 @@ export function useFetchWithPayment(
196
196
  const showErrorModal = showModal
197
197
  ? (data: {
198
198
  errorData: Parameters<typeof PaymentErrorModal>[0]["errorData"];
199
+ requestUrl?: string;
199
200
  onRetry: () => void;
200
201
  onCancel: () => void;
201
202
  }) => {
@@ -203,6 +204,7 @@ export function useFetchWithPayment(
203
204
  <PaymentErrorModal
204
205
  client={client}
205
206
  errorData={data.errorData}
207
+ requestUrl={data.requestUrl}
206
208
  onCancel={() => {
207
209
  setRootEl(null);
208
210
  data.onCancel();
@@ -0,0 +1,117 @@
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
+ import { renderHook, waitFor } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { TEST_CLIENT } from "~test/test-clients.js";
6
+ import type { TokenWithPrices } from "../../../../../bridge/index.js";
7
+ import { getToken } from "../../../../../pay/convert/get-token.js";
8
+ import { useTokenQuery } from "./token-query.js";
9
+
10
+ vi.mock("../../../../../pay/convert/get-token.js", () => ({
11
+ getToken: vi.fn(),
12
+ }));
13
+
14
+ const TOKEN_ADDRESS = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";
15
+ const CHAIN_ID = 137;
16
+
17
+ const MOCK_TOKEN = {
18
+ address: TOKEN_ADDRESS,
19
+ chainId: CHAIN_ID,
20
+ decimals: 6,
21
+ name: "USD Coin",
22
+ prices: { USD: 1 },
23
+ symbol: "USDC",
24
+ } as TokenWithPrices;
25
+
26
+ const createWrapper = () => {
27
+ const queryClient = new QueryClient({
28
+ defaultOptions: {
29
+ queries: {
30
+ retry: false,
31
+ },
32
+ },
33
+ });
34
+ return ({ children }: { children: ReactNode }) => (
35
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
36
+ );
37
+ };
38
+
39
+ describe("useTokenQuery", () => {
40
+ beforeEach(() => {
41
+ vi.mocked(getToken).mockReset();
42
+ });
43
+
44
+ it("returns success when getToken resolves", async () => {
45
+ vi.mocked(getToken).mockResolvedValue(MOCK_TOKEN);
46
+
47
+ const { result } = renderHook(
48
+ () =>
49
+ useTokenQuery({
50
+ chainId: CHAIN_ID,
51
+ client: TEST_CLIENT,
52
+ tokenAddress: TOKEN_ADDRESS,
53
+ }),
54
+ { wrapper: createWrapper() },
55
+ );
56
+
57
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
58
+ expect(result.current.data).toEqual({
59
+ token: MOCK_TOKEN,
60
+ type: "success",
61
+ });
62
+ });
63
+
64
+ it("returns unsupported_token when getToken throws a not-supported Error", async () => {
65
+ vi.mocked(getToken).mockRejectedValue(new Error("Token not supported"));
66
+
67
+ const { result } = renderHook(
68
+ () =>
69
+ useTokenQuery({
70
+ chainId: CHAIN_ID,
71
+ client: TEST_CLIENT,
72
+ tokenAddress: TOKEN_ADDRESS,
73
+ }),
74
+ { wrapper: createWrapper() },
75
+ );
76
+
77
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
78
+ expect(result.current.data).toEqual({ type: "unsupported_token" });
79
+ expect(result.current.isError).toBe(false);
80
+ });
81
+
82
+ it("surfaces 401 failures as query errors instead of unsupported_token", async () => {
83
+ vi.mocked(getToken).mockRejectedValue(new Error("401 Unauthorized"));
84
+
85
+ const { result } = renderHook(
86
+ () =>
87
+ useTokenQuery({
88
+ chainId: CHAIN_ID,
89
+ client: TEST_CLIENT,
90
+ tokenAddress: TOKEN_ADDRESS,
91
+ }),
92
+ { wrapper: createWrapper() },
93
+ );
94
+
95
+ await waitFor(() => expect(result.current.isError).toBe(true));
96
+ expect(result.current.data).toBeUndefined();
97
+ expect(result.current.error).toBeInstanceOf(Error);
98
+ expect((result.current.error as Error).message).toBe("401 Unauthorized");
99
+ });
100
+
101
+ it("surfaces non-Error rejections as query errors instead of unsupported_token", async () => {
102
+ vi.mocked(getToken).mockRejectedValue("timeout");
103
+
104
+ const { result } = renderHook(
105
+ () =>
106
+ useTokenQuery({
107
+ chainId: CHAIN_ID,
108
+ client: TEST_CLIENT,
109
+ tokenAddress: TOKEN_ADDRESS,
110
+ }),
111
+ { wrapper: createWrapper() },
112
+ );
113
+
114
+ await waitFor(() => expect(result.current.isError).toBe(true));
115
+ expect(result.current.data).toBeUndefined();
116
+ });
117
+ });
@@ -27,7 +27,10 @@ export function useTokenQuery(params: {
27
27
  tokenAddress,
28
28
  params.chainId,
29
29
  ).catch((err) => {
30
- err.message.includes("not supported") ? undefined : Promise.reject(err);
30
+ if (err instanceof Error && err.message.includes("not supported")) {
31
+ return undefined;
32
+ }
33
+ throw err;
31
34
  });
32
35
 
33
36
  if (!token) {
@@ -5,6 +5,7 @@ import type { ThirdwebClient } from "../../../../client/client.js";
5
5
  import {
6
6
  extractEvmChainId,
7
7
  networkToCaip2ChainId,
8
+ parsePaymentRequirementsForDisplay,
8
9
  type RequestedPaymentRequirements,
9
10
  } from "../../../../x402/schemas.js";
10
11
  import type { PaymentRequiredResult } from "../../../../x402/types.js";
@@ -25,6 +26,7 @@ import { Text } from "../components/text.js";
25
26
  type PaymentErrorModalProps = {
26
27
  client: ThirdwebClient;
27
28
  errorData: PaymentRequiredResult["responseBody"];
29
+ requestUrl?: string;
28
30
  onRetry: () => void;
29
31
  onCancel: () => void;
30
32
  theme: Theme | "light" | "dark";
@@ -54,6 +56,7 @@ export function PaymentErrorModal(props: PaymentErrorModalProps) {
54
56
  const {
55
57
  client,
56
58
  errorData,
59
+ requestUrl,
57
60
  onRetry,
58
61
  onCancel,
59
62
  theme,
@@ -66,13 +69,15 @@ export function PaymentErrorModal(props: PaymentErrorModalProps) {
66
69
 
67
70
  // Extract chain and token info from errorData for BuyWidget
68
71
  const getBuyWidgetConfig = () => {
69
- if (!errorData.accepts || errorData.accepts.length === 0) {
72
+ // Get payment requirements from errorData
73
+ const parsedPaymentRequirements = parsePaymentRequirementsForDisplay(
74
+ errorData,
75
+ requestUrl,
76
+ );
77
+ if (parsedPaymentRequirements.length === 0) {
70
78
  return null;
71
79
  }
72
80
 
73
- // Get payment requirements from errorData
74
- const parsedPaymentRequirements = errorData.accepts;
75
-
76
81
  // Get the current chain from wallet
77
82
  const currentChain = wallet?.getChain();
78
83
  const currentChainId = currentChain?.id;