Commit 7519a098 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into search-bar

parents 0845994e 9f7e0784
......@@ -19,7 +19,8 @@ NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_CURREN
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS__
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS__
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED__
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE__
NEXT_PUBLIC_NETWORK_RPC_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_RPC_URL__
# ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=__PLACEHOLDER_FOR_NEXT_PUBLIC_BLOCKSCOUT_VERSION__
......
......@@ -46,6 +46,7 @@ The app instance could be customized by passing following variables to NodeJS en
| 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` | Chain server RPC url, see [https://chainlist.org/](https://chainlist.org/) for the reference | `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` |
......
......@@ -74,7 +74,8 @@ const config = Object.freeze({
},
assetsPathname: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME),
explorers: parseEnvJson<Array<NetworkExplorer>>(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_EXPLORERS)) || [],
verificationType: process.env.NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE || 'mining',
verificationType: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE) || 'mining',
rpcUrl: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_RPC_URL),
},
footerLinks: {
github: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK),
......
......@@ -14,6 +14,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-tracker','title':'Token Approval Tracker','logo':'https://approval-tracker.vercel.app/icon-192.png','categories':['security','tools'],'shortDescription':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','site':'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker','description':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','url':'https://approval-tracker.vercel.app/'},{'author': 'Revoke','id':'revoke.cash','title':'Revoke.cash','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FVBMGyUFnd6CScjfK7CYQ%252Frevoke_sing.png%3Falt%3Dmedia%26token%3D9ab94986-7ab1-41c8-bf7e-d9ce11d23182','categories':['security','tools'],'shortDescription': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','site': 'https://revoke.cash/about','description': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','url':'https://revoke.cash/'},{'author':'Aave','id': 'aave','title': 'Aave','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrZkUTIUCG7Zx8BW6Em34%252FAave.png%3Falt%3Dmedia%26token%3D249797a4-4c1e-4372-9cd2-3e48e05e5f30','categories':['tools'],'shortDescription':'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','site': 'https://docs.aave.com/faq/','description': 'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','url': 'https://staging.aave.com/'},{'author':'LooksRare','id':'looksrare','external':true,'title':'LooksRare','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FeAI4gy3qPMt68mZOZHAx%252FLooksRare.png%3Falt%3Dmedia%26token%3D44c01439-ae09-40aa-b904-3a9ce5b2e002','categories':['tools'],'shortDescription': 'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','site':'https://docs.looksrare.org/about/welcome-to-looksrare','description':'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','url': 'https://goerli.looksrare.org/'},{'author':'zkSync Bridge','id':'zksync-bridge','external':true,'title':'zkSync Bridge','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrtQsaAz9BjGBc35tVAnq%252FzkSync.png%3Falt%3Dmedia%26token%3D5c18171c-8ccf-4a88-8f44-680cbf238115','categories':['security','tools'],'shortDescription':'zkSync 2.0 Goerli Bridge','site':'https://v2-docs.zksync.io/dev/','description':'zkSync 2.0 Goerli Bridge','url':'https://portal.zksync.io/bridge'},{'author':'dYdX','id':'dydx','external':true,'title':'dYdX','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FCrOglR72wpi0UhEscwe4%252Fdxdy.png%3Falt%3Dmedia%26token%3D8811909e-93e3-487c-9614-dffce37223e9','categories': ['security','tools'],'shortDescription':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','site':'https://help.dydx.exchange/en/articles/3047379-introduction-and-overview','description':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','url':'https://trade.stage.dydx.exchange/portfolio/overview'},{'author':'MetalSwap','id':'metalswap','title':'MetalSwap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252F8xqldTvxb6avrwVVc3rS%252FMetalSwap.png%3Falt%3Dmedia%26token%3D92d2db99-853a-487d-8d8c-8cdeaeaaf014','categories':['security','tools'],'shortDescription':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','site':'https://docs.metalswap.finance/','description':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','url':'https://demo.metalswap.finance/'},{'author':'FaucetDao','id':'faucetdao','title':'FaucetDao','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252Ffnnt3ZNZhzRwqMM5YYjD%252FPlaceholder.png%3Falt%3Dmedia%26token%3D507571bb-d76f-4d96-a35e-2b278608f7ca','categories':['tools'],'shortDescription':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','site':'https://linktr.ee/faucet_dao','description':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','url':'https://www.faucetdao.shop/swap?chain=goerli'},{'author':'Uniswap','id':'uniswap','title':'Uniswap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FJc0QAyeaBmFIL97tSmGv%252FUniswap.png%3Falt%3Dmedia%26token%3D5d25d796-c273-4e22-92fa-ff85206bec76','categories':['tools'],'shortDescription':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','site':'https://docs.uniswap.org/','description':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','url':'https://app.uniswap.org/swap'}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
......
......@@ -20,6 +20,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-tracker','title':'Token Approval Tracker','logo':'https://approval-tracker.vercel.app/icon-192.png','categories':['security','tools'],'shortDescription':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','site':'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker','description':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','url':'https://approval-tracker.vercel.app/'},{'author': 'Revoke','id':'revoke.cash','title':'Revoke.cash','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FVBMGyUFnd6CScjfK7CYQ%252Frevoke_sing.png%3Falt%3Dmedia%26token%3D9ab94986-7ab1-41c8-bf7e-d9ce11d23182','categories':['security','tools'],'shortDescription': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','site': 'https://revoke.cash/about','description': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','url':'https://revoke.cash/'},{'author':'Aave','id': 'aave','title': 'Aave','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrZkUTIUCG7Zx8BW6Em34%252FAave.png%3Falt%3Dmedia%26token%3D249797a4-4c1e-4372-9cd2-3e48e05e5f30','categories':['tools'],'shortDescription':'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','site': 'https://docs.aave.com/faq/','description': 'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','url': 'https://staging.aave.com/'},{'author':'LooksRare','id':'looksrare','external':true,'title':'LooksRare','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FeAI4gy3qPMt68mZOZHAx%252FLooksRare.png%3Falt%3Dmedia%26token%3D44c01439-ae09-40aa-b904-3a9ce5b2e002','categories':['tools'],'shortDescription': 'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','site':'https://docs.looksrare.org/about/welcome-to-looksrare','description':'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','url': 'https://goerli.looksrare.org/'},{'author':'zkSync Bridge','id':'zksync-bridge','external':true,'title':'zkSync Bridge','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrtQsaAz9BjGBc35tVAnq%252FzkSync.png%3Falt%3Dmedia%26token%3D5c18171c-8ccf-4a88-8f44-680cbf238115','categories':['security','tools'],'shortDescription':'zkSync 2.0 Goerli Bridge','site':'https://v2-docs.zksync.io/dev/','description':'zkSync 2.0 Goerli Bridge','url':'https://portal.zksync.io/bridge'},{'author':'dYdX','id':'dydx','external':true,'title':'dYdX','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FCrOglR72wpi0UhEscwe4%252Fdxdy.png%3Falt%3Dmedia%26token%3D8811909e-93e3-487c-9614-dffce37223e9','categories': ['security','tools'],'shortDescription':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','site':'https://help.dydx.exchange/en/articles/3047379-introduction-and-overview','description':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','url':'https://trade.stage.dydx.exchange/portfolio/overview'},{'author':'MetalSwap','id':'metalswap','title':'MetalSwap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252F8xqldTvxb6avrwVVc3rS%252FMetalSwap.png%3Falt%3Dmedia%26token%3D92d2db99-853a-487d-8d8c-8cdeaeaaf014','categories':['security','tools'],'shortDescription':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','site':'https://docs.metalswap.finance/','description':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','url':'https://demo.metalswap.finance/'},{'author':'FaucetDao','id':'faucetdao','title':'FaucetDao','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252Ffnnt3ZNZhzRwqMM5YYjD%252FPlaceholder.png%3Falt%3Dmedia%26token%3D507571bb-d76f-4d96-a35e-2b278608f7ca','categories':['tools'],'shortDescription':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','site':'https://linktr.ee/faucet_dao','description':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','url':'https://www.faucetdao.shop/swap?chain=goerli'},{'author':'Uniswap','id':'uniswap','title':'Uniswap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FJc0QAyeaBmFIL97tSmGv%252FUniswap.png%3Falt%3Dmedia%26token%3D5d25d796-c273-4e22-92fa-ff85206bec76','categories':['tools'],'shortDescription':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','site':'https://docs.uniswap.org/','description':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','url':'https://app.uniswap.org/swap'}]
......
......@@ -13,6 +13,7 @@ NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_NETWORK_LOGO=
NEXT_PUBLIC_NETWORK_SMALL_LOGO=
NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
# api config
NEXT_PUBLIC_API_HOST=blockscout.com
......
This diff is collapsed.
......@@ -397,6 +397,8 @@ frontend:
_default: "['daily_txs','coin_price','market_cup']"
NEXT_PUBLIC_APP_HOST:
_default: blockscout.com
NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]"
......@@ -17,7 +17,6 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch
import type { SmartContract } from 'types/api/contract';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search';
......@@ -203,11 +202,6 @@ export const RESOURCES = {
path: '/api/v2/main-page/indexing-status',
},
// CONFIG
config_json_rpc: {
path: '/api/v2/config/json-rpc-url',
},
// SEARCH
search: {
path: '/api/v2/search',
......@@ -301,7 +295,6 @@ Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters :
Q extends 'token_holders' ? TokenHolders :
Q extends 'config_json_rpc' ? JsonRpcUrlResponse :
Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract :
never;
......
......@@ -2,8 +2,10 @@
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import minMax from 'dayjs/plugin/minMax';
import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale';
import weekOfYear from 'dayjs/plugin/weekOfYear';
const relativeTimeConfig = {
thresholds: [
......@@ -26,6 +28,8 @@ dayjs.extend(relativeTime, relativeTimeConfig);
dayjs.extend(updateLocale);
dayjs.extend(localizedFormat);
dayjs.extend(duration);
dayjs.extend(weekOfYear);
dayjs.extend(minMax);
dayjs.updateLocale('en', {
formats: {
......
......@@ -20,15 +20,13 @@ export default function useFetch() {
const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => {
const hasBody = params?.method && params?.body && ![ 'GET', 'HEAD' ].includes(params.method);
const reqParams = {
...params,
body: params?.method && params?.body && ![ 'GET', 'HEAD' ].includes(params.method) ?
JSON.stringify({
...params.body,
_csrf_token: token,
}) :
undefined,
body: hasBody ? JSON.stringify({ ...params.body, _csrf_token: token }) : undefined,
headers: {
...(hasBody ? { 'Content-type': 'application/json' } : undefined),
...params?.headers,
// ...(token ? { 'x-csrf-token': token } : {}),
},
......
......@@ -3,21 +3,15 @@ import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import { resourceKey } from 'lib/api/resources';
import type { ResourceError } from 'lib/api/resources';
import * as cookies from 'lib/cookies';
import useLoginUrl from 'lib/hooks/useLoginUrl';
export interface ErrorType {
error?: {
status: Response['status'];
statusText: Response['statusText'];
};
}
export default function useRedirectForInvalidAuthToken() {
const queryClient = useQueryClient();
const state = queryClient.getQueryState<unknown, ErrorType>([ resourceKey('user_info') ]);
const errorStatus = state?.error?.error?.status;
const state = queryClient.getQueryState<unknown, ResourceError>([ resourceKey('user_info') ]);
const errorStatus = state?.error?.status;
const loginUrl = useLoginUrl();
React.useEffect(() => {
......
......@@ -41,4 +41,15 @@ export const withToken: Address = {
creator_address_hash: null,
exchange_rate: null,
implementation_address: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: false,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false,
has_tokens: true,
has_validated_blocks: false,
};
export const base = {
import type { AddressCoinBalanceHistoryItem, AddressCoinBalanceHistoryResponse, AddressCoinBalanceHistoryChart } from 'types/api/address';
export const base: AddressCoinBalanceHistoryItem = {
block_number: 30367643,
block_timestamp: '2022-12-11T17:55:20Z',
delta: '-5568096000000000',
transaction_hash: null,
value: '107014805905725000000',
};
export const baseResponse: AddressCoinBalanceHistoryResponse = {
items: [
{
block_number: 30367643,
block_timestamp: '2022-10-11T17:55:20Z',
delta: '-2105682233848856',
transaction_hash: null,
value: '10102109526582662088',
},
{
block_number: 30367234,
block_timestamp: '2022-10-01T17:55:20Z',
delta: '1933020674364000',
transaction_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
value: '10143933697708939226',
},
{
block_number: 30363402,
block_timestamp: '2022-09-03T17:55:20Z',
delta: '-1448410607186694',
transaction_hash: null,
value: '10142485287101752532',
},
],
next_page_params: null,
};
export const chartResponse: AddressCoinBalanceHistoryChart = [
{
date: '2022-11-02',
value: '128238612887883515',
},
{
date: '2022-11-03',
value: '199807583157570922',
},
{
date: '2022-11-04',
value: '114487912907005778',
},
{
date: '2022-11-05',
value: '219533112907005778',
},
{
date: '2022-11-06',
value: '116487912907005778',
},
{
date: '2022-11-07',
value: '199807583157570922',
},
{
date: '2022-11-08',
value: '216488112907005778',
},
];
......@@ -12,6 +12,17 @@ export interface Address {
creator_address_hash: string | null;
creation_tx_hash: string | null;
exchange_rate: string | null;
has_custom_methods_read: boolean;
has_custom_methods_write: boolean;
has_decompiled_code: boolean;
has_logs: boolean;
has_methods_read: boolean;
has_methods_read_proxy: boolean;
has_methods_write: boolean;
has_methods_write_proxy: boolean;
has_token_transfers: boolean;
has_tokens: boolean;
has_validated_blocks: boolean;
hash: string;
implementation_address: string | null;
implementation_name: string | null;
......@@ -77,7 +88,7 @@ export interface AddressCoinBalanceHistoryResponse {
next_page_params: {
block_number: number;
items_count: number;
};
} | null;
}
export type AddressCoinBalanceHistoryChart = Array<{
......
export type JsonRpcUrlResponse = {
json_rpc_url: string;
}
......@@ -22,7 +22,11 @@ import { default as Thead } from 'ui/shared/TheadSticky';
import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem';
import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem';
const AddressBlocksValidated = () => {
interface Props {
scrollRef?: React.RefObject<HTMLDivElement>;
}
const AddressBlocksValidated = ({ scrollRef }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient();
const router = useRouter();
......@@ -31,6 +35,7 @@ const AddressBlocksValidated = () => {
const query = useQueryWithPages({
resourceName: 'address_blocks_validated',
pathParams: { id: addressHash },
scrollRef,
});
const handleSocketError = React.useCallback(() => {
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as balanceHistoryMock from 'mocks/address/coinBalanceHistory';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressCoinBalance from './AddressCoinBalance';
const addressHash = 'hash';
const BALANCE_HISTORY_API_URL = buildApiUrl('address_coin_balance', { id: addressHash });
const BALANCE_HISTORY_CHART_API_URL = buildApiUrl('address_coin_balance_chart', { id: addressHash });
const hooksConfig = {
router: {
query: { id: addressHash },
},
};
test('base view +@dark-mode +@mobile', async({ mount, page }) => {
await page.route(BALANCE_HISTORY_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(balanceHistoryMock.baseResponse),
}));
await page.route(BALANCE_HISTORY_CHART_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(balanceHistoryMock.chartResponse),
}));
const component = await mount(
<TestApp>
<AddressCoinBalance/>
</TestApp>,
{ hooksConfig },
);
await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Balances-small"]')?.getAttribute('opacity') === '1';
});
await page.mouse.move(240, 100);
await expect(component).toHaveScreenshot();
});
import { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -11,6 +10,7 @@ import blockIcon from 'icons/block.svg';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
......@@ -29,9 +29,10 @@ import TokenSelect from './tokenSelect/TokenSelect';
interface Props {
addressQuery: UseQueryResult<TAddress>;
scrollRef?: React.RefObject<HTMLDivElement>;
}
const AddressDetails = ({ addressQuery }: Props) => {
const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter();
const isMobile = useIsMobile();
......@@ -41,27 +42,27 @@ const AddressDetails = ({ addressQuery }: Props) => {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
});
const tokenBalancesQuery = useApiQuery('address_token_balances', {
pathParams: { id: router.query.id?.toString() },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
});
const handleCounterItemClick = React.useCallback(() => {
window.setTimeout(() => {
// cannot do scroll instantly, have to wait a little
scrollRef?.current?.scrollIntoView({ behavior: 'smooth' });
}, 500);
}, [ scrollRef ]);
if (addressQuery.isError) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
}
if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) {
if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>;
}
if (countersQuery.isError || addressQuery.isError || tokenBalancesQuery.isError) {
if (addressQuery.isError) {
return <DataFetchAlert/>;
}
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
const validationsCount = Number(countersQuery.data.validations_count);
return (
<Box>
......@@ -104,38 +105,42 @@ const AddressDetails = ({ addressQuery }: Props) => {
</DetailsInfoItem>
) }
<AddressBalance data={ addressQuery.data }/>
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value."
alignSelf="center"
py={ 0 }
>
<TokenSelect/>
</DetailsInfoItem>
{ addressQuery.data.has_tokens && (
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value."
alignSelf="center"
py={ 0 }
>
<TokenSelect/>
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address."
>
{ Number(countersQuery.data.transactions_count).toLocaleString() }
</DetailsInfoItem>
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
{ Number(countersQuery.data.token_transfers_count).toLocaleString() }
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem>
{ addressQuery.data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address."
>
{ BigNumber(countersQuery.data.gas_usage_count).toFormat() }
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem>
{ !Object.is(validationsCount, NaN) && validationsCount > 0 && (
{ addressQuery.data.has_validated_blocks && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator."
>
{ validationsCount.toLocaleString() }
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem>
) }
{ addressQuery.data.block_number_balance_updated_at && (
......
......@@ -30,7 +30,6 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
return (
<ChartWidget
chartHeight="200px"
title="Balances"
items={ items }
isLoading={ isLoading }
......
import { Link, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import NextLink from 'next/link';
import React from 'react';
import type { AddressCounters } from 'types/api/address';
import link from 'lib/link/link';
interface Props {
prop: keyof AddressCounters;
query: UseQueryResult<AddressCounters>;
address: string;
onClick: () => void;
}
const PROP_TO_TAB = {
transactions_count: 'txs',
token_transfers_count: 'token_transfers',
validations_count: 'blocks_validated',
};
const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
if (query.isLoading) {
return <Skeleton h={ 5 } w="80px" borderRadius="full"/>;
}
const data = query.data?.[prop];
if (query.isError || data === null || data === undefined) {
return <span>no data</span>;
}
switch (prop) {
case 'gas_usage_count':
return <span>{ BigNumber(data).toFormat() }</span>;
case 'transactions_count':
case 'token_transfers_count':
case 'validations_count': {
if (data === '0') {
return <span>0</span>;
}
return (
<NextLink href={ link('address_index', { id: address }, { tab: PROP_TO_TAB[prop] }) } passHref>
<Link onClick={ onClick }>
{ Number(data).toLocaleString() }
</Link>
</NextLink>
);
}
}
};
export default React.memo(AddressCounterItem);
......@@ -54,7 +54,7 @@ const TxInternalsListItem = ({
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut }/>
</Address>
{ (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> :
......@@ -62,7 +62,7 @@ const TxInternalsListItem = ({
}
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ toData.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/>
</Address>
</Box>
<HStack spacing={ 3 }>
......
......@@ -62,7 +62,7 @@ const AddressIntTxsTableItem = ({
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ isOut }/>
</Address>
</Td>
<Td px={ 0 } verticalAlign="middle">
......@@ -74,7 +74,7 @@ const AddressIntTxsTableItem = ({
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/>
<AddressLink hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/>
<AddressLink hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/>
</Address>
</Td>
<Td isNumeric verticalAlign="middle">
......
import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQueryClient, useIsFetching } from '@tanstack/react-query';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -9,6 +10,7 @@ import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -21,7 +23,8 @@ const TokenSelect = () => {
const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>();
const addressResourceKey = getResourceKey('address', { pathParams: { id: router.query.id?.toString() } });
const addressHash = router.query.id?.toString();
const addressResourceKey = getResourceKey('address', { pathParams: { id: addressHash } });
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
......@@ -75,14 +78,19 @@ const TokenSelect = () => {
<TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/>
}
<Tooltip label="Show all tokens">
<IconButton
aria-label="Show all tokens"
variant="outline"
size="sm"
pl="6px"
pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
/>
<Box>
<NextLink href={ link('address_index', { id: addressHash }, { tab: 'tokens' }) } passHref>
<IconButton
aria-label="Show all tokens"
variant="outline"
size="sm"
pl="6px"
pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
as="a"
/>
</NextLink>
</Box>
</Tooltip>
</Flex>
);
......
......@@ -5,6 +5,7 @@ import type { TimeChartData } from 'ui/shared/chart/types';
import ethTokenTransferData from 'data/charts_eth_token_transfer.json';
import ethTxsData from 'data/charts_eth_txs.json';
import dayjs from 'lib/date/dayjs';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis';
import ChartGridLine from 'ui/shared/chart/ChartGridLine';
......@@ -22,24 +23,30 @@ const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 };
const CHART_OFFSET = {
y: 26, // legend height
};
const RANGE_DEFAULT_START_DATE = dayjs.min(dayjs(ethTokenTransferData[0].date), dayjs(ethTxsData[0].date)).toDate();
const RANGE_DEFAULT_LAST_DATE = dayjs.max(dayjs(ethTokenTransferData.at(-1)?.date), dayjs(ethTxsData.at(-1)?.date)).toDate();
const EthereumChart = () => {
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN, CHART_OFFSET);
const [ range, setRange ] = React.useState<[number, number]>([ 0, Infinity ]);
const [ range, setRange ] = React.useState<[ Date, Date ]>([ RANGE_DEFAULT_START_DATE, RANGE_DEFAULT_LAST_DATE ]);
const data: TimeChartData = [
{
name: 'Daily txs',
color: useToken('colors', 'blue.500'),
items: ethTxsData.slice(range[0], range[1]).map((d) => ({ ...d, date: new Date(d.date) })),
items: ethTxsData
.map((d) => ({ ...d, date: new Date(d.date) }))
.filter((d) => d.date >= range[0] && d.date <= range[1]),
},
{
name: 'ERC-20 tr.',
color: useToken('colors', 'green.500'),
items: ethTokenTransferData.slice(range[0], range[1]).map((d) => ({ ...d, date: new Date(d.date) })),
items: ethTokenTransferData
.map((d) => ({ ...d, date: new Date(d.date) }))
.filter((d) => d.date >= range[0] && d.date <= range[1]),
},
];
......@@ -52,12 +59,12 @@ const EthereumChart = () => {
height: innerHeight,
});
const handleRangeSelect = React.useCallback((nextRange: [number, number]) => {
setRange([ range[0] + nextRange[0], range[0] + nextRange[1] ]);
}, [ range ]);
const handleRangeSelect = React.useCallback((nextRange: [ Date, Date ]) => {
setRange([ nextRange[0], nextRange[1] ]);
}, [ ]);
const handleZoomReset = React.useCallback(() => {
setRange([ 0, Infinity ]);
setRange([ RANGE_DEFAULT_START_DATE, RANGE_DEFAULT_LAST_DATE ]);
}, [ ]);
// uncomment if we need brush the chart
......@@ -156,7 +163,7 @@ const EthereumChart = () => {
</ChartOverlay>
</g>
</svg>
{ (range[0] !== 0 || range[1] !== Infinity) && (
{ (range[0] !== RANGE_DEFAULT_START_DATE || range[1] !== RANGE_DEFAULT_LAST_DATE) && (
<Button
size="sm"
variant="outline"
......
......@@ -21,15 +21,6 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
const CONTRACT_TABS = [
{ id: 'contact_code', title: 'Code', component: <ContractCode/> },
{ id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> },
{ id: 'read_contract', title: 'Read contract', component: <div>Read contract</div> },
{ id: 'read_proxy', title: 'Read proxy', component: <div>Read proxy</div> },
{ id: 'write_contract', title: 'Write contract', component: <div>Write contract</div> },
{ id: 'write_proxy', title: 'Write proxy', component: <div>Write proxy</div> },
];
const AddressPageContent = () => {
const router = useRouter();
......@@ -46,27 +37,54 @@ const AddressPageContent = () => {
...(addressQuery.data?.watchlist_names || []),
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const isContract = addressQuery.data?.is_contract;
const contractTabs = React.useMemo(() => {
return [
{ id: 'contact_code', title: 'Code', component: <ContractCode/> },
addressQuery.data?.has_decompiled_code ?
{ id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } :
undefined,
addressQuery.data?.has_methods_read ?
{ id: 'read_contract', title: 'Read contract', component: <div>Read contract</div> } :
undefined,
addressQuery.data?.has_methods_read_proxy ?
{ id: 'read_proxy', title: 'Read proxy', component: <div>Read proxy</div> } :
undefined,
addressQuery.data?.has_custom_methods_read ?
{ id: 'read_custom_methods', title: 'Read custom methods', component: <div>Read custom methods</div> } :
undefined,
addressQuery.data?.has_methods_write ?
{ id: 'write_contract', title: 'Write contract', component: <div>Write contract</div> } :
undefined,
addressQuery.data?.has_methods_write_proxy ?
{ id: 'write_proxy', title: 'Write proxy', component: <div>Write proxy</div> } :
undefined,
addressQuery.data?.has_custom_methods_write ?
{ id: 'write_custom_methods', title: 'Write custom methods', component: <div>Write custom methods</div> } :
undefined,
].filter(notEmpty);
}, [ addressQuery.data ]);
const tabs: Array<RoutedTab> = React.useMemo(() => {
return [
{ id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> },
{ id: 'tokens', title: 'Tokens', component: null },
addressQuery.data?.has_token_transfers ?
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } :
undefined,
addressQuery.data?.has_tokens ? { id: 'tokens', title: 'Tokens', component: null } : undefined,
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> },
// temporary show this tab in all address
// later api will return info about available tabs
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated/> },
isContract ? { id: 'logs', title: 'Logs', component: <AddressLogs scrollRef={ tabsScrollRef }/> } : undefined,
isContract ? {
addressQuery.data?.has_validated_blocks ?
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated scrollRef={ tabsScrollRef }/> } :
undefined,
addressQuery.data?.has_logs ? { id: 'logs', title: 'Logs', component: <AddressLogs scrollRef={ tabsScrollRef }/> } : undefined,
addressQuery.data?.is_contract ? {
id: 'contract',
title: 'Contract',
component: <AddressContract tabs={ CONTRACT_TABS }/>,
subTabs: CONTRACT_TABS,
component: <AddressContract tabs={ contractTabs }/>,
subTabs: contractTabs,
} : undefined,
].filter(notEmpty);
}, [ isContract ]);
}, [ addressQuery.data, contractTabs ]);
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
......@@ -81,7 +99,7 @@ const AddressPageContent = () => {
additionals={ tagsNode }
/>
) }
<AddressDetails addressQuery={ addressQuery }/>
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> }
......
......@@ -3,7 +3,6 @@ import React from 'react';
import config from 'configs/app/config';
import PlusIcon from 'icons/plus.svg';
import useApiQuery from 'lib/api/useApiQuery';
import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu';
......@@ -25,8 +24,6 @@ const Apps = () => {
handleFavoriteClick,
} = useMarketplaceApps();
useApiQuery('config_json_rpc');
return (
<>
<Box
......
......@@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link';
import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page';
......@@ -23,10 +22,6 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
setIsFrameLoading(false);
}, []);
const { data: jsonRpcUrlResponse } = useApiQuery('config_json_rpc', {
queryOptions: { refetchOnMount: false },
});
useEffect(() => {
if (app && !isFrameLoading) {
const message = {
......@@ -37,12 +32,12 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
blockscoutNetworkName: appConfig.network.name,
blockscoutNetworkId: Number(appConfig.network.id),
blockscoutNetworkCurrency: appConfig.network.currency,
blockscoutNetworkRpc: jsonRpcUrlResponse?.json_rpc_url,
blockscoutNetworkRpc: appConfig.network.rpcUrl,
};
ref?.current?.contentWindow?.postMessage(message, app.url);
}
}, [ isFrameLoading, app, colorMode, ref, jsonRpcUrlResponse ]);
}, [ isFrameLoading, app, colorMode, ref ]);
const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' +
'allow-pointer-lock allow-popups-to-escape-sandbox ' +
......
......@@ -30,7 +30,7 @@ const ActionBar = ({ children, className }: Props) => {
position="sticky"
top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }}
transitionProperty="top,box-shadow,background-color,color"
transitionDuration="slow"
transitionDuration="normal"
zIndex={{ base: 'sticky2', lg: 'docked' }}
boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }}
ref={ ref }
......
......@@ -81,7 +81,7 @@ const TokenTransferListItem = ({
<Flex w="100%" columnGap={ 3 }>
<Address width={ addressWidth }>
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash }/>
</Address>
{ baseAddress ?
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> :
......@@ -89,7 +89,7 @@ const TokenTransferListItem = ({
}
<Address width={ addressWidth }>
<AddressIcon address={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash }/>
</Address>
</Flex>
{ value && (
......
......@@ -70,7 +70,7 @@ const TokenTransferTableItem = ({
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ baseAddress === from.hash }/>
</Address>
</Td>
{ baseAddress && (
......@@ -81,7 +81,7 @@ const TokenTransferTableItem = ({
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 } isDisabled={ baseAddress === to.hash }/>
</Address>
</Td>
<Td isNumeric verticalAlign="top" lineHeight="30px">
......
import { Box, chakra } from '@chakra-ui/react';
import { Box, chakra, Tooltip } from '@chakra-ui/react';
import React from 'react';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
......@@ -19,9 +19,11 @@ const AddressIcon = ({ address, className }: Props) => {
}
return (
<Box className={ className } width="24px" display="inline-flex">
<Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address.hash) }/>
</Box>
<Tooltip label={ address.implementation_name }>
<Box className={ className } width="24px" display="inline-flex">
<Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address.hash) }/>
</Box>
</Tooltip>
);
};
......
......@@ -16,9 +16,10 @@ interface Props {
fontWeight?: string;
id?: string;
target?: HTMLAttributeAnchorTarget;
isDisabled?: boolean;
}
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight, target = '_self' }: Props) => {
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight, target = '_self', isDisabled }: Props) => {
const isMobile = useIsMobile();
let url;
......@@ -52,6 +53,18 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
}
})();
if (isDisabled) {
return (
<chakra.span
className={ className }
overflow="hidden"
whiteSpace="nowrap"
>
{ content }
</chakra.span>
);
}
return (
<Link
className={ className }
......
......@@ -47,7 +47,14 @@ const ChartArea = ({ id, xScale, yScale, color, data, disableAnimation, ...props
return (
<>
<path ref={ ref } d={ d } fill={ color ? `url(#${ gradientColorId })` : 'url(#gradient-chart-area-default)' } opacity={ 0 } { ...props }/>
<path
ref={ ref }
d={ d }
fill={ color ? `url(#${ gradientColorId })` : 'url(#gradient-chart-area-default)' }
opacity={ 0 }
data-name={ id || 'gradient-chart-area' }
{ ...props }
/>
{ color ? (
<defs>
<linearGradient id={ `${ gradientColorId }` } x1="0%" x2="0%" y1="0%" y2="100%">
......
......@@ -4,14 +4,16 @@ import React from 'react';
import type { TimeChartData, TimeChartItem } from 'ui/shared/chart/types';
const SELECTION_THRESHOLD = 1;
import dayjs from 'lib/date/dayjs';
const SELECTION_THRESHOLD = 2;
interface Props {
height: number;
anchorEl?: SVGRectElement | null;
scale: d3.ScaleTime<number, number>;
data: TimeChartData;
onSelect: (range: [number, number]) => void;
onSelect: (range: [Date, Date]) => void;
}
const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => {
......@@ -51,13 +53,13 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
}, []);
const handleSelect = React.useCallback((x0: number, x1: number) => {
const index0 = getIndexByX(x0);
const index1 = getIndexByX(x1);
const startDate = scale.invert(x0);
const endDate = scale.invert(x1);
if (Math.abs(index0 - index1) > SELECTION_THRESHOLD) {
onSelect([ Math.min(index0, index1), Math.max(index0, index1) ]);
if (Math.abs(dayjs(startDate).diff(endDate, 'day')) > SELECTION_THRESHOLD) {
onSelect([ dayjs.min(dayjs(startDate), dayjs(endDate)).toDate(), dayjs.max(dayjs(startDate), dayjs(endDate)).toDate() ]);
}
}, [ getIndexByX, onSelect ]);
}, [ onSelect, scale ]);
const cleanUp = React.useCallback(() => {
isActive.current = false;
......
......@@ -11,6 +11,7 @@ import { trackPointer } from 'ui/shared/chart/utils/pointerTracker';
interface Props {
chartId?: string;
width?: number;
tooltipWidth?: number;
height?: number;
data: TimeChartData;
xScale: d3.ScaleTime<number, number>;
......@@ -23,7 +24,7 @@ const PADDING = 16;
const LINE_SPACE = 10;
const POINT_SIZE = 16;
const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl, ...props }: Props) => {
const ChartTooltip = ({ chartId, xScale, yScale, width, tooltipWidth, height, data, anchorEl, ...props }: Props) => {
const lineColor = useToken('colors', 'gray.400');
const titleColor = useToken('colors', 'blue.100');
const textColor = useToken('colors', 'white');
......@@ -66,11 +67,14 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
return `translate(${ translateX }, ${ translateY })`;
});
const date = xScale.invert(x);
const dateLabel = data[0].items.find((item) => item.date.getTime() === date.getTime())?.dateLabel;
tooltipContent
.select('.ChartTooltip__contentDate')
.text(d3.timeFormat('%e %b %Y')(xScale.invert(x)));
.text(dateLabel || d3.timeFormat('%e %b %Y')(xScale.invert(x)));
},
[ xScale, width, height ],
[ xScale, data, width, height ],
);
const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => {
......@@ -226,7 +230,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
rx={ 12 }
ry={ 12 }
fill={ bgColor }
width={ 200 }
width={ tooltipWidth || 200 }
height={ 2 * PADDING + (data.length + 1) * TEXT_LINE_HEIGHT + data.length * LINE_SPACE }
/>
<g transform={ `translate(${ PADDING },${ PADDING })` }>
......
......@@ -99,6 +99,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
return (
<>
<Box
height={ chartHeight }
ref={ ref }
padding={{ base: 3, lg: 4 }}
borderRadius="md"
......@@ -198,6 +199,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
<Box h={ chartHeight || 'auto' }>
<ChartWidgetGraph
margin={{ bottom: 20 }}
items={ items }
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
......
import { useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React, { useEffect, useMemo } from 'react';
import type { TimeChartItem } from 'ui/shared/chart/types';
import type { ChartMargin, TimeChartItem } from 'ui/shared/chart/types';
import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis';
......@@ -20,21 +22,35 @@ interface Props {
items: Array<TimeChartItem>;
onZoom: () => void;
isZoomResetInitial: boolean;
margin: ChartMargin;
}
const CHART_MARGIN = { bottom: 20, left: 40, right: 20, top: 10 };
const MAX_SHOW_ITEMS = 100;
const DEFAULT_CHART_MARGIN = { bottom: 20, left: 40, right: 20, top: 10 };
const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title }: Props) => {
const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin }: Props) => {
const isMobile = useIsMobile();
const ref = React.useRef<SVGSVGElement>(null);
const color = useToken('colors', 'blue.200');
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const [ range, setRange ] = React.useState<[ number, number ]>([ 0, Infinity ]);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
const chartMargin = { ...DEFAULT_CHART_MARGIN, ...margin };
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, chartMargin);
const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`;
const displayedData = useMemo(() => items.slice(range[0], range[1]), [ items, range ]);
const chartData = [ { items: items, name: 'Value', color } ];
const [ range, setRange ] = React.useState<[ Date, Date ]>([ items[0].date, items[items.length - 1].date ]);
const rangedItems = useMemo(() =>
items.filter((item) => item.date >= range[0] && item.date <= range[1]),
[ items, range ]);
const isGroupedValues = rangedItems.length > MAX_SHOW_ITEMS;
const displayedData = useMemo(() => {
if (isGroupedValues) {
return groupChartItemsByWeekNumber(rangedItems);
} else {
return rangedItems;
}
}, [ isGroupedValues, rangedItems ]);
const chartData = [ { items: displayedData, name: 'Value', color } ];
const { yTickFormat, xScale, yScale } = useTimeChartController({
data: [ { items: displayedData, name: title, color } ],
......@@ -42,21 +58,21 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
height: innerHeight,
});
const handleRangeSelect = React.useCallback((nextRange: [ number, number ]) => {
setRange([ range[0] + nextRange[0], range[0] + nextRange[1] ]);
const handleRangeSelect = React.useCallback((nextRange: [ Date, Date ]) => {
setRange([ nextRange[0], nextRange[1] ]);
onZoom();
}, [ onZoom, range ]);
}, [ onZoom ]);
useEffect(() => {
if (isZoomResetInitial) {
setRange([ 0, Infinity ]);
setRange([ items[0].date, items[items.length - 1].date ]);
}
}, [ isZoomResetInitial ]);
}, [ isZoomResetInitial, items ]);
return (
<svg width={ width || '100%' } height={ height || 'auto' } ref={ ref } cursor="pointer" id={ chartId } opacity={ width ? 1 : 0 }>
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref } cursor="pointer" id={ chartId } opacity={ width ? 1 : 0 }>
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` }>
<g transform={ `translate(${ chartMargin?.left || 0 },${ chartMargin?.top || 0 })` }>
<ChartGridLine
type="horizontal"
scale={ yScale }
......@@ -104,6 +120,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
chartId={ chartId }
anchorEl={ overlayRef.current }
width={ innerWidth }
tooltipWidth={ isGroupedValues ? 280 : 200 }
height={ innerHeight }
xScale={ xScale }
yScale={ yScale }
......@@ -124,3 +141,14 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
};
export default React.memo(ChartWidgetGraph);
function groupChartItemsByWeekNumber(items: Array<TimeChartItem>): Array<TimeChartItem> {
return d3.rollups(items,
(group) => ({
date: group[0].date,
value: d3.sum(group, (d) => d.value),
dateLabel: `${ d3.timeFormat('%e %b %Y')(group[0].date) }${ d3.timeFormat('%e %b %Y')(group[group.length - 1].date) }`,
}),
(t) => dayjs(t.date).week(),
).map(([ , v ]) => v);
}
......@@ -91,6 +91,7 @@ const FullscreenChartModal = ({
h="100%"
>
<ChartWidgetGraph
margin={{ bottom: 60 }}
isEnlarged
items={ items }
onZoom={ handleZoom }
......
export interface TimeChartItem {
date: Date;
dateLabel?: string;
value: number;
}
......
......@@ -48,7 +48,7 @@ export const statsChartsScheme: Array<StatsSection> = [
title: 'Blocks',
charts: [
{
apiId: 'newBlocksPerDay',
apiId: 'newBlocks',
title: 'New blocks',
description: 'New blocks number',
},
......@@ -97,7 +97,7 @@ export const statsChartsScheme: Array<StatsSection> = [
{
apiId: 'averageGasPrice',
title: 'Average gas price',
description: 'Average gas price for the period',
description: 'Average gas price for the period (Gwei)',
},
],
},
......
......@@ -108,6 +108,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
alias={ tx.from.name }
fontWeight="500"
ml={ 2 }
isDisabled={ isOut }
/>
</Address>
{ (isIn || isOut) ?
......@@ -126,6 +127,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
alias={ dataTo.name }
fontWeight="500"
ml={ 2 }
isDisabled={ isIn }
/>
</Address>
</Flex>
......
......@@ -7,7 +7,6 @@ import {
Icon,
VStack,
Text,
Tooltip,
Popover,
PopoverTrigger,
PopoverContent,
......@@ -50,10 +49,8 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
const addressFrom = (
<Address>
<Tooltip label={ tx.from.implementation_name }>
<Box display="flex"><AddressIcon address={ tx.from }/></Box>
</Tooltip>
<AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 } truncation="constant"/>
<AddressIcon address={ tx.from }/>
<AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isOut }/>
</Address>
);
......@@ -61,10 +58,8 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
const addressTo = (
<Address>
<Tooltip label={ dataTo.implementation_name }>
<Box display="flex"><AddressIcon address={ dataTo }/></Box>
</Tooltip>
<AddressLink hash={ dataTo.hash } alias={ dataTo.name } fontWeight="500" ml={ 2 } truncation="constant"/>
<AddressIcon address={ dataTo }/>
<AddressLink hash={ dataTo.hash } alias={ dataTo.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isIn }/>
</Address>
);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment