Commit 83f11ee2 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into contract-read

parents e0f84599 4ada09dd
...@@ -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` |
...@@ -76,7 +77,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -76,7 +77,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` *(optional)* | Gradient value for hero plate on the homepage | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` | | NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` *(optional)* | Gradient value for hero plate on the homepage | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` | | NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` | | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
| NEXT_PUBLIC_DOMAIN_WITH_AD | `string` *(optional)* | The domain on which we display ads | `blockscout.com` | | NEXT_PUBLIC_DOMAIN_WITH_AD | `string` *(optional)* | The domain on which we display ads | `blockscout.com` |
| NEXT_PUBLIC_AD_ADBUTLER_ON | `boolean` *(optional)* | Set to true to show Adbutler banner instead of Coinzilla banner | `false` | | NEXT_PUBLIC_AD_ADBUTLER_ON | `boolean` *(optional)* | Set to true to show Adbutler banner instead of Coinzilla banner | `false` |
### App configuration ### App configuration
...@@ -132,7 +133,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -132,7 +133,7 @@ The app instance could be customized by passing following variables to NodeJS en
| id | `string` | Used as slug for the app. Must be unique in the app list. | `'app'` | | id | `string` | Used as slug for the app. Must be unique in the app list. | `'app'` |
| external | `boolean` | If true means that the application opens in a new window, but not in an iframe. | `true` | | external | `boolean` | If true means that the application opens in a new window, but not in an iframe. | `true` |
| title | `string` | Displayed title of the app. | `'The App'` | | title | `string` | Displayed title of the app. | `'The App'` |
| logo | `string` | URL to logo file. Should be at least 144x144. | `'https://foo.app/icon.png'` | | logo | `string` | URL to logo file. Should be at least 288x288. | `'https://foo.app/icon.png'` |
| shortDescription | `string` | Displayed only in the app list. | `'Awesome app'` | | shortDescription | `string` | Displayed only in the app list. | `'Awesome app'` |
| categories | `Array<MarketplaceCategoryId>` | Displayed category. Select one of the following bellow. | `['security', 'tools']` | | categories | `Array<MarketplaceCategoryId>` | Displayed category. Select one of the following bellow. | `['security', 'tools']` |
| author | `string` | Displayed author of the app | `'Bob'` | | author | `string` | Displayed author of the app | `'Bob'` |
......
...@@ -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.
...@@ -395,6 +395,8 @@ frontend: ...@@ -395,6 +395,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,11 +17,10 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch ...@@ -17,11 +17,10 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } 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 { Stats, Charts, HomeStats } from 'types/api/stats'; import type { Stats, Charts, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
...@@ -191,6 +190,11 @@ export const RESOURCES = { ...@@ -191,6 +190,11 @@ export const RESOURCES = {
token_counters: { token_counters: {
path: '/api/v2/tokens/:hash/counters', path: '/api/v2/tokens/:hash/counters',
}, },
token_holders: {
path: '/api/v2/tokens/:hash/holders',
paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [],
},
// HOMEPAGE // HOMEPAGE
homepage_stats: { homepage_stats: {
...@@ -212,11 +216,6 @@ export const RESOURCES = { ...@@ -212,11 +216,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',
},
// DEPRECATED // DEPRECATED
old_api: { old_api: {
path: '/api', path: '/api',
...@@ -247,7 +246,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -247,7 +246,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' | 'txs_validated' | 'txs_pending' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'address_logs'; 'address_logs' |
'token_holders';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -290,7 +290,7 @@ Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : ...@@ -290,7 +290,7 @@ Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress : 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 'config_json_rpc' ? JsonRpcUrlResponse : Q extends 'token_holders' ? TokenHolders :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> : Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> : Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
......
...@@ -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: {
......
...@@ -4,7 +4,7 @@ import omit from 'lodash/omit'; ...@@ -4,7 +4,7 @@ import omit from 'lodash/omit';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll, scroller } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { PaginatedResources, PaginatedResponse, PaginationFilters } from 'lib/api/resources'; import type { PaginatedResources, PaginatedResponse, PaginationFilters } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources'; import { RESOURCES } from 'lib/api/resources';
...@@ -16,7 +16,7 @@ interface Params<Resource extends PaginatedResources> { ...@@ -16,7 +16,7 @@ interface Params<Resource extends PaginatedResources> {
options?: UseApiQueryParams<Resource>['queryOptions']; options?: UseApiQueryParams<Resource>['queryOptions'];
pathParams?: UseApiQueryParams<Resource>['pathParams']; pathParams?: UseApiQueryParams<Resource>['pathParams'];
filters?: PaginationFilters<Resource>; filters?: PaginationFilters<Resource>;
scroll?: { elem: string; offset: number }; scrollRef?: React.RefObject<HTMLDivElement>;
} }
export default function useQueryWithPages<Resource extends PaginatedResources>({ export default function useQueryWithPages<Resource extends PaginatedResources>({
...@@ -24,7 +24,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -24,7 +24,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
filters, filters,
options, options,
pathParams, pathParams,
scroll, scrollRef,
}: Params<Resource>) { }: Params<Resource>) {
const resource = RESOURCES[resourceName]; const resource = RESOURCES[resourceName];
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -46,8 +46,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -46,8 +46,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryParams = { ...filters, ...pageParams[page] }; const queryParams = { ...filters, ...pageParams[page] };
const scrollToTop = useCallback(() => { const scrollToTop = useCallback(() => {
scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 }); scrollRef?.current ? scrollRef.current.scrollIntoView(true) : animateScroll.scrollToTop({ duration: 0 });
}, [ scroll ]); }, [ scrollRef ]);
const queryResult = useApiQuery(resourceName, { const queryResult = useApiQuery(resourceName, {
pathParams, pathParams,
...@@ -77,10 +77,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -77,10 +77,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
nextPageQuery.page = String(page + 1); nextPageQuery.page = String(page + 1);
setHasPagination(true); setHasPagination(true);
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) scrollToTop();
.then(() => {
scrollToTop(); router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true });
});
}, [ data?.next_page_params, page, router, scrollToTop ]); }, [ data?.next_page_params, page, router, scrollToTop ]);
const onPrevPageClick = useCallback(() => { const onPrevPageClick = useCallback(() => {
...@@ -96,9 +95,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -96,9 +95,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
nextPageQuery.page = String(page - 1); nextPageQuery.page = String(page - 1);
} }
scrollToTop();
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => { .then(() => {
scrollToTop();
setPage(prev => prev - 1); setPage(prev => prev - 1);
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] }); page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
}); });
...@@ -108,9 +107,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -108,9 +107,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop();
router.push({ pathname: router.pathname, query: omit(router.query, resource.paginationFields, 'page') }, undefined, { shallow: true }).then(() => { router.push({ pathname: router.pathname, query: omit(router.query, resource.paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop();
setPage(1); setPage(1);
setPageParams({}); setPageParams({});
canGoBackwards.current = true; canGoBackwards.current = true;
...@@ -133,6 +132,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -133,6 +132,8 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
} }
}); });
} }
scrollToTop();
router.push( router.push(
{ {
pathname: router.pathname, pathname: router.pathname,
...@@ -143,7 +144,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -143,7 +144,6 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
).then(() => { ).then(() => {
setPage(1); setPage(1);
setPageParams({}); setPageParams({});
scrollToTop();
}); });
}, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]); }, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]);
......
...@@ -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',
},
];
import type { TokenHolders } from 'types/api/tokenInfo';
import { withName, withoutName } from 'mocks/address/address';
export const tokenHolders: TokenHolders = {
items: [
{
address: withName,
value: '107014805905725000000',
},
{
address: withoutName,
value: '207014805905725000000',
},
],
next_page_params: {
value: '50',
items_count: 50,
},
};
...@@ -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;
}
import type { AddressParam } from './addressParams';
export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155'; export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155';
export interface TokenInfo { export interface TokenInfo {
...@@ -17,3 +19,18 @@ export interface TokenCounters { ...@@ -17,3 +19,18 @@ export interface TokenCounters {
} }
export type TokenInfoGeneric<Type extends TokenType> = Omit<TokenInfo, 'type'> & { type: Type }; export type TokenInfoGeneric<Type extends TokenType> = Omit<TokenInfo, 'type'> & { type: Type };
export interface TokenHolders {
items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination;
}
export type TokenHolder = {
address: AddressParam;
value: string;
}
export type TokenHoldersPagination = {
items_count: number;
value: 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 && (
......
...@@ -2,7 +2,6 @@ import { Text, Show, Hide } from '@chakra-ui/react'; ...@@ -2,7 +2,6 @@ import { Text, Show, Hide } from '@chakra-ui/react';
import castArray from 'lodash/castArray'; import castArray from 'lodash/castArray';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
...@@ -21,12 +20,9 @@ import Pagination from 'ui/shared/Pagination'; ...@@ -21,12 +20,9 @@ import Pagination from 'ui/shared/Pagination';
import AddressTxsFilter from './AddressTxsFilter'; import AddressTxsFilter from './AddressTxsFilter';
import AddressIntTxsList from './internals/AddressIntTxsList'; import AddressIntTxsList from './internals/AddressIntTxsList';
const SCROLL_ELEM = 'address-internas-txs';
const SCROLL_OFFSET = -100;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues); const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressInternalTxs = () => { const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter(); const router = useRouter();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter)); const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
...@@ -38,7 +34,7 @@ const AddressInternalTxs = () => { ...@@ -38,7 +34,7 @@ const AddressInternalTxs = () => {
resourceName: 'address_internal_txs', resourceName: 'address_internal_txs',
pathParams: { id: queryIdStr }, pathParams: { id: queryIdStr },
filters: { filter: filterValue }, filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET }, scrollRef,
}); });
const handleFilterChange = React.useCallback((val: string | Array<string>) => { const handleFilterChange = React.useCallback((val: string | Array<string>) => {
...@@ -83,7 +79,7 @@ const AddressInternalTxs = () => { ...@@ -83,7 +79,7 @@ const AddressInternalTxs = () => {
} }
return ( return (
<Element name={ SCROLL_ELEM }> <>
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<AddressTxsFilter <AddressTxsFilter
defaultFilter={ filterValue } defaultFilter={ filterValue }
...@@ -93,7 +89,7 @@ const AddressInternalTxs = () => { ...@@ -93,7 +89,7 @@ const AddressInternalTxs = () => {
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> } { isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar> </ActionBar>
{ content } { content }
</Element> </>
); );
}; };
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { Element } from 'react-scroll';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
...@@ -10,19 +9,14 @@ import LogItem from 'ui/shared/logs/LogItem'; ...@@ -10,19 +9,14 @@ import LogItem from 'ui/shared/logs/LogItem';
import LogSkeleton from 'ui/shared/logs/LogSkeleton'; import LogSkeleton from 'ui/shared/logs/LogSkeleton';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
const SCROLL_PARAMS = { const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
elem: 'address-logs',
offset: -100,
};
const AddressLogs = () => {
const router = useRouter(); const router = useRouter();
const addressHash = String(router.query?.id); const addressHash = String(router.query?.id);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_logs', resourceName: 'address_logs',
pathParams: { id: addressHash }, pathParams: { id: addressHash },
scroll: SCROLL_PARAMS, scrollRef,
}); });
if (isError) { if (isError) {
...@@ -50,10 +44,10 @@ const AddressLogs = () => { ...@@ -50,10 +44,10 @@ const AddressLogs = () => {
} }
return ( return (
<Element name={ SCROLL_PARAMS.elem }> <>
{ bar } { bar }
{ data.items.map((item, index) => <LogItem key={ index } { ...item } type="address"/>) } { data.items.map((item, index) => <LogItem key={ index } { ...item } type="address"/>) }
</Element> </>
); );
}; };
......
...@@ -3,7 +3,7 @@ import React from 'react'; ...@@ -3,7 +3,7 @@ import React from 'react';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
const AddressTokenTransfers = () => { const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter(); const router = useRouter();
const hash = router.query.id; const hash = router.query.id;
...@@ -13,6 +13,7 @@ const AddressTokenTransfers = () => { ...@@ -13,6 +13,7 @@ const AddressTokenTransfers = () => {
pathParams={{ id: hash?.toString() }} pathParams={{ id: hash?.toString() }}
baseAddress={ typeof hash === 'string' ? hash : undefined } baseAddress={ typeof hash === 'string' ? hash : undefined }
enableTimeIncrement enableTimeIncrement
scrollRef={ scrollRef }
/> />
); );
}; };
......
import castArray from 'lodash/castArray'; import castArray from 'lodash/castArray';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
...@@ -17,10 +16,7 @@ import AddressTxsFilter from './AddressTxsFilter'; ...@@ -17,10 +16,7 @@ import AddressTxsFilter from './AddressTxsFilter';
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues); const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const SCROLL_ELEM = 'address-txs'; const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const SCROLL_OFFSET = -100;
const AddressTxs = () => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -31,7 +27,7 @@ const AddressTxs = () => { ...@@ -31,7 +27,7 @@ const AddressTxs = () => {
resourceName: 'address_txs', resourceName: 'address_txs',
pathParams: { id: castArray(router.query.id)[0] }, pathParams: { id: castArray(router.query.id)[0] },
filters: { filter: filterValue }, filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET }, scrollRef,
}); });
const handleFilterChange = React.useCallback((val: string | Array<string>) => { const handleFilterChange = React.useCallback((val: string | Array<string>) => {
...@@ -50,7 +46,7 @@ const AddressTxs = () => { ...@@ -50,7 +46,7 @@ const AddressTxs = () => {
); );
return ( return (
<Element name={ SCROLL_ELEM }> <>
{ !isMobile && ( { !isMobile && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
{ filter } { filter }
...@@ -64,7 +60,7 @@ const AddressTxs = () => { ...@@ -64,7 +60,7 @@ const AddressTxs = () => {
currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined } currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined }
enableTimeIncrement enableTimeIncrement
/> />
</Element> </>
); );
}; };
......
...@@ -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"
......
import { Flex, Skeleton, Tag } from '@chakra-ui/react'; import { Flex, Skeleton, Tag, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -23,18 +23,11 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -23,18 +23,11 @@ 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: <ContractRead/> },
{ id: 'read_proxy', title: 'Read proxy', component: <ContractRead isProxy/> },
{ id: 'write_contract', title: 'Write contract', component: <ContractWrite/> },
{ id: 'write_proxy', title: 'Write proxy', component: <ContractWrite isProxy/> },
];
const AddressPageContent = () => { const AddressPageContent = () => {
const router = useRouter(); const router = useRouter();
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() }, pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) }, queryOptions: { enabled: Boolean(router.query.id) },
...@@ -46,27 +39,54 @@ const AddressPageContent = () => { ...@@ -46,27 +39,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: <ContractRead/> } :
undefined,
addressQuery.data?.has_methods_read_proxy ?
{ id: 'read_proxy', title: 'Read proxy', component: <ContractRead isProxy/> } :
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: <ContractWrite/> } :
undefined,
addressQuery.data?.has_methods_write_proxy ?
{ id: 'write_proxy', title: 'Write proxy', component: <ContractWrite isProxy/> } :
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/> }, { id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> }, addressQuery.data?.has_token_transfers ?
{ id: 'tokens', title: 'Tokens', component: null }, { id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } :
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs/> }, undefined,
addressQuery.data?.has_tokens ? { id: 'tokens', title: 'Tokens', component: null } : undefined,
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> }, { 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/> } : 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 +101,9 @@ const AddressPageContent = () => { ...@@ -81,7 +101,9 @@ const AddressPageContent = () => {
additionals={ tagsNode } additionals={ tagsNode }
/> />
) } ) }
<AddressDetails addressQuery={ addressQuery }/> <AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> } { addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> }
</Page> </Page>
); );
......
...@@ -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 ' +
......
import { Skeleton } from '@chakra-ui/react'; import { Skeleton, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { Element } from 'react-scroll';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenContractInfo from 'ui/token/TokenContractInfo'; import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails'; import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
export type TokenTabs = 'token_transfers' | 'holders' export type TokenTabs = 'token_transfers' | 'holders'
const TokenPageContent = () => { const TokenPageContent = () => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile();
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenQuery = useApiQuery('token', { const tokenQuery = useApiQuery('token', {
pathParams: { hash: router.query.hash?.toString() }, pathParams: { hash: router.query.hash?.toString() },
queryOptions: { enabled: Boolean(router.query.hash) }, queryOptions: { enabled: Boolean(router.query.hash) },
}); });
// const transfersQuery = useQueryWithPages({
// resourceName: 'token_transfers',
// pathParams: { hash: router.query.hash?.toString() },
// options: {
// enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data),
// },
// });
const holdersQuery = useQueryWithPages({
resourceName: 'token_holders',
pathParams: { hash: router.query.hash?.toString() },
scrollRef,
options: {
enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data),
},
});
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: null }, { id: 'token_transfers', title: 'Token transfers', component: null },
{ id: 'holders', title: 'Holders', component: null }, { id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
]; ];
let hasPagination;
let pagination;
// if (router.query.tab === 'token_transfers') {
// hasPagination = transfersQuery.isPaginationVisible;
// pagination = transfersQuery.pagination;
// }
if (router.query.tab === 'holders') {
hasPagination = holdersQuery.isPaginationVisible;
pagination = holdersQuery.pagination;
}
return ( return (
<Page> <Page>
{ tokenQuery.isLoading ? { tokenQuery.isLoading ?
...@@ -34,7 +69,16 @@ const TokenPageContent = () => { ...@@ -34,7 +69,16 @@ const TokenPageContent = () => {
<PageTitle text={ `${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token` }/> } <PageTitle text={ `${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token` }/> }
<TokenContractInfo tokenQuery={ tokenQuery }/> <TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/> <TokenDetails tokenQuery={ tokenQuery }/>
<Element name="token-tabs"><RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/></Element>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
stickyEnabled={ !isMobile }
/>
</Page> </Page>
); );
}; };
......
...@@ -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 }
......
import { Hide, Show, Text } from '@chakra-ui/react'; import { Hide, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
...@@ -28,9 +27,6 @@ import { TOKEN_TYPE } from './helpers'; ...@@ -28,9 +27,6 @@ import { TOKEN_TYPE } from './helpers';
const TOKEN_TYPES = TOKEN_TYPE.map(i => i.id); const TOKEN_TYPES = TOKEN_TYPE.map(i => i.id);
const SCROLL_ELEM = 'token-transfers';
const SCROLL_OFFSET = -100;
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES); const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues); const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
...@@ -43,6 +39,7 @@ interface Props<Resource extends 'tx_token_transfers' | 'address_token_transfers ...@@ -43,6 +39,7 @@ interface Props<Resource extends 'tx_token_transfers' | 'address_token_transfers
txHash?: string; txHash?: string;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
pathParams?: UseApiQueryParams<Resource>['pathParams']; pathParams?: UseApiQueryParams<Resource>['pathParams'];
scrollRef?: React.RefObject<HTMLDivElement>;
} }
type State = { type State = {
...@@ -58,6 +55,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr ...@@ -58,6 +55,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
showTxInfo = true, showTxInfo = true,
enableTimeIncrement, enableTimeIncrement,
pathParams, pathParams,
scrollRef,
}: Props<Resource>) => { }: Props<Resource>) => {
const router = useRouter(); const router = useRouter();
const [ filters, setFilters ] = React.useState<State>( const [ filters, setFilters ] = React.useState<State>(
...@@ -69,7 +67,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr ...@@ -69,7 +67,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
pathParams, pathParams,
options: { enabled: !isDisabled }, options: { enabled: !isDisabled },
filters: filters as PaginationFilters<Resource>, filters: filters as PaginationFilters<Resource>,
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET }, scrollRef,
}); });
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => { const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
...@@ -129,7 +127,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr ...@@ -129,7 +127,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
})(); })();
return ( return (
<Element name={ SCROLL_ELEM }> <>
{ !isActionBarHidden && ( { !isActionBarHidden && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<TokenTransferFilter <TokenTransferFilter
...@@ -144,7 +142,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr ...@@ -144,7 +142,7 @@ const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_tr
</ActionBar> </ActionBar>
) } ) }
{ content } { content }
</Element> </>
); );
}; };
......
...@@ -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)',
}, },
], ],
}, },
......
import { Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { TokenHolders, TokenInfo } from 'types/api/tokenInfo';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TokenHoldersList from './TokenHoldersList';
import TokenHoldersTable from './TokenHoldersTable';
type Props = {
tokenQuery: UseQueryResult<TokenInfo>;
holdersQuery: UseQueryResult<TokenHolders> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
const isMobile = useIsMobile();
if (holdersQuery.isError || tokenQuery.isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && holdersQuery.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...holdersQuery.pagination }/>
</ActionBar>
);
if (holdersQuery.isLoading || tokenQuery.isLoading) {
return (
<>
{ bar }
{ isMobile && <SkeletonList/> }
{ !isMobile && (
<SkeletonTable columns={ [ '100%', '300px', '175px' ] }/>
) }
</>
);
}
const items = holdersQuery.data.items;
if (!items?.length) {
return <Text as="span">There are no holders for this token.</Text>;
}
return (
<>
{ bar }
{ !isMobile && <TokenHoldersTable data={ items } token={ tokenQuery.data }/> }
{ isMobile && <TokenHoldersList data={ items } token={ tokenQuery.data }/> }
</>
);
};
export default TokenHoldersContent;
import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import { tokenHolders } from 'mocks/tokens/tokenHolders';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import TestApp from 'playwright/TestApp';
import TokenHoldersList from './TokenHoldersList';
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ mount }) => {
const component = await mount(
<TestApp>
<TokenHoldersList data={ tokenHolders.items } token={ tokenInfo }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import TokenHoldersListItem from './TokenHoldersListItem';
interface Props {
data: Array<TokenHolder>;
token: TokenInfo;
}
const TokenHoldersList = ({ data, token }: Props) => {
return (
<Box>
{ data.map((item) => (
<TokenHoldersListItem
key={ item.address.hash }
token={ token }
holder={ item }
/>
)) }
</Box>
);
};
export default TokenHoldersList;
import { Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
holder: TokenHolder;
token: TokenInfo;
}
const TokenHoldersListItem = ({ holder, token }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).dp(6).toFormat();
return (
<ListItemMobile rowGap={ 3 }>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/>
<AddressLink ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
</Address>
<Flex justifyContent="space-between" alignItems="center" width="100%">
{ quantity }
{ token.total_supply && (
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green"
ml={ 6 }
/>
) }
</Flex>
</ListItemMobile>
);
};
export default TokenHoldersListItem;
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { tokenHolders } from 'mocks/tokens/tokenHolders';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import TestApp from 'playwright/TestApp';
import TokenHoldersTable from './TokenHoldersTable';
test('base view', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h="128px"/>
<TokenHoldersTable data={ tokenHolders.items } token={ tokenInfo }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokenHoldersTableItem from 'ui/token/TokenHolders/TokenHoldersTableItem';
interface Props {
data: Array<TokenHolder>;
token: TokenInfo;
}
const TokenHoldersTable = ({ data, token }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th>Holder</Th>
<Th isNumeric width="300px">Quantity</Th>
{ token.total_supply && <Th isNumeric width="175px">Percentage</Th> }
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TokenHoldersTableItem key={ item.address.hash } holder={ item } token={ token }/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(TokenHoldersTable);
import { Tr, Td } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Utilization from 'ui/shared/Utilization/Utilization';
type Props = {
holder: TokenHolder;
token: TokenInfo;
}
const TokenTransferTableItem = ({ holder, token }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).toFormat();
return (
<Tr>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/>
<AddressLink ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
</Address>
</Td>
<Td isNumeric>
{ quantity }
</Td>
{ token.total_supply && (
<Td isNumeric>
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green"
display="inline-flex"
/>
</Td>
) }
</Tr>
);
};
export default React.memo(TokenTransferTableItem);
...@@ -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