@armory-sh/base 0.2.29 → 0.2.31

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/README.md CHANGED
@@ -14,56 +14,343 @@ bun add @armory-sh/base
14
14
 
15
15
  Armory enables HTTP API payments via EIP-3009 `transferWithAuthorization`. Let your users pay with USDC directly from their wallet—no credit cards, no middlemen, no gas for payers.
16
16
 
17
- ## Key Exports
17
+ ## API Reference
18
+
19
+ ### Protocol Types
18
20
 
19
21
  ```typescript
20
- import {
21
- // Types
22
- type PaymentPayload,
23
- type PaymentPayloadV2,
24
- type PaymentRequirementsV2,
25
- type SettlementResponseV2,
26
- type CustomToken,
27
- type NetworkConfig,
22
+ import type {
23
+ // V2 Protocol (x402 v2 / CAIP)
24
+ PaymentPayloadV2,
25
+ PaymentRequirementsV2,
26
+ PaymentRequiredV2,
27
+ SettlementResponseV2,
28
+ PayToV2,
29
+ EIP3009Authorization,
30
+ Extensions,
31
+ ResourceInfo,
32
+ Signature,
33
+ Address,
34
+ CAIPAssetId,
35
+
36
+ // Unified Types
37
+ PaymentPayload,
38
+ PaymentRequirements,
39
+ SettlementResponse,
40
+ } from '@armory-sh/base';
41
+ ```
28
42
 
29
- // Encoding/Decoding
43
+ ### Encoding / Decoding
44
+
45
+ ```typescript
46
+ import {
47
+ // V2 Encoding
30
48
  encodePaymentV2,
31
49
  decodePaymentV2,
32
50
  encodeSettlementV2,
33
51
  decodeSettlementV2,
52
+
53
+ // x402 Cross-Version Encoding
54
+ encodePayment,
55
+ decodePayment,
56
+ encodeSettlementResponse,
57
+ decodeSettlementResponse,
58
+ encodeX402Response,
59
+ decodeX402Response,
60
+
61
+ // Base64 Utilities
34
62
  safeBase64Encode,
35
63
  safeBase64Decode,
64
+ toBase64Url,
65
+ normalizeBase64Url,
66
+ encodeUtf8ToBase64,
67
+ decodeBase64ToUtf8,
68
+
69
+ // Header Creation
70
+ createPaymentRequiredHeaders,
71
+ createSettlementHeaders,
72
+ extractPaymentFromHeaders,
73
+
74
+ // Type Guards
75
+ isPaymentV2,
76
+ isSettlementV2,
77
+ isX402V2Payload,
78
+ isX402V2PaymentRequired,
79
+ isX402V2Settlement,
80
+ isPaymentPayload,
81
+ detectPaymentVersion,
82
+ } from '@armory-sh/base';
83
+ ```
84
+
85
+ ### EIP-712 (EIP-3009 Signing)
36
86
 
37
- // EIP-712
87
+ ```typescript
88
+ import {
89
+ // Domain & Typed Data
38
90
  createEIP712Domain,
91
+ EIP712_TYPES,
92
+ USDC_DOMAIN,
93
+
94
+ // Transfer With Authorization
39
95
  createTransferWithAuthorization,
40
96
  validateTransferWithAuthorization,
41
- EIP712_TYPES,
42
97
 
43
- // Networks
98
+ // Types
99
+ type EIP712Domain,
100
+ type TransferWithAuthorization,
101
+ type TypedDataDomain,
102
+ } from '@armory-sh/base';
103
+ ```
104
+
105
+ ### Networks
106
+
107
+ ```typescript
108
+ import {
109
+ // Network Registry
44
110
  NETWORKS,
45
111
  getNetworkConfig,
46
112
  getNetworkByChainId,
47
113
  getMainnets,
48
114
  getTestnets,
49
115
 
50
- // Token Registry
116
+ // Network Resolution
117
+ resolveNetwork,
118
+ normalizeNetworkName,
119
+ getAvailableNetworks,
120
+
121
+ // CAIP-2 Conversion
122
+ networkToCaip2,
123
+ caip2ToNetwork,
124
+
125
+ // Types
126
+ type NetworkConfig,
127
+ type ResolvedNetwork,
128
+ } from '@armory-sh/base';
129
+ ```
130
+
131
+ **Supported Networks**
132
+
133
+ | Key | Chain ID | Name |
134
+ |-----|----------|------|
135
+ | `ethereum` | 1 | Ethereum Mainnet |
136
+ | `base` | 8453 | Base Mainnet |
137
+ | `base-sepolia` | 84532 | Base Sepolia |
138
+ | `skale-base` | 1187947933 | SKALE Base |
139
+ | `skale-base-sepolia` | 324705682 | SKALE Base Sepolia |
140
+ | `ethereum-sepolia` | 11155111 | Ethereum Sepolia |
141
+
142
+ ### Tokens
143
+
144
+ ```typescript
145
+ import {
146
+ // Pre-configured Tokens
51
147
  TOKENS,
148
+ USDC_BASE,
149
+ USDC_BASE_SEPOLIA,
150
+ EURC_BASE,
151
+ getUSDCTokens,
152
+ getEURCTokens,
153
+ getAllTokens,
154
+ getTokensByChain,
155
+ getTokensBySymbol,
156
+
157
+ // Custom Token Registry
52
158
  registerToken,
53
159
  getCustomToken,
54
160
  getAllCustomTokens,
55
161
  unregisterToken,
56
162
  isCustomToken,
57
163
 
58
- // Utilities
164
+ // Token Resolution
165
+ resolveToken,
166
+ getAvailableTokens,
167
+ addressToAssetId,
168
+ assetIdToAddress,
169
+
170
+ // Types
171
+ type CustomToken,
172
+ type ResolvedToken,
173
+ } from '@armory-sh/base';
174
+ ```
175
+
176
+ **Supported Tokens**
177
+
178
+ USDC, EURC, USDT, WBTC, WETH, SKL across supported networks.
179
+
180
+ ### Protocol Utilities
181
+
182
+ ```typescript
183
+ import {
184
+ // Nonce & Time
185
+ generateNonce,
59
186
  createNonce,
60
- toAtomicUnits,
61
- fromAtomicUnits,
187
+ getCurrentTimestamp,
188
+ calculateValidBefore,
189
+
190
+ // Parsing
191
+ parseJsonOrBase64,
192
+
193
+ // Headers
194
+ getPaymentHeaderName,
195
+ PAYMENT_REQUIRED_HEADER,
196
+ PAYMENT_SIGNATURE_HEADER,
197
+ PAYMENT_RESPONSE_HEADER,
198
+
199
+ // Version Detection
200
+ detectX402Version,
201
+ X402_VERSION,
202
+ } from '@armory-sh/base';
203
+ ```
204
+
205
+ ### Payment Requirements (Middleware)
206
+
207
+ ```typescript
208
+ import {
209
+ // Requirement Creation
210
+ createPaymentRequirements,
211
+
212
+ // Requirement Lookup
213
+ findRequirementByNetwork,
214
+ findRequirementByAccepted,
215
+
216
+ // Types
217
+ type PaymentConfig,
218
+ type ResolvedRequirementsConfig,
219
+ } from '@armory-sh/base';
220
+ ```
221
+
222
+ ### Validation
223
+
224
+ ```typescript
225
+ import {
226
+ // Network & Token Validation
62
227
  resolveNetwork,
63
228
  resolveToken,
229
+ validatePaymentConfig,
230
+ validateAcceptConfig,
231
+
232
+ // Facilitator Validation
233
+ resolveFacilitator,
234
+ checkFacilitatorSupport,
235
+
236
+ // Error Creation
237
+ createError,
238
+
239
+ // Type Guards
240
+ isValidationError,
241
+ isResolvedNetwork,
242
+ isResolvedToken,
243
+
244
+ // Available Options
245
+ getAvailableNetworks,
246
+ getAvailableTokens,
64
247
  } from '@armory-sh/base';
65
248
  ```
