Merge branch 'feature/swr-everything' into develop

This commit is contained in:
Willian Mitsuda 2022-08-16 00:47:43 -03:00
commit 18ab4dff67
No known key found for this signature in database
21 changed files with 338 additions and 655 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect, useContext, useCallback, useMemo } from "react";
import React, { useEffect, useContext, useCallback } from "react";
import {
useParams,
useNavigate,
@ -23,9 +23,9 @@ import Contracts from "./address/Contracts";
import { RuntimeContext } from "./useRuntime";
import { useAppConfigContext } from "./useAppConfig";
import { useAddressOrENS } from "./useResolvedAddresses";
import { useMultipleMetadata } from "./sourcify/useSourcify";
import { useSourcifyMetadata } from "./sourcify/useSourcify";
import { ChecksummedAddress } from "./types";
import { useAddressesWithCode } from "./useErigonHooks";
import { useHasCode } from "./useErigonHooks";
import { useChainInfo } from "./useChainInfo";
const AddressTransactionByNonce = React.lazy(
@ -65,25 +65,13 @@ const Address: React.FC = () => {
}
}, [addressOrName, checksummedAddress, isENS]);
const hasCode = useHasCode(provider, checksummedAddress, "latest");
const { sourcifySource } = useAppConfigContext();
const checksummedAddressAsArray = useMemo(
() => (checksummedAddress !== undefined ? [checksummedAddress] : []),
[checksummedAddress]
);
const contractAddresses = useAddressesWithCode(
provider,
checksummedAddressAsArray
);
const metadatas = useMultipleMetadata(
undefined,
contractAddresses,
const addressMetadata = useSourcifyMetadata(
hasCode ? checksummedAddress : undefined,
provider?.network.chainId,
sourcifySource
);
const addressMetadata =
checksummedAddress !== undefined
? metadatas[checksummedAddress]
: undefined;
const { network, faucets } = useChainInfo();
@ -134,7 +122,7 @@ const Address: React.FC = () => {
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href={`/address/${addressOrName}`}>Overview</NavTab>
{(contractAddresses?.length ?? 0) > 0 && (
{hasCode && (
<NavTab href={`/address/${addressOrName}/contract`}>
<span
className={`flex items-baseline space-x-2 ${
@ -181,12 +169,7 @@ const Address: React.FC = () => {
element={
<Contracts
checksummedAddress={checksummedAddress}
rawMetadata={
contractAddresses !== undefined &&
contractAddresses.length === 0
? null
: addressMetadata
}
rawMetadata={addressMetadata}
/>
}
/>

View File

@ -6,28 +6,20 @@ import TransactionAddress from "./components/TransactionAddress";
import ValueHighlighter from "./components/ValueHighlighter";
import FormattedBalance from "./components/FormattedBalance";
import USDAmount from "./components/USDAmount";
import {
AddressContext,
ChecksummedAddress,
TokenMeta,
TokenTransfer,
} from "./types";
import { AddressContext, TokenMeta, TokenTransfer } from "./types";
import { RuntimeContext } from "./useRuntime";
import { useBlockNumberContext } from "./useBlockTagContext";
import { Metadata } from "./sourcify/useSourcify";
import { useTokenUSDOracle } from "./usePriceOracle";
type TokenTransferItemProps = {
t: TokenTransfer;
tokenMeta?: TokenMeta | null | undefined;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
};
// TODO: handle partial
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
t,
tokenMeta,
metadatas,
}) => {
const { provider } = useContext(RuntimeContext);
const blockNumber = useBlockNumberContext();
@ -40,7 +32,6 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<TransactionAddress
address={t.from}
addressCtx={AddressContext.FROM}
metadata={metadatas[t.from]}
showCodeIndicator
/>
</div>
@ -51,7 +42,6 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<TransactionAddress
address={t.to}
addressCtx={AddressContext.TO}
metadata={metadatas[t.to]}
showCodeIndicator
/>
</div>
@ -67,7 +57,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
/>
</ValueHighlighter>
</span>
<TransactionAddress address={t.token} metadata={metadatas[t.token]} />
<TransactionAddress address={t.token} />
{tokenMeta && quote !== undefined && decimals !== undefined && (
<span className="px-2 border-gray-200 border rounded-lg bg-gray-100 text-gray-600">
<USDAmount

View File

@ -1,13 +1,71 @@
import React from "react";
import { useParams } from "react-router-dom";
import TransactionPageContent from "./TransactionPageContent";
import React, { useContext } from "react";
import { useParams, Route, Routes } from "react-router-dom";
import { Tab } from "@headlessui/react";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
import NavTab from "./components/NavTab";
import { RuntimeContext } from "./useRuntime";
import { useTxData } from "./useErigonHooks";
import { SelectionContext, useSelection } from "./useSelection";
import { SelectedTransactionContext } from "./useSelectedTransaction";
import { BlockNumberContext } from "./useBlockTagContext";
const Details = React.lazy(() => import("./transaction/Details"));
const Logs = React.lazy(() => import("./transaction/Logs"));
const Trace = React.lazy(() => import("./transaction/Trace"));
const Transaction: React.FC = () => {
const { txhash } = useParams();
if (txhash === undefined) {
const { txhash: txHash } = useParams();
if (txHash === undefined) {
throw new Error("txhash couldn't be undefined here");
}
return <TransactionPageContent txHash={txhash} />;
const { provider } = useContext(RuntimeContext);
const txData = useTxData(provider, txHash);
const selectionCtx = useSelection();
return (
<SelectedTransactionContext.Provider value={txData}>
<BlockNumberContext.Provider value={txData?.confirmedData?.blockNumber}>
<StandardFrame>
<StandardSubtitle>Transaction Details</StandardSubtitle>
{txData === null && (
<ContentFrame>
<div className="py-4 text-sm">
Transaction <span className="font-hash">{txHash}</span> not
found.
</div>
</ContentFrame>
)}
{txData && (
<SelectionContext.Provider value={selectionCtx}>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href=".">Overview</NavTab>
{txData.confirmedData?.blockNumber !== undefined && (
<NavTab href="logs">
Logs
{txData &&
` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab>
)}
<NavTab href="trace">Trace</NavTab>
</Tab.List>
</Tab.Group>
<React.Suspense fallback={null}>
<Routes>
<Route index element={<Details txData={txData} />} />
<Route path="logs" element={<Logs txData={txData} />} />
<Route path="trace" element={<Trace txData={txData} />} />
</Routes>
</React.Suspense>
</SelectionContext.Provider>
)}
</StandardFrame>
</BlockNumberContext.Provider>
</SelectedTransactionContext.Provider>
);
};
export default Transaction;

View File

@ -1,113 +0,0 @@
import React, { useContext, useMemo } from "react";
import { Route, Routes } from "react-router-dom";
import { Tab } from "@headlessui/react";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
import NavTab from "./components/NavTab";
import { RuntimeContext } from "./useRuntime";
import { useInternalOperations, useTxData } from "./useErigonHooks";
import { SelectionContext, useSelection } from "./useSelection";
import { SelectedTransactionContext } from "./useSelectedTransaction";
import { BlockNumberContext } from "./useBlockTagContext";
import { useAppConfigContext } from "./useAppConfig";
import { useSourcify, useTransactionDescription } from "./sourcify/useSourcify";
const Details = React.lazy(() => import("./transaction/Details"));
const Logs = React.lazy(() => import("./transaction/Logs"));
const Trace = React.lazy(() => import("./transaction/Trace"));
type TransactionPageContentProps = {
txHash: string;
};
const TransactionPageContent: React.FC<TransactionPageContentProps> = ({
txHash,
}) => {
const { provider } = useContext(RuntimeContext);
const txData = useTxData(provider, txHash);
const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => {
if (!txData || !internalOps) {
return false;
}
for (const t of internalOps) {
if (t.to === txData.confirmedData?.miner) {
return true;
}
}
return false;
}, [txData, internalOps]);
const selectionCtx = useSelection();
const { sourcifySource } = useAppConfigContext();
const metadata = useSourcify(
txData?.to,
provider?.network.chainId,
sourcifySource
);
const txDesc = useTransactionDescription(metadata, txData);
return (
<SelectedTransactionContext.Provider value={txData}>
<BlockNumberContext.Provider value={txData?.confirmedData?.blockNumber}>
<StandardFrame>
<StandardSubtitle>Transaction Details</StandardSubtitle>
{txData === null && (
<ContentFrame>
<div className="py-4 text-sm">
Transaction <span className="font-hash">{txHash}</span> not
found.
</div>
</ContentFrame>
)}
{txData && (
<SelectionContext.Provider value={selectionCtx}>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href=".">Overview</NavTab>
{txData.confirmedData?.blockNumber !== undefined && (
<NavTab href="logs">
Logs
{txData &&
` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab>
)}
<NavTab href="trace">Trace</NavTab>
</Tab.List>
</Tab.Group>
<React.Suspense fallback={null}>
<Routes>
<Route
index
element={
<Details
txData={txData}
txDesc={txDesc}
toMetadata={metadata}
userDoc={metadata?.output.userdoc}
devDoc={metadata?.output.devdoc}
internalOps={internalOps}
sendsEthToMiner={sendsEthToMiner}
/>
}
/>
<Route
path="logs"
element={<Logs txData={txData} metadata={metadata} />}
/>
<Route path="trace" element={<Trace txData={txData} />} />
</Routes>
</React.Suspense>
</SelectionContext.Provider>
)}
</StandardFrame>
</BlockNumberContext.Provider>
</SelectedTransactionContext.Provider>
);
};
export default TransactionPageContent;

View File

@ -18,7 +18,6 @@ import { useMultipleETHUSDOracle } from "../usePriceOracle";
import { RuntimeContext } from "../useRuntime";
import { useParams, useSearchParams } from "react-router-dom";
import { ChecksummedAddress, ProcessedTransaction } from "../types";
import { useContractsMetadata } from "../hooks";
import { useAddressBalance, useContractCreator } from "../useErigonHooks";
import { BlockNumberContext } from "../useBlockTagContext";
@ -112,22 +111,6 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
}, [page]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
// Calculate Sourcify metadata for all addresses that appear on this page results
const addresses = useMemo(() => {
const _addresses = [address];
if (page) {
for (const t of page) {
if (t.to) {
_addresses.push(t.to);
}
if (t.createdContractAddress) {
_addresses.push(t.createdContractAddress);
}
}
}
return _addresses;
}, [address, page]);
const metadatas = useContractsMetadata(addresses, provider);
const balance = useAddressBalance(provider, address);
const creator = useContractCreator(provider, address);
@ -181,7 +164,6 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
selectedAddress={address}
feeDisplay={feeDisplay}
priceMap={priceMap}
metadatas={metadatas}
/>
))}
<NavBar address={address} page={page} controller={controller} />

View File

@ -1,31 +1,11 @@
import React from "react";
import { SyntaxHighlighter, docco } from "../highlight-init";
import { useContract } from "../sourcify/useSourcify";
import { useAppConfigContext } from "../useAppConfig";
type ContractProps = {
checksummedAddress: string;
networkId: number;
filename: string;
source: any;
content: any;
};
const Contract: React.FC<ContractProps> = ({
checksummedAddress,
networkId,
filename,
source,
}) => {
const { sourcifySource } = useAppConfigContext();
const content = useContract(
checksummedAddress,
networkId,
filename,
source,
sourcifySource
);
return (
const Contract: React.FC<ContractProps> = ({ content }) => (
<SyntaxHighlighter
className="w-full h-full border font-code text-base"
language="solidity"
@ -35,6 +15,5 @@ const Contract: React.FC<ContractProps> = ({
{content ?? ""}
</SyntaxHighlighter>
);
};
export default React.memo(Contract);

View File

@ -0,0 +1,37 @@
import React from "react";
import { SyntaxHighlighter, docco } from "../highlight-init";
import { useContract } from "../sourcify/useSourcify";
import { useAppConfigContext } from "../useAppConfig";
type ContractFromRepoProps = {
checksummedAddress: string;
networkId: number;
filename: string;
};
const ContractFromRepo: React.FC<ContractFromRepoProps> = ({
checksummedAddress,
networkId,
filename,
}) => {
const { sourcifySource } = useAppConfigContext();
const content = useContract(
checksummedAddress,
networkId,
filename,
sourcifySource
);
return (
<SyntaxHighlighter
className="w-full h-full border font-code text-base"
language="solidity"
style={docco}
showLineNumbers
>
{content ?? ""}
</SyntaxHighlighter>
);
};
export default React.memo(ContractFromRepo);

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useContext, Fragment } from "react";
import React, { useState, useEffect, useContext } from "react";
import { commify } from "@ethersproject/units";
import { Menu } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,6 +6,7 @@ import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import ContentFrame from "../ContentFrame";
import InfoRow from "../components/InfoRow";
import Contract from "./Contract";
import ContractFromRepo from "./ContractFromRepo";
import { RuntimeContext } from "../useRuntime";
import { Metadata } from "../sourcify/useSourcify";
import ExternalLink from "../components/ExternalLink";
@ -101,7 +102,7 @@ const Contracts: React.FC<ContractsProps> = ({
className={`flex text-sm px-2 py-1 ${
selected === k
? "font-bold bg-gray-200 text-gray-500"
: "hover:border-orange-200 hover:text-gray-500 text-gray-400 transition-transform transition-colors duration-75"
: "hover:text-gray-500 text-gray-400 transition-colors duration-75"
}`}
onClick={() => setSelected(k)}
>
@ -113,13 +114,18 @@ const Contracts: React.FC<ContractsProps> = ({
</div>
</Menu>
{selected && (
<Contract
<>
{rawMetadata.sources[selected].content ? (
<Contract content={rawMetadata.sources[selected].content} />
) : (
<ContractFromRepo
checksummedAddress={checksummedAddress}
networkId={provider!.network.chainId}
filename={selected}
source={rawMetadata.sources[selected]}
/>
)}
</>
)}
</div>
</>
)}

View File

@ -8,10 +8,9 @@ import TransactionItem from "../search/TransactionItem";
import { useFeeToggler } from "../search/useFeeToggler";
import { RuntimeContext } from "../useRuntime";
import { SelectionContext, useSelection } from "../useSelection";
import { ChecksummedAddress, ProcessedTransaction } from "../types";
import { ProcessedTransaction } from "../types";
import { PAGE_SIZE } from "../params";
import { useMultipleETHUSDOracle } from "../usePriceOracle";
import { useContractsMetadata } from "../hooks";
type BlockTransactionResultsProps = {
blockTag: BlockTag;
@ -32,24 +31,6 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
const blockTags = useMemo(() => [blockTag], [blockTag]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
const addresses = useMemo((): ChecksummedAddress[] => {
if (!page) {
return [];
}
const _addresses: ChecksummedAddress[] = [];
for (const t of page) {
if (t.to) {
_addresses.push(t.to);
}
if (t.createdContractAddress) {
_addresses.push(t.createdContractAddress);
}
}
return _addresses;
}, [page]);
const metadatas = useContractsMetadata(addresses, provider);
return (
<ContentFrame>
<div className="flex justify-between items-baseline py-3">
@ -78,7 +59,6 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
tx={tx}
feeDisplay={feeDisplay}
priceMap={priceMap}
metadatas={metadatas}
/>
))}
<div className="flex justify-between items-baseline py-3">

View File

@ -8,8 +8,9 @@ import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn";
import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins";
import SourcifyLogo from "../sourcify/SourcifyLogo";
import PlainAddress from "./PlainAddress";
import { Metadata } from "../sourcify/useSourcify";
import { RuntimeContext } from "../useRuntime";
import { useAppConfigContext } from "../useAppConfig";
import { useSourcifyMetadata } from "../sourcify/useSourcify";
import { useResolvedAddress } from "../useResolvedAddresses";
import { AddressContext, ChecksummedAddress, ZERO_ADDRESS } from "../types";
import { resolverRendererRegistry } from "../api/address-resolver";
@ -23,7 +24,6 @@ type DecoratedAddressLinkProps = {
selfDestruct?: boolean | undefined;
txFrom?: boolean | undefined;
txTo?: boolean | undefined;
metadata?: Metadata | null | undefined;
eoa?: boolean | undefined;
};
@ -36,9 +36,16 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
selfDestruct,
txFrom,
txTo,
metadata,
eoa,
}) => {
const { provider } = useContext(RuntimeContext);
const { sourcifySource } = useAppConfigContext();
const metadata = useSourcifyMetadata(
address,
provider?.network.chainId,
sourcifySource
);
const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS;
const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS;

View File

@ -5,20 +5,17 @@ import { useSelectedTransaction } from "../useSelectedTransaction";
import { useBlockNumberContext } from "../useBlockTagContext";
import { RuntimeContext } from "../useRuntime";
import { useHasCode } from "../useErigonHooks";
import { Metadata } from "../sourcify/useSourcify";
import { AddressContext, ChecksummedAddress } from "../types";
type TransactionAddressProps = {
address: ChecksummedAddress;
addressCtx?: AddressContext | undefined;
metadata?: Metadata | null | undefined;
showCodeIndicator?: boolean;
};
const TransactionAddress: React.FC<TransactionAddressProps> = ({
address,
addressCtx,
metadata,
showCodeIndicator = false,
}) => {
const txData = useSelectedTransaction();
@ -46,7 +43,6 @@ const TransactionAddress: React.FC<TransactionAddressProps> = ({
txFrom={address === txData?.from}
txTo={address === txData?.to || creation}
creation={creation}
metadata={metadata}
eoa={
showCodeIndicator && blockNumber !== undefined
? !toHasCode

View File

@ -1,33 +0,0 @@
import { useMemo } from "react";
import { JsonRpcProvider } from "@ethersproject/providers";
import { ChecksummedAddress } from "./types";
import { Metadata, useMultipleMetadata } from "./sourcify/useSourcify";
import { useAppConfigContext } from "./useAppConfig";
import { useAddressesWithCode } from "./useErigonHooks";
export const useDedupedAddresses = (
addresses: ChecksummedAddress[]
): ChecksummedAddress[] => {
return useMemo(() => {
const deduped = new Set(addresses);
return [...deduped];
}, [addresses]);
};
export const useContractsMetadata = (
addresses: ChecksummedAddress[],
provider: JsonRpcProvider | undefined,
baseMetadatas?: Record<string, Metadata | null>
) => {
const deduped = useDedupedAddresses(addresses);
const contracts = useAddressesWithCode(provider, deduped);
const { sourcifySource } = useAppConfigContext();
const metadatas = useMultipleMetadata(
baseMetadatas,
contracts,
provider?.network.chainId,
sourcifySource
);
return metadatas;
};

View File

@ -14,20 +14,18 @@ import TransactionDirection, {
Flags,
} from "../components/TransactionDirection";
import TransactionValue from "../components/TransactionValue";
import { ChecksummedAddress, ProcessedTransaction } from "../types";
import { ProcessedTransaction } from "../types";
import { FeeDisplay } from "./useFeeToggler";
import { RuntimeContext } from "../useRuntime";
import { useHasCode } from "../useErigonHooks";
import { formatValue } from "../components/formatter";
import ETH2USDValue from "../components/ETH2USDValue";
import { Metadata } from "../sourcify/useSourcify";
type TransactionItemProps = {
tx: ProcessedTransaction;
selectedAddress?: string;
feeDisplay: FeeDisplay;
priceMap: Record<BlockTag, BigNumber>;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
};
const TransactionItem: React.FC<TransactionItemProps> = ({
@ -35,7 +33,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
selectedAddress,
feeDisplay,
priceMap,
metadatas,
}) => {
const { provider } = useContext(RuntimeContext);
const toHasCode = useHasCode(
@ -113,7 +110,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
address={tx.to}
selectedAddress={selectedAddress}
miner={tx.miner === tx.to}
metadata={metadatas[tx.to]}
eoa={toHasCode === undefined ? undefined : !toHasCode}
/>
</AddressHighlighter>
@ -123,7 +119,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
address={tx.createdContractAddress!}
selectedAddress={selectedAddress}
creation
metadata={metadatas[tx.createdContractAddress!]}
eoa={false}
/>
</AddressHighlighter>

View File

@ -1,6 +1,7 @@
import { useState, useEffect, useMemo } from "react";
import { useMemo } from "react";
import { Interface } from "@ethersproject/abi";
import { ErrorDescription } from "@ethersproject/abi/lib/interface";
import useSWRImmutable from "swr/immutable";
import { ChecksummedAddress, TransactionData } from "../types";
import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "../url";
@ -80,127 +81,54 @@ export type Metadata = {
};
};
const fetchSourcifyMetadata = async (
address: ChecksummedAddress,
chainId: number,
source: SourcifySource,
abortController: AbortController
): Promise<Metadata | null> => {
const sourcifyFetcher = async (url: string) => {
try {
const metadataURL = sourcifyMetadata(address, chainId, source);
const result = await fetch(metadataURL, {
signal: abortController.signal,
});
if (result.ok) {
return await result.json();
const res = await fetch(url);
if (res.ok) {
return res.json();
}
return null;
} catch (err) {
console.error(err);
console.warn(
`error while getting Sourcify metadata: url=${url} err=${err}`
);
return null;
}
};
// TODO: replace every occurrence with the multiple version one
export const useSourcify = (
export const useSourcifyMetadata = (
address: ChecksummedAddress | undefined,
chainId: number | undefined,
source: SourcifySource
): Metadata | null | undefined => {
const [rawMetadata, setRawMetadata] = useState<Metadata | null | undefined>();
useEffect(() => {
if (!address || chainId === undefined) {
return;
}
setRawMetadata(undefined);
const abortController = new AbortController();
const fetchMetadata = async () => {
const _metadata = await fetchSourcifyMetadata(
address,
chainId,
source,
abortController
const metadataURL = () =>
address === undefined || chainId === undefined
? null
: sourcifyMetadata(address, chainId, source);
const { data, error } = useSWRImmutable<Metadata>(
metadataURL,
sourcifyFetcher
);
setRawMetadata(_metadata);
};
fetchMetadata();
return () => {
abortController.abort();
};
}, [address, chainId, source]);
return rawMetadata;
if (error) {
return null;
}
return data;
};
export const useMultipleMetadata = (
baseMetadatas: Record<string, Metadata | null> | undefined,
addresses: ChecksummedAddress[] | undefined,
chainId: number | undefined,
source: SourcifySource
): Record<ChecksummedAddress, Metadata | null | undefined> => {
const [rawMetadata, setRawMetadata] = useState<
Record<string, Metadata | null | undefined>
>({});
useEffect(() => {
if (addresses === undefined || chainId === undefined) {
return;
const contractFetcher = async (url: string): Promise<string | null> => {
const res = await fetch(url);
if (res.ok) {
return await res.text();
}
setRawMetadata({});
const abortController = new AbortController();
const fetchMetadata = async (_addresses: string[]) => {
const fetchers: Promise<Metadata | null>[] = [];
for (const address of _addresses) {
fetchers.push(
fetchSourcifyMetadata(address, chainId, source, abortController)
);
}
const results = await Promise.all(fetchers);
if (abortController.signal.aborted) {
return;
}
let metadatas: Record<string, Metadata | null> = {};
if (baseMetadatas) {
metadatas = { ...baseMetadatas };
}
for (let i = 0; i < results.length; i++) {
metadatas[_addresses[i]] = results[i];
}
setRawMetadata(metadatas);
};
const filtered = addresses.filter((a) => baseMetadatas?.[a] === undefined);
fetchMetadata(filtered);
return () => {
abortController.abort();
};
}, [baseMetadatas, addresses, chainId, source]);
return rawMetadata;
return null;
};
export const useContract = (
checksummedAddress: string,
networkId: number,
filename: string,
source: any,
sourcifySource: SourcifySource
) => {
const [content, setContent] = useState<string>(source.content);
useEffect(() => {
if (source.content) {
return;
}
const abortController = new AbortController();
const readContent = async () => {
const normalizedFilename = filename.replaceAll(/[@:]/g, "_");
const url = sourcifySourceFile(
checksummedAddress,
@ -208,20 +136,12 @@ export const useContract = (
normalizedFilename,
sourcifySource
);
const res = await fetch(url, { signal: abortController.signal });
if (res.ok) {
const _content = await res.text();
setContent(_content);
const { data, error } = useSWRImmutable(url, contractFetcher);
if (error) {
return undefined;
}
};
readContent();
return () => {
abortController.abort();
};
}, [checksummedAddress, networkId, filename, source.content, sourcifySource]);
return content;
return data;
};
export const useTransactionDescription = (

View File

@ -1,6 +1,5 @@
import React, { useContext, useMemo, useState } from "react";
import { Tab } from "@headlessui/react";
import { TransactionDescription } from "@ethersproject/abi";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
import { faCube } from "@fortawesome/free-solid-svg-icons/faCube";
@ -25,11 +24,7 @@ import USDValue from "../components/USDValue";
import FormattedBalance from "../components/FormattedBalance";
import ETH2USDValue from "../components/ETH2USDValue";
import TokenTransferItem from "../TokenTransferItem";
import {
TransactionData,
InternalOperation,
ChecksummedAddress,
} from "../types";
import { TransactionData } from "../types";
import PercentageBar from "../components/PercentageBar";
import ExternalLink from "../components/ExternalLink";
import RelativePosition from "../components/RelativePosition";
@ -41,32 +36,24 @@ import {
use4Bytes,
useTransactionDescription,
} from "../use4Bytes";
import { DevDoc, Metadata, useError, UserDoc } from "../sourcify/useSourcify";
import { useAppConfigContext } from "../useAppConfig";
import {
useError,
useSourcifyMetadata,
useTransactionDescription as useSourcifyTransactionDescription,
} from "../sourcify/useSourcify";
import { RuntimeContext } from "../useRuntime";
import { useContractsMetadata } from "../hooks";
import { useTransactionError } from "../useErigonHooks";
import { useInternalOperations, useTransactionError } from "../useErigonHooks";
import { useChainInfo } from "../useChainInfo";
import { useETHUSDOracle } from "../usePriceOracle";
type DetailsProps = {
txData: TransactionData;
txDesc: TransactionDescription | null | undefined;
toMetadata: Metadata | null | undefined;
userDoc?: UserDoc | undefined;
devDoc?: DevDoc | undefined;
internalOps?: InternalOperation[];
sendsEthToMiner: boolean;
};
const Details: React.FC<DetailsProps> = ({
txData,
txDesc,
toMetadata,
userDoc,
devDoc,
internalOps,
sendsEthToMiner,
}) => {
const Details: React.FC<DetailsProps> = ({ txData }) => {
const { provider } = useContext(RuntimeContext);
const hasEIP1559 =
txData.confirmedData?.blockBaseFeePerGas !== undefined &&
txData.confirmedData?.blockBaseFeePerGas !== null;
@ -80,11 +67,34 @@ const Details: React.FC<DetailsProps> = ({
txData.value
);
const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => {
if (!txData || !internalOps) {
return false;
}
for (const t of internalOps) {
if (t.to === txData.confirmedData?.miner) {
return true;
}
}
return false;
}, [txData, internalOps]);
const { sourcifySource } = useAppConfigContext();
const metadata = useSourcifyMetadata(
txData?.to,
provider?.network.chainId,
sourcifySource
);
const txDesc = useSourcifyTransactionDescription(metadata, txData);
const userDoc = metadata?.output.userdoc;
const devDoc = metadata?.output.devdoc;
const resolvedTxDesc = txDesc ?? fourBytesTxDesc;
const userMethod = txDesc ? userDoc?.methods[txDesc.signature] : undefined;
const devMethod = txDesc ? devDoc?.methods[txDesc.signature] : undefined;
const { provider } = useContext(RuntimeContext);
const {
nativeCurrency: { name, symbol },
} = useChainInfo();
@ -94,28 +104,12 @@ const Details: React.FC<DetailsProps> = ({
txData?.confirmedData?.blockNumber
);
const addresses = useMemo(() => {
const _addresses: ChecksummedAddress[] = [];
if (txData.to) {
_addresses.push(txData.to);
}
if (txData.confirmedData?.createdContractAddress) {
_addresses.push(txData.confirmedData.createdContractAddress);
}
for (const t of txData.tokenTransfers) {
_addresses.push(t.from);
_addresses.push(t.to);
_addresses.push(t.token);
}
return _addresses;
}, [txData]);
const metadatas = useContractsMetadata(addresses, provider);
const [errorMsg, outputData, isCustomError] = useTransactionError(
provider,
txData.transactionHash
);
const errorDescription = useError(
toMetadata,
metadata,
isCustomError ? outputData : undefined
);
const userError = errorDescription
@ -269,11 +263,7 @@ const Details: React.FC<DetailsProps> = ({
<InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}>
{txData.to ? (
<div className="flex items-baseline space-x-2 -ml-1">
<TransactionAddress
address={txData.to}
metadata={metadatas?.[txData.to]}
showCodeIndicator
/>
<TransactionAddress address={txData.to} showCodeIndicator />
<Copy value={txData.to} />
</div>
) : txData.confirmedData === undefined ? (
@ -284,9 +274,6 @@ const Details: React.FC<DetailsProps> = ({
<div className="flex items-baseline space-x-2 -ml-1">
<TransactionAddress
address={txData.confirmedData?.createdContractAddress!}
metadata={
metadatas?.[txData.confirmedData?.createdContractAddress!]
}
/>
<Copy value={txData.confirmedData.createdContractAddress!} />
</div>
@ -316,7 +303,6 @@ const Details: React.FC<DetailsProps> = ({
key={i}
t={t}
tokenMeta={txData.tokenMetas[t.token]}
metadatas={metadatas}
/>
))}
</InfoRow>

View File

@ -1,6 +1,6 @@
import React, { useMemo } from "react";
import React, { useContext, useMemo } from "react";
import { Log } from "@ethersproject/abstract-provider";
import { Fragment, Interface, LogDescription } from "@ethersproject/abi";
import { Fragment, Interface } from "@ethersproject/abi";
import { Tab } from "@headlessui/react";
import TransactionAddress from "../components/TransactionAddress";
import Copy from "../components/Copy";
@ -8,16 +8,41 @@ import ModeTab from "../components/ModeTab";
import DecodedParamsTable from "./decoder/DecodedParamsTable";
import DecodedLogSignature from "./decoder/DecodedLogSignature";
import { useTopic0 } from "../useTopic0";
import { ChecksummedAddress } from "../types";
import { Metadata } from "../sourcify/useSourcify";
import { RuntimeContext } from "../useRuntime";
import { useAppConfigContext } from "../useAppConfig";
import { useSourcifyMetadata } from "../sourcify/useSourcify";
type LogEntryProps = {
log: Log;
logDesc: LogDescription | null | undefined;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
};
const LogEntry: React.FC<LogEntryProps> = ({ log, logDesc, metadatas }) => {
const LogEntry: React.FC<LogEntryProps> = ({ log }) => {
const { provider } = useContext(RuntimeContext);
const { sourcifySource } = useAppConfigContext();
const metadata = useSourcifyMetadata(
log.address,
provider?.network.chainId,
sourcifySource
);
const logDesc = useMemo(() => {
if (!metadata) {
return metadata;
}
const abi = metadata.output.abi;
const intf = new Interface(abi as any);
try {
return intf.parseLog({
topics: log.topics,
data: log.data,
});
} catch (err) {
console.warn("Couldn't find function signature", err);
return null;
}
}, [log, metadata]);
const rawTopic0 = log.topics[0];
const topic0 = useTopic0(rawTopic0);
@ -56,10 +81,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ log, logDesc, metadatas }) => {
<div className="font-bold text-right">Address</div>
<div className="col-span-11 mr-auto">
<div className="flex items-baseline space-x-2 -ml-1 mr-3">
<TransactionAddress
address={log.address}
metadata={metadatas[log.address]}
/>
<TransactionAddress address={log.address} />
<Copy value={log.address} />
</div>
</div>

View File

@ -1,73 +1,20 @@
import React, { useContext, useMemo } from "react";
import { Interface } from "@ethersproject/abi";
import React from "react";
import ContentFrame from "../ContentFrame";
import LogEntry from "./LogEntry";
import { TransactionData } from "../types";
import { Metadata } from "../sourcify/useSourcify";
import { RuntimeContext } from "../useRuntime";
import { useContractsMetadata } from "../hooks";
type LogsProps = {
txData: TransactionData;
metadata: Metadata | null | undefined;
};
const Logs: React.FC<LogsProps> = ({ txData, metadata }) => {
const baseMetadatas = useMemo((): Record<string, Metadata | null> => {
if (!txData.to || metadata === undefined) {
return {};
}
const md: Record<string, Metadata | null> = {};
md[txData.to] = metadata;
return md;
}, [txData.to, metadata]);
const logAddresses = useMemo(
() => txData.confirmedData?.logs.map((l) => l.address) ?? [],
[txData]
);
const { provider } = useContext(RuntimeContext);
const metadatas = useContractsMetadata(logAddresses, provider, baseMetadatas);
const logDescs = useMemo(() => {
if (!txData) {
return undefined;
}
return txData.confirmedData?.logs.map((l) => {
const mt = metadatas[l.address];
if (!mt) {
return mt;
}
const abi = mt.output.abi;
const intf = new Interface(abi as any);
try {
return intf.parseLog({
topics: l.topics,
data: l.data,
});
} catch (err) {
console.warn("Couldn't find function signature", err);
return null;
}
});
}, [metadatas, txData]);
return (
const Logs: React.FC<LogsProps> = ({ txData }) => (
<ContentFrame tabs>
{txData.confirmedData && (
<>
{txData.confirmedData.logs.length > 0 ? (
<>
{txData.confirmedData.logs.map((l, i) => (
<LogEntry
key={i}
log={l}
logDesc={logDescs?.[i]}
metadatas={metadatas}
/>
<LogEntry key={i} log={l} />
))}
</>
) : (
@ -77,6 +24,5 @@ const Logs: React.FC<LogsProps> = ({ txData, metadata }) => {
)}
</ContentFrame>
);
};
export default React.memo(Logs);

View File

@ -53,6 +53,10 @@ const resolveSourcifySource = (source: SourcifySource) => {
throw new Error(`Unknown Sourcify integration source code: ${source}`);
};
/**
* Builds a complete Sourcify metadata.json URL given the contract address
* and chain.
*/
export const sourcifyMetadata = (
address: ChecksummedAddress,
chainId: number,

View File

@ -5,6 +5,7 @@ import {
TransactionDescription,
} from "@ethersproject/abi";
import { BigNumberish } from "@ethersproject/bignumber";
import { Fetcher } from "swr";
import useSWRImmutable from "swr/immutable";
import { RuntimeContext } from "./useRuntime";
import { fourBytesURL } from "./url";
@ -29,16 +30,32 @@ export const extract4Bytes = (rawInput: string): string | null => {
return rawInput.slice(0, 10);
};
const fetch4Bytes = async (
assetsURLPrefix: string,
fourBytes: string
): Promise<FourBytesEntry | null> => {
type FourBytesKey = [id: "4bytes", fourBytes: string];
type FourBytesFetcher = Fetcher<
FourBytesEntry | null | undefined,
FourBytesKey
>;
const fourBytesFetcher =
(assetsURLPrefix: string): FourBytesFetcher =>
async (_, key) => {
if (key === null || key === "0x") {
return undefined;
}
// Handle simple transfers with invalid selector like tx:
// 0x8bcbdcc1589b5c34c1e55909c8269a411f0267a4fed59a73dd4348cc71addbb9,
// which contains 0x00 as data
if (key.length !== 10) {
return undefined;
}
const fourBytes = key.slice(2);
const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes);
try {
const res = await fetch(signatureURL);
if (!res.ok) {
console.error(`Signature does not exist in 4bytes DB: ${fourBytes}`);
console.warn(`Signature does not exist in 4bytes DB: ${fourBytes}`);
return null;
}
@ -53,10 +70,6 @@ const fetch4Bytes = async (
signature: sig,
};
return entry;
} catch (err) {
console.error(`Couldn't fetch signature URL ${signatureURL}`, err);
return null;
}
};
/**
@ -75,26 +88,10 @@ export const use4Bytes = (
const { config } = useContext(RuntimeContext);
const assetsURLPrefix = config?.assetsURLPrefix;
const fourBytesKey = assetsURLPrefix !== undefined ? rawFourBytes : null;
const fourBytesFetcher = (key: string | null) => {
if (key === null || key === "0x") {
return undefined;
}
// Handle simple transfers with invalid selector like tx:
// 0x8bcbdcc1589b5c34c1e55909c8269a411f0267a4fed59a73dd4348cc71addbb9,
// which contains 0x00 as data
if (key.length !== 10) {
return undefined;
}
return fetch4Bytes(assetsURLPrefix!, key.slice(2));
};
const { data, error } = useSWRImmutable<FourBytesEntry | null | undefined>(
assetsURLPrefix !== undefined ? rawFourBytes : null,
fourBytesFetcher
);
const fetcher = fourBytesFetcher(assetsURLPrefix!);
const { data, error } = useSWRImmutable(["4bytes", fourBytesKey], fetcher);
return error ? undefined : data;
};

View File

@ -427,44 +427,6 @@ export const useTraceTransaction = (
return traceGroups;
};
const hasCode = async (
provider: JsonRpcProvider,
address: ChecksummedAddress
): Promise<boolean> => {
const result = await provider.send("ots_hasCode", [address, "latest"]);
return result as boolean;
};
export const useAddressesWithCode = (
provider: JsonRpcProvider | undefined,
addresses: ChecksummedAddress[]
): ChecksummedAddress[] | undefined => {
const [results, setResults] = useState<ChecksummedAddress[] | undefined>();
useEffect(() => {
// Reset
setResults(undefined);
if (provider === undefined) {
return;
}
const readCodes = async () => {
const checkers: Promise<boolean>[] = [];
for (const a of addresses) {
checkers.push(hasCode(provider, a));
}
const result = await Promise.all(checkers);
const filtered = addresses.filter((_, i) => result[i]);
setResults(filtered);
};
readCodes();
}, [provider, addresses]);
return results;
};
// Error(string)
const ERROR_MESSAGE_SELECTOR = "0x08c379a0";
@ -582,7 +544,6 @@ export const prefetchTransactionBySenderAndNonce = (
}
return getTransactionBySenderAndNonceFetcher(provider)(key);
});
// }
};
export const useTransactionBySenderAndNonce = (
@ -705,7 +666,6 @@ export const providerFetcher =
const method = key[0];
const args = key.slice(1);
const result = await provider.send(method, args);
// console.log(`providerFetcher: ${method} ${args} === ${result}`);
return result;
};

View File

@ -1,4 +1,5 @@
import { useState, useEffect, useContext } from "react";
import { useContext } from "react";
import useSWRImmutable from "swr/immutable";
import { RuntimeContext } from "./useRuntime";
import { topic0URL } from "./url";
@ -6,7 +7,28 @@ export type Topic0Entry = {
signatures: string[] | undefined;
};
const fullCache = new Map<string, Topic0Entry | null>();
const topic0Fetcher = async (
signatureURL: string
): Promise<Topic0Entry | null | undefined> => {
try {
const res = await fetch(signatureURL);
if (!res.ok) {
console.error(`Signature does not exist in topic0 DB: ${signatureURL}`);
return null;
}
// Get only the first occurrence, for now ignore alternative param names
const sig = await res.text();
const sigs = sig.split(";");
const entry: Topic0Entry = {
signatures: sigs,
};
return entry;
} catch (err) {
console.error(`Couldn't fetch signature URL ${signatureURL}`, err);
return null;
}
};
/**
* Extract topic0 DB info
@ -26,52 +48,11 @@ export const useTopic0 = (
const assetsURLPrefix = runtime.config?.assetsURLPrefix;
const topic0 = rawTopic0.slice(2);
const [entry, setEntry] = useState<Topic0Entry | null | undefined>(
fullCache.get(topic0)
);
useEffect(() => {
if (assetsURLPrefix === undefined) {
return;
const signatureURL = () =>
assetsURLPrefix === undefined ? null : topic0URL(assetsURLPrefix, topic0);
const { data, error } = useSWRImmutable(signatureURL, topic0Fetcher);
if (error) {
return null;
}
const signatureURL = topic0URL(assetsURLPrefix, topic0);
fetch(signatureURL)
.then(async (res) => {
if (!res.ok) {
console.error(`Signature does not exist in topic0 DB: ${topic0}`);
fullCache.set(topic0, null);
setEntry(null);
return;
}
// Get only the first occurrence, for now ignore alternative param names
const sig = await res.text();
const sigs = sig.split(";");
const entry: Topic0Entry = {
signatures: sigs,
};
setEntry(entry);
fullCache.set(topic0, entry);
})
.catch((err) => {
console.error(`Couldn't fetch signature URL ${signatureURL}`, err);
setEntry(null);
fullCache.set(topic0, null);
});
}, [topic0, assetsURLPrefix]);
if (assetsURLPrefix === undefined) {
return undefined;
}
// Try to resolve topic0 name
if (entry === null || entry === undefined) {
return entry;
}
// Simulates LRU
// TODO: implement LRU purging
fullCache.delete(topic0);
fullCache.set(topic0, entry);
return entry;
return data;
};