@armory-sh/base 0.2.28 → 0.2.29
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/client-hooks-runtime.d.ts +9 -0
- package/dist/encoding/x402.d.ts +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/index.d.ts +29 -22
- package/dist/index.js +1121 -689
- package/dist/payment-client.d.ts +1 -1
- package/dist/payment-requirements.d.ts +29 -0
- package/dist/protocol.d.ts +33 -0
- package/dist/types/api.d.ts +1 -1
- package/dist/types/hooks.d.ts +17 -1
- package/dist/types/protocol.d.ts +1 -1
- package/dist/types/wallet.d.ts +24 -0
- package/dist/utils/base64.d.ts +4 -0
- package/dist/utils/x402.d.ts +1 -1
- package/dist/validation.d.ts +1 -1
- package/package.json +15 -2
- package/src/abi/erc20.ts +84 -0
- package/src/client-hooks-runtime.ts +153 -0
- package/src/data/tokens.ts +199 -0
- package/src/eip712.ts +108 -0
- package/src/encoding/x402.ts +205 -0
- package/src/encoding.ts +98 -0
- package/src/errors.ts +23 -0
- package/src/index.ts +323 -0
- package/src/payment-client.ts +201 -0
- package/src/payment-requirements.ts +300 -0
- package/src/protocol.ts +57 -0
- package/src/types/api.ts +304 -0
- package/src/types/hooks.ts +85 -0
- package/src/types/networks.ts +175 -0
- package/src/types/protocol.ts +182 -0
- package/src/types/v2.ts +282 -0
- package/src/types/wallet.ts +30 -0
- package/src/types/x402.ts +151 -0
- package/src/utils/base64.ts +48 -0
- package/src/utils/routes.ts +240 -0
- package/src/utils/utils/index.ts +7 -0
- package/src/utils/utils/mock-facilitator.ts +184 -0
- package/src/utils/x402.ts +147 -0
- package/src/validation.ts +654 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive validation for Armory configurations
|
|
3
|
+
* Ensures networks, tokens, and facilitators are compatible
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CustomToken, NetworkConfig } from "./types/networks";
|
|
7
|
+
import type { CAIPAssetId } from "./types/v2";
|
|
8
|
+
|
|
9
|
+
const isEvmAddress = (value: string): boolean =>
|
|
10
|
+
/^0x[a-fA-F0-9]{40}$/.test(value);
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
FacilitatorConfig,
|
|
14
|
+
NetworkId,
|
|
15
|
+
PaymentErrorCode,
|
|
16
|
+
ResolvedFacilitator,
|
|
17
|
+
ResolvedNetwork,
|
|
18
|
+
ResolvedPaymentConfig,
|
|
19
|
+
ResolvedToken,
|
|
20
|
+
TokenId,
|
|
21
|
+
ValidationError,
|
|
22
|
+
} from "./types/api";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
getAllCustomTokens,
|
|
26
|
+
getCustomToken,
|
|
27
|
+
getNetworkByChainId,
|
|
28
|
+
getNetworkConfig,
|
|
29
|
+
NETWORKS,
|
|
30
|
+
} from "./types/networks";
|
|
31
|
+
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════
|
|
33
|
+
// Error Creation
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
export const createError = (
|
|
37
|
+
code: PaymentErrorCode,
|
|
38
|
+
message: string,
|
|
39
|
+
details?: Partial<ValidationError>,
|
|
40
|
+
): ValidationError => ({
|
|
41
|
+
code,
|
|
42
|
+
message,
|
|
43
|
+
...details,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ═══════════════════════════════════════════════════════════════
|
|
47
|
+
// Network Resolution & Validation
|
|
48
|
+
// ═══════════════════════════════════════════════════════════════
|
|
49
|
+
|
|
50
|
+
export const normalizeNetworkName = (name: string): string =>
|
|
51
|
+
name
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.replace(/\s+/g, "-")
|
|
54
|
+
.replace("-mainnet", "")
|
|
55
|
+
.replace(/-mainnet/, "")
|
|
56
|
+
.replace("mainnet-", "")
|
|
57
|
+
.replace(/-sepolia$/, "-sepolia")
|
|
58
|
+
.replace(/^-|-$/g, "");
|
|
59
|
+
|
|
60
|
+
export const resolveNetwork = (
|
|
61
|
+
input: unknown,
|
|
62
|
+
): ResolvedNetwork | ValidationError => {
|
|
63
|
+
if (typeof input === "object" && input !== null && "chainId" in input) {
|
|
64
|
+
const config = input as NetworkConfig;
|
|
65
|
+
if (!config.chainId || !config.usdcAddress || !config.caip2Id) {
|
|
66
|
+
return createError("INVALID_CAIP_FORMAT", "Invalid network config", {
|
|
67
|
+
value: config,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
input: input as unknown as NetworkId,
|
|
72
|
+
config,
|
|
73
|
+
caip2: config.caip2Id as `eip155:${number}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof input === "number") {
|
|
78
|
+
const config = getNetworkByChainId(input);
|
|
79
|
+
if (!config) {
|
|
80
|
+
const validChainIds = Object.values(NETWORKS).map((n) => n.chainId);
|
|
81
|
+
return createError("UNKNOWN_NETWORK", `Unknown chain ID: ${input}`, {
|
|
82
|
+
value: input,
|
|
83
|
+
validOptions: validChainIds.map(String),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
input,
|
|
88
|
+
config,
|
|
89
|
+
caip2: config.caip2Id as `eip155:${number}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof input === "string") {
|
|
94
|
+
if (input.startsWith("eip155:")) {
|
|
95
|
+
const parts = input.split(":");
|
|
96
|
+
if (parts.length !== 2 || Number.isNaN(Number(parts[1]))) {
|
|
97
|
+
return createError(
|
|
98
|
+
"INVALID_CAIP_FORMAT",
|
|
99
|
+
`Invalid CAIP-2 format: ${input}`,
|
|
100
|
+
{ value: input },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const chainId = Number(parts[1]);
|
|
104
|
+
const config = getNetworkByChainId(chainId);
|
|
105
|
+
if (!config) {
|
|
106
|
+
return createError(
|
|
107
|
+
"UNKNOWN_NETWORK",
|
|
108
|
+
`Unknown CAIP-2 network: ${input}`,
|
|
109
|
+
{ value: input },
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
input,
|
|
114
|
+
config,
|
|
115
|
+
caip2: input as `eip155:${number}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const normalizedName = normalizeNetworkName(input);
|
|
120
|
+
const config = getNetworkConfig(normalizedName);
|
|
121
|
+
if (!config) {
|
|
122
|
+
const validNames = Object.keys(NETWORKS);
|
|
123
|
+
return createError("UNKNOWN_NETWORK", `Unknown network: "${input}"`, {
|
|
124
|
+
value: input,
|
|
125
|
+
validOptions: validNames,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
input,
|
|
130
|
+
config,
|
|
131
|
+
caip2: config.caip2Id as `eip155:${number}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return createError(
|
|
136
|
+
"UNKNOWN_NETWORK",
|
|
137
|
+
`Invalid network identifier type: ${typeof input}`,
|
|
138
|
+
{ value: input },
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const getAvailableNetworks = (): string[] => Object.keys(NETWORKS);
|
|
143
|
+
|
|
144
|
+
// ═══════════════════════════════════════════════════════════════
|
|
145
|
+
// Token Resolution & Validation
|
|
146
|
+
// ═══════════════════════════════════════════════════════════════
|
|
147
|
+
|
|
148
|
+
const normalizeTokenSymbol = (symbol: string): string => symbol.toUpperCase();
|
|
149
|
+
|
|
150
|
+
export const resolveToken = (
|
|
151
|
+
input: unknown,
|
|
152
|
+
network?: ResolvedNetwork,
|
|
153
|
+
): ResolvedToken | ValidationError => {
|
|
154
|
+
if (
|
|
155
|
+
typeof input === "object" &&
|
|
156
|
+
input !== null &&
|
|
157
|
+
"contractAddress" in input
|
|
158
|
+
) {
|
|
159
|
+
const config = input as CustomToken;
|
|
160
|
+
if (!config.chainId || !config.contractAddress || !config.symbol) {
|
|
161
|
+
return createError("VALIDATION_FAILED", "Invalid token config", {
|
|
162
|
+
value: config,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (network && config.chainId !== network.config.chainId) {
|
|
167
|
+
return createError(
|
|
168
|
+
"TOKEN_NOT_ON_NETWORK",
|
|
169
|
+
`Token ${config.symbol} is on chain ${config.chainId}, but expected chain ${network.config.chainId}`,
|
|
170
|
+
{ value: config },
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const tokenNetwork = resolveNetwork(config.chainId);
|
|
175
|
+
if ("code" in tokenNetwork) {
|
|
176
|
+
return tokenNetwork;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const caipAsset =
|
|
180
|
+
`eip155:${config.chainId}/erc20:${config.contractAddress}` as CAIPAssetId;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
input: input as unknown as TokenId,
|
|
184
|
+
config,
|
|
185
|
+
caipAsset,
|
|
186
|
+
network: tokenNetwork,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (typeof input === "string") {
|
|
191
|
+
if (isEvmAddress(input)) {
|
|
192
|
+
const contractAddress = input as `0x${string}`;
|
|
193
|
+
|
|
194
|
+
if (network) {
|
|
195
|
+
const customToken = getCustomToken(
|
|
196
|
+
network.config.chainId,
|
|
197
|
+
contractAddress,
|
|
198
|
+
);
|
|
199
|
+
const config = customToken || {
|
|
200
|
+
symbol: "CUSTOM",
|
|
201
|
+
name: "Custom Token",
|
|
202
|
+
version: "1",
|
|
203
|
+
chainId: network.config.chainId,
|
|
204
|
+
contractAddress,
|
|
205
|
+
decimals: 18,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const caipAsset: CAIPAssetId = `eip155:${network.config.chainId}/erc20:${contractAddress}`;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
input,
|
|
212
|
+
config,
|
|
213
|
+
caipAsset,
|
|
214
|
+
network,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const customTokens = getAllCustomTokens();
|
|
219
|
+
const matchingToken = customTokens.find(
|
|
220
|
+
(t) =>
|
|
221
|
+
t.contractAddress.toLowerCase() === contractAddress.toLowerCase(),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (matchingToken) {
|
|
225
|
+
const tokenNetwork = resolveNetwork(matchingToken.chainId);
|
|
226
|
+
if ("code" in tokenNetwork) {
|
|
227
|
+
return tokenNetwork;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const caipAsset: CAIPAssetId = `eip155:${matchingToken.chainId}/erc20:${contractAddress}`;
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
input,
|
|
234
|
+
config: matchingToken,
|
|
235
|
+
caipAsset,
|
|
236
|
+
network: tokenNetwork,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return createError(
|
|
241
|
+
"UNKNOWN_TOKEN",
|
|
242
|
+
`Token address "${contractAddress}" not found in registry. Please specify a network.`,
|
|
243
|
+
{ value: input, validOptions: ["Specify network parameter"] },
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (input.includes("/erc20:")) {
|
|
248
|
+
const parts = input.split("/");
|
|
249
|
+
if (parts.length !== 2) {
|
|
250
|
+
return createError(
|
|
251
|
+
"INVALID_CAIP_FORMAT",
|
|
252
|
+
`Invalid CAIP Asset ID format: ${input}`,
|
|
253
|
+
{ value: input },
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const chainId = Number(parts[0].split(":")[1]);
|
|
257
|
+
const contractAddress = parts[1].split(":")[1] as `0x${string}`;
|
|
258
|
+
|
|
259
|
+
const tokenNetwork = resolveNetwork(chainId);
|
|
260
|
+
if ("code" in tokenNetwork) {
|
|
261
|
+
return tokenNetwork;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const customToken = getCustomToken(chainId, contractAddress);
|
|
265
|
+
const config = customToken || {
|
|
266
|
+
symbol: "CUSTOM",
|
|
267
|
+
name: "Custom Token",
|
|
268
|
+
version: "1",
|
|
269
|
+
chainId,
|
|
270
|
+
contractAddress,
|
|
271
|
+
decimals: 18,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
input,
|
|
276
|
+
config,
|
|
277
|
+
caipAsset: input as `eip155:${number}/erc20:${string}`,
|
|
278
|
+
network: tokenNetwork,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const normalizedSymbol = normalizeTokenSymbol(input);
|
|
283
|
+
|
|
284
|
+
if (network) {
|
|
285
|
+
const customTokens = getAllCustomTokens();
|
|
286
|
+
const matchingToken = customTokens.find(
|
|
287
|
+
(t) =>
|
|
288
|
+
t.symbol?.toUpperCase() === normalizedSymbol &&
|
|
289
|
+
t.chainId === network.config.chainId,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (matchingToken) {
|
|
293
|
+
return {
|
|
294
|
+
input,
|
|
295
|
+
config: matchingToken,
|
|
296
|
+
caipAsset:
|
|
297
|
+
`eip155:${matchingToken.chainId}/erc20:${matchingToken.contractAddress}` as const,
|
|
298
|
+
network,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
input,
|
|
304
|
+
config: {
|
|
305
|
+
symbol: normalizedSymbol,
|
|
306
|
+
name: `${network.config.name} ${normalizedSymbol}`,
|
|
307
|
+
version: "2",
|
|
308
|
+
chainId: network.config.chainId,
|
|
309
|
+
contractAddress: network.config.usdcAddress,
|
|
310
|
+
decimals: 6,
|
|
311
|
+
},
|
|
312
|
+
caipAsset: network.config
|
|
313
|
+
.caipAssetId as `eip155:${number}/erc20:${string}`,
|
|
314
|
+
network,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const customTokens = getAllCustomTokens();
|
|
319
|
+
const matchingToken = customTokens.find(
|
|
320
|
+
(t) => t.symbol?.toUpperCase() === normalizedSymbol,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (matchingToken) {
|
|
324
|
+
const tokenNetwork = resolveNetwork(matchingToken.chainId);
|
|
325
|
+
if ("code" in tokenNetwork) {
|
|
326
|
+
return tokenNetwork;
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
input,
|
|
330
|
+
config: matchingToken,
|
|
331
|
+
caipAsset:
|
|
332
|
+
`eip155:${matchingToken.chainId}/erc20:${matchingToken.contractAddress}` as const,
|
|
333
|
+
network: tokenNetwork,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const baseNetwork = resolveNetwork("base");
|
|
338
|
+
if ("code" in baseNetwork) {
|
|
339
|
+
return baseNetwork;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
input,
|
|
344
|
+
config: {
|
|
345
|
+
symbol: normalizedSymbol,
|
|
346
|
+
name: "USD Coin",
|
|
347
|
+
version: "2",
|
|
348
|
+
chainId: 8453,
|
|
349
|
+
contractAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
350
|
+
decimals: 6,
|
|
351
|
+
},
|
|
352
|
+
caipAsset: baseNetwork.config
|
|
353
|
+
.caipAssetId as `eip155:${number}/erc20:${string}`,
|
|
354
|
+
network: baseNetwork,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return createError(
|
|
359
|
+
"UNKNOWN_TOKEN",
|
|
360
|
+
`Invalid token identifier type: ${typeof input}`,
|
|
361
|
+
{ value: input },
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export const getAvailableTokens = (): string[] => {
|
|
366
|
+
const customTokens = getAllCustomTokens();
|
|
367
|
+
const symbols = new Set(customTokens.map((t) => t.symbol.toUpperCase()));
|
|
368
|
+
return Array.from(symbols);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// ═══════════════════════════════════════════════════════════════
|
|
372
|
+
// Facilitator Resolution & Validation
|
|
373
|
+
// ═══════════════════════════════════════════════════════════════
|
|
374
|
+
|
|
375
|
+
export const resolveFacilitator = (
|
|
376
|
+
input: FacilitatorConfig,
|
|
377
|
+
supportedNetworks?: ResolvedNetwork[],
|
|
378
|
+
supportedTokens?: ResolvedToken[],
|
|
379
|
+
): ResolvedFacilitator | ValidationError => {
|
|
380
|
+
try {
|
|
381
|
+
new URL(input.url);
|
|
382
|
+
} catch {
|
|
383
|
+
return createError(
|
|
384
|
+
"VALIDATION_FAILED",
|
|
385
|
+
`Invalid facilitator URL: ${input.url}`,
|
|
386
|
+
{ value: input.url },
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const networks: ResolvedNetwork[] = [];
|
|
391
|
+
if (input.networks) {
|
|
392
|
+
for (const network of input.networks) {
|
|
393
|
+
const resolved = resolveNetwork(network);
|
|
394
|
+
if ("code" in resolved) {
|
|
395
|
+
return resolved;
|
|
396
|
+
}
|
|
397
|
+
networks.push(resolved);
|
|
398
|
+
}
|
|
399
|
+
} else if (supportedNetworks) {
|
|
400
|
+
networks.push(...supportedNetworks);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const tokens: ResolvedToken[] = [];
|
|
404
|
+
if (input.tokens) {
|
|
405
|
+
for (const token of input.tokens) {
|
|
406
|
+
for (const network of networks.length > 0
|
|
407
|
+
? networks
|
|
408
|
+
: supportedNetworks || []) {
|
|
409
|
+
const resolved = resolveToken(token, network);
|
|
410
|
+
if ("code" in resolved) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
tokens.push(resolved);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} else if (supportedTokens) {
|
|
418
|
+
tokens.push(...supportedTokens);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
input,
|
|
423
|
+
url: input.url,
|
|
424
|
+
networks,
|
|
425
|
+
tokens,
|
|
426
|
+
};
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
export const checkFacilitatorSupport = (
|
|
430
|
+
facilitator: ResolvedFacilitator,
|
|
431
|
+
network: ResolvedNetwork,
|
|
432
|
+
token: ResolvedToken,
|
|
433
|
+
): ValidationError | { supported: true } => {
|
|
434
|
+
if (facilitator.networks.length > 0) {
|
|
435
|
+
const networkSupported = facilitator.networks.some(
|
|
436
|
+
(n) => n.config.chainId === network.config.chainId,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
if (!networkSupported) {
|
|
440
|
+
const supportedNames = facilitator.networks.map((n) => n.config.name);
|
|
441
|
+
return createError(
|
|
442
|
+
"FACILITATOR_NO_NETWORK_SUPPORT",
|
|
443
|
+
`Facilitator "${facilitator.url}" does not support network "${network.config.name}" (chain ${network.config.chainId}). Supported: ${supportedNames.join(", ")}`,
|
|
444
|
+
{
|
|
445
|
+
value: { facilitator: facilitator.url, network: network.config.name },
|
|
446
|
+
validOptions: supportedNames,
|
|
447
|
+
},
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (facilitator.tokens.length > 0) {
|
|
453
|
+
const tokenSupported = facilitator.tokens.some(
|
|
454
|
+
(t) =>
|
|
455
|
+
t.config.contractAddress.toLowerCase() ===
|
|
456
|
+
token.config.contractAddress.toLowerCase() &&
|
|
457
|
+
t.network.config.chainId === token.network.config.chainId,
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
if (!tokenSupported) {
|
|
461
|
+
const supportedTokens = facilitator.tokens.map(
|
|
462
|
+
(t) => `${t.config.symbol} (${t.network.config.name})`,
|
|
463
|
+
);
|
|
464
|
+
return createError(
|
|
465
|
+
"FACILITATOR_NO_TOKEN_SUPPORT",
|
|
466
|
+
`Facilitator "${facilitator.url}" does not support token "${token.config.symbol}" on "${token.network.config.name}". Supported: ${supportedTokens.join(", ")}`,
|
|
467
|
+
{
|
|
468
|
+
value: {
|
|
469
|
+
facilitator: facilitator.url,
|
|
470
|
+
token: token.config.symbol,
|
|
471
|
+
network: token.network.config.name,
|
|
472
|
+
},
|
|
473
|
+
validOptions: supportedTokens,
|
|
474
|
+
},
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return { supported: true };
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// ═══════════════════════════════════════════════════════════════
|
|
483
|
+
// Full Configuration Validation
|
|
484
|
+
// ═══════════════════════════════════════════════════════════════
|
|
485
|
+
|
|
486
|
+
export const validatePaymentConfig = (
|
|
487
|
+
network: NetworkId,
|
|
488
|
+
token: TokenId,
|
|
489
|
+
facilitators?: FacilitatorConfig | FacilitatorConfig[],
|
|
490
|
+
payTo: string = "0x0000000000000000000000000000000000000000",
|
|
491
|
+
amount: string = "1.0",
|
|
492
|
+
): ResolvedPaymentConfig | ValidationError => {
|
|
493
|
+
const resolvedNetwork = resolveNetwork(network);
|
|
494
|
+
if ("code" in resolvedNetwork) {
|
|
495
|
+
return resolvedNetwork;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const resolvedToken = resolveToken(token, resolvedNetwork);
|
|
499
|
+
if ("code" in resolvedToken) {
|
|
500
|
+
return resolvedToken;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const resolvedFacilitators: ResolvedFacilitator[] = [];
|
|
504
|
+
if (facilitators) {
|
|
505
|
+
const facilitatorArray = Array.isArray(facilitators)
|
|
506
|
+
? facilitators
|
|
507
|
+
: [facilitators];
|
|
508
|
+
for (const facilitator of facilitatorArray) {
|
|
509
|
+
const resolved = resolveFacilitator(
|
|
510
|
+
facilitator,
|
|
511
|
+
[resolvedNetwork],
|
|
512
|
+
[resolvedToken],
|
|
513
|
+
);
|
|
514
|
+
if ("code" in resolved) {
|
|
515
|
+
return resolved;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const supportCheck = checkFacilitatorSupport(
|
|
519
|
+
resolved,
|
|
520
|
+
resolvedNetwork,
|
|
521
|
+
resolvedToken,
|
|
522
|
+
);
|
|
523
|
+
if ("code" in supportCheck) {
|
|
524
|
+
return supportCheck;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
resolvedFacilitators.push(resolved);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
network: resolvedNetwork,
|
|
533
|
+
token: resolvedToken,
|
|
534
|
+
facilitators: resolvedFacilitators,
|
|
535
|
+
version: 2,
|
|
536
|
+
payTo: payTo as `0x${string}`,
|
|
537
|
+
amount,
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
export const validateAcceptConfig = (
|
|
542
|
+
options: {
|
|
543
|
+
networks?: NetworkId[];
|
|
544
|
+
tokens?: TokenId[];
|
|
545
|
+
facilitators?: FacilitatorConfig | FacilitatorConfig[];
|
|
546
|
+
version?: 2;
|
|
547
|
+
},
|
|
548
|
+
payTo: string,
|
|
549
|
+
amount: string,
|
|
550
|
+
):
|
|
551
|
+
| { success: true; config: ResolvedPaymentConfig[] }
|
|
552
|
+
| { success: false; error: ValidationError } => {
|
|
553
|
+
const {
|
|
554
|
+
networks: networkInputs,
|
|
555
|
+
tokens: tokenInputs,
|
|
556
|
+
facilitators,
|
|
557
|
+
version = 2,
|
|
558
|
+
} = options;
|
|
559
|
+
|
|
560
|
+
const networkIds = networkInputs?.length
|
|
561
|
+
? networkInputs
|
|
562
|
+
: Object.keys(NETWORKS);
|
|
563
|
+
const tokenIds = tokenInputs?.length ? tokenInputs : ["usdc"];
|
|
564
|
+
|
|
565
|
+
const networks: ResolvedNetwork[] = [];
|
|
566
|
+
for (const networkId of networkIds) {
|
|
567
|
+
const resolved = resolveNetwork(networkId);
|
|
568
|
+
if ("code" in resolved) {
|
|
569
|
+
return { success: false, error: resolved };
|
|
570
|
+
}
|
|
571
|
+
networks.push(resolved);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const tokens: ResolvedToken[] = [];
|
|
575
|
+
for (const tokenId of tokenIds) {
|
|
576
|
+
let matches = 0;
|
|
577
|
+
for (const network of networks) {
|
|
578
|
+
const resolved = resolveToken(tokenId, network);
|
|
579
|
+
if ("code" in resolved) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
tokens.push(resolved);
|
|
583
|
+
matches += 1;
|
|
584
|
+
}
|
|
585
|
+
if (matches === 0) {
|
|
586
|
+
return {
|
|
587
|
+
success: false,
|
|
588
|
+
error: createError(
|
|
589
|
+
"TOKEN_NOT_ON_NETWORK",
|
|
590
|
+
`Token "${String(tokenId)}" not found on any of the specified networks`,
|
|
591
|
+
{ value: tokenId, validOptions: networks.map((n) => n.config.name) },
|
|
592
|
+
),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const facilitatorArray = facilitators
|
|
598
|
+
? Array.isArray(facilitators)
|
|
599
|
+
? facilitators
|
|
600
|
+
: [facilitators]
|
|
601
|
+
: [];
|
|
602
|
+
const resolvedFacilitators: ResolvedFacilitator[] = [];
|
|
603
|
+
for (const facilitator of facilitatorArray) {
|
|
604
|
+
const resolved = resolveFacilitator(facilitator, networks, tokens);
|
|
605
|
+
if ("code" in resolved) {
|
|
606
|
+
return { success: false, error: resolved };
|
|
607
|
+
}
|
|
608
|
+
resolvedFacilitators.push(resolved);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const configs: ResolvedPaymentConfig[] = [];
|
|
612
|
+
for (const network of networks) {
|
|
613
|
+
for (const token of tokens) {
|
|
614
|
+
if (token.network.config.chainId === network.config.chainId) {
|
|
615
|
+
configs.push({
|
|
616
|
+
network,
|
|
617
|
+
token,
|
|
618
|
+
facilitators: resolvedFacilitators,
|
|
619
|
+
version,
|
|
620
|
+
payTo: payTo as `0x${string}`,
|
|
621
|
+
amount,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return { success: true, config: configs };
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// ═══════════════════════════════════════════════════════════════
|
|
631
|
+
// Type Guards
|
|
632
|
+
// ═══════════════════════════════════════════════════════════════
|
|
633
|
+
|
|
634
|
+
export const isValidationError = (value: unknown): value is ValidationError => {
|
|
635
|
+
return typeof value === "object" && value !== null && "code" in value;
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
export const isResolvedNetwork = (value: unknown): value is ResolvedNetwork => {
|
|
639
|
+
return (
|
|
640
|
+
typeof value === "object" &&
|
|
641
|
+
value !== null &&
|
|
642
|
+
"config" in value &&
|
|
643
|
+
"caip2" in value
|
|
644
|
+
);
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
export const isResolvedToken = (value: unknown): value is ResolvedToken => {
|
|
648
|
+
return (
|
|
649
|
+
typeof value === "object" &&
|
|
650
|
+
value !== null &&
|
|
651
|
+
"config" in value &&
|
|
652
|
+
"caipAsset" in value
|
|
653
|
+
);
|
|
654
|
+
};
|