Merge branch 'feature/sourcify-partial-matches' into develop; fix #344
This commit is contained in:
commit
88c0b9f516
|
@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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`}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
47
src/url.ts
47
src/url.ts
|
@ -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}`;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext } from "react";
|
||||
import { SourcifySource } from "./url";
|
||||
import { SourcifySource } from "./sourcify/useSourcify";
|
||||
|
||||
export type AppConfig = {
|
||||
sourcifySource: SourcifySource;
|
||||
|
|
Loading…
Reference in New Issue