Files
edc54514f1 OP Superchain explorer: aggregated views (#3040)
* refactor widgets on home page

* coming soon placeholder component

* top accounts page

* contract data

* address ERC-20 tokens list

* chain filter for address ERC-20 tokens

* token select for address detailed view

* address details fields

* tokens table

* stats widgets on home page

* chain indicators graph on the home page

* quick search

* search results all tab

* tabs and chain select on search results page

* search results mobile view

* add contract info to search and address details

* add text filter for tokens and search improvements

* load all results inside tab on search results page

* token balance info

* add support for domains in the full search

* Multichain explorer

Resolves #2989

* add envs validator for multichain

* add some tests

* fix search bar on search results page on mobile

* home stats fixes

* fix ts

* optimize amount of queries on address page

* add more contract info on address page

* add enter key hint to search input

* fix tests

* fix test

* update bg.primary css-variable on color theme change

* sitemap for multichain

* txs page: render first tab if blob tab is not available on the chain

* filter out in active chains from chain select on address page and user ops page

* add chain icon to advanced filter table

* fix tooltip on entity with shield

* fix ts

* show contract implementations

* fixes

* add chain icon to all lists

* refactor zetachain chains config type

* use chain id as primary option instead of chain slug

* refactor chain config type for multichain and essential dapps

* refactor chain icon and chain select

* rename chain-slug in chain_slug

* change default radius of chain icon and update screenshots for multichain tests

* change conditions for "isContract" and "isVerified"

* test fixes

* make shield optional for external chain entities

* add chain icon to approvals lists

* add stats page for multichain

* add chain postfix to chart .png and .csv assets name

* fix tests

* sync verified contracts language filter and fix loading state in the search results page

* fix sol2uml diagram and csv export

* regcred from helmfile for review

* link fixes

* fix state for unknown address

* fix incorrect chain in the context during spa transition to the same route on different chain

* add links to stats widgets

* add mixpanel to L2 demo values

* show message of on-demand contract verification

* fix screenshots

---------

Co-authored-by: Nick Zenchik <[email protected]>
2025-11-18 09:44:22 +01:00

240 lines
7.0 KiB
TypeScript

import ERC20Artifact from '@openzeppelin/contracts/build/contracts/ERC20.json';
import { uniq } from 'es-toolkit';
import { useCallback } from 'react';
import { getAddress, formatUnits, slice } from 'viem';
import type { PublicClient, Log } from 'viem';
import type { AddressTokenBalancesResponse } from 'types/api/address';
import type { TokenInfo } from 'types/api/token';
import type { EssentialDappsChainConfig } from 'types/client/marketplace';
import type { AllowanceType, ContractAllowanceType } from 'types/client/revoke';
import useApiFetch from 'lib/api/useApiFetch';
import useGetBlockTimestamp from './useGetBlockTimestamp';
function formatAllowance(
allowance: bigint,
decimals: number = 18,
totalSupply: bigint | undefined,
): string {
if (totalSupply && allowance > totalSupply) {
return 'Unlimited';
}
return formatUnits(allowance, decimals);
}
async function getERC20AllowanceFromApproval(
tokenAddress: `0x${ string }`,
ownerAddress: string,
approval: Log,
publicClient: PublicClient,
): Promise<ContractAllowanceType> {
const spender = getAddress(slice(approval.topics[2] as `0x${ string }`, 12));
const allowance = await publicClient
.readContract({
address: tokenAddress,
abi: ERC20Artifact.abi,
functionName: 'allowance',
args: [ ownerAddress, spender ],
})
.catch(() => undefined);
return {
transactionId: approval.transactionHash,
spender,
allowance: allowance as bigint | undefined,
blockNumber: approval.blockNumber as bigint,
};
}
async function getERC20AllowancesFromApprovals(
tokenAddress: `0x${ string }`,
ownerAddress: string,
approvals: Array<Log>,
publicClient: PublicClient,
) {
const deduplicatedApprovals = approvals.filter(
(approval, i) =>
i ===
approvals.findIndex((other) => approval.topics[2] === other.topics[2]),
);
const allowances: Array<ContractAllowanceType> = await Promise.all(
deduplicatedApprovals.map((approval) =>
getERC20AllowanceFromApproval(
tokenAddress,
ownerAddress,
approval,
publicClient,
),
),
);
return allowances;
}
const useGetERC20TokenData = () => {
const apiFetch = useApiFetch();
return useCallback(async(
tokenAddress: `0x${ string }`,
chain: EssentialDappsChainConfig | undefined,
signal?: AbortSignal,
) => {
try {
const data = await apiFetch('general:token', {
pathParams: { hash: tokenAddress },
chain,
fetchParams: {
signal,
},
}) as TokenInfo;
return {
symbol: data.symbol || undefined,
decimals: data.decimals ? Number(data.decimals) : undefined,
totalSupply: data.total_supply ? BigInt(data.total_supply) : undefined,
name: data.name || undefined,
tokenIcon: data.icon_url || undefined,
price: data.exchange_rate || undefined,
tokenReputation: data.reputation,
balance: undefined,
};
} catch {
return undefined;
}
}, [ apiFetch ]);
};
const useGetERC20Allowances = () => {
const getERC20TokenData = useGetERC20TokenData();
const getBlockTimestamp = useGetBlockTimestamp();
const apiFetch = useApiFetch();
return useCallback(async(
chain: EssentialDappsChainConfig | undefined,
tokenAddresses: Array<`0x${ string }`>,
searchQuery: string,
approvals: Array<Log>,
publicClient: PublicClient,
signal?: AbortSignal,
) => {
const allowances: Array<AllowanceType> = [];
let balances: Record<string, bigint> = {};
const response = await apiFetch('general:address_token_balances', {
pathParams: { hash: searchQuery },
chain,
fetchParams: {
signal,
},
}) as AddressTokenBalancesResponse;
balances = Object.fromEntries(
response.map((entry) => [ entry.token.address_hash, BigInt(entry.value) ]),
);
await Promise.all(
tokenAddresses.map(async(tokenAddress) => {
if (signal?.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
const tokenData = await getERC20TokenData(tokenAddress, chain, signal);
if (tokenData) {
const tokenApprovals = approvals.filter(
(approval) => getAddress(approval.address) === tokenAddress,
);
if (tokenApprovals.length > 0) {
const tokenAllowances = await getERC20AllowancesFromApprovals(
tokenAddress,
searchQuery,
tokenApprovals,
publicClient,
);
await Promise.all(
tokenAllowances.map(async(allowance) => {
if (signal?.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
const timestampMs = await getBlockTimestamp(chain, allowance.blockNumber, signal);
let valueAtRiskUsd;
if (
allowance.allowance &&
balances[tokenAddress] &&
tokenData.price &&
tokenData.decimals
) {
const valueAtRisk = allowance.allowance > balances[tokenAddress] ?
balances[tokenAddress] :
allowance.allowance;
valueAtRiskUsd = Number(
(
parseFloat(formatUnits(valueAtRisk, tokenData.decimals)) *
parseFloat(tokenData.price)
).toFixed(2),
);
}
if (allowance.allowance && allowance.allowance !== BigInt(0)) {
allowances.push({
...tokenData,
type: 'ERC-20',
address: tokenAddress,
transactionId: allowance.transactionId,
spender: allowance.spender,
timestamp: timestampMs,
allowance: allowance.allowance ?
formatAllowance(
allowance.allowance,
tokenData.decimals,
tokenData.totalSupply,
) : undefined,
valueAtRiskUsd,
});
}
}),
);
}
}
}),
);
return allowances;
}, [ getERC20TokenData, apiFetch, getBlockTimestamp ]);
};
export default function useSearchErc20Allowances() {
const getERC20Allowances = useGetERC20Allowances();
return useCallback(async(
chain: EssentialDappsChainConfig | undefined,
searchQuery: string,
approvalEvents: Array<Log>,
publicClient: PublicClient,
signal?: AbortSignal,
) => {
const erc20Events = approvalEvents.filter((ev) => ev.topics.length === 3);
const erc20Addresses = uniq(
erc20Events.map((event) => getAddress(event.address)),
);
return getERC20Allowances(
chain,
erc20Addresses,
searchQuery,
erc20Events,
publicClient,
signal,
);
}, [ getERC20Allowances ]);
}