66
249
 
250
+ ### Utilities
251
+
252
+ ```typescript
253
+ import {
254
+ // Unit Conversion
255
+ toAtomicUnits,
256
+ fromAtomicUnits,
257
+
258
+ // Address Utilities
259
+ normalizeAddress,
260
+ isValidAddress,
261
+ isAddress,
262
+
263
+ // Route Matching
264
+ matchRoute,
265
+ parseRoutePattern,
266
+ findMatchingRoute,
267
+ validateRouteConfig,
268
+
269
+ // Types
270
+ type RouteMatcher,
271
+ type RoutePattern,
272
+ } from '@armory-sh/base';
273
+ ```
274
+
275
+ ### Client Hooks Runtime
276
+
277
+ ```typescript
278
+ import {
279
+ // Hook Execution
280
+ runOnPaymentRequiredHooks,
281
+ runBeforeSignPaymentHooks,
282
+ runAfterPaymentResponseHooks,
283
+ selectRequirementWithHooks,
284
+
285
+ // Types
286
+ type ClientHook,
287
+ type OnPaymentRequiredHook,
288
+ type BeforePaymentHook,
289
+ type PaymentRequiredContext,
290
+ type PaymentPayloadContext,
291
+ type ClientHookErrorContext,
292
+ } from '@armory-sh/base';
293
+ ```
294
+
295
+ ### ERC20 ABI
296
+
297
+ ```typescript
298
+ import {
299
+ ERC20_ABI,
300
+
301
+ // Types
302
+ type ERC20Abi,
303
+ type TransferWithAuthorizationParams,
304
+ type BalanceOfParams,
305
+ } from '@armory-sh/base';
306
+ ```
307
+
308
+ ### Payment Client (Facilitator)
309
+
310
+ ```typescript
311
+ import {
312
+ // Payment Operations
313
+ verifyPayment,
314
+ settlePayment,
315
+
316
+ // Payload Extraction
317
+ decodePayloadHeader,
318
+ extractPayerAddress,
319
+
320
+ // Facilitator Discovery
321
+ getSupported,
322
+
323
+ // Types
324
+ type FacilitatorClientConfig,
325
+ type SupportedResponse,
326
+ } from '@armory-sh/base';
327
+ ```
328
+
329
+ ### Error Classes
330
+
331
+ ```typescript
332
+ import {
333
+ X402ClientError,
334
+ SigningError,
335
+ PaymentException,
336
+ } from '@armory-sh/base';
337
+ ```
338
+
339
+ ### Headers Constants
340
+
341
+ ```typescript
342
+ import {
343
+ PAYMENT_REQUIRED_HEADER,
344
+ PAYMENT_SIGNATURE_HEADER,
345
+ PAYMENT_RESPONSE_HEADER,
346
+ V2_HEADERS,
347
+ } from '@armory-sh/base';
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Quick Start
353
+
67
354
  ## Quick Start
68
355
 
69
356
  ### Pre-configured Tokens
@@ -155,17 +442,6 @@ const decoded = decodePaymentV2(encoded)
155
442
  - **Custom Tokens**: Register your own tokens
156
443
  - **Encoding**: Base64URL encoding for HTTP headers
157
444
 
158
- ## Supported Networks
159
-
160
- | Network | Chain ID |
161
- |---------|----------|
162
- | Ethereum | 1 |
163
- | Base | 8453 |
164
- | Base Sepolia | 84532 |
165
- | SKALE Base | 1187947933 |
166
- | SKALE Base Sepolia | 324705682 |
167
- | Ethereum Sepolia | 11155111 |
168
-
169
445
  ## License
170
446
 
171
447
  MIT © [Sawyer Cutler](https://github.com/TheGreatAxios/armory)
@@ -0,0 +1,9 @@
1
+ import type { PaymentRequirementsV2 } from "./types/v2";
2
+ import type { FacilitatorRoutingConfig } from "./payment-requirements";
3
+ export declare function filterExtensionsForFacilitator(extensions: Record<string, unknown> | undefined, facilitatorUrl: string | undefined, network: string, ttlMs?: number): Promise<Record<string, unknown>>;
4
+ export declare function filterExtensionsForFacilitators(extensions: Record<string, unknown> | undefined, facilitators: Array<{
5
+ url?: string;
6
+ network: string;
7
+ }>, ttlMs?: number): Promise<Record<string, unknown>>;
8
+ export declare function filterExtensionsForRequirements(extensions: Record<string, unknown> | undefined, requirements: PaymentRequirementsV2[], config: FacilitatorRoutingConfig, ttlMs?: number): Promise<Record<string, unknown>>;
9
+ export declare function clearFacilitatorCapabilityCache(): void;
package/dist/index.d.ts CHANGED
@@ -10,8 +10,9 @@ export { createPaymentRequiredHeaders, createSettlementHeaders, decodePayment, d
10
10
  export { PaymentException, SigningError, X402ClientError, } from "./errors";
11
11
  export type { FacilitatorClientConfig, SupportedKind, SupportedResponse, } from "./payment-client";
12
12
  export { decodePayloadHeader, extractPayerAddress, getSupported, settlePayment, verifyPayment, } from "./payment-client";
13
+ export { clearFacilitatorCapabilityCache, filterExtensionsForFacilitator, filterExtensionsForFacilitators, filterExtensionsForRequirements, } from "./facilitator-capabilities";
13
14
  export type { PaymentConfig, ResolvedRequirementsConfig, } from "./payment-requirements";
14
- export { createPaymentRequirements, findRequirementByAccepted, findRequirementByNetwork, } from "./payment-requirements";
15
+ export { createPaymentRequirements, enrichPaymentRequirement, enrichPaymentRequirements, findRequirementByAccepted, findRequirementByNetwork, resolveFacilitatorUrlFromRequirement, } from "./payment-requirements";
15
16
  export { calculateValidBefore, detectX402Version, generateNonce, getPaymentHeaderName, parseJsonOrBase64, } from "./protocol";
16
17
  export type { AcceptPaymentOptions, ArmoryPaymentResult, FacilitatorConfig, FacilitatorSettleResult, FacilitatorVerifyResult, NetworkId, PaymentError, PaymentErrorCode, PaymentResult, PayToAddress, PricingConfig, ResolvedFacilitator, ResolvedNetwork, ResolvedPaymentConfig, ResolvedToken, SettlementMode, TokenId, ValidationError, } from "./types/api";
17
18
  export type { BeforePaymentHook, ClientHook, ClientHookErrorContext, ExtensionHook, HookConfig, HookRegistry, HookResult, OnPaymentRequiredHook, PaymentPayloadContext, PaymentRequiredContext, } from "./types/hooks";
package/dist/index.js CHANGED
@@ -1380,6 +1380,50 @@ var DEFAULT_TOKENS = ["usdc"];
1380
1380
  var isValidationError2 = (value) => {
1381
1381
  return typeof value === "object" && value !== null && "code" in value;
1382
1382
  };
1383
+ var coerceMetadata = (requirement) => {
1384
+ if (typeof requirement.name === "string" && typeof requirement.version === "string") {
1385
+ return { name: requirement.name, version: requirement.version };
1386
+ }
1387
+ const extraName = requirement.extra && typeof requirement.extra === "object" && typeof requirement.extra.name === "string" ? requirement.extra.name : void 0;
1388
+ const extraVersion = requirement.extra && typeof requirement.extra === "object" && typeof requirement.extra.version === "string" ? requirement.extra.version : void 0;
1389
+ return { name: extraName, version: extraVersion };
1390
+ };
1391
+ var resolveTokenMetadata = (requirement) => {
1392
+ const chainId = parseInt(requirement.network.split(":")[1] || "0", 10);
1393
+ if (!Number.isFinite(chainId) || chainId <= 0) {
1394
+ return {};
1395
+ }
1396
+ const token = getToken(chainId, requirement.asset);
1397
+ if (!token) {
1398
+ return {};
1399
+ }
1400
+ return {
1401
+ name: token.name,
1402
+ version: token.version
1403
+ };
1404
+ };
1405
+ function enrichPaymentRequirement(requirement) {
1406
+ const metadata = coerceMetadata(requirement);
1407
+ const fallback = resolveTokenMetadata(requirement);
1408
+ const name = metadata.name ?? fallback.name;
1409
+ const version = metadata.version ?? fallback.version;
1410
+ if (!name || !version) {
1411
+ return requirement;
1412
+ }
1413
+ return {
1414
+ ...requirement,
1415
+ name,
1416
+ version,
1417
+ extra: {
1418
+ ...requirement.extra ?? {},
1419
+ name,
1420
+ version
1421
+ }
1422
+ };
1423
+ }
1424
+ function enrichPaymentRequirements(requirements) {
1425
+ return requirements.map(enrichPaymentRequirement);
1426
+ }
1383
1427
  function resolvePayTo(config, network, token) {
1384
1428
  const chainId = network.config.chainId;
1385
1429
  if (config.payToByToken) {
@@ -1434,6 +1478,37 @@ function resolveFacilitatorUrl(config, network, token) {
1434
1478
  }
1435
1479
  return config.facilitatorUrl;
1436
1480
  }
1481
+ function resolveFacilitatorUrlFromRequirement(config, requirement) {
1482
+ const chainId = parseInt(requirement.network.split(":")[1] || "0", 10);
1483
+ const assetAddress = requirement.asset.toLowerCase();
1484
+ if (config.facilitatorUrlByToken) {
1485
+ for (const [chainKey, tokenMap] of Object.entries(
1486
+ config.facilitatorUrlByToken
1487
+ )) {
1488
+ const resolvedChain = resolveNetwork(chainKey);
1489
+ if (isValidationError2(resolvedChain) || resolvedChain.config.chainId !== chainId) {
1490
+ continue;
1491
+ }
1492
+ for (const [tokenKey2, url] of Object.entries(tokenMap)) {
1493
+ const resolvedToken = resolveToken(tokenKey2, resolvedChain);
1494
+ if (!isValidationError2(resolvedToken) && resolvedToken.config.contractAddress.toLowerCase() === assetAddress) {
1495
+ return url;
1496
+ }
1497
+ }
1498
+ }
1499
+ }
1500
+ if (config.facilitatorUrlByChain) {
1501
+ for (const [chainKey, url] of Object.entries(
1502
+ config.facilitatorUrlByChain
1503
+ )) {
1504
+ const resolvedChain = resolveNetwork(chainKey);
1505
+ if (!isValidationError2(resolvedChain) && resolvedChain.config.chainId === chainId) {
1506
+ return url;
1507
+ }
1508
+ }
1509
+ }
1510
+ return config.facilitatorUrl;
1511
+ }
1437
1512
  function resolveNetworks(chainInputs) {
1438
1513
  const resolvedNetworks = [];
1439
1514
  const errors = [];
@@ -1496,6 +1571,8 @@ function createPaymentRequirements(config) {
1496
1571
  asset: tokenConfig.contractAddress,
1497
1572
  payTo: resolvedPayTo,
1498
1573
  maxTimeoutSeconds,
1574
+ name: tokenConfig.name,
1575
+ version: tokenConfig.version,
1499
1576
  extra: {
1500
1577
  name: tokenConfig.name,
1501
1578
  version: tokenConfig.version
@@ -1540,6 +1617,91 @@ function findRequirementByAccepted(requirements, accepted) {
1540
1617
  );
1541
1618
  }
1542
1619
 
1620
+ // src/facilitator-capabilities.ts
1621
+ var DEFAULT_CAPABILITY_TTL_MS = 5 * 60 * 1e3;
1622
+ var capabilityCache = /* @__PURE__ */ new Map();
1623
+ var normalizeNetwork = (network) => {
1624
+ return network.toLowerCase();
1625
+ };
1626
+ async function getExtensionKeysForFacilitator(url, network, ttlMs) {
1627
+ const cacheKey = `${url}|${normalizeNetwork(network)}`;
1628
+ const now = Date.now();
1629
+ const cached = capabilityCache.get(cacheKey);
1630
+ if (cached && cached.expiresAt > now) {
1631
+ return cached.keys;
1632
+ }
1633
+ try {
1634
+ const supported = await getSupported({ url });
1635
+ const extensionKeys = /* @__PURE__ */ new Set();
1636
+ for (const kind of supported.kinds) {
1637
+ if (normalizeNetwork(kind.network) !== normalizeNetwork(network)) {
1638
+ continue;
1639
+ }
1640
+ if (kind.extra && typeof kind.extra === "object") {
1641
+ for (const key of Object.keys(kind.extra)) {
1642
+ extensionKeys.add(key);
1643
+ }
1644
+ }
1645
+ }
1646
+ capabilityCache.set(cacheKey, {
1647
+ expiresAt: now + ttlMs,
1648
+ keys: extensionKeys
1649
+ });
1650
+ return extensionKeys;
1651
+ } catch {
1652
+ capabilityCache.set(cacheKey, {
1653
+ expiresAt: now + ttlMs,
1654
+ keys: /* @__PURE__ */ new Set()
1655
+ });
1656
+ return /* @__PURE__ */ new Set();
1657
+ }
1658
+ }
1659
+ async function filterExtensionsForFacilitator(extensions, facilitatorUrl, network, ttlMs = DEFAULT_CAPABILITY_TTL_MS) {
1660
+ if (!extensions || !facilitatorUrl) {
1661
+ return extensions ?? {};
1662
+ }
1663
+ const supportedKeys = await getExtensionKeysForFacilitator(
1664
+ facilitatorUrl,
1665
+ network,
1666
+ ttlMs
1667
+ );
1668
+ const filtered = {};
1669
+ for (const [key, value] of Object.entries(extensions)) {
1670
+ if (supportedKeys.has(key)) {
1671
+ filtered[key] = value;
1672
+ }
1673
+ }
1674
+ return filtered;
1675
+ }
1676
+ async function filterExtensionsForFacilitators(extensions, facilitators, ttlMs = DEFAULT_CAPABILITY_TTL_MS) {
1677
+ if (!extensions) {
1678
+ return {};
1679
+ }
1680
+ let filtered = { ...extensions };
1681
+ for (const facilitator of facilitators) {
1682
+ filtered = await filterExtensionsForFacilitator(
1683
+ filtered,
1684
+ facilitator.url,
1685
+ facilitator.network,
1686
+ ttlMs
1687
+ );
1688
+ if (Object.keys(filtered).length === 0) {
1689
+ return {};
1690
+ }
1691
+ }
1692
+ return filtered;
1693
+ }
1694
+ async function filterExtensionsForRequirements(extensions, requirements, config, ttlMs = DEFAULT_CAPABILITY_TTL_MS) {
1695
+ const facilitators = requirements.map((requirement) => ({
1696
+ url: resolveFacilitatorUrlFromRequirement(config, requirement),
1697
+ network: requirement.network
1698
+ }));
1699
+ return filterExtensionsForFacilitators(extensions, facilitators, ttlMs);
1700
+ }
1701
+ function clearFacilitatorCapabilityCache() {
1702
+ capabilityCache.clear();
1703
+ }
1704
+
1543
1705
  // src/protocol.ts
1544
1706
  function parseJsonOrBase64(value) {
1545
1707
  try {
@@ -1753,4 +1915,4 @@ function validateRouteConfig(config) {
1753
1915
  return null;
1754
1916
  }
1755
1917
 
1756
- export { EIP712_TYPES, ERC20_ABI, EURC_BASE, NETWORKS, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PAYMENT_SIGNATURE_HEADER, PaymentException, SCHEMES, SKL_SKALE_BASE, SKL_SKALE_BASE_SEPOLIA, SigningError, TOKENS, USDC_BASE, USDC_BASE_SEPOLIA, USDC_DOMAIN, USDC_SKALE_BASE, USDC_SKALE_BASE_SEPOLIA, USDT_SKALE_BASE, USDT_SKALE_BASE_SEPOLIA, V2_HEADERS, WBTC_SKALE_BASE, WBTC_SKALE_BASE_SEPOLIA, WETH_SKALE_BASE, WETH_SKALE_BASE_SEPOLIA, X402ClientError, X402_VERSION, addressToAssetId, assetIdToAddress, caip2ToNetwork, calculateValidBefore, checkFacilitatorSupport, combineSignature as combineSignatureV2, createEIP712Domain, createError, createNonce, createPaymentRequiredHeaders, createPaymentRequirements, createSettlementHeaders, createTransferWithAuthorization, decodeBase64ToUtf8, decodePayloadHeader, decodePayment, decodePaymentV2, decodeSettlementResponse, decodeSettlementV2, decodeX402Response, detectPaymentVersion, detectX402Version, encodePayment, encodePaymentV2, encodeSettlementResponse, encodeSettlementV2, encodeUtf8ToBase64, encodeX402Response, extractPayerAddress, extractPaymentFromHeaders, findMatchingRoute, findRequirementByAccepted, findRequirementByNetwork, fromAtomicUnits, generateNonce, getAllCustomTokens, getAllTokens, getAvailableNetworks, getAvailableTokens, getCurrentTimestamp, getCustomToken, getEURCTokens, getMainnets, getNetworkByChainId, getNetworkConfig, getPaymentHeaderName, getSKLTokens, getSupported, getTestnets, getToken, getTokensByChain, getTokensBySymbol, getTxHash, getUSDCTokens, getUSDTTokens, getWBTCTokens, getWETHTokens, isAddress2 as isAddress, isCAIP2ChainId, isCAIPAssetId, isCustomToken, isExactEvmPayload, isPaymentPayload, isPaymentPayloadV2, isPaymentRequiredV2, isPaymentV2, isResolvedNetwork, isResolvedToken, isSettlementSuccessful, isSettlementV2, isValidAddress, isValidationError, isX402V2Payload, isX402V2PaymentRequired, isX402V2Requirements, isX402V2Settlement, matchRoute, networkToCaip2, normalizeAddress, normalizeBase64Url, normalizeNetworkName, parseJsonOrBase64, parseRoutePattern, parseSignature as parseSignatureV2, registerToken, resolveFacilitator, resolveNetwork, resolveToken, runAfterPaymentResponseHooks, runBeforeSignPaymentHooks, runOnPaymentRequiredHooks, safeBase64Decode2 as safeBase64Decode, safeBase64Encode2 as safeBase64Encode, selectRequirementWithHooks, settlePayment, toAtomicUnits, toBase64Url, unregisterToken, validateAcceptConfig, validatePaymentConfig, validateRouteConfig, validateTransferWithAuthorization, verifyPayment };
1918
+ export { EIP712_TYPES, ERC20_ABI, EURC_BASE, NETWORKS, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PAYMENT_SIGNATURE_HEADER, PaymentException, SCHEMES, SKL_SKALE_BASE, SKL_SKALE_BASE_SEPOLIA, SigningError, TOKENS, USDC_BASE, USDC_BASE_SEPOLIA, USDC_DOMAIN, USDC_SKALE_BASE, USDC_SKALE_BASE_SEPOLIA, USDT_SKALE_BASE, USDT_SKALE_BASE_SEPOLIA, V2_HEADERS, WBTC_SKALE_BASE, WBTC_SKALE_BASE_SEPOLIA, WETH_SKALE_BASE, WETH_SKALE_BASE_SEPOLIA, X402ClientError, X402_VERSION, addressToAssetId, assetIdToAddress, caip2ToNetwork, calculateValidBefore, checkFacilitatorSupport, clearFacilitatorCapabilityCache, combineSignature as combineSignatureV2, createEIP712Domain, createError, createNonce, createPaymentRequiredHeaders, createPaymentRequirements, createSettlementHeaders, createTransferWithAuthorization, decodeBase64ToUtf8, decodePayloadHeader, decodePayment, decodePaymentV2, decodeSettlementResponse, decodeSettlementV2, decodeX402Response, detectPaymentVersion, detectX402Version, encodePayment, encodePaymentV2, encodeSettlementResponse, encodeSettlementV2, encodeUtf8ToBase64, encodeX402Response, enrichPaymentRequirement, enrichPaymentRequirements, extractPayerAddress, extractPaymentFromHeaders, filterExtensionsForFacilitator, filterExtensionsForFacilitators, filterExtensionsForRequirements, findMatchingRoute, findRequirementByAccepted, findRequirementByNetwork, fromAtomicUnits, generateNonce, getAllCustomTokens, getAllTokens, getAvailableNetworks, getAvailableTokens, getCurrentTimestamp, getCustomToken, getEURCTokens, getMainnets, getNetworkByChainId, getNetworkConfig, getPaymentHeaderName, getSKLTokens, getSupported, getTestnets, getToken, getTokensByChain, getTokensBySymbol, getTxHash, getUSDCTokens, getUSDTTokens, getWBTCTokens, getWETHTokens, isAddress2 as isAddress, isCAIP2ChainId, isCAIPAssetId, isCustomToken, isExactEvmPayload, isPaymentPayload, isPaymentPayloadV2, isPaymentRequiredV2, isPaymentV2, isResolvedNetwork, isResolvedToken, isSettlementSuccessful, isSettlementV2, isValidAddress, isValidationError, isX402V2Payload, isX402V2PaymentRequired, isX402V2Requirements, isX402V2Settlement, matchRoute, networkToCaip2, normalizeAddress, normalizeBase64Url, normalizeNetworkName, parseJsonOrBase64, parseRoutePattern, parseSignature as parseSignatureV2, registerToken, resolveFacilitator, resolveFacilitatorUrlFromRequirement, resolveNetwork, resolveToken, runAfterPaymentResponseHooks, runBeforeSignPaymentHooks, runOnPaymentRequiredHooks, safeBase64Decode2 as safeBase64Decode, safeBase64Encode2 as safeBase64Encode, selectRequirementWithHooks, settlePayment, toAtomicUnits, toBase64Url, unregisterToken, validateAcceptConfig, validatePaymentConfig, validateRouteConfig, validateTransferWithAuthorization, verifyPayment };
@@ -18,6 +18,14 @@ export interface ResolvedRequirementsConfig {
18
18
  requirements: PaymentRequirementsV2[];
19
19
  error?: ValidationError;
20
20
  }
21
+ export interface FacilitatorRoutingConfig {
22
+ facilitatorUrl?: string;
23
+ facilitatorUrlByChain?: Record<string, string>;
24
+ facilitatorUrlByToken?: Record<string, Record<string, string>>;
25
+ }
26
+ export declare function enrichPaymentRequirement(requirement: PaymentRequirementsV2): PaymentRequirementsV2;
27
+ export declare function enrichPaymentRequirements(requirements: PaymentRequirementsV2[]): PaymentRequirementsV2[];
28
+ export declare function resolveFacilitatorUrlFromRequirement(config: FacilitatorRoutingConfig, requirement: Pick<PaymentRequirementsV2, "network" | "asset">): string | undefined;
21
29
  export declare function createPaymentRequirements(config: PaymentConfig): ResolvedRequirementsConfig;
22
30
  export declare function findRequirementByNetwork(requirements: PaymentRequirementsV2[], network: string): PaymentRequirementsV2 | undefined;
23
31
  export declare function findRequirementByAccepted(requirements: PaymentRequirementsV2[], accepted: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@armory-sh/base",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "license": "MIT",
5
5
  "author": "Sawyer Cutler <sawyer@dirtroad.dev>",
6
6
  "keywords": [
@@ -0,0 +1,125 @@
1
+ import { getSupported } from "./payment-client";
2
+ import type { PaymentRequirementsV2 } from "./types/v2";
3
+ import type { FacilitatorRoutingConfig } from "./payment-requirements";
4
+ import { resolveFacilitatorUrlFromRequirement } from "./payment-requirements";
5
+
6
+ const DEFAULT_CAPABILITY_TTL_MS = 5 * 60 * 1000;
7
+
8
+ type CapabilityCacheEntry = {
9
+ expiresAt: number;
10
+ keys: Set<string>;
11
+ };
12
+
13
+ const capabilityCache = new Map<string, CapabilityCacheEntry>();
14
+
15
+ const normalizeNetwork = (network: string): string => {
16
+ return network.toLowerCase();
17
+ };
18
+
19
+ async function getExtensionKeysForFacilitator(
20
+ url: string,
21
+ network: string,
22
+ ttlMs: number,
23
+ ): Promise<Set<string>> {
24
+ const cacheKey = `${url}|${normalizeNetwork(network)}`;
25
+ const now = Date.now();
26
+ const cached = capabilityCache.get(cacheKey);
27
+ if (cached && cached.expiresAt > now) {
28
+ return cached.keys;
29
+ }
30
+
31
+ try {
32
+ const supported = await getSupported({ url });
33
+ const extensionKeys = new Set<string>();
34
+ for (const kind of supported.kinds) {
35
+ if (normalizeNetwork(kind.network) !== normalizeNetwork(network)) {
36
+ continue;
37
+ }
38
+ if (kind.extra && typeof kind.extra === "object") {
39
+ for (const key of Object.keys(kind.extra)) {
40
+ extensionKeys.add(key);
41
+ }
42
+ }
43
+ }
44
+ capabilityCache.set(cacheKey, {
45
+ expiresAt: now + ttlMs,
46
+ keys: extensionKeys,
47
+ });
48
+ return extensionKeys;
49
+ } catch {
50
+ capabilityCache.set(cacheKey, {
51
+ expiresAt: now + ttlMs,
52
+ keys: new Set<string>(),
53
+ });
54
+ return new Set<string>();
55
+ }
56
+ }
57
+
58
+ export async function filterExtensionsForFacilitator(
59
+ extensions: Record<string, unknown> | undefined,
60
+ facilitatorUrl: string | undefined,
61
+ network: string,
62
+ ttlMs: number = DEFAULT_CAPABILITY_TTL_MS,
63
+ ): Promise<Record<string, unknown>> {
64
+ if (!extensions || !facilitatorUrl) {
65
+ return extensions ?? {};
66
+ }
67
+
68
+ const supportedKeys = await getExtensionKeysForFacilitator(
69
+ facilitatorUrl,
70
+ network,
71
+ ttlMs,
72
+ );
73
+
74
+ const filtered: Record<string, unknown> = {};
75
+ for (const [key, value] of Object.entries(extensions)) {
76
+ if (supportedKeys.has(key)) {
77
+ filtered[key] = value;
78
+ }
79
+ }
80
+
81
+ return filtered;
82
+ }
83
+
84
+ export async function filterExtensionsForFacilitators(
85
+ extensions: Record<string, unknown> | undefined,
86
+ facilitators: Array<{ url?: string; network: string }>,
87
+ ttlMs: number = DEFAULT_CAPABILITY_TTL_MS,
88
+ ): Promise<Record<string, unknown>> {
89
+ if (!extensions) {
90
+ return {};
91
+ }
92
+
93
+ let filtered = { ...extensions };
94
+ for (const facilitator of facilitators) {
95
+ filtered = await filterExtensionsForFacilitator(
96
+ filtered,
97
+ facilitator.url,
98
+ facilitator.network,
99
+ ttlMs,
100
+ );
101
+ if (Object.keys(filtered).length === 0) {
102
+ return {};
103
+ }
104
+ }
105
+
106
+ return filtered;
107
+ }
108
+
109
+ export async function filterExtensionsForRequirements(
110
+ extensions: Record<string, unknown> | undefined,
111
+ requirements: PaymentRequirementsV2[],
112
+ config: FacilitatorRoutingConfig,
113
+ ttlMs: number = DEFAULT_CAPABILITY_TTL_MS,
114
+ ): Promise<Record<string, unknown>> {
115
+ const facilitators = requirements.map((requirement) => ({
116
+ url: resolveFacilitatorUrlFromRequirement(config, requirement),
117
+ network: requirement.network,
118
+ }));
119
+
120
+ return filterExtensionsForFacilitators(extensions, facilitators, ttlMs);
121
+ }
122
+
123
+ export function clearFacilitatorCapabilityCache(): void {
124
+ capabilityCache.clear();
125
+ }
package/src/index.ts CHANGED
@@ -116,14 +116,23 @@ export {
116
116
  settlePayment,
117
117
  verifyPayment,
118
118
  } from "./payment-client";
119
+ export {
120
+ clearFacilitatorCapabilityCache,
121
+ filterExtensionsForFacilitator,
122
+ filterExtensionsForFacilitators,
123
+ filterExtensionsForRequirements,
124
+ } from "./facilitator-capabilities";
119
125
  export type {
120
126
  PaymentConfig,
121
127
  ResolvedRequirementsConfig,
122
128
  } from "./payment-requirements";
123
129
  export {
124
130
  createPaymentRequirements,
131
+ enrichPaymentRequirement,
132
+ enrichPaymentRequirements,
125
133
  findRequirementByAccepted,
126
134
  findRequirementByNetwork,
135
+ resolveFacilitatorUrlFromRequirement,
127
136
  } from "./payment-requirements";
128
137
  // ============================================
129
138
  // Protocol utilities (shared across all client packages)
@@ -6,6 +6,7 @@ import type {
6
6
  ValidationError,
7
7
  } from "./types/api";
8
8
  import { getNetworkConfig } from "./types/networks";
9
+ import { getToken } from "./data/tokens";
9
10
  import type { Address, PaymentRequirementsV2 } from "./types/v2";
10
11
  import { toAtomicUnits } from "./utils/x402";
11
12
  import {
@@ -36,6 +37,12 @@ export interface ResolvedRequirementsConfig {
36
37
  error?: ValidationError;
37
38
  }
38
39
 
40
+ export interface FacilitatorRoutingConfig {
41
+ facilitatorUrl?: string;
42
+ facilitatorUrlByChain?: Record<string, string>;
43
+ facilitatorUrlByToken?: Record<string, Record<string, string>>;
44
+ }
45
+
39
46
  const DEFAULT_NETWORKS: NetworkId[] = [
40
47
  "ethereum",
41
48
  "base",
@@ -51,6 +58,78 @@ const isValidationError = (value: unknown): value is ValidationError => {
51
58
  return typeof value === "object" && value !== null && "code" in value;
52
59
  };
53
60
 
61
+ const coerceMetadata = (
62
+ requirement: PaymentRequirementsV2,
63
+ ): { name?: string; version?: string } => {
64
+ if (typeof requirement.name === "string" && typeof requirement.version === "string") {
65
+ return { name: requirement.name, version: requirement.version };
66
+ }
67
+
68
+ const extraName =
69
+ requirement.extra &&
70
+ typeof requirement.extra === "object" &&
71
+ typeof requirement.extra.name === "string"
72
+ ? requirement.extra.name
73
+ : undefined;
74
+ const extraVersion =
75
+ requirement.extra &&
76
+ typeof requirement.extra === "object" &&
77
+ typeof requirement.extra.version === "string"
78
+ ? requirement.extra.version
79
+ : undefined;
80
+
81
+ return { name: extraName, version: extraVersion };
82
+ };
83
+
84
+ const resolveTokenMetadata = (
85
+ requirement: PaymentRequirementsV2,
86
+ ): { name?: string; version?: string } => {
87
+ const chainId = parseInt(requirement.network.split(":")[1] || "0", 10);
88
+ if (!Number.isFinite(chainId) || chainId <= 0) {
89
+ return {};
90
+ }
91
+
92
+ const token = getToken(chainId, requirement.asset);
93
+ if (!token) {
94
+ return {};
95
+ }
96
+
97
+ return {
98
+ name: token.name,
99
+ version: token.version,
100
+ };
101
+ };
102
+
103
+ export function enrichPaymentRequirement(
104
+ requirement: PaymentRequirementsV2,
105
+ ): PaymentRequirementsV2 {
106
+ const metadata = coerceMetadata(requirement);
107
+ const fallback = resolveTokenMetadata(requirement);
108
+ const name = metadata.name ?? fallback.name;
109
+ const version = metadata.version ?? fallback.version;
110
+
111
+ if (!name || !version) {
112
+ return requirement;
113
+ }
114
+
115
+ return {
116
+ ...requirement,
117
+ name,
118
+ version,
119
+ extra: {
120
+ ...(requirement.extra ?? {}),
121
+ name,
122
+ version,
123
+ },
124
+ };
125
+ }
126
+
127
+ export function enrichPaymentRequirements(
128
+ requirements: PaymentRequirementsV2[],
129
+ ): PaymentRequirementsV2[] {
130
+ return requirements.map(enrichPaymentRequirement);
131
+ }
132
+
54
133
  function resolvePayTo(
55
134
  config: PaymentConfig,
56
135
  network: ResolvedNetwork,
@@ -141,6 +220,54 @@ function resolveFacilitatorUrl(
141
220
  return config.facilitatorUrl;
142
221
  }
143
222
 
223
+ export function resolveFacilitatorUrlFromRequirement(
224
+ config: FacilitatorRoutingConfig,
225
+ requirement: Pick<PaymentRequirementsV2, "network" | "asset">,
226
+ ): string | undefined {
227
+ const chainId = parseInt(requirement.network.split(":")[1] || "0", 10);
228
+ const assetAddress = requirement.asset.toLowerCase();
229
+
230
+ if (config.facilitatorUrlByToken) {
231
+ for (const [chainKey, tokenMap] of Object.entries(
232
+ config.facilitatorUrlByToken,
233
+ )) {
234
+ const resolvedChain = resolveNetwork(chainKey);
235
+ if (
236
+ isValidationError(resolvedChain) ||
237
+ resolvedChain.config.chainId !== chainId
238
+ ) {
239
+ continue;
240
+ }
241
+
242
+ for (const [tokenKey, url] of Object.entries(tokenMap)) {
243
+ const resolvedToken = resolveToken(tokenKey, resolvedChain);
244
+ if (
245
+ !isValidationError(resolvedToken) &&
246
+ resolvedToken.config.contractAddress.toLowerCase() === assetAddress
247
+ ) {
248
+ return url;
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ if (config.facilitatorUrlByChain) {
255
+ for (const [chainKey, url] of Object.entries(
256
+ config.facilitatorUrlByChain,
257
+ )) {
258
+ const resolvedChain = resolveNetwork(chainKey);
259
+ if (
260
+ !isValidationError(resolvedChain) &&
261
+ resolvedChain.config.chainId === chainId
262
+ ) {
263
+ return url;
264
+ }
265
+ }
266
+ }
267
+
268
+ return config.facilitatorUrl;
269
+ }
270
+
144
271
  function resolveNetworks(chainInputs: NetworkId[] | undefined): {
145
272
  networks: ResolvedNetwork[];
146
273
  error?: ValidationError;
@@ -221,6 +348,8 @@ export function createPaymentRequirements(
221
348
  asset: tokenConfig.contractAddress,
222
349
  payTo: resolvedPayTo as `0x${string}`,
223
350
  maxTimeoutSeconds,
351
+ name: tokenConfig.name,
352
+ version: tokenConfig.version,
224
353
  extra: {
225
354
  name: tokenConfig.name,
226
355
  version: tokenConfig.version,