bsv-x402 0.7.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 +347 -33
- package/dist/index.d.cts +86 -2
- package/dist/index.d.ts +86 -2
- package/dist/index.js +345 -33
- 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
|
|
@@ -574,6 +576,96 @@ async function constructBrc105Proof(challenge, wallet, origin) {
|
|
|
574
576
|
return { proof, abort };
|
|
575
577
|
}
|
|
576
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 };
|
|
667
|
+
}
|
|
668
|
+
|
|
577
669
|
// src/challenge.ts
|
|
578
670
|
function parseChallenge(header) {
|
|
579
671
|
let parsed;
|
|
@@ -607,15 +699,15 @@ function parseChallenge(header) {
|
|
|
607
699
|
}
|
|
608
700
|
|
|
609
701
|
// src/x402-fetch.ts
|
|
610
|
-
function
|
|
702
|
+
function bytesToBase643(bytes) {
|
|
611
703
|
let binary = "";
|
|
612
704
|
for (const b of bytes) binary += String.fromCharCode(b);
|
|
613
705
|
return btoa(binary);
|
|
614
706
|
}
|
|
615
|
-
function
|
|
616
|
-
return
|
|
707
|
+
function numberArrayToBase643(arr) {
|
|
708
|
+
return bytesToBase643(new Uint8Array(arr));
|
|
617
709
|
}
|
|
618
|
-
function
|
|
710
|
+
function hexToBytes3(hex) {
|
|
619
711
|
if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
|
|
620
712
|
const bytes = new Uint8Array(hex.length / 2);
|
|
621
713
|
for (let i = 0; i < hex.length; i += 2) {
|
|
@@ -624,14 +716,14 @@ function hexToBytes2(hex) {
|
|
|
624
716
|
return bytes;
|
|
625
717
|
}
|
|
626
718
|
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
627
|
-
function
|
|
719
|
+
function base58ToBytes(encoded) {
|
|
628
720
|
let leadingZeros = 0;
|
|
629
|
-
for (const c of
|
|
721
|
+
for (const c of encoded) {
|
|
630
722
|
if (c === "1") leadingZeros++;
|
|
631
723
|
else break;
|
|
632
724
|
}
|
|
633
725
|
let n = BigInt(0);
|
|
634
|
-
for (const c of
|
|
726
|
+
for (const c of encoded) {
|
|
635
727
|
const i = BASE58_ALPHABET.indexOf(c);
|
|
636
728
|
if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
|
|
637
729
|
n = n * 58n + BigInt(i);
|
|
@@ -644,11 +736,14 @@ function base58DecodeCheck(address) {
|
|
|
644
736
|
}
|
|
645
737
|
const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
|
|
646
738
|
allBytes.set(bigintBytes, leadingZeros);
|
|
739
|
+
return allBytes;
|
|
740
|
+
}
|
|
741
|
+
function base58DecodeCheck(address) {
|
|
742
|
+
const allBytes = base58ToBytes(address);
|
|
647
743
|
if (allBytes.length !== 25) {
|
|
648
744
|
throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
|
|
649
745
|
}
|
|
650
746
|
const body = allBytes.slice(0, 21);
|
|
651
|
-
const checksum = allBytes.slice(21);
|
|
652
747
|
const version = allBytes[0];
|
|
653
748
|
if (version !== 0 && version !== 111) {
|
|
654
749
|
throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
|
|
@@ -688,9 +783,9 @@ async function defaultConstructProof(challenge) {
|
|
|
688
783
|
}
|
|
689
784
|
let beef;
|
|
690
785
|
if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
|
|
691
|
-
beef =
|
|
786
|
+
beef = numberArrayToBase643(result.tx);
|
|
692
787
|
} else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
|
|
693
|
-
beef =
|
|
788
|
+
beef = bytesToBase643(hexToBytes3(result.rawTx));
|
|
694
789
|
} else {
|
|
695
790
|
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
696
791
|
}
|
|
@@ -703,9 +798,71 @@ function createX402Fetch(config = {}) {
|
|
|
703
798
|
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
704
799
|
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
705
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
|
+
}
|
|
706
858
|
return async function x402Fetch2(input, init) {
|
|
707
|
-
const
|
|
708
|
-
|
|
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
|
+
}
|
|
709
866
|
const origin = extractOrigin(input);
|
|
710
867
|
const challengeHeader = response.headers.get("X402-Challenge");
|
|
711
868
|
if (challengeHeader) {
|
|
@@ -725,7 +882,10 @@ function createX402Fetch(config = {}) {
|
|
|
725
882
|
}
|
|
726
883
|
const headers = new Headers(init?.headers);
|
|
727
884
|
headers.set("X402-Proof", JSON.stringify(proof));
|
|
728
|
-
|
|
885
|
+
injectAckHeader(headers);
|
|
886
|
+
const retryResp = await fetch(input, { ...init, headers });
|
|
887
|
+
await processPendingBeefs(retryResp);
|
|
888
|
+
return retryResp;
|
|
729
889
|
}
|
|
730
890
|
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
731
891
|
if (brc105Version) {
|
|
@@ -736,42 +896,191 @@ function createX402Fetch(config = {}) {
|
|
|
736
896
|
} catch {
|
|
737
897
|
return response;
|
|
738
898
|
}
|
|
899
|
+
const buildProof = async () => {
|
|
900
|
+
if (brc105ProofConstructor) {
|
|
901
|
+
return brc105ProofConstructor(brc105Challenge);
|
|
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
|
+
};
|
|
739
912
|
let proof;
|
|
740
913
|
let abort;
|
|
741
914
|
try {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
abort = result.abort;
|
|
746
|
-
} else {
|
|
747
|
-
const result = await constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
748
|
-
proof = result.proof;
|
|
749
|
-
abort = result.abort;
|
|
750
|
-
}
|
|
915
|
+
const result = await buildProof();
|
|
916
|
+
proof = result.proof;
|
|
917
|
+
abort = result.abort;
|
|
751
918
|
} catch (err) {
|
|
752
919
|
console.error("[x402] Proof construction failed (brc105):", err);
|
|
753
920
|
config.onProofError?.(err, "brc105");
|
|
754
921
|
return response;
|
|
755
922
|
}
|
|
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
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;
|
|
760
1013
|
try {
|
|
761
|
-
|
|
1014
|
+
const result = await buildProof();
|
|
1015
|
+
proof = result.proof;
|
|
1016
|
+
abort = result.abort;
|
|
762
1017
|
} catch (err) {
|
|
763
|
-
|
|
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 {
|
|
764
1047
|
await abort();
|
|
765
|
-
console.warn("[x402] BRC-
|
|
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);
|
|
766
1051
|
}
|
|
767
|
-
throw err;
|
|
768
1052
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
+
);
|
|
772
1071
|
}
|
|
773
|
-
|
|
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;
|
|
774
1082
|
}
|
|
1083
|
+
await processPendingBeefs(response);
|
|
775
1084
|
return response;
|
|
776
1085
|
};
|
|
777
1086
|
}
|
|
@@ -780,6 +1089,9 @@ async function x402Fetch(input, init) {
|
|
|
780
1089
|
if (!singleton) singleton = createX402Fetch();
|
|
781
1090
|
return singleton(input, init);
|
|
782
1091
|
}
|
|
1092
|
+
function delay(ms) {
|
|
1093
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1094
|
+
}
|
|
783
1095
|
function resolveRelativeUrl(url) {
|
|
784
1096
|
const loc = globalThis.location;
|
|
785
1097
|
if (loc?.href) {
|
|
@@ -881,9 +1193,11 @@ function initialState(config, walletBalance) {
|
|
|
881
1193
|
checkPayment,
|
|
882
1194
|
clampBalanceToTier,
|
|
883
1195
|
constructBrc105Proof,
|
|
1196
|
+
constructBrc121Proof,
|
|
884
1197
|
createX402Fetch,
|
|
885
1198
|
initialState,
|
|
886
1199
|
parseBrc105Challenge,
|
|
1200
|
+
parseBrc121Challenge,
|
|
887
1201
|
parseChallenge,
|
|
888
1202
|
recordPayment,
|
|
889
1203
|
x402Fetch
|
package/dist/index.d.cts
CHANGED
|
@@ -55,7 +55,28 @@ interface Brc105ProofResult {
|
|
|
55
55
|
abort?: () => Promise<void>;
|
|
56
56
|
}
|
|
57
57
|
type Brc105ProofConstructor = (challenge: Brc105Challenge) => Promise<Brc105ProofResult>;
|
|
58
|
-
|
|
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';
|
|
59
80
|
interface PaymentRequest {
|
|
60
81
|
amount: number;
|
|
61
82
|
origin: string;
|
|
@@ -71,11 +92,49 @@ interface AutospendConfig {
|
|
|
71
92
|
interface AutospendState {
|
|
72
93
|
balance: number;
|
|
73
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
|
+
}
|
|
74
123
|
interface X402Config {
|
|
75
124
|
proofConstructor?: (challenge: Challenge) => Promise<Proof>;
|
|
76
125
|
brc105ProofConstructor?: Brc105ProofConstructor;
|
|
77
126
|
brc105Wallet?: Brc105Wallet;
|
|
127
|
+
/** Custom BRC-121 proof constructor. */
|
|
128
|
+
brc121ProofConstructor?: Brc121ProofConstructor;
|
|
129
|
+
/** Wallet for BRC-121 payments (reuses Brc105Wallet interface). */
|
|
130
|
+
brc121Wallet?: Brc105Wallet;
|
|
78
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;
|
|
79
138
|
}
|
|
80
139
|
interface CWICreateActionOutput {
|
|
81
140
|
satoshis: number;
|
|
@@ -129,6 +188,31 @@ declare function parseBrc105Challenge(response: Response): Brc105Challenge;
|
|
|
129
188
|
*/
|
|
130
189
|
declare function constructBrc105Proof(challenge: Brc105Challenge, wallet: Brc105Wallet, origin?: string): Promise<Brc105ProofResult>;
|
|
131
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>;
|
|
215
|
+
|
|
132
216
|
/**
|
|
133
217
|
* Autospend balance cap per tier (max sats that can be auto-spent without confirmation).
|
|
134
218
|
* Doom II difficulty progression — each tier is 10x the previous.
|
|
@@ -169,4 +253,4 @@ declare function clampBalanceToTier(state: AutospendState, config: AutospendConf
|
|
|
169
253
|
*/
|
|
170
254
|
declare function initialState(config: AutospendConfig, walletBalance: number): AutospendState;
|
|
171
255
|
|
|
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 };
|
|
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
|
@@ -55,7 +55,28 @@ interface Brc105ProofResult {
|
|
|
55
55
|
abort?: () => Promise<void>;
|
|
56
56
|
}
|
|
57
57
|
type Brc105ProofConstructor = (challenge: Brc105Challenge) => Promise<Brc105ProofResult>;
|
|
58
|
-
|
|
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';
|
|
59
80
|
interface PaymentRequest {
|
|
60
81
|
amount: number;
|
|
61
82
|
origin: string;
|
|
@@ -71,11 +92,49 @@ interface AutospendConfig {
|
|
|
71
92
|
interface AutospendState {
|
|
72
93
|
balance: number;
|
|
73
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
|
+
}
|
|
74
123
|
interface X402Config {
|
|
75
124
|
proofConstructor?: (challenge: Challenge) => Promise<Proof>;
|
|
76
125
|
brc105ProofConstructor?: Brc105ProofConstructor;
|
|
77
126
|
brc105Wallet?: Brc105Wallet;
|
|
127
|
+
/** Custom BRC-121 proof constructor. */
|
|
128
|
+
brc121ProofConstructor?: Brc121ProofConstructor;
|
|
129
|
+
/** Wallet for BRC-121 payments (reuses Brc105Wallet interface). */
|
|
130
|
+
brc121Wallet?: Brc105Wallet;
|
|
78
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;
|
|
79
138
|
}
|
|
80
139
|
interface CWICreateActionOutput {
|
|
81
140
|
satoshis: number;
|
|
@@ -129,6 +188,31 @@ declare function parseBrc105Challenge(response: Response): Brc105Challenge;
|
|
|
129
188
|
*/
|
|
130
189
|
declare function constructBrc105Proof(challenge: Brc105Challenge, wallet: Brc105Wallet, origin?: string): Promise<Brc105ProofResult>;
|
|
131
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>;
|
|
215
|
+
|
|
132
216
|
/**
|
|
133
217
|
* Autospend balance cap per tier (max sats that can be auto-spent without confirmation).
|
|
134
218
|
* Doom II difficulty progression — each tier is 10x the previous.
|
|
@@ -169,4 +253,4 @@ declare function clampBalanceToTier(state: AutospendState, config: AutospendConf
|
|
|
169
253
|
*/
|
|
170
254
|
declare function initialState(config: AutospendConfig, walletBalance: number): AutospendState;
|
|
171
255
|
|
|
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 };
|
|
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
|
@@ -536,6 +536,96 @@ async function constructBrc105Proof(challenge, wallet, origin) {
|
|
|
536
536
|
return { proof, abort };
|
|
537
537
|
}
|
|
538
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 };
|
|
627
|
+
}
|
|
628
|
+
|
|
539
629
|
// src/challenge.ts
|
|
540
630
|
function parseChallenge(header) {
|
|
541
631
|
let parsed;
|
|
@@ -569,15 +659,15 @@ function parseChallenge(header) {
|
|
|
569
659
|
}
|
|
570
660
|
|
|
571
661
|
// src/x402-fetch.ts
|
|
572
|
-
function
|
|
662
|
+
function bytesToBase643(bytes) {
|
|
573
663
|
let binary = "";
|
|
574
664
|
for (const b of bytes) binary += String.fromCharCode(b);
|
|
575
665
|
return btoa(binary);
|
|
576
666
|
}
|
|
577
|
-
function
|
|
578
|
-
return
|
|
667
|
+
function numberArrayToBase643(arr) {
|
|
668
|
+
return bytesToBase643(new Uint8Array(arr));
|
|
579
669
|
}
|
|
580
|
-
function
|
|
670
|
+
function hexToBytes3(hex) {
|
|
581
671
|
if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
|
|
582
672
|
const bytes = new Uint8Array(hex.length / 2);
|
|
583
673
|
for (let i = 0; i < hex.length; i += 2) {
|
|
@@ -586,14 +676,14 @@ function hexToBytes2(hex) {
|
|
|
586
676
|
return bytes;
|
|
587
677
|
}
|
|
588
678
|
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
589
|
-
function
|
|
679
|
+
function base58ToBytes(encoded) {
|
|
590
680
|
let leadingZeros = 0;
|
|
591
|
-
for (const c of
|
|
681
|
+
for (const c of encoded) {
|
|
592
682
|
if (c === "1") leadingZeros++;
|
|
593
683
|
else break;
|
|
594
684
|
}
|
|
595
685
|
let n = BigInt(0);
|
|
596
|
-
for (const c of
|
|
686
|
+
for (const c of encoded) {
|
|
597
687
|
const i = BASE58_ALPHABET.indexOf(c);
|
|
598
688
|
if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
|
|
599
689
|
n = n * 58n + BigInt(i);
|
|
@@ -606,11 +696,14 @@ function base58DecodeCheck(address) {
|
|
|
606
696
|
}
|
|
607
697
|
const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
|
|
608
698
|
allBytes.set(bigintBytes, leadingZeros);
|
|
699
|
+
return allBytes;
|
|
700
|
+
}
|
|
701
|
+
function base58DecodeCheck(address) {
|
|
702
|
+
const allBytes = base58ToBytes(address);
|
|
609
703
|
if (allBytes.length !== 25) {
|
|
610
704
|
throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
|
|
611
705
|
}
|
|
612
706
|
const body = allBytes.slice(0, 21);
|
|
613
|
-
const checksum = allBytes.slice(21);
|
|
614
707
|
const version = allBytes[0];
|
|
615
708
|
if (version !== 0 && version !== 111) {
|
|
616
709
|
throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
|
|
@@ -650,9 +743,9 @@ async function defaultConstructProof(challenge) {
|
|
|
650
743
|
}
|
|
651
744
|
let beef;
|
|
652
745
|
if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
|
|
653
|
-
beef =
|
|
746
|
+
beef = numberArrayToBase643(result.tx);
|
|
654
747
|
} else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
|
|
655
|
-
beef =
|
|
748
|
+
beef = bytesToBase643(hexToBytes3(result.rawTx));
|
|
656
749
|
} else {
|
|
657
750
|
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
658
751
|
}
|
|
@@ -665,9 +758,71 @@ function createX402Fetch(config = {}) {
|
|
|
665
758
|
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
666
759
|
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
667
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
|
+
}
|
|
668
818
|
return async function x402Fetch2(input, init) {
|
|
669
|
-
const
|
|
670
|
-
|
|
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
|
+
}
|
|
671
826
|
const origin = extractOrigin(input);
|
|
672
827
|
const challengeHeader = response.headers.get("X402-Challenge");
|
|
673
828
|
if (challengeHeader) {
|
|
@@ -687,7 +842,10 @@ function createX402Fetch(config = {}) {
|
|
|
687
842
|
}
|
|
688
843
|
const headers = new Headers(init?.headers);
|
|
689
844
|
headers.set("X402-Proof", JSON.stringify(proof));
|
|
690
|
-
|
|
845
|
+
injectAckHeader(headers);
|
|
846
|
+
const retryResp = await fetch(input, { ...init, headers });
|
|
847
|
+
await processPendingBeefs(retryResp);
|
|
848
|
+
return retryResp;
|
|
691
849
|
}
|
|
692
850
|
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
693
851
|
if (brc105Version) {
|
|
@@ -698,42 +856,191 @@ function createX402Fetch(config = {}) {
|
|
|
698
856
|
} catch {
|
|
699
857
|
return response;
|
|
700
858
|
}
|
|
859
|
+
const buildProof = async () => {
|
|
860
|
+
if (brc105ProofConstructor) {
|
|
861
|
+
return brc105ProofConstructor(brc105Challenge);
|
|
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
|
+
};
|
|
701
872
|
let proof;
|
|
702
873
|
let abort;
|
|
703
874
|
try {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
abort = result.abort;
|
|
708
|
-
} else {
|
|
709
|
-
const result = await constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
|
|
710
|
-
proof = result.proof;
|
|
711
|
-
abort = result.abort;
|
|
712
|
-
}
|
|
875
|
+
const result = await buildProof();
|
|
876
|
+
proof = result.proof;
|
|
877
|
+
abort = result.abort;
|
|
713
878
|
} catch (err) {
|
|
714
879
|
console.error("[x402] Proof construction failed (brc105):", err);
|
|
715
880
|
config.onProofError?.(err, "brc105");
|
|
716
881
|
return response;
|
|
717
882
|
}
|
|
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
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;
|
|
722
973
|
try {
|
|
723
|
-
|
|
974
|
+
const result = await buildProof();
|
|
975
|
+
proof = result.proof;
|
|
976
|
+
abort = result.abort;
|
|
724
977
|
} catch (err) {
|
|
725
|
-
|
|
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 {
|
|
726
1007
|
await abort();
|
|
727
|
-
console.warn("[x402] BRC-
|
|
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);
|
|
728
1011
|
}
|
|
729
|
-
throw err;
|
|
730
1012
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
+
);
|
|
734
1031
|
}
|
|
735
|
-
|
|
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;
|
|
736
1042
|
}
|
|
1043
|
+
await processPendingBeefs(response);
|
|
737
1044
|
return response;
|
|
738
1045
|
};
|
|
739
1046
|
}
|
|
@@ -742,6 +1049,9 @@ async function x402Fetch(input, init) {
|
|
|
742
1049
|
if (!singleton) singleton = createX402Fetch();
|
|
743
1050
|
return singleton(input, init);
|
|
744
1051
|
}
|
|
1052
|
+
function delay(ms) {
|
|
1053
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1054
|
+
}
|
|
745
1055
|
function resolveRelativeUrl(url) {
|
|
746
1056
|
const loc = globalThis.location;
|
|
747
1057
|
if (loc?.href) {
|
|
@@ -842,9 +1152,11 @@ export {
|
|
|
842
1152
|
checkPayment,
|
|
843
1153
|
clampBalanceToTier,
|
|
844
1154
|
constructBrc105Proof,
|
|
1155
|
+
constructBrc121Proof,
|
|
845
1156
|
createX402Fetch,
|
|
846
1157
|
initialState,
|
|
847
1158
|
parseBrc105Challenge,
|
|
1159
|
+
parseBrc121Challenge,
|
|
848
1160
|
parseChallenge,
|
|
849
1161
|
recordPayment,
|
|
850
1162
|
x402Fetch
|