Add support for partial match on Sourcify
This commit is contained in:
parent
94146d9004
commit
971554fe82
|
@ -55,7 +55,7 @@ const AddressMainPage: React.FC<AddressMainPageProps> = ({}) => {
|
||||||
|
|
||||||
const { provider } = useContext(RuntimeContext);
|
const { provider } = useContext(RuntimeContext);
|
||||||
const hasCode = useHasCode(provider, checksummedAddress, "latest");
|
const hasCode = useHasCode(provider, checksummedAddress, "latest");
|
||||||
const addressMetadata = useSourcifyMetadata(
|
const match = useSourcifyMetadata(
|
||||||
hasCode ? checksummedAddress : undefined,
|
hasCode ? checksummedAddress : undefined,
|
||||||
provider?.network.chainId
|
provider?.network.chainId
|
||||||
);
|
);
|
||||||
|
@ -110,18 +110,18 @@ const AddressMainPage: React.FC<AddressMainPageProps> = ({}) => {
|
||||||
<NavTab href={`/address/${addressOrName}/contract`}>
|
<NavTab href={`/address/${addressOrName}/contract`}>
|
||||||
<span
|
<span
|
||||||
className={`flex items-baseline space-x-2 ${
|
className={`flex items-baseline space-x-2 ${
|
||||||
addressMetadata === undefined ? "italic opacity-50" : ""
|
match === undefined ? "italic opacity-50" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>Contract</span>
|
<span>Contract</span>
|
||||||
{addressMetadata === undefined ? (
|
{match === undefined ? (
|
||||||
<span className="self-center">
|
<span className="self-center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="animate-spin"
|
className="animate-spin"
|
||||||
icon={faCircleNotch}
|
icon={faCircleNotch}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
) : addressMetadata === null ? (
|
) : match === null ? (
|
||||||
<span className="self-center text-red-500">
|
<span className="self-center text-red-500">
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} />
|
<FontAwesomeIcon icon={faQuestionCircle} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -153,7 +153,7 @@ const AddressMainPage: React.FC<AddressMainPageProps> = ({}) => {
|
||||||
element={
|
element={
|
||||||
<Contracts
|
<Contracts
|
||||||
checksummedAddress={checksummedAddress}
|
checksummedAddress={checksummedAddress}
|
||||||
rawMetadata={addressMetadata}
|
match={match}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SyntaxHighlighter, docco } from "../highlight-init";
|
import { SyntaxHighlighter, docco } from "../highlight-init";
|
||||||
import { useContract } from "../sourcify/useSourcify";
|
import { MatchType, useContract } from "../sourcify/useSourcify";
|
||||||
import { useAppConfigContext } from "../useAppConfig";
|
import { useAppConfigContext } from "../useAppConfig";
|
||||||
|
|
||||||
type ContractFromRepoProps = {
|
type ContractFromRepoProps = {
|
||||||
checksummedAddress: string;
|
checksummedAddress: string;
|
||||||
networkId: number;
|
networkId: number;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
type: MatchType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContractFromRepo: React.FC<ContractFromRepoProps> = ({
|
const ContractFromRepo: React.FC<ContractFromRepoProps> = ({
|
||||||
checksummedAddress,
|
checksummedAddress,
|
||||||
networkId,
|
networkId,
|
||||||
filename,
|
filename,
|
||||||
|
type,
|
||||||
}) => {
|
}) => {
|
||||||
const { sourcifySource } = useAppConfigContext();
|
const { sourcifySource } = useAppConfigContext();
|
||||||
const content = useContract(
|
const content = useContract(
|
||||||
checksummedAddress,
|
checksummedAddress,
|
||||||
networkId,
|
networkId,
|
||||||
filename,
|
filename,
|
||||||
sourcifySource
|
sourcifySource,
|
||||||
|
type
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,39 +8,39 @@ import InfoRow from "../components/InfoRow";
|
||||||
import Contract from "./Contract";
|
import Contract from "./Contract";
|
||||||
import ContractFromRepo from "./ContractFromRepo";
|
import ContractFromRepo from "./ContractFromRepo";
|
||||||
import { RuntimeContext } from "../useRuntime";
|
import { RuntimeContext } from "../useRuntime";
|
||||||
import { Metadata } from "../sourcify/useSourcify";
|
import { Match, MatchType } from "../sourcify/useSourcify";
|
||||||
import ExternalLink from "../components/ExternalLink";
|
import ExternalLink from "../components/ExternalLink";
|
||||||
import { openInRemixURL } from "../url";
|
import { openInRemixURL } from "../url";
|
||||||
import ContractABI from "./ContractABI";
|
import ContractABI from "./ContractABI";
|
||||||
|
|
||||||
type ContractsProps = {
|
type ContractsProps = {
|
||||||
checksummedAddress: string;
|
checksummedAddress: string;
|
||||||
rawMetadata: Metadata | null | undefined;
|
match: Match | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Contracts: React.FC<ContractsProps> = ({
|
const Contracts: React.FC<ContractsProps> = ({ checksummedAddress, match }) => {
|
||||||
checksummedAddress,
|
|
||||||
rawMetadata,
|
|
||||||
}) => {
|
|
||||||
const { provider } = useContext(RuntimeContext);
|
const { provider } = useContext(RuntimeContext);
|
||||||
|
|
||||||
const [selected, setSelected] = useState<string>();
|
const [selected, setSelected] = useState<string>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rawMetadata) {
|
if (match) {
|
||||||
setSelected(Object.keys(rawMetadata.sources)[0]);
|
setSelected(Object.keys(match.metadata.sources)[0]);
|
||||||
}
|
}
|
||||||
}, [rawMetadata]);
|
}, [match]);
|
||||||
const optimizer = rawMetadata?.settings?.optimizer;
|
const optimizer = match?.metadata.settings?.optimizer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentFrame tabs>
|
<ContentFrame tabs>
|
||||||
{rawMetadata && (
|
{match && (
|
||||||
<>
|
<>
|
||||||
|
<InfoRow title="Match">
|
||||||
|
{match.type === MatchType.FULL_MATCH ? "Full" : "Partial"}
|
||||||
|
</InfoRow>
|
||||||
<InfoRow title="Language">
|
<InfoRow title="Language">
|
||||||
<span>{rawMetadata.language}</span>
|
<span>{match.metadata.language}</span>
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
<InfoRow title="Compiler">
|
<InfoRow title="Compiler">
|
||||||
<span>{rawMetadata.compiler.version}</span>
|
<span>{match.metadata.compiler.version}</span>
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
<InfoRow title="Optimizer Enabled">
|
<InfoRow title="Optimizer Enabled">
|
||||||
{optimizer?.enabled ? (
|
{optimizer?.enabled ? (
|
||||||
|
@ -58,19 +58,19 @@ const Contracts: React.FC<ContractsProps> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="py-5">
|
<div className="py-5">
|
||||||
{rawMetadata === undefined && (
|
{match === undefined && (
|
||||||
<span>Getting data from Sourcify repository...</span>
|
<span>Getting data from Sourcify repository...</span>
|
||||||
)}
|
)}
|
||||||
{rawMetadata === null && (
|
{match === null && (
|
||||||
<span>
|
<span>
|
||||||
Address is not a contract or couldn't find contract metadata in
|
Address is not a contract or couldn't find contract metadata in
|
||||||
Sourcify repository.
|
Sourcify repository.
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{rawMetadata !== undefined && rawMetadata !== null && (
|
{match !== undefined && match !== null && (
|
||||||
<>
|
<>
|
||||||
{rawMetadata.output.abi && (
|
{match.metadata.output.abi && (
|
||||||
<ContractABI abi={rawMetadata.output.abi} />
|
<ContractABI abi={match.metadata.output.abi} />
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Menu>
|
<Menu>
|
||||||
|
@ -96,7 +96,7 @@ const Contracts: React.FC<ContractsProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Menu.Items className="absolute border p-1 rounded-b bg-white flex flex-col">
|
<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}>
|
<Menu.Item key={k}>
|
||||||
<button
|
<button
|
||||||
className={`flex text-sm px-2 py-1 ${
|
className={`flex text-sm px-2 py-1 ${
|
||||||
|
@ -115,13 +115,16 @@ const Contracts: React.FC<ContractsProps> = ({
|
||||||
</Menu>
|
</Menu>
|
||||||
{selected && (
|
{selected && (
|
||||||
<>
|
<>
|
||||||
{rawMetadata.sources[selected].content ? (
|
{match.metadata.sources[selected].content ? (
|
||||||
<Contract content={rawMetadata.sources[selected].content} />
|
<Contract
|
||||||
|
content={match.metadata.sources[selected].content}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ContractFromRepo
|
<ContractFromRepo
|
||||||
checksummedAddress={checksummedAddress}
|
checksummedAddress={checksummedAddress}
|
||||||
networkId={provider!.network.chainId}
|
networkId={provider!.network.chainId}
|
||||||
filename={selected}
|
filename={selected}
|
||||||
|
type={match.type}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -38,7 +38,7 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
|
||||||
eoa,
|
eoa,
|
||||||
}) => {
|
}) => {
|
||||||
const { provider } = useContext(RuntimeContext);
|
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 mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS;
|
||||||
const burn = addressCtx === AddressContext.TO && 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" />
|
<FontAwesomeIcon icon={faCoins} size="1x" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{metadata && (
|
{match && (
|
||||||
<NavLink
|
<NavLink
|
||||||
className="self-center shrink-0 flex items-center"
|
className="self-center shrink-0 flex items-center"
|
||||||
to={`/address/${address}/contract`}
|
to={`/address/${address}/contract`}
|
||||||
|
|
|
@ -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 {
|
try {
|
||||||
|
const url = sourcifyMetadata(
|
||||||
|
address,
|
||||||
|
chainId,
|
||||||
|
sourcifySource,
|
||||||
|
MatchType.FULL_MATCH
|
||||||
|
);
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (res.ok) {
|
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;
|
return null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -100,13 +146,13 @@ const sourcifyFetcher = async (url: string) => {
|
||||||
export const useSourcifyMetadata = (
|
export const useSourcifyMetadata = (
|
||||||
address: ChecksummedAddress | undefined,
|
address: ChecksummedAddress | undefined,
|
||||||
chainId: number | undefined
|
chainId: number | undefined
|
||||||
): Metadata | null | undefined => {
|
): Match | null | undefined => {
|
||||||
const { sourcifySource } = useAppConfigContext();
|
const { sourcifySource } = useAppConfigContext();
|
||||||
const metadataURL = () =>
|
const metadataURL = () =>
|
||||||
address === undefined || chainId === undefined
|
address === undefined || chainId === undefined
|
||||||
? null
|
? null
|
||||||
: sourcifyMetadata(address, chainId, sourcifySource);
|
: ["sourcify", address, chainId, sourcifySource];
|
||||||
const { data, error } = useSWRImmutable<Metadata>(
|
const { data, error } = useSWRImmutable<Match | null | undefined>(
|
||||||
metadataURL,
|
metadataURL,
|
||||||
sourcifyFetcher
|
sourcifyFetcher
|
||||||
);
|
);
|
||||||
|
@ -128,14 +174,16 @@ export const useContract = (
|
||||||
checksummedAddress: string,
|
checksummedAddress: string,
|
||||||
networkId: number,
|
networkId: number,
|
||||||
filename: string,
|
filename: string,
|
||||||
sourcifySource: SourcifySource
|
sourcifySource: SourcifySource,
|
||||||
|
type: MatchType
|
||||||
) => {
|
) => {
|
||||||
const normalizedFilename = filename.replaceAll(/[@:]/g, "_");
|
const normalizedFilename = filename.replaceAll(/[@:]/g, "_");
|
||||||
const url = sourcifySourceFile(
|
const url = sourcifySourceFile(
|
||||||
checksummedAddress,
|
checksummedAddress,
|
||||||
networkId,
|
networkId,
|
||||||
normalizedFilename,
|
normalizedFilename,
|
||||||
sourcifySource
|
sourcifySource,
|
||||||
|
type
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, error } = useSWRImmutable(url, contractFetcher);
|
const { data, error } = useSWRImmutable(url, contractFetcher);
|
||||||
|
|
|
@ -80,7 +80,8 @@ const Details: React.FC<DetailsProps> = ({ txData }) => {
|
||||||
|
|
||||||
const tokenTransfers = useTokenTransfers(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 txDesc = useSourcifyTransactionDescription(metadata, txData);
|
||||||
const userDoc = metadata?.output.userdoc;
|
const userDoc = metadata?.output.userdoc;
|
||||||
|
|
|
@ -17,14 +17,14 @@ type LogEntryProps = {
|
||||||
|
|
||||||
const LogEntry: React.FC<LogEntryProps> = ({ log }) => {
|
const LogEntry: React.FC<LogEntryProps> = ({ log }) => {
|
||||||
const { provider } = useContext(RuntimeContext);
|
const { provider } = useContext(RuntimeContext);
|
||||||
const metadata = useSourcifyMetadata(log.address, provider?.network.chainId);
|
const match = useSourcifyMetadata(log.address, provider?.network.chainId);
|
||||||
|
|
||||||
const logDesc = useMemo(() => {
|
const logDesc = useMemo(() => {
|
||||||
if (!metadata) {
|
if (!match) {
|
||||||
return metadata;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
const abi = metadata.output.abi;
|
const abi = match.metadata.output.abi;
|
||||||
const intf = new Interface(abi as any);
|
const intf = new Interface(abi as any);
|
||||||
try {
|
try {
|
||||||
return intf.parseLog({
|
return intf.parseLog({
|
||||||
|
@ -35,7 +35,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ log }) => {
|
||||||
console.warn("Couldn't find function signature", err);
|
console.warn("Couldn't find function signature", err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [log, metadata]);
|
}, [log, match]);
|
||||||
|
|
||||||
const rawTopic0 = log.topics[0];
|
const rawTopic0 = log.topics[0];
|
||||||
const topic0 = useTopic0(rawTopic0);
|
const topic0 = useTopic0(rawTopic0);
|
||||||
|
|
19
src/url.ts
19
src/url.ts
|
@ -1,4 +1,5 @@
|
||||||
import { BlockTag } from "@ethersproject/abstract-provider";
|
import { BlockTag } from "@ethersproject/abstract-provider";
|
||||||
|
import { MatchType } from "./sourcify/useSourcify";
|
||||||
import { ChecksummedAddress } from "./types";
|
import { ChecksummedAddress } from "./types";
|
||||||
|
|
||||||
export const fourBytesURL = (
|
export const fourBytesURL = (
|
||||||
|
@ -60,21 +61,23 @@ const resolveSourcifySource = (source: SourcifySource) => {
|
||||||
export const sourcifyMetadata = (
|
export const sourcifyMetadata = (
|
||||||
address: ChecksummedAddress,
|
address: ChecksummedAddress,
|
||||||
chainId: number,
|
chainId: number,
|
||||||
source: SourcifySource
|
source: SourcifySource,
|
||||||
|
type: MatchType
|
||||||
) =>
|
) =>
|
||||||
`${resolveSourcifySource(
|
`${resolveSourcifySource(source)}/contracts/${
|
||||||
source
|
type === MatchType.FULL_MATCH ? "full_match" : "partial_match"
|
||||||
)}/contracts/full_match/${chainId}/${address}/metadata.json`;
|
}/${chainId}/${address}/metadata.json`;
|
||||||
|
|
||||||
export const sourcifySourceFile = (
|
export const sourcifySourceFile = (
|
||||||
address: ChecksummedAddress,
|
address: ChecksummedAddress,
|
||||||
chainId: number,
|
chainId: number,
|
||||||
filepath: string,
|
filepath: string,
|
||||||
source: SourcifySource
|
source: SourcifySource,
|
||||||
|
type: MatchType
|
||||||
) =>
|
) =>
|
||||||
`${resolveSourcifySource(
|
`${resolveSourcifySource(source)}/contracts/${
|
||||||
source
|
type === MatchType.FULL_MATCH ? "full_match" : "partial_match"
|
||||||
)}/contracts/full_match/${chainId}/${address}/sources/${filepath}`;
|
}/${chainId}/${address}/sources/${filepath}`;
|
||||||
|
|
||||||
export const openInRemixURL = (checksummedAddress: string, networkId: number) =>
|
export const openInRemixURL = (checksummedAddress: string, networkId: number) =>
|
||||||
`https://remix.ethereum.org/#activate=sourcify&call=sourcify//fetchAndSave//${checksummedAddress}//${networkId}`;
|
`https://remix.ethereum.org/#activate=sourcify&call=sourcify//fetchAndSave//${checksummedAddress}//${networkId}`;
|
||||||
|
|
Loading…
Reference in New Issue