@alleyboss/micropay-solana-x402-paywall 3.2.2 → 3.3.1

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/index.js CHANGED
@@ -4,7 +4,7 @@ export * from '@x402/core/client';
4
4
  export * from '@x402/svm';
5
5
  import { decodePaymentRequiredHeader, encodePaymentSignatureHeader } from '@x402/core/http';
6
6
  import { PublicKey, Transaction, SystemProgram, LAMPORTS_PER_SOL, Keypair, TransactionMessage, VersionedTransaction, ComputeBudgetProgram } from '@solana/web3.js';
7
- import { useState, useCallback, useEffect } from 'react';
7
+ import { useState, useCallback, useEffect, useRef } from 'react';
8
8
  import bs58 from 'bs58';
9
9
  import { SignJWT, jwtVerify } from 'jose';
10
10
 
@@ -41,8 +41,8 @@ function buildSolanaPayUrl(params) {
41
41
  }
42
42
  return url.toString();
43
43
  }
44
- function createPaymentFlow(config2) {
45
- const { network, recipientWallet, amount, asset = "native", memo } = config2;
44
+ function createPaymentFlow(config) {
45
+ const { network, recipientWallet, amount, asset = "native", memo } = config;
46
46
  let decimals = 9;
47
47
  let mintAddress;
48
48
  if (asset === "usdc") {
@@ -58,7 +58,7 @@ function createPaymentFlow(config2) {
58
58
  const naturalAmount = Number(amount) / Math.pow(10, decimals);
59
59
  return {
60
60
  /** Get the payment configuration */
61
- getConfig: () => ({ ...config2 }),
61
+ getConfig: () => ({ ...config }),
62
62
  /** Get amount in natural display units (e.g., 0.01 SOL) */
63
63
  getDisplayAmount: () => naturalAmount,
64
64
  /** Get amount formatted with symbol */
@@ -188,13 +188,17 @@ function usePaywallResource({
188
188
  if (wwwAuth) {
189
189
  setPaymentHeader(wwwAuth);
190
190
  try {
191
- const match = wwwAuth.match(/amount="([^"]+)",\s*payTo="([^"]+)"/);
192
- if (match) {
193
- setPrice(BigInt(match[1]));
194
- setRecipient(match[2]);
191
+ const { decodePaymentRequiredHeader: decodePaymentRequiredHeader2 } = await import('@x402/core/http');
192
+ const cleanHeader = wwwAuth.replace(/^[Xx]402\s+/, "");
193
+ const decoded = decodePaymentRequiredHeader2(cleanHeader);
194
+ const accepts = Array.isArray(decoded.accepts) ? decoded.accepts[0] : decoded.accepts;
195
+ if (accepts) {
196
+ const amountStr = accepts.amount || accepts.maxAmountRequired || "0";
197
+ setPrice(BigInt(amountStr));
198
+ setRecipient(accepts.payTo);
195
199
  }
196
200
  } catch (e) {
197
- console.warn("Failed to parse 402 details from header", e);
201
+ console.warn("[usePaywallResource] Failed to parse x402 header:", e);
198
202
  }
199
203
  }
200
204
  setIsLocked(true);
@@ -246,10 +250,265 @@ function usePaywallResource({
246
250
  unlock
247
251
  };
248
252
  }
253
+
254
+ // src/pricing/utils.ts
255
+ function lamportsToSol(lamports) {
256
+ return Number(lamports) / 1e9;
257
+ }
258
+
259
+ // src/pricing/index.ts
260
+ var priceCache = null;
261
+ var currentConfig = {};
262
+ function configurePricing(newConfig) {
263
+ currentConfig = { ...currentConfig, ...newConfig };
264
+ }
265
+ var PROVIDERS = [
266
+ {
267
+ name: "coincap",
268
+ url: "https://api.coincap.io/v2/assets/solana",
269
+ parse: (data) => parseFloat(data.data?.priceUsd || "0")
270
+ },
271
+ {
272
+ name: "binance",
273
+ url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
274
+ parse: (data) => parseFloat(data.price || "0")
275
+ },
276
+ {
277
+ name: "coingecko",
278
+ url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
279
+ parse: (data) => data.solana?.usd || 0
280
+ },
281
+ {
282
+ name: "kraken",
283
+ url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
284
+ parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
285
+ }
286
+ ];
287
+ async function fetchFromProvider(provider, timeout) {
288
+ const controller = new AbortController();
289
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
290
+ try {
291
+ const response = await fetch(provider.url, {
292
+ headers: { "Accept": "application/json" },
293
+ signal: controller.signal
294
+ });
295
+ if (!response.ok) {
296
+ throw new Error(`HTTP ${response.status}`);
297
+ }
298
+ const data = await response.json();
299
+ const price = provider.parse(data);
300
+ if (!price || price <= 0) {
301
+ throw new Error("Invalid price");
302
+ }
303
+ return { price, source: provider.name };
304
+ } finally {
305
+ clearTimeout(timeoutId);
306
+ }
307
+ }
308
+ async function fetchPriceParallel(timeout) {
309
+ const promises = PROVIDERS.map(
310
+ (provider) => fetchFromProvider(provider, timeout).catch(() => null)
311
+ );
312
+ const results = await Promise.all(promises);
313
+ const validResult = results.find((r) => r !== null);
314
+ if (validResult) {
315
+ return validResult;
316
+ }
317
+ throw new Error("All providers failed");
318
+ }
319
+ async function fetchPriceSequential(timeout) {
320
+ for (const provider of PROVIDERS) {
321
+ try {
322
+ return await fetchFromProvider(provider, timeout);
323
+ } catch {
324
+ continue;
325
+ }
326
+ }
327
+ throw new Error("All providers failed");
328
+ }
329
+ async function getSolPrice() {
330
+ const cacheTTL = currentConfig.cacheTTL ?? 6e4;
331
+ const timeout = currentConfig.timeout ?? 3e3;
332
+ const useParallel = currentConfig.parallelFetch ?? true;
333
+ const now = Date.now();
334
+ if (priceCache && now - priceCache.timestamp < cacheTTL) {
335
+ return priceCache.data;
336
+ }
337
+ if (currentConfig.customProvider) {
338
+ try {
339
+ const price = await currentConfig.customProvider();
340
+ if (price > 0) {
341
+ const data = {
342
+ solPrice: price,
343
+ fetchedAt: /* @__PURE__ */ new Date(),
344
+ source: "custom"
345
+ };
346
+ priceCache = { data, timestamp: now };
347
+ return data;
348
+ }
349
+ } catch {
350
+ }
351
+ }
352
+ try {
353
+ const result = useParallel ? await fetchPriceParallel(timeout) : await fetchPriceSequential(timeout);
354
+ const data = {
355
+ solPrice: result.price,
356
+ fetchedAt: /* @__PURE__ */ new Date(),
357
+ source: result.source
358
+ };
359
+ priceCache = { data, timestamp: now };
360
+ return data;
361
+ } catch {
362
+ if (priceCache) {
363
+ return {
364
+ ...priceCache.data,
365
+ source: `${priceCache.data.source} (stale)`
366
+ };
367
+ }
368
+ throw new Error(
369
+ "Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
370
+ );
371
+ }
372
+ }
373
+ async function lamportsToUsd(lamports) {
374
+ const { solPrice } = await getSolPrice();
375
+ const sol = Number(lamports) / 1e9;
376
+ return sol * solPrice;
377
+ }
378
+ async function usdToLamports(usd) {
379
+ const { solPrice } = await getSolPrice();
380
+ const sol = usd / solPrice;
381
+ return BigInt(Math.floor(sol * 1e9));
382
+ }
383
+ async function formatPriceDisplay(lamports) {
384
+ const { solPrice } = await getSolPrice();
385
+ const sol = Number(lamports) / 1e9;
386
+ const usd = sol * solPrice;
387
+ return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
388
+ }
389
+ function formatPriceSync(lamports, solPrice) {
390
+ const sol = Number(lamports) / 1e9;
391
+ const usd = sol * solPrice;
392
+ return {
393
+ sol,
394
+ usd,
395
+ formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
396
+ };
397
+ }
398
+ function clearPriceCache() {
399
+ priceCache = null;
400
+ }
401
+ function getProviders() {
402
+ return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
403
+ }
404
+
405
+ // src/client/hooks.ts
406
+ function usePricing(refreshIntervalMs = 6e4) {
407
+ const [priceData, setPriceData] = useState(null);
408
+ const [isLoading, setIsLoading] = useState(true);
409
+ const [error, setError] = useState(null);
410
+ const intervalRef = useRef(null);
411
+ const fetchPrice = useCallback(async () => {
412
+ try {
413
+ setIsLoading(true);
414
+ setError(null);
415
+ const data = await getSolPrice();
416
+ setPriceData(data);
417
+ } catch (err) {
418
+ setError(err instanceof Error ? err.message : "Failed to fetch price");
419
+ } finally {
420
+ setIsLoading(false);
421
+ }
422
+ }, []);
423
+ useEffect(() => {
424
+ fetchPrice();
425
+ if (refreshIntervalMs > 0) {
426
+ intervalRef.current = setInterval(fetchPrice, refreshIntervalMs);
427
+ }
428
+ return () => {
429
+ if (intervalRef.current) {
430
+ clearInterval(intervalRef.current);
431
+ }
432
+ };
433
+ }, [fetchPrice, refreshIntervalMs]);
434
+ return {
435
+ solPrice: priceData?.solPrice ?? null,
436
+ source: priceData?.source ?? null,
437
+ fetchedAt: priceData?.fetchedAt ?? null,
438
+ isLoading,
439
+ error,
440
+ refresh: fetchPrice
441
+ };
442
+ }
443
+ function useLamportsToUsd(lamports) {
444
+ const { solPrice, isLoading } = usePricing();
445
+ if (isLoading || !solPrice || lamports === null) {
446
+ return { usd: null, formatted: null, isLoading };
447
+ }
448
+ const lamportsBigInt = typeof lamports === "number" ? BigInt(lamports) : lamports;
449
+ const sol = Number(lamportsBigInt) / 1e9;
450
+ const usd = sol * solPrice;
451
+ return {
452
+ usd,
453
+ formatted: `$${usd.toFixed(2)}`,
454
+ isLoading: false
455
+ };
456
+ }
457
+ function useMicropay() {
458
+ const [status, setStatus] = useState("idle");
459
+ const [error, setError] = useState(null);
460
+ const [signature, setSignature] = useState(null);
461
+ const reset = useCallback(() => {
462
+ setStatus("idle");
463
+ setError(null);
464
+ setSignature(null);
465
+ }, []);
466
+ const pay = useCallback(async (_options) => {
467
+ setStatus("pending");
468
+ setError(null);
469
+ try {
470
+ throw new Error(
471
+ "useMicropay requires implementation of onSign/onSend callbacks. See documentation for wallet adapter integration."
472
+ );
473
+ } catch (err) {
474
+ const errorMessage = err instanceof Error ? err.message : "Payment failed";
475
+ setError(errorMessage);
476
+ setStatus("error");
477
+ return { success: false, error: errorMessage };
478
+ }
479
+ }, []);
480
+ return {
481
+ status,
482
+ error,
483
+ signature,
484
+ pay,
485
+ reset
486
+ };
487
+ }
488
+ function useFormatPrice(lamports) {
489
+ const { solPrice, isLoading } = usePricing();
490
+ if (isLoading || !solPrice || lamports === null) {
491
+ return {
492
+ sol: null,
493
+ usd: null,
494
+ formatted: "Loading...",
495
+ isLoading
496
+ };
497
+ }
498
+ const lamportsBigInt = typeof lamports === "number" ? BigInt(lamports) : lamports;
499
+ const sol = Number(lamportsBigInt) / 1e9;
500
+ const usd = sol * solPrice;
501
+ return {
502
+ sol,
503
+ usd,
504
+ formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`,
505
+ isLoading: false
506
+ };
507
+ }
249
508
  var DEFAULT_COMPUTE_UNITS = 2e5;
250
509
  var DEFAULT_MICRO_LAMPORTS = 1e3;
251
- function createPriorityFeeInstructions(config2 = {}) {
252
- const { enabled = false, microLamports, computeUnits } = config2;
510
+ function createPriorityFeeInstructions(config = {}) {
511
+ const { enabled = false, microLamports, computeUnits } = config;
253
512
  if (!enabled) {
254
513
  return [];
255
514
  }
@@ -260,14 +519,14 @@ function createPriorityFeeInstructions(config2 = {}) {
260
519
  instructions.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: price }));
261
520
  return instructions;
262
521
  }
263
- async function buildVersionedTransaction(config2) {
522
+ async function buildVersionedTransaction(config) {
264
523
  const {
265
524
  connection,
266
525
  payer,
267
526
  instructions,
268
527
  priorityFee,
269
528
  recentBlockhash
270
- } = config2;
529
+ } = config;
271
530
  const priorityIxs = createPriorityFeeInstructions(priorityFee);
272
531
  const allInstructions = [...priorityIxs, ...instructions];
273
532
  let blockhash;
@@ -383,9 +642,9 @@ async function getAgentBalance(connection, agentKeypair) {
383
642
  balanceSol: balance / LAMPORTS_PER_SOL
384
643
  };
385
644
  }
386
- async function hasAgentSufficientBalance(connection, agentKeypair, requiredLamports) {
645
+ async function hasAgentSufficientBalance(connection, agentKeypair, requiredLamports, feeBufferLamports = 5000000n) {
387
646
  const { balance } = await getAgentBalance(connection, agentKeypair);
388
- const totalRequired = requiredLamports + 10000n;
647
+ const totalRequired = requiredLamports + feeBufferLamports;
389
648
  return {
390
649
  sufficient: balance >= totalRequired,
391
650
  balance,
@@ -434,20 +693,20 @@ function validateWalletAddress(address) {
434
693
  const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
435
694
  return base58Regex.test(address);
436
695
  }
437
- async function createCreditSession(walletAddress, purchaseId, config2) {
696
+ async function createCreditSession(walletAddress, purchaseId, config) {
438
697
  if (!validateWalletAddress(walletAddress)) {
439
698
  throw new Error("Invalid wallet address format");
440
699
  }
441
- if (config2.initialCredits <= 0 || config2.initialCredits > MAX_CREDITS) {
700
+ if (config.initialCredits <= 0 || config.initialCredits > MAX_CREDITS) {
442
701
  throw new Error(`Credits must be between 1 and ${MAX_CREDITS}`);
443
702
  }
444
- if (!config2.durationHours || config2.durationHours <= 0 || config2.durationHours > 8760) {
703
+ if (!config.durationHours || config.durationHours <= 0 || config.durationHours > 8760) {
445
704
  throw new Error("Session duration must be between 1 and 8760 hours (1 year)");
446
705
  }
447
706
  const sessionId = uuidv4();
448
707
  const now = Math.floor(Date.now() / 1e3);
449
- const expiresAt = now + config2.durationHours * 3600;
450
- const bundleExpiry = config2.bundleExpiryHours ? now + config2.bundleExpiryHours * 3600 : expiresAt;
708
+ const expiresAt = now + config.durationHours * 3600;
709
+ const bundleExpiry = config.bundleExpiryHours ? now + config.bundleExpiryHours * 3600 : expiresAt;
451
710
  const session = {
452
711
  id: sessionId,
453
712
  walletAddress,
@@ -455,22 +714,22 @@ async function createCreditSession(walletAddress, purchaseId, config2) {
455
714
  siteWideUnlock: false,
456
715
  createdAt: now,
457
716
  expiresAt,
458
- credits: config2.initialCredits,
717
+ credits: config.initialCredits,
459
718
  bundleExpiry,
460
- bundleType: config2.bundleType
719
+ bundleType: config.bundleType
461
720
  };
462
721
  const payload = {
463
722
  sub: walletAddress,
464
723
  sid: sessionId,
465
724
  articles: session.unlockedArticles,
466
725
  siteWide: false,
467
- credits: config2.initialCredits,
726
+ credits: config.initialCredits,
468
727
  bundleExpiry,
469
- bundleType: config2.bundleType,
728
+ bundleType: config.bundleType,
470
729
  iat: now,
471
730
  exp: expiresAt
472
731
  };
473
- const token = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config2.durationHours}h`).sign(getSecretKey(config2.secret));
732
+ const token = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config.durationHours}h`).sign(getSecretKey(config.secret));
474
733
  return { token, session };
475
734
  }
476
735
  async function validateCreditSession(token, secret) {
@@ -588,139 +847,4 @@ async function getRemainingCredits(token, secret) {
588
847
  };
589
848
  }
590
849
 
591
- // src/pricing/utils.ts
592
- function lamportsToSol(lamports) {
593
- return Number(lamports) / 1e9;
594
- }
595
-
596
- // src/pricing/index.ts
597
- var cachedPrice = null;
598
- var config = {};
599
- var lastProviderIndex = -1;
600
- function configurePricing(newConfig) {
601
- config = { ...config, ...newConfig };
602
- cachedPrice = null;
603
- }
604
- var PROVIDERS = [
605
- {
606
- name: "coincap",
607
- url: "https://api.coincap.io/v2/assets/solana",
608
- parse: (data) => parseFloat(data.data?.priceUsd || "0")
609
- },
610
- {
611
- name: "binance",
612
- url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
613
- parse: (data) => parseFloat(data.price || "0")
614
- },
615
- {
616
- name: "coingecko",
617
- url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
618
- parse: (data) => data.solana?.usd || 0
619
- },
620
- {
621
- name: "kraken",
622
- url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
623
- parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
624
- }
625
- ];
626
- async function fetchFromProvider(provider, timeout) {
627
- const controller = new AbortController();
628
- const timeoutId = setTimeout(() => controller.abort(), timeout);
629
- try {
630
- const response = await fetch(provider.url, {
631
- headers: { "Accept": "application/json" },
632
- signal: controller.signal
633
- });
634
- if (!response.ok) {
635
- throw new Error(`HTTP ${response.status}`);
636
- }
637
- const data = await response.json();
638
- const price = provider.parse(data);
639
- if (!price || price <= 0) {
640
- throw new Error("Invalid price");
641
- }
642
- return price;
643
- } finally {
644
- clearTimeout(timeoutId);
645
- }
646
- }
647
- async function getSolPrice() {
648
- const cacheTTL = config.cacheTTL ?? 6e4;
649
- const timeout = config.timeout ?? 5e3;
650
- if (cachedPrice && Date.now() - cachedPrice.fetchedAt.getTime() < cacheTTL) {
651
- return cachedPrice;
652
- }
653
- if (config.customProvider) {
654
- try {
655
- const price = await config.customProvider();
656
- if (price > 0) {
657
- cachedPrice = {
658
- solPrice: price,
659
- fetchedAt: /* @__PURE__ */ new Date(),
660
- source: "custom"
661
- };
662
- return cachedPrice;
663
- }
664
- } catch {
665
- }
666
- }
667
- for (let i = 0; i < PROVIDERS.length; i++) {
668
- const idx = (lastProviderIndex + 1 + i) % PROVIDERS.length;
669
- const provider = PROVIDERS[idx];
670
- try {
671
- const price = await fetchFromProvider(provider, timeout);
672
- lastProviderIndex = idx;
673
- cachedPrice = {
674
- solPrice: price,
675
- fetchedAt: /* @__PURE__ */ new Date(),
676
- source: provider.name
677
- };
678
- return cachedPrice;
679
- } catch {
680
- continue;
681
- }
682
- }
683
- if (cachedPrice) {
684
- return {
685
- ...cachedPrice,
686
- source: `${cachedPrice.source} (stale)`
687
- };
688
- }
689
- throw new Error(
690
- "Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
691
- );
692
- }
693
- async function lamportsToUsd(lamports) {
694
- const { solPrice } = await getSolPrice();
695
- const sol = Number(lamports) / 1e9;
696
- return sol * solPrice;
697
- }
698
- async function usdToLamports(usd) {
699
- const { solPrice } = await getSolPrice();
700
- const sol = usd / solPrice;
701
- return BigInt(Math.floor(sol * 1e9));
702
- }
703
- async function formatPriceDisplay(lamports) {
704
- const { solPrice } = await getSolPrice();
705
- const sol = Number(lamports) / 1e9;
706
- const usd = sol * solPrice;
707
- return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
708
- }
709
- function formatPriceSync(lamports, solPrice) {
710
- const sol = Number(lamports) / 1e9;
711
- const usd = sol * solPrice;
712
- return {
713
- sol,
714
- usd,
715
- formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
716
- };
717
- }
718
- function clearPriceCache() {
719
- cachedPrice = null;
720
- lastProviderIndex = -1;
721
- }
722
- function getProviders() {
723
- return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
724
- }
725
-
726
- export { addCredits, buildSolanaPayUrl, clearPriceCache, configurePricing, createCreditSession, createPaymentFlow, createPaymentReference, createX402AuthorizationHeader, executeAgentPayment, formatPriceDisplay, formatPriceSync, generateAgentKeypair, getAgentBalance, getProviders, getRemainingCredits, getSolPrice, hasAgentSufficientBalance, keypairFromBase58, lamportsToSol, lamportsToUsd, sendSolanaPayment, usdToLamports, useCredit, usePaywallResource, validateCreditSession };
850
+ export { addCredits, buildSolanaPayUrl, clearPriceCache, configurePricing, createCreditSession, createPaymentFlow, createPaymentReference, createX402AuthorizationHeader, executeAgentPayment, formatPriceDisplay, formatPriceSync, generateAgentKeypair, getAgentBalance, getProviders, getRemainingCredits, getSolPrice, hasAgentSufficientBalance, keypairFromBase58, lamportsToSol, lamportsToUsd, sendSolanaPayment, usdToLamports, useCredit, useFormatPrice, useLamportsToUsd, useMicropay, usePaywallResource, usePricing, validateCreditSession };