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 ...@@ -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_CURRENCY_DECIMALS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS__
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS__ 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_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 # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=__PLACEHOLDER_FOR_NEXT_PUBLIC_BLOCKSCOUT_VERSION__ 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 ...@@ -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_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_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_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_NAME | `string` | Network currency name | `Ether` |
| NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | `ETH` | | NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL | `string` | Network currency symbol | `ETH` |
| NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | `18` | | NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS | `string` | Network currency decimals | `18` |
......
...@@ -74,7 +74,8 @@ const config = Object.freeze({ ...@@ -74,7 +74,8 @@ const config = Object.freeze({
}, },
assetsPathname: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME), assetsPathname: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME),
explorers: parseEnvJson<Array<NetworkExplorer>>(getEnvValue(process.env.NEXT_PUBLIC_NETWORK_EXPLORERS)) || [], 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: { footerLinks: {
github: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK), github: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK),
......
...@@ -14,6 +14,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 ...@@ -14,6 +14,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation 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_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_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'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
......
...@@ -20,6 +20,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 ...@@ -20,6 +20,7 @@ NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172 NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation 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_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_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 ...@@ -13,6 +13,7 @@ NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_NETWORK_LOGO= NEXT_PUBLIC_NETWORK_LOGO=
NEXT_PUBLIC_NETWORK_SMALL_LOGO= NEXT_PUBLIC_NETWORK_SMALL_LOGO=
NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
......
This diff is collapsed.
...@@ -397,6 +397,8 @@ frontend: ...@@ -397,6 +397,8 @@ frontend:
_default: "['daily_txs','coin_price','market_cup']" _default: "['daily_txs','coin_price','market_cup']"
NEXT_PUBLIC_APP_HOST: NEXT_PUBLIC_APP_HOST:
_default: blockscout.com _default: blockscout.com
NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_EXPLORERS: 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'}}]" _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 ...@@ -17,7 +17,6 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch
import type { SmartContract } from 'types/api/contract'; import type { SmartContract } from 'types/api/contract';
import type { IndexingStatus } from 'types/api/indexingStatus'; import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search'; import type { SearchResult, SearchResultFilters } from 'types/api/search';
...@@ -203,11 +202,6 @@ export const RESOURCES = { ...@@ -203,11 +202,6 @@ export const RESOURCES = {
path: '/api/v2/main-page/indexing-status', path: '/api/v2/main-page/indexing-status',
}, },
// CONFIG
config_json_rpc: {
path: '/api/v2/config/json-rpc-url',
},
// SEARCH // SEARCH
search: { search: {
path: '/api/v2/search', path: '/api/v2/search',
...@@ -301,7 +295,6 @@ Q extends 'address_logs' ? LogsResponseAddress : ...@@ -301,7 +295,6 @@ Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'token' ? TokenInfo : Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters : Q extends 'token_counters' ? TokenCounters :
Q extends 'token_holders' ? TokenHolders : Q extends 'token_holders' ? TokenHolders :
Q extends 'config_json_rpc' ? JsonRpcUrlResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
never; never;
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration'; import duration from 'dayjs/plugin/duration';
import localizedFormat from 'dayjs/plugin/localizedFormat'; import localizedFormat from 'dayjs/plugin/localizedFormat';
import minMax from 'dayjs/plugin/minMax';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale'; import updateLocale from 'dayjs/plugin/updateLocale';
import weekOfYear from 'dayjs/plugin/weekOfYear';
const relativeTimeConfig = { const relativeTimeConfig = {
thresholds: [ thresholds: [
...@@ -26,6 +28,8 @@ dayjs.extend(relativeTime, relativeTimeConfig); ...@@ -26,6 +28,8 @@ dayjs.extend(relativeTime, relativeTimeConfig);
dayjs.extend(updateLocale); dayjs.extend(updateLocale);
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
dayjs.extend(duration); dayjs.extend(duration);
dayjs.extend(weekOfYear);
dayjs.extend(minMax);
dayjs.updateLocale('en', { dayjs.updateLocale('en', {
formats: { formats: {
......
...@@ -20,15 +20,13 @@ export default function useFetch() { ...@@ -20,15 +20,13 @@ export default function useFetch() {
const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {}; const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => { 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 = { const reqParams = {
...params, ...params,
body: params?.method && params?.body && ![ 'GET', 'HEAD' ].includes(params.method) ? body: hasBody ? JSON.stringify({ ...params.body, _csrf_token: token }) : undefined,
JSON.stringify({
...params.body,
_csrf_token: token,
}) :
undefined,
headers: { headers: {
...(hasBody ? { 'Content-type': 'application/json' } : undefined),
...params?.headers, ...params?.headers,
// ...(token ? { 'x-csrf-token': token } : {}), // ...(token ? { 'x-csrf-token': token } : {}),
}, },
......
...@@ -3,21 +3,15 @@ import { useQueryClient } from '@tanstack/react-query'; ...@@ -3,21 +3,15 @@ import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { resourceKey } from 'lib/api/resources'; import { resourceKey } from 'lib/api/resources';
import type { ResourceError } from 'lib/api/resources';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useLoginUrl from 'lib/hooks/useLoginUrl'; import useLoginUrl from 'lib/hooks/useLoginUrl';
export interface ErrorType {
error?: {
status: Response['status'];
statusText: Response['statusText'];
};
}
export default function useRedirectForInvalidAuthToken() { export default function useRedirectForInvalidAuthToken() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const state = queryClient.getQueryState<unknown, ErrorType>([ resourceKey('user_info') ]); const state = queryClient.getQueryState<unknown, ResourceError>([ resourceKey('user_info') ]);
const errorStatus = state?.error?.error?.status; const errorStatus = state?.error?.status;
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
React.useEffect(() => { React.useEffect(() => {
......
...@@ -41,4 +41,15 @@ export const withToken: Address = { ...@@ -41,4 +41,15 @@ export const withToken: Address = {
creator_address_hash: null, creator_address_hash: null,
exchange_rate: null, exchange_rate: null,
implementation_address: 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_number: 30367643,
block_timestamp: '2022-12-11T17:55:20Z', block_timestamp: '2022-12-11T17:55:20Z',
delta: '-5568096000000000', delta: '-5568096000000000',
transaction_hash: null, transaction_hash: null,
value: '107014805905725000000', 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 { ...@@ -12,6 +12,17 @@ export interface Address {
creator_address_hash: string | null; creator_address_hash: string | null;
creation_tx_hash: string | null; creation_tx_hash: string | null;
exchange_rate: 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; hash: string;
implementation_address: string | null; implementation_address: string | null;
implementation_name: string | null; implementation_name: string | null;
...@@ -77,7 +88,7 @@ export interface AddressCoinBalanceHistoryResponse { ...@@ -77,7 +88,7 @@ export interface AddressCoinBalanceHistoryResponse {
next_page_params: { next_page_params: {
block_number: number; block_number: number;
items_count: number; items_count: number;
}; } | null;
} }
export type AddressCoinBalanceHistoryChart = Array<{ export type AddressCoinBalanceHistoryChart = Array<{
......
export type JsonRpcUrlResponse = {
json_rpc_url: string;
}
...@@ -22,7 +22,11 @@ import { default as Thead } from 'ui/shared/TheadSticky'; ...@@ -22,7 +22,11 @@ import { default as Thead } from 'ui/shared/TheadSticky';
import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem'; import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem';
import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem'; import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem';
const AddressBlocksValidated = () => { interface Props {
scrollRef?: React.RefObject<HTMLDivElement>;
}
const AddressBlocksValidated = ({ scrollRef }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false); const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
...@@ -31,6 +35,7 @@ const AddressBlocksValidated = () => { ...@@ -31,6 +35,7 @@ const AddressBlocksValidated = () => {
const query = useQueryWithPages({ const query = useQueryWithPages({
resourceName: 'address_blocks_validated', resourceName: 'address_blocks_validated',
pathParams: { id: addressHash }, pathParams: { id: addressHash },
scrollRef,
}); });
const handleSocketError = React.useCallback(() => { 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 { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -11,6 +10,7 @@ import blockIcon from 'icons/block.svg'; ...@@ -11,6 +10,7 @@ import blockIcon from 'icons/block.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -29,9 +29,10 @@ import TokenSelect from './tokenSelect/TokenSelect'; ...@@ -29,9 +29,10 @@ import TokenSelect from './tokenSelect/TokenSelect';
interface Props { interface Props {
addressQuery: UseQueryResult<TAddress>; addressQuery: UseQueryResult<TAddress>;
scrollRef?: React.RefObject<HTMLDivElement>;
} }
const AddressDetails = ({ addressQuery }: Props) => { const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -41,27 +42,27 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -41,27 +42,27 @@ const AddressDetails = ({ addressQuery }: Props) => {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data), enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
}, },
}); });
const tokenBalancesQuery = useApiQuery('address_token_balances', {
pathParams: { id: router.query.id?.toString() }, const handleCounterItemClick = React.useCallback(() => {
queryOptions: { window.setTimeout(() => {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data), // cannot do scroll instantly, have to wait a little
}, scrollRef?.current?.scrollIntoView({ behavior: 'smooth' });
}); }, 500);
}, [ scrollRef ]);
if (addressQuery.isError) { if (addressQuery.isError) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error }); throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
} }
if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) { if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>; return <AddressDetailsSkeleton/>;
} }
if (countersQuery.isError || addressQuery.isError || tokenBalancesQuery.isError) { if (addressQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address); const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
const validationsCount = Number(countersQuery.data.validations_count);
return ( return (
<Box> <Box>
...@@ -104,38 +105,42 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -104,38 +105,42 @@ const AddressDetails = ({ addressQuery }: Props) => {
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<AddressBalance data={ addressQuery.data }/> <AddressBalance data={ addressQuery.data }/>
<DetailsInfoItem { addressQuery.data.has_tokens && (
title="Tokens" <DetailsInfoItem
hint="All tokens in the account and total value." title="Tokens"
alignSelf="center" hint="All tokens in the account and total value."
py={ 0 } alignSelf="center"
> py={ 0 }
<TokenSelect/> >
</DetailsInfoItem> <TokenSelect/>
</DetailsInfoItem>
) }
<DetailsInfoItem <DetailsInfoItem
title="Transactions" title="Transactions"
hint="Number of transactions related to this address." hint="Number of transactions related to this address."
> >
{ Number(countersQuery.data.transactions_count).toLocaleString() } <AddressCounterItem prop="transactions_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
{ Number(countersQuery.data.token_transfers_count).toLocaleString() }
</DetailsInfoItem> </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 <DetailsInfoItem
title="Gas used" title="Gas used"
hint="Gas used by the address." 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> </DetailsInfoItem>
{ !Object.is(validationsCount, NaN) && validationsCount > 0 && ( { addressQuery.data.has_validated_blocks && (
<DetailsInfoItem <DetailsInfoItem
title="Blocks validated" title="Blocks validated"
hint="Number of blocks validated by this validator." hint="Number of blocks validated by this validator."
> >
{ validationsCount.toLocaleString() } <AddressCounterItem prop="validations_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ addressQuery.data.block_number_balance_updated_at && ( { addressQuery.data.block_number_balance_updated_at && (
......
...@@ -30,7 +30,6 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => { ...@@ -30,7 +30,6 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
return ( return (
<ChartWidget <ChartWidget
chartHeight="200px"
title="Balances" title="Balances"
items={ items } items={ items }
isLoading={ isLoading } 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 = ({ ...@@ -54,7 +54,7 @@ const TxInternalsListItem = ({
<Box w="100%" display="flex" columnGap={ 3 }> <Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut }/>
</Address> </Address>
{ (isIn || isOut) ? { (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> : <InOutTag isIn={ isIn } isOut={ isOut }/> :
...@@ -62,7 +62,7 @@ const TxInternalsListItem = ({ ...@@ -62,7 +62,7 @@ const TxInternalsListItem = ({
} }
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/> <AddressIcon address={ toData }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ toData.hash }/> <AddressLink ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/>
</Address> </Address>
</Box> </Box>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
......
...@@ -62,7 +62,7 @@ const AddressIntTxsTableItem = ({ ...@@ -62,7 +62,7 @@ const AddressIntTxsTableItem = ({
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ from }/> <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> </Address>
</Td> </Td>
<Td px={ 0 } verticalAlign="middle"> <Td px={ 0 } verticalAlign="middle">
...@@ -74,7 +74,7 @@ const AddressIntTxsTableItem = ({ ...@@ -74,7 +74,7 @@ const AddressIntTxsTableItem = ({
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/> <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> </Address>
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
......
import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react'; import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQueryClient, useIsFetching } from '@tanstack/react-query'; import { useQueryClient, useIsFetching } from '@tanstack/react-query';
import NextLink from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,6 +10,7 @@ import type { Address } from 'types/api/address'; ...@@ -9,6 +10,7 @@ import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -21,7 +23,8 @@ const TokenSelect = () => { ...@@ -21,7 +23,8 @@ const TokenSelect = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>(); 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); const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
...@@ -75,14 +78,19 @@ const TokenSelect = () => { ...@@ -75,14 +78,19 @@ const TokenSelect = () => {
<TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/> <TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/>
} }
<Tooltip label="Show all tokens"> <Tooltip label="Show all tokens">
<IconButton <Box>
aria-label="Show all tokens" <NextLink href={ link('address_index', { id: addressHash }, { tab: 'tokens' }) } passHref>
variant="outline" <IconButton
size="sm" aria-label="Show all tokens"
pl="6px" variant="outline"
pr="6px" size="sm"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> } pl="6px"
/> pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
as="a"
/>
</NextLink>
</Box>
</Tooltip> </Tooltip>
</Flex> </Flex>
); );
......
...@@ -5,6 +5,7 @@ import type { TimeChartData } from 'ui/shared/chart/types'; ...@@ -5,6 +5,7 @@ import type { TimeChartData } from 'ui/shared/chart/types';
import ethTokenTransferData from 'data/charts_eth_token_transfer.json'; import ethTokenTransferData from 'data/charts_eth_token_transfer.json';
import ethTxsData from 'data/charts_eth_txs.json'; import ethTxsData from 'data/charts_eth_txs.json';
import dayjs from 'lib/date/dayjs';
import ChartArea from 'ui/shared/chart/ChartArea'; import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis'; import ChartAxis from 'ui/shared/chart/ChartAxis';
import ChartGridLine from 'ui/shared/chart/ChartGridLine'; import ChartGridLine from 'ui/shared/chart/ChartGridLine';
...@@ -22,24 +23,30 @@ const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 }; ...@@ -22,24 +23,30 @@ const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 };
const CHART_OFFSET = { const CHART_OFFSET = {
y: 26, // legend height 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 EthereumChart = () => {
const ref = React.useRef<SVGSVGElement>(null); const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null); const overlayRef = React.useRef<SVGRectElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN, CHART_OFFSET); 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 = [ const data: TimeChartData = [
{ {
name: 'Daily txs', name: 'Daily txs',
color: useToken('colors', 'blue.500'), 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.', name: 'ERC-20 tr.',
color: useToken('colors', 'green.500'), 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 = () => { ...@@ -52,12 +59,12 @@ const EthereumChart = () => {
height: innerHeight, height: innerHeight,
}); });
const handleRangeSelect = React.useCallback((nextRange: [number, number]) => { const handleRangeSelect = React.useCallback((nextRange: [ Date, Date ]) => {
setRange([ range[0] + nextRange[0], range[0] + nextRange[1] ]); setRange([ nextRange[0], nextRange[1] ]);
}, [ range ]); }, [ ]);
const handleZoomReset = React.useCallback(() => { const handleZoomReset = React.useCallback(() => {
setRange([ 0, Infinity ]); setRange([ RANGE_DEFAULT_START_DATE, RANGE_DEFAULT_LAST_DATE ]);
}, [ ]); }, [ ]);
// uncomment if we need brush the chart // uncomment if we need brush the chart
...@@ -156,7 +163,7 @@ const EthereumChart = () => { ...@@ -156,7 +163,7 @@ const EthereumChart = () => {
</ChartOverlay> </ChartOverlay>
</g> </g>
</svg> </svg>
{ (range[0] !== 0 || range[1] !== Infinity) && ( { (range[0] !== RANGE_DEFAULT_START_DATE || range[1] !== RANGE_DEFAULT_LAST_DATE) && (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
......
...@@ -21,15 +21,6 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -21,15 +21,6 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs'; 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 AddressPageContent = () => {
const router = useRouter(); const router = useRouter();
...@@ -46,27 +37,54 @@ const AddressPageContent = () => { ...@@ -46,27 +37,54 @@ const AddressPageContent = () => {
...(addressQuery.data?.watchlist_names || []), ...(addressQuery.data?.watchlist_names || []),
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>); ].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(() => { const tabs: Array<RoutedTab> = React.useMemo(() => {
return [ return [
{ id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> }, { id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> }, addressQuery.data?.has_token_transfers ?
{ id: 'tokens', title: 'Tokens', component: null }, { 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: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> }, { id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> },
// temporary show this tab in all address addressQuery.data?.has_validated_blocks ?
// later api will return info about available tabs { id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated scrollRef={ tabsScrollRef }/> } :
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated/> }, undefined,
isContract ? { id: 'logs', title: 'Logs', component: <AddressLogs scrollRef={ tabsScrollRef }/> } : undefined, addressQuery.data?.has_logs ? { id: 'logs', title: 'Logs', component: <AddressLogs scrollRef={ tabsScrollRef }/> } : undefined,
isContract ? { addressQuery.data?.is_contract ? {
id: 'contract', id: 'contract',
title: 'Contract', title: 'Contract',
component: <AddressContract tabs={ CONTRACT_TABS }/>, component: <AddressContract tabs={ contractTabs }/>,
subTabs: CONTRACT_TABS, subTabs: contractTabs,
} : undefined, } : undefined,
].filter(notEmpty); ].filter(notEmpty);
}, [ isContract ]); }, [ addressQuery.data, contractTabs ]);
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null; const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
...@@ -81,7 +99,7 @@ const AddressPageContent = () => { ...@@ -81,7 +99,7 @@ const AddressPageContent = () => {
additionals={ tagsNode } additionals={ tagsNode }
/> />
) } ) }
<AddressDetails addressQuery={ addressQuery }/> <AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up whith pagination */ } { /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box> <Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> } { addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> }
......
...@@ -3,7 +3,6 @@ import React from 'react'; ...@@ -3,7 +3,6 @@ import React from 'react';
import config from 'configs/app/config'; import config from 'configs/app/config';
import PlusIcon from 'icons/plus.svg'; import PlusIcon from 'icons/plus.svg';
import useApiQuery from 'lib/api/useApiQuery';
import AppList from 'ui/apps/AppList'; import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton'; import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu'; import CategoriesMenu from 'ui/apps/CategoriesMenu';
...@@ -25,8 +24,6 @@ const Apps = () => { ...@@ -25,8 +24,6 @@ const Apps = () => {
handleFavoriteClick, handleFavoriteClick,
} = useMarketplaceApps(); } = useMarketplaceApps();
useApiQuery('config_json_rpc');
return ( return (
<> <>
<Box <Box
......
...@@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; ...@@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link'; import link from 'lib/link/link';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
...@@ -23,10 +22,6 @@ const MarketplaceApp = ({ app, isLoading }: Props) => { ...@@ -23,10 +22,6 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
setIsFrameLoading(false); setIsFrameLoading(false);
}, []); }, []);
const { data: jsonRpcUrlResponse } = useApiQuery('config_json_rpc', {
queryOptions: { refetchOnMount: false },
});
useEffect(() => { useEffect(() => {
if (app && !isFrameLoading) { if (app && !isFrameLoading) {
const message = { const message = {
...@@ -37,12 +32,12 @@ const MarketplaceApp = ({ app, isLoading }: Props) => { ...@@ -37,12 +32,12 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
blockscoutNetworkName: appConfig.network.name, blockscoutNetworkName: appConfig.network.name,
blockscoutNetworkId: Number(appConfig.network.id), blockscoutNetworkId: Number(appConfig.network.id),
blockscoutNetworkCurrency: appConfig.network.currency, blockscoutNetworkCurrency: appConfig.network.currency,
blockscoutNetworkRpc: jsonRpcUrlResponse?.json_rpc_url, blockscoutNetworkRpc: appConfig.network.rpcUrl,
}; };
ref?.current?.contentWindow?.postMessage(message, app.url); ref?.current?.contentWindow?.postMessage(message, app.url);
} }
}, [ isFrameLoading, app, colorMode, ref, jsonRpcUrlResponse ]); }, [ isFrameLoading, app, colorMode, ref ]);
const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' + const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' +
'allow-pointer-lock allow-popups-to-escape-sandbox ' + 'allow-pointer-lock allow-popups-to-escape-sandbox ' +
......
...@@ -30,7 +30,7 @@ const ActionBar = ({ children, className }: Props) => { ...@@ -30,7 +30,7 @@ const ActionBar = ({ children, className }: Props) => {
position="sticky" position="sticky"
top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }} top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }}
transitionProperty="top,box-shadow,background-color,color" transitionProperty="top,box-shadow,background-color,color"
transitionDuration="slow" transitionDuration="normal"
zIndex={{ base: 'sticky2', lg: 'docked' }} zIndex={{ base: 'sticky2', lg: 'docked' }}
boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }} boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }}
ref={ ref } ref={ ref }
......
...@@ -81,7 +81,7 @@ const TokenTransferListItem = ({ ...@@ -81,7 +81,7 @@ const TokenTransferListItem = ({
<Flex w="100%" columnGap={ 3 }> <Flex w="100%" columnGap={ 3 }>
<Address width={ addressWidth }> <Address width={ addressWidth }>
<AddressIcon address={ from }/> <AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash }/>
</Address> </Address>
{ baseAddress ? { baseAddress ?
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> : <InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> :
...@@ -89,7 +89,7 @@ const TokenTransferListItem = ({ ...@@ -89,7 +89,7 @@ const TokenTransferListItem = ({
} }
<Address width={ addressWidth }> <Address width={ addressWidth }>
<AddressIcon address={ to }/> <AddressIcon address={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash }/> <AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash }/>
</Address> </Address>
</Flex> </Flex>
{ value && ( { value && (
......
...@@ -70,7 +70,7 @@ const TokenTransferTableItem = ({ ...@@ -70,7 +70,7 @@ const TokenTransferTableItem = ({
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ from }/> <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> </Address>
</Td> </Td>
{ baseAddress && ( { baseAddress && (
...@@ -81,7 +81,7 @@ const TokenTransferTableItem = ({ ...@@ -81,7 +81,7 @@ const TokenTransferTableItem = ({
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ to }/> <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> </Address>
</Td> </Td>
<Td isNumeric verticalAlign="top" lineHeight="30px"> <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 React from 'react';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon'; import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
...@@ -19,9 +19,11 @@ const AddressIcon = ({ address, className }: Props) => { ...@@ -19,9 +19,11 @@ const AddressIcon = ({ address, className }: Props) => {
} }
return ( return (
<Box className={ className } width="24px" display="inline-flex"> <Tooltip label={ address.implementation_name }>
<Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address.hash) }/> <Box className={ className } width="24px" display="inline-flex">
</Box> <Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address.hash) }/>
</Box>
</Tooltip>
); );
}; };
......
...@@ -16,9 +16,10 @@ interface Props { ...@@ -16,9 +16,10 @@ interface Props {
fontWeight?: string; fontWeight?: string;
id?: string; id?: string;
target?: HTMLAttributeAnchorTarget; 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(); const isMobile = useIsMobile();
let url; let url;
...@@ -52,6 +53,18 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, ...@@ -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 ( return (
<Link <Link
className={ className } className={ className }
......
...@@ -47,7 +47,14 @@ const ChartArea = ({ id, xScale, yScale, color, data, disableAnimation, ...props ...@@ -47,7 +47,14 @@ const ChartArea = ({ id, xScale, yScale, color, data, disableAnimation, ...props
return ( 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 ? ( { color ? (
<defs> <defs>
<linearGradient id={ `${ gradientColorId }` } x1="0%" x2="0%" y1="0%" y2="100%"> <linearGradient id={ `${ gradientColorId }` } x1="0%" x2="0%" y1="0%" y2="100%">
......
...@@ -4,14 +4,16 @@ import React from 'react'; ...@@ -4,14 +4,16 @@ import React from 'react';
import type { TimeChartData, TimeChartItem } from 'ui/shared/chart/types'; 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 { interface Props {
height: number; height: number;
anchorEl?: SVGRectElement | null; anchorEl?: SVGRectElement | null;
scale: d3.ScaleTime<number, number>; scale: d3.ScaleTime<number, number>;
data: TimeChartData; data: TimeChartData;
onSelect: (range: [number, number]) => void; onSelect: (range: [Date, Date]) => void;
} }
const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => { const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => {
...@@ -51,13 +53,13 @@ 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 handleSelect = React.useCallback((x0: number, x1: number) => {
const index0 = getIndexByX(x0); const startDate = scale.invert(x0);
const index1 = getIndexByX(x1); const endDate = scale.invert(x1);
if (Math.abs(index0 - index1) > SELECTION_THRESHOLD) { if (Math.abs(dayjs(startDate).diff(endDate, 'day')) > SELECTION_THRESHOLD) {
onSelect([ Math.min(index0, index1), Math.max(index0, index1) ]); onSelect([ dayjs.min(dayjs(startDate), dayjs(endDate)).toDate(), dayjs.max(dayjs(startDate), dayjs(endDate)).toDate() ]);
} }
}, [ getIndexByX, onSelect ]); }, [ onSelect, scale ]);
const cleanUp = React.useCallback(() => { const cleanUp = React.useCallback(() => {
isActive.current = false; isActive.current = false;
......
...@@ -11,6 +11,7 @@ import { trackPointer } from 'ui/shared/chart/utils/pointerTracker'; ...@@ -11,6 +11,7 @@ import { trackPointer } from 'ui/shared/chart/utils/pointerTracker';
interface Props { interface Props {
chartId?: string; chartId?: string;
width?: number; width?: number;
tooltipWidth?: number;
height?: number; height?: number;
data: TimeChartData; data: TimeChartData;
xScale: d3.ScaleTime<number, number>; xScale: d3.ScaleTime<number, number>;
...@@ -23,7 +24,7 @@ const PADDING = 16; ...@@ -23,7 +24,7 @@ const PADDING = 16;
const LINE_SPACE = 10; const LINE_SPACE = 10;
const POINT_SIZE = 16; 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 lineColor = useToken('colors', 'gray.400');
const titleColor = useToken('colors', 'blue.100'); const titleColor = useToken('colors', 'blue.100');
const textColor = useToken('colors', 'white'); const textColor = useToken('colors', 'white');
...@@ -66,11 +67,14 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl, ...@@ -66,11 +67,14 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
return `translate(${ translateX }, ${ translateY })`; return `translate(${ translateX }, ${ translateY })`;
}); });
const date = xScale.invert(x);
const dateLabel = data[0].items.find((item) => item.date.getTime() === date.getTime())?.dateLabel;
tooltipContent tooltipContent
.select('.ChartTooltip__contentDate') .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) => { const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => {
...@@ -226,7 +230,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl, ...@@ -226,7 +230,7 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
rx={ 12 } rx={ 12 }
ry={ 12 } ry={ 12 }
fill={ bgColor } fill={ bgColor }
width={ 200 } width={ tooltipWidth || 200 }
height={ 2 * PADDING + (data.length + 1) * TEXT_LINE_HEIGHT + data.length * LINE_SPACE } height={ 2 * PADDING + (data.length + 1) * TEXT_LINE_HEIGHT + data.length * LINE_SPACE }
/> />
<g transform={ `translate(${ PADDING },${ PADDING })` }> <g transform={ `translate(${ PADDING },${ PADDING })` }>
......
...@@ -99,6 +99,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop ...@@ -99,6 +99,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
return ( return (
<> <>
<Box <Box
height={ chartHeight }
ref={ ref } ref={ ref }
padding={{ base: 3, lg: 4 }} padding={{ base: 3, lg: 4 }}
borderRadius="md" borderRadius="md"
...@@ -198,6 +199,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop ...@@ -198,6 +199,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
<Box h={ chartHeight || 'auto' }> <Box h={ chartHeight || 'auto' }>
<ChartWidgetGraph <ChartWidgetGraph
margin={{ bottom: 20 }}
items={ items } items={ items }
onZoom={ handleZoom } onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial } isZoomResetInitial={ isZoomResetInitial }
......
import { useToken } from '@chakra-ui/react'; import { useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React, { useEffect, useMemo } from 'react'; 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 useIsMobile from 'lib/hooks/useIsMobile';
import ChartArea from 'ui/shared/chart/ChartArea'; import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis'; import ChartAxis from 'ui/shared/chart/ChartAxis';
...@@ -20,21 +22,35 @@ interface Props { ...@@ -20,21 +22,35 @@ interface Props {
items: Array<TimeChartItem>; items: Array<TimeChartItem>;
onZoom: () => void; onZoom: () => void;
isZoomResetInitial: boolean; 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 isMobile = useIsMobile();
const ref = React.useRef<SVGSVGElement>(null);
const color = useToken('colors', 'blue.200'); const color = useToken('colors', 'blue.200');
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null); const overlayRef = React.useRef<SVGRectElement>(null);
const [ range, setRange ] = React.useState<[ number, number ]>([ 0, Infinity ]); const chartMargin = { ...DEFAULT_CHART_MARGIN, ...margin };
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN); const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, chartMargin);
const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`; const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`;
const [ range, setRange ] = React.useState<[ Date, Date ]>([ items[0].date, items[items.length - 1].date ]);
const displayedData = useMemo(() => items.slice(range[0], range[1]), [ items, range ]);
const chartData = [ { items: items, name: 'Value', color } ]; 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({ const { yTickFormat, xScale, yScale } = useTimeChartController({
data: [ { items: displayedData, name: title, color } ], data: [ { items: displayedData, name: title, color } ],
...@@ -42,21 +58,21 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title ...@@ -42,21 +58,21 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
height: innerHeight, height: innerHeight,
}); });
const handleRangeSelect = React.useCallback((nextRange: [ number, number ]) => { const handleRangeSelect = React.useCallback((nextRange: [ Date, Date ]) => {
setRange([ range[0] + nextRange[0], range[0] + nextRange[1] ]); setRange([ nextRange[0], nextRange[1] ]);
onZoom(); onZoom();
}, [ onZoom, range ]); }, [ onZoom ]);
useEffect(() => { useEffect(() => {
if (isZoomResetInitial) { if (isZoomResetInitial) {
setRange([ 0, Infinity ]); setRange([ items[0].date, items[items.length - 1].date ]);
} }
}, [ isZoomResetInitial ]); }, [ isZoomResetInitial, items ]);
return ( 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 <ChartGridLine
type="horizontal" type="horizontal"
scale={ yScale } scale={ yScale }
...@@ -104,6 +120,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title ...@@ -104,6 +120,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
chartId={ chartId } chartId={ chartId }
anchorEl={ overlayRef.current } anchorEl={ overlayRef.current }
width={ innerWidth } width={ innerWidth }
tooltipWidth={ isGroupedValues ? 280 : 200 }
height={ innerHeight } height={ innerHeight }
xScale={ xScale } xScale={ xScale }
yScale={ yScale } yScale={ yScale }
...@@ -124,3 +141,14 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title ...@@ -124,3 +141,14 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
}; };
export default React.memo(ChartWidgetGraph); 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 = ({ ...@@ -91,6 +91,7 @@ const FullscreenChartModal = ({
h="100%" h="100%"
> >
<ChartWidgetGraph <ChartWidgetGraph
margin={{ bottom: 60 }}
isEnlarged isEnlarged
items={ items } items={ items }
onZoom={ handleZoom } onZoom={ handleZoom }
......
export interface TimeChartItem { export interface TimeChartItem {
date: Date; date: Date;
dateLabel?: string;
value: number; value: number;
} }
......
...@@ -48,7 +48,7 @@ export const statsChartsScheme: Array<StatsSection> = [ ...@@ -48,7 +48,7 @@ export const statsChartsScheme: Array<StatsSection> = [
title: 'Blocks', title: 'Blocks',
charts: [ charts: [
{ {
apiId: 'newBlocksPerDay', apiId: 'newBlocks',
title: 'New blocks', title: 'New blocks',
description: 'New blocks number', description: 'New blocks number',
}, },
...@@ -97,7 +97,7 @@ export const statsChartsScheme: Array<StatsSection> = [ ...@@ -97,7 +97,7 @@ export const statsChartsScheme: Array<StatsSection> = [
{ {
apiId: 'averageGasPrice', apiId: 'averageGasPrice',
title: 'Average gas price', 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 }: ...@@ -108,6 +108,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
alias={ tx.from.name } alias={ tx.from.name }
fontWeight="500" fontWeight="500"
ml={ 2 } ml={ 2 }
isDisabled={ isOut }
/> />
</Address> </Address>
{ (isIn || isOut) ? { (isIn || isOut) ?
...@@ -126,6 +127,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: ...@@ -126,6 +127,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
alias={ dataTo.name } alias={ dataTo.name }
fontWeight="500" fontWeight="500"
ml={ 2 } ml={ 2 }
isDisabled={ isIn }
/> />
</Address> </Address>
</Flex> </Flex>
......
...@@ -7,7 +7,6 @@ import { ...@@ -7,7 +7,6 @@ import {
Icon, Icon,
VStack, VStack,
Text, Text,
Tooltip,
Popover, Popover,
PopoverTrigger, PopoverTrigger,
PopoverContent, PopoverContent,
...@@ -50,10 +49,8 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -50,10 +49,8 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
const addressFrom = ( const addressFrom = (
<Address> <Address>
<Tooltip label={ tx.from.implementation_name }> <AddressIcon address={ tx.from }/>
<Box display="flex"><AddressIcon address={ tx.from }/></Box> <AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isOut }/>
</Tooltip>
<AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 } truncation="constant"/>
</Address> </Address>
); );
...@@ -61,10 +58,8 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -61,10 +58,8 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
const addressTo = ( const addressTo = (
<Address> <Address>
<Tooltip label={ dataTo.implementation_name }> <AddressIcon address={ dataTo }/>
<Box display="flex"><AddressIcon address={ dataTo }/></Box> <AddressLink hash={ dataTo.hash } alias={ dataTo.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isIn }/>
</Tooltip>
<AddressLink hash={ dataTo.hash } alias={ dataTo.name } fontWeight="500" ml={ 2 } truncation="constant"/>
</Address> </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