Merge branch 'feature/address-page' into develop

This commit is contained in:
Willian Mitsuda 2022-02-22 14:10:56 -03:00
commit 5dd96f38b2
8 changed files with 193 additions and 62 deletions

View File

@ -10,6 +10,7 @@ import { useLatestBlock } from "./useLatestBlock";
const ETH_FEED_DECIMALS = 8; const ETH_FEED_DECIMALS = 8;
// TODO: reduce duplication with useETHUSDOracle
const PriceBox: React.FC = () => { const PriceBox: React.FC = () => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const latestBlock = useLatestBlock(provider); const latestBlock = useLatestBlock(provider);

View File

@ -1,6 +1,12 @@
import React, { useContext, useEffect, useMemo, useState } from "react"; import React, { useContext, useEffect, useMemo, useState } from "react";
import { BlockTag } from "@ethersproject/providers"; import { BlockTag } from "@ethersproject/providers";
import ContentFrame from "../ContentFrame"; import ContentFrame from "../ContentFrame";
import InfoRow from "../components/InfoRow";
import TransactionValue from "../components/TransactionValue";
import ETH2USDValue from "../components/ETH2USDValue";
import TransactionAddress from "../components/TransactionAddress";
import Copy from "../components/Copy";
import TransactionLink from "../components/TransactionLink";
import PendingResults from "../search/PendingResults"; import PendingResults from "../search/PendingResults";
import ResultHeader from "../search/ResultHeader"; import ResultHeader from "../search/ResultHeader";
import { SearchController } from "../search/search"; import { SearchController } from "../search/search";
@ -11,8 +17,9 @@ import { SelectionContext, useSelection } from "../useSelection";
import { useMultipleETHUSDOracle } from "../usePriceOracle"; import { useMultipleETHUSDOracle } from "../usePriceOracle";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useParams, useSearchParams } from "react-router-dom"; import { useParams, useSearchParams } from "react-router-dom";
import { ChecksummedAddress } from "../types"; import { ChecksummedAddress, ProcessedTransaction } from "../types";
import { useContractsMetadata } from "../hooks"; import { useContractsMetadata } from "../hooks";
import { useAddressBalance, useContractCreator } from "../useErigonHooks";
type AddressTransactionResultsProps = { type AddressTransactionResultsProps = {
address: ChecksummedAddress; address: ChecksummedAddress;
@ -95,9 +102,12 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
// TODO: dedup blockTags // TODO: dedup blockTags
const blockTags: BlockTag[] = useMemo(() => { const blockTags: BlockTag[] = useMemo(() => {
if (!page) { if (!page) {
return []; return ["latest"];
} }
return page.map((t) => t.blockNumber);
const blockTags: BlockTag[] = page.map((t) => t.blockNumber);
blockTags.push("latest");
return blockTags;
}, [page]); }, [page]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags); const priceMap = useMultipleETHUSDOracle(provider, blockTags);
@ -117,65 +127,91 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
return _addresses; return _addresses;
}, [address, page]); }, [address, page]);
const metadatas = useContractsMetadata(addresses, provider); const metadatas = useContractsMetadata(addresses, provider);
const balance = useAddressBalance(provider, address);
const creator = useContractCreator(provider, address);
return ( return (
<ContentFrame tabs> <ContentFrame tabs>
<div className="flex justify-between items-baseline py-3"> <SelectionContext.Provider value={selectionCtx}>
<div className="text-sm text-gray-500"> {balance && (
{page === undefined ? ( <InfoRow title="Balance">
<>Waiting for search results...</> <div className="space-x-2">
) : ( <TransactionValue value={balance} />
<>{page.length} transactions on this page</> {!balance.isZero() && priceMap["latest"] !== undefined && (
)} <span className="px-2 border-green-200 border rounded-lg bg-green-100 text-green-600">
</div> <ETH2USDValue
<UndefinedPageControl ethAmount={balance}
address={address} eth2USDValue={priceMap["latest"]}
isFirst={controller?.isFirst} />
isLast={controller?.isLast} </span>
prevHash={page?.[0]?.hash ?? ""}
nextHash={page?.[page.length - 1]?.hash ?? ""}
disabled={controller === undefined}
/>
</div>
<ResultHeader
feeDisplay={feeDisplay}
feeDisplayToggler={feeDisplayToggler}
/>
{page ? (
<SelectionContext.Provider value={selectionCtx}>
{page.map((tx) => (
<TransactionItem
key={tx.hash}
tx={tx}
selectedAddress={address}
feeDisplay={feeDisplay}
priceMap={priceMap}
metadatas={metadatas}
/>
))}
<div className="flex justify-between items-baseline py-3">
<div className="text-sm text-gray-500">
{page === undefined ? (
<>Waiting for search results...</>
) : (
<>{page.length} transactions on this page</>
)} )}
</div> </div>
<UndefinedPageControl </InfoRow>
address={address} )}
isFirst={controller?.isFirst} {creator && (
isLast={controller?.isLast} <InfoRow title="Contract creator">
prevHash={page?.[0]?.hash ?? ""} <div className="flex divide-x-2 divide-dotted divide-gray-300">
nextHash={page?.[page.length - 1]?.hash ?? ""} <div className="flex items-baseline space-x-2 -ml-1 mr-3">
disabled={controller === undefined} <TransactionAddress address={creator.creator} />
/> <Copy value={creator.creator} />
</div> </div>
</SelectionContext.Provider> <div className="flex items-baseline pl-3">
) : ( <TransactionLink txHash={creator.hash} />
<PendingResults /> </div>
)} </div>
</InfoRow>
)}
<NavBar address={address} page={page} controller={controller} />
<ResultHeader
feeDisplay={feeDisplay}
feeDisplayToggler={feeDisplayToggler}
/>
{page ? (
<>
{page.map((tx) => (
<TransactionItem
key={tx.hash}
tx={tx}
selectedAddress={address}
feeDisplay={feeDisplay}
priceMap={priceMap}
metadatas={metadatas}
/>
))}
<NavBar address={address} page={page} controller={controller} />
</>
) : (
<PendingResults />
)}
</SelectionContext.Provider>
</ContentFrame> </ContentFrame>
); );
}; };
type NavBarProps = {
address: ChecksummedAddress;
page: ProcessedTransaction[] | undefined;
controller: SearchController | undefined;
};
const NavBar: React.FC<NavBarProps> = ({ address, page, controller }) => (
<div className="flex justify-between items-baseline py-3">
<div className="text-sm text-gray-500">
{page === undefined ? (
<>Waiting for search results...</>
) : (
<>{page.length} transactions on this page</>
)}
</div>
<UndefinedPageControl
address={address}
isFirst={controller?.isFirst}
isLast={controller?.isLast}
prevHash={page?.[0]?.hash ?? ""}
nextHash={page?.[page.length - 1]?.hash ?? ""}
disabled={controller === undefined}
/>
</div>
);
export default AddressTransactionResults; export default AddressTransactionResults;

