bsv-x402 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +31 -10
  2. package/dist/index.cjs +393 -86
  3. package/dist/index.d.cts +73 -2
  4. package/dist/index.d.ts +73 -2
  5. package/dist/index.js +391 -86
  6. package/dist/plugins/chromium/background.js +100759 -0
  7. package/dist/plugins/chromium/content-script.js +219 -0
  8. package/dist/plugins/chromium/icons/icon-128.png +0 -0
  9. package/dist/plugins/chromium/icons/icon-16.png +0 -0
  10. package/dist/plugins/chromium/icons/icon-48.png +0 -0
  11. package/dist/plugins/chromium/manifest.json +34 -0
  12. package/dist/plugins/chromium/page-script.js +160 -0
  13. package/dist/plugins/chromium/ui/admin/utxos.html +138 -0
  14. package/dist/plugins/chromium/ui/admin/utxos.js +238 -0
  15. package/dist/plugins/chromium/ui/popup.css +661 -0
  16. package/dist/plugins/chromium/ui/popup.html +27 -0
  17. package/dist/plugins/chromium/ui/popup.js +1624 -0
  18. package/dist/plugins/chromium/ui/wallet/setup.html +353 -0
  19. package/dist/plugins/chromium/ui/wallet/setup.js +148 -0
  20. package/dist/plugins/chromium/ui/x402/approve.html +194 -0
  21. package/dist/plugins/chromium/ui/x402/approve.js +54 -0
  22. package/dist/plugins/firefox/background.js +100759 -0
  23. package/dist/plugins/firefox/content-script.js +219 -0
  24. package/dist/plugins/firefox/icons/icon-128.png +0 -0
  25. package/dist/plugins/firefox/icons/icon-16.png +0 -0
  26. package/dist/plugins/firefox/icons/icon-48.png +0 -0
  27. package/dist/plugins/firefox/manifest.json +40 -0
  28. package/dist/plugins/firefox/page-script.js +160 -0
  29. package/dist/plugins/firefox/ui/admin/utxos.html +138 -0
  30. package/dist/plugins/firefox/ui/admin/utxos.js +238 -0
  31. package/dist/plugins/firefox/ui/popup.css +661 -0
  32. package/dist/plugins/firefox/ui/popup.html +27 -0
  33. package/dist/plugins/firefox/ui/popup.js +1624 -0
  34. package/dist/plugins/firefox/ui/wallet/setup.html +353 -0
  35. package/dist/plugins/firefox/ui/wallet/setup.js +148 -0
  36. package/dist/plugins/firefox/ui/x402/approve.html +194 -0
  37. package/dist/plugins/firefox/ui/x402/approve.js +54 -0
  38. package/dist/plugins/safari/background.js +100759 -0
  39. package/dist/plugins/safari/content-script.js +219 -0
  40. package/dist/plugins/safari/icons/icon-128.png +0 -0
  41. package/dist/plugins/safari/icons/icon-16.png +0 -0
  42. package/dist/plugins/safari/icons/icon-48.png +0 -0
  43. package/dist/plugins/safari/manifest.json +34 -0
  44. package/dist/plugins/safari/page-script.js +160 -0
  45. package/dist/plugins/safari/ui/admin/utxos.html +138 -0
  46. package/dist/plugins/safari/ui/admin/utxos.js +238 -0
  47. package/dist/plugins/safari/ui/popup.css +661 -0
  48. package/dist/plugins/safari/ui/popup.html +27 -0
  49. package/dist/plugins/safari/ui/popup.js +1624 -0
  50. package/dist/plugins/safari/ui/wallet/setup.html +353 -0
  51. package/dist/plugins/safari/ui/wallet/setup.js +148 -0
  52. package/dist/plugins/safari/ui/x402/approve.html +194 -0
  53. package/dist/plugins/safari/ui/x402/approve.js +54 -0
  54. package/package.json +3 -2
package/README.md CHANGED
@@ -1,24 +1,35 @@
1
1
  # bsv-x402
2
2
 
