bsv-x402 0.6.0 → 0.8.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/dist/index.cjs +384 -20
- package/dist/index.d.cts +98 -5
- package/dist/index.d.ts +98 -5
- package/dist/index.js +382 -20
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -27,9 +27,11 @@ __export(index_exports, {
|
|
|
27
27
|
checkPayment: () => checkPayment,
|
|
28
28
|
clampBalanceToTier: () => clampBalanceToTier,
|
|
29
29
|
constructBrc105Proof: () => constructBrc105Proof,
|
|
30
|
+
constructBrc121Proof: () => constructBrc121Proof,
|
|
30
31
|
createX402Fetch: () => createX402Fetch,
|
|
31
32
|
initialState: () => initialState,
|
|
32
33
|
parseBrc105Challenge: () => parseBrc105Challenge,
|
|
34
|
+
parseBrc121Challenge: () => parseBrc121Challenge,
|
|
33
35
|
parseChallenge: () => parseChallenge,
|
|
34
36
|
recordPayment: () => recordPayment,
|
|
35
37
|
x402Fetch: () => x402Fetch
|
|
@@ -544,6 +546,8 @@ async function constructBrc105Proof(challenge, wallet, origin) {
|
|
|
544
546
|
})
|
|
545
547
|
}],
|
|
546
548
|
options: {
|
|
549
|
+
returnTXIDOnly: false,
|
|
550
|
+
noSend: true,
|
|
547
551
|
randomizeOutputs: false
|
|
548
552
|
}
|
|
549
553
|
});
|
|
@@ -555,13 +559,111 @@ async function constructBrc105Proof(challenge, wallet, origin) {
|
|
|
555
559
|
} else {
|
|
556
560
|
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
557
561
|
}
|
|
558
|
-
|
|
562
|
+
const proof = {
|
|
559
563
|
derivationPrefix: challenge.derivationPrefix,
|
|
560
564
|
derivationSuffix,
|
|
561
565
|
transaction: transactionBase64,
|
|
562
566
|
clientIdentityKey,
|
|
563
567
|
txid: result.txid
|
|
564
568
|
};
|
|
569
|
+
const abort = wallet.abortAction ? async () => {
|
|
570
|
+
try {
|
|
571
|
+
await wallet.abortAction({ reference: result.txid });
|
|
572
|
+
} catch (err) {
|
|
573
|
+
console.warn("[x402] abortAction failed:", err);
|
|
574
|
+
}
|
|
575
|
+
} : void 0;
|
|
576
|
+
return { proof, abort };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/brc121-challenge.ts
|
|
580
|
+
function parseBrc121Challenge(response) {
|
|
581
|
+
if (response.headers.get("x-bsv-payment-version") !== null) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const satsHeader = response.headers.get("x-bsv-sats");
|
|
585
|
+
const serverHeader = response.headers.get("x-bsv-server");
|
|
586
|
+
if (!satsHeader || !serverHeader) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
const satoshis = Number(satsHeader);
|
|
590
|
+
if (!Number.isFinite(satoshis) || !Number.isInteger(satoshis) || satoshis <= 0) {
|
|
591
|
+
throw new Error(`BRC-121: x-bsv-sats must be a positive integer, got "${satsHeader}"`);
|
|
592
|
+
}
|
|
593
|
+
if (!/^0[23][0-9a-fA-F]{64}$/.test(serverHeader)) {
|
|
594
|
+
throw new Error("BRC-121: x-bsv-server must be a 33-byte compressed public key (hex)");
|
|
595
|
+
}
|
|
596
|
+
return { satoshis, serverIdentityKey: serverHeader };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/brc121-proof.ts
|
|
600
|
+
function bytesToBase642(bytes) {
|
|
601
|
+
let binary = "";
|
|
602
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
603
|
+
return btoa(binary);
|
|
604
|
+
}
|
|
605
|
+
function numberArrayToBase642(arr) {
|
|
606
|
+
return bytesToBase642(new Uint8Array(arr));
|
|
607
|
+
}
|
|
608
|
+
function hexToBytes2(hex) {
|
|
609
|
+
if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
|
|
610
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
611
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
612
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
613
|
+
}
|
|
614
|
+
return bytes;
|
|
615
|
+
}
|
|
616
|
+
async function constructBrc121Proof(challenge, wallet, origin) {
|
|
617
|
+
const { publicKey: clientIdentityKey } = await wallet.getPublicKey({ identityKey: true });
|
|
618
|
+
const nonceBytes = crypto.getRandomValues(new Uint8Array(8));
|
|
619
|
+
const nonce = btoa(String.fromCharCode(...nonceBytes));
|
|
620
|
+
const time = String(Date.now());
|
|
621
|
+
const timeSuffixB64 = btoa(time);
|
|
622
|
+
const keyID = `${nonce} ${timeSuffixB64}`;
|
|
623
|
+
const { publicKey: derivedPublicKey } = await wallet.getPublicKey({
|
|
624
|
+
protocolID: [2, "3241645161d8"],
|
|
625
|
+
keyID,
|
|
626
|
+
counterparty: challenge.serverIdentityKey
|
|
627
|
+
});
|
|
628
|
+
const lockingScript = await pubkeyToP2PKHLockingScript(derivedPublicKey);
|
|
629
|
+
const description = origin ? `Payment for request to ${origin}` : "BRC-121 payment";
|
|
630
|
+
const result = await wallet.createAction({
|
|
631
|
+
description,
|
|
632
|
+
outputs: [{
|
|
633
|
+
satoshis: challenge.satoshis,
|
|
634
|
+
lockingScript,
|
|
635
|
+
outputDescription: "BRC-121 payment"
|
|
636
|
+
}],
|
|
637
|
+
options: {
|
|
638
|
+
randomizeOutputs: false,
|
|
639
|
+
noSend: true,
|
|
640
|
+
returnTXIDOnly: false
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
let transactionBase64;
|
|
644
|
+
if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
|
|
645
|
+
transactionBase64 = numberArrayToBase642(result.tx);
|
|
646
|
+
} else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
|
|
647
|
+
transactionBase64 = bytesToBase642(hexToBytes2(result.rawTx));
|
|
648
|
+
} else {
|
|
649
|
+
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
650
|
+
}
|
|
651
|
+
const proof = {
|
|
652
|
+
beef: transactionBase64,
|
|
653
|
+
senderIdentityKey: clientIdentityKey,
|
|
654
|
+
nonce,
|
|
655
|
+
time,
|
|
656
|
+
vout: "0",
|
|
657
|
+
txid: result.txid
|
|
658
|
+
};
|
|
659
|
+
const abort = wallet.abortAction ? async () => {
|
|
660
|
+
try {
|
|
661
|
+
await wallet.abortAction({ reference: result.txid });
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.warn("[x402] abortAction failed:", err);
|
|
664
|
+
}
|
|
665
|
+
} : void 0;
|
|
666
|
+
return { proof, abort };
|
|
565
667
|
}
|
|
566
668
|
|
|
567
669
|
// src/challenge.ts
|
|
@@ -597,15 +699,31 @@ function parseChallenge(header) {
|
|
|
597
699
|
}
|
|
598
700
|
|
|
599
701
|
// src/x402-fetch.ts
|
|
702
|
+
function bytesToBase643(bytes) {
|
|
703
|
+
let binary = "";
|
|
704
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
705
|
+
return btoa(binary);
|
|
706
|
+
}
|
|
707
|
+
function numberArrayToBase643(arr) {
|
|
708
|
+
return bytesToBase643(new Uint8Array(arr));
|
|
709
|
+
}
|
|
710
|
+
function hexToBytes3(hex) {
|
|
711
|
+
if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
|
|
712
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
713
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
714
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
715
|
+
}
|
|
716
|
+
return bytes;
|
|
717
|
+
}
|
|
600
718
|
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
601
|
-
function
|
|
719
|
+
function base58ToBytes(encoded) {
|
|
602
720
|
let leadingZeros = 0;
|
|
603
|
-
for (const c of
|
|
721
|
+
for (const c of encoded) {
|
|
604
722
|
if (c === "1") leadingZeros++;
|
|
605
723
|
else break;
|
|
606
724
|
}
|
|
607
725
|
let n = BigInt(0);
|
|
608
|
-
for (const c of
|
|
726
|
+
for (const c of encoded) {
|
|
609
727
|
const i = BASE58_ALPHABET.indexOf(c);
|
|
610
728
|
if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
|
|
611
729
|
n = n * 58n + BigInt(i);
|
|
@@ -618,11 +736,14 @@ function base58DecodeCheck(address) {
|
|
|
618
736
|
}
|
|
619
737
|
const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
|
|
620
738
|
allBytes.set(bigintBytes, leadingZeros);
|
|
739
|
+
return allBytes;
|
|
740
|
+
}
|
|
741
|
+
function base58DecodeCheck(address) {
|
|
742
|
+
const allBytes = base58ToBytes(address);
|
|
621
743
|
if (allBytes.length !== 25) {
|
|
622
744
|
throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
|
|
623
745
|
}
|
|
624
746
|
const body = allBytes.slice(0, 21);
|
|
625
|
-
const checksum = allBytes.slice(21);
|
|
626
747
|
const version = allBytes[0];
|
|
627
748
|
if (version !== 0 && version !== 111) {
|
|
628
749
|
throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
|
|
@@ -660,21 +781,88 @@ async function defaultConstructProof(challenge) {
|
|
|
660
781
|
if (!result || !result.txid) {
|
|
661
782
|
throw new Error("Wallet declined payment or returned invalid result");
|
|
662
783
|
}
|
|
663
|
-
|
|
664
|
-
|
|
784
|
+
let beef;
|
|
785
|
+
if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
|
|
786
|
+
beef = numberArrayToBase643(result.tx);
|
|
787
|
+
} else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
|
|
788
|
+
beef = bytesToBase643(hexToBytes3(result.rawTx));
|
|
789
|
+
} else {
|
|
790
|
+
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
665
791
|
}
|
|
666
792
|
return {
|
|
667
793
|
txid: result.txid,
|
|
668
|
-
|
|
794
|
+
beef
|
|
669
795
|
};
|
|
670
796
|
}
|
|
671
797
|
function createX402Fetch(config = {}) {
|
|
672
798
|
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
673
799
|
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
674
800
|
const brc105Wallet = config.brc105Wallet;
|
|
801
|
+
const brc121ProofConstructor = config.brc121ProofConstructor;
|
|
802
|
+
const brc121Wallet = config.brc121Wallet;
|
|
803
|
+
const maxRetries = config.maxRetries ?? 2;
|
|
804
|
+
const ackWallet = config.ackWallet;
|
|
805
|
+
const serverIdentityKey = config.serverIdentityKey;
|
|
806
|
+
const ackedTxids = /* @__PURE__ */ new Set();
|
|
807
|
+
const internalisedTxids = /* @__PURE__ */ new Set();
|
|
808
|
+
function injectAckHeader(headers) {
|
|
809
|
+
if (ackedTxids.size > 0) {
|
|
810
|
+
headers.set("x-bsv-ack", [...ackedTxids].join(","));
|
|
811
|
+
ackedTxids.clear();
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async function processPendingBeefs(response) {
|
|
815
|
+
if (!ackWallet) return;
|
|
816
|
+
let body;
|
|
817
|
+
try {
|
|
818
|
+
const clone = response.clone();
|
|
819
|
+
body = await clone.json();
|
|
820
|
+
} catch {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (!Array.isArray(body?.pendingBeefs)) return;
|
|
824
|
+
for (const entry of body.pendingBeefs) {
|
|
825
|
+
if (!entry?.txid || !entry?.beef) continue;
|
|
826
|
+
if (!entry.derivationPrefix || !entry.derivationSuffix) continue;
|
|
827
|
+
if (internalisedTxids.has(entry.txid)) {
|
|
828
|
+
ackedTxids.add(entry.txid);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
const senderKey = entry.senderIdentityKey ?? serverIdentityKey;
|
|
832
|
+
if (!senderKey) {
|
|
833
|
+
console.warn(`[x402] Cannot internalise BEEF ${entry.txid}: no senderIdentityKey`);
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
const txBytes = Array.from(atob(entry.beef), (c) => c.charCodeAt(0));
|
|
838
|
+
await ackWallet.internalizeAction({
|
|
839
|
+
tx: txBytes,
|
|
840
|
+
outputs: [{
|
|
841
|
+
outputIndex: entry.outputIndex ?? 0,
|
|
842
|
+
protocol: "wallet payment",
|
|
843
|
+
paymentRemittance: {
|
|
844
|
+
derivationPrefix: entry.derivationPrefix,
|
|
845
|
+
derivationSuffix: entry.derivationSuffix,
|
|
846
|
+
senderIdentityKey: senderKey
|
|
847
|
+
}
|
|
848
|
+
}],
|
|
849
|
+
description: "x402 refund receipt"
|
|
850
|
+
});
|
|
851
|
+
internalisedTxids.add(entry.txid);
|
|
852
|
+
ackedTxids.add(entry.txid);
|
|
853
|
+
} catch (err) {
|
|
854
|
+
console.warn(`[x402] Failed to internalise BEEF ${entry.txid}:`, err);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
675
858
|
return async function x402Fetch2(input, init) {
|
|
676
|
-
const
|
|
677
|
-
|
|
859
|
+
const initialHeaders = new Headers(init?.headers);
|
|
860
|
+
injectAckHeader(initialHeaders);
|
|
861
|
+
const response = await fetch(input, { ...init, headers: initialHeaders });
|
|
862
|
+
if (response.status !== 402) {
|
|
863
|
+
await processPendingBeefs(response);
|
|
864
|
+
return response;
|
|
865
|
+
}
|
|
678
866
|
const origin = extractOrigin(input);
|
|
679
867
|
const challengeHeader = response.headers.get("X402-Challenge");
|
|
680
868
|
if (challengeHeader) {
|
|
@@ -694,7 +882,10 @@ function createX402Fetch(config = {}) {
|
|
|
694
882
|
}
|
|
695
883
|
const headers = new Headers(init?.headers);
|
|
696
884
|
headers.set("X402-Proof", JSON.stringify(proof));
|
|
697
|
-
|
|
885
|
+
injectAckHeader(headers);
|
|
886
|
+
const retryResp = await fetch(input, { ...init, headers });
|
|
887
|
+
await processPendingBeefs(retryResp);
|
|
888
|
+
return retryResp;
|
|
698
889
|
}
|
|
699
890
|
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
700
891
|
if (brc105Version) {
|
|
@@ -705,23 +896,191 @@ function createX402Fetch(config = {}) {
|
|
|
705
896
|
} catch {
|
|
706
897
|
return response;
|
|
707
898
|
}
|
|
708
|
-
|
|
709
|
-
try {
|
|
899
|
+
const buildProof = async () => {
|
|
710
900
|
if (brc105ProofConstructor) {
|
|
711
|
-
|
|
712
|
-
} else {
|
|
713
|
-
proof = await constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
901
|
+
return brc105ProofConstructor(brc105Challenge);
|
|
714
902
|
}
|
|
903
|
+
return constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
904
|
+
};
|
|
905
|
+
const proofHeaders = (proof2) => {
|
|
906
|
+
const h = new Headers(init?.headers);
|
|
907
|
+
h.set("x-bsv-payment", JSON.stringify(proof2));
|
|
908
|
+
h.set("x-bsv-auth-identity-key", proof2.clientIdentityKey);
|
|
909
|
+
injectAckHeader(h);
|
|
910
|
+
return h;
|
|
911
|
+
};
|
|
912
|
+
let proof;
|
|
913
|
+
let abort;
|
|
914
|
+
try {
|
|
915
|
+
const result = await buildProof();
|
|
916
|
+
proof = result.proof;
|
|
917
|
+
abort = result.abort;
|
|
715
918
|
} catch (err) {
|
|
716
919
|
console.error("[x402] Proof construction failed (brc105):", err);
|
|
717
920
|
config.onProofError?.(err, "brc105");
|
|
718
921
|
return response;
|
|
719
922
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
923
|
+
let retryResponse;
|
|
924
|
+
let networkError = false;
|
|
925
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
926
|
+
if (attempt > 0) {
|
|
927
|
+
await delay(1e3 * 2 ** (attempt - 1));
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
retryResponse = await fetch(input, { ...init, headers: proofHeaders(proof) });
|
|
931
|
+
networkError = false;
|
|
932
|
+
break;
|
|
933
|
+
} catch {
|
|
934
|
+
networkError = true;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (networkError) {
|
|
938
|
+
throw new Error(
|
|
939
|
+
`[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.`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
if (retryResponse.ok) {
|
|
943
|
+
await processPendingBeefs(retryResponse);
|
|
944
|
+
return retryResponse;
|
|
945
|
+
}
|
|
946
|
+
if (abort) {
|
|
947
|
+
try {
|
|
948
|
+
await abort();
|
|
949
|
+
console.warn("[x402] Server rejected BRC-105 payment, UTXOs released via abortAction");
|
|
950
|
+
} catch (err) {
|
|
951
|
+
console.warn("[x402] abortAction failed during server rejection:", err);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
let freshProof;
|
|
955
|
+
let freshAbort;
|
|
956
|
+
try {
|
|
957
|
+
const result = await buildProof();
|
|
958
|
+
freshProof = result.proof;
|
|
959
|
+
freshAbort = result.abort;
|
|
960
|
+
} catch (err) {
|
|
961
|
+
console.error("[x402] Fresh proof construction failed (brc105):", err);
|
|
962
|
+
config.onProofError?.(err, "brc105");
|
|
963
|
+
return retryResponse;
|
|
964
|
+
}
|
|
965
|
+
let freshResponse;
|
|
966
|
+
try {
|
|
967
|
+
freshResponse = await fetch(input, { ...init, headers: proofHeaders(freshProof) });
|
|
968
|
+
} catch {
|
|
969
|
+
throw new Error(
|
|
970
|
+
"[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."
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
if (!freshResponse.ok && freshAbort) {
|
|
974
|
+
try {
|
|
975
|
+
await freshAbort();
|
|
976
|
+
console.warn("[x402] Server rejected fresh BRC-105 payment, UTXOs released via abortAction");
|
|
977
|
+
} catch (err) {
|
|
978
|
+
console.warn("[x402] freshAbort failed during double rejection:", err);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
await processPendingBeefs(freshResponse);
|
|
982
|
+
return freshResponse;
|
|
983
|
+
}
|
|
984
|
+
let brc121Challenge;
|
|
985
|
+
try {
|
|
986
|
+
brc121Challenge = parseBrc121Challenge(response);
|
|
987
|
+
} catch {
|
|
988
|
+
return response;
|
|
989
|
+
}
|
|
990
|
+
if (brc121Challenge) {
|
|
991
|
+
if (!brc121ProofConstructor && !brc121Wallet) {
|
|
992
|
+
await processPendingBeefs(response);
|
|
993
|
+
return response;
|
|
994
|
+
}
|
|
995
|
+
const buildProof = async () => {
|
|
996
|
+
if (brc121ProofConstructor) {
|
|
997
|
+
return brc121ProofConstructor(brc121Challenge);
|
|
998
|
+
}
|
|
999
|
+
return constructBrc121Proof(brc121Challenge, brc121Wallet, origin);
|
|
1000
|
+
};
|
|
1001
|
+
const proofHeaders = (proof2) => {
|
|
1002
|
+
const h = new Headers(init?.headers);
|
|
1003
|
+
h.set("x-bsv-beef", proof2.beef);
|
|
1004
|
+
h.set("x-bsv-sender", proof2.senderIdentityKey);
|
|
1005
|
+
h.set("x-bsv-nonce", proof2.nonce);
|
|
1006
|
+
h.set("x-bsv-time", proof2.time);
|
|
1007
|
+
h.set("x-bsv-vout", proof2.vout);
|
|
1008
|
+
injectAckHeader(h);
|
|
1009
|
+
return h;
|
|
1010
|
+
};
|
|
1011
|
+
let proof;
|
|
1012
|
+
let abort;
|
|
1013
|
+
try {
|
|
1014
|
+
const result = await buildProof();
|
|
1015
|
+
proof = result.proof;
|
|
1016
|
+
abort = result.abort;
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
console.error("[x402] Proof construction failed (brc121):", err);
|
|
1019
|
+
config.onProofError?.(err, "brc121");
|
|
1020
|
+
return response;
|
|
1021
|
+
}
|
|
1022
|
+
let retryResponse;
|
|
1023
|
+
let networkError = false;
|
|
1024
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1025
|
+
if (attempt > 0) {
|
|
1026
|
+
await delay(1e3 * 2 ** (attempt - 1));
|
|
1027
|
+
}
|
|
1028
|
+
try {
|
|
1029
|
+
retryResponse = await fetch(input, { ...init, headers: proofHeaders(proof) });
|
|
1030
|
+
networkError = false;
|
|
1031
|
+
break;
|
|
1032
|
+
} catch {
|
|
1033
|
+
networkError = true;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (networkError) {
|
|
1037
|
+
throw new Error(
|
|
1038
|
+
`[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.`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
if (retryResponse.ok) {
|
|
1042
|
+
await processPendingBeefs(retryResponse);
|
|
1043
|
+
return retryResponse;
|
|
1044
|
+
}
|
|
1045
|
+
if (abort) {
|
|
1046
|
+
try {
|
|
1047
|
+
await abort();
|
|
1048
|
+
console.warn("[x402] Server rejected BRC-121 payment, UTXOs released via abortAction");
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
console.warn("[x402] abortAction failed during server rejection:", err);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
let freshProof;
|
|
1054
|
+
let freshAbort;
|
|
1055
|
+
try {
|
|
1056
|
+
const result = await buildProof();
|
|
1057
|
+
freshProof = result.proof;
|
|
1058
|
+
freshAbort = result.abort;
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
console.error("[x402] Fresh proof construction failed (brc121):", err);
|
|
1061
|
+
config.onProofError?.(err, "brc121");
|
|
1062
|
+
return retryResponse;
|
|
1063
|
+
}
|
|
1064
|
+
let freshResponse;
|
|
1065
|
+
try {
|
|
1066
|
+
freshResponse = await fetch(input, { ...init, headers: proofHeaders(freshProof) });
|
|
1067
|
+
} catch {
|
|
1068
|
+
throw new Error(
|
|
1069
|
+
"[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."
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
if (!freshResponse.ok && freshAbort) {
|
|
1073
|
+
try {
|
|
1074
|
+
await freshAbort();
|
|
1075
|
+
console.warn("[x402] Server rejected fresh BRC-121 payment, UTXOs released via abortAction");
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
console.warn("[x402] freshAbort failed during double rejection:", err);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
await processPendingBeefs(freshResponse);
|
|
1081
|
+
return freshResponse;
|
|
724
1082
|
}
|
|
1083
|
+
await processPendingBeefs(response);
|
|
725
1084
|
return response;
|
|
726
1085
|
};
|
|
727
1086
|
}
|
|
@@ -730,6 +1089,9 @@ async function x402Fetch(input, init) {
|
|
|
730
1089
|
if (!singleton) singleton = createX402Fetch();
|
|
731
1090
|
return singleton(input, init);
|
|
732
1091
|
}
|
|
1092
|
+
function delay(ms) {
|
|
1093
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1094
|
+
}
|
|
733
1095
|
function resolveRelativeUrl(url) {
|
|
734
1096
|
const loc = globalThis.location;
|
|
735
1097
|
if (loc?.href) {
|
|
@@ -831,9 +1193,11 @@ function initialState(config, walletBalance) {
|
|
|
831
1193
|
checkPayment,
|
|
832
1194
|
clampBalanceToTier,
|
|
833
1195
|
constructBrc105Proof,
|
|
1196
|
+
constructBrc121Proof,
|
|
834
1197
|
createX402Fetch,
|
|
835
1198
|
initialState,
|
|
836
1199
|
parseBrc105Challenge,
|
|
1200
|
+
parseBrc121Challenge,
|
|
837
1201
|
parseChallenge,
|
|
838
1202
|
recordPayment,
|
|
839
1203
|
x402Fetch
|
package/dist/index.d.cts
CHANGED
|
@@ -6,7 +6,7 @@ interface Challenge {
|
|
|
6
6
|
}
|
|
7
7
|
interface Proof {
|
|
8
8
|
txid: string;
|
|
9
|
-
|
|
9
|
+
beef: string;
|
|
10
10
|
}
|
|
11
11
|
interface Brc105Challenge {
|
|
12
12
|
version: string;
|
|
@@ -37,6 +37,11 @@ interface Brc105Wallet {
|
|
|
37
37
|
hmac: number[];
|
|
38
38
|
}>;
|
|
39
39
|
createAction(params: CWICreateActionParams): Promise<CWICreateActionResult>;
|
|
40
|
+
abortAction?: (args: {
|
|
41
|
+
reference: string;
|
|
42
|
+
}) => Promise<{
|
|
43
|
+
aborted: boolean;
|
|
44
|
+
}>;
|
|
40
45
|
}
|
|
41
46
|
interface Brc105Proof {
|
|
42
47
|
derivationPrefix: string;
|
|
@@ -45,8 +50,33 @@ interface Brc105Proof {
|
|
|
45
50
|
clientIdentityKey: string;
|
|
46
51
|
txid: string;
|
|
47
52
|
}
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
interface Brc105ProofResult {
|
|
54
|
+
proof: Brc105Proof;
|
|
55
|
+
abort?: () => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
type Brc105ProofConstructor = (challenge: Brc105Challenge) => Promise<Brc105ProofResult>;
|
|
58
|
+
/** BRC-121 challenge parsed from 402 response headers. */
|
|
59
|
+
interface Brc121Challenge {
|
|
60
|
+
satoshis: number;
|
|
61
|
+
serverIdentityKey: string;
|
|
62
|
+
}
|
|
63
|
+
/** BRC-121 proof — maps to 5 individual HTTP headers. */
|
|
64
|
+
interface Brc121Proof {
|
|
65
|
+
beef: string;
|
|
66
|
+
senderIdentityKey: string;
|
|
67
|
+
nonce: string;
|
|
68
|
+
time: string;
|
|
69
|
+
vout: string;
|
|
70
|
+
txid: string;
|
|
71
|
+
}
|
|
72
|
+
/** Result from BRC-121 proof construction, with optional abort. */
|
|
73
|
+
interface Brc121ProofResult {
|
|
74
|
+
proof: Brc121Proof;
|
|
75
|
+
abort?: () => Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/** Custom BRC-121 proof constructor. */
|
|
78
|
+
type Brc121ProofConstructor = (challenge: Brc121Challenge) => Promise<Brc121ProofResult>;
|
|
79
|
+
type PaymentProtocol = 'x402' | 'brc105' | 'brc121';
|
|
50
80
|
interface PaymentRequest {
|
|
51
81
|
amount: number;
|
|
52
82
|
origin: string;
|
|
@@ -62,11 +92,49 @@ interface AutospendConfig {
|
|
|
62
92
|
interface AutospendState {
|
|
63
93
|
balance: number;
|
|
64
94
|
}
|
|
95
|
+
/** Shape of each entry in the server's `pendingBeefs` response array. */
|
|
96
|
+
interface PendingBeef {
|
|
97
|
+
txid: string;
|
|
98
|
+
beef: string;
|
|
99
|
+
derivationPrefix: string;
|
|
100
|
+
derivationSuffix: string;
|
|
101
|
+
senderIdentityKey: string;
|
|
102
|
+
outputIndex: number;
|
|
103
|
+
}
|
|
104
|
+
/** Narrow wallet interface for BEEF acknowledgement (subset of CWIInterface). */
|
|
105
|
+
interface AckWallet {
|
|
106
|
+
internalizeAction(params: {
|
|
107
|
+
tx: number[];
|
|
108
|
+
outputs: Array<{
|
|
109
|
+
outputIndex: number;
|
|
110
|
+
protocol: 'wallet payment';
|
|
111
|
+
paymentRemittance: {
|
|
112
|
+
derivationPrefix: string;
|
|
113
|
+
derivationSuffix: string;
|
|
114
|
+
senderIdentityKey: string;
|
|
115
|
+
};
|
|
116
|
+
}>;
|
|
117
|
+
description: string;
|
|
118
|
+
labels?: string[];
|
|
119
|
+
}): Promise<{
|
|
120
|
+
accepted: boolean;
|
|
121
|
+
}>;
|
|
122
|
+
}
|
|
65
123
|
interface X402Config {
|
|
66
124
|
proofConstructor?: (challenge: Challenge) => Promise<Proof>;
|
|
67
125
|
brc105ProofConstructor?: Brc105ProofConstructor;
|
|
68
126
|
brc105Wallet?: Brc105Wallet;
|
|
127
|
+
/** Custom BRC-121 proof constructor. */
|
|
128
|
+
brc121ProofConstructor?: Brc121ProofConstructor;
|
|
129
|
+
/** Wallet for BRC-121 payments (reuses Brc105Wallet interface). */
|
|
130
|
+
brc121Wallet?: Brc105Wallet;
|
|
69
131
|
onProofError?: (error: unknown, protocol: PaymentProtocol) => void;
|
|
132
|
+
/** Maximum number of retries for network errors during BRC-105 payment (default: 2). */
|
|
133
|
+
maxRetries?: number;
|
|
134
|
+
/** Wallet for internalising received BEEFs and enabling ack headers. */
|
|
135
|
+
ackWallet?: AckWallet;
|
|
136
|
+
/** Server identity key for paymentRemittance.senderIdentityKey (fallback when not per-entry). */
|
|
137
|
+
serverIdentityKey?: string;
|
|
70
138
|
}
|
|
71
139
|
interface CWICreateActionOutput {
|
|
72
140
|
satoshis: number;
|
|
@@ -118,7 +186,32 @@ declare function parseBrc105Challenge(response: Response): Brc105Challenge;
|
|
|
118
186
|
* 4. Call wallet.createAction with the locking script and custom instructions
|
|
119
187
|
* 5. Convert transaction to base64
|
|
120
188
|
*/
|
|
121
|
-
declare function constructBrc105Proof(challenge: Brc105Challenge, wallet: Brc105Wallet, origin?: string): Promise<
|
|
189
|
+
declare function constructBrc105Proof(challenge: Brc105Challenge, wallet: Brc105Wallet, origin?: string): Promise<Brc105ProofResult>;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parses BRC-121 challenge headers from a 402 response.
|
|
193
|
+
*
|
|
194
|
+
* Returns `null` when the required headers are absent or when
|
|
195
|
+
* `x-bsv-payment-version` is present (indicating BRC-105, not BRC-121).
|
|
196
|
+
*
|
|
197
|
+
* Throws on malformed values once the headers are confirmed present,
|
|
198
|
+
* matching the validation pattern of `parseBrc105Challenge`.
|
|
199
|
+
*/
|
|
200
|
+
declare function parseBrc121Challenge(response: Response): Brc121Challenge | null;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Construct a BRC-121 payment proof from a challenge using BRC-29 key derivation.
|
|
204
|
+
*
|
|
205
|
+
* Algorithm (matching @bsv/402-pay reference):
|
|
206
|
+
* 1. Get client's identity key
|
|
207
|
+
* 2. Generate nonce (derivation prefix): 8 random bytes → base64
|
|
208
|
+
* 3. Generate derivation suffix: btoa(Date.now().toString())
|
|
209
|
+
* 4. Derive payee public key using BRC-29 protocol
|
|
210
|
+
* 5. Build P2PKH locking script from derived public key
|
|
211
|
+
* 6. Create transaction via wallet with noSend: true
|
|
212
|
+
* 7. Convert transaction to base64
|
|
213
|
+
*/
|
|
214
|
+
declare function constructBrc121Proof(challenge: Brc121Challenge, wallet: Brc105Wallet, origin?: string): Promise<Brc121ProofResult>;
|
|
122
215
|
|
|
123
216
|
/**
|
|
124
217
|
* Autospend balance cap per tier (max sats that can be auto-spent without confirmation).
|
|
@@ -160,4 +253,4 @@ declare function clampBalanceToTier(state: AutospendState, config: AutospendConf
|
|
|
160
253
|
*/
|
|
161
254
|
declare function initialState(config: AutospendConfig, walletBalance: number): AutospendState;
|
|
162
255
|
|
|
163
|
-
export { type AutospendConfig, type AutospendState, type Brc105Challenge, type Brc105Proof, type Brc105ProofConstructor, type Brc105Wallet, type CWICreateActionOutput, type CWICreateActionParams, type CWICreateActionResult, type Challenge, PICKUP_PERCENTAGES, type PaymentDecision, type PaymentProtocol, type PaymentRequest, type PickupName, type Proof, TIER_CAPS, type TierName, WEAPON_CAPS, type WeaponName, type X402Config, type X402FetchFn, applyPickup, checkPayment, clampBalanceToTier, constructBrc105Proof, createX402Fetch, initialState, parseBrc105Challenge, parseChallenge, recordPayment, x402Fetch };
|
|
256
|
+
export { type AckWallet, type AutospendConfig, type AutospendState, type Brc105Challenge, type Brc105Proof, type Brc105ProofConstructor, type Brc105Wallet, type Brc121Challenge, type Brc121Proof, type Brc121ProofConstructor, type Brc121ProofResult, type CWICreateActionOutput, type CWICreateActionParams, type CWICreateActionResult, type Challenge, PICKUP_PERCENTAGES, type PaymentDecision, type PaymentProtocol, type PaymentRequest, type PendingBeef, type PickupName, type Proof, TIER_CAPS, type TierName, WEAPON_CAPS, type WeaponName, type X402Config, type X402FetchFn, applyPickup, checkPayment, clampBalanceToTier, constructBrc105Proof, constructBrc121Proof, createX402Fetch, initialState, parseBrc105Challenge, parseBrc121Challenge, parseChallenge, recordPayment, x402Fetch };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ interface Challenge {
|
|
|
6
6
|
}
|
|
7
7
|
interface Proof {
|
|
8
8
|
txid: string;
|
|
9
|
-
|
|
9
|
+
beef: string;
|
|
10
10
|
}
|
|
11
11
|
interface Brc105Challenge {
|
|
12
12
|
version: string;
|
|
@@ -37,6 +37,11 @@ interface Brc105Wallet {
|
|
|
37
37
|
hmac: number[];
|
|
38
38
|
}>;
|
|
39
39
|
createAction(params: CWICreateActionParams): Promise<CWICreateActionResult>;
|
|
40
|
+
abortAction?: (args: {
|
|
41
|
+
reference: string;
|
|
42
|
+
}) => Promise<{
|
|
43
|
+
aborted: boolean;
|
|
44
|
+
}>;
|
|
40
45
|
}
|
|
41
46
|
interface Brc105Proof {
|
|
42
47
|
derivationPrefix: string;
|
|
@@ -45,8 +50,33 @@ interface Brc105Proof {
|
|
|
45
50
|
clientIdentityKey: string;
|
|
46
51
|
txid: string;
|
|
47
52
|
}
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
interface Brc105ProofResult {
|
|
54
|
+
proof: Brc105Proof;
|
|
55
|
+
abort?: () => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
type Brc105ProofConstructor = (challenge: Brc105Challenge) => Promise<Brc105ProofResult>;
|
|
58
|
+
/** BRC-121 challenge parsed from 402 response headers. */
|
|
59
|
+
interface Brc121Challenge {
|
|
60
|
+
satoshis: number;
|
|
61
|
+
serverIdentityKey: string;
|
|
62
|
+
}
|
|
63
|
+
/** BRC-121 proof — maps to 5 individual HTTP headers. */
|
|
64
|
+
interface Brc121Proof {
|
|
65
|
+
beef: string;
|
|
66
|
+
senderIdentityKey: string;
|
|
67
|
+
nonce: string;
|
|
68
|
+
time: string;
|
|
69
|
+
vout: string;
|
|
70
|
+
txid: string;
|
|
71
|
+
}
|
|
72
|
+
/** Result from BRC-121 proof construction, with optional abort. */
|
|
73
|
+
interface Brc121ProofResult {
|
|
74
|
+
proof: Brc121Proof;
|
|
75
|
+
abort?: () => Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/** Custom BRC-121 proof constructor. */
|
|
78
|
+
type Brc121ProofConstructor = (challenge: Brc121Challenge) => Promise<Brc121ProofResult>;
|
|
79
|
+
type PaymentProtocol = 'x402' | 'brc105' | 'brc121';
|
|
50
80
|
interface PaymentRequest {
|
|
51
81
|
amount: number;
|
|
52
82
|
origin: string;
|
|
@@ -62,11 +92,49 @@ interface AutospendConfig {
|
|
|
62
92
|
interface AutospendState {
|
|
63
93
|
balance: number;
|
|
64
94
|
}
|
|
95
|
+
/** Shape of each entry in the server's `pendingBeefs` response array. */
|
|
96
|
+
interface PendingBeef {
|
|
97
|
+
txid: string;
|
|
98
|
+
beef: string;
|
|
99
|
+
derivationPrefix: string;
|
|
100
|
+
derivationSuffix: string;
|
|
101
|
+
senderIdentityKey: string;
|
|
102
|
+
outputIndex: number;
|
|
103
|
+
}
|
|
104
|
+
/** Narrow wallet interface for BEEF acknowledgement (subset of CWIInterface). */
|
|
105
|
+
interface AckWallet {
|
|
106
|
+
internalizeAction(params: {
|
|
107
|
+
tx: number[];
|
|
108
|
+
outputs: Array<{
|
|
109
|
+
outputIndex: number;
|
|
110
|
+
protocol: 'wallet payment';
|
|
111
|
+
paymentRemittance: {
|
|
112
|
+
derivationPrefix: string;
|
|
113
|
+
derivationSuffix: string;
|
|
114
|
+
senderIdentityKey: string;
|
|
115
|
+
};
|
|
116
|
+
}>;
|
|
117
|
+
description: string;
|
|
118
|
+
labels?: string[];
|
|
119
|
+
}): Promise<{
|
|
120
|
+
accepted: boolean;
|
|
121
|
+
}>;
|
|
122
|
+
}
|
|
65
123
|
interface X402Config {
|
|
66
124
|
proofConstructor?: (challenge: Challenge) => Promise<Proof>;
|
|
67
125
|
brc105ProofConstructor?: Brc105ProofConstructor;
|
|
68
126
|
brc105Wallet?: Brc105Wallet;
|
|
127
|
+
/** Custom BRC-121 proof constructor. */
|
|
128
|
+
brc121ProofConstructor?: Brc121ProofConstructor;
|
|
129
|
+
/** Wallet for BRC-121 payments (reuses Brc105Wallet interface). */
|
|
130
|
+
brc121Wallet?: Brc105Wallet;
|
|
69
131
|
onProofError?: (error: unknown, protocol: PaymentProtocol) => void;
|
|
132
|
+
/** Maximum number of retries for network errors during BRC-105 payment (default: 2). */
|
|
133
|
+
maxRetries?: number;
|
|
134
|
+
/** Wallet for internalising received BEEFs and enabling ack headers. */
|
|
135
|
+
ackWallet?: AckWallet;
|
|
136
|
+
/** Server identity key for paymentRemittance.senderIdentityKey (fallback when not per-entry). */
|
|
137
|
+
serverIdentityKey?: string;
|
|
70
138
|
}
|
|
71
139
|
interface CWICreateActionOutput {
|
|
72
140
|
satoshis: number;
|
|
@@ -118,7 +186,32 @@ declare function parseBrc105Challenge(response: Response): Brc105Challenge;
|
|
|
118
186
|
* 4. Call wallet.createAction with the locking script and custom instructions
|
|
119
187
|
* 5. Convert transaction to base64
|
|
120
188
|
*/
|
|
121
|
-
declare function constructBrc105Proof(challenge: Brc105Challenge, wallet: Brc105Wallet, origin?: string): Promise<
|
|
189
|
+
declare function constructBrc105Proof(challenge: Brc105Challenge, wallet: Brc105Wallet, origin?: string): Promise<Brc105ProofResult>;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parses BRC-121 challenge headers from a 402 response.
|
|
193
|
+
*
|
|
194
|
+
* Returns `null` when the required headers are absent or when
|
|
195
|
+
* `x-bsv-payment-version` is present (indicating BRC-105, not BRC-121).
|
|
196
|
+
*
|
|
197
|
+
* Throws on malformed values once the headers are confirmed present,
|
|
198
|
+
* matching the validation pattern of `parseBrc105Challenge`.
|
|
199
|
+
*/
|
|
200
|
+
declare function parseBrc121Challenge(response: Response): Brc121Challenge | null;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Construct a BRC-121 payment proof from a challenge using BRC-29 key derivation.
|
|
204
|
+
*
|
|
205
|
+
* Algorithm (matching @bsv/402-pay reference):
|
|
206
|
+
* 1. Get client's identity key
|
|
207
|
+
* 2. Generate nonce (derivation prefix): 8 random bytes → base64
|
|
208
|
+
* 3. Generate derivation suffix: btoa(Date.now().toString())
|
|
209
|
+
* 4. Derive payee public key using BRC-29 protocol
|
|
210
|
+
* 5. Build P2PKH locking script from derived public key
|
|
211
|
+
* 6. Create transaction via wallet with noSend: true
|
|
212
|
+
* 7. Convert transaction to base64
|
|
213
|
+
*/
|
|
214
|
+
declare function constructBrc121Proof(challenge: Brc121Challenge, wallet: Brc105Wallet, origin?: string): Promise<Brc121ProofResult>;
|
|
122
215
|
|
|
123
216
|
/**
|
|
124
217
|
* Autospend balance cap per tier (max sats that can be auto-spent without confirmation).
|
|
@@ -160,4 +253,4 @@ declare function clampBalanceToTier(state: AutospendState, config: AutospendConf
|
|
|
160
253
|
*/
|
|
161
254
|
declare function initialState(config: AutospendConfig, walletBalance: number): AutospendState;
|
|
162
255
|
|
|
163
|
-
export { type AutospendConfig, type AutospendState, type Brc105Challenge, type Brc105Proof, type Brc105ProofConstructor, type Brc105Wallet, type CWICreateActionOutput, type CWICreateActionParams, type CWICreateActionResult, type Challenge, PICKUP_PERCENTAGES, type PaymentDecision, type PaymentProtocol, type PaymentRequest, type PickupName, type Proof, TIER_CAPS, type TierName, WEAPON_CAPS, type WeaponName, type X402Config, type X402FetchFn, applyPickup, checkPayment, clampBalanceToTier, constructBrc105Proof, createX402Fetch, initialState, parseBrc105Challenge, parseChallenge, recordPayment, x402Fetch };
|
|
256
|
+
export { type AckWallet, type AutospendConfig, type AutospendState, type Brc105Challenge, type Brc105Proof, type Brc105ProofConstructor, type Brc105Wallet, type Brc121Challenge, type Brc121Proof, type Brc121ProofConstructor, type Brc121ProofResult, type CWICreateActionOutput, type CWICreateActionParams, type CWICreateActionResult, type Challenge, PICKUP_PERCENTAGES, type PaymentDecision, type PaymentProtocol, type PaymentRequest, type PendingBeef, type PickupName, type Proof, TIER_CAPS, type TierName, WEAPON_CAPS, type WeaponName, type X402Config, type X402FetchFn, applyPickup, checkPayment, clampBalanceToTier, constructBrc105Proof, constructBrc121Proof, createX402Fetch, initialState, parseBrc105Challenge, parseBrc121Challenge, parseChallenge, recordPayment, x402Fetch };
|
package/dist/index.js
CHANGED
|
@@ -506,6 +506,8 @@ async function constructBrc105Proof(challenge, wallet, origin) {
|
|
|
506
506
|
})
|
|
507
507
|
}],
|
|
508
508
|
options: {
|
|
509
|
+
returnTXIDOnly: false,
|
|
510
|
+
noSend: true,
|
|
509
511
|
randomizeOutputs: false
|
|
510
512
|
}
|
|
511
513
|
});
|
|
@@ -517,13 +519,111 @@ async function constructBrc105Proof(challenge, wallet, origin) {
|
|
|
517
519
|
} else {
|
|
518
520
|
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
519
521
|
}
|
|
520
|
-
|
|
522
|
+
const proof = {
|
|
521
523
|
derivationPrefix: challenge.derivationPrefix,
|
|
522
524
|
derivationSuffix,
|
|
523
525
|
transaction: transactionBase64,
|
|
524
526
|
clientIdentityKey,
|
|
525
527
|
txid: result.txid
|
|
526
528
|
};
|
|
529
|
+
const abort = wallet.abortAction ? async () => {
|
|
530
|
+
try {
|
|
531
|
+
await wallet.abortAction({ reference: result.txid });
|
|
532
|
+
} catch (err) {
|
|
533
|
+
console.warn("[x402] abortAction failed:", err);
|
|
534
|
+
}
|
|
535
|
+
} : void 0;
|
|
536
|
+
return { proof, abort };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/brc121-challenge.ts
|
|
540
|
+
function parseBrc121Challenge(response) {
|
|
541
|
+
if (response.headers.get("x-bsv-payment-version") !== null) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
const satsHeader = response.headers.get("x-bsv-sats");
|
|
545
|
+
const serverHeader = response.headers.get("x-bsv-server");
|
|
546
|
+
if (!satsHeader || !serverHeader) {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
const satoshis = Number(satsHeader);
|
|
550
|
+
if (!Number.isFinite(satoshis) || !Number.isInteger(satoshis) || satoshis <= 0) {
|
|
551
|
+
throw new Error(`BRC-121: x-bsv-sats must be a positive integer, got "${satsHeader}"`);
|
|
552
|
+
}
|
|
553
|
+
if (!/^0[23][0-9a-fA-F]{64}$/.test(serverHeader)) {
|
|
554
|
+
throw new Error("BRC-121: x-bsv-server must be a 33-byte compressed public key (hex)");
|
|
555
|
+
}
|
|
556
|
+
return { satoshis, serverIdentityKey: serverHeader };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/brc121-proof.ts
|
|
560
|
+
function bytesToBase642(bytes) {
|
|
561
|
+
let binary = "";
|
|
562
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
563
|
+
return btoa(binary);
|
|
564
|
+
}
|
|
565
|
+
function numberArrayToBase642(arr) {
|
|
566
|
+
return bytesToBase642(new Uint8Array(arr));
|
|
567
|
+
}
|
|
568
|
+
function hexToBytes2(hex) {
|
|
569
|
+
if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
|
|
570
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
571
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
572
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
573
|
+
}
|
|
574
|
+
return bytes;
|
|
575
|
+
}
|
|
576
|
+
async function constructBrc121Proof(challenge, wallet, origin) {
|
|
577
|
+
const { publicKey: clientIdentityKey } = await wallet.getPublicKey({ identityKey: true });
|
|
578
|
+
const nonceBytes = crypto.getRandomValues(new Uint8Array(8));
|
|
579
|
+
const nonce = btoa(String.fromCharCode(...nonceBytes));
|
|
580
|
+
const time = String(Date.now());
|
|
581
|
+
const timeSuffixB64 = btoa(time);
|
|
582
|
+
const keyID = `${nonce} ${timeSuffixB64}`;
|
|
583
|
+
const { publicKey: derivedPublicKey } = await wallet.getPublicKey({
|
|
584
|
+
protocolID: [2, "3241645161d8"],
|
|
585
|
+
keyID,
|
|
586
|
+
counterparty: challenge.serverIdentityKey
|
|
587
|
+
});
|
|
588
|
+
const lockingScript = await pubkeyToP2PKHLockingScript(derivedPublicKey);
|
|
589
|
+
const description = origin ? `Payment for request to ${origin}` : "BRC-121 payment";
|
|
590
|
+
const result = await wallet.createAction({
|
|
591
|
+
description,
|
|
592
|
+
outputs: [{
|
|
593
|
+
satoshis: challenge.satoshis,
|
|
594
|
+
lockingScript,
|
|
595
|
+
outputDescription: "BRC-121 payment"
|
|
596
|
+
}],
|
|
597
|
+
options: {
|
|
598
|
+
randomizeOutputs: false,
|
|
599
|
+
noSend: true,
|
|
600
|
+
returnTXIDOnly: false
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
let transactionBase64;
|
|
604
|
+
if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
|
|
605
|
+
transactionBase64 = numberArrayToBase642(result.tx);
|
|
606
|
+
} else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
|
|
607
|
+
transactionBase64 = bytesToBase642(hexToBytes2(result.rawTx));
|
|
608
|
+
} else {
|
|
609
|
+
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
610
|
+
}
|
|
611
|
+
const proof = {
|
|
612
|
+
beef: transactionBase64,
|
|
613
|
+
senderIdentityKey: clientIdentityKey,
|
|
614
|
+
nonce,
|
|
615
|
+
time,
|
|
616
|
+
vout: "0",
|
|
617
|
+
txid: result.txid
|
|
618
|
+
};
|
|
619
|
+
const abort = wallet.abortAction ? async () => {
|
|
620
|
+
try {
|
|
621
|
+
await wallet.abortAction({ reference: result.txid });
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.warn("[x402] abortAction failed:", err);
|
|
624
|
+
}
|
|
625
|
+
} : void 0;
|
|
626
|
+
return { proof, abort };
|
|
527
627
|
}
|
|
528
628
|
|
|
529
629
|
// src/challenge.ts
|
|
@@ -559,15 +659,31 @@ function parseChallenge(header) {
|
|
|
559
659
|
}
|
|
560
660
|
|
|
561
661
|
// src/x402-fetch.ts
|
|
662
|
+
function bytesToBase643(bytes) {
|
|
663
|
+
let binary = "";
|
|
664
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
665
|
+
return btoa(binary);
|
|
666
|
+
}
|
|
667
|
+
function numberArrayToBase643(arr) {
|
|
668
|
+
return bytesToBase643(new Uint8Array(arr));
|
|
669
|
+
}
|
|
670
|
+
function hexToBytes3(hex) {
|
|
671
|
+
if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
|
|
672
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
673
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
674
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
675
|
+
}
|
|
676
|
+
return bytes;
|
|
677
|
+
}
|
|
562
678
|
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
563
|
-
function
|
|
679
|
+
function base58ToBytes(encoded) {
|
|
564
680
|
let leadingZeros = 0;
|
|
565
|
-
for (const c of
|
|
681
|
+
for (const c of encoded) {
|
|
566
682
|
if (c === "1") leadingZeros++;
|
|
567
683
|
else break;
|
|
568
684
|
}
|
|
569
685
|
let n = BigInt(0);
|
|
570
|
-
for (const c of
|
|
686
|
+
for (const c of encoded) {
|
|
571
687
|
const i = BASE58_ALPHABET.indexOf(c);
|
|
572
688
|
if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
|
|
573
689
|
n = n * 58n + BigInt(i);
|
|
@@ -580,11 +696,14 @@ function base58DecodeCheck(address) {
|
|
|
580
696
|
}
|
|
581
697
|
const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
|
|
582
698
|
allBytes.set(bigintBytes, leadingZeros);
|
|
699
|
+
return allBytes;
|
|
700
|
+
}
|
|
701
|
+
function base58DecodeCheck(address) {
|
|
702
|
+
const allBytes = base58ToBytes(address);
|
|
583
703
|
if (allBytes.length !== 25) {
|
|
584
704
|
throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
|
|
585
705
|
}
|
|
586
706
|
const body = allBytes.slice(0, 21);
|
|
587
|
-
const checksum = allBytes.slice(21);
|
|
588
707
|
const version = allBytes[0];
|
|
589
708
|
if (version !== 0 && version !== 111) {
|
|
590
709
|
throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
|
|
@@ -622,21 +741,88 @@ async function defaultConstructProof(challenge) {
|
|
|
622
741
|
if (!result || !result.txid) {
|
|
623
742
|
throw new Error("Wallet declined payment or returned invalid result");
|
|
624
743
|
}
|
|
625
|
-
|
|
626
|
-
|
|
744
|
+
let beef;
|
|
745
|
+
if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
|
|
746
|
+
beef = numberArrayToBase643(result.tx);
|
|
747
|
+
} else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
|
|
748
|
+
beef = bytesToBase643(hexToBytes3(result.rawTx));
|
|
749
|
+
} else {
|
|
750
|
+
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
627
751
|
}
|
|
628
752
|
return {
|
|
629
753
|
txid: result.txid,
|
|
630
|
-
|
|
754
|
+
beef
|
|
631
755
|
};
|
|
632
756
|
}
|
|
633
757
|
function createX402Fetch(config = {}) {
|
|
634
758
|
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
635
759
|
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
636
760
|
const brc105Wallet = config.brc105Wallet;
|
|
761
|
+
const brc121ProofConstructor = config.brc121ProofConstructor;
|
|
762
|
+
const brc121Wallet = config.brc121Wallet;
|
|
763
|
+
const maxRetries = config.maxRetries ?? 2;
|
|
764
|
+
const ackWallet = config.ackWallet;
|
|
765
|
+
const serverIdentityKey = config.serverIdentityKey;
|
|
766
|
+
const ackedTxids = /* @__PURE__ */ new Set();
|
|
767
|
+
const internalisedTxids = /* @__PURE__ */ new Set();
|
|
768
|
+
function injectAckHeader(headers) {
|
|
769
|
+
if (ackedTxids.size > 0) {
|
|
770
|
+
headers.set("x-bsv-ack", [...ackedTxids].join(","));
|
|
771
|
+
ackedTxids.clear();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function processPendingBeefs(response) {
|
|
775
|
+
if (!ackWallet) return;
|
|
776
|
+
let body;
|
|
777
|
+
try {
|
|
778
|
+
const clone = response.clone();
|
|
779
|
+
body = await clone.json();
|
|
780
|
+
} catch {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (!Array.isArray(body?.pendingBeefs)) return;
|
|
784
|
+
for (const entry of body.pendingBeefs) {
|
|
785
|
+
if (!entry?.txid || !entry?.beef) continue;
|
|
786
|
+
if (!entry.derivationPrefix || !entry.derivationSuffix) continue;
|
|
787
|
+
if (internalisedTxids.has(entry.txid)) {
|
|
788
|
+
ackedTxids.add(entry.txid);
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const senderKey = entry.senderIdentityKey ?? serverIdentityKey;
|
|
792
|
+
if (!senderKey) {
|
|
793
|
+
console.warn(`[x402] Cannot internalise BEEF ${entry.txid}: no senderIdentityKey`);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
const txBytes = Array.from(atob(entry.beef), (c) => c.charCodeAt(0));
|
|
798
|
+
await ackWallet.internalizeAction({
|
|
799
|
+
tx: txBytes,
|
|
800
|
+
outputs: [{
|
|
801
|
+
outputIndex: entry.outputIndex ?? 0,
|
|
802
|
+
protocol: "wallet payment",
|
|
803
|
+
paymentRemittance: {
|
|
804
|
+
derivationPrefix: entry.derivationPrefix,
|
|
805
|
+
derivationSuffix: entry.derivationSuffix,
|
|
806
|
+
senderIdentityKey: senderKey
|
|
807
|
+
}
|
|
808
|
+
}],
|
|
809
|
+
description: "x402 refund receipt"
|
|
810
|
+
});
|
|
811
|
+
internalisedTxids.add(entry.txid);
|
|
812
|
+
ackedTxids.add(entry.txid);
|
|
813
|
+
} catch (err) {
|
|
814
|
+
console.warn(`[x402] Failed to internalise BEEF ${entry.txid}:`, err);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
637
818
|
return async function x402Fetch2(input, init) {
|
|
638
|
-
const
|
|
639
|
-
|
|
819
|
+
const initialHeaders = new Headers(init?.headers);
|
|
820
|
+
injectAckHeader(initialHeaders);
|
|
821
|
+
const response = await fetch(input, { ...init, headers: initialHeaders });
|
|
822
|
+
if (response.status !== 402) {
|
|
823
|
+
await processPendingBeefs(response);
|
|
824
|
+
return response;
|
|
825
|
+
}
|
|
640
826
|
const origin = extractOrigin(input);
|
|
641
827
|
const challengeHeader = response.headers.get("X402-Challenge");
|
|
642
828
|
if (challengeHeader) {
|
|
@@ -656,7 +842,10 @@ function createX402Fetch(config = {}) {
|
|
|
656
842
|
}
|
|
657
843
|
const headers = new Headers(init?.headers);
|
|
658
844
|
headers.set("X402-Proof", JSON.stringify(proof));
|
|
659
|
-
|
|
845
|
+
injectAckHeader(headers);
|
|
846
|
+
const retryResp = await fetch(input, { ...init, headers });
|
|
847
|
+
await processPendingBeefs(retryResp);
|
|
848
|
+
return retryResp;
|
|
660
849
|
}
|
|
661
850
|
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
662
851
|
if (brc105Version) {
|
|
@@ -667,23 +856,191 @@ function createX402Fetch(config = {}) {
|
|
|
667
856
|
} catch {
|
|
668
857
|
return response;
|
|
669
858
|
}
|
|
670
|
-
|
|
671
|
-
try {
|
|
859
|
+
const buildProof = async () => {
|
|
672
860
|
if (brc105ProofConstructor) {
|
|
673
|
-
|
|
674
|
-
} else {
|
|
675
|
-
proof = await constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
861
|
+
return brc105ProofConstructor(brc105Challenge);
|
|
676
862
|
}
|
|
863
|
+
return constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
864
|
+
};
|
|
865
|
+
const proofHeaders = (proof2) => {
|
|
866
|
+
const h = new Headers(init?.headers);
|
|
867
|
+
h.set("x-bsv-payment", JSON.stringify(proof2));
|
|
868
|
+
h.set("x-bsv-auth-identity-key", proof2.clientIdentityKey);
|
|
869
|
+
injectAckHeader(h);
|
|
870
|
+
return h;
|
|
871
|
+
};
|
|
872
|
+
let proof;
|
|
873
|
+
let abort;
|
|
874
|
+
try {
|
|
875
|
+
const result = await buildProof();
|
|
876
|
+
proof = result.proof;
|
|
877
|
+
abort = result.abort;
|
|
677
878
|
} catch (err) {
|
|
678
879
|
console.error("[x402] Proof construction failed (brc105):", err);
|
|
679
880
|
config.onProofError?.(err, "brc105");
|
|
680
881
|
return response;
|
|
681
882
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
883
|
+
let retryResponse;
|
|
884
|
+
let networkError = false;
|
|
885
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
886
|
+
if (attempt > 0) {
|
|
887
|
+
await delay(1e3 * 2 ** (attempt - 1));
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
retryResponse = await fetch(input, { ...init, headers: proofHeaders(proof) });
|
|
891
|
+
networkError = false;
|
|
892
|
+
break;
|
|
893
|
+
} catch {
|
|
894
|
+
networkError = true;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (networkError) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`[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.`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
if (retryResponse.ok) {
|
|
903
|
+
await processPendingBeefs(retryResponse);
|
|
904
|
+
return retryResponse;
|
|
905
|
+
}
|
|
906
|
+
if (abort) {
|
|
907
|
+
try {
|
|
908
|
+
await abort();
|
|
909
|
+
console.warn("[x402] Server rejected BRC-105 payment, UTXOs released via abortAction");
|
|
910
|
+
} catch (err) {
|
|
911
|
+
console.warn("[x402] abortAction failed during server rejection:", err);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
let freshProof;
|
|
915
|
+
let freshAbort;
|
|
916
|
+
try {
|
|
917
|
+
const result = await buildProof();
|
|
918
|
+
freshProof = result.proof;
|
|
919
|
+
freshAbort = result.abort;
|
|
920
|
+
} catch (err) {
|
|
921
|
+
console.error("[x402] Fresh proof construction failed (brc105):", err);
|
|
922
|
+
config.onProofError?.(err, "brc105");
|
|
923
|
+
return retryResponse;
|
|
924
|
+
}
|
|
925
|
+
let freshResponse;
|
|
926
|
+
try {
|
|
927
|
+
freshResponse = await fetch(input, { ...init, headers: proofHeaders(freshProof) });
|
|
928
|
+
} catch {
|
|
929
|
+
throw new Error(
|
|
930
|
+
"[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."
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
if (!freshResponse.ok && freshAbort) {
|
|
934
|
+
try {
|
|
935
|
+
await freshAbort();
|
|
936
|
+
console.warn("[x402] Server rejected fresh BRC-105 payment, UTXOs released via abortAction");
|
|
937
|
+
} catch (err) {
|
|
938
|
+
console.warn("[x402] freshAbort failed during double rejection:", err);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
await processPendingBeefs(freshResponse);
|
|
942
|
+
return freshResponse;
|
|
943
|
+
}
|
|
944
|
+
let brc121Challenge;
|
|
945
|
+
try {
|
|
946
|
+
brc121Challenge = parseBrc121Challenge(response);
|
|
947
|
+
} catch {
|
|
948
|
+
return response;
|
|
949
|
+
}
|
|
950
|
+
if (brc121Challenge) {
|
|
951
|
+
if (!brc121ProofConstructor && !brc121Wallet) {
|
|
952
|
+
await processPendingBeefs(response);
|
|
953
|
+
return response;
|
|
954
|
+
}
|
|
955
|
+
const buildProof = async () => {
|
|
956
|
+
if (brc121ProofConstructor) {
|
|
957
|
+
return brc121ProofConstructor(brc121Challenge);
|
|
958
|
+
}
|
|
959
|
+
return constructBrc121Proof(brc121Challenge, brc121Wallet, origin);
|
|
960
|
+
};
|
|
961
|
+
const proofHeaders = (proof2) => {
|
|
962
|
+
const h = new Headers(init?.headers);
|
|
963
|
+
h.set("x-bsv-beef", proof2.beef);
|
|
964
|
+
h.set("x-bsv-sender", proof2.senderIdentityKey);
|
|
965
|
+
h.set("x-bsv-nonce", proof2.nonce);
|
|
966
|
+
h.set("x-bsv-time", proof2.time);
|
|
967
|
+
h.set("x-bsv-vout", proof2.vout);
|
|
968
|
+
injectAckHeader(h);
|
|
969
|
+
return h;
|
|
970
|
+
};
|
|
971
|
+
let proof;
|
|
972
|
+
let abort;
|
|
973
|
+
try {
|
|
974
|
+
const result = await buildProof();
|
|
975
|
+
proof = result.proof;
|
|
976
|
+
abort = result.abort;
|
|
977
|
+
} catch (err) {
|
|
978
|
+
console.error("[x402] Proof construction failed (brc121):", err);
|
|
979
|
+
config.onProofError?.(err, "brc121");
|
|
980
|
+
return response;
|
|
981
|
+
}
|
|
982
|
+
let retryResponse;
|
|
983
|
+
let networkError = false;
|
|
984
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
985
|
+
if (attempt > 0) {
|
|
986
|
+
await delay(1e3 * 2 ** (attempt - 1));
|
|
987
|
+
}
|
|
988
|
+
try {
|
|
989
|
+
retryResponse = await fetch(input, { ...init, headers: proofHeaders(proof) });
|
|
990
|
+
networkError = false;
|
|
991
|
+
break;
|
|
992
|
+
} catch {
|
|
993
|
+
networkError = true;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (networkError) {
|
|
997
|
+
throw new Error(
|
|
998
|
+
`[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.`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
if (retryResponse.ok) {
|
|
1002
|
+
await processPendingBeefs(retryResponse);
|
|
1003
|
+
return retryResponse;
|
|
1004
|
+
}
|
|
1005
|
+
if (abort) {
|
|
1006
|
+
try {
|
|
1007
|
+
await abort();
|
|
1008
|
+
console.warn("[x402] Server rejected BRC-121 payment, UTXOs released via abortAction");
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
console.warn("[x402] abortAction failed during server rejection:", err);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
let freshProof;
|
|
1014
|
+
let freshAbort;
|
|
1015
|
+
try {
|
|
1016
|
+
const result = await buildProof();
|
|
1017
|
+
freshProof = result.proof;
|
|
1018
|
+
freshAbort = result.abort;
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
console.error("[x402] Fresh proof construction failed (brc121):", err);
|
|
1021
|
+
config.onProofError?.(err, "brc121");
|
|
1022
|
+
return retryResponse;
|
|
1023
|
+
}
|
|
1024
|
+
let freshResponse;
|
|
1025
|
+
try {
|
|
1026
|
+
freshResponse = await fetch(input, { ...init, headers: proofHeaders(freshProof) });
|
|
1027
|
+
} catch {
|
|
1028
|
+
throw new Error(
|
|
1029
|
+
"[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."
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
if (!freshResponse.ok && freshAbort) {
|
|
1033
|
+
try {
|
|
1034
|
+
await freshAbort();
|
|
1035
|
+
console.warn("[x402] Server rejected fresh BRC-121 payment, UTXOs released via abortAction");
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
console.warn("[x402] freshAbort failed during double rejection:", err);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
await processPendingBeefs(freshResponse);
|
|
1041
|
+
return freshResponse;
|
|
686
1042
|
}
|
|
1043
|
+
await processPendingBeefs(response);
|
|
687
1044
|
return response;
|
|
688
1045
|
};
|
|
689
1046
|
}
|
|
@@ -692,6 +1049,9 @@ async function x402Fetch(input, init) {
|
|
|
692
1049
|
if (!singleton) singleton = createX402Fetch();
|
|
693
1050
|
return singleton(input, init);
|
|
694
1051
|
}
|
|
1052
|
+
function delay(ms) {
|
|
1053
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1054
|
+
}
|
|
695
1055
|
function resolveRelativeUrl(url) {
|
|
696
1056
|
const loc = globalThis.location;
|
|
697
1057
|
if (loc?.href) {
|
|
@@ -792,9 +1152,11 @@ export {
|
|
|
792
1152
|
checkPayment,
|
|
793
1153
|
clampBalanceToTier,
|
|
794
1154
|
constructBrc105Proof,
|
|
1155
|
+
constructBrc121Proof,
|
|
795
1156
|
createX402Fetch,
|
|
796
1157
|
initialState,
|
|
797
1158
|
parseBrc105Challenge,
|
|
1159
|
+
parseBrc121Challenge,
|
|
798
1160
|
parseChallenge,
|
|
799
1161
|
recordPayment,
|
|
800
1162
|
x402Fetch
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bsv-x402",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "x402 payment protocol client — a fetch() wrapper that handles 402 payment flows transparently",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"url": "git+https://github.com/sgbett/bsv-x402.git"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@bsv/wallet-toolbox-client": "
|
|
37
|
+
"@bsv/wallet-toolbox-client": "npm:@sgbett/wallet-toolbox-client@2.1.19-fix.1",
|
|
38
38
|
"@types/chrome": "^0.1.38",
|
|
39
39
|
"@types/express": "^5.0.6",
|
|
40
40
|
"express": "^5.2.1",
|