@dexterai/x402 2.0.0 → 3.0.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 +443 -2
- package/dist/adapters/index.d.cts +4 -3
- package/dist/adapters/index.d.ts +4 -3
- package/dist/adapters/index.js +437 -2
- package/dist/client/index.cjs +532 -46
- package/dist/client/index.d.cts +184 -63
- package/dist/client/index.d.ts +184 -63
- package/dist/client/index.js +531 -45
- package/dist/react/index.cjs +452 -4
- package/dist/react/index.d.cts +5 -5
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.js +452 -4
- package/dist/server/index.cjs +3 -3
- package/dist/server/index.d.cts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +3 -3
- package/dist/{sponsored-access-DjLEKhOV.d.ts → sponsored-access-BVoucsEW.d.ts} +2 -2
- package/dist/{sponsored-access-BgEDLg_H.d.cts → sponsored-access-CE7WpV5b.d.cts} +2 -2
- package/dist/{types-DWhpiOBD.d.cts → types-C_aQh02s.d.cts} +86 -3
- package/dist/{types-D1TGACsL.d.ts → types-DBS0XOsH.d.ts} +86 -3
- 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/package.json +1 -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
|
+
capabilitySearch: () => capabilitySearch,
|
|
194
195
|
createBudgetAccount: () => createBudgetAccount,
|
|
195
196
|
createEvmAdapter: () => createEvmAdapter,
|
|
196
197
|
createEvmKeypairWallet: () => createEvmKeypairWallet,
|
|
@@ -203,7 +204,6 @@ __export(client_exports, {
|
|
|
203
204
|
getSponsoredRecommendations: () => getSponsoredRecommendations,
|
|
204
205
|
isEvmKeypairWallet: () => isEvmKeypairWallet,
|
|
205
206
|
isKeypairWallet: () => isKeypairWallet,
|
|
206
|
-
searchAPIs: () => searchAPIs,
|
|
207
207
|
wrapFetch: () => wrapFetch
|
|
208
208
|
});
|
|
209
209
|
module.exports = __toCommonJS(client_exports);
|
|
@@ -414,16 +414,40 @@ function createSolanaAdapter(config) {
|
|
|
414
414
|
}
|
|
415
415
|
|
|
416
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");
|
|
417
437
|
var BASE_MAINNET = "eip155:8453";
|
|
418
438
|
var BASE_SEPOLIA = "eip155:84532";
|
|
419
439
|
var ARBITRUM_ONE = "eip155:42161";
|
|
420
440
|
var POLYGON = "eip155:137";
|
|
421
441
|
var OPTIMISM = "eip155:10";
|
|
422
442
|
var AVALANCHE = "eip155:43114";
|
|
443
|
+
var BSC_MAINNET = "eip155:56";
|
|
423
444
|
var SKALE_BASE = "eip155:1187947933";
|
|
424
445
|
var SKALE_BASE_SEPOLIA = "eip155:324705682";
|
|
425
446
|
var ETHEREUM_MAINNET = "eip155:1";
|
|
447
|
+
var BSC_USDT = "0x55d398326f99059fF775485246999027B3197955";
|
|
448
|
+
var BSC_USDC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d";
|
|
426
449
|
var CHAIN_IDS = {
|
|
450
|
+
[BSC_MAINNET]: 56,
|
|
427
451
|
[BASE_MAINNET]: 8453,
|
|
428
452
|
[BASE_SEPOLIA]: 84532,
|
|
429
453
|
[ARBITRUM_ONE]: 42161,
|
|
@@ -435,6 +459,7 @@ var CHAIN_IDS = {
|
|
|
435
459
|
[ETHEREUM_MAINNET]: 1
|
|
436
460
|
};
|
|
437
461
|
var DEFAULT_RPC_URLS2 = {
|
|
462
|
+
[BSC_MAINNET]: "https://bsc-dataseed1.binance.org",
|
|
438
463
|
[BASE_MAINNET]: "https://api.dexter.cash/api/base/rpc",
|
|
439
464
|
[BASE_SEPOLIA]: "https://sepolia.base.org",
|
|
440
465
|
[ARBITRUM_ONE]: "https://arb1.arbitrum.io/rpc",
|
|
@@ -446,6 +471,7 @@ var DEFAULT_RPC_URLS2 = {
|
|
|
446
471
|
[ETHEREUM_MAINNET]: "https://eth.llamarpc.com"
|
|
447
472
|
};
|
|
448
473
|
var USDC_ADDRESSES = {
|
|
474
|
+
[BSC_MAINNET]: BSC_USDC,
|
|
449
475
|
[BASE_MAINNET]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
450
476
|
[BASE_SEPOLIA]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
451
477
|
[ARBITRUM_ONE]: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
@@ -456,6 +482,10 @@ var USDC_ADDRESSES = {
|
|
|
456
482
|
[SKALE_BASE_SEPOLIA]: "0x2e08028E3C4c2356572E096d8EF835cD5C6030bD",
|
|
457
483
|
[ETHEREUM_MAINNET]: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
|
458
484
|
};
|
|
485
|
+
var BSC_STABLECOIN_ADDRESSES = {
|
|
486
|
+
[BSC_USDT]: { symbol: "USDT", decimals: 18 },
|
|
487
|
+
[BSC_USDC]: { symbol: "USDC", decimals: 18 }
|
|
488
|
+
};
|
|
459
489
|
function isEvmWallet(wallet) {
|
|
460
490
|
if (!wallet || typeof wallet !== "object") return false;
|
|
461
491
|
const w = wallet;
|
|
@@ -463,7 +493,7 @@ function isEvmWallet(wallet) {
|
|
|
463
493
|
}
|
|
464
494
|
var EvmAdapter = class {
|
|
465
495
|
name = "EVM";
|
|
466
|
-
networks = [BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE];
|
|
496
|
+
networks = [BSC_MAINNET, BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE];
|
|
467
497
|
config;
|
|
468
498
|
log;
|
|
469
499
|
constructor(config = {}) {
|
|
@@ -474,6 +504,7 @@ var EvmAdapter = class {
|
|
|
474
504
|
canHandle(network) {
|
|
475
505
|
if (this.networks.includes(network)) return true;
|
|
476
506
|
if (network === "base") return true;
|
|
507
|
+
if (network === "bsc") return true;
|
|
477
508
|
if (network === "ethereum") return true;
|
|
478
509
|
if (network === "arbitrum") return true;
|
|
479
510
|
if (network.startsWith("eip155:")) return true;
|
|
@@ -487,6 +518,7 @@ var EvmAdapter = class {
|
|
|
487
518
|
return DEFAULT_RPC_URLS2[network];
|
|
488
519
|
}
|
|
489
520
|
if (network === "base") return DEFAULT_RPC_URLS2[BASE_MAINNET];
|
|
521
|
+
if (network === "bsc") return DEFAULT_RPC_URLS2[BSC_MAINNET];
|
|
490
522
|
if (network === "ethereum") return DEFAULT_RPC_URLS2[ETHEREUM_MAINNET];
|
|
491
523
|
if (network === "arbitrum") return DEFAULT_RPC_URLS2[ARBITRUM_ONE];
|
|
492
524
|
return DEFAULT_RPC_URLS2[BASE_MAINNET];
|
|
@@ -506,6 +538,7 @@ var EvmAdapter = class {
|
|
|
506
538
|
return parseInt(chainIdStr, 10);
|
|
507
539
|
}
|
|
508
540
|
if (network === "base") return 8453;
|
|
541
|
+
if (network === "bsc") return 56;
|
|
509
542
|
if (network === "ethereum") return 1;
|
|
510
543
|
if (network === "arbitrum") return 42161;
|
|
511
544
|
return 8453;
|
|
@@ -555,13 +588,19 @@ var EvmAdapter = class {
|
|
|
555
588
|
const paddedAddress = address.slice(2).toLowerCase().padStart(64, "0");
|
|
556
589
|
return selector + paddedAddress;
|
|
557
590
|
}
|
|
558
|
-
async buildTransaction(accept, wallet,
|
|
591
|
+
async buildTransaction(accept, wallet, rpcUrl) {
|
|
559
592
|
if (!isEvmWallet(wallet)) {
|
|
560
593
|
throw new Error("Invalid EVM wallet");
|
|
561
594
|
}
|
|
562
595
|
if (!wallet.address) {
|
|
563
596
|
throw new Error("Wallet not connected");
|
|
564
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
|
+
}
|
|
565
604
|
const { payTo, asset, extra } = accept;
|
|
566
605
|
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
567
606
|
if (!amount) {
|
|
@@ -632,6 +671,393 @@ var EvmAdapter = class {
|
|
|
632
671
|
signature
|
|
633
672
|
};
|
|
634
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
|
+
let approvalExtension;
|
|
824
|
+
if (currentAllowance < BigInt(amount)) {
|
|
825
|
+
const approveData = this.encodeApprove(PERMIT2_ADDRESS, MAX_UINT256);
|
|
826
|
+
if (wallet.signTransaction) {
|
|
827
|
+
this.log(`Signing Permit2 approval for relay (current allowance: ${currentAllowance})`);
|
|
828
|
+
const chainId2 = this.getChainId(accept.network);
|
|
829
|
+
const gasPrice = await this.readGasPrice(url);
|
|
830
|
+
const nonce2 = await this.readNonce(url, wallet.address);
|
|
831
|
+
const signedTx = await wallet.signTransaction({
|
|
832
|
+
to: asset,
|
|
833
|
+
data: approveData,
|
|
834
|
+
chainId: chainId2,
|
|
835
|
+
gas: 50000n,
|
|
836
|
+
// standard ERC-20 approve
|
|
837
|
+
gasPrice,
|
|
838
|
+
nonce: nonce2
|
|
839
|
+
});
|
|
840
|
+
approvalExtension = {
|
|
841
|
+
erc20ApprovalGasSponsoring: {
|
|
842
|
+
info: {
|
|
843
|
+
from: wallet.address,
|
|
844
|
+
asset,
|
|
845
|
+
spender: PERMIT2_ADDRESS,
|
|
846
|
+
amount: MAX_UINT256.toString(),
|
|
847
|
+
signedTransaction: signedTx,
|
|
848
|
+
version: "1"
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
this.log("Permit2 approval signed for facilitator relay");
|
|
853
|
+
} else if (wallet.sendTransaction) {
|
|
854
|
+
this.log(`Approving Permit2 directly (current allowance: ${currentAllowance})`);
|
|
855
|
+
const approveTxHash = await wallet.sendTransaction({
|
|
856
|
+
to: asset,
|
|
857
|
+
data: approveData,
|
|
858
|
+
value: 0n
|
|
859
|
+
});
|
|
860
|
+
this.log(`Permit2 approval tx sent: ${approveTxHash}`);
|
|
861
|
+
await this.waitForReceipt(url, approveTxHash);
|
|
862
|
+
this.log("Permit2 approval confirmed");
|
|
863
|
+
} else {
|
|
864
|
+
throw new Error(
|
|
865
|
+
"Permit2 payments require a wallet that supports signTransaction or sendTransaction for the one-time Permit2 approval. Use createEvmKeypairWallet() or a browser wallet with transaction support."
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
} else {
|
|
869
|
+
this.log("Sufficient Permit2 allowance, skipping approval");
|
|
870
|
+
}
|
|
871
|
+
const nonceBytes = new Uint8Array(32);
|
|
872
|
+
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(nonceBytes);
|
|
873
|
+
const nonce = [...nonceBytes].reduce((acc, b) => acc * 256n + BigInt(b), 0n);
|
|
874
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
875
|
+
const validAfter = now - 600;
|
|
876
|
+
const deadline = now + (accept.maxTimeoutSeconds || 300);
|
|
877
|
+
const chainId = this.getChainId(accept.network);
|
|
878
|
+
const domain = {
|
|
879
|
+
name: "Permit2",
|
|
880
|
+
chainId: BigInt(chainId),
|
|
881
|
+
verifyingContract: PERMIT2_ADDRESS
|
|
882
|
+
};
|
|
883
|
+
const message = {
|
|
884
|
+
permitted: {
|
|
885
|
+
token: asset,
|
|
886
|
+
amount: BigInt(amount)
|
|
887
|
+
},
|
|
888
|
+
spender: X402_EXACT_PERMIT2_PROXY,
|
|
889
|
+
nonce,
|
|
890
|
+
deadline: BigInt(deadline),
|
|
891
|
+
witness: {
|
|
892
|
+
to: payTo,
|
|
893
|
+
validAfter: BigInt(validAfter)
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
const signature = await wallet.signTypedData({
|
|
897
|
+
domain,
|
|
898
|
+
types: PERMIT2_WITNESS_TYPES,
|
|
899
|
+
primaryType: "PermitWitnessTransferFrom",
|
|
900
|
+
message
|
|
901
|
+
});
|
|
902
|
+
this.log("Permit2 PermitWitnessTransferFrom signature obtained");
|
|
903
|
+
const payload = {
|
|
904
|
+
signature,
|
|
905
|
+
permit2Authorization: {
|
|
906
|
+
from: wallet.address,
|
|
907
|
+
permitted: {
|
|
908
|
+
token: asset,
|
|
909
|
+
amount
|
|
910
|
+
},
|
|
911
|
+
spender: X402_EXACT_PERMIT2_PROXY,
|
|
912
|
+
nonce: nonce.toString(),
|
|
913
|
+
deadline: String(deadline),
|
|
914
|
+
witness: {
|
|
915
|
+
to: payTo,
|
|
916
|
+
validAfter: String(validAfter)
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
return {
|
|
921
|
+
serialized: JSON.stringify(payload),
|
|
922
|
+
signature,
|
|
923
|
+
extensions: approvalExtension
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Read ERC-20 allowance via raw eth_call (no viem dependency needed).
|
|
928
|
+
*/
|
|
929
|
+
async readAllowance(rpcUrl, token, owner, spender) {
|
|
930
|
+
const selector = "0xdd62ed3e";
|
|
931
|
+
const paddedOwner = owner.slice(2).toLowerCase().padStart(64, "0");
|
|
932
|
+
const paddedSpender = spender.slice(2).toLowerCase().padStart(64, "0");
|
|
933
|
+
const data = selector + paddedOwner + paddedSpender;
|
|
934
|
+
try {
|
|
935
|
+
const response = await fetch(rpcUrl, {
|
|
936
|
+
method: "POST",
|
|
937
|
+
headers: { "Content-Type": "application/json" },
|
|
938
|
+
body: JSON.stringify({
|
|
939
|
+
jsonrpc: "2.0",
|
|
940
|
+
id: 1,
|
|
941
|
+
method: "eth_call",
|
|
942
|
+
params: [{ to: token, data }, "latest"]
|
|
943
|
+
})
|
|
944
|
+
});
|
|
945
|
+
const result = await response.json();
|
|
946
|
+
if (result.error || !result.result || result.result === "0x") return 0n;
|
|
947
|
+
return BigInt(result.result);
|
|
948
|
+
} catch {
|
|
949
|
+
return 0n;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Encode ERC-20 approve(address,uint256) calldata.
|
|
954
|
+
*/
|
|
955
|
+
encodeApprove(spender, amount) {
|
|
956
|
+
const selector = "0x095ea7b3";
|
|
957
|
+
const paddedSpender = spender.slice(2).toLowerCase().padStart(64, "0");
|
|
958
|
+
const paddedAmount = amount.toString(16).padStart(64, "0");
|
|
959
|
+
return selector + paddedSpender + paddedAmount;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Wait for a transaction receipt by polling eth_getTransactionReceipt.
|
|
963
|
+
*/
|
|
964
|
+
async waitForReceipt(rpcUrl, txHash, timeoutMs = 3e4) {
|
|
965
|
+
const start = Date.now();
|
|
966
|
+
while (Date.now() - start < timeoutMs) {
|
|
967
|
+
try {
|
|
968
|
+
const response = await fetch(rpcUrl, {
|
|
969
|
+
method: "POST",
|
|
970
|
+
headers: { "Content-Type": "application/json" },
|
|
971
|
+
body: JSON.stringify({
|
|
972
|
+
jsonrpc: "2.0",
|
|
973
|
+
id: 1,
|
|
974
|
+
method: "eth_getTransactionReceipt",
|
|
975
|
+
params: [txHash]
|
|
976
|
+
})
|
|
977
|
+
});
|
|
978
|
+
const result = await response.json();
|
|
979
|
+
if (result.result) {
|
|
980
|
+
if (result.result.status === "0x0") {
|
|
981
|
+
throw new Error(`Approval transaction reverted: ${txHash}`);
|
|
982
|
+
}
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
} catch (err) {
|
|
986
|
+
if (err instanceof Error && err.message.includes("reverted")) throw err;
|
|
987
|
+
}
|
|
988
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
989
|
+
}
|
|
990
|
+
throw new Error(`Approval transaction receipt timeout after ${timeoutMs}ms: ${txHash}`);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Read gas price via eth_gasPrice RPC call.
|
|
994
|
+
*/
|
|
995
|
+
async readGasPrice(rpcUrl) {
|
|
996
|
+
try {
|
|
997
|
+
const response = await fetch(rpcUrl, {
|
|
998
|
+
method: "POST",
|
|
999
|
+
headers: { "Content-Type": "application/json" },
|
|
1000
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_gasPrice", params: [] })
|
|
1001
|
+
});
|
|
1002
|
+
const result = await response.json();
|
|
1003
|
+
return result.result ? BigInt(result.result) : 50000000n;
|
|
1004
|
+
} catch {
|
|
1005
|
+
return 50000000n;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Read transaction count (nonce) via eth_getTransactionCount RPC call.
|
|
1010
|
+
*/
|
|
1011
|
+
async readNonce(rpcUrl, address) {
|
|
1012
|
+
try {
|
|
1013
|
+
const response = await fetch(rpcUrl, {
|
|
1014
|
+
method: "POST",
|
|
1015
|
+
headers: { "Content-Type": "application/json" },
|
|
1016
|
+
body: JSON.stringify({
|
|
1017
|
+
jsonrpc: "2.0",
|
|
1018
|
+
id: 1,
|
|
1019
|
+
method: "eth_getTransactionCount",
|
|
1020
|
+
params: [address, "latest"]
|
|
1021
|
+
})
|
|
1022
|
+
});
|
|
1023
|
+
const result = await response.json();
|
|
1024
|
+
return result.result ? parseInt(result.result, 16) : 0;
|
|
1025
|
+
} catch {
|
|
1026
|
+
return 0;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Calculate how much to approve based on the facilitator's approval strategy.
|
|
1031
|
+
* Buffered approvals reduce the number of on-chain approval txs for micropayments.
|
|
1032
|
+
*/
|
|
1033
|
+
calculateApprovalAmount(paymentAmount, fee, strategy) {
|
|
1034
|
+
const total = BigInt(paymentAmount) + BigInt(fee);
|
|
1035
|
+
if (!strategy || strategy.mode === "exact") {
|
|
1036
|
+
return total;
|
|
1037
|
+
}
|
|
1038
|
+
const multiple = BigInt(strategy.defaultMultiple ?? 10);
|
|
1039
|
+
const buffered = total * multiple;
|
|
1040
|
+
if (strategy.maxCapUsd) {
|
|
1041
|
+
const decimals = this.inferDecimals(paymentAmount);
|
|
1042
|
+
const maxCap = BigInt(Math.floor(strategy.maxCapUsd * Math.pow(10, decimals)));
|
|
1043
|
+
if (buffered > maxCap) return maxCap;
|
|
1044
|
+
}
|
|
1045
|
+
if (strategy.exactAboveUsd) {
|
|
1046
|
+
const decimals = this.inferDecimals(paymentAmount);
|
|
1047
|
+
const threshold = BigInt(Math.floor(strategy.exactAboveUsd * Math.pow(10, decimals)));
|
|
1048
|
+
if (BigInt(paymentAmount) > threshold) return total;
|
|
1049
|
+
}
|
|
1050
|
+
return buffered;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Infer token decimals from payment amount magnitude.
|
|
1054
|
+
* BSC stablecoins use 18 decimals, all others use 6.
|
|
1055
|
+
* A $1 payment is 1000000 (6 dec) or 1000000000000000000 (18 dec).
|
|
1056
|
+
* If the amount has > 12 digits, it's almost certainly 18 decimals.
|
|
1057
|
+
*/
|
|
1058
|
+
inferDecimals(amount) {
|
|
1059
|
+
return amount.length > 12 ? 18 : 6;
|
|
1060
|
+
}
|
|
635
1061
|
};
|
|
636
1062
|
function createEvmAdapter(config) {
|
|
637
1063
|
return new EvmAdapter(config);
|
|
@@ -645,6 +1071,9 @@ function isKnownUSDC(asset) {
|
|
|
645
1071
|
for (const addr of Object.values(USDC_ADDRESSES)) {
|
|
646
1072
|
if (addr.toLowerCase() === lc) return true;
|
|
647
1073
|
}
|
|
1074
|
+
for (const addr of Object.keys(BSC_STABLECOIN_ADDRESSES)) {
|
|
1075
|
+
if (addr.toLowerCase() === lc) return true;
|
|
1076
|
+
}
|
|
648
1077
|
return false;
|
|
649
1078
|
}
|
|
650
1079
|
|
|
@@ -753,6 +1182,19 @@ function createX402Client(config) {
|
|
|
753
1182
|
}
|
|
754
1183
|
return candidates[0];
|
|
755
1184
|
}
|
|
1185
|
+
function getChainDisplayName(network, adapterName) {
|
|
1186
|
+
const names = {
|
|
1187
|
+
"eip155:56": "BSC",
|
|
1188
|
+
"eip155:8453": "Base",
|
|
1189
|
+
"eip155:84532": "Base Sepolia",
|
|
1190
|
+
"eip155:42161": "Arbitrum",
|
|
1191
|
+
"eip155:137": "Polygon",
|
|
1192
|
+
"eip155:10": "Optimism",
|
|
1193
|
+
"eip155:43114": "Avalanche",
|
|
1194
|
+
"eip155:1": "Ethereum"
|
|
1195
|
+
};
|
|
1196
|
+
return names[network] || adapterName;
|
|
1197
|
+
}
|
|
756
1198
|
function getRpcUrl(network, adapter) {
|
|
757
1199
|
return rpcUrls[network] || adapter.getDefaultRpcUrl(network);
|
|
758
1200
|
}
|
|
@@ -845,6 +1287,9 @@ function createX402Client(config) {
|
|
|
845
1287
|
accepted: accept,
|
|
846
1288
|
payload
|
|
847
1289
|
};
|
|
1290
|
+
if (signedTx.extensions) {
|
|
1291
|
+
paymentSignature.extensions = signedTx.extensions;
|
|
1292
|
+
}
|
|
848
1293
|
const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
|
|
849
1294
|
const passResponse = await customFetch(passUrl, {
|
|
850
1295
|
...init,
|
|
@@ -967,10 +1412,10 @@ function createX402Client(config) {
|
|
|
967
1412
|
const balance = await adapter.getBalance(accept, wallet, rpcUrl);
|
|
968
1413
|
const requiredAmount = Number(paymentAmount) / Math.pow(10, decimals);
|
|
969
1414
|
if (balance < requiredAmount) {
|
|
970
|
-
const
|
|
1415
|
+
const chainName = getChainDisplayName(accept.network, adapter.name);
|
|
971
1416
|
throw new X402Error(
|
|
972
1417
|
"insufficient_balance",
|
|
973
|
-
`Insufficient
|
|
1418
|
+
`Insufficient balance on ${chainName}. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
|
|
974
1419
|
);
|
|
975
1420
|
}
|
|
976
1421
|
log(`Balance OK: $${balance.toFixed(4)} >= $${requiredAmount.toFixed(4)}`);
|
|
@@ -1023,6 +1468,9 @@ function createX402Client(config) {
|
|
|
1023
1468
|
accepted: accept,
|
|
1024
1469
|
payload
|
|
1025
1470
|
};
|
|
1471
|
+
if (signedTx.extensions) {
|
|
1472
|
+
paymentSignature.extensions = signedTx.extensions;
|
|
1473
|
+
}
|
|
1026
1474
|
const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
|
|
1027
1475
|
log("Retrying request with payment...");
|
|
1028
1476
|
const retryResponse = await fetchWithRetry(input, {
|
|
@@ -1151,7 +1599,16 @@ async function createEvmKeypairWallet(privateKey) {
|
|
|
1151
1599
|
const account = privateKeyToAccount(normalizedKey);
|
|
1152
1600
|
return {
|
|
1153
1601
|
address: account.address,
|
|
1154
|
-
signTypedData: (params) => account.signTypedData(params)
|
|
1602
|
+
signTypedData: (params) => account.signTypedData(params),
|
|
1603
|
+
signTransaction: (params) => account.signTransaction({
|
|
1604
|
+
to: params.to,
|
|
1605
|
+
data: params.data,
|
|
1606
|
+
chainId: params.chainId,
|
|
1607
|
+
gas: params.gas,
|
|
1608
|
+
gasPrice: params.gasPrice,
|
|
1609
|
+
nonce: params.nonce,
|
|
1610
|
+
type: "legacy"
|
|
1611
|
+
})
|
|
1155
1612
|
};
|
|
1156
1613
|
}
|
|
1157
1614
|
function isEvmKeypairWallet(wallet) {
|
|
@@ -1218,55 +1675,84 @@ function wrapFetch(fetchImpl, options) {
|
|
|
1218
1675
|
}
|
|
1219
1676
|
|
|
1220
1677
|
// src/client/discovery.ts
|
|
1221
|
-
var
|
|
1222
|
-
|
|
1678
|
+
var DEFAULT_CAPABILITY_ENDPOINT = "https://x402.dexter.cash/api/x402gle/capability";
|
|
1679
|
+
function formatPriceLabel(priceUsdc) {
|
|
1680
|
+
if (priceUsdc == null) return "price on request";
|
|
1681
|
+
if (priceUsdc === 0) return "free";
|
|
1682
|
+
if (priceUsdc < 0.01) return `$${priceUsdc.toFixed(4)}`;
|
|
1683
|
+
return `$${priceUsdc.toFixed(2)}`;
|
|
1684
|
+
}
|
|
1685
|
+
function mapResult(r) {
|
|
1686
|
+
return {
|
|
1687
|
+
resourceId: r.resourceId,
|
|
1688
|
+
name: r.displayName ?? r.resourceUrl,
|
|
1689
|
+
url: r.resourceUrl,
|
|
1690
|
+
method: r.method || "GET",
|
|
1691
|
+
price: formatPriceLabel(r.pricing.usdc),
|
|
1692
|
+
priceUsdc: r.pricing.usdc,
|
|
1693
|
+
network: r.pricing.network,
|
|
1694
|
+
description: r.description ?? "",
|
|
1695
|
+
category: r.category ?? "uncategorized",
|
|
1696
|
+
qualityScore: r.verification.qualityScore,
|
|
1697
|
+
verified: r.verification.status === "pass",
|
|
1698
|
+
verificationStatus: r.verification.status,
|
|
1699
|
+
totalCalls: r.usage.totalSettlements,
|
|
1700
|
+
totalVolumeUsdc: r.usage.totalVolumeUsdc,
|
|
1701
|
+
iconUrl: r.icon,
|
|
1702
|
+
host: r.host,
|
|
1703
|
+
gamingFlags: r.gaming.flags,
|
|
1704
|
+
gamingSuspicious: r.gaming.suspicious,
|
|
1705
|
+
tier: r.tier,
|
|
1706
|
+
similarity: Math.round(r.similarity * 1e3) / 1e3,
|
|
1707
|
+
why: r.why,
|
|
1708
|
+
score: r.score
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
async function capabilitySearch(options) {
|
|
1712
|
+
if (!options?.query || !options.query.trim()) {
|
|
1713
|
+
throw new Error("capabilitySearch: query is required");
|
|
1714
|
+
}
|
|
1223
1715
|
const {
|
|
1224
1716
|
query,
|
|
1225
|
-
category,
|
|
1226
|
-
network,
|
|
1227
|
-
maxPrice,
|
|
1228
|
-
verifiedOnly,
|
|
1229
|
-
sort = "marketplace",
|
|
1230
1717
|
limit = 20,
|
|
1231
|
-
|
|
1718
|
+
unverified,
|
|
1719
|
+
testnets,
|
|
1720
|
+
rerank,
|
|
1721
|
+
endpoint = DEFAULT_CAPABILITY_ENDPOINT
|
|
1232
1722
|
} = options;
|
|
1233
1723
|
const params = new URLSearchParams();
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
if (
|
|
1237
|
-
if (
|
|
1238
|
-
if (
|
|
1239
|
-
params.
|
|
1240
|
-
params.set("order", "desc");
|
|
1241
|
-
params.set("limit", String(Math.min(limit, 50)));
|
|
1242
|
-
const url = `${marketplaceUrl}?${params.toString()}`;
|
|
1724
|
+
params.set("q", query);
|
|
1725
|
+
params.set("limit", String(Math.min(Math.max(limit, 1), 50)));
|
|
1726
|
+
if (unverified) params.set("unverified", "true");
|
|
1727
|
+
if (testnets) params.set("testnets", "true");
|
|
1728
|
+
if (rerank === false) params.set("rerank", "false");
|
|
1729
|
+
const url = `${endpoint}?${params.toString()}`;
|
|
1243
1730
|
const response = await fetch(url, {
|
|
1244
|
-
headers: {
|
|
1245
|
-
signal: AbortSignal.timeout(
|
|
1731
|
+
headers: { Accept: "application/json" },
|
|
1732
|
+
signal: AbortSignal.timeout(2e4)
|
|
1246
1733
|
});
|
|
1247
1734
|
if (!response.ok) {
|
|
1248
|
-
|
|
1735
|
+
const body = await response.text().catch(() => "");
|
|
1736
|
+
throw new Error(`Capability search failed: ${response.status} ${body.slice(0, 400)}`);
|
|
1249
1737
|
}
|
|
1250
1738
|
const data = await response.json();
|
|
1251
|
-
if (!data.
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
lastActive: r.lastSettlementAt ?? null
|
|
1269
|
-
}));
|
|
1739
|
+
if (!data.ok) {
|
|
1740
|
+
throw new Error(
|
|
1741
|
+
`Capability search error${data.stage ? ` at stage ${data.stage}` : ""}: ${data.error ?? "unknown"}`
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
return {
|
|
1745
|
+
query: data.query,
|
|
1746
|
+
strongResults: data.strongResults.map(mapResult),
|
|
1747
|
+
relatedResults: data.relatedResults.map(mapResult),
|
|
1748
|
+
strongCount: data.strongCount,
|
|
1749
|
+
relatedCount: data.relatedCount,
|
|
1750
|
+
topSimilarity: data.topSimilarity,
|
|
1751
|
+
noMatchReason: data.noMatchReason,
|
|
1752
|
+
rerank: data.rerank,
|
|
1753
|
+
intent: data.intent,
|
|
1754
|
+
durationMs: data.durationMs
|
|
1755
|
+
};
|
|
1270
1756
|
}
|
|
1271
1757
|
|
|
1272
1758
|
// src/client/budget-account.ts
|
|
@@ -1404,6 +1890,7 @@ async function fireImpressionBeacon(response) {
|
|
|
1404
1890
|
SOLANA_MAINNET,
|
|
1405
1891
|
USDC_MINT,
|
|
1406
1892
|
X402Error,
|
|
1893
|
+
capabilitySearch,
|
|
1407
1894
|
createBudgetAccount,
|
|
1408
1895
|
createEvmAdapter,
|
|
1409
1896
|
createEvmKeypairWallet,
|
|
@@ -1416,6 +1903,5 @@ async function fireImpressionBeacon(response) {
|
|
|
1416
1903
|
getSponsoredRecommendations,
|
|
1417
1904
|
isEvmKeypairWallet,
|
|
1418
1905
|
isKeypairWallet,
|
|
1419
|
-
searchAPIs,
|
|
1420
1906
|
wrapFetch
|
|
1421
1907
|
});
|