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 { 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}
/> />
} }
/> />

View File

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

View File

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

View File

@ -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 (

View File

@ -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}
/> />
)} )}
</> </>

View File

@ -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`}

View File

@ -3,7 +3,6 @@ import { Interface } from "@ethersproject/abi";
import { ErrorDescription } from "@ethersproject/abi/lib/interface"; import { ErrorDescription } from "@ethersproject/abi/lib/interface";
import useSWRImmutable from "swr/immutable"; import useSWRImmutable from "swr/immutable";
import { ChecksummedAddress, TransactionData } from "../types"; import { ChecksummedAddress, TransactionData } from "../types";
import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "../url";
import { useAppConfigContext } from "../useAppConfig"; import { useAppConfigContext } from "../useAppConfig";
export type UserMethod = { 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 { 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 +194,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 +222,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);

View File

@ -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;

View File

@ -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);

View File

@ -29,52 +29,5 @@ export const transactionURL = (txHash: string) => `/tx/${txHash}`;
export const addressByNonceURL = (address: ChecksummedAddress, nonce: number) => export const addressByNonceURL = (address: ChecksummedAddress, nonce: number) =>
`/address/${address}?nonce=${nonce}`; `/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) => 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}`;

View File

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