Commit 1d39d53d authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into feat/token-icon-in-search

parents f4d40cef f3f732e3
......@@ -45,6 +45,9 @@ NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHO
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME__
NEXT_PUBLIC_AD_DOMAIN_WITH_AD=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_DOMAIN_WITH_AD__
NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP__
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE__
NEXT_PUBLIC_AD_SLISE_ON=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_SLISE_ON__
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__
NEXT_PUBLIC_WEB3_DEFAULT_WALLET=__PLACEHOLDER_FOR_NEXT_PUBLIC_WEB3_DEFAULT_WALLET__
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=__PLACEHOLDER_FOR_NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET__
......
/* eslint-disable no-restricted-properties */
import type { AdButlerConfig } from 'types/client/adButlerConfig';
import type { NavItemExternal } from 'types/client/navigation-items';
import type { WalletType } from 'types/client/wallets';
import type { NetworkExplorer } from 'types/networks';
......@@ -114,6 +115,9 @@ const config = Object.freeze({
ad: {
domainWithAd: getEnvValue(process.env.NEXT_PUBLIC_AD_DOMAIN_WITH_AD) || 'blockscout.com',
adButlerOn: getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_ON) === 'true',
adButlerConfigDesktop: parseEnvJson<AdButlerConfig>(getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP)),
adButlerConfigMobile: parseEnvJson<AdButlerConfig>(getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE)),
sliseOn: getEnvValue(process.env.NEXT_PUBLIC_AD_SLISE_ON) === 'true',
},
web3: {
defaultWallet: getWeb3DefaultWallet(),
......
......@@ -133,3 +133,5 @@ frontend:
_default: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID:
_default: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
NEXT_PUBLIC_AD_SLISE_ON:
_default: 'true'
......@@ -46,6 +46,9 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` |
| NEXT_PUBLIC_AD_DOMAIN_WITH_AD | `string` | The domain on which we display ads | - | - | `blockscout.com` |
| NEXT_PUBLIC_AD_ADBUTLER_ON | `boolean` | Set to true to show Adbutler banner instead of Coinzilla banner | - | `false` | `true` |
| NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP | `{ id: string; width: string; height: string }` | Placement config for desktop Adbutler banner | - | - | `{'id':'123456','width':'728','height':'90'}` |
| NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE | `{ id: string; width: number; height: number }` | Placement config for mobile Adbutler banner | - | - | `{'id':'654321','width':'300','height':'100'}` |
| NEXT_PUBLIC_AD_SLISE_ON | `boolean` | Set to true to show Slise banner instead of Coinzilla banner | - | `false` | `true` |
| NEXT_PUBLIC_API_SPEC_URL | `string` | Spec to be displayed on api-docs page | - | - | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` |
| NEXT_PUBLIC_GRAPHIQL_TRANSACTION | `string` | Txn hash for default query at GraphQl playground page | - | - | `0x69e3923eef50eada197c3336d546936d0c994211492c9f947a24c02827568f9f` |
| NEXT_PUBLIC_WEB3_DEFAULT_WALLET | `metamask` \| `coinbase`| Type of Web3 wallet which will be used by default to add tokens or chains to | - | `metamask` | `coinbase` |
......
......@@ -18,8 +18,8 @@ export default function buildUrl<R extends ResourceName>(
const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
// there are some pagination params that can be null for the next page
(value || value === null) && url.searchParams.append(key, String(value));
// there are some pagination params that can be null or false for the next page
value !== undefined && value !== '' && url.searchParams.append(key, String(value));
});
return url.toString();
......
import Base64 from 'crypto-js/enc-base64';
import sha256 from 'crypto-js/sha256';
import type CspDev from 'csp-dev';
import isSelfHosted from 'lib/isSelfHosted';
import { connectAdbutler, placeAd } from 'ui/shared/ad/adbutlerScript';
export function ad(): CspDev.DirectiveDescriptor {
if (!isSelfHosted()) {
......@@ -12,6 +15,7 @@ export function ad(): CspDev.DirectiveDescriptor {
'coinzilla.com',
'*.coinzilla.com',
'request-global.czilladx.com',
'*.slise.xyz',
],
'frame-src': [
'request-global.czilladx.com',
......@@ -19,10 +23,9 @@ export function ad(): CspDev.DirectiveDescriptor {
'script-src': [
'coinzillatag.com',
'servedbyadbutler.com',
// what hash is this?
'\'sha256-wMOeDjJaOTjCfNjluteV+tSqHW547T89sgxd8W6tQJM=\'',
// what hash is this?
'\'sha256-FcyIn1h7zra8TVnnRhYrwrplxJW7dpD5TV7kP2AG/kI=\'',
`'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd)) }'`,
'*.slise.xyz',
],
'img-src': [
'servedbyadbutler.com',
......
......@@ -2,6 +2,7 @@ import type { TokenCounters, TokenInfo } from 'types/api/token';
export const tokenInfo: TokenInfo = {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
circulating_market_cap: '117629601.61913824',
decimals: '18',
exchange_rate: '2.0101',
holders: '46554',
......@@ -19,6 +20,7 @@ export const tokenCounters: TokenCounters = {
export const tokenInfoERC20a: TokenInfo = {
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
circulating_market_cap: '117268489.23970924',
decimals: '18',
exchange_rate: null,
holders: '23',
......@@ -31,6 +33,7 @@ export const tokenInfoERC20a: TokenInfo = {
export const tokenInfoERC20b: TokenInfo = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
circulating_market_cap: '115060192.36105014',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
......@@ -43,6 +46,7 @@ export const tokenInfoERC20b: TokenInfo = {
export const tokenInfoERC20c: TokenInfo = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
circulating_market_cap: null,
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
......@@ -55,6 +59,7 @@ export const tokenInfoERC20c: TokenInfo = {
export const tokenInfoERC20d: TokenInfo = {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
circulating_market_cap: null,
decimals: '18',
exchange_rate: null,
holders: '102625',
......@@ -67,6 +72,7 @@ export const tokenInfoERC20d: TokenInfo = {
export const tokenInfoERC20LongSymbol: TokenInfo = {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
circulating_market_cap: '112855875.75888918',
decimals: '18',
exchange_rate: '1328.89',
holders: '102625',
......@@ -79,6 +85,7 @@ export const tokenInfoERC20LongSymbol: TokenInfo = {
export const tokenInfoERC721a: TokenInfo = {
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '7',
......@@ -91,6 +98,7 @@ export const tokenInfoERC721a: TokenInfo = {
export const tokenInfoERC721b: TokenInfo = {
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '2',
......@@ -103,6 +111,7 @@ export const tokenInfoERC721b: TokenInfo = {
export const tokenInfoERC721c: TokenInfo = {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '12',
......@@ -115,6 +124,7 @@ export const tokenInfoERC721c: TokenInfo = {
export const tokenInfoERC721LongSymbol: TokenInfo = {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '12',
......@@ -127,6 +137,7 @@ export const tokenInfoERC721LongSymbol: TokenInfo = {
export const tokenInfoERC1155a: TokenInfo = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '22',
......@@ -139,6 +150,7 @@ export const tokenInfoERC1155a: TokenInfo = {
export const tokenInfoERC1155b: TokenInfo = {
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '100',
......@@ -151,6 +163,7 @@ export const tokenInfoERC1155b: TokenInfo = {
export const tokenInfoERC1155WithoutName: TokenInfo = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '22',
......
......@@ -23,6 +23,7 @@ export const erc20: TokenTransfer = {
},
token: {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
circulating_market_cap: '117629601.61913824',
decimals: '18',
exchange_rate: null,
holders: '46554',
......@@ -67,6 +68,7 @@ export const erc721: TokenTransfer = {
},
token: {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '63090',
......@@ -110,6 +112,7 @@ export const erc1155A: TokenTransfer = {
},
token: {
address: '0xF56b7693E4212C584de4a83117f805B8E89224CB',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '1',
......
......@@ -24,6 +24,7 @@ export const mintToken: TxStateChange = {
is_miner: false,
token: {
address: '0x8977EA6C55e878125d1bF3433EBf72138B7a4543',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '9191',
......@@ -60,6 +61,7 @@ export const receiveMintedToken: TxStateChange = {
is_miner: false,
token: {
address: '0x8977EA6C55e878125d1bF3433EBf72138B7a4543',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '9191',
......@@ -89,6 +91,7 @@ export const transfer1155Token: TxStateChange = {
is_miner: false,
token: {
address: '0x56Cc277717106E528A9FcC2CD342Ff98db758041',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '50413',
......
......@@ -8,6 +8,7 @@ import { generateListStub } from './utils';
export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
address: ADDRESS_HASH,
circulating_market_cap: '117629601.61913824',
decimals: '18',
exchange_rate: '0.999997',
holders: '16026',
......@@ -20,11 +21,13 @@ export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
export const TOKEN_INFO_ERC_721: TokenInfo<'ERC-721'> = {
...TOKEN_INFO_ERC_20,
circulating_market_cap: null,
type: 'ERC-721',
};
export const TOKEN_INFO_ERC_1155: TokenInfo<'ERC-1155'> = {
...TOKEN_INFO_ERC_20,
circulating_market_cap: null,
type: 'ERC-1155',
};
......
......@@ -13,6 +13,7 @@ export interface TokenInfo<T extends TokenType = TokenType> {
exchange_rate: string | null;
total_supply: string | null;
icon_url: string | null;
circulating_market_cap: string | null;
}
export interface TokenCounters {
......
......@@ -45,7 +45,6 @@ export type TxStateChanges = {
items: Array<TxStateChange>;
next_page_params: {
items_count: number;
// ???
state_changes: null;
};
};
export type AdButlerConfig = {
id: string;
width: string;
height: string;
}
......@@ -2,7 +2,6 @@ import React from 'react';
import type { RoutedSubTab } from 'ui/shared/Tabs/types';
import { ContractContextProvider } from 'ui/address/contract/context';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
......@@ -15,17 +14,17 @@ const TAB_LIST_PROPS = {
columnGap: 3,
};
const AddressContract = ({ addressHash, tabs }: Props) => {
const AddressContract = ({ tabs }: Props) => {
const fallback = React.useCallback(() => {
const noProviderTabs = tabs.filter(({ id }) => id === 'contact_code');
return <RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>;
return (
<RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
);
}, [ tabs ]);
return (
<Web3ModalProvider fallback={ fallback }>
<ContractContextProvider addressHash={ addressHash }>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
</ContractContextProvider>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
</Web3ModalProvider>
);
};
......
......@@ -212,16 +212,10 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
isLoading={ isPlaceholderData }
/>
) }
{ data?.source_code && (
{ data?.is_verified && (
<ContractSourceCode
data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
address={ addressHash }
isViper={ Boolean(data.is_vyper_contract) }
filePath={ data.file_path }
additionalSource={ data.additional_sources }
remappings={ data.compiler_settings?.remappings }
isLoading={ isPlaceholderData }
implementationAddress={ addressInfo?.implementation_address ?? undefined }
/>
) }
{ data?.compiler_settings ? (
......
import { Flex, Skeleton, Text, Tooltip } from '@chakra-ui/react';
import { Box, Flex, Select, Skeleton, Text, Tooltip } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SmartContract } from 'types/api/contract';
import type { ArrayElement } from 'types/utils';
import useApiQuery from 'lib/api/useApiQuery';
import * as stubs from 'stubs/contract';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/LinkInternal';
import CodeEditor from 'ui/shared/monaco/CodeEditor';
import formatFilePath from 'ui/shared/monaco/utils/formatFilePath';
const SOURCE_CODE_OPTIONS = [
{ id: 'primary', label: 'Proxy' } as const,
{ id: 'secondary', label: 'Implementation' } as const,
];
type SourceCodeType = ArrayElement<typeof SOURCE_CODE_OPTIONS>['id'];
function getEditorData(contractInfo: SmartContract | undefined) {
if (!contractInfo || !contractInfo.source_code) {
return undefined;
}
const defaultName = contractInfo.is_vyper_contract ? '/index.vy' : '/index.sol';
return [
{ file_path: formatFilePath(contractInfo.file_path || defaultName), source_code: contractInfo.source_code },
...(contractInfo.additional_sources || []).map((source) => ({ ...source, file_path: formatFilePath(source.file_path) })),
];
}
interface Props {
data: string;
hasSol2Yml: boolean;
address?: string;
isViper: boolean;
filePath?: string;
additionalSource?: SmartContract['additional_sources'];
remappings?: Array<string>;
isLoading?: boolean;
implementationAddress?: string;
}
const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource, remappings, isLoading }: Props) => {
const ContractSourceCode = ({ address, implementationAddress }: Props) => {
const [ sourceType, setSourceType ] = React.useState<SourceCodeType>('primary');
const primaryContractQuery = useApiQuery('contract', {
pathParams: { hash: address },
queryOptions: {
enabled: Boolean(address),
refetchOnMount: false,
placeholderData: stubs.CONTRACT_CODE_VERIFIED,
},
});
const secondaryContractQuery = useApiQuery('contract', {
pathParams: { hash: implementationAddress },
queryOptions: {
enabled: Boolean(implementationAddress),
refetchOnMount: false,
placeholderData: stubs.CONTRACT_CODE_VERIFIED,
},
});
const isLoading = implementationAddress ?
primaryContractQuery.isPlaceholderData || secondaryContractQuery.isPlaceholderData :
primaryContractQuery.isPlaceholderData;
const primaryEditorData = React.useMemo(() => {
return getEditorData(primaryContractQuery.data);
}, [ primaryContractQuery.data ]);
const secondaryEditorData = React.useMemo(() => {
return getEditorData(secondaryContractQuery.data);
}, [ secondaryContractQuery.data ]);
const activeContract = sourceType === 'secondary' ? secondaryContractQuery.data : primaryContractQuery.data;
const activeContractData = sourceType === 'secondary' ? secondaryEditorData : primaryEditorData;
const heading = (
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
<span>Contract source code</span>
<Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text>
<Text whiteSpace="pre" as="span" variant="secondary"> ({ activeContract?.is_vyper_contract ? 'Vyper' : 'Solidity' })</Text>
</Skeleton>
);
const diagramLink = hasSol2Yml && address ? (
const diagramLinkAddress = (() => {
if (!activeContract?.can_be_visualized_via_sol2uml) {
return;
}
return sourceType === 'secondary' ? implementationAddress : address;
})();
const diagramLink = diagramLinkAddress ? (
<Tooltip label="Visualize contract code using Sol2Uml JS library">
<LinkInternal
href={ route({ pathname: '/visualize/sol2uml', query: { address } }) }
href={ route({ pathname: '/visualize/sol2uml', query: { address: diagramLinkAddress } }) }
ml="auto"
>
<Skeleton isLoaded={ !isLoading }>
......@@ -39,27 +96,66 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
</Skeleton>
</LinkInternal>
</Tooltip>
) : <Box ml="auto"/>;
const copyToClipboard = activeContractData?.length === 1 ?
<CopyToClipboard text={ activeContractData[0].source_code } isLoading={ isLoading } ml={ 3 }/> :
null;
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSourceType(event.target.value as SourceCodeType);
}, []);
const editorSourceTypeSelector = !secondaryContractQuery.isPlaceholderData && secondaryContractQuery.data?.source_code ? (
<Select
size="xs"
value={ sourceType }
onChange={ handleSelectChange }
focusBorderColor="none"
w="auto"
ml={ 3 }
borderRadius="base"
>
{ SOURCE_CODE_OPTIONS.map((option) => <option key={ option.id } value={ option.id }>{ option.label }</option>) }
</Select>
) : null;
const editorData = React.useMemo(() => {
const defaultName = isViper ? '/index.vy' : '/index.sol';
return [
{ file_path: formatFilePath(filePath || defaultName), source_code: data },
...(additionalSource || []).map((source) => ({ ...source, file_path: formatFilePath(source.file_path) })) ];
}, [ additionalSource, data, filePath, isViper ]);
const content = (() => {
if (isLoading) {
return <Skeleton h="557px" w="100%"/>;
}
const copyToClipboard = editorData.length === 1 ?
<CopyToClipboard text={ editorData[0].source_code } isLoading={ isLoading } ml={ 3 }/> :
null;
if (!primaryEditorData) {
return null;
}
return (
<>
<Box display={ sourceType === 'primary' ? 'block' : 'none' }>
<CodeEditor data={ primaryEditorData } remappings={ primaryContractQuery.data?.compiler_settings?.remappings }/>
</Box>
{ secondaryEditorData && (
<Box display={ sourceType === 'secondary' ? 'block' : 'none' }>
<CodeEditor data={ secondaryEditorData } remappings={ secondaryContractQuery.data?.compiler_settings?.remappings }/>
</Box>
) }
</>
);
})();
if (!primaryEditorData) {
return null;
}
return (
<section>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
{ heading }
{ editorSourceTypeSelector }
{ diagramLink }
{ copyToClipboard }
</Flex>
{ isLoading ? <Skeleton h="557px" w="100%"/> : <CodeEditor data={ editorData } remappings={ remappings }/> }
{ content }
</section>
);
};
......
......@@ -9,12 +9,12 @@ import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordi
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import { useContractContext } from './context';
import ContractConnectWallet from './ContractConnectWallet';
import ContractCustomAbiAlert from './ContractCustomAbiAlert';
import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethodCallable from './ContractMethodCallable';
import ContractWriteResult from './ContractWriteResult';
import useContractAbi from './useContractAbi';
import { getNativeCoinValue } from './utils';
interface Props {
......@@ -39,18 +39,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
},
});
const { contractInfo, customInfo, proxyInfo } = useContractContext();
const abi = (() => {
if (isProxy) {
return proxyInfo?.abi;
}
if (isCustomAbi) {
return customInfo?.abi;
}
return contractInfo?.abi;
})();
const contractAbi = useContractAbi({ addressHash, isProxy, isCustomAbi });
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<unknown>>) => {
if (!isConnected) {
......@@ -61,7 +50,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
await switchNetworkAsync?.(Number(config.network.id));
}
if (!abi) {
if (!contractAbi) {
throw new Error('Something went wrong. Try again later.');
}
......@@ -84,14 +73,14 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const hash = await walletClient?.writeContract({
args: _args,
abi: abi,
abi: contractAbi,
functionName: methodName,
address: addressHash as `0x${ string }`,
value: value as undefined,
});
return { hash };
}, [ isConnected, chain, abi, walletClient, addressHash, switchNetworkAsync ]);
}, [ isConnected, chain, contractAbi, walletClient, addressHash, switchNetworkAsync ]);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
return (
......
import { useQueryClient } from '@tanstack/react-query';
import type { Abi } from 'abitype';
import React from 'react';
import type { Address } from 'types/api/address';
import type { SmartContract } from 'types/api/contract';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
type ProviderProps = {
interface Params {
addressHash?: string;
children: React.ReactNode;
isProxy?: boolean;
isCustomAbi?: boolean;
}
type TContractContext = {
contractInfo: SmartContract | undefined;
proxyInfo: SmartContract | undefined;
customInfo: SmartContract | undefined;
};
const ContractContext = React.createContext<TContractContext>({
proxyInfo: undefined,
contractInfo: undefined,
customInfo: undefined,
});
export function ContractContextProvider({ addressHash, children }: ProviderProps) {
export default function useContractAbi({ addressHash, isProxy, isCustomAbi }: Params): Abi | undefined {
const queryClient = useQueryClient();
const { data: contractInfo } = useApiQuery('contract', {
......@@ -55,23 +44,15 @@ export function ContractContextProvider({ addressHash, children }: ProviderProps
},
});
const value = React.useMemo(() => ({
proxyInfo,
contractInfo,
customInfo,
} as TContractContext), [ proxyInfo, contractInfo, customInfo ]);
return React.useMemo(() => {
if (isProxy) {
return proxyInfo?.abi ?? undefined;
}
return (
<ContractContext.Provider value={ value }>
{ children }
</ContractContext.Provider>
);
}
if (isCustomAbi) {
return customInfo;
}
export function useContractContext() {
const context = React.useContext(ContractContext);
if (context === undefined) {
throw new Error('useContractContext must be used within a ContractContextProvider');
}
return context;
return contractInfo?.abi ?? undefined;
}, [ contractInfo?.abi, customInfo, isCustomAbi, isProxy, proxyInfo?.abi ]);
}
......@@ -94,20 +94,25 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
}, [ clearErrors ]);
const handleOpenWeb3Modal = React.useCallback(() => {
clearErrors('root');
openWeb3Modal();
}, [ openWeb3Modal ]);
}, [ clearErrors, openWeb3Modal ]);
const handleWeb3SignClick = React.useCallback(() => {
clearErrors('root');
if (!isConnected) {
return setError('root', { type: 'manual', message: 'Please connect to your Web3 wallet first' });
}
const message = getValues('message');
signMessage({ message });
}, [ getValues, signMessage, isConnected, setError ]);
}, [ clearErrors, isConnected, getValues, signMessage, setError ]);
const handleManualSignClick = React.useCallback(() => {
clearErrors('root');
onSubmit();
}, [ onSubmit ]);
}, [ clearErrors, onSubmit ]);
const button = (() => {
if (signMethod === 'manually') {
......
......@@ -86,11 +86,11 @@ const AddressPageContent = () => {
return 'Contract';
},
component: <AddressContract tabs={ contractTabs } addressHash={ hash }/>,
component: <AddressContract tabs={ contractTabs }/>,
subTabs: contractTabs.map(tab => tab.id),
} : undefined,
].filter(Boolean);
}, [ addressQuery.data, contractTabs, hash ]);
}, [ addressQuery.data, contractTabs ]);
const tags = (
<EntityTags
......
......@@ -178,7 +178,7 @@ const TokenPageContent = () => {
return 'Contract';
},
component: <AddressContract tabs={ contractTabs } addressHash={ hashString }/>,
component: <AddressContract tabs={ contractTabs }/>,
subTabs: contractTabs.map(tab => tab.id),
} : undefined,
].filter(Boolean);
......
......@@ -16,6 +16,7 @@ const DefaultView = () => {
const tokenData: TokenInfo = {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
circulating_market_cap: '117629601.61913824',
type: 'ERC-20',
symbol: 'SHAAAAAAAAAAAAA',
name: null,
......
......@@ -19,6 +19,7 @@ const LongNameAndManyTags = () => {
const tokenData: TokenInfo = {
address: '0xa77A39CC9680B10C00af5D4ABFc92e1F07406c64',
circulating_market_cap: null,
decimals: null,
exchange_rate: null,
holders: '294',
......
......@@ -12,6 +12,7 @@ test.use(devices['iPhone 13 Pro']);
test('unnamed', async({ mount }) => {
const data: TokenInfo = {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
circulating_market_cap: '117629601.61913824',
type: 'ERC-20',
symbol: 'xDAI',
name: null,
......@@ -33,6 +34,7 @@ test('unnamed', async({ mount }) => {
test('named', async({ mount }) => {
const data: TokenInfo = {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
circulating_market_cap: '117629601.61913824',
type: 'ERC-20',
symbol: 'SHA',
name: 'Shavuha token',
......@@ -55,6 +57,7 @@ test('with logo and long symbol', async({ mount, page }) => {
const API_URL = 'https://example.com/logo.png';
const data: TokenInfo = {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
circulating_market_cap: '117629601.61913824',
type: 'ERC-20',
symbol: 'SHAAAAAAAAAAAAA',
name: null,
......
......@@ -8,6 +8,7 @@ import isSelfHosted from 'lib/isSelfHosted';
import AdbutlerBanner from './AdbutlerBanner';
import CoinzillaBanner from './CoinzillaBanner';
import SliseBanner from './SliseBanner';
const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: boolean }) => {
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
......@@ -16,14 +17,24 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo
return null;
}
const content = appConfig.ad.adButlerOn ? <AdbutlerBanner/> : <CoinzillaBanner/>;
const content = (() => {
if (appConfig.ad.adButlerOn) {
return <AdbutlerBanner/>;
}
if (appConfig.ad.sliseOn) {
return <SliseBanner/>;
}
return <CoinzillaBanner/>;
})();
return (
<Skeleton
className={ className }
isLoaded={ !isLoading }
borderRadius="none"
maxW={ appConfig.ad.adButlerOn ? '760px' : '728px' }
maxW={ appConfig.ad.adButlerOn ? appConfig.ad.adButlerConfigDesktop?.width : '728px' }
w="100%"
>
{ content }
......
......@@ -3,29 +3,14 @@ import { Flex, chakra } from '@chakra-ui/react';
import Script from 'next/script';
import React from 'react';
const scriptText1 = `if (!window.AdButler){(function(){var s = document.createElement("script"); s.async = true; s.type = "text/javascript";s.src = 'https://servedbyadbutler.com/app.js';var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(s, n);}());}`;
const scriptText2 = `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || '';
const isMobile = window.matchMedia("only screen and (max-width: 760px)").matches;
if (isMobile) {
var plc539876 = window.plc539876 || 0;
document.getElementById('ad-banner').innerHTML += '<'+'div id="placement_539876_'+plc539876+'"></'+'div>';
document.getElementById("ad-banner").className = "ad-container mb-3";
AdButler.ads.push({handler: function(opt){ AdButler.register(182226, 539876, [320,100], 'placement_539876_'+opt.place, opt); }, opt: { place: plc539876++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
} else {
var plc523705 = window.plc523705 || 0;
document.getElementById('ad-banner').innerHTML += '<'+'div id="placement_523705_'+plc523705+'"></'+'div>';
AdButler.ads.push({handler: function(opt){ AdButler.register(182226, 523705, [728,90], 'placement_523705_'+opt.place, opt); }, opt: { place: plc523705++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
}
`;
import { connectAdbutler, placeAd } from 'ui/shared/ad/adbutlerScript';
const AdbutlerBanner = ({ className }: { className?: string }) => {
return (
<Flex className={ className } id="adBanner" h={{ base: '100px', lg: '90px' }}>
<div id="ad-banner"></div>
<Script id="ad-butler-1">{ scriptText1 }</Script>
<Script id="ad-butler-2">{ scriptText2 }</Script>
<Script id="ad-butler-1">{ connectAdbutler }</Script>
<Script id="ad-butler-2">{ placeAd }</Script>
</Flex>
);
};
......
import { Flex, chakra } from '@chakra-ui/react';
import { SliseAd } from '@slise/embed-react';
import React from 'react';
const SliseBanner = ({ className }: { className?: string }) => {
return (
<>
<Flex className={ className } h="90px" display={{ base: 'none', lg: 'flex' }}>
<SliseAd
slotId="leaderboard"
pub="pub-10"
format="728x90"
style={{ width: '728px', height: '90px' }}/>
</Flex>
<Flex className={ className } h="90px" display={{ base: 'flex', lg: 'none' }}>
<SliseAd
slotId="leaderboard"
pub="pub-10"
format="270x90"
style={{ width: '270px', height: '90px' }}/>
</Flex>
</>
);
};
export default chakra(SliseBanner);
/* eslint-disable max-len */
import appConfig from 'configs/app/config';
export const connectAdbutler = `if (!window.AdButler){(function(){var s = document.createElement("script"); s.async = true; s.type = "text/javascript";s.src = 'https://servedbyadbutler.com/app.js';var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(s, n);}());}`;
export const placeAd = `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || '';
const isMobile = window.matchMedia("only screen and (max-width: 760px)").matches;
if (isMobile) {
var plc${ appConfig.ad.adButlerConfigMobile?.id } = window.plc${ appConfig.ad.adButlerConfigMobile?.id } || 0;
document.getElementById('ad-banner').innerHTML += '<'+'div id="placement_${ appConfig.ad.adButlerConfigMobile?.id }_'+plc${ appConfig.ad.adButlerConfigMobile?.id }+'"></'+'div>';
document.getElementById("ad-banner").className = "ad-container mb-3";
AdButler.ads.push({handler: function(opt){ AdButler.register(182226, ${ appConfig.ad.adButlerConfigMobile?.id }, [${ appConfig.ad.adButlerConfigMobile?.width },${ appConfig.ad.adButlerConfigMobile?.height }], 'placement_${ appConfig.ad.adButlerConfigMobile?.id }_'+opt.place, opt); }, opt: { place: plc${ appConfig.ad.adButlerConfigMobile?.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
} else {
var plc${ appConfig.ad.adButlerConfigDesktop?.id } = window.plc${ appConfig.ad.adButlerConfigDesktop?.id } || 0;
document.getElementById('ad-banner').innerHTML += '<'+'div id="placement_${ appConfig.ad.adButlerConfigDesktop?.id }_'+plc${ appConfig.ad.adButlerConfigDesktop?.id }+'"></'+'div>';
AdButler.ads.push({handler: function(opt){ AdButler.register(182226, ${ appConfig.ad.adButlerConfigDesktop?.id }, [${ appConfig.ad.adButlerConfigDesktop?.width },${ appConfig.ad.adButlerConfigDesktop?.height }], 'placement_${ appConfig.ad.adButlerConfigDesktop?.id }_'+opt.place, opt); }, opt: { place: plc${ appConfig.ad.adButlerConfigDesktop?.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
}
`;
import { Box, Flex, Grid, Link, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { scroller } from 'react-scroll';
......@@ -65,17 +66,16 @@ const TokenDetails = ({ tokenQuery }: Props) => {
const {
exchange_rate: exchangeRate,
total_supply: totalSupply,
circulating_market_cap: marketCap,
decimals,
symbol,
type,
} = tokenQuery.data || {};
let marketcap;
let totalSupplyValue;
if (type === 'ERC-20') {
const totalValue = totalSupply ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined;
marketcap = totalValue?.usd;
totalSupplyValue = totalValue?.valueStr;
} else {
totalSupplyValue = Number(totalSupply).toLocaleString();
......@@ -100,7 +100,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
</Skeleton>
</DetailsInfoItem>
) }
{ marketcap && (
{ marketCap && (
<DetailsInfoItem
title="Fully diluted market cap"
hint="Total supply * Price"
......@@ -108,7 +108,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
isLoading={ tokenQuery.isPlaceholderData }
>
<Skeleton isLoaded={ !tokenQuery.isPlaceholderData } display="inline-block">
<span>{ `$${ marketcap }` }</span>
<span>{ `$${ BigNumber(marketCap).toFormat() }` }</span>
</Skeleton>
</DetailsInfoItem>
) }
......
import { Flex, HStack, Grid, GridItem, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink';
import Tag from 'ui/shared/chakra/Tag';
......@@ -29,17 +29,14 @@ const TokensTableItem = ({
const {
address,
total_supply: totalSupply,
exchange_rate: exchangeRate,
type,
name,
symbol,
decimals,
holders,
circulating_market_cap: marketCap,
} = token;
const totalValue = totalSupply !== null ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined;
const tokenString = [ name, symbol && `(${ symbol })` ].filter(Boolean).join(' ');
return (
......@@ -74,10 +71,10 @@ const TokensTableItem = ({
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary"><span>{ exchangeRate }</span></Skeleton>
</HStack>
) }
{ totalValue?.usd && (
{ marketCap && (
<HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>On-chain market cap</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary"><span>{ totalValue.usd }</span></Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary"><span>{ BigNumber(marketCap).toFormat() }</span></Skeleton>
</HStack>
) }
<HStack spacing={ 3 }>
......
import { Box, Flex, Td, Tr, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue';
import Address from 'ui/shared/address/Address';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -29,17 +29,14 @@ const TokensTableItem = ({
const {
address,
total_supply: totalSupply,
exchange_rate: exchangeRate,
type,
name,
symbol,
decimals,
holders,
circulating_market_cap: marketCap,
} = token;
const totalValue = totalSupply !== null ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined;
const tokenString = [ name, symbol && `(${ symbol })` ].filter(Boolean).join(' ');
return (
......@@ -81,7 +78,7 @@ const TokensTableItem = ({
</Td>
<Td isNumeric maxWidth="300px" width="300px">
<Skeleton isLoaded={ !isLoading } fontSize="sm" lineHeight="24px" fontWeight={ 500 } display="inline-block">
{ totalValue?.usd && `$${ totalValue.usd }` }
{ marketCap && `$${ BigNumber(marketCap).toFormat() }` }
</Skeleton>
</Td>
<Td isNumeric>
......
......@@ -13,6 +13,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import HashStringShorten from 'ui/shared/HashStringShorten';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
......@@ -34,6 +35,7 @@ const VerifiedContractsListItem = ({ data, isLoading }: Props) => {
<Skeleton isLoaded={ !isLoading } color="text_secondary" ml="auto">
<HashStringShorten hash={ data.address.hash } isTooltipDisabled/>
</Skeleton>
<CopyToClipboard text={ data.address.hash } ml={ -1 } isLoading={ isLoading }/>
</Address>
<Flex columnGap={ 3 }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Balance { appConfig.network.currency.symbol }</Skeleton>
......
......@@ -12,6 +12,7 @@ import dayjs from 'lib/date/dayjs';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import HashStringShorten from 'ui/shared/HashStringShorten';
interface Props {
......@@ -31,9 +32,12 @@ const VerifiedContractsTableItem = ({ data, isLoading }: Props) => {
<AddressIcon address={ data.address } isLoading={ isLoading }/>
<Flex columnGap={ 2 } flexWrap="wrap" w="calc(100% - 32px)">
<AddressLink hash={ data.address.hash } type="address" alias={ data.address.name } isLoading={ isLoading } my={ 1 }/>
<Skeleton isLoaded={ !isLoading } color="text_secondary" my={ 1 }>
<HashStringShorten hash={ data.address.hash } isTooltipDisabled/>
</Skeleton>
<Flex alignItems="center">
<Skeleton isLoaded={ !isLoading } color="text_secondary" my={ 1 }>
<HashStringShorten hash={ data.address.hash } isTooltipDisabled/>
</Skeleton>
<CopyToClipboard text={ data.address.hash } isLoading={ isLoading }/>
</Flex>
</Flex>
</Flex>
</Td>
......
......@@ -3928,6 +3928,13 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@slise/embed-react@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@slise/embed-react/-/embed-react-2.2.0.tgz#346bf34d375144a7f5173354c1672d3687fa8b47"
integrity sha512-btboJc24ABEg5ncbVnab+asKarp3kTSTdMHHcndrnkCDlXNQNSw3vL0Lv8tanqgE3Ogt51AF8QGEhWDNOZAcxQ==
dependencies:
react-script-hook "^1.7.2"
"@solana/buffer-layout@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15"
......@@ -4671,6 +4678,11 @@
dependencies:
"@types/node" "*"
"@types/crypto-js@^4.1.1":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d"
integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==
"@types/csp-dev@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/csp-dev/-/csp-dev-1.0.0.tgz#59e2fd69f276988b349765c2f6a39ea0a4a1a161"
......@@ -6859,6 +6871,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
crypto-js@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
css-box-model@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
......@@ -11550,6 +11567,11 @@ react-remove-scroll@^2.4.3, react-remove-scroll@^2.5.5:
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-script-hook@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/react-script-hook/-/react-script-hook-1.7.2.tgz#ec130d67f9a25fcde57fbfd1faa87e5b97521948"
integrity sha512-fhyCEfXb94fag34UPRF0zry1XGwmVY+79iibWwTqAoOiCzYJQOYTiWJ7CnqglA9tMSV8g45cQpHCMcBwr7dwhA==
react-scroll@^1.8.7:
version "1.8.7"
resolved "https://registry.yarnpkg.com/react-scroll/-/react-scroll-1.8.7.tgz#8020035329efad00f03964e18aff6822137de3aa"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment