Merge branch 'main' of github.com:blockscout/frontend into marketplace-config
@@ -52,7 +52,6 @@ jobs:
|
||||
name: Run unit tests with Jest
|
||||
needs: [ lint, type_check ]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ false }} # disable since there are no jest test yet
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
@@ -45,8 +45,8 @@ The app instance could be customized by passing following variables to NodeJS en
|
||||
| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | `Gnosis Chain` |
|
||||
| NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` *(optional)* | Used for SEO attributes (page title and description) | `OoG` |
|
||||
| NEXT_PUBLIC_NETWORK_TYPE | `string` *(optional)* | Network type (used for matching pre-defined assets, e.g network logo and icon, which are stored in the project). See all possible values here | `xdai_mainnet` |
|
||||
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org/](https://chainlist.org/) for the reference | `99` |
|
||||
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` *(optional)* | Chain server RPC url, see [https://chainlist.org/](https://chainlist.org/) for the reference. If not provided, some functionality of the explorer, related to smart contracts interaction and third-party apps integration, will be unavailable | `https://core.poa.network` |
|
||||
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference | `99` |
|
||||
| NEXT_PUBLIC_NETWORK_RPC_URL | `string` *(optional)* | Chain server RPC url, see [https://chainlist.org](https://chainlist.org) for the reference. If not provided, some functionality of the explorer, related to smart contracts interaction and third-party apps integration, will be unavailable | `https://core.poa.network` |
|
||||
| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | `Ether` |
|
||||
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | `ETH` |
|
||||
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | `18` |
|
||||
|
||||
@@ -5,7 +5,7 @@ blockscout:
|
||||
app: blockscout
|
||||
enabled: true
|
||||
image:
|
||||
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.0-prerelease-303a0afa
|
||||
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.2-prerelease-84e22ed2
|
||||
replicas:
|
||||
app: 1
|
||||
# init container
|
||||
@@ -93,14 +93,8 @@ blockscout:
|
||||
_default: 'true'
|
||||
BLOCKSCOUT_HOST:
|
||||
_default: 'blockscout-optimism-goerli.test.aws-k8s.blockscout.com'
|
||||
SOCKET_ROOT:
|
||||
_default: "/"
|
||||
NETWORK_PATH:
|
||||
_default: "/"
|
||||
API_PATH:
|
||||
_default: "/"
|
||||
API_BASE_PATH:
|
||||
_default: "/"
|
||||
APPS_MENU:
|
||||
_default: 'true'
|
||||
EXTERNAL_APPS:
|
||||
@@ -149,6 +143,9 @@ postgres:
|
||||
|
||||
command: '["docker-entrypoint.sh", "-c"]'
|
||||
args: '["max_connections=300"]'
|
||||
|
||||
customShm:
|
||||
enabled: false
|
||||
|
||||
files:
|
||||
enabled: true
|
||||
@@ -337,7 +334,7 @@ frontend:
|
||||
_default: "0.2"
|
||||
environment:
|
||||
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
|
||||
_default: v5.1.0-beta
|
||||
_default: v5.1.2-beta
|
||||
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
|
||||
_default: https://github.com/blockscout/blockscout
|
||||
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
|
||||
|
||||
@@ -47,14 +47,14 @@ blockscout:
|
||||
resources:
|
||||
limits:
|
||||
memory:
|
||||
_default: "5Gi"
|
||||
_default: "4Gi"
|
||||
cpu:
|
||||
_default: "4"
|
||||
_default: "3"
|
||||
requests:
|
||||
memory:
|
||||
_default: "5Gi"
|
||||
_default: "4Gi"
|
||||
cpu:
|
||||
_default: "4"
|
||||
_default: "3"
|
||||
# node label
|
||||
nodeSelector:
|
||||
enabled: true
|
||||
@@ -73,23 +73,17 @@ blockscout:
|
||||
# # _default: ws://geth-svc:8546
|
||||
# _default: ws://geth-svc.goerli.svc.cluster.local:8546
|
||||
BLOCKSCOUT_VERSION:
|
||||
_default: v5.1.0-beta
|
||||
_default: v5.1.2-beta
|
||||
ECTO_USE_SSL:
|
||||
_default: 'false'
|
||||
ETHEREUM_JSONRPC_VARIANT:
|
||||
_default: geth
|
||||
HEART_BEAT_TIMEOUT:
|
||||
_default: 30
|
||||
SHOW_PRICE_CHART:
|
||||
_default: false
|
||||
CACHE_BLOCK_COUNT_PERIOD:
|
||||
_default: 7200
|
||||
PORT:
|
||||
_default: 4000
|
||||
SUBNETWORK:
|
||||
_default: Ethereum
|
||||
HEALTHY_BLOCKS_PERIOD:
|
||||
_default: 60
|
||||
NETWORK:
|
||||
_default: (Goerli)
|
||||
NETWORK_ICON:
|
||||
@@ -100,10 +94,6 @@ blockscout:
|
||||
_default: ETH
|
||||
LOGO:
|
||||
_default: /images/goerli_logo.svg
|
||||
HISTORY_FETCH_INTERVAL:
|
||||
_default: 60
|
||||
TXS_HISTORIAN_INIT_LAG:
|
||||
_default: 0
|
||||
TXS_STATS_DAYS_TO_COMPILE_AT_INIT:
|
||||
_default: 1
|
||||
COIN_BALANCE_HISTORY_DAYS:
|
||||
@@ -138,7 +128,7 @@ blockscout:
|
||||
_default: '20s'
|
||||
INDEXER_INTERNAL_TRANSACTIONS_BATCH_SIZE:
|
||||
_default: 15
|
||||
INDEXER_DISABLE_EMPTY_BLOCK_SANITIZER:
|
||||
INDEXER_DISABLE_EMPTY_BLOCKS_SANITIZER:
|
||||
_default: 'true'
|
||||
INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER:
|
||||
_default: 'true'
|
||||
@@ -151,13 +141,13 @@ blockscout:
|
||||
DISABLE_INDEXER:
|
||||
_default: 'false'
|
||||
FIRST_BLOCK:
|
||||
_default: '8446041'
|
||||
_default: '8739119'
|
||||
LAST_BLOCK:
|
||||
_default: '8446041'
|
||||
_default: '8739119'
|
||||
TRACE_FIRST_BLOCK:
|
||||
_default: '8446041'
|
||||
_default: '8739119'
|
||||
TRACE_LAST_BLOCK:
|
||||
_default: '8446041'
|
||||
_default: '8739119'
|
||||
|
||||
postgres:
|
||||
enabled: true
|
||||
@@ -170,6 +160,10 @@ postgres:
|
||||
# strategy: Recreate
|
||||
|
||||
persistence: true
|
||||
customShm:
|
||||
enabled: true
|
||||
sizeLimit: 256Mi
|
||||
|
||||
storage: 1400Gi
|
||||
|
||||
resources:
|
||||
@@ -301,12 +295,12 @@ frontend:
|
||||
resources:
|
||||
limits:
|
||||
memory:
|
||||
_default: "0.1Gi"
|
||||
_default: "0.3Gi"
|
||||
cpu:
|
||||
_default: "0.5"
|
||||
requests:
|
||||
memory:
|
||||
_default: "0.1Gi"
|
||||
_default: "0.3Gi"
|
||||
cpu:
|
||||
_default: "0.5"
|
||||
environment:
|
||||
@@ -344,7 +338,7 @@ frontend:
|
||||
NEXT_PUBLIC_API_BASE_PATH:
|
||||
_default: /
|
||||
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
|
||||
_default: v4.1.8-beta
|
||||
_default: v5.1.2-beta
|
||||
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
|
||||
_default: https://github.com/blockscout/blockscout
|
||||
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
|
||||
|
||||
@@ -17,7 +17,6 @@ frontend:
|
||||
# - "/(apps|auth/profile|account)"
|
||||
- "/"
|
||||
prefix:
|
||||
# - "/(apps|auth/profile|account)"
|
||||
- "/_next"
|
||||
- "/node-api"
|
||||
- "/account"
|
||||
@@ -46,15 +45,15 @@ frontend:
|
||||
memory:
|
||||
_default: "2Gi"
|
||||
cpu:
|
||||
_default: "2"
|
||||
_default: "1"
|
||||
requests:
|
||||
memory:
|
||||
_default: "2Gi"
|
||||
cpu:
|
||||
_default: "2"
|
||||
_default: "1"
|
||||
environment:
|
||||
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
|
||||
_default: v4.1.8-beta
|
||||
_default: v5.1.2-beta
|
||||
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
|
||||
_default: https://github.com/blockscout/blockscout
|
||||
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
|
||||
|
||||
@@ -374,7 +374,7 @@ export const RESOURCES = {
|
||||
},
|
||||
|
||||
deposits_count: {
|
||||
path: '/api/v2/optimism/deposits-count',
|
||||
path: '/api/v2/optimism/deposits/count',
|
||||
},
|
||||
|
||||
withdrawals: {
|
||||
@@ -384,7 +384,7 @@ export const RESOURCES = {
|
||||
},
|
||||
|
||||
withdrawals_count: {
|
||||
path: '/api/v2/optimism/withdrawals-count',
|
||||
path: '/api/v2/optimism/withdrawals/count',
|
||||
},
|
||||
|
||||
output_roots: {
|
||||
@@ -394,7 +394,7 @@ export const RESOURCES = {
|
||||
},
|
||||
|
||||
output_roots_count: {
|
||||
path: '/api/v2/optimism/output-roots-count',
|
||||
path: '/api/v2/optimism/output-roots/count',
|
||||
},
|
||||
|
||||
txn_batches: {
|
||||
@@ -404,7 +404,7 @@ export const RESOURCES = {
|
||||
},
|
||||
|
||||
txn_batches_count: {
|
||||
path: '/api/v2/optimism/txn-batches-count',
|
||||
path: '/api/v2/optimism/txn-batches/count',
|
||||
},
|
||||
|
||||
// DEPRECATED
|
||||
|
||||
@@ -8,6 +8,7 @@ function generateCspPolicy() {
|
||||
descriptors.googleAnalytics(),
|
||||
descriptors.googleFonts(),
|
||||
descriptors.googleReCaptcha(),
|
||||
descriptors.monaco(),
|
||||
descriptors.sentry(),
|
||||
descriptors.walletConnect(),
|
||||
);
|
||||
|
||||
@@ -3,5 +3,6 @@ export { app } from './app';
|
||||
export { googleAnalytics } from './googleAnalytics';
|
||||
export { googleFonts } from './googleFonts';
|
||||
export { googleReCaptcha } from './googleReCaptcha';
|
||||
export { monaco } from './monaco';
|
||||
export { sentry } from './sentry';
|
||||
export { walletConnect } from './walletConnect';
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type CspDev from 'csp-dev';
|
||||
|
||||
import { KEY_WORDS } from '../utils';
|
||||
|
||||
export function monaco(): CspDev.DirectiveDescriptor {
|
||||
return {
|
||||
'script-src': [
|
||||
KEY_WORDS.BLOB,
|
||||
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/loader.js',
|
||||
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/editor/editor.main.js',
|
||||
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/editor/editor.main.nls.js',
|
||||
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/basic-languages/solidity/solidity.js',
|
||||
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/base/worker/workerMain.js',
|
||||
],
|
||||
'style-src': [
|
||||
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/editor/editor.main.css',
|
||||
],
|
||||
'font-src': [
|
||||
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/base/browser/ui/codicons/codicon/codicon.ttf',
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -19,3 +19,6 @@ export const minus = String.fromCharCode(8722); // −
|
||||
export const leftLineArrow = String.fromCharCode(8592); // ←
|
||||
export const rightLineArrow = String.fromCharCode(8594); // →
|
||||
export const apos = String.fromCharCode(39); // apostrophe '
|
||||
export const shift = String.fromCharCode(8679); // upwards white arrow ⇧
|
||||
export const cmd = String.fromCharCode(8984); // place of interest sign ⌘
|
||||
export const alt = String.fromCharCode(9095); // alternate key symbol ⎇
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function isMetaKey(event: React.KeyboardEvent) {
|
||||
return event.metaKey || event.getModifierState('Meta') || event.getModifierState('OS');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
const stripLeadingSlash = (str: string) => str[0] === '/' ? str.slice(1) : str;
|
||||
|
||||
export default stripLeadingSlash;
|
||||
@@ -34,6 +34,7 @@
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@metamask/providers": "^10.2.1",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@sentry/nextjs": "^7.12.1",
|
||||
"@sentry/react": "^7.24.0",
|
||||
"@sentry/tracing": "^7.24.0",
|
||||
@@ -43,7 +44,6 @@
|
||||
"@types/react-scroll": "^1.8.4",
|
||||
"@web3modal/ethereum": "^2.0.0-rc.2",
|
||||
"@web3modal/react": "^2.0.0-rc.2",
|
||||
"ace-builds": "^1.14.0",
|
||||
"bignumber.js": "^9.1.0",
|
||||
"chakra-react-select": "^4.4.3",
|
||||
"d3": "^7.6.1",
|
||||
@@ -56,6 +56,7 @@
|
||||
"graphql-ws": "^5.11.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash": "^4.0.0",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"next": "12.2.5",
|
||||
"nextjs-routes": "^1.0.8",
|
||||
"node-fetch": "^3.2.9",
|
||||
@@ -66,7 +67,6 @@
|
||||
"pino-pretty": "^9.1.1",
|
||||
"qrcode": "^1.5.1",
|
||||
"react": "18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-google-recaptcha": "^2.1.0",
|
||||
"react-hook-form": "^7.33.1",
|
||||
@@ -96,6 +96,7 @@
|
||||
"@types/swagger-ui-react": "^4.11.0",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-next": "^12.3.0",
|
||||
@@ -112,6 +113,7 @@
|
||||
"mockdate": "^3.0.5",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"playwright": "1.31.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"svgo": "^2.8.0",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
|
||||
@@ -13,7 +13,7 @@ const API_URL = buildApiUrl('address_token_transfers', { hash: '0xd789a607CEac2f
|
||||
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token_hash: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
|
||||
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
|
||||
const [ socketAlert, setSocketAlert ] = React.useState('');
|
||||
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
|
||||
|
||||
const tokenFilter = getQueryParamString(router.query.token_hash) || undefined;
|
||||
const tokenFilter = getQueryParamString(router.query.token) || undefined;
|
||||
|
||||
const [ filters, setFilters ] = React.useState<Filters>(
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page
|
||||
status: 200,
|
||||
body: JSON.stringify(contractMock.withChangedByteCode),
|
||||
}));
|
||||
await page.route('https://cdn.jsdelivr.net/npm/[email protected]/**', (route) => route.abort());
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
@@ -36,6 +37,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => {
|
||||
status: 200,
|
||||
body: JSON.stringify(contractMock.withMultiplePaths),
|
||||
}));
|
||||
await page.route('https://cdn.jsdelivr.net/npm/[email protected]/**', (route) => route.abort());
|
||||
|
||||
await mount(
|
||||
<TestApp>
|
||||
@@ -54,6 +56,7 @@ test('verified via sourcify', async({ mount, page }) => {
|
||||
status: 200,
|
||||
body: JSON.stringify(contractMock.verifiedViaSourcify),
|
||||
}));
|
||||
await page.route('https://cdn.jsdelivr.net/npm/[email protected]/**', (route) => route.abort());
|
||||
|
||||
await mount(
|
||||
<TestApp>
|
||||
@@ -70,6 +73,7 @@ test('self destructed', async({ mount, page }) => {
|
||||
status: 200,
|
||||
body: JSON.stringify(contractMock.selfDestructed),
|
||||
}));
|
||||
await page.route('https://cdn.jsdelivr.net/npm/[email protected]/**', (route) => route.abort());
|
||||
|
||||
await mount(
|
||||
<TestApp>
|
||||
@@ -87,6 +91,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => {
|
||||
status: 200,
|
||||
body: JSON.stringify(contractMock.withTwinAddress),
|
||||
}));
|
||||
await page.route('https://cdn.jsdelivr.net/npm/[email protected]/**', (route) => route.abort());
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
@@ -103,6 +108,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => {
|
||||
status: 200,
|
||||
body: JSON.stringify(contractMock.withProxyAddress),
|
||||
}));
|
||||
await page.route('https://cdn.jsdelivr.net/npm/[email protected]/**', (route) => route.abort());
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
@@ -119,6 +125,7 @@ test('non verified', async({ mount, page }) => {
|
||||
status: 200,
|
||||
body: JSON.stringify(contractMock.nonVerified),
|
||||
}));
|
||||
await page.route('https://cdn.jsdelivr.net/npm/[email protected]/**', (route) => route.abort());
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
|
||||
@@ -68,7 +68,7 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
|
||||
];
|
||||
}, [ data ]);
|
||||
|
||||
const { control, handleSubmit, setValue } = useForm<MethodFormFields>({
|
||||
const { control, handleSubmit, setValue, getValues } = useForm<MethodFormFields>({
|
||||
defaultValues: _fromPairs(inputs.map(({ name }, index) => [ getFieldName(name, index), '' ])),
|
||||
});
|
||||
|
||||
@@ -118,11 +118,13 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
|
||||
<ContractMethodField
|
||||
key={ fieldName }
|
||||
name={ fieldName }
|
||||
valueType={ type }
|
||||
placeholder={ `${ name }(${ type })` }
|
||||
control={ control }
|
||||
setValue={ setValue }
|
||||
getValues={ getValues }
|
||||
isDisabled={ isLoading }
|
||||
onClear={ handleFormChange }
|
||||
onChange={ handleFormChange }
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
|
||||
@@ -1,29 +1,50 @@
|
||||
import { FormControl, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
|
||||
import {
|
||||
FormControl,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { Control, ControllerRenderProps, UseFormSetValue } from 'react-hook-form';
|
||||
import type { Control, ControllerRenderProps, UseFormGetValues, UseFormSetValue } from 'react-hook-form';
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
import type { MethodFormFields } from './types';
|
||||
import type { SmartContractMethodArgType } from 'types/api/contract';
|
||||
|
||||
import InputClearButton from 'ui/shared/InputClearButton';
|
||||
|
||||
import ContractMethodFieldZeroes from './ContractMethodFieldZeroes';
|
||||
import { addZeroesAllowed } from './utils';
|
||||
|
||||
interface Props {
|
||||
control: Control<MethodFormFields>;
|
||||
setValue: UseFormSetValue<MethodFormFields>;
|
||||
getValues: UseFormGetValues<MethodFormFields>;
|
||||
placeholder: string;
|
||||
name: string;
|
||||
valueType: SmartContractMethodArgType;
|
||||
isDisabled: boolean;
|
||||
onClear: () => void;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
const ContractMethodField = ({ control, name, placeholder, setValue, isDisabled, onClear }: Props) => {
|
||||
const ContractMethodField = ({ control, name, valueType, placeholder, setValue, getValues, isDisabled, onChange }: Props) => {
|
||||
const ref = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleClear = React.useCallback(() => {
|
||||
setValue(name, '');
|
||||
onClear();
|
||||
onChange();
|
||||
ref.current?.focus();
|
||||
}, [ name, onClear, setValue ]);
|
||||
}, [ name, onChange, setValue ]);
|
||||
|
||||
const handleAddZeroesClick = React.useCallback((power: number) => {
|
||||
const value = getValues()[name];
|
||||
const zeroes = Array(power).fill('0').join('');
|
||||
const newValue = value ? value + zeroes : '1' + zeroes;
|
||||
setValue(name, newValue);
|
||||
onChange();
|
||||
}, [ getValues, name, onChange, setValue ]);
|
||||
|
||||
const hasZerosControl = addZeroesAllowed(valueType);
|
||||
|
||||
const renderInput = React.useCallback(({ field }: { field: ControllerRenderProps<MethodFormFields> }) => {
|
||||
return (
|
||||
@@ -32,23 +53,23 @@ const ContractMethodField = ({ control, name, placeholder, setValue, isDisabled,
|
||||
flexBasis={{ base: '100%', lg: 'calc((100% - 24px) / 3 - 65px)' }}
|
||||
w={{ base: '100%', lg: 'auto' }}
|
||||
flexGrow={ 1 }
|
||||
isDisabled={ isDisabled
|
||||
}>
|
||||
isDisabled={ isDisabled }
|
||||
>
|
||||
<InputGroup size="xs">
|
||||
<Input
|
||||
{ ...field }
|
||||
ref={ ref }
|
||||
placeholder={ placeholder }
|
||||
paddingRight={ hasZerosControl ? '120px' : '40px' }
|
||||
/>
|
||||
{ field.value && (
|
||||
<InputRightElement>
|
||||
<InputClearButton onClick={ handleClear } isDisabled={ isDisabled }/>
|
||||
</InputRightElement>
|
||||
) }
|
||||
<InputRightElement w="auto" right={ 1 }>
|
||||
{ field.value && <InputClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
|
||||
{ hasZerosControl && <ContractMethodFieldZeroes onClick={ handleAddZeroesClick }/> }
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
);
|
||||
}, [ handleClear, isDisabled, name, placeholder ]);
|
||||
}, [ name, isDisabled, placeholder, hasZerosControl, handleClear, handleAddZeroesClick ]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
chakra,
|
||||
Popover,
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Portal,
|
||||
Button,
|
||||
List,
|
||||
ListItem,
|
||||
Icon,
|
||||
useDisclosure,
|
||||
Input,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import iconEastMini from 'icons/arrows/east-mini.svg';
|
||||
import iconCheck from 'icons/check.svg';
|
||||
import { times } from 'lib/html-entities';
|
||||
|
||||
interface Props {
|
||||
onClick: (power: number) => void;
|
||||
}
|
||||
|
||||
const ContractMethodFieldZeroes = ({ onClick }: Props) => {
|
||||
const [ selectedOption, setSelectedOption ] = React.useState<number | undefined>(18);
|
||||
const [ customValue, setCustomValue ] = React.useState<number>();
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
|
||||
const handleOptionClick = React.useCallback((event: React.MouseEvent) => {
|
||||
const id = Number((event.currentTarget as HTMLDivElement).getAttribute('data-id'));
|
||||
if (!Object.is(id, NaN)) {
|
||||
setSelectedOption((prev) => prev === id ? undefined : id);
|
||||
setCustomValue(undefined);
|
||||
onClose();
|
||||
}
|
||||
}, [ onClose ]);
|
||||
|
||||
const handleInputChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomValue(Number(event.target.value));
|
||||
setSelectedOption(undefined);
|
||||
}, []);
|
||||
|
||||
const value = selectedOption || customValue;
|
||||
|
||||
const handleButtonClick = React.useCallback(() => {
|
||||
value && onClick(value);
|
||||
}, [ onClick, value ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ Boolean(value) && (
|
||||
<Button
|
||||
px={ 1 }
|
||||
lineHeight={ 6 }
|
||||
h={ 6 }
|
||||
fontWeight={ 500 }
|
||||
ml={ 1 }
|
||||
variant="subtle"
|
||||
colorScheme="gray"
|
||||
display="inline"
|
||||
onClick={ handleButtonClick }
|
||||
>
|
||||
{ times }
|
||||
<chakra.span>10</chakra.span>
|
||||
<chakra.span fontSize="xs" lineHeight={ 4 } verticalAlign="super">{ value }</chakra.span>
|
||||
</Button>
|
||||
) }
|
||||
<Popover placement="bottom-end" isLazy isOpen={ isOpen } onClose={ onClose }>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
variant="subtle"
|
||||
colorScheme="gray"
|
||||
size="xs"
|
||||
cursor="pointer"
|
||||
ml={ 1 }
|
||||
p={ 0 }
|
||||
onClick={ onToggle }
|
||||
>
|
||||
<Icon as={ iconEastMini } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } boxSize={ 6 }/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent w="110px">
|
||||
<PopoverBody py={ 2 }>
|
||||
<List>
|
||||
{ [ 8, 12, 16, 18, 20 ].map((id) => (
|
||||
<ListItem
|
||||
key={ id }
|
||||
py={ 2 }
|
||||
data-id={ id }
|
||||
onClick={ handleOptionClick }
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
cursor="pointer"
|
||||
>
|
||||
<span>10*{ id }</span>
|
||||
{ selectedOption === id && <Icon as={ iconCheck } boxSize={ 6 } color="blue.600"/> }
|
||||
</ListItem>
|
||||
)) }
|
||||
<ListItem
|
||||
py={ 2 }
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<span>10*</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={ 0 }
|
||||
max={ 100 }
|
||||
ml={ 3 }
|
||||
size="xs"
|
||||
onChange={ handleInputChange }
|
||||
value={ customValue || '' }
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ContractMethodFieldZeroes);
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Box, chakra, Flex, Text, Tooltip } from '@chakra-ui/react';
|
||||
import { Flex, Text, Tooltip } from '@chakra-ui/react';
|
||||
import { route } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
import type { SmartContract } from 'types/api/contract';
|
||||
|
||||
import CodeEditor from 'ui/shared/CodeEditor';
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
data: string;
|
||||
@@ -37,38 +38,25 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
|
||||
</Tooltip>
|
||||
) : null;
|
||||
|
||||
if (!additionalSource?.length) {
|
||||
return (
|
||||
<section>
|
||||
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
|
||||
{ heading }
|
||||
{ diagramLink }
|
||||
<CopyToClipboard text={ data }/>
|
||||
</Flex>
|
||||
<CodeEditor value={ data } id="source_code"/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
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 copyToClipboard = editorData.length === 1 ?
|
||||
<CopyToClipboard text={ editorData[0].source_code }/> :
|
||||
null;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
|
||||
{ heading }
|
||||
{ diagramLink }
|
||||
{ copyToClipboard }
|
||||
</Flex>
|
||||
<Flex flexDir="column" rowGap={ 3 }>
|
||||
{ [ { file_path: filePath, source_code: data }, ...additionalSource ].map((item, index, array) => (
|
||||
<Box key={ index }>
|
||||
<Flex justifyContent="space-between" alignItems="flex-end" mb={ 3 }>
|
||||
<chakra.span fontSize="sm" wordBreak="break-all" lineHeight="20px">
|
||||
File { index + 1 } of { array.length }: { item.file_path }
|
||||
</chakra.span>
|
||||
<CopyToClipboard text={ item.source_code } ml={ 4 }/>
|
||||
</Flex>
|
||||
<CodeEditor value={ item.source_code } id={ `source_code_${ index }` }/>
|
||||
</Box>
|
||||
)) }
|
||||
</Flex>
|
||||
<CodeEditor data={ editorData }/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 94 KiB |
@@ -7,6 +7,24 @@ export const getNativeCoinValue = (value: string | Array<string>) => {
|
||||
return BigNumber(_value).times(10 ** config.network.currency.decimals).toString();
|
||||
};
|
||||
|
||||
export const addZeroesAllowed = (valueType: string) => {
|
||||
if (valueType.includes('[]')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const REGEXP = /u?int(\d+)/i;
|
||||
|
||||
const match = valueType.match(REGEXP);
|
||||
const power = match?.[1];
|
||||
|
||||
if (power) {
|
||||
// show control for all inputs which allows to insert 10^18 or greater numbers
|
||||
return Number(power) >= 64;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
interface ExtendedError extends Error {
|
||||
detectedNetwork?: {
|
||||
chain: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
|
||||
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, useColorModeValue } from '@chakra-ui/react';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
@@ -40,6 +40,8 @@ const BlockDetails = ({ query }: Props) => {
|
||||
const router = useRouter();
|
||||
const heightOrHash = getQueryParamString(router.query.height);
|
||||
|
||||
const separatorColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
const { data, isLoading, isError, error } = query;
|
||||
|
||||
const handleCutClick = React.useCallback(() => {
|
||||
@@ -197,11 +199,15 @@ const BlockDetails = ({ query }: Props) => {
|
||||
<Text>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
|
||||
<Utilization
|
||||
ml={ 4 }
|
||||
mr={ 5 }
|
||||
colorScheme="gray"
|
||||
value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }
|
||||
/>
|
||||
<GasUsedToTargetRatio value={ data.gas_target_percentage || undefined }/>
|
||||
{ data.gas_target_percentage && (
|
||||
<>
|
||||
<TextSeparator color={ separatorColor } mx={ 1 }/>
|
||||
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
|
||||
</>
|
||||
) }
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Gas limit"
|
||||
|
||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
@@ -1,4 +1,4 @@
|
||||
import { Flex, Spinner, Text, Box, Icon } from '@chakra-ui/react';
|
||||
import { Flex, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { route } from 'nextjs-routes';
|
||||
@@ -16,6 +16,7 @@ import AddressLink from 'ui/shared/address/AddressLink';
|
||||
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
import TextSeparator from 'ui/shared/TextSeparator';
|
||||
import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
|
||||
interface Props {
|
||||
@@ -29,6 +30,8 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
|
||||
const burntFees = BigNumber(data.burnt_fees || 0);
|
||||
const txFees = BigNumber(data.tx_fees || 0);
|
||||
|
||||
const separatorColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
return (
|
||||
<ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated>
|
||||
<Flex justifyContent="space-between" w="100%">
|
||||
@@ -63,10 +66,15 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
|
||||
</Flex>
|
||||
<Box>
|
||||
<Text fontWeight={ 500 }>Gas used</Text>
|
||||
<Flex columnGap={ 4 } mt={ 2 }>
|
||||
<Text variant="secondary">{ BigNumber(data.gas_used || 0).toFormat() }</Text>
|
||||
<Flex mt={ 2 }>
|
||||
<Text variant="secondary" mr={ 4 }>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
|
||||
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() }/>
|
||||
<GasUsedToTargetRatio value={ data.gas_target_percentage || undefined }/>
|
||||
{ data.gas_target_percentage && (
|
||||
<>
|
||||
<TextSeparator color={ separatorColor } mx={ 1 }/>
|
||||
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
|
||||
</>
|
||||
) }
|
||||
</Flex>
|
||||
</Box>
|
||||
<Flex columnGap={ 2 }>
|
||||
|
||||
@@ -13,6 +13,7 @@ import BlockTimestamp from 'ui/blocks/BlockTimestamp';
|
||||
import AddressLink from 'ui/shared/address/AddressLink';
|
||||
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
import TextSeparator from 'ui/shared/TextSeparator';
|
||||
import Utilization from 'ui/shared/Utilization/Utilization';
|
||||
|
||||
interface Props {
|
||||
@@ -26,6 +27,8 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
|
||||
const burntFees = BigNumber(data.burnt_fees || 0);
|
||||
const txFees = BigNumber(data.tx_fees || 0);
|
||||
|
||||
const separatorColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
return (
|
||||
<Tr
|
||||
as={ motion.tr }
|
||||
@@ -68,11 +71,12 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
|
||||
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Tooltip label="% of Gas Target">
|
||||
<Box>
|
||||
<GasUsedToTargetRatio ml={ 2 } value={ data.gas_target_percentage || undefined }/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{ data.gas_target_percentage && (
|
||||
<>
|
||||
<TextSeparator color={ separatorColor } mx={ 1 }/>
|
||||
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
|
||||
</>
|
||||
) }
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td fontSize="sm">{ totalReward.toFixed(8) }</Td>
|
||||
|
||||
@@ -92,7 +92,7 @@ test.describe('sourcify', () => {
|
||||
});
|
||||
testWithSocket.describe.configure({ mode: 'serial', timeout: 20_000 });
|
||||
|
||||
testWithSocket('with multiple contracts +@mobile', async({ mount, page, createSocket }) => {
|
||||
testWithSocket('with multiple contracts', async({ mount, page, createSocket }) => {
|
||||
const component = await mount(
|
||||
<TestApp withSocket>
|
||||
<ContractVerificationForm config={ formConfig } hash={ hash }/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, chakra, useUpdateEffect } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { route } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
@@ -20,7 +20,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify
|
||||
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
|
||||
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
|
||||
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
|
||||
import { prepareRequestBody, formatSocketErrors, DEFAULT_VALUES } from './utils';
|
||||
import { prepareRequestBody, formatSocketErrors, getDefaultValues } from './utils';
|
||||
|
||||
const METHOD_COMPONENTS = {
|
||||
'flattened-code': <ContractVerificationFlattenSourceCode/>,
|
||||
@@ -40,14 +40,13 @@ interface Props {
|
||||
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
|
||||
const formApi = useForm<FormFields>({
|
||||
mode: 'onBlur',
|
||||
defaultValues: methodFromQuery ? DEFAULT_VALUES[methodFromQuery] : undefined,
|
||||
defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config) : undefined,
|
||||
});
|
||||
const { control, handleSubmit, watch, formState, setError, reset } = formApi;
|
||||
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
|
||||
|
||||
const apiFetch = useApiFetch();
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
|
||||
const body = prepareRequestBody(data);
|
||||
@@ -86,8 +85,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { shallow: false });
|
||||
}, [ hash, router, setError, toast ]);
|
||||
window.location.assign(route({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }));
|
||||
}, [ hash, setError, toast ]);
|
||||
|
||||
const handleSocketError = React.useCallback(() => {
|
||||
if (!formState.isSubmitting) {
|
||||
@@ -129,7 +128,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (methodValue) {
|
||||
reset(DEFAULT_VALUES[methodValue]);
|
||||
reset(getDefaultValues(methodValue, config));
|
||||
}
|
||||
// !!! should run only when method is changed
|
||||
}, [ methodValue ]);
|
||||
|
||||
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 51 KiB |
@@ -60,14 +60,14 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
|
||||
const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => {
|
||||
switch (method) {
|
||||
case 'flattened-code':
|
||||
return <ListItem>Verification through flattened source code.</ListItem>;
|
||||
return <ListItem key={ method }>Verification through flattened source code.</ListItem>;
|
||||
case 'multi-part':
|
||||
return <ListItem>Verification of multi-part Solidity files.</ListItem>;
|
||||
return <ListItem key={ method }>Verification of multi-part Solidity files.</ListItem>;
|
||||
case 'sourcify':
|
||||
return <ListItem>Verification through <Link href="https://sourcify.dev/" target="_blank">Sourcify</Link>.</ListItem>;
|
||||
return <ListItem key={ method }>Verification through <Link href="https://sourcify.dev/" target="_blank">Sourcify</Link>.</ListItem>;
|
||||
case 'standard-input':
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItem key={ method }>
|
||||
<span>Verification using </span>
|
||||
<Link
|
||||
href="https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description"
|
||||
@@ -79,9 +79,9 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
|
||||
</ListItem>
|
||||
);
|
||||
case 'vyper-code':
|
||||
return <ListItem>Verification of Vyper contract.</ListItem>;
|
||||
return <ListItem key={ method }>Verification of Vyper contract.</ListItem>;
|
||||
case 'vyper-multi-part':
|
||||
return <ListItem>Verification of multi-part Vyper files.</ListItem>;
|
||||
return <ListItem key={ method }>Verification of multi-part Vyper files.</ListItem>;
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ export interface FormFieldsFlattenSourceCode {
|
||||
method: MethodOption;
|
||||
is_yul: boolean;
|
||||
name: string;
|
||||
compiler: Option;
|
||||
evm_version: Option;
|
||||
compiler: Option | null;
|
||||
evm_version: Option | null;
|
||||
is_optimization_enabled: boolean;
|
||||
optimization_runs: string;
|
||||
code: string;
|
||||
@@ -28,7 +28,7 @@ export interface FormFieldsFlattenSourceCode {
|
||||
export interface FormFieldsStandardInput {
|
||||
method: MethodOption;
|
||||
name: string;
|
||||
compiler: Option;
|
||||
compiler: Option | null;
|
||||
sources: Array<File>;
|
||||
autodetect_constructor_args: boolean;
|
||||
constructor_args: string;
|
||||
@@ -42,8 +42,8 @@ export interface FormFieldsSourcify {
|
||||
|
||||
export interface FormFieldsMultiPartFile {
|
||||
method: MethodOption;
|
||||
compiler: Option;
|
||||
evm_version: Option;
|
||||
compiler: Option | null;
|
||||
evm_version: Option | null;
|
||||
is_optimization_enabled: boolean;
|
||||
optimization_runs: string;
|
||||
sources: Array<File>;
|
||||
@@ -53,15 +53,15 @@ export interface FormFieldsMultiPartFile {
|
||||
export interface FormFieldsVyperContract {
|
||||
method: MethodOption;
|
||||
name: string;
|
||||
compiler: Option;
|
||||
compiler: Option | null;
|
||||
code: string;
|
||||
constructor_args: string;
|
||||
}
|
||||
|
||||
export interface FormFieldsVyperMultiPartFile {
|
||||
method: MethodOption;
|
||||
compiler: Option;
|
||||
evm_version: Option;
|
||||
compiler: Option | null;
|
||||
evm_version: Option | null;
|
||||
sources: Array<File>;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
FormFieldsVyperContract,
|
||||
FormFieldsVyperMultiPartFile,
|
||||
} from './types';
|
||||
import type { SmartContractVerificationMethod, SmartContractVerificationError } from 'types/api/contract';
|
||||
import type { SmartContractVerificationMethod, SmartContractVerificationError, SmartContractVerificationConfig } from 'types/api/contract';
|
||||
|
||||
import type { Params as FetchParams } from 'lib/hooks/useFetch';
|
||||
|
||||
@@ -32,7 +32,7 @@ export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = {
|
||||
'vyper-multi-part': 'Vyper (Multi-part files)',
|
||||
};
|
||||
|
||||
export const DEFAULT_VALUES = {
|
||||
export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> = {
|
||||
'flattened-code': {
|
||||
method: {
|
||||
value: 'flattened-code' as const,
|
||||
@@ -75,7 +75,7 @@ export const DEFAULT_VALUES = {
|
||||
compiler: null,
|
||||
evm_version: null,
|
||||
is_optimization_enabled: true,
|
||||
optimization_runs: 200,
|
||||
optimization_runs: '200',
|
||||
sources: [],
|
||||
libraries: [],
|
||||
},
|
||||
@@ -100,6 +100,22 @@ export const DEFAULT_VALUES = {
|
||||
},
|
||||
};
|
||||
|
||||
export function getDefaultValues(method: SmartContractVerificationMethod, config: SmartContractVerificationConfig) {
|
||||
const defaultValues = DEFAULT_VALUES[method];
|
||||
|
||||
if ('evm_version' in defaultValues) {
|
||||
if (method === 'flattened-code' || method === 'multi-part') {
|
||||
defaultValues.evm_version = config.solidity_evm_versions.find((value) => value === 'default') ? { label: 'default', value: 'default' } : null;
|
||||
}
|
||||
|
||||
if (method === 'vyper-multi-part') {
|
||||
defaultValues.evm_version = config.vyper_evm_versions.find((value) => value === 'default') ? { label: 'default', value: 'default' } : null;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValues;
|
||||
}
|
||||
|
||||
export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod {
|
||||
return method && SUPPORTED_VERIFICATION_METHODS.includes(method) ? true : false;
|
||||
}
|
||||
@@ -141,7 +157,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
|
||||
const _data = data as FormFieldsStandardInput;
|
||||
|
||||
const body = new FormData();
|
||||
body.set('compiler_version', _data.compiler?.value);
|
||||
_data.compiler && body.set('compiler_version', _data.compiler.value);
|
||||
body.set('contract_name', _data.name);
|
||||
body.set('autodetect_constructor_args', String(Boolean(_data.autodetect_constructor_args)));
|
||||
body.set('constructor_args', _data.constructor_args);
|
||||
@@ -163,8 +179,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
|
||||
const _data = data as FormFieldsMultiPartFile;
|
||||
|
||||
const body = new FormData();
|
||||
body.set('compiler_version', _data.compiler?.value);
|
||||
body.set('evm_version', _data.evm_version?.value);
|
||||
_data.compiler && body.set('compiler_version', _data.compiler.value);
|
||||
_data.evm_version && body.set('evm_version', _data.evm_version.value);
|
||||
body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled)));
|
||||
_data.is_optimization_enabled && body.set('optimization_runs', _data.optimization_runs);
|
||||
|
||||
@@ -190,8 +206,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
|
||||
const _data = data as FormFieldsVyperMultiPartFile;
|
||||
|
||||
const body = new FormData();
|
||||
body.set('compiler_version', _data.compiler?.value);
|
||||
body.set('evm_version', _data.evm_version?.value);
|
||||
_data.compiler && body.set('compiler_version', _data.compiler.value);
|
||||
_data.evm_version && body.set('evm_version', _data.evm_version.value);
|
||||
addFilesToFormData(body, _data.sources);
|
||||
|
||||
return body;
|
||||
|
||||
@@ -31,14 +31,25 @@ const Deposits = () => {
|
||||
|
||||
const text = (() => {
|
||||
if (countersQuery.isLoading) {
|
||||
return <Skeleton w={{ base: '100%', lg: '320px' }} h="26px" mb={ 6 } mt={{ base: 0, lg: 6 }}/>;
|
||||
return (
|
||||
<Skeleton
|
||||
w={{ base: '100%', lg: '320px' }}
|
||||
h="24px"
|
||||
mb={{ base: 7, lg: isPaginationVisible ? 0 : 7 }}
|
||||
mt={{ base: 1, lg: isPaginationVisible ? 0 : 7 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (countersQuery.isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }}>A total of { countersQuery.data.toLocaleString('en') } deposits found</Text>;
|
||||
return (
|
||||
<Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}>
|
||||
A total of { countersQuery.data.toLocaleString('en') } deposits found
|
||||
</Text>
|
||||
);
|
||||
})();
|
||||
|
||||
const actionBar = (
|
||||
|
||||
@@ -30,7 +30,14 @@ const OutputRoots = () => {
|
||||
|
||||
const text = (() => {
|
||||
if (countersQuery.isLoading || isLoading) {
|
||||
return <Skeleton w={{ base: '100%', lg: '400px' }} h={{ base: '48px', lg: '26px' }} mb={{ base: 6, lg: 7 }} mt={{ base: 0, lg: 7 }}/>;
|
||||
return (
|
||||
<Skeleton
|
||||
w={{ base: '100%', lg: '400px' }}
|
||||
h={{ base: '48px', lg: '24px' }}
|
||||
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
|
||||
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (countersQuery.isError || isError || data?.items.length === 0) {
|
||||
|
||||
@@ -31,7 +31,14 @@ const TxnBatches = () => {
|
||||
|
||||
const text = (() => {
|
||||
if (countersQuery.isLoading || isLoading) {
|
||||
return <Skeleton w={{ base: '100%', lg: '400px' }} h={{ base: '48px', lg: '26px' }} mb={{ base: 6, lg: 7 }} mt={{ base: 0, lg: 7 }}/>;
|
||||
return (
|
||||
<Skeleton
|
||||
w={{ base: '100%', lg: '400px' }}
|
||||
h={{ base: '48px', lg: '24px' }}
|
||||
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
|
||||
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (countersQuery.isError || isError || data.items.length === 0) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import type { VerifiedContractsFilters } from 'types/api/contracts';
|
||||
|
||||
import useDebounce from 'lib/hooks/useDebounce';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import { apos } from 'lib/html-entities';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
@@ -29,6 +30,8 @@ const VerifiedContracts = () => {
|
||||
|
||||
const debouncedSearchTerm = useDebounce(searchTerm || '', 300);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { isError, isLoading, data, isPaginationVisible, pagination, onFilterChange } = useQueryWithPages({
|
||||
resourceName: 'verified_contracts',
|
||||
filters: { q: debouncedSearchTerm, filter: type },
|
||||
@@ -87,13 +90,15 @@ const VerifiedContracts = () => {
|
||||
{ sortButton }
|
||||
{ filterInput }
|
||||
</HStack>
|
||||
<ActionBar mt={ -6 }>
|
||||
<HStack spacing={ 3 } display={{ base: 'none', lg: 'flex' }}>
|
||||
{ typeFilter }
|
||||
{ filterInput }
|
||||
</HStack>
|
||||
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
|
||||
</ActionBar>
|
||||
{ (!isMobile || isPaginationVisible) && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<HStack spacing={ 3 } display={{ base: 'none', lg: 'flex' }}>
|
||||
{ typeFilter }
|
||||
{ filterInput }
|
||||
</HStack>
|
||||
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
|
||||
</ActionBar>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -31,14 +31,25 @@ const Withdrawals = () => {
|
||||
|
||||
const text = (() => {
|
||||
if (countersQuery.isLoading) {
|
||||
return <Skeleton w={{ base: '100%', lg: '320px' }} h="26px" mb={ 6 } mt={{ base: 0, lg: 6 }}/>;
|
||||
return (
|
||||
<Skeleton
|
||||
w={{ base: '100%', lg: '320px' }}
|
||||
h="24px"
|
||||
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
|
||||
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (countersQuery.isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }}>A total of { countersQuery.data.toLocaleString('en') } withdrawals found</Text>;
|
||||
return (
|
||||
<Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}>
|
||||
A total of { countersQuery.data.toLocaleString('en') } withdrawals found
|
||||
</Text>
|
||||
);
|
||||
})();
|
||||
|
||||
const actionBar = (
|
||||
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
@@ -1,75 +0,0 @@
|
||||
import { chakra, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type ReactAce from 'react-ace/lib/ace';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
value: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CodeEditorBase = chakra(({ id, value, className }: Props) => {
|
||||
|
||||
const [ AceEditor, setAceEditor ] = React.useState<{default: typeof ReactAce} | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const load = async() => {
|
||||
const component = await import('react-ace');
|
||||
await import('ace-builds/src-noconflict/mode-csharp');
|
||||
await import('ace-builds/src-noconflict/theme-tomorrow');
|
||||
await import('ace-builds/src-noconflict/theme-tomorrow_night');
|
||||
await import('ace-builds/src-noconflict/ext-language_tools');
|
||||
setAceEditor(component);
|
||||
};
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
setAceEditor(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const theme = useColorModeValue('tomorrow', 'tomorrow_night');
|
||||
|
||||
if (!AceEditor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AceEditor.default
|
||||
className={ className }
|
||||
mode="csharp" // TODO need to find mode for solidity
|
||||
theme={ theme }
|
||||
value={ value }
|
||||
name={ id }
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
readOnly
|
||||
width="100%"
|
||||
showPrintMargin={ false }
|
||||
maxLines={ 25 }
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const CodeEditor = ({ id, value }: Props) => {
|
||||
// see theme/components/Textarea.ts variantFilledInactive
|
||||
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
|
||||
const gutterBgColor = useColorModeValue('gray.100', '#25282c');
|
||||
|
||||
return (
|
||||
<CodeEditorBase
|
||||
id={ id }
|
||||
value={ value }
|
||||
bgColor={ bgColor }
|
||||
borderRadius="md"
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
'.ace_gutter': {
|
||||
backgroundColor: gutterBgColor,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditor);
|
||||
@@ -4,10 +4,10 @@ import React, { useEffect, useState } from 'react';
|
||||
import CopyIcon from 'icons/copy.svg';
|
||||
|
||||
const CopyToClipboard = ({ text, className }: {text: string; className?: string}) => {
|
||||
const { hasCopied, onCopy } = useClipboard(text, 3000);
|
||||
const { hasCopied, onCopy } = useClipboard(text, 1000);
|
||||
const [ copied, setCopied ] = useState(false);
|
||||
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
|
||||
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCopied) {
|
||||
@@ -17,13 +17,8 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
|
||||
}
|
||||
}, [ hasCopied ]);
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
onToggle();
|
||||
onCopy();
|
||||
}, [ onCopy, onToggle ]);
|
||||
|
||||
return (
|
||||
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen }>
|
||||
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen || copied }>
|
||||
<IconButton
|
||||
aria-label="copy"
|
||||
icon={ <CopyIcon/> }
|
||||
@@ -32,7 +27,7 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
|
||||
variant="simple"
|
||||
display="inline-block"
|
||||
flexShrink={ 0 }
|
||||
onClick={ handleClick }
|
||||
onClick={ onCopy }
|
||||
className={ className }
|
||||
onMouseEnter={ onOpen }
|
||||
onMouseLeave={ onClose }
|
||||
|
||||
@@ -25,7 +25,7 @@ interface AsyncSelectProps extends AsyncProps<Option, boolean, GroupBase<Option>
|
||||
|
||||
type Props = RegularSelectProps | AsyncSelectProps;
|
||||
|
||||
const FancySelect = (props: Props) => {
|
||||
const FancySelect = (props: Props, ref: React.LegacyRef<HTMLDivElement>) => {
|
||||
const menuZIndex = useToken('zIndices', 'dropdown');
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
@@ -42,6 +42,7 @@ const FancySelect = (props: Props) => {
|
||||
variant="floating"
|
||||
size={ props.size || 'md' }
|
||||
isRequired={ props.isRequired }
|
||||
ref={ ref }
|
||||
{ ...(props.error ? { 'aria-invalid': true } : {}) }
|
||||
{ ...(props.isDisabled ? { 'aria-disabled': true } : {}) }
|
||||
{ ...(props.value ? { 'data-active': true } : {}) }
|
||||
|
||||
@@ -1,32 +1,18 @@
|
||||
import { Stat, StatArrow, Text, chakra } from '@chakra-ui/react';
|
||||
import { Text, Tooltip } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
type Props = ({
|
||||
type Props = {
|
||||
value: number;
|
||||
} | {
|
||||
used: number;
|
||||
target: number;
|
||||
}) & {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const GasUsedToTargetRatio = (props: Props) => {
|
||||
const percentage = (() => {
|
||||
if ('value' in props) {
|
||||
return props.value;
|
||||
}
|
||||
|
||||
return (props.used / props.target - 1) * 100;
|
||||
})();
|
||||
|
||||
const GasUsedToTargetRatio = ({ value }: Props) => {
|
||||
return (
|
||||
<Stat className={ props.className }>
|
||||
<StatArrow type={ percentage >= 0 ? 'increase' : 'decrease' }/>
|
||||
<Text as="span" color={ percentage >= 0 ? 'green.500' : 'red.500' } fontWeight={ 600 }>
|
||||
{ Math.abs(percentage).toLocaleString('en', { maximumFractionDigits: 2 }) } %
|
||||
<Tooltip label="% of Gas Target">
|
||||
<Text variant="secondary">
|
||||
{ (value > 0 ? '+' : '') + value.toLocaleString('en', { maximumFractionDigits: 2 }) }%
|
||||
</Text>
|
||||
</Stat>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(chakra(GasUsedToTargetRatio));
|
||||
export default React.memo(GasUsedToTargetRatio);
|
||||
|
||||
@@ -12,7 +12,7 @@ const WIDTH = 50;
|
||||
|
||||
const Utilization = ({ className, value, colorScheme = 'green' }: Props) => {
|
||||
const valueString = (clamp(value * 100 || 0, 0, 100)).toLocaleString('en', { maximumFractionDigits: 2 }) + '%';
|
||||
const colorGrayScheme = useColorModeValue('gray.500', 'gray.500');
|
||||
const colorGrayScheme = useColorModeValue('gray.500', 'gray.400');
|
||||
const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500';
|
||||
|
||||
return (
|
||||
|
||||
@@ -50,7 +50,7 @@ const AddressLink = (props: Props) => {
|
||||
} else if (type === 'block') {
|
||||
url = route({ pathname: '/block/[height]', query: { height: props.blockHeight } });
|
||||
} else if (type === 'address_token') {
|
||||
url = route({ pathname: '/address/[hash]', query: { hash, tab: 'token_transfers', token_hash: props.tokenHash, scroll_to_tabs: 'true' } });
|
||||
url = route({ pathname: '/address/[hash]', query: { hash, tab: 'token_transfers', token: props.tokenHash, scroll_to_tabs: 'true' } });
|
||||
} else {
|
||||
url = route({ pathname: '/address/[hash]', query: { hash } });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import type { SystemStyleObject } from '@chakra-ui/react';
|
||||
import { Box, useColorMode, Flex } from '@chakra-ui/react';
|
||||
import type { EditorProps } from '@monaco-editor/react';
|
||||
import MonacoEditor from '@monaco-editor/react';
|
||||
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import React from 'react';
|
||||
|
||||
import type { File, Monaco } from './types';
|
||||
|
||||
import useClientRect from 'lib/hooks/useClientRect';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import isMetaKey from 'lib/isMetaKey';
|
||||
|
||||
import CodeEditorBreadcrumbs from './CodeEditorBreadcrumbs';
|
||||
import CodeEditorLoading from './CodeEditorLoading';
|
||||
import CodeEditorSideBar, { CONTAINER_WIDTH as SIDE_BAR_WIDTH } from './CodeEditorSideBar';
|
||||
import CodeEditorTabs from './CodeEditorTabs';
|
||||
import addFileImportDecorations from './utils/addFileImportDecorations';
|
||||
import getFullPathOfImportedFile from './utils/getFullPathOfImportedFile';
|
||||
import * as themes from './utils/themes';
|
||||
import useThemeColors from './utils/useThemeColors';
|
||||
|
||||
const EDITOR_OPTIONS: EditorProps['options'] = {
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: true,
|
||||
},
|
||||
dragAndDrop: false,
|
||||
};
|
||||
|
||||
const TABS_HEIGHT = 35;
|
||||
const BREADCRUMBS_HEIGHT = 22;
|
||||
const EDITOR_HEIGHT = 500;
|
||||
|
||||
interface Props {
|
||||
data: Array<File>;
|
||||
}
|
||||
|
||||
const CodeEditor = ({ data }: Props) => {
|
||||
const [ instance, setInstance ] = React.useState<Monaco | undefined>();
|
||||
const [ editor, setEditor ] = React.useState<monaco.editor.IStandaloneCodeEditor | undefined>();
|
||||
const [ index, setIndex ] = React.useState(0);
|
||||
const [ tabs, setTabs ] = React.useState([ data[index].file_path ]);
|
||||
const [ isMetaPressed, setIsMetaPressed ] = React.useState(false);
|
||||
|
||||
const [ containerRect, containerNodeRef ] = useClientRect<HTMLDivElement>();
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
const isMobile = useIsMobile();
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
const editorWidth = containerRect ? containerRect.width - (isMobile ? 0 : SIDE_BAR_WIDTH) : 0;
|
||||
|
||||
React.useEffect(() => {
|
||||
instance?.editor.setTheme(colorMode === 'light' ? 'blockscout-light' : 'blockscout-dark');
|
||||
}, [ colorMode, instance?.editor ]);
|
||||
|
||||
const handleEditorDidMount = React.useCallback((editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||
setInstance(monaco);
|
||||
setEditor(editor);
|
||||
|
||||
monaco.editor.defineTheme('blockscout-light', themes.light);
|
||||
monaco.editor.defineTheme('blockscout-dark', themes.dark);
|
||||
monaco.editor.setTheme(colorMode === 'light' ? 'blockscout-light' : 'blockscout-dark');
|
||||
|
||||
const loadedModels = monaco.editor.getModels();
|
||||
const loadedModelsPaths = loadedModels.map((model) => model.uri.path);
|
||||
const newModels = data.slice(1)
|
||||
.filter((file) => !loadedModelsPaths.includes(file.file_path))
|
||||
.map((file) => monaco.editor.createModel(file.source_code, 'sol', monaco.Uri.parse(file.file_path)));
|
||||
|
||||
loadedModels.concat(newModels).forEach(addFileImportDecorations);
|
||||
|
||||
editor.addAction({
|
||||
id: 'close-tab',
|
||||
label: 'Close current tab',
|
||||
keybindings: [
|
||||
monaco.KeyMod.Alt | monaco.KeyCode.KeyW,
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.7,
|
||||
run: function(editor) {
|
||||
const model = editor.getModel();
|
||||
const path = model?.uri.path;
|
||||
if (path) {
|
||||
handleTabClose(path, true);
|
||||
}
|
||||
},
|
||||
});
|
||||
// componentDidMount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ ]);
|
||||
|
||||
const handleSelectFile = React.useCallback((index: number, lineNumber?: number) => {
|
||||
setIndex(index);
|
||||
setTabs((prev) => prev.some((item) => item === data[index].file_path) ? prev : ([ ...prev, data[index].file_path ]));
|
||||
if (lineNumber !== undefined && !Object.is(lineNumber, NaN)) {
|
||||
window.setTimeout(() => {
|
||||
editor?.revealLineInCenter(lineNumber);
|
||||
}, 0);
|
||||
}
|
||||
editor?.focus();
|
||||
}, [ data, editor ]);
|
||||
|
||||
const handleTabSelect = React.useCallback((path: string) => {
|
||||
const index = data.findIndex((item) => item.file_path === path);
|
||||
if (index > -1) {
|
||||
setIndex(index);
|
||||
}
|
||||
}, [ data ]);
|
||||
|
||||
const handleTabClose = React.useCallback((path: string, _isActive?: boolean) => {
|
||||
setTabs((prev) => {
|
||||
if (prev.length > 1) {
|
||||
const tabIndex = prev.findIndex((item) => item === path);
|
||||
const isActive = _isActive !== undefined ? _isActive : data[index].file_path === path;
|
||||
|
||||
if (isActive) {
|
||||
const nextActiveIndex = data.findIndex((item) => item.file_path === prev[(tabIndex === 0 ? 1 : tabIndex - 1)]);
|
||||
setIndex(nextActiveIndex);
|
||||
}
|
||||
|
||||
return prev.filter((item) => item !== path);
|
||||
}
|
||||
|
||||
return prev;
|
||||
});
|
||||
}, [ data, index ]);
|
||||
|
||||
const handleClick = React.useCallback((event: React.MouseEvent) => {
|
||||
if (!isMetaPressed && !isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target as HTMLSpanElement;
|
||||
const isImportLink = target.classList.contains('import-link');
|
||||
if (isImportLink) {
|
||||
const path = target.innerText;
|
||||
const fullPath = getFullPathOfImportedFile(data[index].file_path, path);
|
||||
const fileIndex = data.findIndex((file) => file.file_path === fullPath);
|
||||
if (fileIndex > -1) {
|
||||
event.stopPropagation();
|
||||
handleSelectFile(fileIndex);
|
||||
}
|
||||
}
|
||||
}, [ data, handleSelectFile, index, isMetaPressed, isMobile ]);
|
||||
|
||||
const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
|
||||
isMetaKey(event) && setIsMetaPressed(true);
|
||||
}, []);
|
||||
|
||||
const handleKeyUp = React.useCallback(() => {
|
||||
setIsMetaPressed(false);
|
||||
}, []);
|
||||
|
||||
const containerSx: SystemStyleObject = React.useMemo(() => ({
|
||||
'.editor-container': {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: `${ editorWidth }px`,
|
||||
height: '100%',
|
||||
},
|
||||
'.highlight': {
|
||||
backgroundColor: themeColors['custom.findMatchHighlightBackground'],
|
||||
},
|
||||
'&&.meta-pressed .import-link': {
|
||||
_hover: {
|
||||
color: themeColors['custom.fileLink.hoverForeground'],
|
||||
textDecoration: 'underline',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
}), [ editorWidth, themeColors ]);
|
||||
|
||||
if (data.length === 1) {
|
||||
return (
|
||||
<Box overflow="hidden" borderRadius="md" height={ `${ EDITOR_HEIGHT }px` }>
|
||||
<MonacoEditor
|
||||
language="sol"
|
||||
path={ data[index].file_path }
|
||||
defaultValue={ data[index].source_code }
|
||||
options={ EDITOR_OPTIONS }
|
||||
onMount={ handleEditorDidMount }
|
||||
loading={ <CodeEditorLoading/> }
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
className={ isMetaPressed ? 'meta-pressed' : undefined }
|
||||
overflow="hidden"
|
||||
borderRadius="md"
|
||||
width="100%"
|
||||
height={ `${ EDITOR_HEIGHT + TABS_HEIGHT + BREADCRUMBS_HEIGHT }px` }
|
||||
position="relative"
|
||||
ref={ containerNodeRef }
|
||||
sx={ containerSx }
|
||||
onClick={ handleClick }
|
||||
onKeyDown={ handleKeyDown }
|
||||
onKeyUp={ handleKeyUp }
|
||||
>
|
||||
<Box flexGrow={ 1 }>
|
||||
<CodeEditorTabs tabs={ tabs } activeTab={ data[index].file_path } onTabSelect={ handleTabSelect } onTabClose={ handleTabClose }/>
|
||||
<CodeEditorBreadcrumbs path={ data[index].file_path }/>
|
||||
<MonacoEditor
|
||||
className="editor-container"
|
||||
height={ `${ EDITOR_HEIGHT }px` }
|
||||
language="sol"
|
||||
path={ data[index].file_path }
|
||||
defaultValue={ data[index].source_code }
|
||||
options={ EDITOR_OPTIONS }
|
||||
onMount={ handleEditorDidMount }
|
||||
loading={ <CodeEditorLoading/> }
|
||||
/>
|
||||
</Box>
|
||||
<CodeEditorSideBar
|
||||
data={ data }
|
||||
onFileSelect={ handleSelectFile }
|
||||
monaco={ instance }
|
||||
editor={ editor }
|
||||
selectedFile={ data[index].file_path }
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditor);
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Flex, Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import stripLeadingSlash from 'lib/stripLeadingSlash';
|
||||
import useThemeColors from 'ui/shared/monaco/utils/useThemeColors';
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
}
|
||||
|
||||
const CodeEditorBreadcrumbs = ({ path }: Props) => {
|
||||
const chunks = stripLeadingSlash(path).split('/');
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
color={ themeColors['breadcrumbs.foreground'] }
|
||||
bgColor={ themeColors['editor.background'] }
|
||||
pl="16px"
|
||||
pr="8px"
|
||||
flexWrap="wrap"
|
||||
fontSize="13px"
|
||||
lineHeight="22px"
|
||||
alignItems="center"
|
||||
>
|
||||
{ chunks.map((chunk, index) => {
|
||||
return (
|
||||
<React.Fragment key={ index }>
|
||||
{ index !== 0 && (
|
||||
<Box
|
||||
className="codicon codicon-breadcrumb-separator"
|
||||
boxSize="16px"
|
||||
_before={{
|
||||
content: '"\\eab6"',
|
||||
}}/>
|
||||
) }
|
||||
<Box>{ chunk }</Box>
|
||||
</React.Fragment>
|
||||
);
|
||||
}) }
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditorBreadcrumbs);
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { File } from './types';
|
||||
|
||||
import CodeEditorFileTree from './CodeEditorFileTree';
|
||||
import CoderEditorCollapseButton from './CoderEditorCollapseButton';
|
||||
import composeFileTree from './utils/composeFileTree';
|
||||
|
||||
interface Props {
|
||||
data: Array<File>;
|
||||
onFileSelect: (index: number) => void;
|
||||
selectedFile: string;
|
||||
isActive: boolean;
|
||||
setActionBarRenderer: React.Dispatch<React.SetStateAction<(() => JSX.Element) | undefined>>;
|
||||
}
|
||||
|
||||
const CodeEditorFileExplorer = ({ data, onFileSelect, selectedFile, isActive, setActionBarRenderer }: Props) => {
|
||||
const [ key, setKey ] = React.useState(0);
|
||||
const tree = React.useMemo(() => {
|
||||
return composeFileTree(data);
|
||||
}, [ data ]);
|
||||
|
||||
const handleCollapseButtonClick = React.useCallback(() => {
|
||||
setKey((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
const renderActionBar = React.useCallback(() => {
|
||||
return (
|
||||
<CoderEditorCollapseButton onClick={ handleCollapseButtonClick } label="Collapse folders"/>
|
||||
);
|
||||
}, [ handleCollapseButtonClick ]);
|
||||
|
||||
const handleFileClick = React.useCallback((event: React.MouseEvent) => {
|
||||
const filePath = (event.currentTarget as HTMLDivElement).getAttribute('data-file-path');
|
||||
const fileIndex = data.findIndex((item) => item.file_path === filePath);
|
||||
|
||||
if (fileIndex > -1) {
|
||||
onFileSelect(fileIndex);
|
||||
}
|
||||
}, [ data, onFileSelect ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
isActive && setActionBarRenderer(() => renderActionBar);
|
||||
}, [ isActive, renderActionBar, setActionBarRenderer ]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<CodeEditorFileTree key={ key } tree={ tree } onItemClick={ handleFileClick } isCollapsed={ key > 0 } selectedFile={ selectedFile }/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditorFileExplorer);
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { ChakraProps } from '@chakra-ui/react';
|
||||
import { Box, Accordion, AccordionButton, AccordionItem, AccordionPanel, Icon, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { FileTree } from './types';
|
||||
|
||||
import iconFile from './icons/file.svg';
|
||||
import iconFolderOpen from './icons/folder-open.svg';
|
||||
import iconFolder from './icons/folder.svg';
|
||||
import iconSolidity from './icons/solidity.svg';
|
||||
import useThemeColors from './utils/useThemeColors';
|
||||
|
||||
interface Props {
|
||||
tree: FileTree;
|
||||
level?: number;
|
||||
isCollapsed?: boolean;
|
||||
onItemClick: (event: React.MouseEvent) => void;
|
||||
selectedFile: string;
|
||||
}
|
||||
|
||||
const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed, selectedFile }: Props) => {
|
||||
const itemProps: ChakraProps = {
|
||||
borderWidth: '0px',
|
||||
cursor: 'pointer',
|
||||
lineHeight: '22px',
|
||||
_last: {
|
||||
borderBottomWidth: '0px',
|
||||
},
|
||||
};
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
return (
|
||||
<Accordion allowMultiple defaultIndex={ isCollapsed ? undefined : tree.map((item, index) => index) } reduceMotion>
|
||||
{
|
||||
tree.map((leaf, index) => {
|
||||
const leafName = <chakra.span overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ leaf.name }</chakra.span>;
|
||||
|
||||
if ('children' in leaf) {
|
||||
return (
|
||||
<AccordionItem key={ index } { ...itemProps }>
|
||||
{ ({ isExpanded }) => (
|
||||
<>
|
||||
<AccordionButton
|
||||
pr="8px"
|
||||
py="0"
|
||||
pl={ `${ 8 + 8 * level }px` }
|
||||
_hover={{ bgColor: themeColors['custom.list.hoverBackground'] }}
|
||||
fontSize="13px"
|
||||
lineHeight="22px"
|
||||
h="22px"
|
||||
transitionDuration="0"
|
||||
>
|
||||
<Box
|
||||
className="codicon codicon-tree-item-expanded"
|
||||
transform={ isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' }
|
||||
boxSize="16px"
|
||||
mr="2px"
|
||||
/>
|
||||
<Icon as={ isExpanded ? iconFolderOpen : iconFolder } boxSize="16px" mr="4px"/>
|
||||
{ leafName }
|
||||
</AccordionButton>
|
||||
<AccordionPanel p="0">
|
||||
<CodeEditorFileTree
|
||||
tree={ leaf.children }
|
||||
level={ level + 1 }
|
||||
onItemClick={ onItemClick }
|
||||
isCollapsed={ isCollapsed }
|
||||
selectedFile={ selectedFile }
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</>
|
||||
) }
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
|
||||
const icon = /.sol|.yul|.vy$/.test(leaf.name) ? iconSolidity : iconFile;
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key={ index }
|
||||
{ ...itemProps }
|
||||
pl={ `${ 26 + (level * 8) }px` }
|
||||
pr="8px"
|
||||
onClick={ onItemClick }
|
||||
data-file-path={ leaf.file_path }
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
overflow="hidden"
|
||||
_hover={{
|
||||
bgColor: selectedFile === leaf.file_path ? themeColors['list.inactiveSelectionBackground'] : themeColors['custom.list.hoverBackground'],
|
||||
}}
|
||||
bgColor={ selectedFile === leaf.file_path ? themeColors['list.inactiveSelectionBackground'] : 'none' }
|
||||
>
|
||||
<Icon as={ icon } boxSize="16px" mr="4px"/>
|
||||
{ leafName }
|
||||
</AccordionItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditorFileTree);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Center } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import ContentLoader from 'ui/shared/ContentLoader';
|
||||
|
||||
import useThemeColors from './utils/useThemeColors';
|
||||
|
||||
const CodeEditorLoading = () => {
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
return (
|
||||
<Center bgColor={ themeColors['editor.background'] } w="100%" h="100%">
|
||||
<ContentLoader/>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditorLoading);
|
||||
@@ -0,0 +1,225 @@
|
||||
import type { ChakraProps } from '@chakra-ui/react';
|
||||
import { Accordion, Box, Input, InputGroup, InputRightElement, useBoolean } from '@chakra-ui/react';
|
||||
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import React from 'react';
|
||||
|
||||
import type { File, Monaco, SearchResult } from './types';
|
||||
|
||||
import useDebounce from 'lib/hooks/useDebounce';
|
||||
|
||||
import CodeEditorSearchSection from './CodeEditorSearchSection';
|
||||
import CoderEditorCollapseButton from './CoderEditorCollapseButton';
|
||||
import useThemeColors from './utils/useThemeColors';
|
||||
|
||||
interface Props {
|
||||
data: Array<File>;
|
||||
monaco: Monaco | undefined;
|
||||
onFileSelect: (index: number, lineNumber?: number) => void;
|
||||
isInputStuck: boolean;
|
||||
isActive: boolean;
|
||||
setActionBarRenderer: React.Dispatch<React.SetStateAction<(() => JSX.Element) | undefined>>;
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, setActionBarRenderer, defaultValue }: Props) => {
|
||||
const [ searchTerm, changeSearchTerm ] = React.useState('');
|
||||
const [ searchResults, setSearchResults ] = React.useState<Array<SearchResult>>([]);
|
||||
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]);
|
||||
const [ isMatchCase, setMatchCase ] = useBoolean();
|
||||
const [ isMatchWholeWord, setMatchWholeWord ] = useBoolean();
|
||||
const [ isMatchRegex, setMatchRegex ] = useBoolean();
|
||||
const decorations = React.useRef<Record<string, Array<string>>>({});
|
||||
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 300);
|
||||
|
||||
React.useEffect(() => {
|
||||
changeSearchTerm(defaultValue);
|
||||
}, [ defaultValue ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!monaco) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!debouncedSearchTerm) {
|
||||
setSearchResults([]);
|
||||
}
|
||||
|
||||
const models = monaco.editor.getModels();
|
||||
const matches = models.map((model) => model.findMatches(debouncedSearchTerm, false, isMatchRegex, isMatchCase, isMatchWholeWord ? 'true' : null, false));
|
||||
|
||||
models.forEach((model, index) => {
|
||||
const filePath = model.uri.path;
|
||||
const prevDecorations = decorations.current[filePath] || [];
|
||||
const newDecorations: Array<monaco.editor.IModelDeltaDecoration> = matches[index].map(({ range }) => ({ range, options: { className: 'highlight' } }));
|
||||
|
||||
const newDecorationsIds = model.deltaDecorations(prevDecorations, newDecorations);
|
||||
decorations.current[filePath] = newDecorationsIds;
|
||||
});
|
||||
|
||||
const result: Array<SearchResult> = matches
|
||||
.map((match, index) => {
|
||||
const model = models[index];
|
||||
return {
|
||||
file_path: model.uri.path,
|
||||
matches: match.map(({ range }) => ({ ...range, lineContent: model.getLineContent(range.startLineNumber) })),
|
||||
};
|
||||
})
|
||||
.filter(({ matches }) => matches.length > 0);
|
||||
|
||||
setSearchResults(result.length > 0 ? result : []);
|
||||
}, [ debouncedSearchTerm, isMatchCase, isMatchRegex, isMatchWholeWord, monaco ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setExpandedSections(searchResults.map((item, index) => index));
|
||||
}, [ searchResults ]);
|
||||
|
||||
const handleSearchTermChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
changeSearchTerm(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleResultItemClick = React.useCallback((filePath: string, lineNumber: number) => {
|
||||
const fileIndex = data.findIndex((item) => item.file_path === filePath);
|
||||
if (fileIndex > -1) {
|
||||
onFileSelect(fileIndex, Number(lineNumber));
|
||||
}
|
||||
}, [ data, onFileSelect ]);
|
||||
|
||||
const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => {
|
||||
setExpandedSections(newValue);
|
||||
}, []);
|
||||
|
||||
const handleToggleCollapseClick = React.useCallback(() => {
|
||||
if (expandedSections.length === 0) {
|
||||
setExpandedSections(searchResults.map((item, index) => index));
|
||||
} else {
|
||||
setExpandedSections([]);
|
||||
}
|
||||
}, [ expandedSections.length, searchResults ]);
|
||||
|
||||
const renderActionBar = React.useCallback(() => {
|
||||
return (
|
||||
<CoderEditorCollapseButton
|
||||
onClick={ handleToggleCollapseClick }
|
||||
label={ expandedSections.length === 0 ? 'Expand all' : 'Collapse all' }
|
||||
isDisabled={ searchResults.length === 0 }
|
||||
isCollapsed={ expandedSections.length === 0 }
|
||||
/>
|
||||
);
|
||||
}, [ expandedSections.length, handleToggleCollapseClick, searchResults.length ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
isActive && setActionBarRenderer(() => renderActionBar);
|
||||
}, [ isActive, renderActionBar, setActionBarRenderer ]);
|
||||
|
||||
const buttonProps: ChakraProps = {
|
||||
boxSize: '20px',
|
||||
p: '1px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '3px',
|
||||
borderWidth: '1px',
|
||||
borderColor: 'transparent',
|
||||
};
|
||||
|
||||
const searchResultNum = (() => {
|
||||
if (!debouncedSearchTerm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalResults = searchResults.map(({ matches }) => matches.length).reduce((result, item) => result + item, 0);
|
||||
|
||||
if (!totalResults) {
|
||||
return (
|
||||
<Box px="8px" fontSize="13px" lineHeight="18px" mb="8px">
|
||||
No results found. Review your settings for configured exclusions.
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box px="8px" fontSize="13px" lineHeight="18px" mb="8px">
|
||||
{ totalResults } result{ totalResults > 1 ? 's' : '' } in { searchResults.length } file{ searchResults.length > 1 ? 's' : '' }
|
||||
</Box>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<InputGroup
|
||||
px="8px"
|
||||
position="sticky"
|
||||
top="35px"
|
||||
left="0"
|
||||
zIndex="2"
|
||||
bgColor={ themeColors['sideBar.background'] }
|
||||
pb="8px"
|
||||
boxShadow={ isInputStuck ? 'md' : 'none' }
|
||||
>
|
||||
<Input
|
||||
size="xs"
|
||||
onChange={ handleSearchTermChange }
|
||||
value={ searchTerm }
|
||||
placeholder="Search"
|
||||
variant="unstyled"
|
||||
color={ themeColors['input.foreground'] }
|
||||
bgColor={ themeColors['input.background'] }
|
||||
borderRadius="none"
|
||||
fontSize="13px"
|
||||
lineHeight="20px"
|
||||
borderWidth="1px"
|
||||
borderColor={ themeColors['input.background'] }
|
||||
py="2px"
|
||||
pl="4px"
|
||||
pr="75px"
|
||||
transitionDuration="0"
|
||||
_focus={{
|
||||
borderColor: themeColors.focusBorder,
|
||||
}}
|
||||
/>
|
||||
<InputRightElement w="auto" h="auto" right="12px" top="3px" columnGap="2px">
|
||||
<Box
|
||||
{ ...buttonProps }
|
||||
className="codicon codicon-case-sensitive"
|
||||
onClick={ setMatchCase.toggle }
|
||||
bgColor={ isMatchCase ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
|
||||
_hover={{ bgColor: isMatchCase ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
|
||||
title="Match Case"
|
||||
aria-label="Match Case"
|
||||
/>
|
||||
<Box
|
||||
{ ...buttonProps }
|
||||
className="codicon codicon-whole-word"
|
||||
bgColor={ isMatchWholeWord ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
|
||||
onClick={ setMatchWholeWord.toggle }
|
||||
_hover={{ bgColor: isMatchWholeWord ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
|
||||
title="Match Whole Word"
|
||||
aria-label="Match Whole Word"
|
||||
/>
|
||||
<Box
|
||||
{ ...buttonProps }
|
||||
className="codicon codicon-regex"
|
||||
bgColor={ isMatchRegex ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
|
||||
onClick={ setMatchRegex.toggle }
|
||||
_hover={{ bgColor: isMatchRegex ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
|
||||
title="Use Regular Expression"
|
||||
aria-label="Use Regular Expression"
|
||||
/>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
{ searchResultNum }
|
||||
<Accordion
|
||||
key={ debouncedSearchTerm }
|
||||
allowMultiple
|
||||
index={ expandedSections }
|
||||
onChange={ handleAccordionStateChange }
|
||||
reduceMotion
|
||||
>
|
||||
{ searchResults.map((item) => <CodeEditorSearchSection key={ item.file_path } data={ item } onItemClick={ handleResultItemClick }/>) }
|
||||
</Accordion>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditorSearch);
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Box, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { SearchResult } from './types';
|
||||
import type ArrayElement from 'types/utils/ArrayElement';
|
||||
|
||||
import useThemeColors from './utils/useThemeColors';
|
||||
|
||||
interface Props extends ArrayElement<SearchResult['matches']> {
|
||||
filePath: string;
|
||||
onClick: (event: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const calculateStartPosition = (lineContent: string, startColumn: number) => {
|
||||
|
||||
let start = 0;
|
||||
|
||||
for (let index = 0; index < startColumn; index++) {
|
||||
const element = lineContent[index];
|
||||
|
||||
if (element === ' ') {
|
||||
start = index + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return start ? start - 1 : 0;
|
||||
};
|
||||
|
||||
const CodeEditorSearchResultItem = ({ lineContent, filePath, onClick, startLineNumber, startColumn, endColumn }: Props) => {
|
||||
const start = calculateStartPosition(lineContent, startColumn);
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
return (
|
||||
<Box
|
||||
pr="8px"
|
||||
pl="36px"
|
||||
fontSize="13px"
|
||||
lineHeight="22px"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
cursor="pointer"
|
||||
data-file-path={ filePath }
|
||||
data-line-number={ startLineNumber }
|
||||
onClick={ onClick }
|
||||
transitionDuration="0"
|
||||
_hover={{ bgColor: themeColors['custom.list.hoverBackground'] }}
|
||||
>
|
||||
<span>{ lineContent.slice(start, startColumn - 1) }</span>
|
||||
<chakra.span bgColor={ themeColors['custom.findMatchHighlightBackground'] }>
|
||||
{ lineContent.slice(startColumn - 1, endColumn - 1) }
|
||||
</chakra.span>
|
||||
<span>{ lineContent.slice(endColumn - 1) }</span>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditorSearchResultItem);
|
||||
@@ -0,0 +1,72 @@
|
||||
import { AccordionButton, AccordionItem, AccordionPanel, Icon, Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { SearchResult } from './types';
|
||||
|
||||
import CodeEditorSearchResultItem from './CodeEditorSearchResultItem';
|
||||
import iconFile from './icons/file.svg';
|
||||
import iconSolidity from './icons/solidity.svg';
|
||||
import getFileName from './utils/getFileName';
|
||||
import useThemeColors from './utils/useThemeColors';
|
||||
|
||||
interface Props {
|
||||
data: SearchResult;
|
||||
onItemClick: (filePath: string, lineNumber: number) => void;
|
||||
}
|
||||
|
||||
const CodeEditorSearchSection = ({ data, onItemClick }: Props) => {
|
||||
const fileName = getFileName(data.file_path);
|
||||
|
||||
const handleFileLineClick = React.useCallback((event: React.MouseEvent) => {
|
||||
const lineNumber = Number((event.currentTarget as HTMLDivElement).getAttribute('data-line-number'));
|
||||
if (!Object.is(lineNumber, NaN)) {
|
||||
onItemClick(data.file_path, Number(lineNumber));
|
||||
}
|
||||
}, [ data.file_path, onItemClick ]);
|
||||
|
||||
const icon = /.sol|.yul|.vy$/.test(fileName) ? iconSolidity : iconFile;
|
||||
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
return (
|
||||
<AccordionItem borderWidth="0px" _last={{ borderBottomWidth: '0px' }} >
|
||||
{ ({ isExpanded }) => (
|
||||
<>
|
||||
<AccordionButton
|
||||
py={ 0 }
|
||||
px={ 2 }
|
||||
_hover={{ bgColor: themeColors['custom.list.hoverBackground'] }}
|
||||
fontSize="13px"
|
||||
transitionDuration="0"
|
||||
lineHeight="22px"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box
|
||||
className="codicon codicon-tree-item-expanded"
|
||||
transform={ isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' }
|
||||
width="20px"
|
||||
height="22px"
|
||||
py="3px"
|
||||
/>
|
||||
<Icon as={ icon } boxSize="16px" mr="4px"/>
|
||||
<span>{ fileName }</span>
|
||||
<Box className="monaco-count-badge" ml="auto" bgColor={ themeColors['badge.background'] }>{ data.matches.length }</Box>
|
||||
</AccordionButton>
|
||||
<AccordionPanel p={ 0 }>
|
||||
{ data.matches.map((match) => (
|
||||
<CodeEditorSearchResultItem
|
||||
key={ data.file_path + '_' + match.startLineNumber + '_' + match.startColumn }
|
||||
filePath={ data.file_path }
|
||||
onClick={ handleFileLineClick }
|
||||
{ ...match }
|
||||
/>
|
||||
),
|
||||
) }
|
||||
</AccordionPanel>
|
||||
</>
|
||||
) }
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditorSearchSection);
|
||||
@@ -0,0 +1,182 @@
|
||||
import type { HTMLChakraProps } from '@chakra-ui/react';
|
||||
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs, useBoolean } from '@chakra-ui/react';
|
||||
import _throttle from 'lodash/throttle';
|
||||
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import React from 'react';
|
||||
|
||||
import type { File, Monaco } from './types';
|
||||
|
||||
import { shift, cmd } from 'lib/html-entities';
|
||||
|
||||
import CodeEditorFileExplorer from './CodeEditorFileExplorer';
|
||||
import CodeEditorSearch from './CodeEditorSearch';
|
||||
import useThemeColors from './utils/useThemeColors';
|
||||
|
||||
interface Props {
|
||||
monaco: Monaco | undefined;
|
||||
editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
data: Array<File>;
|
||||
onFileSelect: (index: number, lineNumber?: number) => void;
|
||||
selectedFile: string;
|
||||
}
|
||||
|
||||
export const CONTAINER_WIDTH = 250;
|
||||
|
||||
const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile }: Props) => {
|
||||
|
||||
const [ isStuck, setIsStuck ] = React.useState(false);
|
||||
const [ isDrawerOpen, setIsDrawerOpen ] = useBoolean(false);
|
||||
const [ tabIndex, setTabIndex ] = React.useState(0);
|
||||
const [ searchValue, setSearchValue ] = React.useState('');
|
||||
const [ actionBarRenderer, setActionBarRenderer ] = React.useState<() => JSX.Element>();
|
||||
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
const tabProps: HTMLChakraProps<'button'> = {
|
||||
fontFamily: 'heading',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: '11px',
|
||||
lineHeight: '35px',
|
||||
fontWeight: 500,
|
||||
color: themeColors['tab.inactiveForeground'],
|
||||
_selected: {
|
||||
color: themeColors['tab.activeForeground'],
|
||||
},
|
||||
px: 0,
|
||||
letterSpacing: 0.3,
|
||||
};
|
||||
|
||||
const handleScrollThrottled = React.useRef(_throttle((event: React.SyntheticEvent) => {
|
||||
setIsStuck((event.target as HTMLDivElement).scrollTop > 0);
|
||||
}, 100));
|
||||
|
||||
const handleFileSelect = React.useCallback((index: number, lineNumber?: number) => {
|
||||
isDrawerOpen && setIsDrawerOpen.off();
|
||||
onFileSelect(index, lineNumber);
|
||||
}, [ isDrawerOpen, onFileSelect, setIsDrawerOpen ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (editor && monaco) {
|
||||
editor.addAction({
|
||||
id: 'file-explorer',
|
||||
label: 'Show File Explorer',
|
||||
keybindings: [
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyE,
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.5,
|
||||
run: function() {
|
||||
setTabIndex(0);
|
||||
},
|
||||
});
|
||||
|
||||
editor.addAction({
|
||||
id: 'search-in-files',
|
||||
label: 'Show Search in Files',
|
||||
keybindings: [
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF,
|
||||
],
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 1.6,
|
||||
run: function(editor) {
|
||||
setTabIndex(1);
|
||||
const selection = editor.getSelection();
|
||||
const selectedValue = selection ? editor.getModel()?.getValueInRange(selection) : '';
|
||||
setSearchValue(selectedValue || '');
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [ editor, monaco ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
w={ `${ CONTAINER_WIDTH }px` }
|
||||
flexShrink={ 0 }
|
||||
bgColor={ themeColors['sideBar.background'] }
|
||||
fontSize="13px"
|
||||
overflowY="scroll"
|
||||
onScroll={ handleScrollThrottled.current }
|
||||
position={{ base: 'absolute', lg: 'relative' }}
|
||||
right={{ base: isDrawerOpen ? '0' : `-${ CONTAINER_WIDTH }px`, lg: '0' }}
|
||||
top={{ base: 0, lg: undefined }}
|
||||
h="100%"
|
||||
pb="22px"
|
||||
boxShadow={{ base: isDrawerOpen ? 'md' : 'none', lg: 'none' }}
|
||||
zIndex={{ base: '2', lg: undefined }}
|
||||
transitionProperty="right"
|
||||
transitionDuration="normal"
|
||||
transitionTimingFunction="ease-in-out"
|
||||
borderTopRightRadius="md"
|
||||
borderBottomRightRadius="md"
|
||||
>
|
||||
<Tabs isLazy lazyBehavior="keepMounted" variant="unstyled" size="13px" index={ tabIndex } onChange={ setTabIndex }>
|
||||
<TabList
|
||||
columnGap={ 3 }
|
||||
position="sticky"
|
||||
top={ 0 }
|
||||
left={ 0 }
|
||||
bgColor={ themeColors['sideBar.background'] }
|
||||
zIndex="1"
|
||||
px={ 2 }
|
||||
boxShadow={ isStuck ? 'md' : 'none' }
|
||||
borderTopRightRadius="md"
|
||||
>
|
||||
<Tab { ...tabProps } title={ `File explorer (${ shift + cmd }E)` }>Explorer</Tab>
|
||||
<Tab { ...tabProps } title={ `Search in files (${ shift + cmd }F)` }>Search</Tab>
|
||||
{ actionBarRenderer?.() }
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel p={ 0 }>
|
||||
<CodeEditorFileExplorer
|
||||
data={ data }
|
||||
onFileSelect={ handleFileSelect }
|
||||
selectedFile={ selectedFile }
|
||||
isActive={ tabIndex === 0 }
|
||||
setActionBarRenderer={ setActionBarRenderer }
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel p={ 0 }>
|
||||
<CodeEditorSearch
|
||||
data={ data }
|
||||
onFileSelect={ handleFileSelect }
|
||||
monaco={ monaco }
|
||||
isInputStuck={ isStuck }
|
||||
isActive={ tabIndex === 1 }
|
||||
setActionBarRenderer={ setActionBarRenderer }
|
||||
defaultValue={ searchValue }
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box
|
||||
boxSize="24px"
|
||||
p="4px"
|
||||
position="absolute"
|
||||
display={{ base: 'block', lg: 'none' }}
|
||||
right={ isDrawerOpen ? `${ CONTAINER_WIDTH - 1 }px` : '0' }
|
||||
top="calc(50% - 12px)"
|
||||
backgroundColor={ themeColors['sideBar.background'] }
|
||||
borderTopLeftRadius="4px"
|
||||
borderBottomLeftRadius="4px"
|
||||
boxShadow="md"
|
||||
onClick={ setIsDrawerOpen.toggle }
|
||||
zIndex="1"
|
||||
transitionProperty="right"
|
||||
transitionDuration="normal"
|
||||
transitionTimingFunction="ease-in-out"
|
||||
title={ isDrawerOpen ? 'Open sidebar' : 'Close sidebar' }
|
||||
aria-label={ isDrawerOpen ? 'Open sidebar' : 'Close sidebar' }
|
||||
>
|
||||
<Box
|
||||
className="codicon codicon-tree-item-expanded"
|
||||
transform={ isDrawerOpen ? 'rotate(-90deg)' : 'rotate(+90deg)' }
|
||||
boxSize="16px"
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditorSideBar);
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Flex, Icon, chakra, Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { alt } from 'lib/html-entities';
|
||||
import useThemeColors from 'ui/shared/monaco/utils/useThemeColors';
|
||||
|
||||
import iconFile from './icons/file.svg';
|
||||
import iconSolidity from './icons/solidity.svg';
|
||||
import getFilePathParts from './utils/getFilePathParts';
|
||||
|
||||
interface Props {
|
||||
isActive?: boolean;
|
||||
path: string;
|
||||
onClick: (path: string) => void;
|
||||
onClose: (path: string) => void;
|
||||
isCloseDisabled: boolean;
|
||||
tabsPathChunks: Array<Array<string>>;
|
||||
}
|
||||
|
||||
const CodeEditorTab = ({ isActive, path, onClick, onClose, isCloseDisabled, tabsPathChunks }: Props) => {
|
||||
const [ fileName, folderName ] = getFilePathParts(path, tabsPathChunks);
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
onClick(path);
|
||||
}, [ onClick, path ]);
|
||||
|
||||
const handleClose = React.useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
!isCloseDisabled && onClose(path);
|
||||
}, [ isCloseDisabled, onClose, path ]);
|
||||
|
||||
const icon = /.sol|.yul|.vy$/.test(fileName) ? iconSolidity : iconFile;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
pl="10px"
|
||||
pr="4px"
|
||||
fontSize="13px"
|
||||
lineHeight="34px"
|
||||
bgColor={ isActive ? themeColors['tab.activeBackground'] : themeColors['tab.inactiveBackground'] }
|
||||
borderRightWidth="1px"
|
||||
borderRightColor={ themeColors['tab.border'] }
|
||||
borderBottomWidth="1px"
|
||||
borderBottomColor={ isActive ? 'transparent' : themeColors['tab.border'] }
|
||||
color={ isActive ? themeColors['tab.activeForeground'] : themeColors['tab.inactiveForeground'] }
|
||||
alignItems="center"
|
||||
fontWeight={ 400 }
|
||||
cursor="pointer"
|
||||
onClick={ handleClick }
|
||||
_hover={{
|
||||
'.codicon-close': {
|
||||
visibility: 'visible',
|
||||
},
|
||||
}}
|
||||
userSelect="none"
|
||||
>
|
||||
<Icon as={ icon } boxSize="16px" mr="4px"/>
|
||||
<span>{ fileName }</span>
|
||||
{ folderName && <chakra.span fontSize="11px" opacity={ 0.8 } ml={ 1 }>{ folderName[0] === '.' ? '' : '...' }{ folderName }</chakra.span> }
|
||||
<Box
|
||||
className="codicon codicon-close"
|
||||
boxSize="20px"
|
||||
ml="4px"
|
||||
p="2px"
|
||||
title={ `Close ${ isActive ? `(${ alt }W)` : '' }` }
|
||||
aria-label="Close"
|
||||
onClick={ handleClose }
|
||||
borderRadius="sm"
|
||||
opacity={ isCloseDisabled ? 0.3 : 1 }
|
||||
visibility={{ base: 'visible', lg: isActive ? 'visible' : 'hidden' }}
|
||||
color={ themeColors['icon.foreground'] }
|
||||
_hover={{ bgColor: isCloseDisabled ? 'transparent' : themeColors['custom.inputOption.hoverBackground'] }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditorTab);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import CodeEditorTab from './CodeEditorTab';
|
||||
import useThemeColors from './utils/useThemeColors';
|
||||
|
||||
interface Props {
|
||||
tabs: Array<string>;
|
||||
activeTab: string;
|
||||
onTabSelect: (tab: string) => void;
|
||||
onTabClose: (tab: string) => void;
|
||||
}
|
||||
|
||||
const CodeEditorTabs = ({ tabs, activeTab, onTabSelect, onTabClose }: Props) => {
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
const tabsPathChunks = React.useMemo(() => {
|
||||
return tabs.map((tab) => tab.split('/'));
|
||||
}, [ tabs ]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
bgColor={ themeColors['sideBar.background'] }
|
||||
flexWrap="wrap"
|
||||
>
|
||||
{ tabs.map((tab) => (
|
||||
<CodeEditorTab
|
||||
key={ tab }
|
||||
path={ tab }
|
||||
isActive={ activeTab === tab }
|
||||
onClick={ onTabSelect }
|
||||
onClose={ onTabClose }
|
||||
isCloseDisabled={ tabs.length === 1 }
|
||||
tabsPathChunks={ tabsPathChunks }
|
||||
/>
|
||||
)) }
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CodeEditorTabs);
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import useThemeColors from './utils/useThemeColors';
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
isDisabled?: boolean;
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
const CoderEditorCollapseButton = ({ onClick, label, isDisabled, isCollapsed }: Props) => {
|
||||
const themeColors = useThemeColors();
|
||||
|
||||
return (
|
||||
<Box
|
||||
ml="auto"
|
||||
alignSelf="center"
|
||||
className={ isCollapsed ? 'codicon codicon-search-expand-results' : 'codicon codicon-collapse-all' }
|
||||
opacity={ isDisabled ? 0.6 : 1 }
|
||||
boxSize="20px"
|
||||
p="2px"
|
||||
borderRadius="sm"
|
||||
_before={{
|
||||
content: isCollapsed ? '"\\eb95"' : '"\\eac5"',
|
||||
}}
|
||||
_hover={{
|
||||
bgColor: themeColors['custom.inputOption.hoverBackground'],
|
||||
}}
|
||||
onClick={ onClick }
|
||||
cursor="pointer"
|
||||
title={ label }
|
||||
aria-label={ label }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CoderEditorCollapseButton);
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m5 2H6v16h12v-9h-7V4z" fill="#42a5f5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 198 B |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7a2 2 0 0 1 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#90a4ae"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#90a4ae"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 182 B |
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#0288d1">
|
||||
<path d="m5.747 14.046 6.254 8.61 6.252-8.61-6.254 3.807z"/>
|
||||
<path d="M11.999 1.343 5.747 11.83l6.252 3.807 6.253-3.807z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 228 B |
@@ -0,0 +1,27 @@
|
||||
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
export interface File {
|
||||
file_path: string;
|
||||
source_code: string;
|
||||
}
|
||||
|
||||
export interface FileTreeFile extends File {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FileTreeFolder {
|
||||
name: string;
|
||||
children: Array<FileTreeFile | FileTreeFolder>;
|
||||
}
|
||||
|
||||
export type FileTree = Array<FileTreeFile | FileTreeFolder>;
|
||||
|
||||
export type Monaco = typeof monaco;
|
||||
|
||||
export interface SearchResult {
|
||||
file_path: string;
|
||||
matches: Array<
|
||||
Pick<monaco.editor.FindMatch['range'], 'startColumn' | 'endColumn' | 'startLineNumber' | 'endLineNumber'> &
|
||||
{ lineContent: string }
|
||||
>;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
export default function addFileImportDecorations(model: monaco.editor.ITextModel) {
|
||||
const matches = model.findMatches('^import "((\\/|\\.)(\\w|\\.|\\/|-)+)"', false, true, false, null, true);
|
||||
const decorations: Array<monaco.editor.IModelDeltaDecoration> = matches.map(({ range }) => ({
|
||||
range: {
|
||||
...range,
|
||||
startColumn: range.startColumn + 8,
|
||||
endColumn: range.endColumn - 1,
|
||||
},
|
||||
options: {
|
||||
inlineClassName: 'import-link',
|
||||
hoverMessage: {
|
||||
value: 'Cmd/Win + click to open file',
|
||||
},
|
||||
},
|
||||
}));
|
||||
model.deltaDecorations([], decorations);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import composeFileTree from './composeFileTree';
|
||||
|
||||
const files = [
|
||||
{
|
||||
file_path: 'index.sol',
|
||||
source_code: 'zero',
|
||||
},
|
||||
{
|
||||
file_path: 'contracts/Zeta.eth.sol',
|
||||
source_code: 'one',
|
||||
},
|
||||
{
|
||||
file_path: '/_openzeppelin/contracts/utils/Context.sol',
|
||||
source_code: 'two',
|
||||
},
|
||||
{
|
||||
file_path: '/_openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol',
|
||||
source_code: 'three',
|
||||
},
|
||||
{
|
||||
file_path: '/_openzeppelin/contracts/token/ERC20/IERC20.sol',
|
||||
source_code: 'four',
|
||||
},
|
||||
{
|
||||
file_path: '/_openzeppelin/contracts/token/ERC20/ERC20.sol',
|
||||
source_code: 'five',
|
||||
},
|
||||
];
|
||||
|
||||
test('builds correct file tree', () => {
|
||||
const result = composeFileTree(files);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"file_path": "/_openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol",
|
||||
"name": "IERC20Metadata.sol",
|
||||
"source_code": "three",
|
||||
},
|
||||
],
|
||||
"name": "extensions",
|
||||
},
|
||||
{
|
||||
"file_path": "/_openzeppelin/contracts/token/ERC20/ERC20.sol",
|
||||
"name": "ERC20.sol",
|
||||
"source_code": "five",
|
||||
},
|
||||
{
|
||||
"file_path": "/_openzeppelin/contracts/token/ERC20/IERC20.sol",
|
||||
"name": "IERC20.sol",
|
||||
"source_code": "four",
|
||||
},
|
||||
],
|
||||
"name": "ERC20",
|
||||
},
|
||||
],
|
||||
"name": "token",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"file_path": "/_openzeppelin/contracts/utils/Context.sol",
|
||||
"name": "Context.sol",
|
||||
"source_code": "two",
|
||||
},
|
||||
],
|
||||
"name": "utils",
|
||||
},
|
||||
],
|
||||
"name": "contracts",
|
||||
},
|
||||
],
|
||||
"name": "_openzeppelin",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"file_path": "contracts/Zeta.eth.sol",
|
||||
"name": "Zeta.eth.sol",
|
||||
"source_code": "one",
|
||||
},
|
||||
],
|
||||
"name": "contracts",
|
||||
},
|
||||
{
|
||||
"file_path": "index.sol",
|
||||
"name": "index.sol",
|
||||
"source_code": "zero",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { File, FileTree } from '../types';
|
||||
|
||||
import stripLeadingSlash from 'lib/stripLeadingSlash';
|
||||
|
||||
import sortFileTree from './sortFileTree';
|
||||
|
||||
export default function composeFileTree(files: Array<File>) {
|
||||
const result: FileTree = [];
|
||||
type Level = {
|
||||
result: FileTree;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} & Record<string, any>;
|
||||
const level: Level = { result };
|
||||
|
||||
files.forEach((file) => {
|
||||
const path = stripLeadingSlash(file.file_path);
|
||||
const segments = path.split('/');
|
||||
|
||||
segments.reduce((acc, segment, currentIndex, array) => {
|
||||
if (!acc[segment]) {
|
||||
acc[segment] = { result: [] };
|
||||
acc.result.push({
|
||||
name: segment,
|
||||
...(currentIndex === array.length - 1 ? file : { children: acc[segment].result }),
|
||||
});
|
||||
}
|
||||
acc.result.sort(sortFileTree);
|
||||
|
||||
return acc[segment];
|
||||
}, level);
|
||||
});
|
||||
|
||||
return result.sort(sortFileTree);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// ensure that path always starts with /
|
||||
export default function formatFilePath(path: string) {
|
||||
if (path[0] === '.' && path[1] === '/') {
|
||||
return path.slice(1);
|
||||
}
|
||||
|
||||
if (path[0] === '/') {
|
||||
return path;
|
||||
}
|
||||
|
||||
return '/' + path;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default function getFileName(path: string) {
|
||||
const chunks = path.split('/');
|
||||
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import getFilePathParts from './getFilePathParts';
|
||||
|
||||
it('computes correct chunks if all file name are unique', () => {
|
||||
const result = getFilePathParts(
|
||||
'/src/utils/Ownable.sol',
|
||||
[
|
||||
'/src/utils/EIP712.sol'.split('/'),
|
||||
'/src/token/BaseERC20.sol'.split('/'),
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toEqual([ 'Ownable.sol', undefined ]);
|
||||
});
|
||||
|
||||
it('computes correct chunks if files with the same name is not in folders with the same name', () => {
|
||||
const result = getFilePathParts(
|
||||
'/src/utils/Ownable.sol',
|
||||
[
|
||||
'/src/utils/EIP712.sol'.split('/'),
|
||||
'/lib/_openzeppelin/contracts/contracts/access/Ownable.sol'.split('/'),
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toEqual([ 'Ownable.sol', '/utils' ]);
|
||||
});
|
||||
|
||||
it('computes correct chunks if files with the same name is in folders with the same name', () => {
|
||||
const result = getFilePathParts(
|
||||
'/src/utils/Ownable.sol',
|
||||
[
|
||||
'/src/utils/EIP712.sol'.split('/'),
|
||||
'/lib/_openzeppelin/contracts/src/utils/Ownable.sol'.split('/'),
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toEqual([ 'Ownable.sol', './src/utils' ]);
|
||||
});
|
||||
|
||||
it('computes correct chunks if file is in root directory', () => {
|
||||
const result = getFilePathParts(
|
||||
'/Ownable.sol',
|
||||
[
|
||||
'/src/utils/EIP712.sol'.split('/'),
|
||||
'/lib/_openzeppelin/contracts/src/utils/Ownable.sol'.split('/'),
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toEqual([ 'Ownable.sol', './' ]);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
export default function getFilePathParts(path: string, tabsPathChunks: Array<Array<string>>): [string, string | undefined] {
|
||||
const chunks = path.split('/');
|
||||
const fileName = chunks[chunks.length - 1];
|
||||
const folderName = getFolderName(chunks, tabsPathChunks);
|
||||
|
||||
return [ fileName, folderName ];
|
||||
}
|
||||
|
||||
function getFolderName(chunks: Array<string>, tabsPathChunks: Array<Array<string>>): string | undefined {
|
||||
const fileName = chunks[chunks.length - 1];
|
||||
const otherTabsPathChunks = tabsPathChunks.filter((item) => item.join('/') !== chunks.join('/'));
|
||||
const tabsWithSameFileName = otherTabsPathChunks.filter((tabChunks) => tabChunks[tabChunks.length - 1] === fileName);
|
||||
|
||||
if (tabsWithSameFileName.length === 0 || chunks.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (chunks.length === 2) {
|
||||
return './' + chunks[chunks.length - 2];
|
||||
}
|
||||
|
||||
let result = '/' + chunks[chunks.length - 2];
|
||||
|
||||
for (let index = 3; index <= chunks.length; index++) {
|
||||
const element = chunks[chunks.length - index];
|
||||
|
||||
if (element === '') {
|
||||
result = '.' + result;
|
||||
}
|
||||
|
||||
const subFolderNames = tabsWithSameFileName.map((tab) => tab[tab.length - index]);
|
||||
if (subFolderNames.includes(element)) {
|
||||
result = '/' + element + result;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import getFullPathOfImportedFile from './getFullPathOfImportedFile';
|
||||
|
||||
it('construct correct absolute path', () => {
|
||||
const result = getFullPathOfImportedFile(
|
||||
'/foo/bar/baz/index.sol',
|
||||
'./.././../abc/contract.sol',
|
||||
);
|
||||
|
||||
expect(result).toBe('/foo/abc/contract.sol');
|
||||
});
|
||||
|
||||
it('returns undefined if imported file is outside the base file folder', () => {
|
||||
const result = getFullPathOfImportedFile(
|
||||
'/index.sol',
|
||||
'../../abc/contract.sol',
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns unmodified path if it is already absolute', () => {
|
||||
const result = getFullPathOfImportedFile(
|
||||
'/index.sol',
|
||||
'/abc/contract.sol',
|
||||
);
|
||||
|
||||
expect(result).toBe('/abc/contract.sol');
|
||||
});
|
||||
|
||||
it('returns undefined for external path', () => {
|
||||
const result = getFullPathOfImportedFile(
|
||||
'/index.sol',
|
||||
'https://github.com/ethereum/dapp/contract.sol',
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import stripLeadingSlash from 'lib/stripLeadingSlash';
|
||||
|
||||
export default function getFullPathOfImportedFile(baseFilePath: string, importedFilePath: string) {
|
||||
if (importedFilePath[0] === '/') {
|
||||
return importedFilePath;
|
||||
}
|
||||
|
||||
if (importedFilePath[0] !== '.') {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseFileChunks = stripLeadingSlash(baseFilePath).split('/');
|
||||
const importedFileChunks = importedFilePath.split('/');
|
||||
|
||||
const result: Array<string> = baseFileChunks.slice(0, -1);
|
||||
|
||||
for (let index = 0; index < importedFileChunks.length - 1; index++) {
|
||||
const element = importedFileChunks[index];
|
||||
|
||||
if (element === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element === '..') {
|
||||
if (result.length === 0) {
|
||||
break;
|
||||
}
|
||||
result.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(element);
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.push(importedFileChunks[importedFileChunks.length - 1]);
|
||||
|
||||
return '/' + result.join('/');
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { FileTree } from '../types';
|
||||
import type ArrayElement from 'types/utils/ArrayElement';
|
||||
|
||||
export default function sortFileTree(a: ArrayElement<FileTree>, b: ArrayElement<FileTree>) {
|
||||
if ('children' in a && !('children' in b)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ('children' in b && !('children' in a)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
export const light = {
|
||||
base: 'vs' as const,
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#f5f5f6',
|
||||
'editorWidget.background': '#f5f5f6',
|
||||
|
||||
'tab.activeBackground': '#f5f5f6',
|
||||
'tab.inactiveBackground': 'rgb(236, 236, 236)',
|
||||
'tab.activeForeground': '#101112', // black
|
||||
'tab.inactiveForeground': '#4a5568', // gray.600
|
||||
'tab.border': 'rgb(243, 243, 243)',
|
||||
|
||||
'icon.foreground': '#616161',
|
||||
|
||||
'input.foreground': '#616161',
|
||||
'input.background': '#fff',
|
||||
|
||||
'list.inactiveSelectionBackground': '#e4e6f1',
|
||||
|
||||
'breadcrumbs.foreground': 'rgb(97, 97, 97)',
|
||||
|
||||
'badge.background': '#c4c4c4',
|
||||
|
||||
'sideBar.background': '#eee',
|
||||
|
||||
focusBorder: '#0090f1',
|
||||
|
||||
// not able to use rgba for standard variables, so we use custom prefix here
|
||||
'custom.list.hoverBackground': 'rgba(16, 17, 18, 0.08)', // blackAlpha.200
|
||||
'custom.findMatchHighlightBackground': 'rgba(234,92,0,0.33)',
|
||||
'custom.inputOption.activeBackground': 'rgba(0, 144, 241, 0.2)',
|
||||
'custom.inputOption.hoverBackground': 'rgba(184, 184, 184, 0.31)',
|
||||
|
||||
// don't know the name of this variables in vscode
|
||||
'custom.fileLink.hoverForeground': '#4299E1', // blue.400
|
||||
} as const,
|
||||
};
|
||||
|
||||
export const dark = {
|
||||
base: 'vs-dark' as const,
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#1a1b1b',
|
||||
'editorWidget.background': '#1a1b1b',
|
||||
|
||||
'tab.activeBackground': '#1a1b1b', // black
|
||||
'tab.inactiveBackground': 'rgb(45, 45, 45)',
|
||||
'tab.activeForeground': '#fff', // white
|
||||
'tab.inactiveForeground': '#a0aec0', // gray.400
|
||||
'tab.border': 'rgb(37, 37, 38)',
|
||||
|
||||
'icon.foreground': '#616161',
|
||||
|
||||
'input.foreground': '#cccccc',
|
||||
'input.background': '#3c3c3c',
|
||||
|
||||
'list.inactiveSelectionBackground': '#37373d',
|
||||
|
||||
'badge.background': '#4d4d4d',
|
||||
|
||||
'breadcrumbs.foreground': 'rgb(97, 97, 97)',
|
||||
|
||||
'sideBar.background': '#222',
|
||||
|
||||
focusBorder: '#007fd4',
|
||||
|
||||
// not able to use rgba for standard variables, so we use custom prefix here
|
||||
'custom.list.hoverBackground': 'rgba(255, 255, 255, 0.08)', // whiteAlpha.200
|
||||
'custom.findMatchHighlightBackground': 'rgba(234,92,0,0.33)',
|
||||
'custom.inputOption.activeBackground': 'rgba(0, 127, 212, 0.4)',
|
||||
'custom.inputOption.hoverBackground': 'rgba(90, 93, 94, 0.31)',
|
||||
|
||||
// don't know the name of this variables in vscode
|
||||
'custom.fileLink.hoverForeground': '#4299E1', // blue.400
|
||||
} as const,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useColorModeValue } from '@chakra-ui/react';
|
||||
|
||||
import * as themes from './themes';
|
||||
|
||||
export default function useThemeColors() {
|
||||
const theme = useColorModeValue(themes.light, themes.dark);
|
||||
|
||||
return theme.colors;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ResponsiveValue } from '@chakra-ui/react';
|
||||
import { AspectRatio, chakra, Icon, Image, shouldForwardProp, Skeleton, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Box, AspectRatio, chakra, Icon, Image, shouldForwardProp, Skeleton, useColorModeValue } from '@chakra-ui/react';
|
||||
import type { Property } from 'csstype';
|
||||
import React from 'react';
|
||||
|
||||
@@ -30,6 +30,7 @@ const Fallback = ({ className, padding }: FallbackProps) => {
|
||||
|
||||
const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
|
||||
const [ isLoading, setIsLoading ] = React.useState(true);
|
||||
const [ isError, setIsError ] = React.useState(false);
|
||||
|
||||
const handleLoad = React.useCallback(() => {
|
||||
setIsLoading(false);
|
||||
@@ -37,10 +38,37 @@ const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
|
||||
|
||||
const handleLoadError = React.useCallback(() => {
|
||||
setIsLoading(false);
|
||||
setIsError(true);
|
||||
}, []);
|
||||
|
||||
const _objectFit = objectFit || 'contain';
|
||||
|
||||
const content = (() => {
|
||||
// as of ChakraUI v2.5.3
|
||||
// fallback prop of Image component doesn't work well with loading prop lazy strategy
|
||||
// so we have to render fallback and loader manually
|
||||
if (isError || !url) {
|
||||
return <Fallback className={ className } padding={ fallbackPadding }/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{ isLoading && <Skeleton position="absolute" left={ 0 } top={ 0 } w="100%" h="100%" zIndex="1"/> }
|
||||
<Image
|
||||
w="100%"
|
||||
h="100%"
|
||||
objectFit={ _objectFit }
|
||||
src={ url }
|
||||
opacity={ isLoading ? 0 : 1 }
|
||||
alt="Token instance image"
|
||||
onError={ handleLoadError }
|
||||
onLoad={ handleLoad }
|
||||
loading={ url ? 'lazy' : undefined }
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<AspectRatio
|
||||
className={ className }
|
||||
@@ -54,17 +82,7 @@ const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
w="100%"
|
||||
h="100%"
|
||||
objectFit={ _objectFit }
|
||||
src={ url || undefined }
|
||||
alt="Token instance image"
|
||||
fallback={ url && isLoading ? <Skeleton/> : <Fallback className={ className } padding={ fallbackPadding }/> }
|
||||
onError={ handleLoadError }
|
||||
onLoad={ handleLoad }
|
||||
loading={ url ? 'lazy' : undefined }
|
||||
/>
|
||||
{ content }
|
||||
</AspectRatio>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ const Sort = <Sort extends string>({ sort, setSort, options }: Props<Sort>) => {
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton>
|
||||
<MenuButton as="div">
|
||||
<SortButton
|
||||
isActive={ isOpen || Boolean(sort) }
|
||||
onClick={ onToggle }
|
||||
|
||||
@@ -74,9 +74,19 @@ const TokenDetails = ({ tokenQuery }: Props) => {
|
||||
total_supply: totalSupply,
|
||||
decimals,
|
||||
symbol,
|
||||
type,
|
||||
} = tokenQuery.data;
|
||||
|
||||
const totalValue = totalSupply !== null ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined;
|
||||
let marketcap;
|
||||
let totalSupplyValue;
|
||||
|
||||
if (type === 'ERC-20') {
|
||||
const totalValue = totalSupply !== null ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined;
|
||||
marketcap = totalValue?.usd;
|
||||
totalSupplyValue = totalValue?.valueStr;
|
||||
} else {
|
||||
totalSupplyValue = Number(totalSupply).toLocaleString('en');
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
@@ -94,13 +104,13 @@ const TokenDetails = ({ tokenQuery }: Props) => {
|
||||
{ `$${ exchangeRate }` }
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
{ totalValue?.usd && (
|
||||
{ marketcap && (
|
||||
<DetailsInfoItem
|
||||
title="Fully diluted market cap"
|
||||
hint="Total supply * Price"
|
||||
alignSelf="center"
|
||||
>
|
||||
{ `$${ totalValue?.usd }` }
|
||||
{ `$${ marketcap }` }
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
<DetailsInfoItem
|
||||
@@ -112,7 +122,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
|
||||
>
|
||||
<Flex w="100%">
|
||||
<Box whiteSpace="nowrap" overflow="hidden">
|
||||
<HashStringShortenDynamic hash={ totalValue?.valueStr || '0' }/>
|
||||
<HashStringShortenDynamic hash={ totalSupplyValue || '0' }/>
|
||||
</Box>
|
||||
<Box flexShrink={ 0 }> { symbol || '' }</Box>
|
||||
</Flex>
|
||||
|
||||
@@ -93,14 +93,12 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
|
||||
/>
|
||||
</Flex>
|
||||
<Grid
|
||||
mt={ 8 }
|
||||
mt={ 5 }
|
||||
columnGap={ 8 }
|
||||
rowGap={{ base: 1, lg: 3 }}
|
||||
templateColumns={{ base: 'minmax(0, 1fr)', lg: '200px minmax(0, 1fr)' }}
|
||||
overflow="hidden"
|
||||
>
|
||||
{ divider }
|
||||
<DetailsSponsoredItem/>
|
||||
{ hasMetadata && (
|
||||
<>
|
||||
{ divider }
|
||||
@@ -148,6 +146,8 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
|
||||
) }
|
||||
</>
|
||||
) }
|
||||
{ divider }
|
||||
<DetailsSponsoredItem/>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 83 KiB |