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.
- package/README.md +31 -10
- package/dist/index.cjs +393 -86
- package/dist/index.d.cts +73 -2
- package/dist/index.d.ts +73 -2
- package/dist/index.js +391 -86
- package/dist/plugins/chromium/background.js +100759 -0
- package/dist/plugins/chromium/content-script.js +219 -0
- package/dist/plugins/chromium/icons/icon-128.png +0 -0
- package/dist/plugins/chromium/icons/icon-16.png +0 -0
- package/dist/plugins/chromium/icons/icon-48.png +0 -0
- package/dist/plugins/chromium/manifest.json +34 -0
- package/dist/plugins/chromium/page-script.js +160 -0
- package/dist/plugins/chromium/ui/admin/utxos.html +138 -0
- package/dist/plugins/chromium/ui/admin/utxos.js +238 -0
- package/dist/plugins/chromium/ui/popup.css +661 -0
- package/dist/plugins/chromium/ui/popup.html +27 -0
- package/dist/plugins/chromium/ui/popup.js +1624 -0
- package/dist/plugins/chromium/ui/wallet/setup.html +353 -0
- package/dist/plugins/chromium/ui/wallet/setup.js +148 -0
- package/dist/plugins/chromium/ui/x402/approve.html +194 -0
- package/dist/plugins/chromium/ui/x402/approve.js +54 -0
- package/dist/plugins/firefox/background.js +100759 -0
- package/dist/plugins/firefox/content-script.js +219 -0
- package/dist/plugins/firefox/icons/icon-128.png +0 -0
- package/dist/plugins/firefox/icons/icon-16.png +0 -0
- package/dist/plugins/firefox/icons/icon-48.png +0 -0
- package/dist/plugins/firefox/manifest.json +40 -0
- package/dist/plugins/firefox/page-script.js +160 -0
- package/dist/plugins/firefox/ui/admin/utxos.html +138 -0
- package/dist/plugins/firefox/ui/admin/utxos.js +238 -0
- package/dist/plugins/firefox/ui/popup.css +661 -0
- package/dist/plugins/firefox/ui/popup.html +27 -0
- package/dist/plugins/firefox/ui/popup.js +1624 -0
- package/dist/plugins/firefox/ui/wallet/setup.html +353 -0
- package/dist/plugins/firefox/ui/wallet/setup.js +148 -0
- package/dist/plugins/firefox/ui/x402/approve.html +194 -0
- package/dist/plugins/firefox/ui/x402/approve.js +54 -0
- package/dist/plugins/safari/background.js +100759 -0
- package/dist/plugins/safari/content-script.js +219 -0
- package/dist/plugins/safari/icons/icon-128.png +0 -0
- package/dist/plugins/safari/icons/icon-16.png +0 -0
- package/dist/plugins/safari/icons/icon-48.png +0 -0
- package/dist/plugins/safari/manifest.json +34 -0
- package/dist/plugins/safari/page-script.js +160 -0
- package/dist/plugins/safari/ui/admin/utxos.html +138 -0
- package/dist/plugins/safari/ui/admin/utxos.js +238 -0
- package/dist/plugins/safari/ui/popup.css +661 -0
- package/dist/plugins/safari/ui/popup.html +27 -0
- package/dist/plugins/safari/ui/popup.js +1624 -0
- package/dist/plugins/safari/ui/wallet/setup.html +353 -0
- package/dist/plugins/safari/ui/wallet/setup.js +148 -0
- package/dist/plugins/safari/ui/x402/approve.html +194 -0
- package/dist/plugins/safari/ui/x402/approve.js +54 -0
- 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
|
|
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 {
|
|
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.
|
|
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.
|
|
18
|
-
4.
|
|
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
|
|
32
|
-
npm run build
|
|
33
|
-
npm test
|
|
34
|
-
npm run typecheck
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
527
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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 =
|
|
702
|
+
transactionBase64 = numberArrayToBase64(result.tx);
|
|
657
703
|
} else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
|
|
658
|
-
transactionBase64 =
|
|
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/
|
|
724
|
-
function
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
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 =
|
|
989
|
+
beef = numberArrayToBase64(result.tx);
|
|
809
990
|
} else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
|
|
810
|
-
beef =
|
|
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
|
});
|