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