Merge branch 'feature/sourcify-partial-matches' into develop; fix #344

This commit is contained in:
Willian Mitsuda 2022-08-31 17:40:31 -03:00
commit 88c0b9f516
No known key found for this signature in database
11 changed files with 151 additions and 95 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

@ -2,7 +2,7 @@ import React, { useMemo, useState } from "react";
import { Outlet } from "react-router-dom";
import Header from "./Header";
import { AppConfig, AppConfigContext } from "./useAppConfig";
import { SourcifySource } from "./url";
import { SourcifySource } from "./sourcify/useSourcify";
const Main: React.FC = () => {
const [sourcifySource, setSourcifySource] = useState<SourcifySource>(

View File

@ -2,8 +2,8 @@ import React, { PropsWithChildren } from "react";
import { Menu } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBars } from "@fortawesome/free-solid-svg-icons/faBars";
import { SourcifySource } from "./url";
import { useAppConfigContext } from "./useAppConfig";
import { SourcifySource } from "./sourcify/useSourcify";
const SourcifyMenu: React.FC = () => {
const { sourcifySource, setSourcifySource } = useAppConfigContext();

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

@ -3,7 +3,6 @@ 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";
import { useAppConfigContext } from "../useAppConfig";
export type UserMethod = {
@ -82,16 +81,111 @@ export type Metadata = {
};
};
const sourcifyFetcher = async (url: string) => {
export enum SourcifySource {
// Resolve trusted IPNS for root IPFS
IPFS_IPNS,
// Centralized Sourcify servers
CENTRAL_SERVER,
}
const sourcifyIPNS =
"k51qzi5uqu5dll0ocge71eudqnrgnogmbr37gsgl12uubsinphjoknl6bbi41p";
const defaultIpfsGatewayPrefix = `https://ipfs.io/ipns/${sourcifyIPNS}`;
const sourcifyHttpRepoPrefix = `https://repo.sourcify.dev`;
const resolveSourcifySource = (source: SourcifySource) => {
if (source === SourcifySource.IPFS_IPNS) {
return defaultIpfsGatewayPrefix;
}
if (source === SourcifySource.CENTRAL_SERVER) {
return sourcifyHttpRepoPrefix;
}
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,
source: SourcifySource,
type: MatchType
) =>
`${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,
type: MatchType
) =>
`${resolveSourcifySource(source)}/contracts/${
type === MatchType.FULL_MATCH ? "full_match" : "partial_match"
}/${chainId}/${address}/sources/${filepath}`;
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 +194,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 +222,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

@ -29,52 +29,5 @@ export const transactionURL = (txHash: string) => `/tx/${txHash}`;
export const addressByNonceURL = (address: ChecksummedAddress, nonce: number) =>
`/address/${address}?nonce=${nonce}`;
export enum SourcifySource {
// Resolve trusted IPNS for root IPFS
IPFS_IPNS,
// Centralized Sourcify servers
CENTRAL_SERVER,
}
const sourcifyIPNS =
"k51qzi5uqu5dll0ocge71eudqnrgnogmbr37gsgl12uubsinphjoknl6bbi41p";
const defaultIpfsGatewayPrefix = `https://ipfs.io/ipns/${sourcifyIPNS}`;
const sourcifyHttpRepoPrefix = `https://repo.sourcify.dev`;
const resolveSourcifySource = (source: SourcifySource) => {
if (source === SourcifySource.IPFS_IPNS) {
return defaultIpfsGatewayPrefix;
}
if (source === SourcifySource.CENTRAL_SERVER) {
return sourcifyHttpRepoPrefix;
}
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,
source: SourcifySource
) =>
`${resolveSourcifySource(
source
)}/contracts/full_match/${chainId}/${address}/metadata.json`;
export const sourcifySourceFile = (
address: ChecksummedAddress,
chainId: number,
filepath: string,
source: SourcifySource
) =>
`${resolveSourcifySource(
source
)}/contracts/full_match/${chainId}/${address}/sources/${filepath}`;
export const openInRemixURL = (checksummedAddress: string, networkId: number) =>
`https://remix.ethereum.org/#activate=sourcify&call=sourcify//fetchAndSave//${checksummedAddress}//${networkId}`;

View File

@ -1,5 +1,5 @@
import React, { useContext } from "react";
import { SourcifySource } from "./url";
import { SourcifySource } from "./sourcify/useSourcify";
export type AppConfig = {
sourcifySource: SourcifySource;