@armory-sh/client-web3 0.2.24 → 0.2.26

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
@@ -8,6 +8,7 @@ Armory x402 SDK — Payment client for Web3.js. Make payments from any Web3.js w
8
8
 
9
9
  ```bash
10
10
  bun add @armory-sh/client-web3
11
+ bun add @armory-sh/client-hooks # optional preference/logger hooks
11
12
  ```
12
13
 
13
14
  ## Why Armory?
@@ -60,9 +61,30 @@ const response = await client.fetch('https://api.example.com/protected')
60
61
  const data = await response.json()
61
62
  ```
62
63
 
64
+ ## Hook Pipeline
65
+
66
+ ```typescript
67
+ import { createX402Client } from '@armory-sh/client-web3'
68
+ import { PaymentPreference, Logger } from '@armory-sh/client-hooks'
69
+
70
+ const client = createX402Client({
71
+ account,
72
+ hooks: [
73
+ PaymentPreference.chain(['base', 'polygon', 'skale']),
74
+ PaymentPreference.token(['USDT', 'USDC', 'WBTC']),
75
+ PaymentPreference.cheapest(),
76
+ Logger.console(),
77
+ ],
78
+ })
79
+ ```
80
+
81
+ `parsePaymentRequired` returns `accepts[]` (x402 v2 challenge options). Clients select from this list.
82
+ `hooks` are lifecycle callbacks. `extensions` are protocol payload fields. Hooks can drive selection and payload behavior, but they are not extensions.
83
+
63
84
  ## Features
64
85
 
65
86
  - **Auto 402 Handling**: Automatically intercepts and pays for 402 responses
87
+ - **Detailed Verification Errors**: 402 retry failures include server details (for example `insufficient_funds`)
66
88
  - **EIP-3009 Signing**: Full support for EIP-3009 TransferWithAuthorization
67
89
  - **Multi-Network**: Ethereum, Base, SKALE support
68
90
  - **Multi-Token**: USDC, EURC, USDT, WBTC, WETH, SKL
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Web3BaseWalletAccount, Web3BaseWallet } from 'web3-types';
2
1
  import { NetworkConfig, CustomToken, PaymentPayloadV2, PaymentRequirementsV2, SettlementResponseV2, NetworkId, TokenId, ArmoryPaymentResult, ValidationError } from '@armory-sh/base';
3
- export { EIP3009Authorization, PaymentPayloadV2, PaymentRequirementsV2, SchemePayloadV2, SettlementResponseV2 } from '@armory-sh/base';
2
+ import { ClientHook } from '@armory-sh/base/types/hooks';
3
+ import { Web3BaseWalletAccount, Web3BaseWallet } from 'web3-types';
4
4
 
5
5
  type Web3Account = Web3BaseWalletAccount | Web3BaseWallet<Web3BaseWalletAccount>;
6
6
  /** Token configuration - can use pre-configured tokens from @armory-sh/tokens */
@@ -16,6 +16,7 @@ interface Web3ClientConfig {
16
16
  domainName?: string;
17
17
  /** Override EIP-712 domain version for custom tokens */
18
18
  domainVersion?: string;
19
+ hooks?: ClientHook<Web3Account>[];
19
20
  }
20
21
  interface PaymentSignatureResult {
21
22
  signature?: {
@@ -67,34 +68,6 @@ interface Web3EIP712Domain {
67
68
  [key: string]: string | number;
68
69
  }
69
70
 
70
- declare const createX402Client: (config: Web3ClientConfig) => Web3X402Client;
71
-
72
- /**
73
- * Create an x402 transport layer for handling payment-required responses
74
- */
75
- declare const createX402Transport: (options: X402TransportOptions) => X402Transport;
76
-
77
- declare const createEIP712Domain: (chainId: number | string, contractAddress: string, domainName?: string, domainVersion?: string) => Web3EIP712Domain;
78
- declare const createTransferWithAuthorization: (params: Web3TransferWithAuthorization) => Record<string, string>;
79
- declare const validateTransferWithAuthorization: (message: Web3TransferWithAuthorization) => boolean;
80
- declare const parseSignature: (signature: string) => {
81
- v: number;
82
- r: string;
83
- s: string;
84
- };
85
- declare const concatenateSignature: (v: number, r: string, s: string) => string;
86
- declare const adjustVForChainId: (v: number, chainId: number) => number;
87
- declare const signTypedData: (_account: Web3BaseWalletAccount | Web3BaseWallet<Web3BaseWalletAccount>, _domain: Web3EIP712Domain, _message: Record<string, string>) => Promise<{
88
- v: number;
89
- r: string;
90
- s: string;
91
- }>;
92
- declare const signWithPrivateKey: (_privateKey: string, _domain: Web3EIP712Domain, _message: Record<string, string>) => Promise<{
93
- v: number;
94
- r: string;
95
- s: string;
96
- }>;
97
-
98
71
  /**
99
72
  * Simple one-line payment API for Armory (Web3)
100
73
  * Focus on DX/UX - "everything just magically works"
@@ -205,4 +178,32 @@ interface ArmoryInstance {
205
178
  }
206
179
  declare const createArmory: (config: ArmoryConfig) => ArmoryInstance;
207
180
 
181
+ declare const createX402Client: (config: Web3ClientConfig) => Web3X402Client;
182
+
183
+ declare const createEIP712Domain: (chainId: number | string, contractAddress: string, domainName?: string, domainVersion?: string) => Web3EIP712Domain;
184
+ declare const createTransferWithAuthorization: (params: Web3TransferWithAuthorization) => Record<string, string>;
185
+ declare const validateTransferWithAuthorization: (message: Web3TransferWithAuthorization) => boolean;
186
+ declare const parseSignature: (signature: string) => {
187
+ v: number;
188
+ r: string;
189
+ s: string;
190
+ };
191
+ declare const concatenateSignature: (v: number, r: string, s: string) => string;
192
+ declare const adjustVForChainId: (v: number, chainId: number) => number;
193
+ declare const signTypedData: (_account: Web3BaseWalletAccount | Web3BaseWallet<Web3BaseWalletAccount>, _domain: Web3EIP712Domain, _message: Record<string, string>) => Promise<{
194
+ v: number;
195
+ r: string;
196
+ s: string;
197
+ }>;
198
+ declare const signWithPrivateKey: (_privateKey: string, _domain: Web3EIP712Domain, _message: Record<string, string>) => Promise<{
199
+ v: number;
200
+ r: string;
201
+ s: string;
202
+ }>;
203
+
204
+ /**
205
+ * Create an x402 transport layer for handling payment-required responses
206
+ */
207
+ declare const createX402Transport: (options: X402TransportOptions) => X402Transport;
208
+
208
209
  export { type ArmoryConfig, type ArmoryInstance, type HttpMethod, type NormalizedWallet, type PaymentOptions, type SimpleWalletInput, adjustVForChainId, armoryDelete, armoryGet, armoryPatch, armoryPay, armoryPost, armoryPut, concatenateSignature, createArmory, createEIP712Domain, createTransferWithAuthorization, createX402Client, createX402Transport, getNetworks, getTokens, getWalletAddress, normalizeWallet, parseSignature, signTypedData, signWithPrivateKey, validateNetwork, validateToken, validateTransferWithAuthorization };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
+ import { encodePaymentV2, validatePaymentConfig, isValidationError, resolveNetwork, resolveToken, V2_HEADERS, getNetworkConfig, getNetworkByChainId, networkToCaip2, combineSignatureV2, normalizeBase64Url, decodeBase64ToUtf8, isX402V2PaymentRequired } from '@armory-sh/base';
2
+ import { runOnPaymentRequiredHooks, getRequirementAttemptOrderWithHooks, runBeforeSignPaymentHooks, runAfterPaymentResponseHooks } from '@armory-sh/base/client-hooks-runtime';
1
3
  import { Web3 } from 'web3';
2
- import { encodePaymentV2, validatePaymentConfig, isValidationError, resolveNetwork, resolveToken, V2_HEADERS, getNetworkConfig, getNetworkByChainId, combineSignatureV2, networkToCaip2, isX402V2PaymentRequired } from '@armory-sh/base';
3
4
 
4
5
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
6
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -74,7 +75,9 @@ var validateTransferWithAuthorization = (message) => {
74
75
  var parseSignature = (signature) => {
75
76
  const hexSig = signature.startsWith("0x") ? signature.slice(2) : signature;
76
77
  if (hexSig.length !== 130) {
77
- throw new Error(`Invalid signature length: ${hexSig.length} (expected 130)`);
78
+ throw new Error(
79
+ `Invalid signature length: ${hexSig.length} (expected 130)`
80
+ );
78
81
  }
79
82
  return {
80
83
  r: `0x${hexSig.slice(0, 64)}`,
@@ -103,40 +106,6 @@ var signWithPrivateKey = async (_privateKey, _domain, _message) => {
103
106
  "Direct private key signing not implemented. Use wallet provider's signTypedData method instead."
104
107
  );
105
108
  };
106
-
107
- // src/bytes.ts
108
- new TextEncoder();
109
- var textDecoder = new TextDecoder();
110
- function getNodeBuffer() {
111
- if ("Buffer" in globalThis) {
112
- return globalThis.Buffer;
113
- }
114
- return void 0;
115
- }
116
- function fromBase64(base64) {
117
- if (typeof atob === "function") {
118
- const binary = atob(base64);
119
- const bytes = new Uint8Array(binary.length);
120
- for (let index = 0; index < binary.length; index += 1) {
121
- bytes[index] = binary.charCodeAt(index);
122
- }
123
- return bytes;
124
- }
125
- const nodeBuffer = getNodeBuffer();
126
- if (nodeBuffer) {
127
- return Uint8Array.from(nodeBuffer.from(base64, "base64"));
128
- }
129
- throw new Error("No base64 decoder available in this runtime");
130
- }
131
- function decodeBase64ToUtf8(value) {
132
- const bytes = fromBase64(value);
133
- return textDecoder.decode(bytes);
134
- }
135
- function normalizeBase64Url(value) {
136
- return value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
137
- }
138
-
139
- // src/protocol.ts
140
109
  var detectX402Version = (_response, _fallbackVersion = 2) => {
141
110
  return 2;
142
111
  };
@@ -230,7 +199,8 @@ var getAddress = (account) => {
230
199
  };
231
200
  var parseSignature2 = (signature) => {
232
201
  const hexSig = signature.startsWith("0x") ? signature.slice(2) : signature;
233
- if (hexSig.length !== 130) throw new Error(`Invalid signature length: ${hexSig.length}`);
202
+ if (hexSig.length !== 130)
203
+ throw new Error(`Invalid signature length: ${hexSig.length}`);
234
204
  return {
235
205
  r: `0x${hexSig.slice(0, 64)}`,
236
206
  s: `0x${hexSig.slice(64, 128)}`,
@@ -256,38 +226,52 @@ var signTypedDataWrapper = async (account, domain, message) => {
256
226
  return parseSignature2(sig);
257
227
  }
258
228
  if ("privateKey" in account && typeof account.privateKey === "string") {
259
- throw new Error("Direct private key signing not implemented. Use wallet provider with signTypedData.");
229
+ throw new Error(
230
+ "Direct private key signing not implemented. Use wallet provider with signTypedData."
231
+ );
260
232
  }
261
233
  throw new Error("Account does not support EIP-712 signing.");
262
234
  };
263
235
  var signPaymentV2 = async (state, network, params) => {
264
236
  const { from, to, amount, nonce, expiry, accepted } = params;
265
- const { domainName, domainVersion } = state;
266
- const domain = createEIP712Domain(network.chainId, network.usdcAddress, domainName, domainVersion);
237
+ const defaultAccepted = accepted ?? {
238
+ scheme: "exact",
239
+ network: networkToCaip2(network.name),
240
+ amount,
241
+ asset: network.usdcAddress,
242
+ payTo: to,
243
+ maxTimeoutSeconds: expiry - Math.floor(Date.now() / 1e3)
244
+ };
245
+ const domainExtra = defaultAccepted.extra;
246
+ const requirementDomainName = defaultAccepted.name ?? (domainExtra && typeof domainExtra === "object" && typeof domainExtra.name === "string" ? domainExtra.name : void 0);
247
+ const requirementDomainVersion = defaultAccepted.version ?? (domainExtra && typeof domainExtra === "object" && typeof domainExtra.version === "string" ? domainExtra.version : void 0);
248
+ const effectiveDomainName = state.domainName ?? requirementDomainName;
249
+ const effectiveDomainVersion = state.domainVersion ?? requirementDomainVersion;
250
+ const chainId = parseInt(defaultAccepted.network.split(":")[1], 10);
251
+ const domain = createEIP712Domain(
252
+ chainId,
253
+ defaultAccepted.asset,
254
+ effectiveDomainName,
255
+ effectiveDomainVersion
256
+ );
257
+ const nowSeconds = Math.floor(Date.now() / 1e3);
258
+ const validAfterHex = `0x${(nowSeconds - 600).toString(16)}`;
267
259
  const message = createTransferWithAuthorization({
268
260
  from,
269
261
  to,
270
262
  value: amount,
271
- validAfter: "0x0",
263
+ validAfter: validAfterHex,
272
264
  validBefore: `0x${expiry.toString(16)}`,
273
265
  nonce: `0x${nonce}`
274
266
  });
275
267
  const signature = await signTypedDataWrapper(state.account, domain, message);
276
268
  const combinedSig = combineSignatureV2(signature.v, signature.r, signature.s);
277
- const defaultAccepted = accepted ?? {
278
- scheme: "exact",
279
- network: networkToCaip2(network.name),
280
- amount,
281
- asset: network.usdcAddress,
282
- payTo: to,
283
- maxTimeoutSeconds: expiry - Math.floor(Date.now() / 1e3)
284
- };
285
269
  const x402Payload = createX402V2Payment({
286
270
  from,
287
271
  to,
288
272
  value: amount,
289
273
  nonce: `0x${nonce.padStart(64, "0")}`,
290
- validAfter: "0x0",
274
+ validAfter: validAfterHex,
291
275
  validBefore: `0x${expiry.toString(16)}`,
292
276
  signature: combinedSig,
293
277
  network: defaultAccepted.network,
@@ -315,31 +299,111 @@ var extractNetworkFromRequirements = (requirements) => {
315
299
  }
316
300
  return network;
317
301
  };
302
+ var getPaymentFailureDetail = async (response) => {
303
+ const text = (await response.clone().text()).trim();
304
+ if (!text) {
305
+ return void 0;
306
+ }
307
+ try {
308
+ const parsed = JSON.parse(text);
309
+ if (typeof parsed.message === "string" && parsed.message.trim()) {
310
+ return parsed.message.trim();
311
+ }
312
+ if (typeof parsed.error === "string" && parsed.error.trim()) {
313
+ return parsed.error.trim();
314
+ }
315
+ } catch {
316
+ }
317
+ return text;
318
+ };
319
+ var createPaymentVerificationError = async (response) => {
320
+ const detail = await getPaymentFailureDetail(response);
321
+ return new Error(
322
+ detail ? `Payment verification failed: ${detail}` : "Payment verification failed"
323
+ );
324
+ };
318
325
  var createX402Client = (config) => {
319
326
  const state = createClientState(config);
327
+ const hooks = config.hooks;
320
328
  const fetch2 = async (url, init) => {
321
- let response = await fetch2(url, init);
329
+ let response = await globalThis.fetch(url, init);
322
330
  if (response.status === 402) {
323
331
  const parsed = await parsePaymentRequired(response);
324
- const selectedRequirements = parsed.requirements[0];
332
+ const selectedRequirements = parsed.requirements.find(
333
+ (requirement) => requirement.network === state.network?.caip2Id
334
+ ) ?? parsed.requirements[0];
325
335
  if (!selectedRequirements) {
326
336
  throw new Error("No supported payment scheme found in requirements");
327
337
  }
328
338
  const from = getAddress(state.account);
329
- const req = selectedRequirements;
330
- const to = typeof req.payTo === "string" ? req.payTo : "0x0000000000000000000000000000000000000000";
331
- const network = extractNetworkFromRequirements(req);
332
- const result = await signPaymentV2(state, network, {
333
- from,
334
- to,
335
- amount: req.amount,
336
- nonce: crypto.randomUUID(),
337
- expiry: Math.floor(Date.now() / 1e3) + DEFAULT_EXPIRY_SECONDS,
338
- accepted: req
339
- });
340
- const paymentHeaders = new Headers(init?.headers);
341
- paymentHeaders.set(V2_HEADERS.PAYMENT_SIGNATURE, encodePaymentV2(result.payload));
342
- response = await fetch2(url, { ...init, headers: paymentHeaders });
339
+ const paymentRequiredContext = {
340
+ url,
341
+ requestInit: init,
342
+ accepts: parsed.requirements,
343
+ requirements: selectedRequirements,
344
+ selectedRequirement: selectedRequirements,
345
+ serverExtensions: void 0,
346
+ fromAddress: from,
347
+ nonce: `0x${Date.now().toString(16).padStart(64, "0")}`,
348
+ validBefore: Math.floor(Date.now() / 1e3) + selectedRequirements.maxTimeoutSeconds
349
+ };
350
+ await runOnPaymentRequiredHooks(hooks, paymentRequiredContext);
351
+ const attemptRequirements = await getRequirementAttemptOrderWithHooks(
352
+ hooks,
353
+ paymentRequiredContext
354
+ );
355
+ let lastError;
356
+ for (const req of attemptRequirements) {
357
+ try {
358
+ const to = typeof req.payTo === "string" ? req.payTo : "0x0000000000000000000000000000000000000000";
359
+ const attemptValidBefore = Math.floor(Date.now() / 1e3) + req.maxTimeoutSeconds;
360
+ const attemptContext = {
361
+ ...paymentRequiredContext,
362
+ requirements: req,
363
+ selectedRequirement: req,
364
+ validBefore: attemptValidBefore
365
+ };
366
+ const network = extractNetworkFromRequirements(req);
367
+ const result = await signPaymentV2(state, network, {
368
+ from,
369
+ to,
370
+ amount: req.amount,
371
+ nonce: crypto.randomUUID(),
372
+ expiry: attemptValidBefore,
373
+ accepted: req
374
+ });
375
+ await runBeforeSignPaymentHooks(hooks, {
376
+ payload: result.payload,
377
+ requirements: req,
378
+ wallet: state.account,
379
+ paymentContext: attemptContext
380
+ });
381
+ const paymentHeaders = new Headers(init?.headers);
382
+ paymentHeaders.set(
383
+ V2_HEADERS.PAYMENT_SIGNATURE,
384
+ encodePaymentV2(result.payload)
385
+ );
386
+ response = await globalThis.fetch(url, {
387
+ ...init,
388
+ headers: paymentHeaders
389
+ });
390
+ await runAfterPaymentResponseHooks(hooks, {
391
+ payload: result.payload,
392
+ requirements: req,
393
+ wallet: state.account,
394
+ paymentContext: attemptContext,
395
+ response
396
+ });
397
+ if (response.status === 402) {
398
+ lastError = await createPaymentVerificationError(response);
399
+ continue;
400
+ }
401
+ return response;
402
+ } catch (error) {
403
+ lastError = error instanceof Error ? error : new Error(String(error));
404
+ }
405
+ }
406
+ throw lastError ?? new Error("Payment verification failed");
343
407
  }
344
408
  return response;
345
409
  };
@@ -350,7 +414,9 @@ var createX402Client = (config) => {
350
414
  getVersion: () => state.version,
351
415
  signPayment: async (options) => {
352
416
  const network = state.network ?? (() => {
353
- throw new Error("Network must be configured for manual payment signing");
417
+ throw new Error(
418
+ "Network must be configured for manual payment signing"
419
+ );
354
420
  })();
355
421
  const from = getAddress(state.account);
356
422
  const to = options.to;
@@ -361,14 +427,22 @@ var createX402Client = (config) => {
361
427
  },
362
428
  createPaymentHeaders: async (options) => {
363
429
  const network = state.network ?? (() => {
364
- throw new Error("Network must be configured for manual payment signing");
430
+ throw new Error(
431
+ "Network must be configured for manual payment signing"
432
+ );
365
433
  })();
366
434
  const from = getAddress(state.account);
367
435
  const to = options.to;
368
436
  const amount = options.amount.toString();
369
437
  const nonce = options.nonce ?? crypto.randomUUID();
370
438
  const expiry = options.expiry ?? Math.floor(Date.now() / 1e3) + DEFAULT_EXPIRY_SECONDS;
371
- const result = await signPaymentV2(state, network, { from, to, amount, nonce, expiry });
439
+ const result = await signPaymentV2(state, network, {
440
+ from,
441
+ to,
442
+ amount,
443
+ nonce,
444
+ expiry
445
+ });
372
446
  const headers = new Headers();
373
447
  headers.set("PAYMENT-SIGNATURE", encodePaymentV2(result.payload));
374
448
  return headers;
@@ -391,69 +465,8 @@ var createX402Client = (config) => {
391
465
  }
392
466
  };
393
467
  };
394
- var DEFAULT_MAX_RETRIES = 3;
395
- var createPaymentHeaders = (payload, _version) => {
396
- const headers = new Headers();
397
- headers.set(V2_HEADERS.PAYMENT_SIGNATURE, encodePaymentV2(payload));
398
- return headers;
399
- };
400
- var isPaymentRelatedError = (error) => error.message.includes("402") || error.message.includes("payment") || error.message.includes("signature") || error.message.includes("Payment");
401
- var backoff = (attempt) => {
402
- const baseDelay = Math.min(1e3 * Math.pow(2, attempt - 1), 1e4);
403
- const jitter = Math.random() * 100;
404
- return new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
405
- };
406
- var handlePaymentRequired = async (response, client) => {
407
- detectX402Version(response, client.getVersion());
408
- const parsed = await parsePaymentRequired(response);
409
- const selectedRequirements = parsed.requirements[0];
410
- if (!selectedRequirements) {
411
- throw new Error("No supported payment scheme found in requirements");
412
- }
413
- const req = selectedRequirements;
414
- const result = await client.handlePaymentRequired(req);
415
- return createPaymentHeaders(result.payload);
416
- };
417
- var mergePaymentHeaders = (init = {}, paymentHeaders) => {
418
- const existingHeaders = new Headers(init.headers);
419
- for (const [key, value] of paymentHeaders.entries()) {
420
- existingHeaders.set(key, value);
421
- }
422
- return { ...init, headers: existingHeaders };
423
- };
424
- var createX402Transport = (options) => {
425
- const client = options.client;
426
- const autoSign = options.autoSign ?? true;
427
- const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
428
- return {
429
- getClient: () => client,
430
- fetch: async (url, init) => {
431
- let attempt = 0;
432
- let lastError;
433
- while (attempt < maxRetries) {
434
- attempt++;
435
- try {
436
- const response = await fetch(url, init);
437
- if (isPaymentRequiredResponse(response) && autoSign) {
438
- const paymentHeaders = await handlePaymentRequired(response, client);
439
- const newInit = mergePaymentHeaders(init, paymentHeaders);
440
- return await fetch(url, newInit);
441
- }
442
- return response;
443
- } catch (error) {
444
- lastError = error instanceof Error ? error : new Error(String(error));
445
- if (!isPaymentRelatedError(lastError)) {
446
- throw lastError;
447
- }
448
- if (attempt < maxRetries) {
449
- await backoff(attempt);
450
- }
451
- }
452
- }
453
- throw lastError ?? new Error("Max retries exceeded");
454
- }
455
- };
456
- };
468
+
469
+ // src/payment-api.ts
457
470
  var normalizeWallet = (wallet) => {
458
471
  if (typeof wallet === "object" && wallet !== null && "account" in wallet) {
459
472
  return wallet.account;
@@ -522,19 +535,37 @@ var armoryPay = async (wallet, url, network, token, options) => {
522
535
  }
523
536
  };
524
537
  var armoryGet = (wallet, url, network, token, options) => {
525
- return armoryPay(wallet, url, network, token, { ...options, method: "GET" });
538
+ return armoryPay(wallet, url, network, token, {
539
+ ...options,
540
+ method: "GET"
541
+ });
526
542
  };
527
543
  var armoryPost = (wallet, url, network, token, body, options) => {
528
- return armoryPay(wallet, url, network, token, { ...options, method: "POST", body });
544
+ return armoryPay(wallet, url, network, token, {
545
+ ...options,
546
+ method: "POST",
547
+ body
548
+ });
529
549
  };
530
550
  var armoryPut = (wallet, url, network, token, body, options) => {
531
- return armoryPay(wallet, url, network, token, { ...options, method: "PUT", body });
551
+ return armoryPay(wallet, url, network, token, {
552
+ ...options,
553
+ method: "PUT",
554
+ body
555
+ });
532
556
  };
533
557
  var armoryDelete = (wallet, url, network, token, options) => {
534
- return armoryPay(wallet, url, network, token, { ...options, method: "DELETE" });
558
+ return armoryPay(wallet, url, network, token, {
559
+ ...options,
560
+ method: "DELETE"
561
+ });
535
562
  };
536
563
  var armoryPatch = (wallet, url, network, token, body, options) => {
537
- return armoryPay(wallet, url, network, token, { ...options, method: "PATCH", body });
564
+ return armoryPay(wallet, url, network, token, {
565
+ ...options,
566
+ method: "PATCH",
567
+ body
568
+ });
538
569
  };
539
570
  var getWalletAddress = (wallet) => {
540
571
  const account = normalizeWallet(wallet);
@@ -550,7 +581,7 @@ var validateNetwork = (network) => {
550
581
  return { success: true, network: resolved.config.name };
551
582
  };
552
583
  var validateToken = (token, network) => {
553
- let resolvedNetwork = void 0;
584
+ let resolvedNetwork;
554
585
  if (network) {
555
586
  const networkResult = resolveNetwork(network);
556
587
  if (isValidationError(networkResult)) {
@@ -578,7 +609,13 @@ var getTokens = () => {
578
609
  };
579
610
 
580
611
  // src/armory-api.ts
581
- var ALL_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH"]);
612
+ var ALL_METHODS = /* @__PURE__ */ new Set([
613
+ "GET",
614
+ "POST",
615
+ "PUT",
616
+ "DELETE",
617
+ "PATCH"
618
+ ]);
582
619
  var arrayify = (value) => {
583
620
  if (value === void 0) return void 0;
584
621
  return Array.isArray(value) ? value : [value];
@@ -634,5 +671,73 @@ var createArmory = (config) => {
634
671
  call: (url) => makeRequest(url, "GET")
635
672
  };
636
673
  };
674
+ var DEFAULT_MAX_RETRIES = 3;
675
+ var createPaymentHeaders = (payload, _version) => {
676
+ const headers = new Headers();
677
+ headers.set(V2_HEADERS.PAYMENT_SIGNATURE, encodePaymentV2(payload));
678
+ return headers;
679
+ };
680
+ var isPaymentRelatedError = (error) => error.message.includes("402") || error.message.includes("payment") || error.message.includes("signature") || error.message.includes("Payment");
681
+ var backoff = (attempt) => {
682
+ const baseDelay = Math.min(1e3 * 2 ** (attempt - 1), 1e4);
683
+ const jitter = Math.random() * 100;
684
+ return new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
685
+ };
686
+ var handlePaymentRequired = async (response, client) => {
687
+ detectX402Version(response, client.getVersion());
688
+ const parsed = await parsePaymentRequired(response);
689
+ const selectedRequirements = parsed.requirements.find(
690
+ (requirement) => requirement.network === client.getNetwork()?.caip2Id
691
+ ) ?? parsed.requirements[0];
692
+ if (!selectedRequirements) {
693
+ throw new Error("No supported payment scheme found in requirements");
694
+ }
695
+ const req = selectedRequirements;
696
+ const result = await client.handlePaymentRequired(req);
697
+ return createPaymentHeaders(result.payload);
698
+ };
699
+ var mergePaymentHeaders = (init = {}, paymentHeaders) => {
700
+ const existingHeaders = new Headers(init.headers);
701
+ for (const [key, value] of paymentHeaders.entries()) {
702
+ existingHeaders.set(key, value);
703
+ }
704
+ return { ...init, headers: existingHeaders };
705
+ };
706
+ var createX402Transport = (options) => {
707
+ const client = options.client;
708
+ const autoSign = options.autoSign ?? true;
709
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
710
+ return {
711
+ getClient: () => client,
712
+ fetch: async (url, init) => {
713
+ let attempt = 0;
714
+ let lastError;
715
+ while (attempt < maxRetries) {
716
+ attempt++;
717
+ try {
718
+ const response = await fetch(url, init);
719
+ if (isPaymentRequiredResponse(response) && autoSign) {
720
+ const paymentHeaders = await handlePaymentRequired(
721
+ response,
722
+ client
723
+ );
724
+ const newInit = mergePaymentHeaders(init, paymentHeaders);
725
+ return await fetch(url, newInit);
726
+ }
727
+ return response;
728
+ } catch (error) {
729
+ lastError = error instanceof Error ? error : new Error(String(error));
730
+ if (!isPaymentRelatedError(lastError)) {
731
+ throw lastError;
732
+ }
733
+ if (attempt < maxRetries) {
734
+ await backoff(attempt);
735
+ }
736
+ }
737
+ }
738
+ throw lastError ?? new Error("Max retries exceeded");
739
+ }
740
+ };
741
+ };
637
742
 
638
743
  export { adjustVForChainId, armoryDelete, armoryGet, armoryPatch, armoryPay, armoryPost, armoryPut, concatenateSignature, createArmory, createEIP712Domain, createTransferWithAuthorization, createX402Client, createX402Transport, getNetworks, getTokens, getWalletAddress, normalizeWallet, parseSignature, signTypedData, signWithPrivateKey, validateNetwork, validateToken, validateTransferWithAuthorization };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@armory-sh/client-web3",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "license": "MIT",
5
5
  "author": "Sawyer Cutler <sawyer@dirtroad.dev>",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  "directory": "packages/client-web3"
47
47
  },
48
48
  "dependencies": {
49
- "@armory-sh/base": "0.2.28",
49
+ "@armory-sh/base": "0.2.30",
50
50
  "web3": "4.16.0",
51
51
  "web3-types": "1.10.0"
52
52
  },
@@ -56,6 +56,8 @@
56
56
  },
57
57
  "scripts": {
58
58
  "build": "rm -rf dist && tsup",
59
+ "lint": "bun run build",
60
+ "format": "bun run lint",
59
61
  "test": "bun test",
60
62
  "example": "bun run examples/"
61
63
  }