Merge branch 'develop' into feature/trace-tx
This commit is contained in:
commit
d2ae5f07fd
14
package-lock.json
generated
14
package-lock.json
generated
@ -41,7 +41,7 @@
|
||||
"react-blockies": "^1.4.1",
|
||||
"react-chartjs-2": "^3.3.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-boundary": "^3.1.3",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-helmet-async": "^1.1.2",
|
||||
"react-image": "^4.0.3",
|
||||
"react-router-dom": "^5.3.0",
|
||||
@ -14621,9 +14621,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-error-boundary": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.3.tgz",
|
||||
"integrity": "sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
|
||||
"integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
},
|
||||
@ -29543,9 +29543,9 @@
|
||||
}
|
||||
},
|
||||
"react-error-boundary": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.3.tgz",
|
||||
"integrity": "sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
|
||||
"integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
||||
"react-blockies": "^1.4.1",
|
||||
"react-chartjs-2": "^3.3.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-boundary": "^3.1.3",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-helmet-async": "^1.1.2",
|
||||
"react-image": "^4.0.3",
|
||||
"react-router-dom": "^5.3.0",
|
||||
|
@ -26,7 +26,7 @@ import PendingResults from "./search/PendingResults";
|
||||
import TransactionItem from "./search/TransactionItem";
|
||||
import { SearchController } from "./search/search";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import { useENSCache } from "./useReverseCache";
|
||||
import { pageCollector, useResolvedAddresses } from "./useResolvedAddresses";
|
||||
import { useFeeToggler } from "./search/useFeeToggler";
|
||||
import { SelectionContext, useSelection } from "./useSelection";
|
||||
import { useMultipleETHUSDOracle } from "./usePriceOracle";
|
||||
@ -165,7 +165,8 @@ const AddressTransactions: React.FC = () => {
|
||||
}, [provider, checksummedAddress, params.direction, hash, controller]);
|
||||
|
||||
const page = useMemo(() => controller?.getPage(), [controller]);
|
||||
const reverseCache = useENSCache(provider, page);
|
||||
const addrCollector = useMemo(() => pageCollector(page), [page]);
|
||||
const resolvedAddresses = useResolvedAddresses(provider, addrCollector);
|
||||
|
||||
const blockTags: BlockTag[] = useMemo(() => {
|
||||
if (!page) {
|
||||
@ -277,13 +278,13 @@ const AddressTransactions: React.FC = () => {
|
||||
feeDisplay={feeDisplay}
|
||||
feeDisplayToggler={feeDisplayToggler}
|
||||
/>
|
||||
{controller ? (
|
||||
{page ? (
|
||||
<SelectionContext.Provider value={selectionCtx}>
|
||||
{controller.getPage().map((tx) => (
|
||||
{page.map((tx) => (
|
||||
<TransactionItem
|
||||
key={tx.hash}
|
||||
tx={tx}
|
||||
ensCache={reverseCache}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
selectedAddress={checksummedAddress}
|
||||
feeDisplay={feeDisplay}
|
||||
priceMap={priceMap}
|
||||
|
@ -11,11 +11,13 @@ import {
|
||||
TokenTransfer,
|
||||
TransactionData,
|
||||
} from "./types";
|
||||
import { ResolvedAddresses } from "./api/address-resolver";
|
||||
|
||||
type TokenTransferItemProps = {
|
||||
t: TokenTransfer;
|
||||
txData: TransactionData;
|
||||
tokenMeta: TokenMeta | null | undefined;
|
||||
tokenMeta?: TokenMeta | null | undefined;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
// TODO: handle partial
|
||||
@ -23,6 +25,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
|
||||
t,
|
||||
txData,
|
||||
tokenMeta,
|
||||
resolvedAddresses,
|
||||
}) => (
|
||||
<div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100">
|
||||
<span className="text-gray-500">
|
||||
@ -64,10 +67,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
|
||||
<AddressHighlighter address={t.token}>
|
||||
<DecoratedAddressLink
|
||||
address={t.token}
|
||||
text={
|
||||
tokenMeta ? `${tokenMeta.name} (${tokenMeta.symbol})` : undefined
|
||||
}
|
||||
tokenMeta={tokenMeta}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
</div>
|
||||
|
@ -11,6 +11,10 @@ import { useInternalOperations, useTxData } from "./useErigonHooks";
|
||||
import { useETHUSDOracle } from "./usePriceOracle";
|
||||
import { useAppConfigContext } from "./useAppConfig";
|
||||
import { useSourcify, useTransactionDescription } from "./useSourcify";
|
||||
import {
|
||||
transactionDataCollector,
|
||||
useResolvedAddresses,
|
||||
} from "./useResolvedAddresses";
|
||||
|
||||
const Details = React.lazy(
|
||||
() =>
|
||||
@ -42,6 +46,11 @@ const Transaction: React.FC = () => {
|
||||
const { txhash } = params;
|
||||
|
||||
const txData = useTxData(provider, txhash);
|
||||
const addrCollector = useMemo(
|
||||
() => transactionDataCollector(txData),
|
||||
[txData]
|
||||
);
|
||||
const resolvedAddresses = useResolvedAddresses(provider, addrCollector);
|
||||
|
||||
const internalOps = useInternalOperations(provider, txData);
|
||||
const sendsEthToMiner = useMemo(() => {
|
||||
@ -107,10 +116,15 @@ const Transaction: React.FC = () => {
|
||||
internalOps={internalOps}
|
||||
sendsEthToMiner={sendsEthToMiner}
|
||||
ethUSDPrice={blockETHUSDPrice}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/tx/:txhash/logs/" exact>
|
||||
<Logs txData={txData} metadata={metadata} />
|
||||
<Logs
|
||||
txData={txData}
|
||||
metadata={metadata}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/tx/:txhash/trace" exact>
|
||||
<Trace txData={txData} />
|
||||
|
28
src/api/address-resolver/CompositeAddressResolver.ts
Normal file
28
src/api/address-resolver/CompositeAddressResolver.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { BaseProvider } from "@ethersproject/providers";
|
||||
import { IAddressResolver } from "./address-resolver";
|
||||
|
||||
export type SelectedResolvedName<T> = [IAddressResolver<T>, T] | null;
|
||||
|
||||
export class CompositeAddressResolver<T = any>
|
||||
implements IAddressResolver<SelectedResolvedName<T>>
|
||||
{
|
||||
private resolvers: IAddressResolver<T>[] = [];
|
||||
|
||||
addResolver(resolver: IAddressResolver<T>) {
|
||||
this.resolvers.push(resolver);
|
||||
}
|
||||
|
||||
async resolveAddress(
|
||||
provider: BaseProvider,
|
||||
address: string
|
||||
): Promise<SelectedResolvedName<T> | undefined> {
|
||||
for (const r of this.resolvers) {
|
||||
const resolvedAddress = await r.resolveAddress(provider, address);
|
||||
if (resolvedAddress !== undefined) {
|
||||
return [r, resolvedAddress];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
15
src/api/address-resolver/ENSAddressResolver.ts
Normal file
15
src/api/address-resolver/ENSAddressResolver.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { BaseProvider } from "@ethersproject/providers";
|
||||
import { IAddressResolver } from "./address-resolver";
|
||||
|
||||
export class ENSAddressResolver implements IAddressResolver<string> {
|
||||
async resolveAddress(
|
||||
provider: BaseProvider,
|
||||
address: string
|
||||
): Promise<string | undefined> {
|
||||
const name = await provider.lookupAddress(address);
|
||||
if (name === null) {
|
||||
return undefined;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
30
src/api/address-resolver/ERCTokenResolver.ts
Normal file
30
src/api/address-resolver/ERCTokenResolver.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { BaseProvider } from "@ethersproject/providers";
|
||||
import { Contract } from "@ethersproject/contracts";
|
||||
import { IAddressResolver } from "./address-resolver";
|
||||
import erc20 from "../../erc20.json";
|
||||
import { TokenMeta } from "../../types";
|
||||
|
||||
export class ERCTokenResolver implements IAddressResolver<TokenMeta> {
|
||||
async resolveAddress(
|
||||
provider: BaseProvider,
|
||||
address: string
|
||||
): Promise<TokenMeta | undefined> {
|
||||
const erc20Contract = new Contract(address, erc20, provider);
|
||||
try {
|
||||
const [name, symbol, decimals] = await Promise.all([
|
||||
erc20Contract.name(),
|
||||
erc20Contract.symbol(),
|
||||
erc20Contract.decimals(),
|
||||
]);
|
||||
return {
|
||||
name,
|
||||
symbol,
|
||||
decimals,
|
||||
};
|
||||
} catch (err) {
|
||||
// Ignore on purpose; this indicates the probe failed and the address
|
||||
// is not a token
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
16
src/api/address-resolver/address-resolver.ts
Normal file
16
src/api/address-resolver/address-resolver.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { BaseProvider } from "@ethersproject/providers";
|
||||
|
||||
export interface IAddressResolver<T> {
|
||||
resolveAddress(
|
||||
provider: BaseProvider,
|
||||
address: string
|
||||
): Promise<T | undefined>;
|
||||
}
|
||||
|
||||
export type ResolvedAddressRenderer<T> = (
|
||||
address: string,
|
||||
resolvedAddress: T,
|
||||
linkable: boolean,
|
||||
dontOverrideColors: boolean
|
||||
) => React.ReactElement;
|
57
src/api/address-resolver/index.ts
Normal file
57
src/api/address-resolver/index.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { BaseProvider } from "@ethersproject/providers";
|
||||
import { ensRenderer } from "../../components/ENSName";
|
||||
import { tokenRenderer } from "../../components/TokenName";
|
||||
import { IAddressResolver, ResolvedAddressRenderer } from "./address-resolver";
|
||||
import {
|
||||
CompositeAddressResolver,
|
||||
SelectedResolvedName,
|
||||
} from "./CompositeAddressResolver";
|
||||
import { ENSAddressResolver } from "./ENSAddressResolver";
|
||||
import { ERCTokenResolver } from "./ERCTokenResolver";
|
||||
|
||||
export type ResolvedAddresses = Record<string, SelectedResolvedName<any>>;
|
||||
|
||||
// Create and configure the main resolver
|
||||
export const ensResolver = new ENSAddressResolver();
|
||||
export const ercTokenResolver = new ERCTokenResolver();
|
||||
|
||||
const _mainResolver = new CompositeAddressResolver();
|
||||
_mainResolver.addResolver(ensResolver);
|
||||
_mainResolver.addResolver(ercTokenResolver);
|
||||
|
||||
export const mainResolver: IAddressResolver<SelectedResolvedName<any>> =
|
||||
_mainResolver;
|
||||
|
||||
export const resolverRendererRegistry = new Map<
|
||||
IAddressResolver<any>,
|
||||
ResolvedAddressRenderer<any>
|
||||
>();
|
||||
resolverRendererRegistry.set(ensResolver, ensRenderer);
|
||||
resolverRendererRegistry.set(ercTokenResolver, tokenRenderer);
|
||||
|
||||
// TODO: implement progressive resolving
|
||||
export const batchPopulate = async (
|
||||
provider: BaseProvider,
|
||||
addresses: string[],
|
||||
currentMap: ResolvedAddresses | undefined
|
||||
): Promise<ResolvedAddresses> => {
|
||||
const solvers: Promise<SelectedResolvedName<any> | undefined>[] = [];
|
||||
const unresolvedAddresses = addresses.filter(
|
||||
(a) => currentMap?.[a] === undefined
|
||||
);
|
||||
for (const a of unresolvedAddresses) {
|
||||
solvers.push(mainResolver.resolveAddress(provider, a));
|
||||
}
|
||||
|
||||
const resultMap: ResolvedAddresses = currentMap ? { ...currentMap } : {};
|
||||
const results = await Promise.all(solvers);
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const r = results[i];
|
||||
if (r === undefined) {
|
||||
continue;
|
||||
}
|
||||
resultMap[unresolvedAddresses[i]] = r;
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
};
|
@ -8,7 +8,7 @@ import TransactionItem from "../search/TransactionItem";
|
||||
import { useFeeToggler } from "../search/useFeeToggler";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { SelectionContext, useSelection } from "../useSelection";
|
||||
import { useENSCache } from "../useReverseCache";
|
||||
import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses";
|
||||
import { ProcessedTransaction } from "../types";
|
||||
import { PAGE_SIZE } from "../params";
|
||||
import { useMultipleETHUSDOracle } from "../usePriceOracle";
|
||||
@ -29,7 +29,8 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
|
||||
const selectionCtx = useSelection();
|
||||
const [feeDisplay, feeDisplayToggler] = useFeeToggler();
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const reverseCache = useENSCache(provider, page);
|
||||
const addrCollector = useMemo(() => pageCollector(page), [page]);
|
||||
const resolvedAddresses = useResolvedAddresses(provider, addrCollector);
|
||||
const blockTags = useMemo(() => [blockTag], [blockTag]);
|
||||
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
|
||||
|
||||
@ -59,7 +60,7 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
|
||||
<TransactionItem
|
||||
key={tx.hash}
|
||||
tx={tx}
|
||||
ensCache={reverseCache}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
feeDisplay={feeDisplay}
|
||||
priceMap={priceMap}
|
||||
/>
|
||||
|
@ -1,13 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
type AddressProps = {
|
||||
address: string;
|
||||
};
|
||||
|
||||
const Address: React.FC<AddressProps> = ({ address }) => (
|
||||
<span className="font-address text-gray-400 truncate" title={address}>
|
||||
<span className="truncate">{address}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
export default React.memo(Address);
|
@ -1,51 +1,49 @@
|
||||
import React from "react";
|
||||
import Address from "./Address";
|
||||
import AddressLink from "./AddressLink";
|
||||
import ENSName from "./ENSName";
|
||||
import ENSNameLink from "./ENSNameLink";
|
||||
import {
|
||||
ResolvedAddresses,
|
||||
resolverRendererRegistry,
|
||||
} from "../api/address-resolver";
|
||||
import PlainAddress from "./PlainAddress";
|
||||
|
||||
type AddressOrENSNameProps = {
|
||||
address: string;
|
||||
ensName?: string;
|
||||
selectedAddress?: string;
|
||||
text?: string;
|
||||
dontOverrideColors?: boolean;
|
||||
resolvedAddresses?: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({
|
||||
address,
|
||||
ensName,
|
||||
selectedAddress,
|
||||
text,
|
||||
dontOverrideColors,
|
||||
}) => (
|
||||
<>
|
||||
{address === selectedAddress ? (
|
||||
<>
|
||||
{ensName ? (
|
||||
<ENSName name={ensName} address={address} />
|
||||
) : (
|
||||
<Address address={address} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{ensName ? (
|
||||
<ENSNameLink
|
||||
name={ensName}
|
||||
address={address}
|
||||
dontOverrideColors={dontOverrideColors}
|
||||
/>
|
||||
) : (
|
||||
<AddressLink
|
||||
address={address}
|
||||
text={text}
|
||||
dontOverrideColors={dontOverrideColors}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const resolvedAddress = resolvedAddresses?.[address];
|
||||
const linkable = address !== selectedAddress;
|
||||
|
||||
if (!resolvedAddress) {
|
||||
return (
|
||||
<PlainAddress
|
||||
address={address}
|
||||
linkable={linkable}
|
||||
dontOverrideColors={dontOverrideColors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const [resolver, resolvedName] = resolvedAddress;
|
||||
const renderer = resolverRendererRegistry.get(resolver);
|
||||
if (renderer === undefined) {
|
||||
return (
|
||||
<PlainAddress
|
||||
address={address}
|
||||
linkable={linkable}
|
||||
dontOverrideColors={dontOverrideColors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return renderer(address, resolvedName, linkable, !!dontOverrideColors);
|
||||
};
|
||||
|
||||
export default AddressOrENSName;
|
||||
|
@ -5,36 +5,32 @@ import { faBomb } from "@fortawesome/free-solid-svg-icons/faBomb";
|
||||
import { faMoneyBillAlt } from "@fortawesome/free-solid-svg-icons/faMoneyBillAlt";
|
||||
import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn";
|
||||
import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins";
|
||||
import TokenLogo from "./TokenLogo";
|
||||
import AddressOrENSName from "./AddressOrENSName";
|
||||
import { AddressContext, TokenMeta, ZERO_ADDRESS } from "../types";
|
||||
import { AddressContext, ZERO_ADDRESS } from "../types";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
|
||||
type DecoratedAddressLinkProps = {
|
||||
address: string;
|
||||
ensName?: string;
|
||||
selectedAddress?: string;
|
||||
text?: string;
|
||||
addressCtx?: AddressContext;
|
||||
creation?: boolean;
|
||||
miner?: boolean;
|
||||
selfDestruct?: boolean;
|
||||
txFrom?: boolean;
|
||||
txTo?: boolean;
|
||||
tokenMeta?: TokenMeta | null | undefined;
|
||||
resolvedAddresses?: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const DecoratedAddresssLink: React.FC<DecoratedAddressLinkProps> = ({
|
||||
const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
|
||||
address,
|
||||
ensName,
|
||||
selectedAddress,
|
||||
text,
|
||||
addressCtx,
|
||||
creation,
|
||||
miner,
|
||||
selfDestruct,
|
||||
txFrom,
|
||||
txTo,
|
||||
tokenMeta,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS;
|
||||
const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS;
|
||||
@ -74,20 +70,14 @@ const DecoratedAddresssLink: React.FC<DecoratedAddressLinkProps> = ({
|
||||
<FontAwesomeIcon icon={faCoins} size="1x" />
|
||||
</span>
|
||||
)}
|
||||
{tokenMeta && (
|
||||
<div className="self-center">
|
||||
<TokenLogo address={address} name={tokenMeta.name} />
|
||||
</div>
|
||||
)}
|
||||
<AddressOrENSName
|
||||
address={address}
|
||||
ensName={ensName}
|
||||
selectedAddress={selectedAddress}
|
||||
text={text}
|
||||
dontOverrideColors={mint || burn}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DecoratedAddresssLink);
|
||||
export default React.memo(DecoratedAddressLink);
|
||||
|
@ -1,25 +1,75 @@
|
||||
import React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver";
|
||||
import ENSLogo from "./ensLogo.svg";
|
||||
|
||||
type ENSNameProps = {
|
||||
name: string;
|
||||
address: string;
|
||||
linkable: boolean;
|
||||
dontOverrideColors?: boolean;
|
||||
};
|
||||
|
||||
const ENSName: React.FC<ENSNameProps> = ({ name, address }) => (
|
||||
<div
|
||||
className="flex items-baseline space-x-1 font-sans text-gray-700 truncate"
|
||||
title={`${name}: ${address}`}
|
||||
>
|
||||
const ENSName: React.FC<ENSNameProps> = ({
|
||||
name,
|
||||
address,
|
||||
linkable,
|
||||
dontOverrideColors,
|
||||
}) => {
|
||||
if (linkable) {
|
||||
return (
|
||||
<NavLink
|
||||
className={`flex items-baseline space-x-1 font-sans ${
|
||||
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
|
||||
} truncate`}
|
||||
to={`/address/${name}`}
|
||||
title={`${name}: ${address}`}
|
||||
>
|
||||
<Content linkable={true} name={name} />
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-baseline space-x-1 font-sans text-gray-700 truncate"
|
||||
title={`${name}: ${address}`}
|
||||
>
|
||||
<Content linkable={false} name={name} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
linkable: boolean;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const Content: React.FC<ContentProps> = ({ linkable, name }) => (
|
||||
<>
|
||||
<img
|
||||
className="self-center filter grayscale"
|
||||
className={`self-center ${linkable ? "" : "filter grayscale"}`}
|
||||
src={ENSLogo}
|
||||
alt="ENS Logo"
|
||||
width={12}
|
||||
height={12}
|
||||
/>
|
||||
<span className="truncate">{name}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export default React.memo(ENSName);
|
||||
export const ensRenderer: ResolvedAddressRenderer<string> = (
|
||||
address,
|
||||
resolvedAddress,
|
||||
linkable,
|
||||
dontOverrideColors
|
||||
) => (
|
||||
<ENSName
|
||||
address={address}
|
||||
name={resolvedAddress}
|
||||
linkable={linkable}
|
||||
dontOverrideColors={dontOverrideColors}
|
||||
/>
|
||||
);
|
||||
|
||||
export default ENSName;
|
||||
|
@ -3,17 +3,23 @@ import InternalTransfer from "./InternalTransfer";
|
||||
import InternalSelfDestruct from "./InternalSelfDestruct";
|
||||
import InternalCreate from "./InternalCreate";
|
||||
import { TransactionData, InternalOperation, OperationType } from "../types";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
|
||||
type InternalTransactionOperationProps = {
|
||||
txData: TransactionData;
|
||||
internalOp: InternalOperation;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const InternalTransactionOperation: React.FC<InternalTransactionOperationProps> =
|
||||
({ txData, internalOp }) => (
|
||||
({ txData, internalOp, resolvedAddresses }) => (
|
||||
<>
|
||||
{internalOp.type === OperationType.TRANSFER && (
|
||||
<InternalTransfer txData={txData} internalOp={internalOp} />
|
||||
<InternalTransfer
|
||||
txData={txData}
|
||||
internalOp={internalOp}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
)}
|
||||
{internalOp.type === OperationType.SELF_DESTRUCT && (
|
||||
<InternalSelfDestruct txData={txData} internalOp={internalOp} />
|
||||
|
@ -5,15 +5,18 @@ import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
|
||||
import AddressHighlighter from "./AddressHighlighter";
|
||||
import DecoratedAddressLink from "./DecoratedAddressLink";
|
||||
import { TransactionData, InternalOperation } from "../types";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
|
||||
type InternalTransferProps = {
|
||||
txData: TransactionData;
|
||||
internalOp: InternalOperation;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const InternalTransfer: React.FC<InternalTransferProps> = ({
|
||||
txData,
|
||||
internalOp,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const fromMiner =
|
||||
txData.confirmedData?.miner !== undefined &&
|
||||
|
36
src/components/PlainAddress.tsx
Normal file
36
src/components/PlainAddress.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
|
||||
type PlainAddressProps = {
|
||||
address: string;
|
||||
linkable: boolean;
|
||||
dontOverrideColors: boolean | undefined;
|
||||
};
|
||||
|
||||
const PlainAddress: React.FC<PlainAddressProps> = ({
|
||||
address,
|
||||
linkable,
|
||||
dontOverrideColors,
|
||||
}) => {
|
||||
if (linkable) {
|
||||
return (
|
||||
<NavLink
|
||||
className={`${
|
||||
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
|
||||
} font-address truncate`}
|
||||
to={`/address/${address}`}
|
||||
title={address}
|
||||
>
|
||||
{address}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="font-address text-gray-400 truncate" title={address}>
|
||||
{address}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlainAddress;
|
@ -9,7 +9,7 @@ type TokenLogoProps = {
|
||||
};
|
||||
|
||||
const TokenLogo: React.FC<TokenLogoProps> = (props) => (
|
||||
<Suspense fallback={<></>}>
|
||||
<Suspense fallback={null}>
|
||||
<InternalTokenLogo {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
|
91
src/components/TokenName.tsx
Normal file
91
src/components/TokenName.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import TokenLogo from "./TokenLogo";
|
||||
import { ResolvedAddressRenderer } from "../api/address-resolver/address-resolver";
|
||||
import { TokenMeta } from "../types";
|
||||
|
||||
type TokenNameProps = {
|
||||
address: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
linkable: boolean;
|
||||
dontOverrideColors?: boolean;
|
||||
};
|
||||
|
||||
const TokenName: React.FC<TokenNameProps> = ({
|
||||
address,
|
||||
name,
|
||||
symbol,
|
||||
linkable,
|
||||
dontOverrideColors,
|
||||
}) => {
|
||||
if (linkable) {
|
||||
return (
|
||||
<NavLink
|
||||
className={`flex items-baseline space-x-1 font-sans ${
|
||||
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
|
||||
} truncate`}
|
||||
to={`/address/${address}`}
|
||||
title={`${name} (${symbol}): ${address}`}
|
||||
>
|
||||
<Content
|
||||
address={address}
|
||||
linkable={true}
|
||||
name={name}
|
||||
symbol={symbol}
|
||||
/>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-baseline space-x-1 font-sans text-gray-700 truncate"
|
||||
title={`${name} (${symbol}): ${address}`}
|
||||
>
|
||||
<Content address={address} linkable={false} name={name} symbol={symbol} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
address: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
linkable: boolean;
|
||||
};
|
||||
|
||||
const Content: React.FC<ContentProps> = ({
|
||||
address,
|
||||
name,
|
||||
symbol,
|
||||
linkable,
|
||||
}) => (
|
||||
<>
|
||||
<div
|
||||
className={`self-center w-5 h-5 ${linkable ? "" : "filter grayscale"}`}
|
||||
>
|
||||
<TokenLogo address={address} name={name} />
|
||||
</div>
|
||||
<span className="truncate">
|
||||
{name} ({symbol})
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
export const tokenRenderer: ResolvedAddressRenderer<TokenMeta> = (
|
||||
address,
|
||||
tokenMeta,
|
||||
linkable,
|
||||
dontOverrideColors
|
||||
) => (
|
||||
<TokenName
|
||||
address={address}
|
||||
name={tokenMeta.name}
|
||||
symbol={tokenMeta.symbol}
|
||||
linkable={linkable}
|
||||
dontOverrideColors={dontOverrideColors}
|
||||
/>
|
||||
);
|
||||
|
||||
export default TokenName;
|
@ -14,14 +14,15 @@ import TransactionDirection, {
|
||||
Flags,
|
||||
} from "../components/TransactionDirection";
|
||||
import TransactionValue from "../components/TransactionValue";
|
||||
import { ENSReverseCache, ProcessedTransaction } from "../types";
|
||||
import { ProcessedTransaction } from "../types";
|
||||
import { FeeDisplay } from "./useFeeToggler";
|
||||
import { formatValue } from "../components/formatter";
|
||||
import ETH2USDValue from "../components/ETH2USDValue";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
|
||||
type TransactionItemProps = {
|
||||
tx: ProcessedTransaction;
|
||||
ensCache?: ENSReverseCache;
|
||||
resolvedAddresses?: ResolvedAddresses;
|
||||
selectedAddress?: string;
|
||||
feeDisplay: FeeDisplay;
|
||||
priceMap: Record<BlockTag, BigNumber>;
|
||||
@ -29,7 +30,7 @@ type TransactionItemProps = {
|
||||
|
||||
const TransactionItem: React.FC<TransactionItemProps> = ({
|
||||
tx,
|
||||
ensCache,
|
||||
resolvedAddresses,
|
||||
selectedAddress,
|
||||
feeDisplay,
|
||||
priceMap,
|
||||
@ -50,12 +51,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const ensFrom = ensCache && tx.from && ensCache[tx.from];
|
||||
const ensTo = ensCache && tx.to && ensCache[tx.to];
|
||||
const ensCreated =
|
||||
ensCache &&
|
||||
tx.createdContractAddress &&
|
||||
ensCache[tx.createdContractAddress];
|
||||
const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction;
|
||||
|
||||
return (
|
||||
@ -87,9 +82,9 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
||||
<AddressHighlighter address={tx.from}>
|
||||
<DecoratedAddressLink
|
||||
address={tx.from}
|
||||
ensName={ensFrom}
|
||||
selectedAddress={selectedAddress}
|
||||
miner={tx.miner === tx.from}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
)}
|
||||
@ -107,18 +102,18 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
||||
<AddressHighlighter address={tx.to}>
|
||||
<DecoratedAddressLink
|
||||
address={tx.to}
|
||||
ensName={ensTo}
|
||||
selectedAddress={selectedAddress}
|
||||
miner={tx.miner === tx.to}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
) : (
|
||||
<AddressHighlighter address={tx.createdContractAddress!}>
|
||||
<DecoratedAddressLink
|
||||
address={tx.createdContractAddress!}
|
||||
ensName={ensCreated}
|
||||
selectedAddress={selectedAddress}
|
||||
creation
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
)}
|
||||
@ -144,4 +139,4 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TransactionItem);
|
||||
export default TransactionItem;
|
||||
|
@ -38,6 +38,7 @@ import ModeTab from "../components/ModeTab";
|
||||
import DecodedParamsTable from "./decoder/DecodedParamsTable";
|
||||
import { rawInputTo4Bytes, use4Bytes } from "../use4Bytes";
|
||||
import { DevDoc, UserDoc } from "../useSourcify";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
|
||||
type DetailsProps = {
|
||||
txData: TransactionData;
|
||||
@ -47,6 +48,7 @@ type DetailsProps = {
|
||||
internalOps?: InternalOperation[];
|
||||
sendsEthToMiner: boolean;
|
||||
ethUSDPrice: BigNumber | undefined;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const Details: React.FC<DetailsProps> = ({
|
||||
@ -57,6 +59,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
internalOps,
|
||||
sendsEthToMiner,
|
||||
ethUSDPrice,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const hasEIP1559 =
|
||||
txData.confirmedData?.blockBaseFeePerGas !== undefined &&
|
||||
@ -154,6 +157,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
address={txData.from}
|
||||
miner={txData.from === txData.confirmedData?.miner}
|
||||
txFrom
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
<Copy value={txData.from} />
|
||||
@ -171,6 +175,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
address={txData.to}
|
||||
miner={txData.to === txData.confirmedData?.miner}
|
||||
txTo
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
<Copy value={txData.to} />
|
||||
@ -188,6 +193,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
address={txData.confirmedData.createdContractAddress!}
|
||||
creation
|
||||
txTo
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
<Copy value={txData.confirmedData.createdContractAddress!} />
|
||||
@ -200,6 +206,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
key={i}
|
||||
txData={txData}
|
||||
internalOp={op}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -219,6 +226,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
t={t}
|
||||
txData={txData}
|
||||
tokenMeta={txData.tokenMetas[t.token]}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -10,14 +10,21 @@ import DecodedParamsTable from "./decoder/DecodedParamsTable";
|
||||
import DecodedLogSignature from "./decoder/DecodedLogSignature";
|
||||
import { TransactionData } from "../types";
|
||||
import { useTopic0 } from "../useTopic0";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
|
||||
type LogEntryProps = {
|
||||
txData: TransactionData;
|
||||
log: Log;
|
||||
logDesc: LogDescription | null | undefined;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => {
|
||||
const LogEntry: React.FC<LogEntryProps> = ({
|
||||
txData,
|
||||
log,
|
||||
logDesc,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const rawTopic0 = log.topics[0];
|
||||
const topic0 = useTopic0(rawTopic0);
|
||||
|
||||
@ -62,6 +69,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => {
|
||||
miner={log.address === txData.confirmedData?.miner}
|
||||
txFrom={log.address === txData.from}
|
||||
txTo={log.address === txData.to}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
<Copy value={log.address} />
|
||||
|
@ -5,13 +5,15 @@ import LogEntry from "./LogEntry";
|
||||
import { TransactionData } from "../types";
|
||||
import { useAppConfigContext } from "../useAppConfig";
|
||||
import { Metadata, useMultipleMetadata } from "../useSourcify";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
|
||||
type LogsProps = {
|
||||
txData: TransactionData;
|
||||
metadata: Metadata | null | undefined;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const Logs: React.FC<LogsProps> = ({ txData, metadata }) => {
|
||||
const Logs: React.FC<LogsProps> = ({ txData, metadata, resolvedAddresses }) => {
|
||||
const baseMetadatas = useMemo((): Record<string, Metadata | null> => {
|
||||
if (!txData.to || metadata === undefined) {
|
||||
return {};
|
||||
@ -70,6 +72,7 @@ const Logs: React.FC<LogsProps> = ({ txData, metadata }) => {
|
||||
txData={txData}
|
||||
log={l}
|
||||
logDesc={logDescs?.[i]}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
@ -32,10 +32,6 @@ export type TransactionChunk = {
|
||||
lastPage: boolean;
|
||||
};
|
||||
|
||||
export type ENSReverseCache = {
|
||||
[address: string]: string;
|
||||
};
|
||||
|
||||
export type TransactionData = {
|
||||
transactionHash: string;
|
||||
from: string;
|
||||
|
@ -38,7 +38,7 @@ const resolveSourcifySource = (source: SourcifySource) => {
|
||||
return sourcifyHttpRepoPrefix;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown Sourcify intergration source code: ${source}`);
|
||||
throw new Error(`Unknown Sourcify integration source code: ${source}`);
|
||||
};
|
||||
|
||||
export const sourcifyMetadata = (
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { JsonRpcProvider, WebSocketProvider } from "@ethersproject/providers";
|
||||
import {
|
||||
JsonRpcProvider,
|
||||
JsonRpcBatchProvider,
|
||||
WebSocketProvider,
|
||||
} from "@ethersproject/providers";
|
||||
import { ConnectionStatus } from "./types";
|
||||
import { MIN_API_LEVEL } from "./params";
|
||||
|
||||
@ -35,7 +39,7 @@ export const useProvider = (
|
||||
if (erigonURL?.startsWith("ws://") || erigonURL?.startsWith("wss://")) {
|
||||
provider = new WebSocketProvider(erigonURL);
|
||||
} else {
|
||||
provider = new JsonRpcProvider(erigonURL);
|
||||
provider = new JsonRpcBatchProvider(erigonURL);
|
||||
}
|
||||
|
||||
// Check if it is at least a regular ETH node
|
||||
|
92
src/useResolvedAddresses.ts
Normal file
92
src/useResolvedAddresses.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { JsonRpcProvider } from "@ethersproject/providers";
|
||||
import { ProcessedTransaction, TransactionData } from "./types";
|
||||
import { batchPopulate, ResolvedAddresses } from "./api/address-resolver";
|
||||
|
||||
export type AddressCollector = () => string[];
|
||||
|
||||
export const pageCollector =
|
||||
(page: ProcessedTransaction[] | undefined): AddressCollector =>
|
||||
() => {
|
||||
if (!page) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uniqueAddresses = new Set<string>();
|
||||
for (const tx of page) {
|
||||
if (tx.from) {
|
||||
uniqueAddresses.add(tx.from);
|
||||
}
|
||||
if (tx.to) {
|
||||
uniqueAddresses.add(tx.to);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueAddresses);
|
||||
};
|
||||
|
||||
export const transactionDataCollector =
|
||||
(txData: TransactionData | null | undefined): AddressCollector =>
|
||||
() => {
|
||||
if (!txData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uniqueAddresses = new Set<string>();
|
||||
|
||||
// Standard fields
|
||||
uniqueAddresses.add(txData.from);
|
||||
if (txData.to) {
|
||||
uniqueAddresses.add(txData.to);
|
||||
}
|
||||
if (txData.confirmedData?.createdContractAddress) {
|
||||
uniqueAddresses.add(txData.confirmedData?.createdContractAddress);
|
||||
}
|
||||
|
||||
// Dig token transfers
|
||||
for (const t of txData.tokenTransfers) {
|
||||
uniqueAddresses.add(t.from);
|
||||
uniqueAddresses.add(t.to);
|
||||
uniqueAddresses.add(t.token);
|
||||
}
|
||||
|
||||
// Dig log addresses
|
||||
if (txData.confirmedData) {
|
||||
for (const l of txData.confirmedData.logs) {
|
||||
uniqueAddresses.add(l.address);
|
||||
// TODO: find a way to dig over decoded address log attributes
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueAddresses);
|
||||
};
|
||||
|
||||
export const useResolvedAddresses = (
|
||||
provider: JsonRpcProvider | undefined,
|
||||
addrCollector: AddressCollector
|
||||
) => {
|
||||
const [names, setNames] = useState<ResolvedAddresses>();
|
||||
const ref = useRef<ResolvedAddresses | undefined>();
|
||||
useEffect(() => {
|
||||
ref.current = names;
|
||||
});
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const populate = async () => {
|
||||
const _addresses = addrCollector();
|
||||
const _names = await batchPopulate(provider, _addresses, ref.current);
|
||||
setNames(_names);
|
||||
};
|
||||
populate();
|
||||
},
|
||||
// DON'T put names variables in dependency array; this is intentional; useRef
|
||||
[provider, addrCollector]
|
||||
);
|
||||
|
||||
return names;
|
||||
};
|
@ -1,48 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { JsonRpcProvider } from "@ethersproject/providers";
|
||||
import { ENSReverseCache, ProcessedTransaction } from "./types";
|
||||
|
||||
export const useENSCache = (
|
||||
provider?: JsonRpcProvider,
|
||||
page?: ProcessedTransaction[]
|
||||
) => {
|
||||
const [reverseCache, setReverseCache] = useState<ENSReverseCache>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider || !page) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addrSet = new Set<string>();
|
||||
for (const tx of page) {
|
||||
if (tx.from) {
|
||||
addrSet.add(tx.from);
|
||||
}
|
||||
if (tx.to) {
|
||||
addrSet.add(tx.to);
|
||||
}
|
||||
}
|
||||
const addresses = Array.from(addrSet);
|
||||
|
||||
const reverseResolve = async () => {
|
||||
const solvers: Promise<string | null>[] = [];
|
||||
for (const a of addresses) {
|
||||
solvers.push(provider.lookupAddress(a));
|
||||
}
|
||||
|
||||
const results = await Promise.all(solvers);
|
||||
const cache: ENSReverseCache = {};
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const r = results[i];
|
||||
if (r === null) {
|
||||
continue;
|
||||
}
|
||||
cache[addresses[i]] = r;
|
||||
}
|
||||
setReverseCache(cache);
|
||||
};
|
||||
reverseResolve();
|
||||
}, [provider, page]);
|
||||
|
||||
return reverseCache;
|
||||
};
|
2
topic0
2
topic0
@ -1 +1 @@
|
||||
Subproject commit 52559d5690d491f8191a2d3fdb3c037516adc68f
|
||||
Subproject commit 5026a20b712c1cad66878821c38e1f070e4a3799
|
Loading…
Reference in New Issue
Block a user