diff --git a/e2e/fixtures/base.ts b/e2e/fixtures/base.ts index 5b59b602..2a830ed9 100644 --- a/e2e/fixtures/base.ts +++ b/e2e/fixtures/base.ts @@ -189,6 +189,28 @@ export const BASE = { }, }, + // ============================================ + // x402 FACILITATORS + // ============================================ + facilitators: { + // PayAI Facilitator - multi-network x402 facilitator + payai: { + address: "0xc6699d2aada6c36dfea5c248dd70f9cb0235cb63", + name: "PayAI Facilitator", + description: "Accept x402 payments on all networks", + websiteUrl: "https://facilitator.payai.network", + baseUrl: "https://facilitator.payai.network", + schemes: ["exact"], + assets: ["EIP-3009", "SPL", "Token-2022"], + }, + // Kobaru - x402 facilitator built by payment veterans + kobaru: { + address: "0x67a3176acd5db920747eef65b813b028ad143cdb", + name: "Kobaru", + websiteUrl: "https://www.kobaru.io", + }, + }, + // Upgrade timestamps (Unix) for reference upgrades: { canyon: { diff --git a/e2e/tests/evm-networks/x402-facilitator.spec.ts b/e2e/tests/evm-networks/x402-facilitator.spec.ts new file mode 100644 index 00000000..a94df744 --- /dev/null +++ b/e2e/tests/evm-networks/x402-facilitator.spec.ts @@ -0,0 +1,200 @@ +import { test, expect } from "../../fixtures/test"; +import { AddressPage } from "../../pages/address.page"; +import { BASE } from "../../fixtures/base"; +import { waitForAddressContent, DEFAULT_TIMEOUT } from "../../helpers/wait"; + +const CHAIN_ID = BASE.chainId; + +// ============================================ +// x402 FACILITATOR TESTS +// ============================================ + +test.describe("x402 Facilitator - Address Page", () => { + test("detects PayAI as x402 facilitator type", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.payai; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + // Verify it's identified as x402 Facilitator + const type = await addressPage.getAddressType(); + expect(type.toLowerCase()).toContain("x402"); + } + }); + + test("displays facilitator info card with name and logo", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.payai; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + // Facilitator Info card should be visible + await expect(page.locator(".facilitator-info-card")).toBeVisible(); + + // Name should be displayed + await expect(page.locator(`text=${facilitator.name}`).first()).toBeVisible(); + + // Logo should be present + await expect(page.locator(".facilitator-logo")).toBeVisible(); + } + }); + + test("displays facilitator description", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.payai; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + await expect(page.locator(".facilitator-info-card")).toBeVisible(); + await expect( + page.locator(`text=${facilitator.description}`).first(), + ).toBeVisible(); + } + }); + + test("displays facilitator website link", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.payai; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + const websiteLink = page.locator(".facilitator-link", { + hasText: facilitator.websiteUrl, + }); + await expect(websiteLink).toBeVisible(); + await expect(websiteLink).toHaveAttribute("href", facilitator.websiteUrl); + await expect(websiteLink).toHaveAttribute("target", "_blank"); + } + }); + + test("displays facilitator base URL", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.payai; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + await expect( + page.locator(`text=${facilitator.baseUrl}`).first(), + ).toBeVisible(); + } + }); + + test("displays facilitator schemes and assets", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.payai; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + // Schemes + await expect( + page.locator(`text=${facilitator.schemes.join(", ")}`), + ).toBeVisible(); + + // Assets + await expect( + page.locator(`text=${facilitator.assets.join(", ")}`), + ).toBeVisible(); + } + }); + + test("displays facilitator capability badges", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.payai; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + // PayAI supports verify, settle, supported, and list + const badges = page.locator(".facilitator-capability-badge.supported"); + await expect(badges.first()).toBeVisible(); + expect(await badges.count()).toBeGreaterThanOrEqual(3); + } + }); + + test("displays balance for facilitator address", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.payai; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + await expect(page.locator("text=Balance:")).toBeVisible(); + const balance = await addressPage.getBalance(); + expect(balance).toContain("ETH"); + } + }); + + test("displays transaction history for facilitator", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.payai; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + // Transaction history section should be present + await expect( + page + .locator("text=Transaction History") + .or(page.locator("text=Transactions:")) + .first(), + ).toBeVisible(); + } + }); + + test("detects Kobaru as x402 facilitator type", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.kobaru; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + const type = await addressPage.getAddressType(); + expect(type.toLowerCase()).toContain("x402"); + + // Facilitator Info card should be visible with correct name + await expect(page.locator(".facilitator-info-card")).toBeVisible(); + await expect( + page.locator(`text=${facilitator.name}`).first(), + ).toBeVisible(); + } + }); + + test("displays contract details when facilitator has code", async ({ page }, testInfo) => { + const addressPage = new AddressPage(page); + const facilitator = BASE.facilitators.payai; + + await addressPage.goto(facilitator.address, CHAIN_ID); + + const loaded = await waitForAddressContent(page, testInfo); + if (loaded) { + // If the facilitator has contract code, Contract Details should be shown + const hasContractDetails = await page + .locator("text=Contract Details") + .isVisible({ timeout: DEFAULT_TIMEOUT }); + if (hasContractDetails) { + await expect(page.locator("text=Contract Details")).toBeVisible(); + await expect( + page + .locator("text=Contract Bytecode") + .or(page.locator("text=Bytecode")), + ).toBeVisible(); + } + } + }); +}); diff --git a/src/components/pages/evm/address/displays/X402FacilitatorDisplay.tsx b/src/components/pages/evm/address/displays/X402FacilitatorDisplay.tsx new file mode 100644 index 00000000..6323e232 --- /dev/null +++ b/src/components/pages/evm/address/displays/X402FacilitatorDisplay.tsx @@ -0,0 +1,247 @@ +import type React from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; +import { getNetworkById } from "../../../../../config/networks"; +import { getX402Facilitator } from "../../../../../config/x402Facilitators"; +import { AppContext } from "../../../../../context"; +import { useContractVerification } from "../../../../../hooks/useContractVerification"; +import { useDataService } from "../../../../../hooks/useDataService"; +import { useProxyInfo } from "../../../../../hooks/useProxyInfo"; +import type { KlerosTag } from "../../../../../services/KlerosService"; +import type { Address, ENSReverseResult, RPCMetadata } from "../../../../../types"; +import { hasContractCode } from "../../../../../utils/addressTypeDetection"; +import { formatNativeFromWei } from "../../../../../utils/unitFormatters"; +import type { ProxyInfo, ProxyType } from "../../../../../utils/proxyDetection"; +import AIAnalysisPanel from "../../../../common/AIAnalysis/AIAnalysisPanel"; +import { compactContractDataForAI } from "../../../../common/AIAnalysis/aiContext"; +import { AddressHeader, TransactionHistory } from "../shared"; +import ContractInfoCard from "../shared/ContractInfoCard"; +import ContractInfoCards from "../shared/ContractInfoCards"; +import FacilitatorInfoCard from "../shared/FacilitatorInfoCard"; + +/** Map Sourcify V2 proxyType string to our ProxyType enum. */ +function mapSourcifyProxyType(sourcifyType: string | undefined): ProxyType { + switch (sourcifyType) { + case "EIP1167Proxy": + return "EIP-1167"; + case "ZeppelinOSProxy": + return "Transparent (Legacy)"; + default: + return "EIP-1967 Transparent"; + } +} + +interface X402FacilitatorDisplayProps { + address: Address; + addressHash: string; + networkId: string; + metadata?: RPCMetadata; + selectedProvider?: string | null; + onProviderSelect?: (provider: string) => void; + ensName?: string | null; + reverseResult?: ENSReverseResult | null; + isMainnet?: boolean; + klerosTag?: KlerosTag | null; +} + +const X402FacilitatorDisplay: React.FC = ({ + address, + addressHash, + networkId, + metadata, + selectedProvider, + onProviderSelect, + ensName, + reverseResult, + isMainnet = true, + klerosTag, +}) => { + const { jsonFiles } = useContext(AppContext); + const network = getNetworkById(networkId); + const networkName = network?.name ?? "Unknown Network"; + const networkCurrency = network?.currency ?? "ETH"; + + const facilitator = getX402Facilitator(Number(networkId), addressHash); + const hasCode = hasContractCode(address.code); + + // Fetch verified contract data (Sourcify -> Etherscan fallback) + const { + data: contractVerifiedData, + loading: sourcifyLoading, + isVerified, + source: verificationSource, + } = useContractVerification(Number(networkId), addressHash, true); + + // RPC-based proxy detection + const rpcProxyInfo = useProxyInfo(addressHash, networkId, address.code ?? ""); + + const proxyInfo = useMemo((): ProxyInfo | null => { + const sp = contractVerifiedData?.proxyResolution; + const implAddr = sp?.implementations?.[0]?.address; + if (sp?.isProxy && implAddr) { + return { + type: rpcProxyInfo?.type ?? mapSourcifyProxyType(sp.proxyType), + implementationAddress: implAddr, + }; + } + return rpcProxyInfo; + }, [contractVerifiedData, rpcProxyInfo]); + + const sourcifyImplName = contractVerifiedData?.proxyResolution?.implementations?.[0]?.name; + + const { data: implSourcifyData, isVerified: implIsVerified } = useContractVerification( + Number(networkId), + proxyInfo?.implementationAddress, + !!proxyInfo, + ); + + // Fetch implementation bytecode via RPC + const dataService = useDataService(Number(networkId)); + const [implCode, setImplCode] = useState(undefined); + useEffect(() => { + const implAddr = proxyInfo?.implementationAddress; + if (!implAddr || !dataService?.networkAdapter) { + setImplCode(undefined); + return; + } + dataService.networkAdapter + .getCode(implAddr) + .then((code) => setImplCode(code && code !== "0x" ? code : undefined)) + .catch(() => setImplCode(undefined)); + }, [proxyInfo?.implementationAddress, dataService]); + + // Local artifact data + const localArtifact = jsonFiles[addressHash.toLowerCase()]; + + const parsedLocalData = useMemo(() => { + if (!localArtifact) return null; + return { + name: localArtifact.contractName, + compilerVersion: localArtifact.buildInfo?.solcLongVersion, + evmVersion: localArtifact.buildInfo?.input?.settings?.evmVersion, + abi: localArtifact.abi, + files: localArtifact.sourceCode + ? [ + { + name: localArtifact.sourceName || "Contract.sol", + path: localArtifact.sourceName || "Contract.sol", + content: localArtifact.sourceCode, + }, + ] + : undefined, + metadata: { + language: localArtifact.buildInfo?.input?.language, + compiler: localArtifact.buildInfo + ? { version: localArtifact.buildInfo.solcVersion } + : undefined, + }, + match: "perfect" as const, + creation_match: null, + runtime_match: null, + chainId: networkId, + address: addressHash, + verifiedAt: undefined, + }; + }, [localArtifact, networkId, addressHash]); + + const contractData = useMemo( + () => (isVerified && contractVerifiedData ? contractVerifiedData : parsedLocalData), + [isVerified, contractVerifiedData, parsedLocalData], + ); + + const hasVerifiedContract = isVerified || !!parsedLocalData; + + const aiContractData = useMemo(() => compactContractDataForAI(contractData), [contractData]); + + const aiContext = useMemo( + () => ({ + address: addressHash, + balanceNative: formatNativeFromWei(address.balance, networkCurrency, 6), + txCount: address.txCount, + accountType: "x402Facilitator", + hasCode: true, + ensName: ensName ?? undefined, + isVerified: hasVerifiedContract, + contractName: aiContractData?.name ?? facilitator?.name ?? undefined, + contractData: aiContractData, + }), + [ + addressHash, + address.balance, + address.txCount, + ensName, + hasVerifiedContract, + aiContractData, + networkCurrency, + facilitator?.name, + ], + ); + + return ( +
+
+ + +
+ {/* Overview + More Info Cards */} + + + {/* Facilitator Info Card */} + {facilitator && } + + {/* Transaction History */} + + + {/* Contract Info Card (only if address has contract code) */} + {hasCode && ( + + )} +
+
+ + +
+ ); +}; + +export default X402FacilitatorDisplay; diff --git a/src/components/pages/evm/address/displays/index.ts b/src/components/pages/evm/address/displays/index.ts index 42830a02..d8517e08 100644 --- a/src/components/pages/evm/address/displays/index.ts +++ b/src/components/pages/evm/address/displays/index.ts @@ -3,3 +3,4 @@ export { default as ContractDisplay } from "./ContractDisplay"; export { default as ERC20Display } from "./ERC20Display"; export { default as ERC721Display } from "./ERC721Display"; export { default as ERC1155Display } from "./ERC1155Display"; +export { default as X402FacilitatorDisplay } from "./X402FacilitatorDisplay"; diff --git a/src/components/pages/evm/address/index.tsx b/src/components/pages/evm/address/index.tsx index 15f552c2..28a7df5a 100644 --- a/src/components/pages/evm/address/index.tsx +++ b/src/components/pages/evm/address/index.tsx @@ -19,6 +19,7 @@ import { ERC20Display, ERC721Display, ERC1155Display, + X402FacilitatorDisplay, } from "./displays"; export default function Address() { @@ -302,6 +303,7 @@ export default function Address() { {addressType === "erc20" && } {addressType === "erc721" && } {addressType === "erc1155" && } + {addressType === "x402Facilitator" && } ); } diff --git a/src/components/pages/evm/address/shared/FacilitatorInfoCard.tsx b/src/components/pages/evm/address/shared/FacilitatorInfoCard.tsx new file mode 100644 index 00000000..1df3ff10 --- /dev/null +++ b/src/components/pages/evm/address/shared/FacilitatorInfoCard.tsx @@ -0,0 +1,129 @@ +import type React from "react"; +import { useTranslation } from "react-i18next"; +import type { X402Facilitator } from "../../../../../config/x402Facilitators"; +import FieldLabel from "../../../../common/FieldLabel"; + +interface FacilitatorInfoCardProps { + facilitator: X402Facilitator; +} + +const FacilitatorInfoCard: React.FC = ({ facilitator }) => { + const { t } = useTranslation("address"); + + return ( +
+
{t("facilitatorInfo")}
+ + {/* Name with logo */} +
+ + + + {facilitator.name} { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + {facilitator.name} + + +
+ + {/* Description */} +
+ {t("facilitatorDescription")}: + {facilitator.description} +
+ + {/* Website */} +
+ {t("facilitatorWebsite")}: + + + {facilitator.websiteUrl} ↗ + + +
+ + {/* Base URL */} +
+ + {facilitator.baseUrl} +
+ + {/* Schemes */} +
+ + {facilitator.schemes.join(", ")} +
+ + {/* Assets */} +
+ + {facilitator.assets.join(", ")} +
+ + {/* Capabilities */} +
+ + + + {facilitator.supports.verify && ( + + {t("facilitatorVerify")} + + )} + {facilitator.supports.settle && ( + + {t("facilitatorSettle")} + + )} + {facilitator.supports.supported && ( + + {t("facilitatorSupported")} + + )} + {facilitator.supports.list && ( + {t("facilitatorList")} + )} + + +
+
+ ); +}; + +export default FacilitatorInfoCard; diff --git a/src/components/pages/evm/address/shared/index.ts b/src/components/pages/evm/address/shared/index.ts index 41ef0b9a..6207f071 100644 --- a/src/components/pages/evm/address/shared/index.ts +++ b/src/components/pages/evm/address/shared/index.ts @@ -8,6 +8,7 @@ export { default as ContractInfoCard } from "./ContractInfoCard"; export { default as ContractInfoCards } from "./ContractInfoCards"; export { default as ContractInteraction } from "./ContractInteraction"; export { default as ERC20TokenInfoCard } from "./ERC20TokenInfoCard"; +export { default as FacilitatorInfoCard } from "./FacilitatorInfoCard"; export { default as NFTCollectionInfoCard } from "./NFTCollectionInfoCard"; export { default as ContractMoreInfoCard } from "./ContractMoreInfoCard"; export { default as CustomTokenModal } from "./CustomTokenModal"; diff --git a/src/config/x402Facilitators.ts b/src/config/x402Facilitators.ts new file mode 100644 index 00000000..4ad3a1c2 --- /dev/null +++ b/src/config/x402Facilitators.ts @@ -0,0 +1,261 @@ +/** + * Static registry of known x402 payment facilitators. + * + * Data is keyed by EVM chain ID → lowercased address for O(1) lookup. + * Only networks currently supported by OpenScan are included. + */ + +export interface X402Facilitator { + name: string; + description: string; + logoUrl: string; + websiteUrl: string; + baseUrl: string; + schemes: string[]; + assets: string[]; + supports: { + verify: boolean; + settle: boolean; + supported: boolean; + list: boolean; + }; +} + +/** + * Chain ID → lowercased address → facilitator metadata. + */ +const X402_FACILITATORS: Record> = { + // ── Base (8453) ────────────────────────────────────────────── + 8453: { + "0x0ea9c5a6df69ff9e7236de69478473726c0109dd": { + name: "0xArchive Facilitator", + description: + "First HyperEVM-native x402 facilitator. Fee-free USDC settlement on HyperEVM and Base Mainnet.", + logoUrl: "/logos/0xarchive.png", + websiteUrl: "https://0xarchive.io", + baseUrl: "https://facilitator.0xarchive.io", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0x15e2e2da7539ef1f652aa3c1d6142a535aa3d7ea": { + name: "Bitrefill Facilitator", + description: "Free x402 facilitator for EVM and Solana", + logoUrl: "/logos/bitrefill.png", + websiteUrl: "https://www.bitrefill.com", + baseUrl: "https://api.bitrefill.com/x402", + schemes: ["exact"], + assets: ["EIP-3009", "SPL"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0x3f8d2fb6fea24e70155bc61471936f3c9c30c206": { + name: "fretchen.eu Facilitator", + description: + "Production x402 v2 Facilitator on Optimism and Base with EIP-3009 USDC payment verification and settlement.", + logoUrl: "/logos/fretchen-facilitator.png", + websiteUrl: "https://www.fretchen.eu/x402/", + baseUrl: "https://facilitator.fretchen.eu", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0x021cc47adeca6673def958e324ca38023b80a5be": { + name: "Heurist Facilitator", + description: + "Enterprise-grade x402 facilitator on EVM chains. Supporting both V1 and V2. OFAC-Compliant.", + logoUrl: "/logos/heurist-x402-logo.png", + websiteUrl: "https://facilitator.heurist.ai", + baseUrl: "https://facilitator.heurist.xyz", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0x1fc230ee3c13d0d520d49360a967dbd1555c8326": { + name: "Heurist Facilitator", + description: + "Enterprise-grade x402 facilitator on EVM chains. Supporting both V1 and V2. OFAC-Compliant.", + logoUrl: "/logos/heurist-x402-logo.png", + websiteUrl: "https://facilitator.heurist.ai", + baseUrl: "https://facilitator.heurist.xyz", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0x290d8b8edcafb25042725cb9e78bcac36b8865f8": { + name: "Heurist Facilitator", + description: + "Enterprise-grade x402 facilitator on EVM chains. Supporting both V1 and V2. OFAC-Compliant.", + logoUrl: "/logos/heurist-x402-logo.png", + websiteUrl: "https://facilitator.heurist.ai", + baseUrl: "https://facilitator.heurist.xyz", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0x3f61093f61817b29d9556d3b092e67746af8cdfd": { + name: "Heurist Facilitator", + description: + "Enterprise-grade x402 facilitator on EVM chains. Supporting both V1 and V2. OFAC-Compliant.", + logoUrl: "/logos/heurist-x402-logo.png", + websiteUrl: "https://facilitator.heurist.ai", + baseUrl: "https://facilitator.heurist.xyz", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0x48ab4b0af4ddc2f666a3fcc43666c793889787a3": { + name: "Heurist Facilitator", + description: + "Enterprise-grade x402 facilitator on EVM chains. Supporting both V1 and V2. OFAC-Compliant.", + logoUrl: "/logos/heurist-x402-logo.png", + websiteUrl: "https://facilitator.heurist.ai", + baseUrl: "https://facilitator.heurist.xyz", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0x612d72dc8402bba997c61aa82ce718ea23b2df5d": { + name: "Heurist Facilitator", + description: + "Enterprise-grade x402 facilitator on EVM chains. Supporting both V1 and V2. OFAC-Compliant.", + logoUrl: "/logos/heurist-x402-logo.png", + websiteUrl: "https://facilitator.heurist.ai", + baseUrl: "https://facilitator.heurist.xyz", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0xb578b7db22581507d62bdbeb85e06acd1be09e11": { + name: "Heurist Facilitator", + description: + "Enterprise-grade x402 facilitator on EVM chains. Supporting both V1 and V2. OFAC-Compliant.", + logoUrl: "/logos/heurist-x402-logo.png", + websiteUrl: "https://facilitator.heurist.ai", + baseUrl: "https://facilitator.heurist.xyz", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0xd97c12726dcf994797c981d31cfb243d231189fb": { + name: "Heurist Facilitator", + description: + "Enterprise-grade x402 facilitator on EVM chains. Supporting both V1 and V2. OFAC-Compliant.", + logoUrl: "/logos/heurist-x402-logo.png", + websiteUrl: "https://facilitator.heurist.ai", + baseUrl: "https://facilitator.heurist.xyz", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0x6448d7772cf9dbd6112ae14176ee5e447a040a45": { + name: "KAMIYO Facilitator", + description: + "x402 payment facilitator powering autonomous agent transactions on Base and Solana.", + logoUrl: "/logos/kamiyo.png", + websiteUrl: "https://kamiyo.ai", + baseUrl: "https://x402.kamiyo.ai", + schemes: ["exact"], + assets: ["ERC-20", "SPL"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0x67a3176acd5db920747eef65b813b028ad143cdb": { + name: "Kobaru", + description: + "x402 Facilitator built from scratch by payment veterans who understand what payment systems demand.", + logoUrl: "/logos/kobaru.png", + websiteUrl: "https://www.kobaru.io", + baseUrl: "https://gateway.kobaru.io", + schemes: ["exact"], + assets: ["SPL", "EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: true }, + }, + "0xc6699d2aada6c36dfea5c248dd70f9cb0235cb63": { + name: "PayAI Facilitator", + description: + "Accept x402 payments on all networks including Avalanche, Base, Polygon, Sei, Solana, and more! Get started in less than 1 minute. Supports all tokens. No API Keys required.", + logoUrl: "/logos/payai.svg", + websiteUrl: "https://facilitator.payai.network", + baseUrl: "https://facilitator.payai.network", + schemes: ["exact"], + assets: ["EIP-3009", "SPL", "Token-2022"], + supports: { verify: true, settle: true, supported: true, list: true }, + }, + "0xb2bd29925cbbcea7628279c91945ca5b98bf371b": { + name: "PayAI Facilitator", + description: + "Accept x402 payments on all networks including Avalanche, Base, Polygon, Sei, Solana, and more! Get started in less than 1 minute. Supports all tokens. No API Keys required.", + logoUrl: "/logos/payai.svg", + websiteUrl: "https://facilitator.payai.network", + baseUrl: "https://facilitator.payai.network", + schemes: ["exact"], + assets: ["EIP-3009", "SPL", "Token-2022"], + supports: { verify: true, settle: true, supported: true, list: true }, + }, + }, + + // ── Optimism (10) ──────────────────────────────────────────── + 10: { + "0x3f8d2fb6fea24e70155bc61471936f3c9c30c206": { + name: "fretchen.eu Facilitator", + description: + "Production x402 v2 Facilitator on Optimism and Base with EIP-3009 USDC payment verification and settlement.", + logoUrl: "/logos/fretchen-facilitator.png", + websiteUrl: "https://www.fretchen.eu/x402/", + baseUrl: "https://facilitator.fretchen.eu", + schemes: ["exact"], + assets: ["EIP-3009"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + }, + + // ── Polygon (137) ──────────────────────────────────────────── + 137: { + "0x15e2e2da7539ef1f652aa3c1d6142a535aa3d7ea": { + name: "Bitrefill Facilitator", + description: "Free x402 facilitator for EVM and Solana", + logoUrl: "/logos/bitrefill.png", + websiteUrl: "https://www.bitrefill.com", + baseUrl: "https://api.bitrefill.com/x402", + schemes: ["exact"], + assets: ["EIP-3009", "SPL"], + supports: { verify: true, settle: true, supported: true, list: false }, + }, + "0xc6699d2aada6c36dfea5c248dd70f9cb0235cb63": { + name: "PayAI Facilitator", + description: + "Accept x402 payments on all networks including Avalanche, Base, Polygon, Sei, Solana, and more! Get started in less than 1 minute. Supports all tokens. No API Keys required.", + logoUrl: "/logos/payai.svg", + websiteUrl: "https://facilitator.payai.network", + baseUrl: "https://facilitator.payai.network", + schemes: ["exact"], + assets: ["EIP-3009", "SPL", "Token-2022"], + supports: { verify: true, settle: true, supported: true, list: true }, + }, + "0xb2bd29925cbbcea7628279c91945ca5b98bf371b": { + name: "PayAI Facilitator", + description: + "Accept x402 payments on all networks including Avalanche, Base, Polygon, Sei, Solana, and more! Get started in less than 1 minute. Supports all tokens. No API Keys required.", + logoUrl: "/logos/payai.svg", + websiteUrl: "https://facilitator.payai.network", + baseUrl: "https://facilitator.payai.network", + schemes: ["exact"], + assets: ["EIP-3009", "SPL", "Token-2022"], + supports: { verify: true, settle: true, supported: true, list: true }, + }, + }, +}; + +/** + * Look up a facilitator by chain ID and address. + * Returns the facilitator metadata or undefined if not found. + */ +export function getX402Facilitator(chainId: number, address: string): X402Facilitator | undefined { + return X402_FACILITATORS[chainId]?.[address.toLowerCase()]; +} + +/** + * Check whether an address is a known x402 facilitator on the given chain. + */ +export function isX402Facilitator(chainId: number, address: string): boolean { + return getX402Facilitator(chainId, address) !== undefined; +} diff --git a/src/locales/en/address.json b/src/locales/en/address.json index 0a20d135..d19f2549 100644 --- a/src/locales/en/address.json +++ b/src/locales/en/address.json @@ -203,5 +203,20 @@ "language": "Language", "optimizer": "Optimizer", "optimizerEnabled": "Enabled ({{runs}} runs)", - "optimizerDisabled": "Disabled" + "optimizerDisabled": "Disabled", + "facilitatorInfo": "Facilitator Info", + "facilitatorName": "Name", + "facilitatorDescription": "Description", + "facilitatorWebsite": "Website", + "facilitatorBaseUrl": "Base URL", + "facilitatorSchemes": "Supported Schemes", + "facilitatorAssets": "Supported Assets", + "facilitatorCapabilities": "Capabilities", + "facilitatorVerify": "Verify", + "facilitatorSettle": "Settle", + "facilitatorSupported": "Supported", + "facilitatorList": "List", + "facilitatorYes": "Yes", + "facilitatorNo": "No", + "facilitatorVisitWebsite": "Visit Website" } diff --git a/src/locales/en/tooltips.json b/src/locales/en/tooltips.json index 10be0d52..eab9fe8b 100644 --- a/src/locales/en/tooltips.json +++ b/src/locales/en/tooltips.json @@ -88,7 +88,12 @@ "contractBytecode": "The compiled EVM bytecode deployed at this address.", "sourceCode": "The original Solidity/Vyper source code, verified to match the deployed bytecode.", "rawAbi": "The Application Binary Interface (ABI) defines how to interact with this contract's functions.", - "functions": "Available contract functions that can be called to read data or modify state." + "functions": "Available contract functions that can be called to read data or modify state.", + "facilitatorName": "The registered name of this x402 payment facilitator — a service that verifies and settles HTTP 402 crypto payments on behalf of API providers.", + "facilitatorBaseUrl": "The API endpoint where this facilitator exposes its x402 protocol operations (verify, settle, etc.).", + "facilitatorSchemes": "The payment scheme this facilitator supports. 'exact' means the payer sends the precise amount requested by the server.", + "facilitatorAssets": "The token standards accepted for payment. EIP-3009 = gasless USDC/EURC transfers, SPL = Solana tokens, Token-2022 = Solana token extensions.", + "facilitatorCapabilities": "The x402 protocol operations this facilitator supports: Verify (validate payment), Settle (execute on-chain transfer), Supported (query accepted tokens/networks), List (discover available services)." }, "token": { "tokenStandard": "The token interface standard (e.g. ERC-20, ERC-721, ERC-1155) that defines how this token behaves.", diff --git a/src/locales/es/address.json b/src/locales/es/address.json index 0abd18a9..f5d6e89b 100644 --- a/src/locales/es/address.json +++ b/src/locales/es/address.json @@ -203,5 +203,20 @@ "language": "Lenguaje", "optimizer": "Optimizador", "optimizerEnabled": "Activado ({{runs}} iteraciones)", - "optimizerDisabled": "Desactivado" + "optimizerDisabled": "Desactivado", + "facilitatorInfo": "Información del Facilitador", + "facilitatorName": "Nombre", + "facilitatorDescription": "Descripción", + "facilitatorWebsite": "Sitio web", + "facilitatorBaseUrl": "URL base", + "facilitatorSchemes": "Esquemas compatibles", + "facilitatorAssets": "Activos compatibles", + "facilitatorCapabilities": "Capacidades", + "facilitatorVerify": "Verificar", + "facilitatorSettle": "Liquidar", + "facilitatorSupported": "Compatible", + "facilitatorList": "Listar", + "facilitatorYes": "Sí", + "facilitatorNo": "No", + "facilitatorVisitWebsite": "Visitar sitio web" } diff --git a/src/locales/es/tooltips.json b/src/locales/es/tooltips.json index 68feb29c..fbbe035e 100644 --- a/src/locales/es/tooltips.json +++ b/src/locales/es/tooltips.json @@ -88,7 +88,12 @@ "contractBytecode": "El bytecode EVM compilado desplegado en esta dirección.", "sourceCode": "El código fuente original en Solidity/Vyper, verificado para coincidir con el bytecode desplegado.", "rawAbi": "La Interfaz Binaria de Aplicación (ABI) define cómo interactuar con las funciones de este contrato.", - "functions": "Funciones de contrato disponibles que pueden ser llamadas para leer datos o modificar el estado." + "functions": "Funciones de contrato disponibles que pueden ser llamadas para leer datos o modificar el estado.", + "facilitatorName": "El nombre registrado de este facilitador de pagos x402 — un servicio que verifica y liquida pagos cripto HTTP 402 en nombre de proveedores de API.", + "facilitatorBaseUrl": "El endpoint de API donde este facilitador expone sus operaciones del protocolo x402 (verificar, liquidar, etc.).", + "facilitatorSchemes": "El esquema de pago que soporta este facilitador. 'exact' significa que el pagador envía el monto exacto solicitado por el servidor.", + "facilitatorAssets": "Los estándares de tokens aceptados para el pago. EIP-3009 = transferencias USDC/EURC sin gas, SPL = tokens de Solana, Token-2022 = extensiones de tokens de Solana.", + "facilitatorCapabilities": "Las operaciones del protocolo x402 que este facilitador soporta: Verify (validar pago), Settle (ejecutar transferencia on-chain), Supported (consultar tokens/redes aceptados), List (descubrir servicios disponibles)." }, "token": { "tokenStandard": "El estándar de interfaz del token (ej. ERC-20, ERC-721, ERC-1155) que define cómo se comporta.", diff --git a/src/locales/ja/tooltips.json b/src/locales/ja/tooltips.json index 47efb91f..db806742 100644 --- a/src/locales/ja/tooltips.json +++ b/src/locales/ja/tooltips.json @@ -88,7 +88,12 @@ "contractBytecode": "このアドレスにデプロイされたコンパイル済みEVMバイトコード。", "sourceCode": "デプロイされたバイトコードと一致することが検証された、元のSolidity/Vyperソースコード。", "rawAbi": "アプリケーションバイナリインターフェース(ABI)は、このコントラクトの関数との対話方法を定義します。", - "functions": "データの読み取りまたは状態の変更のために呼び出し可能なコントラクト関数。" + "functions": "データの読み取りまたは状態の変更のために呼び出し可能なコントラクト関数。", + "facilitatorName": "このx402決済ファシリテーターの登録名 — APIプロバイダーに代わってHTTP 402暗号決済を検証・決済するサービスです。", + "facilitatorBaseUrl": "このファシリテーターがx402プロトコル操作(検証、決済など)を公開するAPIエンドポイント。", + "facilitatorSchemes": "このファシリテーターがサポートする決済スキーム。「exact」は支払者がサーバーの要求する正確な金額を送信することを意味します。", + "facilitatorAssets": "決済に受け入れられるトークン規格。EIP-3009 = ガスレスUSDC/EURC転送、SPL = Solanaトークン、Token-2022 = Solanaトークン拡張。", + "facilitatorCapabilities": "このファシリテーターがサポートするx402プロトコル操作:Verify(決済検証)、Settle(オンチェーン転送実行)、Supported(対応トークン/ネットワーク照会)、List(利用可能なサービスの発見)。" }, "token": { "tokenStandard": "トークンのインターフェース標準(ERC-20、ERC-721、ERC-1155など)。トークンの動作を定義します。", diff --git a/src/locales/pt-BR/tooltips.json b/src/locales/pt-BR/tooltips.json index 757fc5b7..dfc399c5 100644 --- a/src/locales/pt-BR/tooltips.json +++ b/src/locales/pt-BR/tooltips.json @@ -88,7 +88,12 @@ "contractBytecode": "O bytecode EVM compilado implantado neste endereço.", "sourceCode": "O código-fonte original em Solidity/Vyper, verificado para corresponder ao bytecode implantado.", "rawAbi": "A Interface Binária de Aplicação (ABI) define como interagir com as funções deste contrato.", - "functions": "Funções de contrato disponíveis que podem ser chamadas para ler dados ou modificar o estado." + "functions": "Funções de contrato disponíveis que podem ser chamadas para ler dados ou modificar o estado.", + "facilitatorName": "O nome registrado deste facilitador de pagamentos x402 — um serviço que verifica e liquida pagamentos cripto HTTP 402 em nome de provedores de API.", + "facilitatorBaseUrl": "O endpoint de API onde este facilitador expõe suas operações do protocolo x402 (verificar, liquidar, etc.).", + "facilitatorSchemes": "O esquema de pagamento que este facilitador suporta. 'exact' significa que o pagador envia o valor exato solicitado pelo servidor.", + "facilitatorAssets": "Os padrões de tokens aceitos para pagamento. EIP-3009 = transferências USDC/EURC sem gas, SPL = tokens Solana, Token-2022 = extensões de tokens Solana.", + "facilitatorCapabilities": "As operações do protocolo x402 que este facilitador suporta: Verify (validar pagamento), Settle (executar transferência on-chain), Supported (consultar tokens/redes aceitos), List (descobrir serviços disponíveis)." }, "token": { "tokenStandard": "O padrão de interface do token (ex: ERC-20, ERC-721, ERC-1155) que define como este token se comporta.", diff --git a/src/locales/zh/tooltips.json b/src/locales/zh/tooltips.json index 7a0deba2..8ceaee51 100644 --- a/src/locales/zh/tooltips.json +++ b/src/locales/zh/tooltips.json @@ -88,7 +88,12 @@ "contractBytecode": "部署在此地址的编译后EVM字节码。", "sourceCode": "经验证与部署字节码匹配的原始Solidity/Vyper源代码。", "rawAbi": "应用程序二进制接口(ABI)定义了如何与此合约的函数进行交互。", - "functions": "可调用的合约函数,用于读取数据或修改状态。" + "functions": "可调用的合约函数,用于读取数据或修改状态。", + "facilitatorName": "此x402支付协调器的注册名称 — 一种代表API提供商验证和结算HTTP 402加密支付的服务。", + "facilitatorBaseUrl": "此协调器公开其x402协议操作(验证、结算等)的API端点。", + "facilitatorSchemes": "此协调器支持的支付方案。'exact'表示付款方发送服务器请求的精确金额。", + "facilitatorAssets": "接受的代币标准。EIP-3009 = 无Gas USDC/EURC转账,SPL = Solana代币,Token-2022 = Solana代币扩展。", + "facilitatorCapabilities": "此协调器支持的x402协议操作:Verify(验证支付)、Settle(执行链上转账)、Supported(查询支持的代币/网络)、List(发现可用服务)。" }, "token": { "tokenStandard": "代币接口标准(如ERC-20、ERC-721、ERC-1155),定义了代币的行为方式。", diff --git a/src/styles/components.css b/src/styles/components.css index 77af954a..706db337 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -7743,6 +7743,65 @@ button.tx-section-header-toggle { font-size: 0.9rem; } +/* ========================================================================== + x402 Facilitator Info Card + ========================================================================== */ + +.facilitator-info-card { + background: var(--color-surface); + border: 1px solid var(--color-primary-alpha-10); + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; +} + +.facilitator-name-display { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.facilitator-logo { + width: 24px; + height: 24px; + border-radius: 4px; + object-fit: cover; +} + +.facilitator-link { + color: var(--color-primary); + text-decoration: none; +} + +.facilitator-link:hover { + text-decoration: underline; +} + +.facilitator-capabilities { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.facilitator-capability-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 4px; + font-size: 0.85em; +} + +.facilitator-capability-badge.supported { + background: rgba(0, 200, 83, 0.1); + color: var(--color-success, #00c853); +} + +.facilitator-capability-badge.not-supported { + background: var(--color-primary-alpha-5); + color: var(--text-tertiary); +} + /* ========================================================================== Contract Details Section (inside Contract Info Card) ========================================================================== */ diff --git a/src/types/index.ts b/src/types/index.ts index 73ecc2a6..b9cc9af9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -285,7 +285,13 @@ export interface Address { /** * Address type classification */ -export type AddressType = "account" | "contract" | "erc20" | "erc721" | "erc1155"; +export type AddressType = + | "account" + | "contract" + | "erc20" + | "erc721" + | "erc1155" + | "x402Facilitator"; export type StorageAt = Record; diff --git a/src/utils/addressTypeDetection.ts b/src/utils/addressTypeDetection.ts index 0ca3ed51..cc664ba7 100644 --- a/src/utils/addressTypeDetection.ts +++ b/src/utils/addressTypeDetection.ts @@ -1,3 +1,4 @@ +import { isX402Facilitator } from "../config/x402Facilitators"; import { fetchToken } from "../services/MetadataService"; import type { Address, AddressType } from "../types"; @@ -275,13 +276,18 @@ export async function fetchAddressWithType( // contracts from being misclassified as EOA when the secondary RPC call fails. const address = preloadedAddress ?? (await fetchAddressData(addressHash, rpcUrl)); - // Step 2: Check for EIP-7702 delegation (EOA with delegated code) + // Step 2: Check x402 facilitator list (synchronous, before any code checks) + if (isX402Facilitator(chainId, addressHash)) { + return { address, addressType: "x402Facilitator" }; + } + + // Step 3: Check for EIP-7702 delegation (EOA with delegated code) if (isEIP7702Delegation(address.code)) { // It's an EOA with EIP-7702 delegation - still treat as account return { address, addressType: "account" }; } - // Step 3: Determine address type based on code + // Step 4: Determine address type based on code if (!hasContractCode(address.code)) { // It's an EOA (no code) return { address, addressType: "account" }; @@ -308,6 +314,8 @@ export function getAddressTypeLabel(type: AddressType): string { return "ERC-721 NFT"; case "erc1155": return "ERC-1155 Multi-Token"; + case "x402Facilitator": + return "Account (EOA)"; default: return "Unknown"; } @@ -328,6 +336,8 @@ export function getAddressTypeIcon(type: AddressType): string { return "🖼️"; case "erc1155": return "📦"; + case "x402Facilitator": + return "👤"; default: return "❓"; }