@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.js
CHANGED
|
@@ -379,16 +379,40 @@ function createSolanaAdapter(config) {
|
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
// src/adapters/evm.ts
|
|
382
|
+
var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
383
|
+
var X402_EXACT_PERMIT2_PROXY = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001";
|
|
384
|
+
var PERMIT2_WITNESS_TYPES = {
|
|
385
|
+
PermitWitnessTransferFrom: [
|
|
386
|
+
{ name: "permitted", type: "TokenPermissions" },
|
|
387
|
+
{ name: "spender", type: "address" },
|
|
388
|
+
{ name: "nonce", type: "uint256" },
|
|
389
|
+
{ name: "deadline", type: "uint256" },
|
|
390
|
+
{ name: "witness", type: "Witness" }
|
|
391
|
+
],
|
|
392
|
+
TokenPermissions: [
|
|
393
|
+
{ name: "token", type: "address" },
|
|
394
|
+
{ name: "amount", type: "uint256" }
|
|
395
|
+
],
|
|
396
|
+
Witness: [
|
|
397
|
+
{ name: "to", type: "address" },
|
|
398
|
+
{ name: "validAfter", type: "uint256" }
|
|
399
|
+
]
|
|
400
|
+
};
|
|
401
|
+
var MAX_UINT256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
|
382
402
|
var BASE_MAINNET = "eip155:8453";
|
|
383
403
|
var BASE_SEPOLIA = "eip155:84532";
|
|
384
404
|
var ARBITRUM_ONE = "eip155:42161";
|
|
385
405
|
var POLYGON = "eip155:137";
|
|
386
406
|
var OPTIMISM = "eip155:10";
|
|
387
407
|
var AVALANCHE = "eip155:43114";
|
|
408
|
+
var BSC_MAINNET = "eip155:56";
|
|
388
409
|
var SKALE_BASE = "eip155:1187947933";
|
|
389
410
|
var SKALE_BASE_SEPOLIA = "eip155:324705682";
|
|
390
411
|
var ETHEREUM_MAINNET = "eip155:1";
|
|
412
|
+
var BSC_USDT = "0x55d398326f99059fF775485246999027B3197955";
|
|
413
|
+
var BSC_USDC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d";
|
|
391
414
|
var CHAIN_IDS = {
|
|
415
|
+
[BSC_MAINNET]: 56,
|
|
392
416
|
[BASE_MAINNET]: 8453,
|
|
393
417
|
[BASE_SEPOLIA]: 84532,
|
|
394
418
|
[ARBITRUM_ONE]: 42161,
|
|
@@ -400,6 +424,7 @@ var CHAIN_IDS = {
|
|
|
400
424
|
[ETHEREUM_MAINNET]: 1
|
|
401
425
|
};
|
|
402
426
|
var DEFAULT_RPC_URLS2 = {
|
|
427
|
+
[BSC_MAINNET]: "https://bsc-dataseed1.binance.org",
|
|
403
428
|
[BASE_MAINNET]: "https://api.dexter.cash/api/base/rpc",
|
|
404
429
|
[BASE_SEPOLIA]: "https://sepolia.base.org",
|
|
405
430
|
[ARBITRUM_ONE]: "https://arb1.arbitrum.io/rpc",
|
|
@@ -411,6 +436,7 @@ var DEFAULT_RPC_URLS2 = {
|
|
|
411
436
|
[ETHEREUM_MAINNET]: "https://eth.llamarpc.com"
|
|
412
437
|
};
|
|
413
438
|
var USDC_ADDRESSES = {
|
|
439
|
+
[BSC_MAINNET]: BSC_USDC,
|
|
414
440
|
[BASE_MAINNET]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
415
441
|
[BASE_SEPOLIA]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
416
442
|
[ARBITRUM_ONE]: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
@@ -421,6 +447,10 @@ var USDC_ADDRESSES = {
|
|
|
421
447
|
[SKALE_BASE_SEPOLIA]: "0x2e08028E3C4c2356572E096d8EF835cD5C6030bD",
|
|
422
448
|
[ETHEREUM_MAINNET]: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
|
423
449
|
};
|
|
450
|
+
var BSC_STABLECOIN_ADDRESSES = {
|
|
451
|
+
[BSC_USDT]: { symbol: "USDT", decimals: 18 },
|
|
452
|
+
[BSC_USDC]: { symbol: "USDC", decimals: 18 }
|
|
453
|
+
};
|
|
424
454
|
function isEvmWallet(wallet) {
|
|
425
455
|
if (!wallet || typeof wallet !== "object") return false;
|
|
426
456
|
const w = wallet;
|
|
@@ -428,7 +458,7 @@ function isEvmWallet(wallet) {
|
|
|
428
458
|
}
|
|
429
459
|
var EvmAdapter = class {
|
|
430
460
|
name = "EVM";
|
|
431
|
-
networks = [BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE];
|
|
461
|
+
networks = [BSC_MAINNET, BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE];
|
|
432
462
|
config;
|
|
433
463
|
log;
|
|
434
464
|
constructor(config = {}) {
|
|
@@ -439,6 +469,7 @@ var EvmAdapter = class {
|
|
|
439
469
|
canHandle(network) {
|
|
440
470
|
if (this.networks.includes(network)) return true;
|
|
441
471
|
if (network === "base") return true;
|
|
472
|
+
if (network === "bsc") return true;
|
|
442
473
|
if (network === "ethereum") return true;
|
|
443
474
|
if (network === "arbitrum") return true;
|
|
444
475
|
if (network.startsWith("eip155:")) return true;
|
|
@@ -452,6 +483,7 @@ var EvmAdapter = class {
|
|
|
452
483
|
return DEFAULT_RPC_URLS2[network];
|
|
453
484
|
}
|
|
454
485
|
if (network === "base") return DEFAULT_RPC_URLS2[BASE_MAINNET];
|
|
486
|
+
if (network === "bsc") return DEFAULT_RPC_URLS2[BSC_MAINNET];
|
|
455
487
|
if (network === "ethereum") return DEFAULT_RPC_URLS2[ETHEREUM_MAINNET];
|
|
456
488
|
if (network === "arbitrum") return DEFAULT_RPC_URLS2[ARBITRUM_ONE];
|
|
457
489
|
return DEFAULT_RPC_URLS2[BASE_MAINNET];
|
|
@@ -471,6 +503,7 @@ var EvmAdapter = class {
|
|
|
471
503
|
return parseInt(chainIdStr, 10);
|
|
472
504
|
}
|
|
473
505
|
if (network === "base") return 8453;
|
|
506
|
+
if (network === "bsc") return 56;
|
|
474
507
|
if (network === "ethereum") return 1;
|
|
475
508
|
if (network === "arbitrum") return 42161;
|
|
476
509
|
return 8453;
|
|
@@ -520,13 +553,19 @@ var EvmAdapter = class {
|
|
|
520
553
|
const paddedAddress = address.slice(2).toLowerCase().padStart(64, "0");
|
|
521
554
|
return selector + paddedAddress;
|
|
522
555
|
}
|
|
523
|
-
async buildTransaction(accept, wallet,
|
|
556
|
+
async buildTransaction(accept, wallet, rpcUrl) {
|
|
524
557
|
if (!isEvmWallet(wallet)) {
|
|
525
558
|
throw new Error("Invalid EVM wallet");
|
|
526
559
|
}
|
|
527
560
|
if (!wallet.address) {
|
|
528
561
|
throw new Error("Wallet not connected");
|
|
529
562
|
}
|
|
563
|
+
if (accept.scheme === "exact-approval") {
|
|
564
|
+
return this.buildApprovalTransaction(accept, wallet, rpcUrl);
|
|
565
|
+
}
|
|
566
|
+
if (accept.extra?.assetTransferMethod === "permit2") {
|
|
567
|
+
return this.buildPermit2Transaction(accept, wallet, rpcUrl);
|
|
568
|
+
}
|
|
530
569
|
const { payTo, asset, extra } = accept;
|
|
531
570
|
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
532
571
|
if (!amount) {
|
|
@@ -597,6 +636,325 @@ var EvmAdapter = class {
|
|
|
597
636
|
signature
|
|
598
637
|
};
|
|
599
638
|
}
|
|
639
|
+
// ===========================================================================
|
|
640
|
+
// exact-approval: BSC and other chains without EIP-3009
|
|
641
|
+
// ===========================================================================
|
|
642
|
+
/**
|
|
643
|
+
* Build a payment transaction for chains that use the approval-based scheme.
|
|
644
|
+
* The facilitator's /supported response provides the EIP-712 domain and types
|
|
645
|
+
* in accept.extra, so the client doesn't hardcode any contract addresses.
|
|
646
|
+
*/
|
|
647
|
+
async buildApprovalTransaction(accept, wallet, rpcUrl) {
|
|
648
|
+
const { payTo, asset, extra } = accept;
|
|
649
|
+
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
650
|
+
if (!amount) {
|
|
651
|
+
throw new Error("Missing amount in payment requirements");
|
|
652
|
+
}
|
|
653
|
+
const facilitatorContract = extra?.facilitatorContract;
|
|
654
|
+
if (!facilitatorContract) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
"exact-approval scheme requires extra.facilitatorContract from the facilitator. The /supported endpoint should provide this."
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (!wallet.signTypedData) {
|
|
660
|
+
throw new Error("Wallet does not support signTypedData (EIP-712)");
|
|
661
|
+
}
|
|
662
|
+
this.log("Building approval-based transaction:", {
|
|
663
|
+
from: wallet.address,
|
|
664
|
+
to: payTo,
|
|
665
|
+
amount,
|
|
666
|
+
asset,
|
|
667
|
+
network: accept.network,
|
|
668
|
+
facilitatorContract
|
|
669
|
+
});
|
|
670
|
+
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
671
|
+
const fee = extra?.fee ?? "0";
|
|
672
|
+
const totalNeeded = BigInt(amount) + BigInt(fee);
|
|
673
|
+
const currentAllowance = await this.readAllowance(url, asset, wallet.address, facilitatorContract);
|
|
674
|
+
if (currentAllowance < totalNeeded) {
|
|
675
|
+
if (!wallet.sendTransaction) {
|
|
676
|
+
throw new Error(
|
|
677
|
+
"BSC payments require a wallet that supports sendTransaction for the one-time token approval. Use createEvmKeypairWallet() or a browser wallet with transaction support."
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
const approvalAmount = this.calculateApprovalAmount(amount, fee, extra?.approvalStrategy);
|
|
681
|
+
this.log(`Approving ${approvalAmount} for ${facilitatorContract} (current allowance: ${currentAllowance})`);
|
|
682
|
+
const approveTxHash = await wallet.sendTransaction({
|
|
683
|
+
to: asset,
|
|
684
|
+
data: this.encodeApprove(facilitatorContract, approvalAmount),
|
|
685
|
+
value: 0n
|
|
686
|
+
});
|
|
687
|
+
this.log(`Approval tx sent: ${approveTxHash}`);
|
|
688
|
+
await this.waitForReceipt(url, approveTxHash);
|
|
689
|
+
this.log("Approval confirmed");
|
|
690
|
+
} else {
|
|
691
|
+
this.log("Sufficient allowance, skipping approval");
|
|
692
|
+
}
|
|
693
|
+
const nonceBytes = new Uint8Array(16);
|
|
694
|
+
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(nonceBytes);
|
|
695
|
+
const nonce = [...nonceBytes].reduce((acc, b) => acc * 256n + BigInt(b), 0n).toString();
|
|
696
|
+
const paymentIdBytes = new Uint8Array(32);
|
|
697
|
+
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(paymentIdBytes);
|
|
698
|
+
const paymentId = "0x" + [...paymentIdBytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
699
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
700
|
+
const deadline = now + (accept.maxTimeoutSeconds || 300);
|
|
701
|
+
const eip712Domain = extra?.eip712Domain;
|
|
702
|
+
const domain = eip712Domain ? {
|
|
703
|
+
name: eip712Domain.name,
|
|
704
|
+
version: eip712Domain.version,
|
|
705
|
+
chainId: BigInt(eip712Domain.chainId),
|
|
706
|
+
verifyingContract: eip712Domain.verifyingContract
|
|
707
|
+
} : {
|
|
708
|
+
name: "DexterBSCFacilitator",
|
|
709
|
+
version: "1",
|
|
710
|
+
chainId: BigInt(this.getChainId(accept.network)),
|
|
711
|
+
verifyingContract: facilitatorContract
|
|
712
|
+
};
|
|
713
|
+
const types = extra?.eip712Types ?? {
|
|
714
|
+
Payment: [
|
|
715
|
+
{ name: "from", type: "address" },
|
|
716
|
+
{ name: "to", type: "address" },
|
|
717
|
+
{ name: "token", type: "address" },
|
|
718
|
+
{ name: "amount", type: "uint256" },
|
|
719
|
+
{ name: "fee", type: "uint256" },
|
|
720
|
+
{ name: "nonce", type: "uint256" },
|
|
721
|
+
{ name: "deadline", type: "uint256" },
|
|
722
|
+
{ name: "paymentId", type: "bytes32" }
|
|
723
|
+
]
|
|
724
|
+
};
|
|
725
|
+
const message = {
|
|
726
|
+
from: wallet.address,
|
|
727
|
+
to: payTo,
|
|
728
|
+
token: asset,
|
|
729
|
+
amount: BigInt(amount),
|
|
730
|
+
fee: BigInt(fee),
|
|
731
|
+
nonce: BigInt(nonce),
|
|
732
|
+
deadline: BigInt(deadline),
|
|
733
|
+
paymentId
|
|
734
|
+
};
|
|
735
|
+
const signature = await wallet.signTypedData({
|
|
736
|
+
domain,
|
|
737
|
+
types,
|
|
738
|
+
primaryType: "Payment",
|
|
739
|
+
message
|
|
740
|
+
});
|
|
741
|
+
this.log("EIP-712 Payment signature obtained");
|
|
742
|
+
const payload = {
|
|
743
|
+
from: wallet.address,
|
|
744
|
+
to: payTo,
|
|
745
|
+
token: asset,
|
|
746
|
+
amount,
|
|
747
|
+
fee,
|
|
748
|
+
nonce,
|
|
749
|
+
deadline,
|
|
750
|
+
paymentId,
|
|
751
|
+
signature
|
|
752
|
+
};
|
|
753
|
+
return {
|
|
754
|
+
serialized: JSON.stringify(payload),
|
|
755
|
+
signature
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
// ===========================================================================
|
|
759
|
+
// Permit2: Universal ERC-20 payments via Uniswap's Permit2 contract
|
|
760
|
+
// ===========================================================================
|
|
761
|
+
/**
|
|
762
|
+
* Build a Permit2 payment transaction. Used when the facilitator signals
|
|
763
|
+
* assetTransferMethod: "permit2" in extra (e.g., BSC where EIP-3009 is unavailable).
|
|
764
|
+
*
|
|
765
|
+
* Flow:
|
|
766
|
+
* 1. Check if token has approved the Permit2 contract. If not, approve(Permit2, maxUint256).
|
|
767
|
+
* 2. Sign EIP-712 PermitWitnessTransferFrom against the Permit2 contract.
|
|
768
|
+
* 3. Return { permit2Authorization, signature } payload for the facilitator.
|
|
769
|
+
*/
|
|
770
|
+
async buildPermit2Transaction(accept, wallet, rpcUrl) {
|
|
771
|
+
const { payTo, asset } = accept;
|
|
772
|
+
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
773
|
+
if (!amount) {
|
|
774
|
+
throw new Error("Missing amount in payment requirements");
|
|
775
|
+
}
|
|
776
|
+
if (!wallet.signTypedData) {
|
|
777
|
+
throw new Error("Wallet does not support signTypedData (EIP-712)");
|
|
778
|
+
}
|
|
779
|
+
this.log("Building Permit2 transaction:", {
|
|
780
|
+
from: wallet.address,
|
|
781
|
+
to: payTo,
|
|
782
|
+
amount,
|
|
783
|
+
asset,
|
|
784
|
+
network: accept.network
|
|
785
|
+
});
|
|
786
|
+
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
787
|
+
const currentAllowance = await this.readAllowance(url, asset, wallet.address, PERMIT2_ADDRESS);
|
|
788
|
+
if (currentAllowance < BigInt(amount)) {
|
|
789
|
+
if (!wallet.sendTransaction) {
|
|
790
|
+
throw new Error(
|
|
791
|
+
"Permit2 payments require a wallet that supports sendTransaction for the one-time Permit2 approval. Use createEvmKeypairWallet() or a browser wallet with transaction support."
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
this.log(`Approving Permit2 for ${asset} (current allowance: ${currentAllowance})`);
|
|
795
|
+
const approveTxHash = await wallet.sendTransaction({
|
|
796
|
+
to: asset,
|
|
797
|
+
data: this.encodeApprove(PERMIT2_ADDRESS, MAX_UINT256),
|
|
798
|
+
value: 0n
|
|
799
|
+
});
|
|
800
|
+
this.log(`Permit2 approval tx sent: ${approveTxHash}`);
|
|
801
|
+
await this.waitForReceipt(url, approveTxHash);
|
|
802
|
+
this.log("Permit2 approval confirmed");
|
|
803
|
+
} else {
|
|
804
|
+
this.log("Sufficient Permit2 allowance, skipping approval");
|
|
805
|
+
}
|
|
806
|
+
const nonceBytes = new Uint8Array(32);
|
|
807
|
+
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(nonceBytes);
|
|
808
|
+
const nonce = [...nonceBytes].reduce((acc, b) => acc * 256n + BigInt(b), 0n);
|
|
809
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
810
|
+
const validAfter = now - 600;
|
|
811
|
+
const deadline = now + (accept.maxTimeoutSeconds || 300);
|
|
812
|
+
const chainId = this.getChainId(accept.network);
|
|
813
|
+
const domain = {
|
|
814
|
+
name: "Permit2",
|
|
815
|
+
chainId: BigInt(chainId),
|
|
816
|
+
verifyingContract: PERMIT2_ADDRESS
|
|
817
|
+
};
|
|
818
|
+
const message = {
|
|
819
|
+
permitted: {
|
|
820
|
+
token: asset,
|
|
821
|
+
amount: BigInt(amount)
|
|
822
|
+
},
|
|
823
|
+
spender: X402_EXACT_PERMIT2_PROXY,
|
|
824
|
+
nonce,
|
|
825
|
+
deadline: BigInt(deadline),
|
|
826
|
+
witness: {
|
|
827
|
+
to: payTo,
|
|
828
|
+
validAfter: BigInt(validAfter)
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
const signature = await wallet.signTypedData({
|
|
832
|
+
domain,
|
|
833
|
+
types: PERMIT2_WITNESS_TYPES,
|
|
834
|
+
primaryType: "PermitWitnessTransferFrom",
|
|
835
|
+
message
|
|
836
|
+
});
|
|
837
|
+
this.log("Permit2 PermitWitnessTransferFrom signature obtained");
|
|
838
|
+
const payload = {
|
|
839
|
+
signature,
|
|
840
|
+
permit2Authorization: {
|
|
841
|
+
from: wallet.address,
|
|
842
|
+
permitted: {
|
|
843
|
+
token: asset,
|
|
844
|
+
amount
|
|
845
|
+
},
|
|
846
|
+
spender: X402_EXACT_PERMIT2_PROXY,
|
|
847
|
+
nonce: nonce.toString(),
|
|
848
|
+
deadline: String(deadline),
|
|
849
|
+
witness: {
|
|
850
|
+
to: payTo,
|
|
851
|
+
validAfter: String(validAfter)
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
return {
|
|
856
|
+
serialized: JSON.stringify(payload),
|
|
857
|
+
signature
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Read ERC-20 allowance via raw eth_call (no viem dependency needed).
|
|
862
|
+
*/
|
|
863
|
+
async readAllowance(rpcUrl, token, owner, spender) {
|
|
864
|
+
const selector = "0xdd62ed3e";
|
|
865
|
+
const paddedOwner = owner.slice(2).toLowerCase().padStart(64, "0");
|
|
866
|
+
const paddedSpender = spender.slice(2).toLowerCase().padStart(64, "0");
|
|
867
|
+
const data = selector + paddedOwner + paddedSpender;
|
|
868
|
+
try {
|
|
869
|
+
const response = await fetch(rpcUrl, {
|
|
870
|
+
method: "POST",
|
|
871
|
+
headers: { "Content-Type": "application/json" },
|
|
872
|
+
body: JSON.stringify({
|
|
873
|
+
jsonrpc: "2.0",
|
|
874
|
+
id: 1,
|
|
875
|
+
method: "eth_call",
|
|
876
|
+
params: [{ to: token, data }, "latest"]
|
|
877
|
+
})
|
|
878
|
+
});
|
|
879
|
+
const result = await response.json();
|
|
880
|
+
if (result.error || !result.result || result.result === "0x") return 0n;
|
|
881
|
+
return BigInt(result.result);
|
|
882
|
+
} catch {
|
|
883
|
+
return 0n;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Encode ERC-20 approve(address,uint256) calldata.
|
|
888
|
+
*/
|
|
889
|
+
encodeApprove(spender, amount) {
|
|
890
|
+
const selector = "0x095ea7b3";
|
|
891
|
+
const paddedSpender = spender.slice(2).toLowerCase().padStart(64, "0");
|
|
892
|
+
const paddedAmount = amount.toString(16).padStart(64, "0");
|
|
893
|
+
return selector + paddedSpender + paddedAmount;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Wait for a transaction receipt by polling eth_getTransactionReceipt.
|
|
897
|
+
*/
|
|
898
|
+
async waitForReceipt(rpcUrl, txHash, timeoutMs = 3e4) {
|
|
899
|
+
const start = Date.now();
|
|
900
|
+
while (Date.now() - start < timeoutMs) {
|
|
901
|
+
try {
|
|
902
|
+
const response = await fetch(rpcUrl, {
|
|
903
|
+
method: "POST",
|
|
904
|
+
headers: { "Content-Type": "application/json" },
|
|
905
|
+
body: JSON.stringify({
|
|
906
|
+
jsonrpc: "2.0",
|
|
907
|
+
id: 1,
|
|
908
|
+
method: "eth_getTransactionReceipt",
|
|
909
|
+
params: [txHash]
|
|
910
|
+
})
|
|
911
|
+
});
|
|
912
|
+
const result = await response.json();
|
|
913
|
+
if (result.result) {
|
|
914
|
+
if (result.result.status === "0x0") {
|
|
915
|
+
throw new Error(`Approval transaction reverted: ${txHash}`);
|
|
916
|
+
}
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
} catch (err) {
|
|
920
|
+
if (err instanceof Error && err.message.includes("reverted")) throw err;
|
|
921
|
+
}
|
|
922
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
923
|
+
}
|
|
924
|
+
throw new Error(`Approval transaction receipt timeout after ${timeoutMs}ms: ${txHash}`);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Calculate how much to approve based on the facilitator's approval strategy.
|
|
928
|
+
* Buffered approvals reduce the number of on-chain approval txs for micropayments.
|
|
929
|
+
*/
|
|
930
|
+
calculateApprovalAmount(paymentAmount, fee, strategy) {
|
|
931
|
+
const total = BigInt(paymentAmount) + BigInt(fee);
|
|
932
|
+
if (!strategy || strategy.mode === "exact") {
|
|
933
|
+
return total;
|
|
934
|
+
}
|
|
935
|
+
const multiple = BigInt(strategy.defaultMultiple ?? 10);
|
|
936
|
+
const buffered = total * multiple;
|
|
937
|
+
if (strategy.maxCapUsd) {
|
|
938
|
+
const decimals = this.inferDecimals(paymentAmount);
|
|
939
|
+
const maxCap = BigInt(Math.floor(strategy.maxCapUsd * Math.pow(10, decimals)));
|
|
940
|
+
if (buffered > maxCap) return maxCap;
|
|
941
|
+
}
|
|
942
|
+
if (strategy.exactAboveUsd) {
|
|
943
|
+
const decimals = this.inferDecimals(paymentAmount);
|
|
944
|
+
const threshold = BigInt(Math.floor(strategy.exactAboveUsd * Math.pow(10, decimals)));
|
|
945
|
+
if (BigInt(paymentAmount) > threshold) return total;
|
|
946
|
+
}
|
|
947
|
+
return buffered;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Infer token decimals from payment amount magnitude.
|
|
951
|
+
* BSC stablecoins use 18 decimals, all others use 6.
|
|
952
|
+
* A $1 payment is 1000000 (6 dec) or 1000000000000000000 (18 dec).
|
|
953
|
+
* If the amount has > 12 digits, it's almost certainly 18 decimals.
|
|
954
|
+
*/
|
|
955
|
+
inferDecimals(amount) {
|
|
956
|
+
return amount.length > 12 ? 18 : 6;
|
|
957
|
+
}
|
|
600
958
|
};
|
|
601
959
|
function createEvmAdapter(config) {
|
|
602
960
|
return new EvmAdapter(config);
|
|
@@ -610,6 +968,9 @@ function isKnownUSDC(asset) {
|
|
|
610
968
|
for (const addr of Object.values(USDC_ADDRESSES)) {
|
|
611
969
|
if (addr.toLowerCase() === lc) return true;
|
|
612
970
|
}
|
|
971
|
+
for (const addr of Object.keys(BSC_STABLECOIN_ADDRESSES)) {
|
|
972
|
+
if (addr.toLowerCase() === lc) return true;
|
|
973
|
+
}
|
|
613
974
|
return false;
|
|
614
975
|
}
|
|
615
976
|
|
|
@@ -629,10 +990,33 @@ function createX402Client(config) {
|
|
|
629
990
|
fetch: customFetch = globalThis.fetch,
|
|
630
991
|
verbose = false,
|
|
631
992
|
accessPass: accessPassConfig,
|
|
632
|
-
onPaymentRequired
|
|
993
|
+
onPaymentRequired,
|
|
994
|
+
maxRetries = 0,
|
|
995
|
+
retryDelayMs = 500
|
|
633
996
|
} = config;
|
|
634
997
|
const log = verbose ? console.log.bind(console, "[x402]") : () => {
|
|
635
998
|
};
|
|
999
|
+
async function fetchWithRetry(input, init) {
|
|
1000
|
+
let lastError;
|
|
1001
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1002
|
+
try {
|
|
1003
|
+
const response = await customFetch(input, init);
|
|
1004
|
+
if (response.status >= 502 && response.status <= 504 && attempt < maxRetries) {
|
|
1005
|
+
log(`Retry ${attempt + 1}/${maxRetries}: server returned ${response.status}`);
|
|
1006
|
+
await new Promise((r) => setTimeout(r, retryDelayMs * Math.pow(2, attempt)));
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
return response;
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
lastError = err;
|
|
1012
|
+
if (attempt < maxRetries) {
|
|
1013
|
+
log(`Retry ${attempt + 1}/${maxRetries}: ${err instanceof Error ? err.message : "network error"}`);
|
|
1014
|
+
await new Promise((r) => setTimeout(r, retryDelayMs * Math.pow(2, attempt)));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
throw lastError;
|
|
1019
|
+
}
|
|
636
1020
|
const passCache = /* @__PURE__ */ new Map();
|
|
637
1021
|
function getCachedPass(url) {
|
|
638
1022
|
try {
|
|
@@ -695,6 +1079,19 @@ function createX402Client(config) {
|
|
|
695
1079
|
}
|
|
696
1080
|
return candidates[0];
|
|
697
1081
|
}
|
|
1082
|
+
function getChainDisplayName(network, adapterName) {
|
|
1083
|
+
const names = {
|
|
1084
|
+
"eip155:56": "BSC",
|
|
1085
|
+
"eip155:8453": "Base",
|
|
1086
|
+
"eip155:84532": "Base Sepolia",
|
|
1087
|
+
"eip155:42161": "Arbitrum",
|
|
1088
|
+
"eip155:137": "Polygon",
|
|
1089
|
+
"eip155:10": "Optimism",
|
|
1090
|
+
"eip155:43114": "Avalanche",
|
|
1091
|
+
"eip155:1": "Ethereum"
|
|
1092
|
+
};
|
|
1093
|
+
return names[network] || adapterName;
|
|
1094
|
+
}
|
|
698
1095
|
function getRpcUrl(network, adapter) {
|
|
699
1096
|
return rpcUrls[network] || adapter.getDefaultRpcUrl(network);
|
|
700
1097
|
}
|
|
@@ -832,7 +1229,7 @@ function createX402Client(config) {
|
|
|
832
1229
|
}
|
|
833
1230
|
}
|
|
834
1231
|
}
|
|
835
|
-
const response = await
|
|
1232
|
+
const response = await fetchWithRetry(input, init);
|
|
836
1233
|
if (response.status !== 402) {
|
|
837
1234
|
return response;
|
|
838
1235
|
}
|
|
@@ -909,10 +1306,10 @@ function createX402Client(config) {
|
|
|
909
1306
|
const balance = await adapter.getBalance(accept, wallet, rpcUrl);
|
|
910
1307
|
const requiredAmount = Number(paymentAmount) / Math.pow(10, decimals);
|
|
911
1308
|
if (balance < requiredAmount) {
|
|
912
|
-
const
|
|
1309
|
+
const chainName = getChainDisplayName(accept.network, adapter.name);
|
|
913
1310
|
throw new X402Error(
|
|
914
1311
|
"insufficient_balance",
|
|
915
|
-
`Insufficient
|
|
1312
|
+
`Insufficient balance on ${chainName}. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
|
|
916
1313
|
);
|
|
917
1314
|
}
|
|
918
1315
|
log(`Balance OK: $${balance.toFixed(4)} >= $${requiredAmount.toFixed(4)}`);
|
|
@@ -967,7 +1364,7 @@ function createX402Client(config) {
|
|
|
967
1364
|
};
|
|
968
1365
|
const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
|
|
969
1366
|
log("Retrying request with payment...");
|
|
970
|
-
const retryResponse = await
|
|
1367
|
+
const retryResponse = await fetchWithRetry(input, {
|
|
971
1368
|
...init,
|
|
972
1369
|
headers: {
|
|
973
1370
|
...init?.headers || {},
|
|
@@ -1111,7 +1508,8 @@ function wrapFetch(fetchImpl, options) {
|
|
|
1111
1508
|
rpcUrls,
|
|
1112
1509
|
maxAmountAtomic,
|
|
1113
1510
|
verbose,
|
|
1114
|
-
accessPass
|
|
1511
|
+
accessPass,
|
|
1512
|
+
onPaymentRequired
|
|
1115
1513
|
} = options;
|
|
1116
1514
|
if (!walletPrivateKey && !evmPrivateKey) {
|
|
1117
1515
|
throw new Error("At least one wallet private key is required (walletPrivateKey or evmPrivateKey)");
|
|
@@ -1144,7 +1542,8 @@ function wrapFetch(fetchImpl, options) {
|
|
|
1144
1542
|
maxAmountAtomic,
|
|
1145
1543
|
fetch: fetchImpl,
|
|
1146
1544
|
verbose,
|
|
1147
|
-
accessPass
|
|
1545
|
+
accessPass,
|
|
1546
|
+
onPaymentRequired
|
|
1148
1547
|
};
|
|
1149
1548
|
const client = createX402Client(clientConfig);
|
|
1150
1549
|
const clientFetch = client.fetch.bind(client);
|
|
@@ -1157,6 +1556,164 @@ function wrapFetch(fetchImpl, options) {
|
|
|
1157
1556
|
return clientFetch;
|
|
1158
1557
|
}
|
|
1159
1558
|
|
|
1559
|
+
// src/client/discovery.ts
|
|
1560
|
+
var DEFAULT_MARKETPLACE = "https://x402.dexter.cash/api/facilitator/marketplace/resources";
|
|
1561
|
+
async function searchAPIs(options = {}) {
|
|
1562
|
+
const {
|
|
1563
|
+
query,
|
|
1564
|
+
category,
|
|
1565
|
+
network,
|
|
1566
|
+
maxPrice,
|
|
1567
|
+
verifiedOnly,
|
|
1568
|
+
sort = "marketplace",
|
|
1569
|
+
limit = 20,
|
|
1570
|
+
marketplaceUrl = DEFAULT_MARKETPLACE
|
|
1571
|
+
} = options;
|
|
1572
|
+
const params = new URLSearchParams();
|
|
1573
|
+
if (query) params.set("search", query);
|
|
1574
|
+
if (category) params.set("category", category);
|
|
1575
|
+
if (network) params.set("network", network);
|
|
1576
|
+
if (maxPrice !== void 0) params.set("maxPrice", String(maxPrice));
|
|
1577
|
+
if (verifiedOnly) params.set("verified", "true");
|
|
1578
|
+
params.set("sort", sort);
|
|
1579
|
+
params.set("order", "desc");
|
|
1580
|
+
params.set("limit", String(Math.min(limit, 50)));
|
|
1581
|
+
const url = `${marketplaceUrl}?${params.toString()}`;
|
|
1582
|
+
const response = await fetch(url, {
|
|
1583
|
+
headers: { "Accept": "application/json" },
|
|
1584
|
+
signal: AbortSignal.timeout(15e3)
|
|
1585
|
+
});
|
|
1586
|
+
if (!response.ok) {
|
|
1587
|
+
throw new Error(`Marketplace search failed: ${response.status}`);
|
|
1588
|
+
}
|
|
1589
|
+
const data = await response.json();
|
|
1590
|
+
if (!data.resources) return [];
|
|
1591
|
+
return data.resources.map((r) => ({
|
|
1592
|
+
name: r.displayName || r.resourceUrl,
|
|
1593
|
+
url: r.resourceUrl,
|
|
1594
|
+
method: r.method || "GET",
|
|
1595
|
+
price: r.priceLabel || (r.priceUsdc ? `$${r.priceUsdc.toFixed(4)}` : "free"),
|
|
1596
|
+
priceUsdc: r.priceUsdc ?? null,
|
|
1597
|
+
network: r.priceNetwork ?? null,
|
|
1598
|
+
description: r.description || "",
|
|
1599
|
+
category: r.category || "uncategorized",
|
|
1600
|
+
qualityScore: r.qualityScore ?? null,
|
|
1601
|
+
verified: r.verificationStatus === "pass",
|
|
1602
|
+
totalCalls: r.totalSettlements || 0,
|
|
1603
|
+
totalVolume: r.totalVolumeUsdc ? `$${r.totalVolumeUsdc.toLocaleString("en-US", { minimumFractionDigits: 2 })}` : null,
|
|
1604
|
+
seller: r.seller?.displayName ?? null,
|
|
1605
|
+
sellerReputation: r.reputationScore ?? null,
|
|
1606
|
+
authRequired: r.authRequired || false,
|
|
1607
|
+
lastActive: r.lastSettlementAt ?? null
|
|
1608
|
+
}));
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/client/budget-account.ts
|
|
1612
|
+
function createBudgetAccount(config) {
|
|
1613
|
+
const { budget, allowedDomains, onPaymentRequired: userOnPayment, ...fetchOptions } = config;
|
|
1614
|
+
const totalBudget = parseFloat(budget.total);
|
|
1615
|
+
const perRequestMax = budget.perRequest ? parseFloat(budget.perRequest) : Infinity;
|
|
1616
|
+
const perHourMax = budget.perHour ? parseFloat(budget.perHour) : Infinity;
|
|
1617
|
+
if (isNaN(totalBudget) || totalBudget <= 0) {
|
|
1618
|
+
throw new Error("budget.total must be a positive number");
|
|
1619
|
+
}
|
|
1620
|
+
let ledger = [];
|
|
1621
|
+
let pendingAmount = 0;
|
|
1622
|
+
function getSpent() {
|
|
1623
|
+
return ledger.reduce((sum, r) => sum + r.amount, 0);
|
|
1624
|
+
}
|
|
1625
|
+
function getHourlySpend() {
|
|
1626
|
+
const cutoff = Date.now() - 36e5;
|
|
1627
|
+
return ledger.filter((r) => r.timestamp >= cutoff).reduce((sum, r) => sum + r.amount, 0);
|
|
1628
|
+
}
|
|
1629
|
+
const innerFetch = wrapFetch(fetch, {
|
|
1630
|
+
...fetchOptions,
|
|
1631
|
+
onPaymentRequired: async (accept) => {
|
|
1632
|
+
const decimals = accept.extra?.decimals ?? 6;
|
|
1633
|
+
const amountUsd = Number(accept.amount) / Math.pow(10, decimals);
|
|
1634
|
+
if (amountUsd > perRequestMax) {
|
|
1635
|
+
throw new X402Error(
|
|
1636
|
+
"amount_exceeds_max",
|
|
1637
|
+
`$${amountUsd.toFixed(4)} exceeds per-request limit of $${perRequestMax.toFixed(2)}`
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
const spent = getSpent();
|
|
1641
|
+
if (spent + amountUsd > totalBudget) {
|
|
1642
|
+
throw new X402Error(
|
|
1643
|
+
"amount_exceeds_max",
|
|
1644
|
+
`Budget exceeded. Spent $${spent.toFixed(2)} of $${totalBudget.toFixed(2)}, payment: $${amountUsd.toFixed(4)}`
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
const hourly = getHourlySpend();
|
|
1648
|
+
if (hourly + amountUsd > perHourMax) {
|
|
1649
|
+
throw new X402Error(
|
|
1650
|
+
"amount_exceeds_max",
|
|
1651
|
+
`Hourly limit ($${perHourMax.toFixed(2)}) exceeded. Spent $${hourly.toFixed(2)} this hour`
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
pendingAmount = amountUsd;
|
|
1655
|
+
if (userOnPayment) return userOnPayment(accept);
|
|
1656
|
+
return true;
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
const budgetFetch = (async (input, init) => {
|
|
1660
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1661
|
+
let domain = "unknown";
|
|
1662
|
+
try {
|
|
1663
|
+
domain = new URL(url).hostname;
|
|
1664
|
+
} catch {
|
|
1665
|
+
}
|
|
1666
|
+
if (allowedDomains) {
|
|
1667
|
+
if (!allowedDomains.some((d) => domain === d || domain.endsWith(`.${d}`))) {
|
|
1668
|
+
throw new X402Error("payment_rejected", `Domain "${domain}" not in allowed domains`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
pendingAmount = 0;
|
|
1672
|
+
const response = await innerFetch(input, init);
|
|
1673
|
+
if (pendingAmount > 0) {
|
|
1674
|
+
let network = "unknown";
|
|
1675
|
+
const paymentHeader = response.headers.get("PAYMENT-RESPONSE");
|
|
1676
|
+
if (paymentHeader) {
|
|
1677
|
+
try {
|
|
1678
|
+
const decoded = JSON.parse(atob(paymentHeader));
|
|
1679
|
+
network = decoded.network || network;
|
|
1680
|
+
} catch {
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
ledger.push({ amount: pendingAmount, domain, network, timestamp: Date.now() });
|
|
1684
|
+
pendingAmount = 0;
|
|
1685
|
+
}
|
|
1686
|
+
return response;
|
|
1687
|
+
});
|
|
1688
|
+
return {
|
|
1689
|
+
fetch: budgetFetch,
|
|
1690
|
+
get spent() {
|
|
1691
|
+
return `$${getSpent().toFixed(2)}`;
|
|
1692
|
+
},
|
|
1693
|
+
get remaining() {
|
|
1694
|
+
return `$${(totalBudget - getSpent()).toFixed(2)}`;
|
|
1695
|
+
},
|
|
1696
|
+
get payments() {
|
|
1697
|
+
return ledger.length;
|
|
1698
|
+
},
|
|
1699
|
+
get spentAmount() {
|
|
1700
|
+
return getSpent();
|
|
1701
|
+
},
|
|
1702
|
+
get remainingAmount() {
|
|
1703
|
+
return totalBudget - getSpent();
|
|
1704
|
+
},
|
|
1705
|
+
get ledger() {
|
|
1706
|
+
return ledger;
|
|
1707
|
+
},
|
|
1708
|
+
get hourlySpend() {
|
|
1709
|
+
return getHourlySpend();
|
|
1710
|
+
},
|
|
1711
|
+
reset() {
|
|
1712
|
+
ledger = [];
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1160
1717
|
// src/client/sponsored-access.ts
|
|
1161
1718
|
function getSponsoredAccessInfo(response) {
|
|
1162
1719
|
const receipt = getPaymentReceipt(response);
|
|
@@ -1185,6 +1742,7 @@ export {
|
|
|
1185
1742
|
SOLANA_MAINNET,
|
|
1186
1743
|
USDC_MINT,
|
|
1187
1744
|
X402Error,
|
|
1745
|
+
createBudgetAccount,
|
|
1188
1746
|
createEvmAdapter,
|
|
1189
1747
|
createEvmKeypairWallet,
|
|
1190
1748
|
createKeypairWallet,
|
|
@@ -1196,6 +1754,6 @@ export {
|
|
|
1196
1754
|
getSponsoredRecommendations,
|
|
1197
1755
|
isEvmKeypairWallet,
|
|
1198
1756
|
isKeypairWallet,
|
|
1757
|
+
searchAPIs,
|
|
1199
1758
|
wrapFetch
|
|
1200
1759
|
};
|
|
1201
|
-
//# sourceMappingURL=index.js.map
|