Add support for partial match on Sourcify

This commit is contained in:
Willian Mitsuda 2022-08-31 17:20:02 -03:00
parent 94146d9004
commit 971554fe82
No known key found for this signature in database
8 changed files with 110 additions and 52 deletions

View File

@ -55,7 +55,7 @@ const AddressMainPage: React.FC<AddressMainPageProps> = ({}) => {
const { provider } = useContext(RuntimeContext);
const hasCode = useHasCode(provider, checksummedAddress, "latest");
const addressMetadata = useSourcifyMetadata(
const match = useSourcifyMetadata(
hasCode ? checksummedAddress : undefined,
provider?.network.chainId
);
@ -110,18 +110,18 @@ const AddressMainPage: React.FC<AddressMainPageProps> = ({}) => {
<NavTab href={`/address/${addressOrName}/contract`}>
<span
className={`flex items-baseline space-x-2 ${
addressMetadata === undefined ? "italic opacity-50" : ""
match === undefined ? "italic opacity-50" : ""
}`}
>
<span>Contract</span>
{addressMetadata === undefined ? (
{match === undefined ? (
<span className="self-center">
<FontAwesomeIcon
className="animate-spin"
icon={faCircleNotch}
/>
</span>
) : addressMetadata === null ? (
) : match === null ? (
<span className="self-center text-red-500">
<FontAwesomeIcon icon={faQuestionCircle} />
</span>
@ -153,7 +153,7 @@ const AddressMainPage: React.FC<AddressMainPageProps> = ({}) => {
element={
<Contracts
checksummedAddress={checksummedAddress}
rawMetadata={addressMetadata}
match={match}
/>
}
/>

View File

@ -1,25 +1,28 @@
import React from "react";
import { SyntaxHighlighter, docco } from "../highlight-init";
import { useContract } from "../sourcify/useSourcify";
import { MatchType, useContract } from "../sourcify/useSourcify";
import { useAppConfigContext } from "../useAppConfig";
type ContractFromRepoProps = {
checksummedAddress: string;
networkId: number;
filename: string;
type: MatchType;
};
const ContractFromRepo: React.FC<ContractFromRepoProps> = ({
checksummedAddress,
networkId,
filename,
type,
}) => {
const { sourcifySource } = useAppConfigContext();
const content = useContract(
checksummedAddress,
networkId,
filename,
sourcifySource
sourcifySource,
type
);
return (

View File

@ -8,39 +8,39 @@ import InfoRow from "../components/InfoRow";
import Contract from "./Contract";
import ContractFromRepo from "./ContractFromRepo";
import { RuntimeContext } from "../useRuntime";
import { Metadata } from "../sourcify/useSourcify";
import { Match, MatchType } from "../sourcify/useSourcify";
import ExternalLink from "../components/ExternalLink";
import { openInRemixURL } from "../url";
import ContractABI from "./ContractABI";
type ContractsProps = {
checksummedAddress: string;
rawMetadata: Metadata | null | undefined;
match: Match | null | undefined;
};
const Contracts: React.FC<ContractsProps> = ({
checksummedAddress,
rawMetadata,
}) => {
const Contracts: React.FC<ContractsProps> = ({ checksummedAddress, match }) => {
const { provider } = useContext(RuntimeContext);
const [selected, setSelected] = useState<string>();
useEffect(() => {
if (rawMetadata) {
setSelected(Object.keys(rawMetadata.sources)[0]);
if (match) {
setSelected(Object.keys(match.metadata.sources)[0]);
}
}, [rawMetadata]);
const optimizer = rawMetadata?.settings?.optimizer;
}, [match]);
const optimizer = match?.metadata.settings?.optimizer;
return (
<ContentFrame tabs>
{rawMetadata && (
{match && (
<>
<InfoRow title="Match">
{match.type === MatchType.FULL_MATCH ? "Full" : "Partial"}
</InfoRow>
<InfoRow title="Language">
<span>{rawMetadata.language}</span>
<span>{match.metadata.language}</span>
</InfoRow>
<InfoRow title="Compiler">
<span>{rawMetadata.compiler.version}</span>
<span>{match.metadata.compiler.version}</span>
</InfoRow>
<InfoRow title="Optimizer Enabled">
{optimizer?.enabled ? (
@ -58,19 +58,19 @@ const Contracts: React.FC<ContractsProps> = ({
</>
)}
<div className="py-5">
{rawMetadata === undefined && (
{match === undefined && (
<span>Getting data from Sourcify repository...</span>
)}
{rawMetadata === null && (
{match === null && (
<span>
Address is not a contract or couldn't find contract metadata in
Sourcify repository.
</span>
)}
{rawMetadata !== undefined && rawMetadata !== null && (
{match !== undefined && match !== null && (
<>
{rawMetadata.output.abi && (
<ContractABI abi={rawMetadata.output.abi} />
{match.metadata.output.abi && (
<ContractABI abi={match.metadata.output.abi} />
)}
<div>
<Menu>
@ -96,7 +96,7 @@ const Contracts: React.FC<ContractsProps> = ({
</div>
<div className="relative">
<Menu.Items className="absolute border p-1 rounded-b bg-white flex flex-col">
{Object.entries(rawMetadata.sources).map(([k]) => (
{Object.entries(match.metadata.sources).map(([k]) => (
<Menu.Item key={k}>
<button
className={`flex text-sm px-2 py-1 ${
@ -115,13 +115,16 @@ const Contracts: React.FC<ContractsProps> = ({
</Menu>
{selected && (
<>
{rawMetadata.sources[selected].content ? (
<Contract content={rawMetadata.sources[selected].content} />
{match.metadata.sources[selected].content ? (
<Contract
content={match.metadata.sources[selected].content}
/>
) : (
<ContractFromRepo
checksummedAddress={checksummedAddress}
networkId={provider!.network.chainId}
filename={selected}
type={match.type}
/>
)}
</>

View File

@ -38,7 +38,7 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
eoa,
}) => {
const { provider } = useContext(RuntimeContext);
const metadata = useSourcifyMetadata(address, provider?.network.chainId);
const match = useSourcifyMetadata(address, provider?.network.chainId);
const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS;
const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS;
@ -78,7 +78,7 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
<FontAwesomeIcon icon={faCoins} size="1x" />
</span>
)}
{metadata && (
{match && (
<NavLink
className="self-center shrink-0 flex items-center"
to={`/address/${address}/contract`}

View File

@ -82,16 +82,62 @@ export type Metadata = {
};
};
const sourcifyFetcher = async (url: string) => {
export enum MatchType {
FULL_MATCH,
PARTIAL_MATCH,
}
export type Match = {
type: MatchType;
metadata: Metadata;
};
const sourcifyFetcher = async (
_: "sourcify",
address: ChecksummedAddress,
chainId: number,
sourcifySource: SourcifySource
): Promise<Match | null | undefined> => {
// Try full match
try {
const url = sourcifyMetadata(
address,
chainId,
sourcifySource,
MatchType.FULL_MATCH
);
const res = await fetch(url);
if (res.ok) {
return res.json();
return {
type: MatchType.FULL_MATCH,
metadata: await res.json(),
};
}
} catch (err) {
console.info(
`error while getting Sourcify full_match metadata: chainId=${chainId} address=${address} err=${err}; falling back to partial_match`
);
}
// Fallback to try partial match
try {
const url = sourcifyMetadata(
address,
chainId,
sourcifySource,
MatchType.PARTIAL_MATCH
);
const res = await fetch(url);
if (res.ok) {
return {
type: MatchType.PARTIAL_MATCH,
metadata: await res.json(),
};
}
return null;
} catch (err) {
console.warn(
`error while getting Sourcify metadata: url=${url} err=${err}`
`error while getting Sourcify partial_match metadata: chainId=${chainId} address=${address} err=${err}`
);
return null;
}
@ -100,13 +146,13 @@ const sourcifyFetcher = async (url: string) => {
export const useSourcifyMetadata = (
address: ChecksummedAddress | undefined,
chainId: number | undefined
): Metadata | null | undefined => {
): Match | null | undefined => {
const { sourcifySource } = useAppConfigContext();
const metadataURL = () =>
address === undefined || chainId === undefined
? null
: sourcifyMetadata(address, chainId, sourcifySource);
const { data, error } = useSWRImmutable<Metadata>(
: ["sourcify", address, chainId, sourcifySource];
const { data, error } = useSWRImmutable<Match | null | undefined>(
metadataURL,
sourcifyFetcher
);
@ -128,14 +174,16 @@ export const useContract = (
checksummedAddress: string,
networkId: number,
filename: string,
sourcifySource: SourcifySource
sourcifySource: SourcifySource,
type: MatchType
) => {
const normalizedFilename = filename.replaceAll(/[@:]/g, "_");
const url = sourcifySourceFile(
checksummedAddress,
networkId,
normalizedFilename,
sourcifySource
sourcifySource,
type
);
const { data, error } = useSWRImmutable(url, contractFetcher);

View File

@ -80,7 +80,8 @@ const Details: React.FC<DetailsProps> = ({ txData }) => {
const tokenTransfers = useTokenTransfers(txData);
const metadata = useSourcifyMetadata(txData?.to, provider?.network.chainId);
const match = useSourcifyMetadata(txData?.to, provider?.network.chainId);
const metadata = match?.metadata;
const txDesc = useSourcifyTransactionDescription(metadata, txData);
const userDoc = metadata?.output.userdoc;

View File

@ -17,14 +17,14 @@ type LogEntryProps = {
const LogEntry: React.FC<LogEntryProps> = ({ log }) => {
const { provider } = useContext(RuntimeContext);
const metadata = useSourcifyMetadata(log.address, provider?.network.chainId);
const match = useSourcifyMetadata(log.address, provider?.network.chainId);
const logDesc = useMemo(() => {
if (!metadata) {
return metadata;
if (!match) {
return match;
}
const abi = metadata.output.abi;
const abi = match.metadata.output.abi;
const intf = new Interface(abi as any);
try {
return intf.parseLog({
@ -35,7 +35,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ log }) => {
console.warn("Couldn't find function signature", err);
return null;
}
}, [log, metadata]);
}, [log, match]);
const rawTopic0 = log.topics[0];
const topic0 = useTopic0(rawTopic0);

View File

@ -1,4 +1,5 @@
import { BlockTag } from "@ethersproject/abstract-provider";
import { MatchType } from "./sourcify/useSourcify";
import { ChecksummedAddress } from "./types";
export const fourBytesURL = (
@ -60,21 +61,23 @@ const resolveSourcifySource = (source: SourcifySource) => {
export const sourcifyMetadata = (
address: ChecksummedAddress,
chainId: number,
source: SourcifySource
source: SourcifySource,
type: MatchType
) =>
`${resolveSourcifySource(
source
)}/contracts/full_match/${chainId}/${address}/metadata.json`;
`${resolveSourcifySource(source)}/contracts/${
type === MatchType.FULL_MATCH ? "full_match" : "partial_match"
}/${chainId}/${address}/metadata.json`;
export const sourcifySourceFile = (
address: ChecksummedAddress,
chainId: number,
filepath: string,
source: SourcifySource
source: SourcifySource,
type: MatchType
) =>
`${resolveSourcifySource(
source
)}/contracts/full_match/${chainId}/${address}/sources/${filepath}`;
`${resolveSourcifySource(source)}/contracts/${
type === MatchType.FULL_MATCH ? "full_match" : "partial_match"
}/${chainId}/${address}/sources/${filepath}`;
export const openInRemixURL = (checksummedAddress: string, networkId: number) =>
`https://remix.ethereum.org/#activate=sourcify&call=sourcify//fetchAndSave//${checksummedAddress}//${networkId}`;