View File

@ -22,7 +22,7 @@ const Copy: React.FC<CopyProps> = ({ value, rounded }) => {
return ( return (
<button <button
className={`text-gray-500 focus:outline-none ${ className={`self-center flex flex-no-wrap justify-center items-center space-x-1 text-gray-500 focus:outline-none ${
rounded rounded
? "transition-colors transition-shadows bg-gray-200 hover:bg-gray-500 hover:text-gray-200 hover:shadow w-7 h-7 rounded-full text-xs" ? "transition-colors transition-shadows bg-gray-200 hover:bg-gray-500 hover:text-gray-200 hover:shadow w-7 h-7 rounded-full text-xs"
: "text-sm" : "text-sm"
@ -34,10 +34,10 @@ const Copy: React.FC<CopyProps> = ({ value, rounded }) => {
rounded ? ( rounded ? (
<FontAwesomeIcon icon={faCheck} size="1x" /> <FontAwesomeIcon icon={faCheck} size="1x" />
) : ( ) : (
<div className="space-x-1"> <>
<FontAwesomeIcon icon={faCheckCircle} size="1x" /> <FontAwesomeIcon icon={faCheckCircle} size="1x" />
{!rounded && <span>Copied</span>} <span className="self-baseline">Copied</span>
</div> </>
) )
) : ( ) : (
<FontAwesomeIcon icon={faCopy} size="1x" /> <FontAwesomeIcon icon={faCopy} size="1x" />

View File

@ -7,6 +7,13 @@ type ETH2USDValueProps = {
eth2USDValue: BigNumber; eth2USDValue: BigNumber;
}; };
/**
* Basic display of ETH -> USD values WITHOUT box decoration, only
* text formatting.
*
* USD amounts are displayed commified with 2 decimals places and $ prefix,
* i.e., "$1,000.00".
*/
const ETH2USDValue: React.FC<ETH2USDValueProps> = ({ const ETH2USDValue: React.FC<ETH2USDValueProps> = ({
ethAmount, ethAmount,
eth2USDValue, eth2USDValue,

View File

@ -7,6 +7,7 @@ type FormatterBalanceProps = {
decimals?: number; decimals?: number;
}; };
// TODO: remove duplication with TransactionValue component
const FormattedBalance: React.FC<FormatterBalanceProps> = ({ const FormattedBalance: React.FC<FormatterBalanceProps> = ({
value, value,
decimals = 18, decimals = 18,

View File

@ -6,22 +6,34 @@ type TransactionValueProps = {
value: BigNumber; value: BigNumber;
decimals?: number; decimals?: number;
hideUnit?: boolean; hideUnit?: boolean;
unitName?: string;
}; };
/**
* Standard component for displaying balances. It:
*
* - Commify non-decimal parts, i.e., 1,000,000.00
* - Light gray absolute zero values
* - Cut out decimal part is it is 0 to reduce UI clutter, i.e., show
* 123 instead of 123.00
*
* TODO: remove duplication with FormattedBalance
*/
const TransactionValue: React.FC<TransactionValueProps> = ({ const TransactionValue: React.FC<TransactionValueProps> = ({
value, value,
decimals = 18, decimals = 18,
hideUnit, hideUnit,
unitName = "ETH",
}) => { }) => {
const formattedValue = formatValue(value, decimals); const formattedValue = formatValue(value, decimals);
return ( return (
<span <span
className={`text-sm ${value.isZero() ? "text-gray-400" : ""}`} className={`text-sm ${value.isZero() ? "text-gray-400" : ""}`}
title={`${formattedValue} Ether`} title={`${formattedValue} ${unitName}`}
> >
<span className={`font-balance`}>{formattedValue}</span> <span className={`font-balance`}>{formattedValue}</span>
{!hideUnit && " Ether"} {!hideUnit && ` ${unitName}`}
</span> </span>
); );
}; };

View File

@ -1,3 +1,3 @@
export const MIN_API_LEVEL = 7; export const MIN_API_LEVEL = 8;
export const PAGE_SIZE = 25; export const PAGE_SIZE = 25;

View File

@ -605,3 +605,77 @@ export const useTransactionBySenderAndNonce = (
} }
return data; return data;
}; };
type ContractCreatorKey = {
type: "cc";
network: number;
address: ChecksummedAddress;
};
type ContractCreator = {
hash: string;
creator: ChecksummedAddress;
};
export const useContractCreator = (
provider: JsonRpcProvider | undefined,
address: ChecksummedAddress | undefined
): ContractCreator | null | undefined => {
const { data, error } = useSWR<
ContractCreator | null | undefined,
any,
ContractCreatorKey | null
>(
provider && address
? {
type: "cc",
network: provider.network.chainId,
address,
}
: null,
getContractCreatorFetcher(provider!)
);
if (error) {
return undefined;
}
return data as ContractCreator;
};
const getContractCreatorFetcher =
(provider: JsonRpcProvider) =>
async ({
network,
address,
}: ContractCreatorKey): Promise<ContractCreator | null | undefined> => {
const result = (await provider.send("ots_getContractCreator", [
address,
])) as ContractCreator;
// Empty or success
if (result) {
result.creator = provider.formatter.address(result.creator);
}
return result;
};
export const useAddressBalance = (
provider: JsonRpcProvider | undefined,
address: ChecksummedAddress | undefined
): BigNumber | null | undefined => {
const [balance, setBalance] = useState<BigNumber | undefined>();
useEffect(() => {
if (!provider || !address) {
return undefined;
}
const readBalance = async () => {
const _balance = await provider.getBalance(address);
setBalance(_balance);
};
readBalance();
}, [provider, address]);
return balance;
};