3
- A JavaScript client library for the [x402 payment protocol](https://x402.merkleworks.io/). Wraps `fetch()` to handle HTTP 402 payment flows transparently.
3
+ A JavaScript client library and browser extension for BSV micropayments over HTTP 402. Wraps `fetch()` to transparently handle payment flows using multiple protocols (Custom X402, BRC-105, BRC-121).
4
4
 
5
5
  ## How It Works
6
6
 
7
7
  ```js
8
- import { x402Fetch } from 'bsv-x402'
8
+ import { createX402Fetch } from 'bsv-x402'
9
9
 
10
+ const x402Fetch = createX402Fetch({ brc105Wallet: window.CWI })
10
11
  const response = await x402Fetch('https://api.example.com/paid-endpoint')
11
12
  ```
12
13
 
13
14
  When the server responds with `402 Payment Required`, the library:
14
15
 
15
- 1. Parses the `X402-Challenge` header
16
+ 1. Detects the protocol from response headers (X402-Challenge, BRC-105, or BRC-121)
16
17
  2. Constructs a payment transaction via the browser wallet ([BRC-100](https://github.com/bitcoin-sv/BRCs/blob/master/wallet/0100.md) / `window.CWI`)
17
- 3. Broadcasts the transaction to the BSV network
18
- 4. Retries the original request with an `X402-Proof` header
18
+ 3. Sends the proof to the server and retries the request
19
+ 4. On server 200: broadcasts the transaction (transitioning `nosend` `unproven`)
20
+ 5. On server 4xx: aborts the transaction (freeing locked UTXOs)
19
21
 
20
22
  The app developer just uses `x402Fetch` in place of `fetch`. The browser wallet handles key management and signing.
21
23
 
24
+ ## Browser Extension
25
+
26
+ The repository also includes browser extensions (Chromium, Firefox, Safari) that provide a full BRC-100 wallet with native x402 support:
27
+
28
+ - `window.CWI` injection (all 28 BRC-100 wallet methods)
29
+ - Built-in wallet via `@bsv/wallet-toolbox-client`
30
+ - Autospend controls with Doom II difficulty tiers
31
+ - UTXO Admin view for wallet diagnostics
32
+
22
33
  ## Installation
23
34
 
24
35
  ```bash
@@ -28,12 +39,22 @@ npm install bsv-x402
28
39
  ## Development
29
40
 
30
41
  ```bash
31
- npm install # Install dependencies
32
- npm run build # Compile TypeScript → dist/
33
- npm test # Run tests
34
- npm run typecheck # Type-check without emitting
42
+ npm install # Install dependencies
43
+ npm run build # Compile TypeScript → dist/
44
+ npm test # Run tests (vitest)
45
+ npm run typecheck # Type-check without emitting
46
+ npm run build:all # Build library + all browser extensions
35
47
  ```
36
48
 
49
+ ## Documentation
50
+
51
+ - [DESIGN.md](DESIGN.md) — architecture and protocol details
52
+ - [ECONOMICS.md](ECONOMICS.md) — economics of ARC broadcasting for micropayments
53
+ - [BEEF-SIGNALLING.md](BEEF-SIGNALLING.md) — how BEEF type communicates broadcast intent
54
+ - [CHANGELOG.md](CHANGELOG.md) — version history
55
+
37
56
  ## Status
38
57
 
39
- Early development. The fetch wrapper and challenge parsing are in place; the BRC-100 wallet integration (`constructProof`) is not yet implemented.
58
+ Active development (v0.9.1). Library core: multi-protocol 402 handling (Custom X402, BRC-105, BRC-121), adversarial `noSend` payment flow with broadcast-on-200, BEEF acknowledgement mechanism. Browser extensions: full BRC-100 wallet, autospend controls, UTXO recovery tools.
59
+
60
+ Server counterpart: [x402-rack](https://github.com/sgbett/x402-rack) (Ruby/Rack middleware).
package/dist/index.cjs CHANGED
@@ -28,11 +28,13 @@ __export(index_exports, {
28
28
  clampBalanceToTier: () => clampBalanceToTier,
29
29
  constructBrc105Proof: () => constructBrc105Proof,
30
30
  constructBrc121Proof: () => constructBrc121Proof,
31
+ constructPayGatewayProof: () => constructPayGatewayProof,
31
32
  createX402Fetch: () => createX402Fetch,
32
33
  initialState: () => initialState,
33
34
  parseBrc105Challenge: () => parseBrc105Challenge,
34
35
  parseBrc121Challenge: () => parseBrc121Challenge,
35
36
  parseChallenge: () => parseChallenge,
37
+ parsePayGatewayChallenge: () => parsePayGatewayChallenge,
36
38
  recordPayment: () => recordPayment,
37
39
  x402Fetch: () => x402Fetch
38
40
  });
@@ -78,7 +80,7 @@ function parseBrc105Challenge(response) {
78
80
  };
79
81
  }
80
82
 
81
- // src/brc105-proof.ts
83
+ // src/bytes.ts
82
84
  function bytesToHex(bytes) {
83
85
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
84
86
  }
@@ -98,6 +100,8 @@ function bytesToBase64(bytes) {
98
100
  function numberArrayToBase64(arr) {
99
101
  return bytesToBase64(new Uint8Array(arr));
100
102
  }
103
+
104
+ // src/brc105-proof.ts
101
105
  var RIPEMD160_CONSTANTS = {
102
106
  // Round constants for left and right paths
103
107
  KL: [0, 1518500249, 1859775393, 2400959708, 2840853838],
@@ -523,34 +527,66 @@ async function createDerivationSuffix(wallet) {
523
527
  return numberArrayToBase64(nonceBytes);
524
528
  }
525
529
  async function constructBrc105Proof(challenge, wallet, origin) {
526
- const { publicKey: clientIdentityKey } = await wallet.getPublicKey({ identityKey: true });
527
- const derivationSuffix = await createDerivationSuffix(wallet);
530
+ let clientIdentityKey;
531
+ try {
532
+ const r = await wallet.getPublicKey({ identityKey: true });
533
+ clientIdentityKey = r.publicKey;
534
+ } catch (err) {
535
+ console.error("[x402] BRC-105 proof: failed to get identity key:", err);
536
+ throw err;
537
+ }
538
+ let derivationSuffix;
539
+ try {
540
+ derivationSuffix = await createDerivationSuffix(wallet);
541
+ } catch (err) {
542
+ console.error("[x402] BRC-105 proof: failed to generate derivation suffix:", err);
543
+ throw err;
544
+ }
528
545
  const keyID = `${challenge.derivationPrefix} ${derivationSuffix}`;
529
- const { publicKey: derivedPublicKey } = await wallet.getPublicKey({
530
- protocolID: [2, "3241645161d8"],
531
- keyID,
532
- counterparty: challenge.serverIdentityKey
533
- });
534
- const lockingScript = await pubkeyToP2PKHLockingScript(derivedPublicKey);
546
+ let derivedPublicKey;
547
+ try {
548
+ const r = await wallet.getPublicKey({
549
+ protocolID: [2, "3241645161d8"],
550
+ keyID,
551
+ counterparty: challenge.serverIdentityKey
552
+ });
553
+ derivedPublicKey = r.publicKey;
554
+ } catch (err) {
555
+ console.error("[x402] BRC-105 proof: BRC-29 key derivation failed:", err);
556
+ throw err;
557
+ }
558
+ let lockingScript;
559
+ try {
560
+ lockingScript = await pubkeyToP2PKHLockingScript(derivedPublicKey);
561
+ } catch (err) {
562
+ console.error("[x402] BRC-105 proof: P2PKH script generation failed:", err);
563
+ throw err;
564
+ }
535
565
  const description = origin ? `Payment for request to ${origin}` : "BRC-105 payment";
536
- const result = await wallet.createAction({
537
- description,
538
- outputs: [{
539
- satoshis: challenge.satoshisRequired,
540
- lockingScript,
541
- outputDescription: "HTTP request payment",
542
- customInstructions: JSON.stringify({
543
- derivationPrefix: challenge.derivationPrefix,
544
- derivationSuffix,
545
- payee: challenge.serverIdentityKey
546
- })
547
- }],
548
- options: {
549
- returnTXIDOnly: false,
550
- noSend: true,
551
- randomizeOutputs: false
552
- }
553
- });
566
+ let result;
567
+ try {
568
+ result = await wallet.createAction({
569
+ description,
570
+ outputs: [{
571
+ satoshis: challenge.satoshisRequired,
572
+ lockingScript,
573
+ outputDescription: "HTTP request payment",
574
+ customInstructions: JSON.stringify({
575
+ derivationPrefix: challenge.derivationPrefix,
576
+ derivationSuffix,
577
+ payee: challenge.serverIdentityKey
578
+ })
579
+ }],
580
+ options: {
581
+ returnTXIDOnly: false,
582
+ noSend: true,
583
+ randomizeOutputs: false
584
+ }
585
+ });
586
+ } catch (err) {
587
+ console.error(`[x402] BRC-105 proof: createAction failed (${challenge.satoshisRequired} sats):`, err);
588
+ throw err;
589
+ }
554
590
  let transactionBase64;
555
591
  if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
556
592
  transactionBase64 = numberArrayToBase64(result.tx);
@@ -608,54 +644,64 @@ function parseBrc121Challenge(response) {
608
644
  }
609
645
 
610
646
  // src/brc121-proof.ts
611
- function bytesToBase642(bytes) {
612
- let binary = "";
613
- for (const b of bytes) binary += String.fromCharCode(b);
614
- return btoa(binary);
615
- }
616
- function numberArrayToBase642(arr) {
617
- return bytesToBase642(new Uint8Array(arr));
618
- }
619
- function hexToBytes2(hex) {
620
- if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
621
- const bytes = new Uint8Array(hex.length / 2);
622
- for (let i = 0; i < hex.length; i += 2) {
623
- bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
624
- }
625
- return bytes;
626
- }
627
647
  async function constructBrc121Proof(challenge, wallet, origin) {
628
- const { publicKey: clientIdentityKey } = await wallet.getPublicKey({ identityKey: true });
648
+ let clientIdentityKey;
649
+ try {
650
+ const r = await wallet.getPublicKey({ identityKey: true });
651
+ clientIdentityKey = r.publicKey;
652
+ } catch (err) {
653
+ console.error("[x402] BRC-121 proof: failed to get identity key:", err);
654
+ throw err;
655
+ }
629
656
  const nonceBytes = crypto.getRandomValues(new Uint8Array(8));
630
657
  const nonce = btoa(String.fromCharCode(...nonceBytes));
631
658
  const time = String(Date.now());
632
659
  const timeSuffixB64 = btoa(time);
633
660
  const keyID = `${nonce} ${timeSuffixB64}`;
634
- const { publicKey: derivedPublicKey } = await wallet.getPublicKey({
635
- protocolID: [2, "3241645161d8"],
636
- keyID,
637
- counterparty: challenge.serverIdentityKey
638
- });
639
- const lockingScript = await pubkeyToP2PKHLockingScript(derivedPublicKey);
661
+ let derivedPublicKey;
662
+ try {
663
+ const r = await wallet.getPublicKey({
664
+ protocolID: [2, "3241645161d8"],
665
+ keyID,
666
+ counterparty: challenge.serverIdentityKey
667
+ });
668
+ derivedPublicKey = r.publicKey;
669
+ } catch (err) {
670
+ console.error("[x402] BRC-121 proof: BRC-29 key derivation failed:", err);
671
+ throw err;
672
+ }
673
+ let lockingScript;
674
+ try {
675
+ lockingScript = await pubkeyToP2PKHLockingScript(derivedPublicKey);
676
+ } catch (err) {
677
+ console.error("[x402] BRC-121 proof: P2PKH script generation failed:", err);
678
+ throw err;
679
+ }
640
680
  const description = origin ? `Payment for request to ${origin}` : "BRC-121 payment";
641
- const result = await wallet.createAction({
642
- description,
643
- outputs: [{
644
- satoshis: challenge.satoshis,
645
- lockingScript,
646
- outputDescription: "BRC-121 payment"
647
- }],
648
- options: {
649
- randomizeOutputs: false,
650
- noSend: true,
651
- returnTXIDOnly: false
652
- }
653
- });
681
+ let result;
682
+ try {
683
+ result = await wallet.createAction({
684
+ description,
685
+ outputs: [{
686
+ satoshis: challenge.satoshis,
687
+ lockingScript,
688
+ outputDescription: "BRC-121 payment"
689
+ }],
690
+ options: {
691
+ randomizeOutputs: false,
692
+ noSend: true,
693
+ returnTXIDOnly: false
694
+ }
695
+ });
696
+ } catch (err) {
697
+ console.error(`[x402] BRC-121 proof: createAction failed (${challenge.satoshis} sats):`, err);
698
+ throw err;
699
+ }
654
700
  let transactionBase64;
655
701
  if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
656
- transactionBase64 = numberArrayToBase642(result.tx);
702
+ transactionBase64 = numberArrayToBase64(result.tx);
657
703
  } else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
658
- transactionBase64 = bytesToBase642(hexToBytes2(result.rawTx));
704
+ transactionBase64 = bytesToBase64(hexToBytes(result.rawTx));
659
705
  } else {
660
706
  throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
661
707
  }
@@ -720,23 +766,158 @@ function parseChallenge(header) {
720
766
  };
721
767
  }
722
768
 
723
- // src/x402-fetch.ts
724
- function bytesToBase643(bytes) {
725
- let binary = "";
726
- for (const b of bytes) binary += String.fromCharCode(b);
727
- return btoa(binary);
728
- }
729
- function numberArrayToBase643(arr) {
730
- return bytesToBase643(new Uint8Array(arr));
769
+ // src/paygateway-challenge.ts
770
+ function parsePayGatewayChallenge(response) {
771
+ const header = response.headers.get("Payment-Required");
772
+ if (header === null || header.length === 0) {
773
+ return null;
774
+ }
775
+ let json;
776
+ try {
777
+ json = atob(header);
778
+ } catch {
779
+ return null;
780
+ }
781
+ let parsed;
782
+ try {
783
+ parsed = JSON.parse(json);
784
+ } catch {
785
+ return null;
786
+ }
787
+ if (typeof parsed !== "object" || parsed === null) {
788
+ return null;
789
+ }
790
+ const obj = parsed;
791
+ if (obj.x402Version !== 2) {
792
+ return null;
793
+ }
794
+ if (!Array.isArray(obj.accepts) || obj.accepts.length === 0) {
795
+ return null;
796
+ }
797
+ const bsvAccept = obj.accepts.find((entry) => {
798
+ if (typeof entry !== "object" || entry === null) return false;
799
+ const e = entry;
800
+ return typeof e.network === "string" && e.network.startsWith("bsv:");
801
+ });
802
+ if (!bsvAccept) {
803
+ return null;
804
+ }
805
+ if (typeof bsvAccept.payTo !== "string" || bsvAccept.payTo.length === 0) {
806
+ throw new Error("[x402] PayGateway: missing or empty payTo in BSV accept entry");
807
+ }
808
+ if (!/^[0-9a-fA-F]+$/.test(bsvAccept.payTo)) {
809
+ throw new Error("[x402] PayGateway: payTo must be a hex-encoded locking script");
810
+ }
811
+ let amountStr;
812
+ if (typeof bsvAccept.amount === "string") {
813
+ amountStr = bsvAccept.amount;
814
+ } else if (typeof bsvAccept.amount === "number") {
815
+ amountStr = String(bsvAccept.amount);
816
+ } else {
817
+ throw new Error("[x402] PayGateway: missing or invalid amount in BSV accept entry");
818
+ }
819
+ const amountNum = Number(amountStr);
820
+ if (!Number.isFinite(amountNum) || !Number.isInteger(amountNum) || amountNum <= 0) {
821
+ throw new Error("[x402] PayGateway: amount must be a positive integer");
822
+ }
823
+ const extra = bsvAccept.extra;
824
+ if (typeof extra !== "object" || extra === null) {
825
+ throw new Error("[x402] PayGateway: missing extra object in BSV accept entry");
826
+ }
827
+ if (typeof extra.payToSig !== "string" || extra.payToSig.length === 0) {
828
+ throw new Error("[x402] PayGateway: missing or empty extra.payToSig in BSV accept entry");
829
+ }
830
+ const selectedAccept = {
831
+ scheme: typeof bsvAccept.scheme === "string" ? bsvAccept.scheme : "exact",
832
+ network: bsvAccept.network,
833
+ amount: amountStr,
834
+ asset: typeof bsvAccept.asset === "string" ? bsvAccept.asset : "BSV",
835
+ payTo: bsvAccept.payTo,
836
+ maxTimeoutSeconds: typeof bsvAccept.maxTimeoutSeconds === "number" ? bsvAccept.maxTimeoutSeconds : 60,
837
+ extra: {
838
+ payToSig: extra.payToSig,
839
+ ...typeof extra.partialTx === "string" ? { partialTx: extra.partialTx } : {},
840
+ ...typeof extra.derivationPrefix === "string" ? { derivationPrefix: extra.derivationPrefix } : {},
841
+ ...typeof extra.derivationSuffix === "string" ? { derivationSuffix: extra.derivationSuffix } : {}
842
+ }
843
+ };
844
+ const resource = typeof obj.resource === "object" && obj.resource !== null ? obj.resource : { url: "" };
845
+ return {
846
+ x402Version: 2,
847
+ resource,
848
+ accepts: [selectedAccept],
849
+ selectedAccept
850
+ };
731
851
  }
732
- function hexToBytes3(hex) {
733
- if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
734
- const bytes = new Uint8Array(hex.length / 2);
735
- for (let i = 0; i < hex.length; i += 2) {
736
- bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
852
+
853
+ // src/paygateway-proof.ts
854
+ async function constructPayGatewayProof(challenge, wallet, origin) {
855
+ const accept = challenge.selectedAccept;
856
+ const satoshis = parseInt(accept.amount, 10);
857
+ if (isNaN(satoshis) || satoshis <= 0) {
858
+ const msg = `[x402] PayGateway proof: invalid amount "${accept.amount}"`;
859
+ console.error(msg);
860
+ throw new Error(msg);
737
861
  }
738
- return bytes;
862
+ const description = origin ? `Payment for request to ${origin}` : "PayGateway payment";
863
+ let result;
864
+ try {
865
+ result = await wallet.createAction({
866
+ description,
867
+ outputs: [{
868
+ satoshis,
869
+ lockingScript: accept.payTo,
870
+ outputDescription: "PayGateway payment"
871
+ }],
872
+ options: {
873
+ noSend: true,
874
+ returnTXIDOnly: false,
875
+ randomizeOutputs: false
876
+ }
877
+ });
878
+ } catch (err) {
879
+ console.error(`[x402] PayGateway proof: createAction failed (${satoshis} sats):`, err);
880
+ throw err;
881
+ }
882
+ let rawtx;
883
+ let beef;
884
+ if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
885
+ const txBytes = new Uint8Array(result.tx);
886
+ rawtx = bytesToHex(txBytes);
887
+ beef = bytesToBase64(txBytes);
888
+ } else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
889
+ rawtx = result.rawTx;
890
+ } else {
891
+ const msg = "[x402] PayGateway proof: wallet returned no transaction data (neither tx nor rawTx)";
892
+ console.error(msg);
893
+ throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
894
+ }
895
+ const proof = { rawtx, txid: result.txid };
896
+ if (beef) {
897
+ proof.beef = beef;
898
+ }
899
+ const abort = wallet.abortAction ? async () => {
900
+ try {
901
+ await wallet.abortAction({ reference: result.txid });
902
+ } catch (err) {
903
+ console.warn("[x402] PayGateway abortAction failed:", err);
904
+ }
905
+ } : void 0;
906
+ const broadcast = wallet.createAction ? async () => {
907
+ try {
908
+ await wallet.createAction({
909
+ description: "Broadcast x402 payment",
910
+ outputs: [],
911
+ options: { sendWith: [result.txid] }
912
+ });
913
+ } catch (err) {
914
+ console.warn("[x402] PayGateway broadcast failed:", err);
915
+ }
916
+ } : void 0;
917
+ return { proof, abort, broadcast };
739
918
  }
919
+
920
+ // src/x402-fetch.ts
740
921
  var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
741
922
  function base58ToBytes(encoded) {
742
923
  let leadingZeros = 0;
@@ -805,9 +986,9 @@ async function defaultConstructProof(challenge) {
805
986
  }
806
987
  let beef;
807
988
  if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
808
- beef = numberArrayToBase643(result.tx);
989
+ beef = numberArrayToBase64(result.tx);
809
990
  } else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
810
- beef = bytesToBase643(hexToBytes3(result.rawTx));
991
+ beef = bytesToBase64(hexToBytes(result.rawTx));
811
992
  } else {
812
993
  throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
813
994
  }
@@ -822,6 +1003,8 @@ function createX402Fetch(config = {}) {
822
1003
  const brc105Wallet = config.brc105Wallet;
823
1004
  const brc121ProofConstructor = config.brc121ProofConstructor;
824
1005
  const brc121Wallet = config.brc121Wallet;
1006
+ const payGatewayProofConstructor = config.payGatewayProofConstructor;
1007
+ const payGatewayWallet = config.payGatewayWallet;
825
1008
  const maxRetries = config.maxRetries ?? 2;
826
1009
  const ackWallet = config.ackWallet;
827
1010
  const serverIdentityKey = config.serverIdentityKey;
@@ -891,7 +1074,8 @@ function createX402Fetch(config = {}) {
891
1074
  let challenge;
892
1075
  try {
893
1076
  challenge = parseChallenge(challengeHeader);
894
- } catch {
1077
+ } catch (err) {
1078
+ console.warn("[x402] Malformed X402-Challenge header, treating as non-payable:", err);
895
1079
  return response;
896
1080
  }
897
1081
  let proof;
@@ -915,7 +1099,8 @@ function createX402Fetch(config = {}) {
915
1099
  let brc105Challenge;
916
1100
  try {
917
1101
  brc105Challenge = parseBrc105Challenge(response);
918
- } catch {
1102
+ } catch (err) {
1103
+ console.warn("[x402] Malformed BRC-105 challenge headers, treating as non-payable:", err);
919
1104
  return response;
920
1105
  }
921
1106
  const buildProof = async () => {
@@ -954,7 +1139,8 @@ function createX402Fetch(config = {}) {
954
1139
  retryResponse = await fetch(input, { ...init, headers: proofHeaders(proof) });
955
1140
  networkError = false;
956
1141
  break;
957
- } catch {
1142
+ } catch (err) {
1143
+ console.warn(`[x402] Network error on retry attempt ${attempt + 1}/${maxRetries + 1}:`, err);
958
1144
  networkError = true;
959
1145
  }
960
1146
  }
@@ -1017,7 +1203,8 @@ function createX402Fetch(config = {}) {
1017
1203
  let brc121Challenge;
1018
1204
  try {
1019
1205
  brc121Challenge = parseBrc121Challenge(response);
1020
- } catch {
1206
+ } catch (err) {
1207
+ console.warn("[x402] Malformed BRC-121 challenge headers, treating as non-payable:", err);
1021
1208
  return response;
1022
1209
  }
1023
1210
  if (brc121Challenge) {
@@ -1064,7 +1251,8 @@ function createX402Fetch(config = {}) {
1064
1251
  retryResponse = await fetch(input, { ...init, headers: proofHeaders(proof) });
1065
1252
  networkError = false;
1066
1253
  break;
1067
- } catch {
1254
+ } catch (err) {
1255
+ console.warn(`[x402] Network error on retry attempt ${attempt + 1}/${maxRetries + 1}:`, err);
1068
1256
  networkError = true;
1069
1257
  }
1070
1258
  }
@@ -1124,6 +1312,123 @@ function createX402Fetch(config = {}) {
1124
1312
  await processPendingBeefs(freshResponse);
1125
1313
  return freshResponse;
1126
1314
  }
1315
+ let payGatewayChallenge;
1316
+ try {
1317
+ payGatewayChallenge = parsePayGatewayChallenge(response);
1318
+ } catch (err) {
1319
+ console.warn("[x402] Malformed PayGateway challenge, treating as non-payable:", err);
1320
+ return response;
1321
+ }
1322
+ if (payGatewayChallenge) {
1323
+ if (!payGatewayProofConstructor && !payGatewayWallet) {
1324
+ await processPendingBeefs(response);
1325
+ return response;
1326
+ }
1327
+ const buildProof = async () => {
1328
+ if (payGatewayProofConstructor) {
1329
+ return payGatewayProofConstructor(payGatewayChallenge);
1330
+ }
1331
+ return constructPayGatewayProof(payGatewayChallenge, payGatewayWallet, origin);
1332
+ };
1333
+ const proofHeaders = (proof2, challenge) => {
1334
+ const h = new Headers(init?.headers);
1335
+ const signaturePayload = {
1336
+ x402Version: challenge.x402Version,
1337
+ accepted: challenge.selectedAccept,
1338
+ payload: {
1339
+ rawtx: proof2.rawtx,
1340
+ txid: proof2.txid,
1341
+ ...proof2.beef ? { beef: proof2.beef } : {}
1342
+ }
1343
+ };
1344
+ h.set("Payment-Signature", btoa(JSON.stringify(signaturePayload)));
1345
+ injectAckHeader(h);
1346
+ return h;
1347
+ };
1348
+ let proof;
1349
+ let abort;
1350
+ let broadcast;
1351
+ try {
1352
+ const result = await buildProof();
1353
+ proof = result.proof;
1354
+ abort = result.abort;
1355
+ broadcast = result.broadcast;
1356
+ } catch (err) {
1357
+ console.error("[x402] Proof construction failed (paygateway):", err);
1358
+ config.onProofError?.(err, "paygateway");
1359
+ return response;
1360
+ }
1361
+ let retryResponse;
1362
+ let networkError = false;
1363
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1364
+ if (attempt > 0) {
1365
+ await delay(1e3 * 2 ** (attempt - 1));
1366
+ }
1367
+ try {
1368
+ retryResponse = await fetch(input, { ...init, headers: proofHeaders(proof, payGatewayChallenge) });
1369
+ networkError = false;
1370
+ break;
1371
+ } catch (err) {
1372
+ console.warn(`[x402] Network error on retry attempt ${attempt + 1}/${maxRetries + 1}:`, err);
1373
+ networkError = true;
1374
+ }
1375
+ }
1376
+ if (networkError) {
1377
+ throw new Error(
1378
+ `[x402] Payment state unknown: network error after ${maxRetries + 1} attempts. The transaction may have been broadcast \u2014 do not retry with a new transaction without checking on-chain state.`
1379
+ );
1380
+ }
1381
+ if (retryResponse.ok) {
1382
+ if (broadcast) {
1383
+ broadcast().catch((err) => console.warn("[x402] broadcast failed:", err));
1384
+ }
1385
+ await processPendingBeefs(retryResponse);
1386
+ return retryResponse;
1387
+ }
1388
+ if (abort) {
1389
+ try {
1390
+ await abort();
1391
+ console.warn("[x402] Server rejected PayGateway payment, UTXOs released via abortAction");
1392
+ } catch (err) {
1393
+ console.warn("[x402] abortAction failed during server rejection:", err);
1394
+ }
1395
+ }
1396
+ let freshProof;
1397
+ let freshAbort;
1398
+ let freshBroadcast;
1399
+ try {
1400
+ const result = await buildProof();
1401
+ freshProof = result.proof;
1402
+ freshAbort = result.abort;
1403
+ freshBroadcast = result.broadcast;
1404
+ } catch (err) {
1405
+ console.error("[x402] Fresh proof construction failed (paygateway):", err);
1406
+ config.onProofError?.(err, "paygateway");
1407
+ return retryResponse;
1408
+ }
1409
+ let freshResponse;
1410
+ try {
1411
+ freshResponse = await fetch(input, { ...init, headers: proofHeaders(freshProof, payGatewayChallenge) });
1412
+ } catch {
1413
+ throw new Error(
1414
+ "[x402] Payment state unknown: network error on fresh retry. The transaction may have been broadcast \u2014 do not retry with a new transaction without checking on-chain state."
1415
+ );
1416
+ }
1417
+ if (freshResponse.ok) {
1418
+ if (freshBroadcast) {
1419
+ freshBroadcast().catch((err) => console.warn("[x402] broadcast failed:", err));
1420
+ }
1421
+ } else if (freshAbort) {
1422
+ try {
1423
+ await freshAbort();
1424
+ console.warn("[x402] Server rejected fresh PayGateway payment, UTXOs released via abortAction");
1425
+ } catch (err) {
1426
+ console.warn("[x402] freshAbort failed during double rejection:", err);
1427
+ }
1428
+ }
1429
+ await processPendingBeefs(freshResponse);
1430
+ return freshResponse;
1431
+ }
1127
1432
  await processPendingBeefs(response);
1128
1433
  return response;
1129
1434
  };
@@ -1238,11 +1543,13 @@ function initialState(config, walletBalance) {
1238
1543
  clampBalanceToTier,
1239
1544
  constructBrc105Proof,
1240
1545
  constructBrc121Proof,
1546
+ constructPayGatewayProof,
1241
1547
  createX402Fetch,
1242
1548
  initialState,
1243
1549
  parseBrc105Challenge,
1244
1550
  parseBrc121Challenge,
1245
1551
  parseChallenge,
1552
+ parsePayGatewayChallenge,
1246
1553
  recordPayment,
1247
1554
  x402Fetch
1248
1555
  });