From 076bbc6383cbd2035a40da00ce6fb2b6f40f0578 Mon Sep 17 00:00:00 2001 From: Augusto Lemble Date: Wed, 4 Mar 2026 12:13:30 -0300 Subject: [PATCH 1/5] feat(address): show Kleros verified tag on address page Add source field support from explorer-metadata PR #11 to display a "Verified by Kleros" tag next to addresses tagged by the Kleros curated list. --- .../evm/address/displays/AccountDisplay.tsx | 3 ++ .../evm/address/displays/ContractDisplay.tsx | 3 ++ .../evm/address/displays/ERC1155Display.tsx | 3 ++ .../evm/address/displays/ERC20Display.tsx | 3 ++ .../evm/address/displays/ERC721Display.tsx | 3 ++ src/components/pages/evm/address/index.tsx | 13 +++++++ .../evm/address/shared/AddressHeader.tsx | 9 +++++ src/locales/en/address.json | 2 ++ src/locales/es/address.json | 2 ++ src/services/MetadataService.ts | 34 +++++++++++++++++++ src/styles/components.css | 11 ++++++ 11 files changed, 86 insertions(+) diff --git a/src/components/pages/evm/address/displays/AccountDisplay.tsx b/src/components/pages/evm/address/displays/AccountDisplay.tsx index 125a97d2..e01925a6 100644 --- a/src/components/pages/evm/address/displays/AccountDisplay.tsx +++ b/src/components/pages/evm/address/displays/AccountDisplay.tsx @@ -18,6 +18,7 @@ interface AccountDisplayProps { ensName?: string | null; reverseResult?: ENSReverseResult | null; isMainnet?: boolean; + isKlerosVerified?: boolean; } const AccountDisplay: React.FC = ({ @@ -30,6 +31,7 @@ const AccountDisplay: React.FC = ({ ensName, reverseResult, isMainnet = true, + isKlerosVerified, }) => { const network = getNetworkById(networkId); const networkName = network?.name ?? "Unknown Network"; @@ -82,6 +84,7 @@ const AccountDisplay: React.FC = ({ metadata={metadata} selectedProvider={selectedProvider} onProviderSelect={onProviderSelect} + isKlerosVerified={isKlerosVerified} />
diff --git a/src/components/pages/evm/address/displays/ContractDisplay.tsx b/src/components/pages/evm/address/displays/ContractDisplay.tsx index 950cef44..f87ca2af 100644 --- a/src/components/pages/evm/address/displays/ContractDisplay.tsx +++ b/src/components/pages/evm/address/displays/ContractDisplay.tsx @@ -23,6 +23,7 @@ interface ContractDisplayProps { ensName?: string | null; reverseResult?: ENSReverseResult | null; isMainnet?: boolean; + isKlerosVerified?: boolean; } const ContractDisplay: React.FC = ({ @@ -35,6 +36,7 @@ const ContractDisplay: React.FC = ({ ensName, reverseResult, isMainnet = true, + isKlerosVerified, }) => { const { jsonFiles } = useContext(AppContext); const network = getNetworkById(networkId); @@ -126,6 +128,7 @@ const ContractDisplay: React.FC = ({ metadata={metadata} selectedProvider={selectedProvider} onProviderSelect={onProviderSelect} + isKlerosVerified={isKlerosVerified} />
diff --git a/src/components/pages/evm/address/displays/ERC1155Display.tsx b/src/components/pages/evm/address/displays/ERC1155Display.tsx index d370b8f5..258d673b 100644 --- a/src/components/pages/evm/address/displays/ERC1155Display.tsx +++ b/src/components/pages/evm/address/displays/ERC1155Display.tsx @@ -29,6 +29,7 @@ interface ERC1155DisplayProps { ensName?: string | null; reverseResult?: ENSReverseResult | null; isMainnet?: boolean; + isKlerosVerified?: boolean; } const ERC1155Display: React.FC = ({ @@ -41,6 +42,7 @@ const ERC1155Display: React.FC = ({ ensName, reverseResult, isMainnet = true, + isKlerosVerified, }) => { const { jsonFiles, rpcUrls } = useContext(AppContext); const [tokenMetadata, setTokenMetadata] = useState(null); @@ -234,6 +236,7 @@ const ERC1155Display: React.FC = ({ onProviderSelect={onProviderSelect} tokenSymbol={collectionSymbol} tokenName={collectionName} + isKlerosVerified={isKlerosVerified} />
diff --git a/src/components/pages/evm/address/displays/ERC20Display.tsx b/src/components/pages/evm/address/displays/ERC20Display.tsx index eb64cf6f..ddd914c8 100644 --- a/src/components/pages/evm/address/displays/ERC20Display.tsx +++ b/src/components/pages/evm/address/displays/ERC20Display.tsx @@ -29,6 +29,7 @@ interface ERC20DisplayProps { ensName?: string | null; reverseResult?: ENSReverseResult | null; isMainnet?: boolean; + isKlerosVerified?: boolean; } const ERC20Display: React.FC = ({ @@ -41,6 +42,7 @@ const ERC20Display: React.FC = ({ ensName, reverseResult, isMainnet = true, + isKlerosVerified, }) => { const { jsonFiles, rpcUrls } = useContext(AppContext); const [tokenMetadata, setTokenMetadata] = useState(null); @@ -247,6 +249,7 @@ const ERC20Display: React.FC = ({ onProviderSelect={onProviderSelect} tokenSymbol={tokenSymbol} tokenName={tokenName} + isKlerosVerified={isKlerosVerified} />
diff --git a/src/components/pages/evm/address/displays/ERC721Display.tsx b/src/components/pages/evm/address/displays/ERC721Display.tsx index 10905af5..cc8b4754 100644 --- a/src/components/pages/evm/address/displays/ERC721Display.tsx +++ b/src/components/pages/evm/address/displays/ERC721Display.tsx @@ -29,6 +29,7 @@ interface ERC721DisplayProps { ensName?: string | null; reverseResult?: ENSReverseResult | null; isMainnet?: boolean; + isKlerosVerified?: boolean; } const ERC721Display: React.FC = ({ @@ -41,6 +42,7 @@ const ERC721Display: React.FC = ({ ensName, reverseResult, isMainnet = true, + isKlerosVerified, }) => { const { jsonFiles, rpcUrls } = useContext(AppContext); const [tokenMetadata, setTokenMetadata] = useState(null); @@ -216,6 +218,7 @@ const ERC721Display: React.FC = ({ onProviderSelect={onProviderSelect} tokenSymbol={collectionSymbol} tokenName={collectionName} + isKlerosVerified={isKlerosVerified} />
diff --git a/src/components/pages/evm/address/index.tsx b/src/components/pages/evm/address/index.tsx index 90c637f4..1347503c 100644 --- a/src/components/pages/evm/address/index.tsx +++ b/src/components/pages/evm/address/index.tsx @@ -7,6 +7,7 @@ import { useENS } from "../../../../hooks/useENS"; import { useProviderSelection } from "../../../../hooks/useProviderSelection"; import { ENSService } from "../../../../services/ENS/ENSService"; import type { Address as AddressData, AddressType, DataWithMetadata } from "../../../../types"; +import { fetchAddress } from "../../../../services/MetadataService"; import { fetchAddressWithType } from "../../../../utils/addressTypeDetection"; import Loader from "../../../common/Loader"; import { @@ -60,6 +61,8 @@ export default function Address() { `address_${numericNetworkId}_${address}`, ); + const [isKlerosVerified, setIsKlerosVerified] = useState(false); + // Resolve ENS name to address useEffect(() => { if (!isEnsName || !addressParam) { @@ -109,6 +112,15 @@ export default function Address() { isMainnet, } = useENS(address ?? undefined, numericNetworkId, initialEnsName); + // Fetch address metadata to detect Kleros verification + useEffect(() => { + if (!address) return; + setIsKlerosVerified(false); + fetchAddress(numericNetworkId, address).then((meta) => { + setIsKlerosVerified(meta?.source?.[0] === "kleros"); + }); + }, [address, numericNetworkId]); + // Fetch address data and detect type in a single flow useEffect(() => { if (!address || !dataService) { @@ -250,6 +262,7 @@ export default function Address() { metadata: addressDataResult?.metadata, selectedProvider, onProviderSelect: setSelectedProvider, + isKlerosVerified, }; // Render appropriate display component based on detected type diff --git a/src/components/pages/evm/address/shared/AddressHeader.tsx b/src/components/pages/evm/address/shared/AddressHeader.tsx index b7e82539..db56b8f7 100644 --- a/src/components/pages/evm/address/shared/AddressHeader.tsx +++ b/src/components/pages/evm/address/shared/AddressHeader.tsx @@ -1,4 +1,5 @@ import type React from "react"; +import { useTranslation } from "react-i18next"; import type { AddressType, RPCMetadata } from "../../../../../types"; import { getAddressTypeIcon, getAddressTypeLabel } from "../../../../../utils/addressTypeDetection"; import { RPCIndicator } from "../../../../common/RPCIndicator"; @@ -12,6 +13,7 @@ interface AddressHeaderProps { onProviderSelect?: (provider: string) => void; tokenSymbol?: string; tokenName?: string; + isKlerosVerified?: boolean; } // Truncate hash to show first and last N characters @@ -31,7 +33,9 @@ const AddressHeader: React.FC = ({ onProviderSelect, tokenSymbol, tokenName, + isKlerosVerified, }) => { + const { t } = useTranslation("address"); const truncatedHash = truncateHash(addressHash, 4); return ( @@ -41,6 +45,11 @@ const AddressHeader: React.FC = ({ {getAddressTypeIcon(addressType)} {getAddressTypeLabel(addressType)} {tokenSymbol && {tokenSymbol}} + {isKlerosVerified && ( + + {t("klerosVerified")} + + )}
{(ensName || tokenName) && {ensName || tokenName}} {addressHash} diff --git a/src/locales/en/address.json b/src/locales/en/address.json index de30b4f4..dcb1cbd3 100644 --- a/src/locales/en/address.json +++ b/src/locales/en/address.json @@ -1,4 +1,6 @@ { + "klerosVerified": "Verified by Kleros", + "klerosVerifiedTooltip": "This address has been tagged by Kleros curated list", "balance": "Balance", "contractName": "Contract Name", "compiler": "Compiler", diff --git a/src/locales/es/address.json b/src/locales/es/address.json index c2f5295a..ae261f23 100644 --- a/src/locales/es/address.json +++ b/src/locales/es/address.json @@ -1,4 +1,6 @@ { + "klerosVerified": "Verificado por Kleros", + "klerosVerifiedTooltip": "Esta dirección ha sido etiquetada por la lista curada de Kleros", "balance": "Balance", "contractName": "Nombre del contrato", "compiler": "Compilador", diff --git a/src/services/MetadataService.ts b/src/services/MetadataService.ts index badb81ce..79fa55dd 100644 --- a/src/services/MetadataService.ts +++ b/src/services/MetadataService.ts @@ -662,6 +662,40 @@ export function getAssetUrl(assetPath: string): string { return `${METADATA_BASE_URL}/${assetPath}`; } +/** + * Address metadata from addresses/evm/{chainId}/{address}.json + */ +export interface AddressMetadata { + address: string; + chainId: number; + supporter: string; + label?: string; + source?: [string, string]; +} + +/** + * Fetch address metadata from addresses/evm/{chainId}/{address}.json + */ +export async function fetchAddress( + chainId: number, + address: string, +): Promise { + try { + const response = await fetch( + `${METADATA_BASE_URL}/addresses/evm/${chainId}/${address.toLowerCase()}.json`, + ); + + if (!response.ok) { + return null; + } + + return await response.json(); + } catch (error) { + logger.error("Error fetching address metadata:", error); + return null; + } +} + /** * Fetch token metadata from tokens/{chainId}/{address}.json */ diff --git a/src/styles/components.css b/src/styles/components.css index e8d2f17f..98c9d0ee 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -4563,6 +4563,17 @@ button.tx-section-header-toggle { border-radius: 4px; } +.kleros-verified-tag { + font-size: 0.75rem; + font-weight: 600; + color: #6b3fa0; + padding: 2px 8px; + background: rgba(107, 63, 160, 0.1); + border: 1px solid rgba(107, 63, 160, 0.3); + border-radius: 4px; + cursor: default; +} + /* Token Info Row */ .token-info-row { display: flex; From 756840279f2f5bcc8fff61673cb410f9ff165297 Mon Sep 17 00:00:00 2001 From: Augusto Lemble Date: Wed, 4 Mar 2026 16:39:38 -0300 Subject: [PATCH 2/5] feat(address): live Kleros tag via The Graph ATQ subgraph - Add KlerosService with GraphQL query against Kleros ATQ subgraph - Add useKlerosTag hook with fallback to metadata CDN - Show Public Name Tag as clickable link to Kleros Curate item - Add Kleros logo to tag badge - Migrate REACT_APP_ENVIRONMENT to VITE_ENVIRONMENT - Inject VITE_THE_GRAPH_API_KEY in CI workflows --- .github/workflows/deploy-gh-pages.yml | 2 + .github/workflows/deploy-pr-preview.yml | 2 + public/kleros-logo.png | Bin 0 -> 2537 bytes .../evm/address/displays/AccountDisplay.tsx | 7 +- .../evm/address/displays/ContractDisplay.tsx | 7 +- .../evm/address/displays/ERC1155Display.tsx | 7 +- .../evm/address/displays/ERC20Display.tsx | 7 +- .../evm/address/displays/ERC721Display.tsx | 7 +- src/components/pages/evm/address/index.tsx | 15 +--- .../evm/address/shared/AddressHeader.tsx | 24 ++++-- src/components/pages/home/index.tsx | 2 +- src/config/networks.ts | 4 +- src/config/subdomains.ts | 2 +- src/context/AppContext.tsx | 2 +- src/hooks/useKlerosTag.ts | 40 +++++++++ src/locales/en/address.json | 3 +- src/locales/es/address.json | 3 +- src/services/KlerosService.ts | 79 ++++++++++++++++++ src/styles/components.css | 15 +++- src/utils/constants.ts | 2 +- vite.config.ts | 2 +- 21 files changed, 187 insertions(+), 45 deletions(-) create mode 100644 public/kleros-logo.png create mode 100644 src/hooks/useKlerosTag.ts create mode 100644 src/services/KlerosService.ts diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml index a543e6d6..52e43eee 100644 --- a/.github/workflows/deploy-gh-pages.yml +++ b/.github/workflows/deploy-gh-pages.yml @@ -31,6 +31,8 @@ jobs: - name: Build Staging run: GITHUB_PAGES=true ./scripts/build-staging.sh + env: + VITE_THE_GRAPH_API_KEY: ${{ secrets.VITE_THE_GRAPH_API_KEY }} - name: Setup Pages uses: actions/configure-pages@v4 diff --git a/.github/workflows/deploy-pr-preview.yml b/.github/workflows/deploy-pr-preview.yml index 84504055..a4fab124 100644 --- a/.github/workflows/deploy-pr-preview.yml +++ b/.github/workflows/deploy-pr-preview.yml @@ -27,6 +27,8 @@ jobs: - name: Build Staging run: ./scripts/build-staging.sh + env: + VITE_THE_GRAPH_API_KEY: ${{ secrets.VITE_THE_GRAPH_API_KEY }} - name: Deploy to Netlify id: netlify diff --git a/public/kleros-logo.png b/public/kleros-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..edf80a4e1c62c41e90235fd97ba5fa532888694d GIT binary patch literal 2537 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 literal 0 HcmV?d00001 diff --git a/src/components/pages/evm/address/displays/AccountDisplay.tsx b/src/components/pages/evm/address/displays/AccountDisplay.tsx index e01925a6..55f8b11d 100644 --- a/src/components/pages/evm/address/displays/AccountDisplay.tsx +++ b/src/components/pages/evm/address/displays/AccountDisplay.tsx @@ -1,6 +1,7 @@ import type React from "react"; import { useCallback, useMemo, useState } from "react"; import { getNetworkById } from "../../../../../config/networks"; +import type { KlerosTag } from "../../../../../services/KlerosService"; import type { Address, ENSReverseResult, RPCMetadata, Transaction } from "../../../../../types"; import AIAnalysisPanel from "../../../../common/AIAnalysis/AIAnalysisPanel"; import { AddressHeader, TransactionHistory } from "../shared"; @@ -18,7 +19,7 @@ interface AccountDisplayProps { ensName?: string | null; reverseResult?: ENSReverseResult | null; isMainnet?: boolean; - isKlerosVerified?: boolean; + klerosTag?: KlerosTag | null; } const AccountDisplay: React.FC = ({ @@ -31,7 +32,7 @@ const AccountDisplay: React.FC = ({ ensName, reverseResult, isMainnet = true, - isKlerosVerified, + klerosTag, }) => { const network = getNetworkById(networkId); const networkName = network?.name ?? "Unknown Network"; @@ -84,7 +85,7 @@ const AccountDisplay: React.FC = ({ metadata={metadata} selectedProvider={selectedProvider} onProviderSelect={onProviderSelect} - isKlerosVerified={isKlerosVerified} + klerosTag={klerosTag} />
diff --git a/src/components/pages/evm/address/displays/ContractDisplay.tsx b/src/components/pages/evm/address/displays/ContractDisplay.tsx index f87ca2af..e46c61b3 100644 --- a/src/components/pages/evm/address/displays/ContractDisplay.tsx +++ b/src/components/pages/evm/address/displays/ContractDisplay.tsx @@ -3,6 +3,7 @@ import { useContext, useMemo } from "react"; import { getNetworkById } from "../../../../../config/networks"; import { AppContext } from "../../../../../context"; import { useSourcify } from "../../../../../hooks/useSourcify"; +import type { KlerosTag } from "../../../../../services/KlerosService"; import type { Address, ENSReverseResult, RPCMetadata } from "../../../../../types"; import AIAnalysisPanel from "../../../../common/AIAnalysis/AIAnalysisPanel"; import { AddressHeader } from "../shared"; @@ -23,7 +24,7 @@ interface ContractDisplayProps { ensName?: string | null; reverseResult?: ENSReverseResult | null; isMainnet?: boolean; - isKlerosVerified?: boolean; + klerosTag?: KlerosTag | null; } const ContractDisplay: React.FC = ({ @@ -36,7 +37,7 @@ const ContractDisplay: React.FC = ({ ensName, reverseResult, isMainnet = true, - isKlerosVerified, + klerosTag, }) => { const { jsonFiles } = useContext(AppContext); const network = getNetworkById(networkId); @@ -128,7 +129,7 @@ const ContractDisplay: React.FC = ({ metadata={metadata} selectedProvider={selectedProvider} onProviderSelect={onProviderSelect} - isKlerosVerified={isKlerosVerified} + klerosTag={klerosTag} />
diff --git a/src/components/pages/evm/address/displays/ERC1155Display.tsx b/src/components/pages/evm/address/displays/ERC1155Display.tsx index 258d673b..ec5375d7 100644 --- a/src/components/pages/evm/address/displays/ERC1155Display.tsx +++ b/src/components/pages/evm/address/displays/ERC1155Display.tsx @@ -8,6 +8,7 @@ import { getAssetUrl, type TokenMetadata, } from "../../../../../services/MetadataService"; +import type { KlerosTag } from "../../../../../services/KlerosService"; import type { Address, ENSReverseResult, RPCMetadata } from "../../../../../types"; import { decodeAbiString } from "../../../../../utils/hexUtils"; import { logger } from "../../../../../utils/logger"; @@ -29,7 +30,7 @@ interface ERC1155DisplayProps { ensName?: string | null; reverseResult?: ENSReverseResult | null; isMainnet?: boolean; - isKlerosVerified?: boolean; + klerosTag?: KlerosTag | null; } const ERC1155Display: React.FC = ({ @@ -42,7 +43,7 @@ const ERC1155Display: React.FC = ({ ensName, reverseResult, isMainnet = true, - isKlerosVerified, + klerosTag, }) => { const { jsonFiles, rpcUrls } = useContext(AppContext); const [tokenMetadata, setTokenMetadata] = useState(null); @@ -236,7 +237,7 @@ const ERC1155Display: React.FC = ({ onProviderSelect={onProviderSelect} tokenSymbol={collectionSymbol} tokenName={collectionName} - isKlerosVerified={isKlerosVerified} + klerosTag={klerosTag} />
diff --git a/src/components/pages/evm/address/displays/ERC20Display.tsx b/src/components/pages/evm/address/displays/ERC20Display.tsx index ddd914c8..e1d093a9 100644 --- a/src/components/pages/evm/address/displays/ERC20Display.tsx +++ b/src/components/pages/evm/address/displays/ERC20Display.tsx @@ -8,6 +8,7 @@ import { getAssetUrl, type TokenMetadata, } from "../../../../../services/MetadataService"; +import type { KlerosTag } from "../../../../../services/KlerosService"; import type { Address, ENSReverseResult, RPCMetadata } from "../../../../../types"; import { hexToUtf8 } from "../../../../../utils/erc20Utils"; import { logger } from "../../../../../utils/logger"; @@ -29,7 +30,7 @@ interface ERC20DisplayProps { ensName?: string | null; reverseResult?: ENSReverseResult | null; isMainnet?: boolean; - isKlerosVerified?: boolean; + klerosTag?: KlerosTag | null; } const ERC20Display: React.FC = ({ @@ -42,7 +43,7 @@ const ERC20Display: React.FC = ({ ensName, reverseResult, isMainnet = true, - isKlerosVerified, + klerosTag, }) => { const { jsonFiles, rpcUrls } = useContext(AppContext); const [tokenMetadata, setTokenMetadata] = useState(null); @@ -249,7 +250,7 @@ const ERC20Display: React.FC = ({ onProviderSelect={onProviderSelect} tokenSymbol={tokenSymbol} tokenName={tokenName} - isKlerosVerified={isKlerosVerified} + klerosTag={klerosTag} />
diff --git a/src/components/pages/evm/address/displays/ERC721Display.tsx b/src/components/pages/evm/address/displays/ERC721Display.tsx index cc8b4754..e9387815 100644 --- a/src/components/pages/evm/address/displays/ERC721Display.tsx +++ b/src/components/pages/evm/address/displays/ERC721Display.tsx @@ -8,6 +8,7 @@ import { getAssetUrl, type TokenMetadata, } from "../../../../../services/MetadataService"; +import type { KlerosTag } from "../../../../../services/KlerosService"; import type { Address, ENSReverseResult, RPCMetadata } from "../../../../../types"; import { decodeAbiString } from "../../../../../utils/hexUtils"; import { logger } from "../../../../../utils/logger"; @@ -29,7 +30,7 @@ interface ERC721DisplayProps { ensName?: string | null; reverseResult?: ENSReverseResult | null; isMainnet?: boolean; - isKlerosVerified?: boolean; + klerosTag?: KlerosTag | null; } const ERC721Display: React.FC = ({ @@ -42,7 +43,7 @@ const ERC721Display: React.FC = ({ ensName, reverseResult, isMainnet = true, - isKlerosVerified, + klerosTag, }) => { const { jsonFiles, rpcUrls } = useContext(AppContext); const [tokenMetadata, setTokenMetadata] = useState(null); @@ -218,7 +219,7 @@ const ERC721Display: React.FC = ({ onProviderSelect={onProviderSelect} tokenSymbol={collectionSymbol} tokenName={collectionName} - isKlerosVerified={isKlerosVerified} + klerosTag={klerosTag} />
diff --git a/src/components/pages/evm/address/index.tsx b/src/components/pages/evm/address/index.tsx index 1347503c..2fa94e57 100644 --- a/src/components/pages/evm/address/index.tsx +++ b/src/components/pages/evm/address/index.tsx @@ -4,10 +4,10 @@ import { useLocation, useParams } from "react-router-dom"; import { AppContext } from "../../../../context"; import { useDataService } from "../../../../hooks/useDataService"; import { useENS } from "../../../../hooks/useENS"; +import { useKlerosTag } from "../../../../hooks/useKlerosTag"; import { useProviderSelection } from "../../../../hooks/useProviderSelection"; import { ENSService } from "../../../../services/ENS/ENSService"; import type { Address as AddressData, AddressType, DataWithMetadata } from "../../../../types"; -import { fetchAddress } from "../../../../services/MetadataService"; import { fetchAddressWithType } from "../../../../utils/addressTypeDetection"; import Loader from "../../../common/Loader"; import { @@ -61,7 +61,7 @@ export default function Address() { `address_${numericNetworkId}_${address}`, ); - const [isKlerosVerified, setIsKlerosVerified] = useState(false); + const klerosTag = useKlerosTag(address, numericNetworkId); // Resolve ENS name to address useEffect(() => { @@ -112,15 +112,6 @@ export default function Address() { isMainnet, } = useENS(address ?? undefined, numericNetworkId, initialEnsName); - // Fetch address metadata to detect Kleros verification - useEffect(() => { - if (!address) return; - setIsKlerosVerified(false); - fetchAddress(numericNetworkId, address).then((meta) => { - setIsKlerosVerified(meta?.source?.[0] === "kleros"); - }); - }, [address, numericNetworkId]); - // Fetch address data and detect type in a single flow useEffect(() => { if (!address || !dataService) { @@ -262,7 +253,7 @@ export default function Address() { metadata: addressDataResult?.metadata, selectedProvider, onProviderSelect: setSelectedProvider, - isKlerosVerified, + klerosTag, }; // Render appropriate display component based on detected type diff --git a/src/components/pages/evm/address/shared/AddressHeader.tsx b/src/components/pages/evm/address/shared/AddressHeader.tsx index db56b8f7..34fc60b5 100644 --- a/src/components/pages/evm/address/shared/AddressHeader.tsx +++ b/src/components/pages/evm/address/shared/AddressHeader.tsx @@ -1,5 +1,6 @@ import type React from "react"; import { useTranslation } from "react-i18next"; +import { getKlerosCurateItemUrl, type KlerosTag } from "../../../../../services/KlerosService"; import type { AddressType, RPCMetadata } from "../../../../../types"; import { getAddressTypeIcon, getAddressTypeLabel } from "../../../../../utils/addressTypeDetection"; import { RPCIndicator } from "../../../../common/RPCIndicator"; @@ -13,7 +14,7 @@ interface AddressHeaderProps { onProviderSelect?: (provider: string) => void; tokenSymbol?: string; tokenName?: string; - isKlerosVerified?: boolean; + klerosTag?: KlerosTag | null; } // Truncate hash to show first and last N characters @@ -33,7 +34,7 @@ const AddressHeader: React.FC = ({ onProviderSelect, tokenSymbol, tokenName, - isKlerosVerified, + klerosTag, }) => { const { t } = useTranslation("address"); const truncatedHash = truncateHash(addressHash, 4); @@ -45,10 +46,21 @@ const AddressHeader: React.FC = ({ {getAddressTypeIcon(addressType)} {getAddressTypeLabel(addressType)} {tokenSymbol && {tokenSymbol}} - {isKlerosVerified && ( - - {t("klerosVerified")} - + {klerosTag && ( + + Kleros + {klerosTag.publicNameTag} ↗ + )}
{(ensName || tokenName) && {ensName || tokenName}} diff --git a/src/components/pages/home/index.tsx b/src/components/pages/home/index.tsx index a2588392..10437e44 100644 --- a/src/components/pages/home/index.tsx +++ b/src/components/pages/home/index.tsx @@ -51,7 +51,7 @@ export default function Home() { const [showTestnets, setShowTestnets] = useState(false); const { productionNetworks, testnetNetworks } = useMemo(() => { - const isDevelopment = process.env.REACT_APP_ENVIRONMENT === "development"; + const isDevelopment = import.meta.env.VITE_ENVIRONMENT === "development"; const localhostChainId = 31337; // In development, treat localhost as a production network (show with other networks) diff --git a/src/config/networks.ts b/src/config/networks.ts index eacaa5ad..b4853235 100644 --- a/src/config/networks.ts +++ b/src/config/networks.ts @@ -100,8 +100,8 @@ export function getEnabledNetworks(): NetworkConfig[] { const envNetworks = process.env.REACT_APP_OPENSCAN_NETWORKS; const localhostChainId = 31337; - // REACT_APP_ENVIRONMENT is set by webpack DefinePlugin based on NODE_ENV - const isDevelopment = process.env.REACT_APP_ENVIRONMENT === "development"; + // VITE_ENVIRONMENT is injected via vite.config.ts define block based on NODE_ENV + const isDevelopment = import.meta.env.VITE_ENVIRONMENT === "development"; // Check if localhost is explicitly enabled in REACT_APP_OPENSCAN_NETWORKS const isLocalhostExplicitlyEnabled = envNetworks diff --git a/src/config/subdomains.ts b/src/config/subdomains.ts index 74874640..2bd520c4 100644 --- a/src/config/subdomains.ts +++ b/src/config/subdomains.ts @@ -14,7 +14,7 @@ export interface SubdomainConfig { const WEENUS_SEPOLIA_ADDRESS = "0x7E0987E5b3a30e3f2828572Bb659A548460a3003"; // Check if we're in development mode -const isDevelopment = process.env.REACT_APP_ENVIRONMENT === "development"; +const isDevelopment = import.meta.env.VITE_ENVIRONMENT === "development"; export const subdomainConfig: SubdomainConfig[] = [ // Network subdomains diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx index a8e36006..c1f1fab3 100644 --- a/src/context/AppContext.tsx +++ b/src/context/AppContext.tsx @@ -111,7 +111,7 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { // Check if Hardhat should be included (only when both conditions are met) const envNetworks = process.env.REACT_APP_OPENSCAN_NETWORKS; - const isDevelopment = process.env.REACT_APP_ENVIRONMENT === "development"; + const isDevelopment = import.meta.env.VITE_ENVIRONMENT === "development"; const hardhatInEnv = envNetworks?.split(",").some((id) => id.trim() === "31337"); // Add Hardhat network if in development AND explicitly enabled diff --git a/src/hooks/useKlerosTag.ts b/src/hooks/useKlerosTag.ts new file mode 100644 index 00000000..e13ed34b --- /dev/null +++ b/src/hooks/useKlerosTag.ts @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; +import { fetchAddress } from "../services/MetadataService"; +import { type KlerosTag, fetchKlerosTag, isKlerosGraphEnabled } from "../services/KlerosService"; + +export function useKlerosTag( + address: string | null | undefined, + chainId: number, +): KlerosTag | null { + const [tag, setTag] = useState(null); + + useEffect(() => { + if (!address || chainId !== 1) { + setTag(null); + return; + } + + setTag(null); + + if (isKlerosGraphEnabled()) { + fetchKlerosTag(address) + .then(setTag) + .catch(() => { + // Fallback to metadata service on Graph error + fetchAddress(chainId, address).then((meta) => { + if (meta?.source?.[0] === "kleros" && meta.label) { + setTag({ itemID: "", publicNameTag: meta.label }); + } + }); + }); + } else { + fetchAddress(chainId, address).then((meta) => { + if (meta?.source?.[0] === "kleros" && meta.label) { + setTag({ itemID: "", publicNameTag: meta.label }); + } + }); + } + }, [address, chainId]); + + return tag; +} diff --git a/src/locales/en/address.json b/src/locales/en/address.json index dcb1cbd3..30b956da 100644 --- a/src/locales/en/address.json +++ b/src/locales/en/address.json @@ -1,6 +1,5 @@ { - "klerosVerified": "Verified by Kleros", - "klerosVerifiedTooltip": "This address has been tagged by Kleros curated list", + "klerosVerifiedTooltip": "Verified by Kleros curated registry", "balance": "Balance", "contractName": "Contract Name", "compiler": "Compiler", diff --git a/src/locales/es/address.json b/src/locales/es/address.json index ae261f23..d7870d38 100644 --- a/src/locales/es/address.json +++ b/src/locales/es/address.json @@ -1,6 +1,5 @@ { - "klerosVerified": "Verificado por Kleros", - "klerosVerifiedTooltip": "Esta dirección ha sido etiquetada por la lista curada de Kleros", + "klerosVerifiedTooltip": "Verificado por el registro curado de Kleros", "balance": "Balance", "contractName": "Nombre del contrato", "compiler": "Compilador", diff --git a/src/services/KlerosService.ts b/src/services/KlerosService.ts new file mode 100644 index 00000000..a48aedc0 --- /dev/null +++ b/src/services/KlerosService.ts @@ -0,0 +1,79 @@ +export interface KlerosTag { + itemID: string; + publicNameTag: string; + projectName?: string; +} + +// Single source of truth — derive both forms from this +const ATQ_REGISTRY_CHECKSUM = "0x66260C69d03837016d88c9877e61e08Ef74C59F2"; +const ATQ_REGISTRY = ATQ_REGISTRY_CHECKSUM.toLowerCase(); + +export const KLEROS_CURATE_REGISTRY_URL = `https://curate.kleros.io/tcr/100/${ATQ_REGISTRY_CHECKSUM}`; + +export const getKlerosCurateItemUrl = (itemID: string) => + itemID ? `${KLEROS_CURATE_REGISTRY_URL}/${itemID}` : KLEROS_CURATE_REGISTRY_URL; + +// In-memory cache: address (lowercase) → { tag, expires } +const cache = new Map(); +const CACHE_TTL = 60 * 60 * 1000; // 1 hour + +export function isKlerosGraphEnabled(): boolean { + return !!import.meta.env.VITE_THE_GRAPH_API_KEY; +} + +const GRAPH_URL = () => + `https://gateway-arbitrum.network.thegraph.com/api/${import.meta.env.VITE_THE_GRAPH_API_KEY}/subgraphs/id/9hHo5MpjpC1JqfD3BsgFnojGurXRHTrHWcUcZPPCo6m8`; + +async function graphQuery(query: string): Promise { + const response = await fetch(GRAPH_URL(), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query }), + }); + if (!response.ok) throw new Error(`The Graph request failed: ${response.status}`); + const text = await response.text(); + try { + return JSON.parse(text); + } catch { + throw new Error(`The Graph returned invalid JSON: ${text.slice(0, 100)}`); + } +} + +export async function fetchKlerosTag(address: string): Promise { + if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return null; + + const key = address.toLowerCase(); + const cached = cache.get(key); + if (cached && Date.now() < cached.expires) return cached.tag; + + const query = `{ itemProps(where: { label: "Contract Address", value_contains_nocase: "${address}" }, first: 1) { item { item { itemID registryAddress status metadata { props { label value } } } } } }`; + + const json = (await graphQuery(query)) as { + data?: { + itemProps?: Array<{ + item?: { + item?: { + itemID: string; + registryAddress: string; + status: string; + metadata?: { props: Array<{ label: string; value: string }> }; + }; + }; + }>; + }; + }; + const litem = json.data?.itemProps?.[0]?.item?.item; + + let tag: KlerosTag | null = null; + if (litem?.status === "Registered" && litem.registryAddress === ATQ_REGISTRY) { + const props = litem.metadata?.props ?? []; + const publicNameTag = props.find((p) => p.label === "Public Name Tag")?.value ?? ""; + const projectName = props.find((p) => p.label === "Project Name")?.value || undefined; + if (publicNameTag) { + tag = { itemID: litem.itemID, publicNameTag, projectName }; + } + } + + cache.set(key, { tag, expires: Date.now() + CACHE_TTL }); + return tag; +} diff --git a/src/styles/components.css b/src/styles/components.css index 98c9d0ee..0dd975b6 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -4564,6 +4564,9 @@ button.tx-section-header-toggle { } .kleros-verified-tag { + display: inline-flex; + align-items: center; + gap: 4px; font-size: 0.75rem; font-weight: 600; color: #6b3fa0; @@ -4571,7 +4574,17 @@ button.tx-section-header-toggle { background: rgba(107, 63, 160, 0.1); border: 1px solid rgba(107, 63, 160, 0.3); border-radius: 4px; - cursor: default; + text-decoration: none; +} + +.kleros-verified-tag:hover { + background: rgba(107, 63, 160, 0.2); +} + +.kleros-logo { + width: 14px; + height: 14px; + object-fit: contain; } /* Token Info Row */ diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b9e64034..47d5063e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1 +1 @@ -export const ENVIRONMENT = process.env.REACT_APP_ENVIRONMENT || "development"; +export const ENVIRONMENT = import.meta.env.VITE_ENVIRONMENT || "development"; diff --git a/vite.config.ts b/vite.config.ts index c8360636..38fa1a12 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -58,7 +58,7 @@ export default defineConfig({ "process.env.REACT_APP_OPENSCAN_NETWORKS": JSON.stringify( process.env.REACT_APP_OPENSCAN_NETWORKS || "" ), - "process.env.REACT_APP_ENVIRONMENT": JSON.stringify( + "import.meta.env.VITE_ENVIRONMENT": JSON.stringify( process.env.NODE_ENV || "development" ), }, From efad60d34984a0117db208cb98df6b563b381a30 Mon Sep 17 00:00:00 2001 From: Augusto Lemble Date: Wed, 4 Mar 2026 16:40:42 -0300 Subject: [PATCH 3/5] ci: add VITE_THE_GRAPH_API_KEY to production build workflow --- .github/workflows/hash-deploy-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/hash-deploy-build.yml b/.github/workflows/hash-deploy-build.yml index 3338dd96..57c52ae4 100644 --- a/.github/workflows/hash-deploy-build.yml +++ b/.github/workflows/hash-deploy-build.yml @@ -34,6 +34,8 @@ jobs: - name: Build Production run: ./scripts/build-production.sh + env: + VITE_THE_GRAPH_API_KEY: ${{ secrets.VITE_THE_GRAPH_API_KEY }} - name: Get IPFS Hash id: ipfs-hash From 80a6f9eac0afa4f6530106636924630aaa8f3f2f Mon Sep 17 00:00:00 2001 From: Augusto Lemble Date: Wed, 4 Mar 2026 16:44:06 -0300 Subject: [PATCH 4/5] feat(kleros): hardcode public Graph API key with authorized domains --- .github/workflows/deploy-gh-pages.yml | 2 -- .github/workflows/deploy-pr-preview.yml | 2 -- .github/workflows/hash-deploy-build.yml | 2 -- src/services/KlerosService.ts | 7 +++++-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml index 52e43eee..a543e6d6 100644 --- a/.github/workflows/deploy-gh-pages.yml +++ b/.github/workflows/deploy-gh-pages.yml @@ -31,8 +31,6 @@ jobs: - name: Build Staging run: GITHUB_PAGES=true ./scripts/build-staging.sh - env: - VITE_THE_GRAPH_API_KEY: ${{ secrets.VITE_THE_GRAPH_API_KEY }} - name: Setup Pages uses: actions/configure-pages@v4 diff --git a/.github/workflows/deploy-pr-preview.yml b/.github/workflows/deploy-pr-preview.yml index a4fab124..84504055 100644 --- a/.github/workflows/deploy-pr-preview.yml +++ b/.github/workflows/deploy-pr-preview.yml @@ -27,8 +27,6 @@ jobs: - name: Build Staging run: ./scripts/build-staging.sh - env: - VITE_THE_GRAPH_API_KEY: ${{ secrets.VITE_THE_GRAPH_API_KEY }} - name: Deploy to Netlify id: netlify diff --git a/.github/workflows/hash-deploy-build.yml b/.github/workflows/hash-deploy-build.yml index 57c52ae4..3338dd96 100644 --- a/.github/workflows/hash-deploy-build.yml +++ b/.github/workflows/hash-deploy-build.yml @@ -34,8 +34,6 @@ jobs: - name: Build Production run: ./scripts/build-production.sh - env: - VITE_THE_GRAPH_API_KEY: ${{ secrets.VITE_THE_GRAPH_API_KEY }} - name: Get IPFS Hash id: ipfs-hash diff --git a/src/services/KlerosService.ts b/src/services/KlerosService.ts index a48aedc0..a6fc67c3 100644 --- a/src/services/KlerosService.ts +++ b/src/services/KlerosService.ts @@ -17,12 +17,15 @@ export const getKlerosCurateItemUrl = (itemID: string) => const cache = new Map(); const CACHE_TTL = 60 * 60 * 1000; // 1 hour +// Public key with authorized domains — safe to include in source +const THE_GRAPH_API_KEY = "60ced6ff18cad4b921e4f9df32ed6f32"; + export function isKlerosGraphEnabled(): boolean { - return !!import.meta.env.VITE_THE_GRAPH_API_KEY; + return true; } const GRAPH_URL = () => - `https://gateway-arbitrum.network.thegraph.com/api/${import.meta.env.VITE_THE_GRAPH_API_KEY}/subgraphs/id/9hHo5MpjpC1JqfD3BsgFnojGurXRHTrHWcUcZPPCo6m8`; + `https://gateway-arbitrum.network.thegraph.com/api/${THE_GRAPH_API_KEY}/subgraphs/id/9hHo5MpjpC1JqfD3BsgFnojGurXRHTrHWcUcZPPCo6m8`; async function graphQuery(query: string): Promise { const response = await fetch(GRAPH_URL(), { From c40524fac204e3a3c3312e5a398d11660708979b Mon Sep 17 00:00:00 2001 From: Augusto Lemble Date: Wed, 4 Mar 2026 16:44:59 -0300 Subject: [PATCH 5/5] fix(kleros): correct public Graph API key --- src/services/KlerosService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/KlerosService.ts b/src/services/KlerosService.ts index a6fc67c3..d2e4b2e9 100644 --- a/src/services/KlerosService.ts +++ b/src/services/KlerosService.ts @@ -18,7 +18,7 @@ const cache = new Map(); const CACHE_TTL = 60 * 60 * 1000; // 1 hour // Public key with authorized domains — safe to include in source -const THE_GRAPH_API_KEY = "60ced6ff18cad4b921e4f9df32ed6f32"; +const THE_GRAPH_API_KEY = "90d378ffb102d0bcce31c59bc16057b6"; export function isKlerosGraphEnabled(): boolean { return true;