@dexterai/x402 1.9.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/adapters/index.cjs +375 -3
- package/dist/adapters/index.d.cts +4 -5
- package/dist/adapters/index.d.ts +4 -5
- package/dist/adapters/index.js +369 -3
- package/dist/client/index.cjs +570 -10
- package/dist/client/index.d.cts +200 -36
- package/dist/client/index.d.ts +200 -36
- package/dist/client/index.js +568 -10
- package/dist/react/index.cjs +404 -8
- package/dist/react/index.d.cts +5 -5
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.js +404 -8
- package/dist/server/index.cjs +3 -4
- package/dist/server/index.d.cts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +3 -4
- package/dist/{sponsored-access-D1_mINs4.d.ts → sponsored-access-DAVzu4x6.d.cts} +13 -2
- package/dist/{sponsored-access-Br6YPA-m.d.cts → sponsored-access-Lxa11w_X.d.ts} +13 -2
- package/dist/types-D1u7iu8n.d.cts +304 -0
- package/dist/types-YQlJI5E3.d.ts +304 -0
- package/dist/{types-CjLMR7qs.d.cts → types-_iT11DL0.d.cts} +2 -2
- package/dist/{types-CjLMR7qs.d.ts → types-_iT11DL0.d.ts} +2 -2
- package/dist/utils/index.cjs +0 -1
- package/dist/utils/index.js +0 -1
- package/package.json +1 -1
- package/dist/adapters/index.cjs.map +0 -1
- package/dist/adapters/index.js.map +0 -1
- package/dist/client/index.cjs.map +0 -1
- package/dist/client/index.js.map +0 -1
- package/dist/react/index.cjs.map +0 -1
- package/dist/react/index.js.map +0 -1
- package/dist/server/index.cjs.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/solana-BcOfK6Eq.d.cts +0 -132
- package/dist/solana-Cxr5byPa.d.ts +0 -132
- package/dist/types-BIHhO2-I.d.ts +0 -123
- package/dist/types-CfKflCZO.d.cts +0 -123
- package/dist/utils/index.cjs.map +0 -1
- package/dist/utils/index.js.map +0 -1
package/dist/client/index.cjs
CHANGED
|
@@ -191,6 +191,7 @@ __export(client_exports, {
|
|
|
191
191
|
SOLANA_MAINNET: () => SOLANA_MAINNET,
|
|
192
192
|
USDC_MINT: () => USDC_MINT,
|
|
193
193
|
X402Error: () => X402Error,
|
|
194
|
+
createBudgetAccount: () => createBudgetAccount,
|
|
194
195
|
createEvmAdapter: () => createEvmAdapter,
|
|
195
196
|
createEvmKeypairWallet: () => createEvmKeypairWallet,
|
|
196
197
|
createKeypairWallet: () => createKeypairWallet,
|
|
@@ -202,6 +203,7 @@ __export(client_exports, {
|
|
|
202
203
|
getSponsoredRecommendations: () => getSponsoredRecommendations,
|
|
203
204
|
isEvmKeypairWallet: () => isEvmKeypairWallet,
|
|
204
205
|
isKeypairWallet: () => isKeypairWallet,
|
|
206
|
+
searchAPIs: () => searchAPIs,
|
|
205
207
|
wrapFetch: () => wrapFetch
|
|
206
208
|
});
|
|
207
209
|
module.exports = __toCommonJS(client_exports);
|
|
@@ -412,16 +414,40 @@ function createSolanaAdapter(config) {
|
|
|
412
414
|
}
|
|
413
415
|
|
|
414
416
|
// src/adapters/evm.ts
|
|
417
|
+
var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
418
|
+
var X402_EXACT_PERMIT2_PROXY = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001";
|
|
419
|
+
var PERMIT2_WITNESS_TYPES = {
|
|
420
|
+
PermitWitnessTransferFrom: [
|
|
421
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
422
|
+
{ name: "spender", type: "address" },
|
|
423
|
+
{ name: "nonce", type: "uint256" },
|
|
424
|
+
{ name: "deadline", type: "uint256" },
|
|
425
|
+
{ name: "witness", type: "Witness" }
|
|
426
|
+
],
|
|
427
|
+
TokenPermissions: [
|
|
428
|
+
{ name: "token", type: "address" },
|
|
429
|
+
{ name: "amount", type: "uint256" }
|
|
430
|
+
],
|
|
431
|
+
Witness: [
|
|
432
|
+
{ name: "to", type: "address" },
|
|
433
|
+
{ name: "validAfter", type: "uint256" }
|
|
434
|
+
]
|
|
435
|
+
};
|
|
436
|
+
var MAX_UINT256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
|
415
437
|
var BASE_MAINNET = "eip155:8453";
|
|
416
438
|
var BASE_SEPOLIA = "eip155:84532";
|
|
417
439
|
var ARBITRUM_ONE = "eip155:42161";
|
|
418
440
|
var POLYGON = "eip155:137";
|
|
419
441
|
var OPTIMISM = "eip155:10";
|
|
420
442
|
var AVALANCHE = "eip155:43114";
|
|
443
|
+
var BSC_MAINNET = "eip155:56";
|
|
421
444
|
var SKALE_BASE = "eip155:1187947933";
|
|
422
445
|
var SKALE_BASE_SEPOLIA = "eip155:324705682";
|
|
423
446
|
var ETHEREUM_MAINNET = "eip155:1";
|
|
447
|
+
var BSC_USDT = "0x55d398326f99059fF775485246999027B3197955";
|
|
448
|
+
var BSC_USDC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d";
|
|
424
449
|
var CHAIN_IDS = {
|
|
450
|
+
[BSC_MAINNET]: 56,
|
|
425
451
|
[BASE_MAINNET]: 8453,
|
|
426
452
|
[BASE_SEPOLIA]: 84532,
|
|
427
453
|
[ARBITRUM_ONE]: 42161,
|
|
@@ -433,6 +459,7 @@ var CHAIN_IDS = {
|
|
|
433
459
|
[ETHEREUM_MAINNET]: 1
|
|
434
460
|
};
|
|
435
461
|
var DEFAULT_RPC_URLS2 = {
|
|
462
|
+
[BSC_MAINNET]: "https://bsc-dataseed1.binance.org",
|
|
436
463
|
[BASE_MAINNET]: "https://api.dexter.cash/api/base/rpc",
|
|
437
464
|
[BASE_SEPOLIA]: "https://sepolia.base.org",
|
|
438
465
|
[ARBITRUM_ONE]: "https://arb1.arbitrum.io/rpc",
|
|
@@ -444,6 +471,7 @@ var DEFAULT_RPC_URLS2 = {
|
|
|
444
471
|
[ETHEREUM_MAINNET]: "https://eth.llamarpc.com"
|
|
445
472
|
};
|
|
446
473
|
var USDC_ADDRESSES = {
|
|
474
|
+
[BSC_MAINNET]: BSC_USDC,
|
|
447
475
|
[BASE_MAINNET]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
448
476
|
[BASE_SEPOLIA]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
449
477
|
[ARBITRUM_ONE]: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
@@ -454,6 +482,10 @@ var USDC_ADDRESSES = {
|
|
|
454
482
|
[SKALE_BASE_SEPOLIA]: "0x2e08028E3C4c2356572E096d8EF835cD5C6030bD",
|
|
455
483
|
[ETHEREUM_MAINNET]: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
|
456
484
|
};
|
|
485
|
+
var BSC_STABLECOIN_ADDRESSES = {
|
|
486
|
+
[BSC_USDT]: { symbol: "USDT", decimals: 18 },
|
|
487
|
+
[BSC_USDC]: { symbol: "USDC", decimals: 18 }
|
|
488
|
+
};
|
|
457
489
|
function isEvmWallet(wallet) {
|
|
458
490
|
if (!wallet || typeof wallet !== "object") return false;
|
|
459
491
|
const w = wallet;
|
|
@@ -461,7 +493,7 @@ function isEvmWallet(wallet) {
|
|
|
461
493
|
}
|
|
462
494
|
var EvmAdapter = class {
|
|
463
495
|
name = "EVM";
|
|
464
|
-
networks = [BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE];
|
|
496
|
+
networks = [BSC_MAINNET, BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE];
|
|
465
497
|
config;
|
|
466
498
|
log;
|
|
467
499
|
constructor(config = {}) {
|
|
@@ -472,6 +504,7 @@ var EvmAdapter = class {
|
|
|
472
504
|
canHandle(network) {
|
|
473
505
|
if (this.networks.includes(network)) return true;
|
|
474
506
|
if (network === "base") return true;
|
|
507
|
+
if (network === "bsc") return true;
|
|
475
508
|
if (network === "ethereum") return true;
|
|
476
509
|
if (network === "arbitrum") return true;
|
|
477
510
|
if (network.startsWith("eip155:")) return true;
|
|
@@ -485,6 +518,7 @@ var EvmAdapter = class {
|
|
|
485
518
|
return DEFAULT_RPC_URLS2[network];
|
|
486
519
|
}
|
|
487
520
|
if (network === "base") return DEFAULT_RPC_URLS2[BASE_MAINNET];
|
|
521
|
+
if (network === "bsc") return DEFAULT_RPC_URLS2[BSC_MAINNET];
|
|
488
522
|
if (network === "ethereum") return DEFAULT_RPC_URLS2[ETHEREUM_MAINNET];
|
|
489
523
|
if (network === "arbitrum") return DEFAULT_RPC_URLS2[ARBITRUM_ONE];
|
|
490
524
|
return DEFAULT_RPC_URLS2[BASE_MAINNET];
|
|
@@ -504,6 +538,7 @@ var EvmAdapter = class {
|
|
|
504
538
|
return parseInt(chainIdStr, 10);
|
|
505
539
|
}
|
|
506
540
|
if (network === "base") return 8453;
|
|
541
|
+
if (network === "bsc") return 56;
|
|
507
542
|
if (network === "ethereum") return 1;
|
|
508
543
|
if (network === "arbitrum") return 42161;
|
|
509
544
|
return 8453;
|
|
@@ -553,13 +588,19 @@ var EvmAdapter = class {
|
|
|
553
588
|
const paddedAddress = address.slice(2).toLowerCase().padStart(64, "0");
|
|
554
589
|
return selector + paddedAddress;
|
|
555
590
|
}
|
|
556
|
-
async buildTransaction(accept, wallet,
|
|
591
|
+
async buildTransaction(accept, wallet, rpcUrl) {
|
|
557
592
|
if (!isEvmWallet(wallet)) {
|
|
558
593
|
throw new Error("Invalid EVM wallet");
|
|
559
594
|
}
|
|
560
595
|
if (!wallet.address) {
|
|
561
596
|
throw new Error("Wallet not connected");
|
|
562
597
|
}
|
|
598
|
+
if (accept.scheme === "exact-approval") {
|
|
599
|
+
return this.buildApprovalTransaction(accept, wallet, rpcUrl);
|
|
600
|
+
}
|
|
601
|
+
if (accept.extra?.assetTransferMethod === "permit2") {
|
|
602
|
+
return this.buildPermit2Transaction(accept, wallet, rpcUrl);
|
|
603
|
+
}
|
|
563
604
|
const { payTo, asset, extra } = accept;
|
|
564
605
|
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
565
606
|
if (!amount) {
|
|
@@ -630,6 +671,325 @@ var EvmAdapter = class {
|
|
|
630
671
|
signature
|
|
631
672
|
};
|
|
632
673
|
}
|
|
674
|
+
// ===========================================================================
|
|
675
|
+
// exact-approval: BSC and other chains without EIP-3009
|
|
676
|
+
// ===========================================================================
|
|
677
|
+
/**
|
|
678
|
+
* Build a payment transaction for chains that use the approval-based scheme.
|
|
679
|
+
* The facilitator's /supported response provides the EIP-712 domain and types
|
|
680
|
+
* in accept.extra, so the client doesn't hardcode any contract addresses.
|
|
681
|
+
*/
|
|
682
|
+
async buildApprovalTransaction(accept, wallet, rpcUrl) {
|
|
683
|
+
const { payTo, asset, extra } = accept;
|
|
684
|
+
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
685
|
+
if (!amount) {
|
|
686
|
+
throw new Error("Missing amount in payment requirements");
|
|
687
|
+
}
|
|
688
|
+
const facilitatorContract = extra?.facilitatorContract;
|
|
689
|
+
if (!facilitatorContract) {
|
|
690
|
+
throw new Error(
|
|
691
|
+
"exact-approval scheme requires extra.facilitatorContract from the facilitator. The /supported endpoint should provide this."
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
if (!wallet.signTypedData) {
|
|
695
|
+
throw new Error("Wallet does not support signTypedData (EIP-712)");
|
|
696
|
+
}
|
|
697
|
+
this.log("Building approval-based transaction:", {
|
|
698
|
+
from: wallet.address,
|
|
699
|
+
to: payTo,
|
|
700
|
+
amount,
|
|
701
|
+
asset,
|
|
702
|
+
network: accept.network,
|
|
703
|
+
facilitatorContract
|
|
704
|
+
});
|
|
705
|
+
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
706
|
+
const fee = extra?.fee ?? "0";
|
|
707
|
+
const totalNeeded = BigInt(amount) + BigInt(fee);
|
|
708
|
+
const currentAllowance = await this.readAllowance(url, asset, wallet.address, facilitatorContract);
|
|
709
|
+
if (currentAllowance < totalNeeded) {
|
|
710
|
+
if (!wallet.sendTransaction) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
"BSC payments require a wallet that supports sendTransaction for the one-time token approval. Use createEvmKeypairWallet() or a browser wallet with transaction support."
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
const approvalAmount = this.calculateApprovalAmount(amount, fee, extra?.approvalStrategy);
|
|
716
|
+
this.log(`Approving ${approvalAmount} for ${facilitatorContract} (current allowance: ${currentAllowance})`);
|
|
717
|
+
const approveTxHash = await wallet.sendTransaction({
|
|
718
|
+
to: asset,
|
|
719
|
+
data: this.encodeApprove(facilitatorContract, approvalAmount),
|
|
720
|
+
value: 0n
|
|
721
|
+
});
|
|
722
|
+
this.log(`Approval tx sent: ${approveTxHash}`);
|
|
723
|
+
await this.waitForReceipt(url, approveTxHash);
|
|
724
|
+
this.log("Approval confirmed");
|
|
725
|
+
} else {
|
|
726
|
+
this.log("Sufficient allowance, skipping approval");
|
|
727
|
+
}
|
|
728
|
+
const nonceBytes = new Uint8Array(16);
|
|
729
|
+
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(nonceBytes);
|
|
730
|
+
const nonce = [...nonceBytes].reduce((acc, b) => acc * 256n + BigInt(b), 0n).toString();
|
|
731
|
+
const paymentIdBytes = new Uint8Array(32);
|
|
732
|
+
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(paymentIdBytes);
|
|
733
|
+
const paymentId = "0x" + [...paymentIdBytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
734
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
735
|
+
const deadline = now + (accept.maxTimeoutSeconds || 300);
|
|
736
|
+
const eip712Domain = extra?.eip712Domain;
|
|
737
|
+
const domain = eip712Domain ? {
|
|
738
|
+
name: eip712Domain.name,
|
|
739
|
+
version: eip712Domain.version,
|
|
740
|
+
chainId: BigInt(eip712Domain.chainId),
|
|
741
|
+
verifyingContract: eip712Domain.verifyingContract
|
|
742
|
+
} : {
|
|
743
|
+
name: "DexterBSCFacilitator",
|
|
744
|
+
version: "1",
|
|
745
|
+
chainId: BigInt(this.getChainId(accept.network)),
|
|
746
|
+
verifyingContract: facilitatorContract
|
|
747
|
+
};
|
|
748
|
+
const types = extra?.eip712Types ?? {
|
|
749
|
+
Payment: [
|
|
750
|
+
{ name: "from", type: "address" },
|
|
751
|
+
{ name: "to", type: "address" },
|
|
752
|
+
{ name: "token", type: "address" },
|
|
753
|
+
{ name: "amount", type: "uint256" },
|
|
754
|
+
{ name: "fee", type: "uint256" },
|
|
755
|
+
{ name: "nonce", type: "uint256" },
|
|
756
|
+
{ name: "deadline", type: "uint256" },
|
|
757
|
+
{ name: "paymentId", type: "bytes32" }
|
|
758
|
+
]
|
|
759
|
+
};
|
|
760
|
+
const message = {
|
|
761
|
+
from: wallet.address,
|
|
762
|
+
to: payTo,
|
|
763
|
+
token: asset,
|
|
764
|
+
amount: BigInt(amount),
|
|
765
|
+
fee: BigInt(fee),
|
|
766
|
+
nonce: BigInt(nonce),
|
|
767
|
+
deadline: BigInt(deadline),
|
|
768
|
+
paymentId
|
|
769
|
+
};
|
|
770
|
+
const signature = await wallet.signTypedData({
|
|
771
|
+
domain,
|
|
772
|
+
types,
|
|
773
|
+
primaryType: "Payment",
|
|
774
|
+
message
|
|
775
|
+
});
|
|
776
|
+
this.log("EIP-712 Payment signature obtained");
|
|
777
|
+
const payload = {
|
|
778
|
+
from: wallet.address,
|
|
779
|
+
to: payTo,
|
|
780
|
+
token: asset,
|
|
781
|
+
amount,
|
|
782
|
+
fee,
|
|
783
|
+
nonce,
|
|
784
|
+
deadline,
|
|
785
|
+
paymentId,
|
|
786
|
+
signature
|
|
787
|
+
};
|
|
788
|
+
return {
|
|
789
|
+
serialized: JSON.stringify(payload),
|
|
790
|
+
signature
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
// ===========================================================================
|
|
794
|
+
// Permit2: Universal ERC-20 payments via Uniswap's Permit2 contract
|
|
795
|
+
// ===========================================================================
|
|
796
|
+
/**
|
|
797
|
+
* Build a Permit2 payment transaction. Used when the facilitator signals
|
|
798
|
+
* assetTransferMethod: "permit2" in extra (e.g., BSC where EIP-3009 is unavailable).
|
|
799
|
+
*
|
|
800
|
+
* Flow:
|
|
801
|
+
* 1. Check if token has approved the Permit2 contract. If not, approve(Permit2, maxUint256).
|
|
802
|
+
* 2. Sign EIP-712 PermitWitnessTransferFrom against the Permit2 contract.
|
|
803
|
+
* 3. Return { permit2Authorization, signature } payload for the facilitator.
|
|
804
|
+
*/
|
|
805
|
+
async buildPermit2Transaction(accept, wallet, rpcUrl) {
|
|
806
|
+
const { payTo, asset } = accept;
|
|
807
|
+
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
808
|
+
if (!amount) {
|
|
809
|
+
throw new Error("Missing amount in payment requirements");
|
|
810
|
+
}
|
|
811
|
+
if (!wallet.signTypedData) {
|
|
812
|
+
throw new Error("Wallet does not support signTypedData (EIP-712)");
|
|
813
|
+
}
|
|
814
|
+
this.log("Building Permit2 transaction:", {
|
|
815
|
+
from: wallet.address,
|
|
816
|
+
to: payTo,
|
|
817
|
+
amount,
|
|
818
|
+
asset,
|
|
819
|
+
network: accept.network
|
|
820
|
+
});
|
|
821
|
+
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
822
|
+
const currentAllowance = await this.readAllowance(url, asset, wallet.address, PERMIT2_ADDRESS);
|
|
823
|
+
if (currentAllowance < BigInt(amount)) {
|
|
824
|
+
if (!wallet.sendTransaction) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
"Permit2 payments require a wallet that supports sendTransaction for the one-time Permit2 approval. Use createEvmKeypairWallet() or a browser wallet with transaction support."
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
this.log(`Approving Permit2 for ${asset} (current allowance: ${currentAllowance})`);
|
|
830
|
+
const approveTxHash = await wallet.sendTransaction({
|
|
831
|
+
to: asset,
|
|
832
|
+
data: this.encodeApprove(PERMIT2_ADDRESS, MAX_UINT256),
|
|
833
|
+
value: 0n
|
|
834
|
+
});
|
|
835
|
+
this.log(`Permit2 approval tx sent: ${approveTxHash}`);
|
|
836
|
+
await this.waitForReceipt(url, approveTxHash);
|
|
837
|
+
this.log("Permit2 approval confirmed");
|
|
838
|
+
} else {
|
|
839
|
+
this.log("Sufficient Permit2 allowance, skipping approval");
|
|
840
|
+
}
|
|
841
|
+
const nonceBytes = new Uint8Array(32);
|
|
842
|
+
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(nonceBytes);
|
|
843
|
+
const nonce = [...nonceBytes].reduce((acc, b) => acc * 256n + BigInt(b), 0n);
|
|
844
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
845
|
+
const validAfter = now - 600;
|
|
846
|
+
const deadline = now + (accept.maxTimeoutSeconds || 300);
|
|
847
|
+
const chainId = this.getChainId(accept.network);
|
|
848
|
+
const domain = {
|
|
849
|
+
name: "Permit2",
|
|
850
|
+
chainId: BigInt(chainId),
|
|
851
|
+
verifyingContract: PERMIT2_ADDRESS
|
|
852
|
+
};
|
|
853
|
+
const message = {
|
|
854
|
+
permitted: {
|
|
855
|
+
token: asset,
|
|
856
|
+
amount: BigInt(amount)
|
|
857
|
+
},
|
|
858
|
+
spender: X402_EXACT_PERMIT2_PROXY,
|
|
859
|
+
nonce,
|
|
860
|
+
deadline: BigInt(deadline),
|
|
861
|
+
witness: {
|
|
862
|
+
to: payTo,
|
|
863
|
+
validAfter: BigInt(validAfter)
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
const signature = await wallet.signTypedData({
|
|
867
|
+
domain,
|
|
868
|
+
types: PERMIT2_WITNESS_TYPES,
|
|
869
|
+
primaryType: "PermitWitnessTransferFrom",
|
|
870
|
+
message
|
|
871
|
+
});
|
|
872
|
+
this.log("Permit2 PermitWitnessTransferFrom signature obtained");
|
|
873
|
+
const payload = {
|
|
874
|
+
signature,
|
|
875
|
+
permit2Authorization: {
|
|
876
|
+
from: wallet.address,
|
|
877
|
+
permitted: {
|
|
878
|
+
token: asset,
|
|
879
|
+
amount
|
|
880
|
+
},
|
|
881
|
+
spender: X402_EXACT_PERMIT2_PROXY,
|
|
882
|
+
nonce: nonce.toString(),
|
|
883
|
+
deadline: String(deadline),
|
|
884
|
+
witness: {
|
|
885
|
+
to: payTo,
|
|
886
|
+
validAfter: String(validAfter)
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
return {
|
|
891
|
+
serialized: JSON.stringify(payload),
|
|
892
|
+
signature
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Read ERC-20 allowance via raw eth_call (no viem dependency needed).
|
|
897
|
+
*/
|
|
898
|
+
async readAllowance(rpcUrl, token, owner, spender) {
|
|
899
|
+
const selector = "0xdd62ed3e";
|
|
900
|
+
const paddedOwner = owner.slice(2).toLowerCase().padStart(64, "0");
|
|
901
|
+
const paddedSpender = spender.slice(2).toLowerCase().padStart(64, "0");
|
|
902
|
+
const data = selector + paddedOwner + paddedSpender;
|
|
903
|
+
try {
|
|
904
|
+
const response = await fetch(rpcUrl, {
|
|
905
|
+
method: "POST",
|
|
906
|
+
headers: { "Content-Type": "application/json" },
|
|
907
|
+
body: JSON.stringify({
|
|
908
|
+
jsonrpc: "2.0",
|
|
909
|
+
id: 1,
|
|
910
|
+
method: "eth_call",
|
|
911
|
+
params: [{ to: token, data }, "latest"]
|
|
912
|
+
})
|
|
913
|
+
});
|
|
914
|
+
const result = await response.json();
|
|
915
|
+
if (result.error || !result.result || result.result === "0x") return 0n;
|
|
916
|
+
return BigInt(result.result);
|
|
917
|
+
} catch {
|
|
918
|
+
return 0n;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Encode ERC-20 approve(address,uint256) calldata.
|
|
923
|
+
*/
|
|
924
|
+
encodeApprove(spender, amount) {
|
|
925
|
+
const selector = "0x095ea7b3";
|
|
926
|
+
const paddedSpender = spender.slice(2).toLowerCase().padStart(64, "0");
|
|
927
|
+
const paddedAmount = amount.toString(16).padStart(64, "0");
|
|
928
|
+
return selector + paddedSpender + paddedAmount;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Wait for a transaction receipt by polling eth_getTransactionReceipt.
|
|
932
|
+
*/
|
|
933
|
+
async waitForReceipt(rpcUrl, txHash, timeoutMs = 3e4) {
|
|
934
|
+
const start = Date.now();
|
|
935
|
+
while (Date.now() - start < timeoutMs) {
|
|
936
|
+
try {
|
|
937
|
+
const response = await fetch(rpcUrl, {
|
|
938
|
+
method: "POST",
|
|
939
|
+
headers: { "Content-Type": "application/json" },
|
|
940
|
+
body: JSON.stringify({
|
|
941
|
+
jsonrpc: "2.0",
|
|
942
|
+
id: 1,
|
|
943
|
+
method: "eth_getTransactionReceipt",
|
|
944
|
+
params: [txHash]
|
|
945
|
+
})
|
|
946
|
+
});
|
|
947
|
+
const result = await response.json();
|
|
948
|
+
if (result.result) {
|
|
949
|
+
if (result.result.status === "0x0") {
|
|
950
|
+
throw new Error(`Approval transaction reverted: ${txHash}`);
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
} catch (err) {
|
|
955
|
+
if (err instanceof Error && err.message.includes("reverted")) throw err;
|
|
956
|
+
}
|
|
957
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
958
|
+
}
|
|
959
|
+
throw new Error(`Approval transaction receipt timeout after ${timeoutMs}ms: ${txHash}`);
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Calculate how much to approve based on the facilitator's approval strategy.
|
|
963
|
+
* Buffered approvals reduce the number of on-chain approval txs for micropayments.
|
|
964
|
+
*/
|
|
965
|
+
calculateApprovalAmount(paymentAmount, fee, strategy) {
|
|
966
|
+
const total = BigInt(paymentAmount) + BigInt(fee);
|
|
967
|
+
if (!strategy || strategy.mode === "exact") {
|
|
968
|
+
return total;
|
|
969
|
+
}
|
|
970
|
+
const multiple = BigInt(strategy.defaultMultiple ?? 10);
|
|
971
|
+
const buffered = total * multiple;
|
|
972
|
+
if (strategy.maxCapUsd) {
|
|
973
|
+
const decimals = this.inferDecimals(paymentAmount);
|
|
974
|
+
const maxCap = BigInt(Math.floor(strategy.maxCapUsd * Math.pow(10, decimals)));
|
|
975
|
+
if (buffered > maxCap) return maxCap;
|
|
976
|
+
}
|
|
977
|
+
if (strategy.exactAboveUsd) {
|
|
978
|
+
const decimals = this.inferDecimals(paymentAmount);
|
|
979
|
+
const threshold = BigInt(Math.floor(strategy.exactAboveUsd * Math.pow(10, decimals)));
|
|
980
|
+
if (BigInt(paymentAmount) > threshold) return total;
|
|
981
|
+
}
|
|
982
|
+
return buffered;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Infer token decimals from payment amount magnitude.
|
|
986
|
+
* BSC stablecoins use 18 decimals, all others use 6.
|
|
987
|
+
* A $1 payment is 1000000 (6 dec) or 1000000000000000000 (18 dec).
|
|
988
|
+
* If the amount has > 12 digits, it's almost certainly 18 decimals.
|
|
989
|
+
*/
|
|
990
|
+
inferDecimals(amount) {
|
|
991
|
+
return amount.length > 12 ? 18 : 6;
|
|
992
|
+
}
|
|
633
993
|
};
|
|
634
994
|
function createEvmAdapter(config) {
|
|
635
995
|
return new EvmAdapter(config);
|
|
@@ -643,6 +1003,9 @@ function isKnownUSDC(asset) {
|
|
|
643
1003
|
for (const addr of Object.values(USDC_ADDRESSES)) {
|
|
644
1004
|
if (addr.toLowerCase() === lc) return true;
|
|
645
1005
|
}
|
|
1006
|
+
for (const addr of Object.keys(BSC_STABLECOIN_ADDRESSES)) {
|
|
1007
|
+
if (addr.toLowerCase() === lc) return true;
|
|
1008
|
+
}
|
|
646
1009
|
return false;
|
|
647
1010
|
}
|
|
648
1011
|
|
|
@@ -662,10 +1025,33 @@ function createX402Client(config) {
|
|
|
662
1025
|
fetch: customFetch = globalThis.fetch,
|
|
663
1026
|
verbose = false,
|
|
664
1027
|
accessPass: accessPassConfig,
|
|
665
|
-
onPaymentRequired
|
|
1028
|
+
onPaymentRequired,
|
|
1029
|
+
maxRetries = 0,
|
|
1030
|
+
retryDelayMs = 500
|
|
666
1031
|
} = config;
|
|
667
1032
|
const log = verbose ? console.log.bind(console, "[x402]") : () => {
|
|
668
1033
|
};
|
|
1034
|
+
async function fetchWithRetry(input, init) {
|
|
1035
|
+
let lastError;
|
|
1036
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1037
|
+
try {
|
|
1038
|
+
const response = await customFetch(input, init);
|
|
1039
|
+
if (response.status >= 502 && response.status <= 504 && attempt < maxRetries) {
|
|
1040
|
+
log(`Retry ${attempt + 1}/${maxRetries}: server returned ${response.status}`);
|
|
1041
|
+
await new Promise((r) => setTimeout(r, retryDelayMs * Math.pow(2, attempt)));
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
return response;
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
lastError = err;
|
|
1047
|
+
if (attempt < maxRetries) {
|
|
1048
|
+
log(`Retry ${attempt + 1}/${maxRetries}: ${err instanceof Error ? err.message : "network error"}`);
|
|
1049
|
+
await new Promise((r) => setTimeout(r, retryDelayMs * Math.pow(2, attempt)));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
throw lastError;
|
|
1054
|
+
}
|
|
669
1055
|
const passCache = /* @__PURE__ */ new Map();
|
|
670
1056
|
function getCachedPass(url) {
|
|
671
1057
|
try {
|
|
@@ -728,6 +1114,19 @@ function createX402Client(config) {
|
|
|
728
1114
|
}
|
|
729
1115
|
return candidates[0];
|
|
730
1116
|
}
|
|
1117
|
+
function getChainDisplayName(network, adapterName) {
|
|
1118
|
+
const names = {
|
|
1119
|
+
"eip155:56": "BSC",
|
|
1120
|
+
"eip155:8453": "Base",
|
|
1121
|
+
"eip155:84532": "Base Sepolia",
|
|
1122
|
+
"eip155:42161": "Arbitrum",
|
|
1123
|
+
"eip155:137": "Polygon",
|
|
1124
|
+
"eip155:10": "Optimism",
|
|
1125
|
+
"eip155:43114": "Avalanche",
|
|
1126
|
+
"eip155:1": "Ethereum"
|
|
1127
|
+
};
|
|
1128
|
+
return names[network] || adapterName;
|
|
1129
|
+
}
|
|
731
1130
|
function getRpcUrl(network, adapter) {
|
|
732
1131
|
return rpcUrls[network] || adapter.getDefaultRpcUrl(network);
|
|
733
1132
|
}
|
|
@@ -865,7 +1264,7 @@ function createX402Client(config) {
|
|
|
865
1264
|
}
|
|
866
1265
|
}
|
|
867
1266
|
}
|
|
868
|
-
const response = await
|
|
1267
|
+
const response = await fetchWithRetry(input, init);
|
|
869
1268
|
if (response.status !== 402) {
|
|
870
1269
|
return response;
|
|
871
1270
|
}
|
|
@@ -942,10 +1341,10 @@ function createX402Client(config) {
|
|
|
942
1341
|
const balance = await adapter.getBalance(accept, wallet, rpcUrl);
|
|
943
1342
|
const requiredAmount = Number(paymentAmount) / Math.pow(10, decimals);
|
|
944
1343
|
if (balance < requiredAmount) {
|
|
945
|
-
const
|
|
1344
|
+
const chainName = getChainDisplayName(accept.network, adapter.name);
|
|
946
1345
|
throw new X402Error(
|
|
947
1346
|
"insufficient_balance",
|
|
948
|
-
`Insufficient
|
|
1347
|
+
`Insufficient balance on ${chainName}. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
|
|
949
1348
|
);
|
|
950
1349
|
}
|
|
951
1350
|
log(`Balance OK: $${balance.toFixed(4)} >= $${requiredAmount.toFixed(4)}`);
|
|
@@ -1000,7 +1399,7 @@ function createX402Client(config) {
|
|
|
1000
1399
|
};
|
|
1001
1400
|
const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
|
|
1002
1401
|
log("Retrying request with payment...");
|
|
1003
|
-
const retryResponse = await
|
|
1402
|
+
const retryResponse = await fetchWithRetry(input, {
|
|
1004
1403
|
...init,
|
|
1005
1404
|
headers: {
|
|
1006
1405
|
...init?.headers || {},
|
|
@@ -1144,7 +1543,8 @@ function wrapFetch(fetchImpl, options) {
|
|
|
1144
1543
|
rpcUrls,
|
|
1145
1544
|
maxAmountAtomic,
|
|
1146
1545
|
verbose,
|
|
1147
|
-
accessPass
|
|
1546
|
+
accessPass,
|
|
1547
|
+
onPaymentRequired
|
|
1148
1548
|
} = options;
|
|
1149
1549
|
if (!walletPrivateKey && !evmPrivateKey) {
|
|
1150
1550
|
throw new Error("At least one wallet private key is required (walletPrivateKey or evmPrivateKey)");
|
|
@@ -1177,7 +1577,8 @@ function wrapFetch(fetchImpl, options) {
|
|
|
1177
1577
|
maxAmountAtomic,
|
|
1178
1578
|
fetch: fetchImpl,
|
|
1179
1579
|
verbose,
|
|
1180
|
-
accessPass
|
|
1580
|
+
accessPass,
|
|
1581
|
+
onPaymentRequired
|
|
1181
1582
|
};
|
|
1182
1583
|
const client = createX402Client(clientConfig);
|
|
1183
1584
|
const clientFetch = client.fetch.bind(client);
|
|
@@ -1190,6 +1591,164 @@ function wrapFetch(fetchImpl, options) {
|
|
|
1190
1591
|
return clientFetch;
|
|
1191
1592
|
}
|
|
1192
1593
|
|
|
1594
|
+
// src/client/discovery.ts
|
|
1595
|
+
var DEFAULT_MARKETPLACE = "https://x402.dexter.cash/api/facilitator/marketplace/resources";
|
|
1596
|
+
async function searchAPIs(options = {}) {
|
|
1597
|
+
const {
|
|
1598
|
+
query,
|
|
1599
|
+
category,
|
|
1600
|
+
network,
|
|
1601
|
+
maxPrice,
|
|
1602
|
+
verifiedOnly,
|
|
1603
|
+
sort = "marketplace",
|
|
1604
|
+
limit = 20,
|
|
1605
|
+
marketplaceUrl = DEFAULT_MARKETPLACE
|
|
1606
|
+
} = options;
|
|
1607
|
+
const params = new URLSearchParams();
|
|
1608
|
+
if (query) params.set("search", query);
|
|
1609
|
+
if (category) params.set("category", category);
|
|
1610
|
+
if (network) params.set("network", network);
|
|
1611
|
+
if (maxPrice !== void 0) params.set("maxPrice", String(maxPrice));
|
|
1612
|
+
if (verifiedOnly) params.set("verified", "true");
|
|
1613
|
+
params.set("sort", sort);
|
|
1614
|
+
params.set("order", "desc");
|
|
1615
|
+
params.set("limit", String(Math.min(limit, 50)));
|
|
1616
|
+
const url = `${marketplaceUrl}?${params.toString()}`;
|
|
1617
|
+
const response = await fetch(url, {
|
|
1618
|
+
headers: { "Accept": "application/json" },
|
|
1619
|
+
signal: AbortSignal.timeout(15e3)
|
|
1620
|
+
});
|
|
1621
|
+
if (!response.ok) {
|
|
1622
|
+
throw new Error(`Marketplace search failed: ${response.status}`);
|
|
1623
|
+
}
|
|
1624
|
+
const data = await response.json();
|
|
1625
|
+
if (!data.resources) return [];
|
|
1626
|
+
return data.resources.map((r) => ({
|
|
1627
|
+
name: r.displayName || r.resourceUrl,
|
|
1628
|
+
url: r.resourceUrl,
|
|
1629
|
+
method: r.method || "GET",
|
|
1630
|
+
price: r.priceLabel || (r.priceUsdc ? `$${r.priceUsdc.toFixed(4)}` : "free"),
|
|
1631
|
+
priceUsdc: r.priceUsdc ?? null,
|
|
1632
|
+
network: r.priceNetwork ?? null,
|
|
1633
|
+
description: r.description || "",
|
|
1634
|
+
category: r.category || "uncategorized",
|
|
1635
|
+
qualityScore: r.qualityScore ?? null,
|
|
1636
|
+
verified: r.verificationStatus === "pass",
|
|
1637
|
+
totalCalls: r.totalSettlements || 0,
|
|
1638
|
+
totalVolume: r.totalVolumeUsdc ? `$${r.totalVolumeUsdc.toLocaleString("en-US", { minimumFractionDigits: 2 })}` : null,
|
|
1639
|
+
seller: r.seller?.displayName ?? null,
|
|
1640
|
+
sellerReputation: r.reputationScore ?? null,
|
|
1641
|
+
authRequired: r.authRequired || false,
|
|
1642
|
+
lastActive: r.lastSettlementAt ?? null
|
|
1643
|
+
}));
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/client/budget-account.ts
|
|
1647
|
+
function createBudgetAccount(config) {
|
|
1648
|
+
const { budget, allowedDomains, onPaymentRequired: userOnPayment, ...fetchOptions } = config;
|
|
1649
|
+
const totalBudget = parseFloat(budget.total);
|
|
1650
|
+
const perRequestMax = budget.perRequest ? parseFloat(budget.perRequest) : Infinity;
|
|
1651
|
+
const perHourMax = budget.perHour ? parseFloat(budget.perHour) : Infinity;
|
|
1652
|
+
if (isNaN(totalBudget) || totalBudget <= 0) {
|
|
1653
|
+
throw new Error("budget.total must be a positive number");
|
|
1654
|
+
}
|
|
1655
|
+
let ledger = [];
|
|
1656
|
+
let pendingAmount = 0;
|
|
1657
|
+
function getSpent() {
|
|
1658
|
+
return ledger.reduce((sum, r) => sum + r.amount, 0);
|
|
1659
|
+
}
|
|
1660
|
+
function getHourlySpend() {
|
|
1661
|
+
const cutoff = Date.now() - 36e5;
|
|
1662
|
+
return ledger.filter((r) => r.timestamp >= cutoff).reduce((sum, r) => sum + r.amount, 0);
|
|
1663
|
+
}
|
|
1664
|
+
const innerFetch = wrapFetch(fetch, {
|
|
1665
|
+
...fetchOptions,
|
|
1666
|
+
onPaymentRequired: async (accept) => {
|
|
1667
|
+
const decimals = accept.extra?.decimals ?? 6;
|
|
1668
|
+
const amountUsd = Number(accept.amount) / Math.pow(10, decimals);
|
|
1669
|
+
if (amountUsd > perRequestMax) {
|
|
1670
|
+
throw new X402Error(
|
|
1671
|
+
"amount_exceeds_max",
|
|
1672
|
+
`$${amountUsd.toFixed(4)} exceeds per-request limit of $${perRequestMax.toFixed(2)}`
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
const spent = getSpent();
|
|
1676
|
+
if (spent + amountUsd > totalBudget) {
|
|
1677
|
+
throw new X402Error(
|
|
1678
|
+
"amount_exceeds_max",
|
|
1679
|
+
`Budget exceeded. Spent $${spent.toFixed(2)} of $${totalBudget.toFixed(2)}, payment: $${amountUsd.toFixed(4)}`
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
const hourly = getHourlySpend();
|
|
1683
|
+
if (hourly + amountUsd > perHourMax) {
|
|
1684
|
+
throw new X402Error(
|
|
1685
|
+
"amount_exceeds_max",
|
|
1686
|
+
`Hourly limit ($${perHourMax.toFixed(2)}) exceeded. Spent $${hourly.toFixed(2)} this hour`
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
pendingAmount = amountUsd;
|
|
1690
|
+
if (userOnPayment) return userOnPayment(accept);
|
|
1691
|
+
return true;
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
const budgetFetch = (async (input, init) => {
|
|
1695
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1696
|
+
let domain = "unknown";
|
|
1697
|
+
try {
|
|
1698
|
+
domain = new URL(url).hostname;
|
|
1699
|
+
} catch {
|
|
1700
|
+
}
|
|
1701
|
+
if (allowedDomains) {
|
|
1702
|
+
if (!allowedDomains.some((d) => domain === d || domain.endsWith(`.${d}`))) {
|
|
1703
|
+
throw new X402Error("payment_rejected", `Domain "${domain}" not in allowed domains`);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
pendingAmount = 0;
|
|
1707
|
+
const response = await innerFetch(input, init);
|
|
1708
|
+
if (pendingAmount > 0) {
|
|
1709
|
+
let network = "unknown";
|
|
1710
|
+
const paymentHeader = response.headers.get("PAYMENT-RESPONSE");
|
|
1711
|
+
if (paymentHeader) {
|
|
1712
|
+
try {
|
|
1713
|
+
const decoded = JSON.parse(atob(paymentHeader));
|
|
1714
|
+
network = decoded.network || network;
|
|
1715
|
+
} catch {
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
ledger.push({ amount: pendingAmount, domain, network, timestamp: Date.now() });
|
|
1719
|
+
pendingAmount = 0;
|
|
1720
|
+
}
|
|
1721
|
+
return response;
|
|
1722
|
+
});
|
|
1723
|
+
return {
|
|
1724
|
+
fetch: budgetFetch,
|
|
1725
|
+
get spent() {
|
|
1726
|
+
return `$${getSpent().toFixed(2)}`;
|
|
1727
|
+
},
|
|
1728
|
+
get remaining() {
|
|
1729
|
+
return `$${(totalBudget - getSpent()).toFixed(2)}`;
|
|
1730
|
+
},
|
|
1731
|
+
get payments() {
|
|
1732
|
+
return ledger.length;
|
|
1733
|
+
},
|
|
1734
|
+
get spentAmount() {
|
|
1735
|
+
return getSpent();
|
|
1736
|
+
},
|
|
1737
|
+
get remainingAmount() {
|
|
1738
|
+
return totalBudget - getSpent();
|
|
1739
|
+
},
|
|
1740
|
+
get ledger() {
|
|
1741
|
+
return ledger;
|
|
1742
|
+
},
|
|
1743
|
+
get hourlySpend() {
|
|
1744
|
+
return getHourlySpend();
|
|
1745
|
+
},
|
|
1746
|
+
reset() {
|
|
1747
|
+
ledger = [];
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1193
1752
|
// src/client/sponsored-access.ts
|
|
1194
1753
|
function getSponsoredAccessInfo(response) {
|
|
1195
1754
|
const receipt = getPaymentReceipt(response);
|
|
@@ -1219,6 +1778,7 @@ async function fireImpressionBeacon(response) {
|
|
|
1219
1778
|
SOLANA_MAINNET,
|
|
1220
1779
|
USDC_MINT,
|
|
1221
1780
|
X402Error,
|
|
1781
|
+
createBudgetAccount,
|
|
1222
1782
|
createEvmAdapter,
|
|
1223
1783
|
createEvmKeypairWallet,
|
|
1224
1784
|
createKeypairWallet,
|
|
@@ -1230,6 +1790,6 @@ async function fireImpressionBeacon(response) {
|
|
|
1230
1790
|
getSponsoredRecommendations,
|
|
1231
1791
|
isEvmKeypairWallet,
|
|
1232
1792
|
isKeypairWallet,
|
|
1793
|
+
searchAPIs,
|
|
1233
1794
|
wrapFetch
|
|
1234
1795
|
});
|
|
1235
|
-
//# sourceMappingURL=index.cjs.map
|