Commit f6ac190a authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into gradual-increment

parents d178bbb0 c7a3c3f9
......@@ -195,7 +195,7 @@ module.exports = {
groups: [
'module',
'/types/',
[ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ],
[ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^mocks/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ],
[ 'parent', 'sibling', 'index' ],
],
alphabetize: { order: 'asc', ignoreCase: true },
......
......@@ -67,6 +67,7 @@ jobs:
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack
globalEnv: review
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
blockscoutIngressHost: blockscout
frontendIngressHost: blockscout
......
......@@ -47,3 +47,5 @@ yarn-error.log*
/test-results/
/playwright-report/
/playwright/.cache/
/playwright/.browser/
/playwright/envs.js
......@@ -102,15 +102,56 @@
"instanceLimit": 1
}
},
// PW TESTS
{
"type": "npm",
"script": "test:pw:docker",
"type": "shell",
"command": "${input:pwDebugFlag} yarn test:pw:local ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: local for current file",
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
"id": "beaker"
},
"runOptions": {
"instanceLimit": 1
},
},
{
"type": "shell",
"command": "yarn test:pw:docker ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright",
"label": "test: playwright: docker for current file",
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
"id": "beaker"
},
"runOptions": {
"instanceLimit": 1
},
},
{
"type": "shell",
"command": "yarn test:pw:docker ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: docker for all files",
"detail": "run visual components tests",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
......@@ -120,6 +161,8 @@
"instanceLimit": 1
}
},
// JEST TESTS
{
"type": "npm",
"script": "test:jest",
......@@ -129,6 +172,7 @@
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
......@@ -148,6 +192,7 @@
"reveal": "always",
"panel": "new",
"close": true,
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
......@@ -157,6 +202,26 @@
"instanceLimit": 1
}
},
{
"type": "shell",
"command": "yarn test:jest ${relativeFileDirname}/${fileBasename} --watch",
"problemMatcher": [],
"label": "test: jest: watch curent file",
"detail": "run jest tests in watch mode for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
"id": "beaker"
},
"runOptions": {
"instanceLimit": 1
},
},
{
"type": "npm",
"script": "build:docker",
......@@ -168,6 +233,7 @@
"panel": "new",
"close": true,
"revealProblems": "onProblem",
"focus": true,
},
"icon": {
"color": "terminal.ansiRed",
......@@ -217,5 +283,29 @@
"instanceLimit": 1
}
},
]
],
"inputs": [
{
"type": "pickString",
"id": "pwDebugFlag",
"description": "What debug flag you want to use?",
"options": [
"",
"PWDEBUG=1",
"DEBUG=pw:browser,pw:api",
"DEBUG=*",
],
"default": ""
},
{
"type": "pickString",
"id": "pwArgs",
"description": "What args you want to pass?",
"options": [
"",
"--update-snapshots",
],
"default": ""
},
],
}
\ No newline at end of file
......@@ -66,7 +66,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| 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_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` |
| NEXT_PUBLIC_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'external': true, 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` *(optional)* | Verification type in the network | `mining` |
......@@ -125,6 +125,7 @@ The app instance could be customized by passing following variables to NodeJS en
| Property | Type | Description | Example value
| --- | --- | --- | --- |
| 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` |
| 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'` |
| shortDescription | `string` | Displayed only in the app list. | `'Awesome app'` |
......
......@@ -15,7 +15,8 @@ NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
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://revoke.cash/assets/images/revoke.svg', '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': 'Hop', 'id': 'hop-exchange', 'title': 'Hop', 'logo': 'https://goerli.hop.exchange/static/media/hop-logo-black.36655970.svg', 'categories': ['tools'], 'shortDescription': 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', 'site': 'https://help.hop.exchange/hc/en-us/articles/4405172445197-What-is-Hop-Protocol-', 'description': 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', 'url': 'https://goerli.hop.exchange'}, {'author': 'Aave', 'id': 'aave', 'title': 'Aave', 'logo': 'https://staging.aave.com/aaveLogo.svg', '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/'}]
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'}]
# api config
NEXT_PUBLIC_API_BASE_PATH=/eth/goerli
NEXT_PUBLIC_API_HOST=eth-goerli.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
......@@ -18,7 +18,7 @@ NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
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://revoke.cash/assets/images/revoke.svg', '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': 'Hop', 'id': 'hop-exchange', 'title': 'Hop', 'logo': 'https://goerli.hop.exchange/static/media/hop-logo-black.36655970.svg', 'categories': ['tools'], 'shortDescription': 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', 'site': 'https://help.hop.exchange/hc/en-us/articles/4405172445197-What-is-Hop-Protocol-', 'description': 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', 'url': 'https://goerli.hop.exchange'}, {'author': 'Aave', 'id': 'aave', 'title': 'Aave', 'logo': 'https://staging.aave.com/aaveLogo.svg', '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/'}]
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'}]
# api config
NEXT_PUBLIC_API_BASE_PATH=/poa/core
# app config
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3100
NEXT_PUBLIC_APP_INSTANCE=pw
NEXT_PUBLIC_APP_ENV=testing
# ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
# api config
NEXT_PUBLIC_API_HOST=blockscout.com
const PATHS = require('../../lib/link/paths');
const PATHS = require('../../lib/link/paths.json');
const oldUrls = [
{
......
......@@ -153,19 +153,21 @@ postgres:
command: '["docker-entrypoint.sh", "-c"]'
args: '["max_connections=300"]'
strategy: Recreate
# strategy: Recreate
persistence: true
resources:
limits:
memory:
_default: "2Gi"
_default: "6Gi"
cpu:
_default: "2"
_default: "4"
requests:
memory:
_default: "2Gi"
_default: "6Gi"
cpu:
_default: "2"
_default: "4"
environment:
POSTGRES_USER:
......@@ -498,7 +500,7 @@ frontend:
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
_default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST:
_default: "[{'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://revoke.cash/assets/images/revoke.svg', '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': 'Hop', 'id': 'hop-exchange', 'title': 'Hop', 'logo': 'https://goerli.hop.exchange/static/media/hop-logo-black.36655970.svg', 'categories': ['tools'], 'shortDescription': 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', 'site': 'https://help.hop.exchange/hc/en-us/articles/4405172445197-What-is-Hop-Protocol-', 'description': 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', 'url': 'https://goerli.hop.exchange'}, {'author': 'Aave', 'id': 'aave', 'title': 'Aave', 'logo': 'https://staging.aave.com/aaveLogo.svg', '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/'}]"
_default: "[{'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'}]"
# api config
NEXT_PUBLIC_API_BASE_PATH:
_default: /
......
......@@ -65,7 +65,7 @@ geth:
frontend:
environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,type:str]
_default: ENC[AES256_GCM,data:yShwsa6ajoFXg/6QSgEARkZRVVrwrdsR69NSmyvBH2O5EUQ0OvsWpW64,iv:K/HT6C9pYCK63LNyF3HERFc79vDS4cB0H4pINIlNhh0=,tag:X0HqeAP01diTvDOwoEP6lw==,type:str]
NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
......@@ -78,8 +78,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-11-16T11:55:15Z"
mac: ENC[AES256_GCM,data:wd7HZEGH1fJO1ufaUeBtcjUaHS21rcOoiGaQPNptEyEPj6q/60rJ1YQmGqkgi3DNVnGly5FQyYjMIIY3/YqXqTZI6MBhJp4RmpCELbqqzQbAFvbomYURmqG/umeT2+kMrSIF/PXrt4d51e1cod2+H4OY9V09VerH9L07D0nTd48=,iv:dDeTSqvmwps4oQKRVgDqmMf/uxf7Egb+jufwTKtm6F4=,tag:CqOCA4XW7d3C5D4dflIFug==,type:str]
lastmodified: "2022-11-28T16:58:46Z"
mac: ENC[AES256_GCM,data:QJvVfWWWVDk5mI66T9J8EnEyVwmJoGEsWO9Pr8vK7jyC3rhAYD2WdKYfpkbwwMKrJzcMBe7UeaOeEY6aApuMNdobeEjsJAvstXCOBzMe5H9XtAFiAY+oxf8r4ELNvQP/gIBZSja+ehSbXBcaP4DkLn4FboaBhkoE8A37W2R6/QA=,iv:FnIC6iGLEZNwRSrbF81vF6eQuyq0yQHNPRTPrx3FB+8=,tag:LRnZCwYkCh4o8lDUcG2m9A==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -3,7 +3,7 @@ global:
# enable Blockscout deploy
blockscout:
app: blockscout
enabled: true
enabled: false
image:
_default: blockscout/blockscout:latest
replicas:
......@@ -117,26 +117,26 @@ blockscout:
_default: 'true'
postgres:
enabled: true
enabled: false
image: postgres:13.8
port: 5432
command: '["docker-entrypoint.sh", "-c"]'
args: '["max_connections=300"]'
strategy: Recreate
# persistence: true
resources:
limits:
memory:
_default: "1Gi"
_default: "4Gi"
cpu:
_default: "1"
_default: "3"
requests:
memory:
_default: "1Gi"
_default: "4Gi"
cpu:
_default: "1"
_default: "3"
environment:
POSTGRES_USER:
......@@ -145,7 +145,7 @@ postgres:
_default: 'trust'
# enable geth deploy
geth:
enabled: true
enabled: false
image:
_default: ethereum/client-go:stable
replicas:
......@@ -200,7 +200,7 @@ geth:
enabled: true
# enable Smart-contract-verifier deploy
scVerifier:
enabled: true
enabled: false
image:
_default: ghcr.io/blockscout/smart-contract-verifier:latest
replicas:
......@@ -347,19 +347,19 @@ frontend:
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME:
_default: Sokol
_default: Ethereum
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA
_default: Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa
_default: ethereum
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa_core
_default: goerli
NEXT_PUBLIC_NETWORK_ID:
_default: 77
_default: 5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol
_default: Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA
_default: ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
......@@ -370,15 +370,20 @@ frontend:
_default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]"
NEXT_PUBLIC_API_HOST:
_default: blockscout.com
review: blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
_default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST:
_default: "[{'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://revoke.cash/assets/images/revoke.svg', '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': 'Hop', 'id': 'hop-exchange', 'title': 'Hop', 'logo': 'https://goerli.hop.exchange/static/media/hop-logo-black.36655970.svg', 'categories': ['tools'], 'shortDescription': 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', 'site': 'https://help.hop.exchange/hc/en-us/articles/4405172445197-What-is-Hop-Protocol-', 'description': 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.', 'url': 'https://goerli.hop.exchange'}, {'author': 'Aave', 'id': 'aave', 'title': 'Aave', 'logo': 'https://staging.aave.com/aaveLogo.svg', '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/'}]"
_default: "[{'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_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout.com/auth/logout
review: blockscout-main.test.aws-k8s.blockscout.com/auth/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cup']"
NEXT_PUBLIC_APP_HOST:
_default: blockscout.com
review: blockscout-main.test.aws-k8s.blockscout.com
......@@ -95,7 +95,9 @@ const config: JestConfigWithTsJest = {
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
modulePathIgnorePatterns: [
'node_modules_linux',
],
// Activates notifications for test results
// notify: false,
......
......@@ -13,7 +13,7 @@ interface Props extends ChakraProviderProps {
export function Chakra({ cookies, theme, children }: Props) {
const colorModeManager =
typeof cookies === 'string' ?
cookieStorageManagerSSR(cookies) :
cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) :
localStorageManager;
return (
......
export default function getPlaceholderWithError(text: string, errorText?: string) {
return `${ text }${ errorText ? ' - ' + errorText : '' }`;
}
......@@ -5,10 +5,8 @@ import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll';
import type { BlockFilters } from 'types/api/block';
import { PAGINATION_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys } from 'types/api/pagination';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination';
import useFetch from 'lib/hooks/useFetch';
......@@ -16,7 +14,7 @@ interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string;
queryName: QueryName;
queryIds?: Array<string>;
filters?: TTxsFilters | BlockFilters;
filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
}
......
const paths = {
network_index: `/`,
watchlist: `/account/watchlist`,
private_tags: `/account/tag_address`,
public_tags: `/account/public_tags_request`,
api_keys: `/account/api_key`,
custom_abi: `/account/custom_abi`,
profile: `/auth/profile`,
txs: `/txs`,
tx: `/tx/:id`,
blocks: `/blocks`,
block: `/block/:id`,
tokens: `/tokens`,
token_index: `/token/:hash`,
token_instance_item: `/token/:hash/instance/:id`,
address_index: `/address/:id`,
address_contract_verification: `/address/:id/contract_verifications/new`,
apps: `/apps`,
app_index: `/apps/:id`,
search_results: `/search-results`,
other: `/search-results`,
auth: `/auth/auth0`,
};
module.exports = paths;
{
"network_index": "/",
"watchlist": "/account/watchlist",
"private_tags": "/account/tag_address",
"public_tags": "/account/public_tags_request",
"api_keys": "/account/api_key",
"custom_abi": "/account/custom_abi",
"profile": "/auth/profile",
"txs": "/txs",
"tx": "/tx/:id",
"blocks": "/blocks",
"block": "/block/:id",
"tokens": "/tokens",
"token_index": "/token/:hash",
"token_instance_item": "/token/:hash/instance/:id",
"address_index": "/address/:id",
"address_contract_verification": "/address/:id/contract_verifications/new",
"apps": "/apps",
"app_index": "/apps/:id",
"search_results": "/search-results",
"other": "/search-results",
"auth": "/auth/auth0",
"stats": "/stats"
}
import appConfig from 'configs/app/config';
import PATHS from './paths.js';
import PATHS from './paths.json';
export interface Route {
pattern: string;
......@@ -85,6 +85,10 @@ export const ROUTES = {
pattern: PATHS.app_index,
},
stats: {
pattern: PATHS.stats,
},
// SEARCH
search_results: {
pattern: PATHS.search_results,
......
......@@ -6,7 +6,7 @@ export const SocketContext = React.createContext<Socket | null>(null);
interface SocketProviderProps {
children: React.ReactNode;
url: string;
url?: string;
options?: Partial<SocketConnectOption>;
}
......@@ -14,6 +14,10 @@ export function SocketProvider({ children, options, url }: SocketProviderProps)
const [ socket, setSocket ] = useState<Socket | null>(null);
useEffect(() => {
if (!url) {
return;
}
const socketInstance = new Socket(url, options);
socketInstance.connect();
setSocket(socketInstance);
......
import type { AddressParam } from 'types/api/addressParams';
export const withName: AddressParam = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'ArianeeStore',
private_tags: [],
watchlist_names: [],
public_tags: [],
};
export const withoutName: AddressParam = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: null,
name: null,
private_tags: [],
watchlist_names: [],
public_tags: [],
};
import type { AddressTag, WatchlistName } from 'types/api/addressParams';
export const privateTag: AddressTag = {
label: 'my-private-tag',
display_name: 'my private tag',
address_hash: '0x',
};
export const publicTag: AddressTag = {
label: 'some-public-tag',
display_name: 'some public tag',
address_hash: '0x',
};
export const watchlistName: WatchlistName = {
label: 'watchlist-name',
display_name: 'watchlist name',
};
import type { Block, BlocksResponse } from 'types/api/block';
export const base: Block = {
base_fee_per_gas: '10000000000',
burnt_fees: '5449200000000000',
burnt_fees_percentage: 20.292245650793845,
difficulty: '340282366920938463463374607431768211454',
extra_data: 'TODO',
gas_limit: '12500000',
gas_target_percentage: -91.28128,
gas_used: '544920',
gas_used_percentage: 4.35936,
hash: '0xccc75136de485434d578b73df66537c06b34c3c9b12d085daf95890c914fc2bc',
height: 30146364,
miner: {
hash: '0xdAd49e6CbDE849353ab27DeC6319E687BFc91A41',
implementation_name: null,
is_contract: false,
is_verified: null,
name: 'Alex Emelyanov',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
nonce: '0x0000000000000000',
parent_hash: '0x44125f0eb36a9d942e0c23bb4e8117f7ba86a9537a69b59c0025986ed2b7500f',
priority_fee: '23211757500000000',
rewards: [
{
reward: '500000000000000000',
type: 'POA Mania Reward',
},
{
reward: '1026853607500000000',
type: 'Validator Reward',
},
{
reward: '500000000000000000',
type: 'Emission Reward',
},
],
size: 2448,
state_root: 'TODO',
timestamp: '2022-11-11T11:59:35Z',
total_difficulty: '10258276095980170141167591583995189665817672619',
tx_count: 5,
tx_fees: '26853607500000000',
type: 'block',
uncles_hashes: [],
};
export const genesis = {
base_fee_per_gas: null,
burnt_fees: null,
burnt_fees_percentage: null,
difficulty: '131072',
extra_data: 'TODO',
gas_limit: '6700000',
gas_target_percentage: -100,
gas_used: '0',
gas_used_percentage: 0,
hash: '0x39f02c003dde5b073b3f6e1700fc0b84b4877f6839bb23edadd3d2d82a488634',
height: 0,
miner: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
nonce: '0x0000000000000000',
parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000',
priority_fee: null,
rewards: [],
size: 533,
state_root: 'TODO',
timestamp: '2017-12-16T00:13:24.000000Z',
total_difficulty: '131072',
tx_count: 0,
tx_fees: '0',
type: 'block',
uncles_hashes: [],
};
export const base2: Block = {
...base,
height: base.height - 1,
size: 592,
miner: {
hash: '0xDfE10D55d9248B2ED66f1647df0b0A46dEb25165',
implementation_name: null,
is_contract: false,
is_verified: null,
name: 'Kiryl Ihnatsyeu',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
timestamp: '2022-11-11T11:46:05Z',
tx_count: 253,
gas_target_percentage: 23.6433,
gas_used: '6333342',
gas_used_percentage: 87.859504,
burnt_fees: '232438000000000000',
burnt_fees_percentage: 65.3333333333334,
rewards: [
{
reward: '500000000000000000',
type: 'Chore Reward',
},
{
reward: '1017432850000000000',
type: 'Miner Reward',
},
{
reward: '500000000000000000',
type: 'Emission Reward',
},
],
};
export const baseListResponse: BlocksResponse = {
items: [
base,
base2,
],
next_page_params: null,
};
import type { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransfer';
export const erc20: TokenTransfer = {
from: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeStore',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
to: {
hash: '0x7d20a8D54F955b4483A66aB335635ab66e151c51',
implementation_name: null,
is_contract: true,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
token: {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
decimals: '18',
exchange_rate: null,
holders: '46554',
name: 'ARIANEE',
symbol: 'ARIA',
type: 'ERC-20',
},
total: {
decimals: '18',
value: '31567373703130350',
},
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
type: 'token_transfer',
};
export const erc721: TokenTransfer = {
from: {
hash: '0x621C2a125ec4A6D8A7C7A655A18a2868d35eb43C',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
to: {
hash: '0x47eE48AEBc4ab9Ed908b805b8c8dAAa71B31Db1A',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
token: {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
decimals: null,
exchange_rate: null,
holders: '63090',
name: 'Arianee Smart-Asset',
symbol: 'AriaSA',
type: 'ERC-721',
},
total: {
token_id: '875879856',
},
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
type: 'token_transfer',
};
export const erc1155: TokenTransfer = {
from: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
to: {
hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
token: {
address: '0xF56b7693E4212C584de4a83117f805B8E89224CB',
decimals: null,
exchange_rate: null,
holders: '1',
name: null,
symbol: null,
type: 'ERC-1155',
},
total: {
token_id: '123',
value: '42',
decimals: null,
},
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
type: 'token_minting',
};
export const erc1155multiple: TokenTransfer = {
...erc1155,
token: {
...erc1155.token,
name: 'OLYMPIC',
},
total: [
{ token_id: '456', value: '42', decimals: null },
{ token_id: '12345678', value: '142', decimals: null },
{ token_id: '1000006457499', value: '11', decimals: null },
],
};
export const mixTokens: TokenTransferResponse = {
items: [
erc20,
erc721,
erc1155,
erc1155multiple,
],
next_page_params: null,
};
import type { DecodedInput } from 'types/api/decodedInput';
export const withoutIndexedFields: DecodedInput = {
method_call: 'CreditSpended(uint256 _type, uint256 _quantity)',
method_id: '58cdf94a',
parameters: [
{
name: '_type',
type: 'uint256',
value: '3',
},
{
name: '_quantity',
type: 'uint256',
value: '1',
},
],
};
export const withIndexedFields: DecodedInput = {
method_call: 'Transfer(address indexed from, address indexed to, uint256 value)',
method_id: 'ddf252ad',
parameters: [
{
indexed: true,
name: 'from',
type: 'address',
value: '0xd789a607ceac2f0e14867de4eb15b15c9ffb5859',
},
{
indexed: true,
name: 'to',
type: 'address',
value: '0x7d20a8d54f955b4483a66ab335635ab66e151c51',
},
{
indexed: false,
name: 'value',
type: 'uint256',
value: '31567373703130350',
},
],
};
import type { InternalTransaction, InternalTransactionsResponse } from 'types/api/internalTransaction';
export const base: InternalTransaction = {
block: 29611822,
created_contract: null,
error: null,
from: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeStore',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
gas_limit: '757586',
index: 1,
success: true,
timestamp: '2022-10-10T14:43:05.000000Z',
to: {
hash: '0x502a9C8af2441a1E276909405119FaE21F3dC421',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeCreditHistory',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff61',
type: 'call',
value: '42000000000000000000',
};
export const typeStaticCall: InternalTransaction = {
...base,
type: 'staticcall',
to: {
...base.to,
name: null,
},
gas_limit: '63424243',
};
export const withContractCreated: InternalTransaction = {
...base,
type: 'delegatecall',
to: null,
from: {
...base.from,
name: null,
},
created_contract: {
hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'Shavuha token',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
value: '1420000000000000000',
gas_limit: '5433',
};
export const baseResponse: InternalTransactionsResponse = {
items: [
base,
typeStaticCall,
withContractCreated,
],
next_page_params: null,
};
/* eslint-disable max-len */
import type { Transaction } from 'types/api/transaction';
import { publicTag, privateTag, watchlistName } from 'mocks/address/tag';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import * as decodedInputDataMock from 'mocks/txs/decodedInputData';
export const base: Transaction = {
base_fee_per_gas: '10000000000',
block: 29611750,
confirmation_duration: [
0,
6364,
],
confirmations: 508299,
created_contract: null,
decoded_input: decodedInputDataMock.withoutIndexedFields,
exchange_rate: '0.00254428',
fee: {
type: 'actual',
value: '7143168000000000',
},
from: {
hash: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9',
implementation_name: null,
is_contract: false,
name: null,
is_verified: null,
private_tags: [ ],
public_tags: [ publicTag ],
watchlist_names: [],
},
gas_limit: '800000',
gas_price: '48000000000',
gas_used: '148816',
hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
max_fee_per_gas: '40190625000',
max_priority_fee_per_gas: '28190625000',
method: 'updateSmartAsset',
nonce: 27831,
position: 7,
priority_fee: '1299672384375000',
raw_input: '0xfa4b78b90000000000000000000000000000000000000000000000000000000005001bcfe835d1028984e9e6e7d016b77164eacbcc6cc061e9333c0b37982b504f7ea791000000000000000000000000a79b29ad7e0196c95b87f4663ded82fbf2e3add8',
result: 'success',
revert_reason: null,
status: 'ok',
timestamp: '2022-10-10T14:34:30.000000Z',
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: false,
is_verified: true,
name: null,
private_tags: [ privateTag ],
public_tags: [],
watchlist_names: [ watchlistName ],
},
token_transfers: [],
token_transfers_overflow: false,
tx_burnt_fee: '461030000000000',
tx_tag: null,
tx_types: [
'contract_call',
'token_transfer',
],
type: 2,
value: '42000000000000000000',
};
export const withContractCreation: Transaction = {
...base,
to: null,
created_contract: {
hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'Shavuha token',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
};
export const withTokenTransfer: Transaction = {
...base,
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeStore',
private_tags: [ privateTag ],
public_tags: [],
watchlist_names: [ watchlistName ],
},
token_transfers: [
tokenTransferMock.erc20,
tokenTransferMock.erc721,
tokenTransferMock.erc1155,
tokenTransferMock.erc1155multiple,
],
};
export const withDecodedRevertReason: Transaction = {
...base,
status: 'error',
result: 'Reverted',
revert_reason: {
method_call: 'SomeCustomError(address addr, uint256 balance)',
method_id: '50289a9f',
parameters: [
{
name: 'addr',
type: 'address',
value: '0xf26594f585de4eb0ae9de865d9053fee02ac6ef1',
},
{
name: 'balance',
type: 'uint256',
value: '123',
},
],
},
};
export const withRawRevertReason: Transaction = {
...base,
status: 'error',
result: 'Reverted',
revert_reason: {
raw: '4f6e6c79206368616972706572736f6e2063616e206769766520726967687420746f20766f74652e',
},
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_verified: true,
is_contract: true,
name: 'Bad guy',
private_tags: [ ],
public_tags: [],
watchlist_names: [ ],
},
};
export const pending: Transaction = {
...base,
base_fee_per_gas: null,
block: null,
confirmation_duration: [],
confirmations: 0,
decoded_input: null,
gas_used: null,
max_fee_per_gas: null,
max_priority_fee_per_gas: null,
method: null,
position: null,
priority_fee: null,
result: 'pending',
revert_reason: null,
status: null,
timestamp: null,
tx_burnt_fee: null,
tx_tag: null,
type: null,
value: '0',
};
......@@ -21,7 +21,8 @@
"lint:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json",
"prepare": "husky install",
"format-svg": "./node_modules/.bin/svgo -r ./icons",
"test:pw": "playwright test -c playwright-ct.config.ts",
"test:pw": "./playwright/make-envs-script.sh && playwright test -c playwright-ct.config.ts",
"test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw",
"test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.28.0-focal ./playwright/run-tests.sh",
"test:jest": "jest",
"test:jest:watch": "jest --watch"
......@@ -67,6 +68,7 @@
"@types/phoenix": "^1.5.4",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.5",
"@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"dotenv-cli": "^6.0.0",
"eslint": "8.16.0",
......@@ -84,7 +86,9 @@
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typescript": "4.7.2",
"vite-tsconfig-paths": "^3.5.2"
"vite-plugin-svgr": "^2.2.2",
"vite-tsconfig-paths": "^3.5.2",
"ws": "^8.11.0"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --cache --fix"
......
......@@ -9,7 +9,7 @@ import { Chakra } from 'lib/Chakra';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme';
import AppError from 'ui/shared/AppError';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
function MyApp({ Component, pageProps }: AppProps) {
......
......@@ -10,11 +10,11 @@ class MyDocument extends Document {
<Html lang="en">
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap"
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="icon" sizes="32x32" type="image/png" href="/static/favicon-32x32.png"/>
......
......@@ -27,7 +27,7 @@ import * as cookies from 'lib/cookies';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props as ServerSidePropsCommon } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsCommon } from 'lib/next/getServerSideProps';
import AppError from 'ui/shared/AppError';
import AppError from 'ui/shared/AppError/AppError';
import Page from 'ui/shared/Page/Page';
type Props = ServerSidePropsCommon & {
......
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
// todo_tom leave only one api endpoint
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats';
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import Stats from '../ui/pages/Stats';
const StatsPage: NextPage = () => {
return (
<>
<Head><title>Ethereum Stats</title></Head>
<Stats/>
</>
);
};
export default StatsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
import { devices } from '@playwright/experimental-ct-react';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths';
/**
......@@ -10,7 +12,7 @@ const config: PlaywrightTestConfig = {
testMatch: /.*\.pw\.tsx/,
snapshotPathTemplate: '{testDir}/{testFileDir}/__screenshots__/{testFileName}_{arg}{ext}',
snapshotPathTemplate: '{testDir}/{testFileDir}/__screenshots__/{testFileName}_{projectName}_{arg}{ext}',
/* Maximum time one test can run for. */
timeout: 10 * 1000,
......@@ -42,15 +44,49 @@ const config: PlaywrightTestConfig = {
headless: true,
ctViteConfig: {
plugins: [ tsconfigPaths() ],
plugins: [
tsconfigPaths(),
react(),
svgr({
exportAsDefault: true,
}),
],
},
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: devices['Desktop Chrome'],
name: 'default',
grepInvert: /-@default/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1200, height: 750 },
},
},
{
name: 'mobile',
grep: /\+@mobile/,
use: {
...devices['iPhone 13 Pro'],
},
},
{
name: 'desktop xl',
grep: /\+@desktop-xl/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1600, height: 1000 },
},
},
{
name: 'dark color mode',
grep: /\+@dark-mode/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1200, height: 750 },
colorScheme: 'dark',
},
},
],
};
......
import { ChakraProvider } from '@chakra-ui/react';
import type { ColorMode } from '@chakra-ui/react';
import React from 'react';
import theme from 'theme';
type Props = {
children: React.ReactNode;
colorMode?: ColorMode;
}
const RenderWithChakra = ({ children, colorMode = 'light' }: Props) => {
return (
<ChakraProvider theme={{ ...theme, config: { ...theme.config, initialColorMode: colorMode } }}>
{ children }
</ChakraProvider>
);
};
export default RenderWithChakra;
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { SocketProvider } from 'lib/socket/context';
import { PORT } from 'playwright/fixtures/socketServer';
import theme from 'theme';
type Props = {
children: React.ReactNode;
withSocket?: boolean;
}
const TestApp = ({ children, withSocket }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 0,
},
},
}));
return (
<ChakraProvider theme={ theme }>
<QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
{ children }
</SocketProvider>
</QueryClientProvider>
</ChakraProvider>
);
};
export default TestApp;
import type { TestFixture } from '@playwright/test';
import type { WebSocket } from 'ws';
import { WebSocketServer } from 'ws';
import type { NewBlockSocketResponse } from 'types/api/block';
type ReturnType = () => Promise<WebSocket>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ArgsType = any;
type Channel = [string, string, string];
export interface SocketServerFixture {
createSocket: ReturnType;
}
export const PORT = 3200;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const createSocket: TestFixture<ReturnType, ArgsType> = async({ page }, use) => {
const socketServer = new WebSocketServer({ port: PORT });
const connectionPromise = new Promise<WebSocket>((resolve) => {
socketServer.on('connection', (socket: WebSocket) => {
resolve(socket);
});
});
await use(() => connectionPromise);
socketServer.close();
};
export const joinChannel = async(socket: WebSocket, channelName: string) => {
return new Promise<[string, string, string]>((resolve, reject) => {
socket.on('message', (msg) => {
try {
const payload: Array<string> = JSON.parse(msg.toString());
if (channelName === payload[2] && payload[3] === 'phx_join') {
socket.send(JSON.stringify([
payload[0],
payload[1],
payload[2],
'phx_reply',
{ response: {}, status: 'ok' },
]));
resolve([ payload[0], payload[1], payload[2] ]);
}
} catch (error) {
reject(error);
}
});
});
};
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([
...channel,
msg,
payload,
]));
}
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -7,6 +7,7 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/playwright/envs.js"></script>
<script type="module" src="/playwright/index.ts"></script>
</body>
</html>
// Import styles, initialize component theme here.
// import '../src/common.css';
import './fonts.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import MockDate from 'mockdate';
import * as router from 'next/router';
const NEXT_ROUTER_MOCK = {
query: {},
};
beforeMount(async({ hooksConfig }) => {
// Before mount, redefine useRouter to return mock value from test.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: I really want to redefine this property :)
router.useRouter = () => hooksConfig?.router || NEXT_ROUTER_MOCK;
// set current date
MockDate.set('2022-11-11T12:00:00Z');
});
export {};
#!/bin/bash
targetFile='./playwright/envs.js'
declare -a envFiles=('./configs/envs/.env.pw' './configs/envs/.env.poa_core')
touch $targetFile;
truncate -s 0 $targetFile;
echo "Creating script file with envs"
echo "window.process = { env: { } };" >> $targetFile;
for envFile in "${envFiles[@]}"
do
# read each env file
while read line; do
# if it is a comment or an empty line, continue to next one
if [ "${line:0:1}" == "#" ] || [ "${line}" == "" ]; then
continue
fi
# split by "=" sign to get variable name and value
configName="$(cut -d'=' -f1 <<<"$line")";
configValue="$(cut -d'=' -f2- <<<"$line")";
# if there is a value, escape it and add line to target file
if [ -n "$configValue" ]; then
escapedConfigValue=$(echo $configValue | sed s/\'/\"/g);
echo "window.process.env.${configName} = '${escapedConfigValue}';" >> $targetFile;
fi
done < $envFile
done
echo "Done"
\ No newline at end of file
#!/bin/sh
yarn install
yarn test:pw
\ No newline at end of file
yarn install --modules-folder node_modules_linux
export NODE_PATH=$(pwd)/node_modules_linux
rm -rf ./playwright/.cache
yarn test:pw "$@"
\ No newline at end of file
......@@ -3,17 +3,39 @@ import {
createMultiStyleConfigHelpers,
defineStyle,
} from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
import { runIfFn } from '@chakra-ui/utils';
const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys);
const baseStyleControl = defineStyle((props) => {
const { colorScheme: c } = props;
return {
_checked: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
borderColor: mode(`${ c }.500`, `${ c }.300`)(props),
_hover: {
bg: mode(`${ c }.600`, `${ c }.400`)(props),
borderColor: mode(`${ c }.600`, `${ c }.400`)(props),
},
},
_indeterminate: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
borderColor: mode(`${ c }.500`, `${ c }.300`)(props),
},
};
});
const baseStyleLabel = defineStyle({
_disabled: { opacity: 0.2 },
});
const baseStyle = definePartsStyle({
const baseStyle = definePartsStyle((props) => ({
label: baseStyleLabel,
});
control: runIfFn(baseStyleControl, props),
}));
const Checkbox = defineMultiStyleConfig({
baseStyle,
......
......@@ -4,6 +4,7 @@ import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
const baseStyle = defineStyle({
display: 'flex',
fontSize: 'md',
marginEnd: '3',
mb: '2',
......
......@@ -9,10 +9,16 @@ const { defineMultiStyleConfig, definePartsStyle } =
const baseStyleLabel = defineStyle({
_disabled: { opacity: 0.2 },
width: 'fit-content',
});
const baseStyleContainer = defineStyle({
width: 'fit-content',
});
const baseStyle = definePartsStyle({
label: baseStyleLabel,
container: baseStyleContainer,
});
const Radio = defineMultiStyleConfig({
......
import { switchAnatomy as parts } from '@chakra-ui/anatomy';
import { defineStyle, createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys);
const baseStyleTrack = defineStyle((props) => {
const { colorScheme: c } = props;
return {
_checked: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
_hover: {
bg: mode(`${ c }.600`, `${ c }.400`)(props),
},
},
};
});
const baseStyle = definePartsStyle((props) => ({
track: baseStyleTrack(props),
}));
const Switch = defineMultiStyleConfig({
baseStyle,
});
export default Switch;
......@@ -36,8 +36,7 @@ const sizes = {
fontSize: 'sm',
},
td: {
px: 4,
py: 6,
p: 4,
},
}),
sm: definePartsStyle({
......@@ -48,7 +47,7 @@ const sizes = {
},
td: {
px: '10px',
py: 6,
py: 4,
fontSize: 'sm',
fontWeight: 500,
},
......@@ -61,7 +60,7 @@ const sizes = {
},
td: {
px: '6px',
py: 6,
py: 4,
fontSize: 'sm',
fontWeight: 500,
},
......
......@@ -34,6 +34,7 @@ const baseStyleContainer = defineStyle({
display: 'inline-block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
borderRadius: 'sm',
...transitionProps,
});
......
......@@ -13,6 +13,7 @@ import Popover from './Popover';
import Radio from './Radio';
import Skeleton from './Skeleton';
import Spinner from './Spinner';
import Switch from './Switch';
import Table from './Table';
import Tabs from './Tabs';
import Tag from './Tag';
......@@ -36,6 +37,7 @@ const components = {
Radio,
Skeleton,
Spinner,
Switch,
Tabs,
Table,
Tag,
......
......@@ -25,8 +25,11 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
_disabled: {
opacity: 1,
backgroundColor: mode('gray.200', 'whiteAlpha.200')(props),
border: 'none',
borderColor: 'transparent',
cursor: 'not-allowed',
_hover: {
borderColor: 'transparent',
},
},
_invalid: {
borderColor: getColor(theme, ec),
......
......@@ -11,9 +11,10 @@ export interface WatchlistName {
export interface AddressParam {
hash: string;
implementation_name: string;
implementation_name: string | null;
name: string | null;
is_contract: boolean;
is_verified: boolean | null;
private_tags: Array<AddressTag> | null;
watchlist_names: Array<WatchlistName> | null;
public_tags: Array<AddressTag> | null;
......
......@@ -16,10 +16,10 @@ export interface Block {
total_difficulty: string;
gas_used: string | null;
gas_limit: string;
nonce: number;
base_fee_per_gas: number | null;
burnt_fees: number | null;
priority_fee: number | null;
nonce: string;
base_fee_per_gas: string | null;
burnt_fees: string | null;
priority_fee: string | null;
extra_data: string | null;
state_root: string | null;
rewards?: Array<Reward>;
......
......@@ -2,14 +2,21 @@ import type { AddressParam } from './addressParams';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
export interface InternalTransaction {
export type InternalTransaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
error: string | null;
success: boolean;
type: TxInternalsType;
transaction_hash: string;
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: string;
index: number;
block: number;
......@@ -25,5 +32,5 @@ export interface InternalTransactionsResponse {
items_count: number;
transaction_hash: string;
transaction_index: number;
};
} | null;
}
......@@ -3,7 +3,7 @@ import type { DecodedInput } from './decodedInput';
export interface Log {
address: AddressParam;
topics: Array<string>;
topics: Array<string | null>;
data: string;
index: number;
decoded: DecodedInput | null;
......
import type { BlocksResponse, BlockTransactionsResponse } from 'types/api/block';
import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponse } from 'types/api/log';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import { QueryKeys } from 'types/client/queries';
import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
......@@ -25,6 +26,13 @@ export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse :
never
export type PaginationFilters<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.blocks ? BlockFilters :
Q extends QueryKeys.txsValidate ? TTxsFilters :
Q extends QueryKeys.txsPending ? TTxsFilters :
Q extends QueryKeys.txTokenTransfers ? TokenTransferFilters :
never
export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params'];
type PaginationFields = {
......
export interface Reward {
reward: string;
type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward';
type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward' | 'POA Mania Reward';
}
......@@ -10,4 +10,5 @@ export type Stats = {
gas_prices: {average: number; fast: number; slow: number};
static_gas_price: string;
market_cap: string;
network_utilization_percentage: number;
}
import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric } from './tokenInfo';
import type { TokenInfoGeneric, TokenType } from './tokenInfo';
export type Erc20TotalPayload = {
decimals: string | null;
......@@ -47,3 +47,7 @@ export interface TokenTransferResponse {
transaction_hash: string;
} | null;
}
export interface TokenTransferFilters {
type: Array<TokenType>;
}
......@@ -5,10 +5,18 @@ import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = {
raw: string;
decoded: string;
} | DecodedInput;
export interface Transaction {
export type Transaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
hash: string;
result: string;
confirmations: number;
......@@ -17,21 +25,19 @@ export interface Transaction {
timestamp: string | null;
confirmation_duration: Array<number>;
from: AddressParam;
to: AddressParam | null;
created_contract: AddressParam;
value: string;
fee: Fee;
gas_price: number;
type: number;
gas_price: string;
type: number | null;
gas_used: string | null;
gas_limit: string;
max_fee_per_gas: number | null;
max_priority_fee_per_gas: number | null;
priority_fee: number | null;
base_fee_per_gas: number | null;
tx_burnt_fee: number | null;
max_fee_per_gas: string | null;
max_priority_fee_per_gas: string | null;
priority_fee: string | null;
base_fee_per_gas: string | null;
tx_burnt_fee: string | null;
nonce: number;
position: number;
position: number | null;
revert_reason: TransactionRevertReason | null;
raw_input: string;
decoded_input: DecodedInput | null;
......
......@@ -19,15 +19,16 @@ export type MarketplaceCategory = { id: MarketplaceCategoriesIds; name: string }
export type AppItemPreview = {
id: string;
external: boolean;
title: string;
logo: string;
shortDescription: string;
categories: Array<MarketplaceCategoriesIds>;
url: string;
}
export type AppItemOverview = AppItemPreview & {
author: string;
url: string;
description: string;
site?: string;
twitter?: string;
......
......@@ -16,4 +16,5 @@ export enum QueryKeys {
chartsMarket = 'charts-market',
indexBlocks='indexBlocks',
indexTxs='indexTxs',
jsonRpcUrl='json-rpc-url'
}
export type StatsSection = { id: StatsSectionIds; title: string; charts: Array<StatsChart> }
export type StatsSectionIds = keyof typeof StatsSectionId;
export enum StatsSectionId {
'all',
'accounts',
'blocks',
'transactions',
'gas',
}
export type StatsInterval = { id: StatsIntervalIds; title: string }
export type StatsIntervalIds = keyof typeof StatsIntervalId;
export enum StatsIntervalId {
'all',
'oneMonth',
'threeMonths',
'sixMonths',
'oneYear',
}
export type StatsChart = {
visible?: boolean;
id: string;
title: string;
description: string;
apiMethodURL: string;
}
......@@ -15,9 +15,9 @@ import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = {
data?: ApiKey;
......@@ -33,7 +33,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'all',
defaultValues: {
token: data?.api_key || '',
......@@ -71,7 +71,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
}
return [ ...(prevData || []), response ];
return [ response, ...(prevData || []) ];
});
onClose();
......@@ -113,7 +113,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
maxLength={ NAME_MAX_LENGTH }
/>
<FormLabel>
{ getPlaceholderWithError('Application name for API key (e.g Web3 project)', errors.name?.message) }
<InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name?.message }/>
</FormLabel>
</FormControl>
);
......@@ -145,7 +145,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Generate API key' }
......
......@@ -30,7 +30,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => {
return (
<Text> API key for <Text fontWeight="600" as="span">{ ` "${ data.name || 'name' }" ` }</Text> will be deleted </Text>
<Text> API key for <Text fontWeight="700" as="span">{ ` "${ data.name || 'name' }" ` }</Text> will be deleted </Text>
);
}, [ data.name ]);
......
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, Text, useColorModeValue } from '@chakra-ui/react';
import type { MouseEvent } from 'react';
import React, { useCallback } from 'react';
......@@ -8,9 +7,9 @@ import type { AppItemPreview } from 'types/client/apps';
import northEastIcon from 'icons/arrows/north-east.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import AppCardLink from './AppCardLink';
import { APP_CATEGORIES } from './constants';
interface Props extends AppItemPreview {
......@@ -19,7 +18,10 @@ interface Props extends AppItemPreview {
onFavoriteClick: (id: string, isFavorite: boolean) => void;
}
const AppCard = ({ id,
const AppCard = ({
id,
url,
external,
title,
logo,
shortDescription,
......@@ -85,11 +87,12 @@ const AppCard = ({ id,
size={{ base: 'xs', sm: 'sm' }}
fontWeight="semibold"
>
<NextLink href={ link('app_index', { id: id }) } passHref>
<LinkOverlay>
{ title }
</LinkOverlay>
</NextLink>
<AppCardLink
id={ id }
url={ url }
external={ external }
title={ title }
/>
</Heading>
<Text
......
import { LinkOverlay } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import link from 'lib/link/link';
type Props = {
id: string;
url: string;
external: boolean;
title: string;
}
const AppLink = ({ url, external, id, title }: Props) => {
return external ? (
<LinkOverlay href={ url } isExternal={ true }>
{ title }
</LinkOverlay>
) : (
<NextLink href={ link('app_index', { id: id }) } passHref>
<LinkOverlay>
{ title }
</LinkOverlay>
</NextLink>
);
};
export default AppLink;
......@@ -41,6 +41,8 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps,
<AppCard
onInfoClick={ onAppClick }
id={ app.id }
external={ app.external }
url={ app.url }
title={ app.title }
logo={ app.logo }
shortDescription={ app.shortDescription }
......
import {
Box, Button, Flex, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
Box, Flex, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
} from '@chakra-ui/react';
import NextLink from 'next/link';
import React, { useCallback } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
......@@ -14,10 +13,11 @@ import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import AppModalLink from './AppModalLink';
import { APP_CATEGORIES } from './constants';
type Props = {
......@@ -35,6 +35,8 @@ const AppModal = ({
}: Props) => {
const {
title,
url,
external,
author,
description,
site,
......@@ -64,11 +66,13 @@ const AppModal = ({
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
const isMobile = useIsMobile();
return (
<Modal
isOpen={ Boolean(id) }
onClose={ onClose }
size={{ base: 'full', lg: 'md' }}
size={ isMobile ? 'full' : 'md' }
isCentered
>
<ModalOverlay/>
......@@ -119,16 +123,12 @@ const AppModal = ({
marginTop={{ base: 6, sm: 0 }}
>
<Box display="flex">
<NextLink href={ link('app_index', { id: id }) } passHref>
<Button
as="a"
size="sm"
marginRight={ 2 }
width={{ base: '100%', sm: 'auto' }}
>
Launch app
</Button>
</NextLink>
<AppModalLink
id={ id }
url={ url }
external={ external }
title={ title }
/>
<IconButton
aria-label="Mark as favorite"
......
import { Button } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import link from 'lib/link/link';
type Props = {
id: string;
url: string;
external: boolean;
title: string;
}
const AppModalLink = ({ url, external, id }: Props) => {
const buttonProps = {
size: 'sm',
marginRight: 2,
width: { base: '100%', sm: 'auto' },
...(external ? {
target: '_blank',
rel: 'noopener noreferrer',
} : {}),
};
return external ? (
<Button
as="a"
href={ url }
{ ...buttonProps }
>Launch app</Button>
) : (
<NextLink href={ link('app_index', { id: id }) } passHref>
<Button
as="a"
{ ...buttonProps }
>Launch app</Button>
</NextLink>
);
};
export default AppModalLink;
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import TestApp from 'playwright/TestApp';
import BlockDetails from './BlockDetails';
const API_URL = '/node-api/blocks/1';
const hooksConfig = {
router: {
query: { id: 1 },
},
};
test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.base),
}));
const component = await mount(
<TestApp>
<BlockDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await page.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
test('genesis block', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.genesis),
}));
const component = await mount(
<TestApp>
<BlockDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await page.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
......@@ -228,7 +228,7 @@ const BlockDetails = () => {
</Tooltip>
) }
</DetailsInfoItem>
{ data.priority_fee !== null && data.priority_fee > 0 && (
{ data.priority_fee !== null && BigNumber(data.priority_fee).gt(ZERO) && (
<DetailsInfoItem
title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion."
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import BlocksContent from './BlocksContent';
const API_URL = '/node-api/blocks';
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp>
<BlocksContent/>
</TestApp>,
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('new item from socket', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp withSocket>
<BlocksContent/>
</TestApp>,
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'blocks:new_block');
socketServer.sendMessage(socket, channel, 'new_block', {
average_block_time: '6212.0',
block: {
...blockMock.base,
height: blockMock.base.height + 1,
timestamp: '2022-11-11T11:59:58Z',
},
});
await expect(component).toHaveScreenshot();
});
test('socket error', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp withSocket>
<BlocksContent/>
</TestApp>,
);
const socket = await createSocket();
await socketServer.joinChannel(socket, 'blocks:new_block');
socket.close();
await expect(component).toHaveScreenshot();
});
......@@ -2,7 +2,6 @@ import {
Box,
Button,
FormControl,
FormLabel,
Input,
Textarea,
useColorModeValue,
......@@ -16,11 +15,11 @@ import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = {
data?: CustomAbi;
......@@ -37,7 +36,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, formState: { errors, isValid }, handleSubmit, setError } = useForm<Inputs>({
const { control, formState: { errors, isValid, isDirty }, handleSubmit, setError } = useForm<Inputs>({
defaultValues: {
contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '',
......@@ -77,7 +76,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
}
return [ ...(prevData || []), response ];
return [ response, ...(prevData || []) ];
});
onClose();
......@@ -119,7 +118,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError('Project name', errors.name?.message) }</FormLabel>
<InputPlaceholder text="Project name" error={ errors.name?.message }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
......@@ -133,7 +132,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
minH="300px"
isInvalid={ Boolean(errors.abi) }
/>
<FormLabel>{ getPlaceholderWithError(`Custom ABI [{...}] (JSON format)`, errors.abi?.message) }</FormLabel>
<InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi?.message }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
......@@ -171,7 +170,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Create custom ABI' }
......
......@@ -29,7 +29,7 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => {
return (
<Text>Custom ABI for<Text fontWeight="600" as="span">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text>
<Text>Custom ABI for<Text fontWeight="700" as="span">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text>
);
}, [ data.name ]);
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block';
import type { Stats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
......@@ -26,10 +27,14 @@ const LatestBlocks = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Block>>(
[ QueryKeys.indexBlocks ],
async() => await fetch(`/api/index/blocks`),
async() => await fetch(`/node-api/index/blocks`),
);
const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
() => fetch('/node-api/stats'),
);
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexBlocks ], (prevData: Array<Block> | undefined) => {
......@@ -78,15 +83,19 @@ const LatestBlocks = () => {
content = (
<>
{ statsQueryResult.isLoading && (
<Skeleton h="24px" w="170px" mb={{ base: 6, lg: 9 }}/>
) }
{ statsQueryResult.data?.network_utilization_percentage !== undefined && (
<Box mb={{ base: 6, lg: 9 }}>
<Text as="span" fontSize="sm">
Network utilization:{ nbsp }
</Text>
{ /* Not implemented in API yet */ }
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
43.8%
{ statsQueryResult.data?.network_utilization_percentage.toFixed(2) }%
</Text>
</Box>
) }
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 6 } height={ `${ BLOCK_HEIGHT * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<AnimatePresence initial={ false } >
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ BLOCK_HEIGHT }/>)) }
......@@ -101,7 +110,7 @@ const LatestBlocks = () => {
return (
<>
<Heading as="h4" fontSize="18px" mb={{ base: 3, lg: 8 }}>Latest Blocks</Heading>
<Heading as="h4" size="sm" mb={{ base: 4, lg: 7 }}>Latest Blocks</Heading>
{ content }
</>
);
......
......@@ -19,7 +19,7 @@ const LatestTransactions = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Transaction>>(
[ QueryKeys.indexTxs ],
async() => await fetch(`/api/index/txs`),
async() => await fetch(`/node-api/index/txs`),
);
let content;
......@@ -54,7 +54,7 @@ const LatestTransactions = () => {
return (
<>
<Heading as="h4" fontSize="18px" mb={{ base: 3, lg: 8 }}>Latest transactions</Heading>
<Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading>
{ content }
</>
);
......
......@@ -40,7 +40,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const iconColor = useColorModeValue('blue.600', 'blue.300');
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const dataTo = tx.to ? tx.to : tx.created_contract;
const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
const isMobile = useIsMobile();
......
......@@ -28,7 +28,7 @@ const Stats = () => {
const { data, isLoading, isError } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
async() => await fetch(`/api/index/stats`),
async() => await fetch(`/node-api/stats`),
);
if (isError) {
......
import { useToken } from '@chakra-ui/react';
import React from 'react';
import type { ChainIndicatorChartData } from './types';
import type { TimeChartData } from 'ui/shared/chart/types';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartLine from 'ui/shared/chart/ChartLine';
......@@ -11,7 +11,7 @@ import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
interface Props {
data: ChainIndicatorChartData;
data: TimeChartData;
caption?: string;
}
......
import { Flex, Spinner } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { ChainIndicatorChartData } from './types';
import type { TimeChartData } from 'ui/shared/chart/types';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ChainIndicatorChart from './ChainIndicatorChart';
type Props = UseQueryResult<ChainIndicatorChartData>;
type Props = UseQueryResult<TimeChartData>;
const ChainIndicatorChartContainer = ({ data, isError, isLoading }: Props) => {
const content = (() => {
if (isLoading) {
return <Spinner size="md" m="auto"/>;
return <ContentLoader mt="auto"/>;
}
if (isError) {
......
......@@ -18,7 +18,7 @@ interface Props {
}
const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => {
const bgColor = useColorModeValue('white', 'gray.900');
const bgColor = useColorModeValue('white', 'black');
const isMobile = useIsMobile();
const handleClick = React.useCallback(() => {
......
......@@ -40,8 +40,8 @@ const ChainIndicators = () => {
() => fetch('/node-api/stats'),
);
const bgColor = useColorModeValue('white', 'gray.900');
const listBgColor = useColorModeValue('gray.50', 'black');
const bgColor = useColorModeValue('white', 'black');
const listBgColor = useColorModeValue('gray.50', 'gray.900');
if (indicators.length === 0) {
return null;
......@@ -91,7 +91,16 @@ const ChainIndicators = () => {
<ChainIndicatorChartContainer { ...queryResult }/>
</Flex>
{ indicators.length > 1 && (
<Flex flexShrink={ 0 } flexDir="column" as="ul" p={ 3 } borderRadius="lg" bgColor={ listBgColor } rowGap={ 3 } order={{ base: 1, lg: 2 }}>
<Flex
flexShrink={ 0 }
flexDir="column"
as="ul"
p={ 3 }
borderRadius="lg"
bgColor={ listBgColor }
rowGap={ 3 }
order={{ base: 1, lg: 2 }}
>
{ indicators.map((indicator) => (
<ChainIndicatorItem
key={ indicator.id }
......
import type { ChartTransactionResponse, ChartMarketResponse } from 'types/api/charts';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { Stats } from 'types/api/stats';
import type { QueryKeys } from 'types/client/queries';
import type { TimeChartDataItem } from 'ui/shared/chart/types';
import type { TimeChartData } from 'ui/shared/chart/types';
export type ChartsQueryKeys = QueryKeys.chartsTxs | QueryKeys.chartsMarket;
......@@ -16,7 +16,7 @@ export interface TChainIndicator<Q extends ChartsQueryKeys> {
api: {
queryName: Q;
path: string;
dataFn: (response: ChartsResponse<Q>) => ChainIndicatorChartData;
dataFn: (response: ChartsResponse<Q>) => TimeChartData;
};
}
......@@ -24,5 +24,3 @@ export type ChartsResponse<Q extends ChartsQueryKeys> =
Q extends QueryKeys.chartsTxs ? ChartTransactionResponse :
Q extends QueryKeys.chartsMarket ? ChartMarketResponse :
never;
export type ChainIndicatorChartData = Array<TimeChartDataItem>;
......@@ -2,13 +2,14 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { TChainIndicator, ChartsResponse, ChartsQueryKeys, ChainIndicatorChartData } from './types';
import type { TChainIndicator, ChartsResponse, ChartsQueryKeys } from './types';
import type { TimeChartData } from 'ui/shared/chart/types';
import useFetch from 'lib/hooks/useFetch';
type NotUndefined<T> = T extends undefined ? never : T;
export default function useFetchCharData<Q extends ChartsQueryKeys>(indicator: TChainIndicator<Q> | undefined): UseQueryResult<ChainIndicatorChartData> {
export default function useFetchChartData<Q extends ChartsQueryKeys>(indicator: TChainIndicator<Q> | undefined): UseQueryResult<TimeChartData> {
const fetch = useFetch();
type ResponseType = ChartsResponse<NotUndefined<typeof indicator>['api']['queryName']>;
......@@ -23,6 +24,6 @@ export default function useFetchCharData<Q extends ChartsQueryKeys>(indicator: T
return {
...queryResult,
data: queryResult.data && indicator ? indicator.api.dataFn(queryResult.data) : queryResult.data,
} as UseQueryResult<ChainIndicatorChartData>;
} as UseQueryResult<TimeChartData>;
}, [ indicator, queryResult ]);
}
......@@ -25,7 +25,7 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
.map((item) => ({ date: new Date(item.date), value: item.tx_count }))
.sort(sortByDateDesc),
name: 'Tx/day',
valueFormatter: (x) => shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 2 }),
valueFormatter: (x: number) => shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 2 }),
} ]),
},
};
......@@ -44,7 +44,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
.sort(sortByDateDesc),
name: `${ appConfig.network.currency.symbol } price`,
valueFormatter: (x) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
} ]),
},
};
......@@ -64,7 +64,7 @@ const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
.sort(sortByDateDesc),
name: 'Market cap',
valueFormatter: (x) => '$' + shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 0 }),
valueFormatter: (x: number) => '$' + shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 0 }),
} ]),
},
};
......
......@@ -14,13 +14,12 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable';
import DataFetchAlert from '../shared/DataFetchAlert';
const DATA_LIMIT = 3;
const ApiKeysPage: React.FC = () => {
......@@ -58,7 +57,7 @@ const ApiKeysPage: React.FC = () => {
const description = (
<AccountPageDescription>
Create API keys to use for your RPC and EthRPC API requests. For more information, see { space }
<Link href="https://docs.blockscout.com/for-users/api#api-keys">“How to use a Blockscout API key”</Link>.
<Link href="https://docs.blockscout.com/for-users/api#api-keys" target="_blank">“How to use a Blockscout API key”</Link>.
</AccountPageDescription>
);
......@@ -108,7 +107,12 @@ const ApiKeysPage: React.FC = () => {
<>
{ description }
{ Boolean(data.length) && list }
<Stack marginTop={ 8 } spacing={ 5 } direction={{ base: 'column', lg: 'row' }}>
<Stack
marginTop={ 8 }
spacing={ 5 }
direction={{ base: 'column', lg: 'row' }}
align={{ base: 'start', lg: 'center' }}
>
<Button
size="lg"
onClick={ apiKeyModalProps.onOpen }
......
......@@ -24,7 +24,7 @@ const BlockPageContent = () => {
return (
<Page>
<PageTitle text={ `Block #${ router.query.id }` }/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 12 }}/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/>
</Page>
);
};
......
......@@ -13,13 +13,12 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable';
import DataFetchAlert from '../shared/DataFetchAlert';
const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
......
......@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { JsonRpcUrlResponse } from 'types/api/json-rpc-url';
import type { AppItemOverview } from 'types/client/apps';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import useFetch from 'lib/hooks/useFetch';
......@@ -27,7 +28,7 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
}, []);
const { data: jsonRpcUrlResponse } = useQuery<unknown, unknown, JsonRpcUrlResponse>(
[ 'json-rpc-url' ],
[ QueryKeys.jsonRpcUrl ],
async() => await fetch(`/node-api/config/json-rpc-url`),
{ refetchOnMount: false },
);
......
......@@ -23,7 +23,7 @@ const MyProfile = () => {
}
return (
<VStack maxW="412px" mt={ 12 } gap={ 5 } alignItems="stretch">
<VStack maxW="412px" mt={ 8 } gap={ 5 } alignItems="stretch">
<UserAvatar size={ 64 } data={ data } isFetched={ isFetched }/>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import StatsFilters from '../stats/StatsFilters';
import useStats from '../stats/useStats';
import WidgetsList from '../stats/WidgetsList';
const Stats = () => {
const {
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
displayedCharts,
} = useStats();
return (
<Page>
<PageTitle text="Ethereum Stats"/>
<Box mb={{ base: 6, sm: 8 }}>
<StatsFilters
section={ section }
onSectionChange={ handleSectionChange }
interval={ interval }
onIntervalChange={ handleIntervalChange }
onFilterInputChange={ debounceFilterCharts }
/>
</Box>
<WidgetsList
charts={ displayedCharts }
/>
</Page>
);
};
export default Stats;
......@@ -82,7 +82,7 @@ const TransactionPageContent = () => {
</Flex>
) }
</Flex>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 12 }}/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/>
</Page>
);
};
......
......@@ -34,7 +34,7 @@ type Inputs = {
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const fetch = useFetch();
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'all',
defaultValues: {
address: data?.address_hash || '',
......@@ -120,7 +120,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
......
......@@ -40,7 +40,7 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const renderText = useCallback(() => {
return (
<Text>Tag<Text fontWeight="600" as="span">{ ` "${ tag || 'tag' }" ` }</Text>will be deleted</Text>
<Text>Tag<Text fontWeight="700" as="span">{ ` "${ tag || 'tag' }" ` }</Text>will be deleted</Text>
);
}, [ tag ]);
......
......@@ -35,7 +35,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
const [ pending, setPending ] = useState(false);
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'all',
defaultValues: {
transaction: data?.transaction_hash || '',
......@@ -119,7 +119,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
......
......@@ -48,7 +48,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
text = (
<>
<Text display="inline" as="span">Public tag</Text>
<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tags[0] }" ` }</Text>
<Text fontWeight="700" whiteSpace="pre" as="span">{ ` "${ tags[0] }" ` }</Text>
<Text as="span">will be removed.</Text>
</>
);
......@@ -57,15 +57,15 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const tagsText: Array<JSX.Element | string> = [];
tags.forEach((tag, index) => {
if (index < tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tag }"` }</Text>);
tagsText.push(<Text fontWeight="700" whiteSpace="pre" as="span">{ ` "${ tag }"` }</Text>);
tagsText.push(',');
}
if (index === tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tag }" ` }</Text>);
tagsText.push(<Text fontWeight="700" whiteSpace="pre" as="span">{ ` "${ tag }" ` }</Text>);
tagsText.push('and');
}
if (index === tags.length - 1) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tag }" ` }</Text>);
tagsText.push(<Text fontWeight="700" whiteSpace="pre" as="span">{ ` "${ tag }" ` }</Text>);
}
});
text = (
......@@ -76,7 +76,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
}
return (
<>
<Box marginBottom={ 12 }>
<Box marginBottom={ 8 }>
{ text }
</Box>
<FormControl variant="floating" id="tag-delete" backgroundColor={ formBackgroundColor }>
......
......@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from './PublicTagsForm';
......@@ -25,7 +25,7 @@ export default function PublicTagFormComment({ control, error, size }: Props) {
isInvalid={ Boolean(error) }
/>
<FormLabel>
{ getPlaceholderWithError('Specify the reason for adding tags and color preference(s)', error?.message) }
<InputPlaceholder text="Specify the reason for adding tags and color preference(s)" error={ error?.message }/>
</FormLabel>
</FormControl>
);
......
......@@ -61,7 +61,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const fetch = useFetch();
const inputSize = { base: 'md', lg: 'lg' };
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
defaultValues: {
fullName: data?.full_name || '',
email: data?.email || '',
......@@ -123,7 +123,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
});
}
return [ ...(prevData || []), response ];
return [ response, ...(prevData || []) ];
});
changeToDataScreen(true);
......@@ -237,7 +237,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading }
>
Send request
......
import type { InputProps } from '@chakra-ui/react';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import { FormControl, Input } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const TEXT_INPUT_MAX_LENGTH = 255;
......@@ -36,7 +36,7 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({
isInvalid={ Boolean(error) }
maxLength={ TEXT_INPUT_MAX_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError(label, error?.message) }</FormLabel>
<InputPlaceholder text={ label } error={ error?.message }/>
</FormControl>
);
}, [ label, required, error, size ]);
......
......@@ -26,7 +26,8 @@ const AccountPageDescription = ({ children }: {children: React.ReactNode}) => {
return function cleanup() {
window.removeEventListener('resize', resizeHandler);
};
}, [ calculateCut ]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
const expand = useCallback(() => {
setExpanded(true);
......@@ -38,7 +39,7 @@ const AccountPageDescription = ({ children }: {children: React.ReactNode}) => {
);
return (
<Box position="relative" marginBottom={{ base: 6, lg: 12 }}>
<Box position="relative" marginBottom={{ base: 6, lg: 8 }}>
<Text
ref={ ref }
maxHeight={ needCut && !expanded ? `${ CUT_HEIGHT }px` : 'auto' }
......
......@@ -2,13 +2,12 @@ import type { InputProps } from '@chakra-ui/react';
import {
Input,
FormControl,
FormLabel,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import { ADDRESS_LENGTH } from 'lib/validations/address';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>;
......@@ -33,7 +32,7 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError(placeholder, error?.message) }</FormLabel>
<InputPlaceholder text={ placeholder } error={ error?.message }/>
</FormControl>
);
}
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import AppError from './AppError';
test.use({ viewport: { width: 900, height: 400 } });
test('status code 404', async({ mount }) => {
const component = await mount(
<TestApp>
<AppError statusCode={ 404 }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('status code 500', async({ mount }) => {
const component = await mount(
<TestApp>
<AppError statusCode={ 500 }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Text } from '@chakra-ui/react';
import { Box, Text, chakra } from '@chakra-ui/react';
import { keyframes } from '@chakra-ui/system';
import React from 'react';
......@@ -7,9 +7,13 @@ const runnerAnimation = keyframes`
100% { left: '100%'; transform: translateX(-99%); }
`;
const ContentLoader = () => {
interface Props {
className?: string;
}
const ContentLoader = ({ className }: Props) => {
return (
<Box display="inline-block">
<Box display="inline-block" className={ className }>
<Box
width="100%"
height="6px"
......@@ -31,4 +35,4 @@ const ContentLoader = () => {
);
};
export default ContentLoader;
export default chakra(ContentLoader);
......@@ -13,6 +13,13 @@ interface Props {
}
const CurrencyValue = ({ value, currency = '', decimals, exchangeRate, className, accuracy, accuracyUsd }: Props) => {
if (value === undefined || value === null) {
return (
<Box as="span" className={ className }>
<Text>N/A</Text>
</Box>
);
}
const valueCurr = BigNumber(value).div(BigNumber(10 ** Number(decimals || '18')));
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
......
......@@ -12,6 +12,7 @@ import {
import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
type Props = {
......@@ -53,8 +54,10 @@ const DeleteModal: React.FC<Props> = ({
mutation.mutate();
}, [ setAlertVisible, mutation ]);
const isMobile = useIsMobile();
return (
<Modal isOpen={ isOpen } onClose={ onModalClose } size={{ base: 'full', lg: 'md' }}>
<Modal isOpen={ isOpen } onClose={ onModalClose } size={ isMobile ? 'full' : 'md' }>
<ModalOverlay/>
<ModalContent>
<ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader>
......
......@@ -6,7 +6,7 @@ import filterIcon from 'icons/filter.svg';
const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 } mr={{ base: 0, lg: 2 }}/>;
interface Props {
isActive: boolean;
isActive?: boolean;
appliedFiltersNum?: number;
onClick: () => void;
}
......
......@@ -15,6 +15,7 @@ type Props = {
const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) => {
const [ filterQuery, setFilterQuery ] = useState('');
const inputRef = React.useRef<HTMLInputElement>(null);
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
......@@ -37,7 +38,7 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
<InputLeftElement
pointerEvents="none"
>
<Icon as={ searchIcon } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
<Icon as={ searchIcon } color={ iconColor }/>
</InputLeftElement>
<Input
......@@ -48,8 +49,10 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
placeholder={ placeholder }
borderWidth="2px"
textOverflow="ellipsis"
whiteSpace="nowrap"
/>
{ filterQuery ? (
<InputRightElement>
<IconButton
colorScheme="gray"
......@@ -57,11 +60,12 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
title="Clear the filter input"
w={ 6 }
h={ 6 }
icon={ <Icon as={ crossIcon } w={ 4 } h={ 4 } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/> }
icon={ <Icon as={ crossIcon } w={ 4 } h={ 4 } color={ iconColor }/> }
size="sm"
onClick={ handleFilterQueryClear }
/>
</InputRightElement>
) : null }
</InputGroup>
);
};
......
......@@ -49,7 +49,7 @@ export default function FormModal<TData>({
<ModalCloseButton/>
<ModalBody mb={ 0 }>
{ (isAlertVisible || text) && (
<Box marginBottom={{ base: 6, lg: 12 }}>
<Box marginBottom={{ base: 6, lg: 8 }}>
{ text && (
<Text lineHeight="30px" mb={ 3 }>
{ text }
......
import { FormLabel, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
text: string;
error?: string;
}
const InputPlaceholder = ({ text, error }: Props) => {
return (
<FormLabel>
<chakra.span>{ text }</chakra.span>
{ error && <chakra.span order={ 3 } whiteSpace="pre"> - { error }</chakra.span> }
</FormLabel>
);
};
export default InputPlaceholder;
......@@ -10,7 +10,7 @@ import useFetch from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import AppError from 'ui/shared/AppError';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/snippets/header/Header';
......
......@@ -3,7 +3,7 @@ import React from 'react';
interface Props {
children: React.ReactNode;
hasSearch: boolean;
hasSearch?: boolean;
}
const PageContent = ({ children, hasSearch }: Props) => {
......
......@@ -3,7 +3,7 @@ import React from 'react';
const PageTitle = ({ text }: {text: string}) => {
return (
<Heading as="h1" size="lg" marginBottom={{ base: 6, lg: 8 }}>{ text }</Heading>
<Heading as="h1" size="lg" marginBottom={ 6 }>{ text }</Heading>
);
};
......
import {
Input,
FormControl,
FormLabel,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const TAG_MAX_LENGTH = 35;
......@@ -24,7 +23,7 @@ function TagInput<Inputs extends FieldValues, Name extends Path<Inputs>>({ field
isInvalid={ Boolean(error) }
maxLength={ TAG_MAX_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError(`Private tag (max 35 characters)`, error?.message) }</FormLabel>
<InputPlaceholder text="Private tag (max 35 characters)" error={ error?.message }/>
</FormControl>
);
}
......
import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import TokenSnippet from './TokenSnippet';
const API_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0x363574E6C5C71c343d7348093D84320c76d5Dd29/logo.png';
test.use(devices['iPhone 13 Pro']);
test('unnamed', async({ mount }) => {
const component = await mount(
<TestApp>
<TokenSnippet hash="0x363574E6C5C71c343d7348093D84320c76d5Dd29" symbol="xDAI"/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('named', async({ mount }) => {
const component = await mount(
<TestApp>
<TokenSnippet hash="0x363574E6C5C71c343d7348093D84320c76d5Dd29" name="Shavuha token" symbol="SHA"/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('with logo', async({ mount, page }) => {
await page.route(API_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
const component = await mount(
<TestApp>
<TokenSnippet hash="0x363574E6C5C71c343d7348093D84320c76d5Dd29"/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import TokenTransfer from './TokenTransfer';
const API_URL = '/node-api/transactions/1/token-transfers';
test('without tx info +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenTransferMock.mixTokens),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<TokenTransfer
path={ API_URL }
queryName={ QueryKeys.txTokenTransfers }
showTxInfo={ false }
/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Text, Flex, Skeleton } from '@chakra-ui/react';
import { Hide, Show, Text } from '@chakra-ui/react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import type { QueryKeys } from 'types/client/queries';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferSkeletonMobile from 'ui/shared/TokenTransfer/TokenTransferSkeletonMobile';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
......@@ -25,29 +28,34 @@ interface Props {
txHash?: string;
}
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true, txHash }: Props) => {
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true }: Props) => {
const [ filters, setFilters ] = React.useState<Array<TokenType>>([]);
const { isError, isLoading, data, pagination } = useQueryWithPages({
apiPath: path,
queryName,
queryIds,
options: { enabled: !isDisabled },
filters: filters.length ? { type: filters } : undefined,
});
const isPaginatorHidden = pagination.page === 1 && !pagination.hasNextPage;
const handleFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
setFilters(nextValue);
}, []);
const isActionBarHidden = filters.length === 0 && !data?.items.length;
const content = (() => {
if (isLoading || isLoadingProp) {
return (
<>
<Hide below="lg">
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="340px"/> }
<SkeletonTable columns={ showTxInfo ?
[ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] :
[ '185px', '25%', '25%', '25%', '25%' ] }
/>
</Hide>
<Show below="lg">
<TokenTransferSkeletonMobile showTxInfo={ showTxInfo } txHash={ txHash }/>
<TokenTransferSkeletonMobile showTxInfo={ showTxInfo }/>
</Show>
</>
);
......@@ -57,15 +65,19 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return <DataFetchAlert/>;
}
if (!data.items?.length) {
if (!data.items?.length && filters.length === 0) {
return <Text as="span">There are no token transfers</Text>;
}
if (!data.items?.length) {
return <EmptySearchResult text={ `Couldn${ apos }t find any token transfer that matches your query.` }/>;
}
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ isPaginatorHidden ? 0 : 80 }/>
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ 80 }/>
</Hide>
<Show below="lg">
<TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>
......@@ -76,14 +88,9 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return (
<>
{ txHash && (data?.items.length || 0 > 0) && (
<Flex mb={ isPaginatorHidden ? 6 : 0 } w="100%">
<Text as="span" fontWeight={ 600 } whiteSpace="pre">Token transfers for by txn hash: </Text>
<HashStringShorten hash={ txHash }/>
</Flex>
) }
{ isPaginatorHidden ? null : (
<ActionBar>
{ !isActionBarHidden && (
<ActionBar mt={ -6 }>
<TokenTransferFilter defaultFilters={ filters } onFilterChange={ handleFilterChange } appliedFiltersNum={ filters.length }/>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, CheckboxGroup, Checkbox, Text, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import FilterButton from 'ui/shared/FilterButton';
import { TOKEN_TYPE } from './helpers';
interface Props {
appliedFiltersNum?: number;
defaultFilters: Array<TokenType>;
onFilterChange: (nextValue: Array<TokenType>) => void;
}
const TokenTransfer = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<FilterButton
isActive={ isOpen || Number(appliedFiltersNum) > 0 }
onClick={ onToggle }
appliedFiltersNum={ appliedFiltersNum }
/>
</PopoverTrigger>
<PopoverContent w="200px">
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }>
<Text variant="secondary" fontWeight={ 600 }>Type</Text>
<CheckboxGroup size="lg" onChange={ onFilterChange } defaultValue={ defaultFilters }>
{ TOKEN_TYPE.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(TokenTransfer);
......@@ -11,7 +11,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TokenSnippet from 'ui/shared/TokenSnippet';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
......
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TokenTransferSkeletonMobile = ({ showTxInfo, txHash }: { showTxInfo?: boolean; txHash?: string }) => {
const TokenTransferSkeletonMobile = ({ showTxInfo }: { showTxInfo?: boolean }) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="100%"/> }
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
......@@ -51,7 +49,6 @@ const TokenTransferSkeletonMobile = ({ showTxInfo, txHash }: { showTxInfo?: bool
</Flex>
)) }
</Box>
</>
);
};
......
......@@ -9,7 +9,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TokenSnippet from 'ui/shared/TokenSnippet';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
......@@ -18,7 +18,7 @@ type Props = TokenTransfer & {
showTxInfo?: boolean;
}
const TxInternalTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => {
const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => {
const value = (() => {
if (!('value' in total)) {
return '-';
......@@ -75,4 +75,4 @@ const TxInternalTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddr
);
};
export default React.memo(TxInternalTableItem);
export default React.memo(TokenTransferTableItem);
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer';
export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => {
......@@ -24,3 +25,9 @@ export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
return 'Token transfer';
}
};
export const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
];
import {
Input,
FormControl,
FormLabel,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, FieldError, FieldValues } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import { TRANSACTION_HASH_LENGTH } from 'lib/validations/transaction';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props<Field> = {
field: Field;
......@@ -23,7 +22,7 @@ function TransactionInput<Field extends Partial<ControllerRenderProps<FieldValue
isInvalid={ Boolean(error) }
maxLength={ TRANSACTION_HASH_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError('Transaction hash (0x...)', error?.message) }</FormLabel>
<InputPlaceholder text="Transaction hash (0x...)" error={ error?.message }/>
</FormControl>
);
}
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import RenderWithChakra from 'playwright/RenderWithChakra';
import TestApp from 'playwright/TestApp';
import Utilization from './Utilization';
test.use({ viewport: { width: 100, height: 50 } });
test('green color scheme in light mode', async({ mount }) => {
test('green color scheme +@dark-mode', async({ mount }) => {
const component = await mount(
<RenderWithChakra>
<TestApp>
<Utilization value={ 0.423 }/>
</RenderWithChakra>,
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('green color scheme in dark mode', async({ mount }) => {
test('gray color scheme +@dark-mode', async({ mount }) => {
const component = await mount(
<RenderWithChakra colorMode="dark">
<Utilization value={ 0.423 }/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
test('gray color scheme in light mode', async({ mount }) => {
const component = await mount(
<RenderWithChakra>
<Utilization value={ 0.423 } colorScheme="gray"/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
test('gray color scheme in dark mode', async({ mount }) => {
const component = await mount(
<RenderWithChakra colorMode="dark">
<TestApp>
<Utilization value={ 0.423 } colorScheme="gray"/>
</RenderWithChakra>,
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
......@@ -35,7 +35,7 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
if (alias) {
return (
<Tooltip label={ hash }>
<Box overflow="hidden" textOverflow="ellipsis">{ alias }</Box>
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ alias }</Box>
</Tooltip>
);
}
......
......@@ -53,7 +53,7 @@ const Header = ({ hideOnScrollDown, hasSearch }: Props) => {
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px"
paddingBottom={ 8 }
>
<Box width="100%">{ hasSearch && <SearchBar/> }</Box>
<ColorModeToggler/>
......
......@@ -41,7 +41,7 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props)
isDisabled={ !isCollapsed }
placement="right"
variant="nav"
gutter={ 15 }
gutter={ 20 }
color={ isActive ? colors.text.active : colors.text.hover }
>
<HStack spacing={ 3 }>
......
......@@ -73,7 +73,7 @@ const NavigationDesktop = () => {
<NetworkLogo isCollapsed={ isCollapsed }/>
<NetworkMenu isCollapsed={ isCollapsed }/>
</Box>
<Box as="nav" mt={ 14 }>
<Box as="nav" mt={ 8 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</VStack>
......
import { Box, Button, Grid, Heading, Text, useColorModeValue } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import ChartWidgetGraph from './ChartWidgetGraph';
import { demoData } from './constants/demo-data';
type Props = {
apiMethodURL: string;
title: string;
description: string;
}
const ChartWidget = ({ title, description }: Props) => {
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
const handleZoom = useCallback(() => {
setIsZoomResetInitial(false);
}, []);
const handleZoomResetClick = useCallback(() => {
setIsZoomResetInitial(true);
}, []);
return (
<Box
padding={{ base: 3, md: 4 }}
borderRadius="md"
border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
>
<Grid>
<Heading
mb={ 1 }
size={{ base: 'xs', md: 'sm' }}
>
{ title }
</Heading>
<Text
mb={ 1 }
gridColumn={ 1 }
as="p"
variant="secondary"
fontSize="xs"
>
{ description }
</Text>
{ !isZoomResetInitial && (
<Button
gridColumn={ 2 }
justifySelf="end"
alignSelf="center"
gridRow="1/3"
size="sm"
variant="outline"
onClick={ handleZoomResetClick }
>
Reset zoom
</Button>
) }
</Grid>
<ChartWidgetGraph
items={ demoData }
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
title={ title }
/>
</Box>
);
};
export default ChartWidget;
import { useToken } from '@chakra-ui/react';
import React, { useEffect, useMemo } from 'react';
import type { TimeChartItem } from 'ui/shared/chart/types';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis';
import ChartGridLine from 'ui/shared/chart/ChartGridLine';
import ChartLine from 'ui/shared/chart/ChartLine';
import ChartOverlay from 'ui/shared/chart/ChartOverlay';
import ChartSelectionX from 'ui/shared/chart/ChartSelectionX';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
interface Props {
title: string;
items: Array<TimeChartItem>;
onZoom: () => void;
isZoomResetInitial: boolean;
}
const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 };
const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial, title }: Props) => {
const ref = React.useRef<SVGSVGElement>(null);
const [ range, setRange ] = React.useState<[ number, number ]>([ 0, Infinity ]);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
const overlayRef = React.useRef<SVGRectElement>(null);
const color = useToken('colors', 'blue.500');
const displayedData = useMemo(() => items.slice(range[0], range[1]).map((d) =>
({ ...d, date: new Date(d.date) })), [ items, range ]);
const chartData = [ { items: items, name: title, color } ];
const { yTickFormat, xScale, yScale } = useTimeChartController({
data: [ { items: displayedData, name: 'chart', color } ],
width: innerWidth,
height: innerHeight,
});
const handleRangeSelect = React.useCallback((nextRange: [ number, number ]) => {
setRange([ range[0] + nextRange[0], range[0] + nextRange[1] ]);
onZoom();
}, [ onZoom, range ]);
useEffect(() => {
if (isZoomResetInitial) {
setRange([ 0, Infinity ]);
}
}, [ isZoomResetInitial ]);
return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref } cursor="pointer">
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ width ? 1 : 0 }>
<ChartGridLine
type="horizontal"
scale={ yScale }
ticks={ 3 }
size={ innerWidth }
disableAnimation
/>
<ChartArea
data={ displayedData }
color={ color }
xScale={ xScale }
yScale={ yScale }
/>
<ChartLine
data={ displayedData }
xScale={ xScale }
yScale={ yScale }
stroke={ color }
animation="left"
strokeWidth={ 3 }
/>
<ChartAxis
type="left"
scale={ yScale }
ticks={ 5 }
tickFormat={ yTickFormat }
disableAnimation
/>
<ChartOverlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }>
<ChartAxis
type="bottom"
scale={ xScale }
transform={ `translate(0, ${ innerHeight })` }
ticks={ 5 }
anchorEl={ overlayRef.current }
disableAnimation
/>
<ChartTooltip
anchorEl={ overlayRef.current }
width={ innerWidth }
height={ innerHeight }
xScale={ xScale }
yScale={ yScale }
data={ chartData }
/>
<ChartSelectionX
anchorEl={ overlayRef.current }
height={ innerHeight }
scale={ xScale }
data={ chartData }
onSelect={ handleRangeSelect }
/>
</ChartOverlay>
</g>
</svg>
);
};
export default React.memo(ChartWidgetGraph);
import { Box, Button, Icon, Menu, MenuButton, MenuItemOption, MenuList, MenuOptionGroup } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
type Props<T extends string> = {
items: Array<{id: T; title: string}>;
selectedId: T;
onSelect: (id: T) => void;
}
export function StatsDropdownMenu<T extends string>({ items, selectedId, onSelect }: Props<T>) {
const selectedCategory = items.find(category => category.id === selectedId);
const handleSelection = useCallback((id: string | Array<string>) => {
const selectedId = Array.isArray(id) ? id[0] : id;
onSelect(selectedId as T);
}, [ onSelect ]);
return (
<Menu
>
<MenuButton
as={ Button }
size="md"
variant="outline"
colorScheme="gray"
w="100%"
>
<Box
as="span"
display="flex"
alignItems="center"
>
{ selectedCategory?.title }
<Icon transform="rotate(-90deg)" ml={{ base: 'auto', sm: 1 }} as={ eastMiniArrowIcon } w={ 5 } h={ 5 }/>
</Box>
</MenuButton>
<MenuList zIndex={ 3 }>
<MenuOptionGroup
value={ selectedId }
type="radio"
onChange={ handleSelection }
>
{ items.map((item) => (
<MenuItemOption
key={ item.id }
value={ item.id }
>
{ item.title }
</MenuItemOption>
)) }
</MenuOptionGroup>
</MenuList>
</Menu>
);
}
export default StatsDropdownMenu;
import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import type { StatsInterval, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats';
import FilterInput from 'ui/shared/FilterInput';
import { STATS_INTERVALS, STATS_SECTIONS } from './constants';
import StatsDropdownMenu from './StatsDropdownMenu';
const sectionsList = Object.keys(STATS_SECTIONS).map((id: string) => ({
id: id,
title: STATS_SECTIONS[id as StatsSectionIds],
})) as Array<StatsSection>;
const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({
id: id,
title: STATS_INTERVALS[id as StatsIntervalIds],
})) as Array<StatsInterval>;
type Props = {
section: StatsSectionIds;
onSectionChange: (newSection: StatsSectionIds) => void;
interval: StatsIntervalIds;
onIntervalChange: (newInterval: StatsIntervalIds) => void;
onFilterInputChange: (q: string) => void;
}
const StatsFilters = ({
section,
onSectionChange,
interval,
onIntervalChange,
onFilterInputChange,
}: Props) => {
return (
<Grid
gap={ 2 }
templateAreas={{
base: `"input input"
"section interval"`,
lg: `"input section interval"`,
}}
gridTemplateColumns={{ lg: '1fr auto auto' }}
>
<GridItem
w="100%"
area="input"
>
<FilterInput
onChange={ onFilterInputChange }
placeholder="Find chart, metric..."/>
</GridItem>
<GridItem
w={{ base: '100%', lg: 'auto' }}
area="section"
>
<StatsDropdownMenu
items={ sectionsList }
selectedId={ section }
onSelect={ onSectionChange }
/>
</GridItem>
<GridItem
w={{ base: '100%', lg: 'auto' }}
area="interval"
>
<StatsDropdownMenu
items={ intervalList }
selectedId={ interval }
onSelect={ onIntervalChange }
/>
</GridItem>
</Grid>
);
};
export default StatsFilters;
import { Grid, GridItem, Heading, List, ListItem } from '@chakra-ui/react';
import React from 'react';
import type { StatsSection } from 'types/client/stats';
import { apos } from 'lib/html-entities';
import EmptySearchResult from '../apps/EmptySearchResult';
import ChartWidget from './ChartWidget';
type Props = {
charts: Array<StatsSection>;
}
const WidgetsList = ({ charts }: Props) => {
const isAnyChartDisplayed = charts.some((section) => section.charts.some(chart => chart.visible));
return isAnyChartDisplayed ? (
<List>
{
charts.map((section) => (
<ListItem
display={ section.charts.every((chart) => !chart.visible) ? 'none' : 'block' }
key={ section.id }
mb={ 8 }
_last={{
marginBottom: 0,
}}
>
<Heading
size="md"
mb={ 4 }
>
{ section.title }
</Heading>
<Grid
templateColumns={{
sm: 'repeat(2, 1fr)',
}}
gap={ 4 }
>
{ section.charts.map((chart) => (
<GridItem
key={ chart.id }
display={ chart.visible ? 'block' : 'none' }
>
<ChartWidget
apiMethodURL={ chart.apiMethodURL }
title={ chart.title }
description={ chart.description }
/>
</GridItem>
)) }
</Grid>
</ListItem>
))
}
</List>
) : (
<EmptySearchResult text={ `Couldn${ apos }t find a chart that matches your filter query.` }/>
);
};
export default WidgetsList;
import type { StatsSection } from 'types/client/stats';
export const statsChartsScheme: Array<StatsSection> = [
{
id: 'blocks',
title: 'Blocks',
charts: [
{
id: 'new-blocks',
title: 'New blocks',
description: 'New blocks number per day',
apiMethodURL: '/node-api/stats/charts/transactions',
},
{
id: 'average-block-size',
title: 'Average block size',
description: 'Average size of blocks in bytes per day',
apiMethodURL: '/node-api/stats/charts/transactions',
},
],
},
{
id: 'transactions',
title: 'Transactions',
charts: [
{
id: 'transaction-fees',
title: 'Transaction fees',
description: 'Amount of tokens paid as fees per day',
apiMethodURL: '/node-api/stats/charts/transactions',
},
{
id: 'native-coin-holders-growth',
title: 'Native coin holders growth',
description: 'Total token holders number per day',
apiMethodURL: '/node-api/stats/charts/transactions',
},
],
},
];
import type { TimeChartItem } from 'ui/shared/chart/types';
export const demoData: Array<TimeChartItem> = [ { date: new Date('2022-10-17T00:00:00.000Z'), value: 432670 }, {
date: new Date('2022-10-18T00:00:00.000Z'),
value: 370100,
}, { date: new Date('2022-10-19T00:00:00.000Z'), value: 283234 }, { date: new Date('2022-10-20T00:00:00.000Z'), value: 420910 }, {
date: new Date('2022-10-21T00:00:00.000Z'),
value: 411988,
}, { date: new Date('2022-10-22T00:00:00.000Z'), value: 356269 }, { date: new Date('2022-10-23T00:00:00.000Z'), value: 389747 }, {
date: new Date('2022-10-24T00:00:00.000Z'),
value: 387130,
}, { date: new Date('2022-10-25T00:00:00.000Z'), value: 428785 }, { date: new Date('2022-10-26T00:00:00.000Z'), value: 63809 }, {
date: new Date('2022-10-27T00:00:00.000Z'),
value: 50518,
}, { date: new Date('2022-10-28T00:00:00.000Z'), value: 39087 }, { date: new Date('2022-10-29T00:00:00.000Z'), value: 36789 }, {
date: new Date('2022-10-30T00:00:00.000Z'),
value: 48569,
}, { date: new Date('2022-10-31T00:00:00.000Z'), value: 62519 }, { date: new Date('2022-11-01T00:00:00.000Z'), value: 152059 }, {
date: new Date('2022-11-02T00:00:00.000Z'),
value: 63743,
}, { date: new Date('2022-11-03T00:00:00.000Z'), value: 83667 }, { date: new Date('2022-11-04T00:00:00.000Z'), value: 91725 }, {
date: new Date('2022-11-05T00:00:00.000Z'),
value: 82897,
}, { date: new Date('2022-11-06T00:00:00.000Z'), value: 62477 }, { date: new Date('2022-11-07T00:00:00.000Z'), value: 58131 }, {
date: new Date('2022-11-08T00:00:00.000Z'),
value: 74197,
}, { date: new Date('2022-11-09T00:00:00.000Z'), value: 43691 }, { date: new Date('2022-11-10T00:00:00.000Z'), value: 92887 }, {
date: new Date('2022-11-11T00:00:00.000Z'),
value: 79493,
}, { date: new Date('2022-11-12T00:00:00.000Z'), value: 86764 }, { date: new Date('2022-11-13T00:00:00.000Z'), value: 22338 }, {
date: new Date('2022-11-14T00:00:00.000Z'),
value: 62266,
}, { date: new Date('2022-11-15T00:00:00.000Z'), value: 84084 }, { date: new Date('2022-11-16T00:00:00.000Z'), value: 75898 } ];
import type { StatsSectionIds, StatsIntervalIds } from 'types/client/stats';
export const STATS_SECTIONS: { [key in StatsSectionIds]: string } = {
all: 'All stats',
accounts: 'Accounts',
blocks: 'Blocks',
transactions: 'Transactions',
gas: 'Gas',
};
export const STATS_INTERVALS: { [key in StatsIntervalIds]: string } = {
all: 'All time',
oneMonth: '1 month',
threeMonths: '3 months',
sixMonths: '6 months',
oneYear: '1 year',
};
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react';
import type { StatsChart, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats';
import { statsChartsScheme } from './constants/charts-scheme';
function isSectionMatches(section: StatsSection, currentSection: StatsSectionIds): boolean {
return currentSection === 'all' || section.id === currentSection;
}
function isChartNameMatches(q: string, chart: StatsChart) {
return chart.title.toLowerCase().includes(q.toLowerCase());
}
export default function useStats() {
const [ isLoading, setIsLoading ] = useState(true);
const [ defaultCharts, setDefaultCharts ] = useState<Array<StatsSection>>();
const [ displayedCharts, setDisplayedCharts ] = useState<Array<StatsSection>>([]);
const [ section, setSection ] = useState<StatsSectionIds>('all');
const [ interval, setInterval ] = useState<StatsIntervalIds>('all');
const [ filterQuery, setFilterQuery ] = useState('');
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterCharts = useCallback(debounce(q => setFilterQuery(q), 500), []);
const filterCharts = useCallback((q: string, currentSection: StatsSectionIds) => {
const charts = defaultCharts
?.map((section: StatsSection) => {
const charts = section.charts.map((chart: StatsChart) => ({
...chart,
visible: isSectionMatches(section, currentSection) && isChartNameMatches(q, chart),
}));
return {
...section,
charts,
};
});
setDisplayedCharts(charts || []);
}, [ defaultCharts ]);
const handleSectionChange = useCallback((newSection: StatsSectionIds) => {
setSection(newSection);
}, []);
const handleIntervalChange = useCallback((newInterval: StatsIntervalIds) => {
setInterval(newInterval);
}, []);
useEffect(() => {
filterCharts(filterQuery, section);
}, [ filterQuery, section, filterCharts ]);
useEffect(() => {
setDefaultCharts(statsChartsScheme);
setDisplayedCharts(statsChartsScheme);
setIsLoading(false);
}, []);
return React.useMemo(() => ({
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
isLoading,
displayedCharts,
}), [
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
displayedCharts,
isLoading,
]);
}
......@@ -4,7 +4,7 @@ import React from 'react';
import nftIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenSnippet from 'ui/shared/TokenSnippet';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
interface Props {
value: string;
......@@ -26,7 +26,7 @@ const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props)
<Link href={ url } fontWeight={ 600 }>{ tokenId }</Link>
</Box>
{ name ? (
<TokenSnippet symbol={ symbol } hash={ hash } name={ name }/>
<TokenSnippet symbol={ symbol } hash={ hash } name={ name } w="auto"/>
) : (
<AddressLink hash={ hash } truncation="constant" type="token"/>
) }
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as mocks from 'mocks/txs/decodedInputData';
import TestApp from 'playwright/TestApp';
import TxDecodedInputData from './TxDecodedInputData';
test('with indexed fields +@mobile +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<TxDecodedInputData data={ mocks.withIndexedFields }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('without indexed fields +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<TxDecodedInputData data={ mocks.withoutIndexedFields }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import TxDetails from './TxDetails';
const API_URL = '/node-api/transactions/1';
const hooksConfig = {
router: {
query: { id: 1 },
},
};
test('between addresses +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.base),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await page.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
test('creating contact', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withContractCreation),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('with token transfer +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withTokenTransfer),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('with decoded revert reason', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withDecodedRevertReason),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('with decoded raw reason', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withRawRevertReason),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('pending', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.pending),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL);
await page.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
......@@ -27,7 +27,7 @@ import Utilization from 'ui/shared/Utilization/Utilization';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData/TxDecodedInputData';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
......@@ -58,7 +58,7 @@ const TxDetails = () => {
...data.from.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const toAddress = data.to && data.to.hash ? data.to : data.created_contract;
const toAddress = data.to ? data.to : data.created_contract;
const addressToTags = [
...toAddress.private_tags || [],
...toAddress.public_tags || [],
......@@ -156,7 +156,7 @@ const TxDetails = () => {
<CopyToClipboard text={ toAddress.hash }/>
</Address>
) : (
<Flex width="100%" whiteSpace="pre">
<Flex width={{ base: '100%', lg: 'auto' }} whiteSpace="pre">
<span>[Contract </span>
<AddressLink hash={ toAddress.hash }/>
<span> created]</span>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import TxInternals from './TxInternals';
const TX_HASH = txMock.base.hash;
const API_URL_TX = `/node-api/transactions/${ TX_HASH }`;
const API_URL_TX_INTERNALS = `/node-api/transactions/${ TX_HASH }/internal-transactions`;
const hooksConfig = {
router: {
query: { id: TX_HASH },
},
};
test('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL_TX, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.base),
}));
await page.route(API_URL_TX_INTERNALS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(internalTxsMock.baseResponse),
}));
const component = await mount(
<TestApp>
<TxInternals/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL_TX),
await page.waitForResponse(API_URL_TX_INTERNALS),
await expect(component).toHaveScreenshot();
});
......@@ -7,7 +7,7 @@ import rightArrowIcon from 'icons/arrows/east.svg';
import { space } from 'lib/html-entities';
import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenSnippet from 'ui/shared/TokenSnippet';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
type Props = TTokenTransfer;
......@@ -34,6 +34,7 @@ const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => {
const payload = total as Erc721TotalPayload;
return (
<NftTokenTransferSnippet
name={ token.name }
tokenId={ payload.token_id }
value="1"
hash={ token.address }
......@@ -47,6 +48,7 @@ const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => {
const items = Array.isArray(payload) ? payload : [ payload ];
return items.map((item) => (
<NftTokenTransferSnippet
name={ token.name }
key={ item.token_id }
tokenId={ item.token_id }
value={ item.value }
......
......@@ -3,7 +3,8 @@ import React from 'react';
import type { TransactionRevertReason } from 'types/api/transaction';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import hexToUtf8 from 'lib/hexToUtf8';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData/TxDecodedInputData';
type Props = TransactionRevertReason;
......@@ -25,7 +26,7 @@ const TxRevertReason = (props: Props) => {
<GridItem fontWeight={ 500 }>Raw:</GridItem>
<GridItem>{ props.raw }</GridItem>
<GridItem fontWeight={ 500 }>Decoded:</GridItem>
<GridItem>{ props.decoded }</GridItem>
<GridItem>{ hexToUtf8(props.raw) }</GridItem>
</Grid>
);
}
......
......@@ -17,7 +17,7 @@ type Props = InternalTransaction;
const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: gasLimit, created_contract: createdContract }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to && to.hash ? to : createdContract;
const toData = to ? to : createdContract;
return (
<AccountListItemMobile rowGap={ 3 }>
......@@ -38,7 +38,9 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit:
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text>
<Text fontSize="sm" variant="secondary">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Text>
</HStack>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text>
......
......@@ -4,6 +4,7 @@ import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -15,7 +16,7 @@ type Props = InternalTransaction
const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: gasLimit, created_contract: createdContract }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to && to.hash ? to : createdContract;
const toData = to ? to : createdContract;
return (
<Tr alignItems="top">
......@@ -45,7 +46,7 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit:
</Address>
</Td>
<Td isNumeric verticalAlign="middle">
{ value }
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Td>
<Td isNumeric verticalAlign="middle">
{ BigNumber(gasLimit).toFormat() }
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as addressMocks from 'mocks/address/address';
import * as inputDataMocks from 'mocks/txs/decodedInputData';
import TestApp from 'playwright/TestApp';
import TxLogItem from './TxLogItem';
const TOPICS = [
'0x3a4ec416703c36a61a4b1f690847f1963a6829eac0b52debd40a23b66c142a56',
'0x0000000000000000000000000000000000000000000000000000000005001bcf',
'0xe835d1028984e9e6e7d016b77164eacbcc6cc061e9333c0b37982b504f7ea791',
null,
];
const DATA = '0x0000000000000000000000000000000000000000000000000070265bf0112cee';
test('with decoded input data +@mobile +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<TxLogItem
index={ 42 }
decoded={ inputDataMocks.withIndexedFields }
address={ addressMocks.withName }
topics={ TOPICS }
data={ DATA }
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('without decoded input data +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<TxLogItem
index={ 42 }
decoded={ null }
address={ addressMocks.withoutName }
topics={ TOPICS }
data={ DATA }
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
......@@ -6,11 +6,12 @@ import type { Log } from 'types/api/log';
// import searchIcon from 'icons/search.svg';
import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxLogTopic from 'ui/tx/logs/TxLogTopic';
import DecodedInputData from 'ui/tx/TxDecodedInputData';
import DecodedInputData from 'ui/tx/TxDecodedInputData/TxDecodedInputData';
type Props = Log;
......@@ -74,7 +75,7 @@ const TxLogItem = ({ address, index, topics, data, decoded }: Props) => {
) }
<RowHeader>Topics</RowHeader>
<GridItem>
{ topics.filter(Boolean).map((item, index) => (
{ topics.filter(notEmpty).map((item, index) => (
<TxLogTopic
key={ index }
hex={ item }
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import TxLogTopic from './TxLogTopic';
test('address view +@mobile -@default', async({ mount }) => {
const component = await mount(
<TestApp>
<TxLogTopic hex="0x000000000000000000000000d789a607ceac2f0e14867de4eb15b15c9ffb5859" index={ 42 }/>
</TestApp>,
);
await component.locator('select[aria-label="Data type"]').selectOption('address');
await expect(component).toHaveScreenshot();
});
test('hex view +@mobile -@default', async({ mount }) => {
const component = await mount(
<TestApp>
<TxLogTopic hex="0x000000000000000000000000d789a607ceac2f0e14867de4eb15b15c9ffb5859" index={ 42 }/>
</TestApp>,
);
await component.locator('select[aria-label="Data type"]').selectOption('hex');
await expect(component).toHaveScreenshot();
});
......@@ -74,6 +74,7 @@ const TxLogTopic = ({ hex, index }: Props) => {
mr={ 3 }
flexShrink={ 0 }
w="auto"
aria-label="Data type"
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ capitalize(option) }</option>) }
</Select>
......
......@@ -28,7 +28,7 @@ const TxAdditionalInfo = ({ tx }: { tx: Transaction }) => {
return (
<>
<Heading as="h4" fontSize="18px" mb={ 6 }>Additional info </Heading>
<Heading as="h4" size="sm" mb={ 6 }>Additional info </Heading>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Transaction fee</Text>
<Flex>
......
......@@ -18,7 +18,7 @@ const TxType = ({ types }: Props) => {
switch (typeToShow) {
case 'contract_call':
label = 'Contract call';
colorScheme = 'blue';
colorScheme = 'green';
break;
case 'contract_creation':
label = 'Contract creation';
......@@ -36,6 +36,10 @@ const TxType = ({ types }: Props) => {
label = 'Coin transfer';
colorScheme = 'teal';
break;
default:
label = 'Transaction';
colorScheme = 'blue';
}
return (
......
......@@ -33,7 +33,7 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo
const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const dataTo = tx.to ? tx.to : tx.created_contract;
return (
<>
......
......@@ -21,7 +21,7 @@ type Props = {
const TxsTable = ({ txs, sort, sorting, top, showBlockInfo }: Props) => {
return (
<Table variant="simple" minWidth="810px" size="xs">
<Table variant="simple" minWidth="950px" size="xs">
<TheadSticky top={ top }>
<Tr>
<Th width="54px"></Th>
......
......@@ -44,7 +44,7 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
</Address>
);
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const dataTo = tx.to ? tx.to : tx.created_contract;
const addressTo = (
<Address>
......
......@@ -73,7 +73,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
notificationsDefault = data.notification_settings;
}
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
defaultValues: {
address: data?.address_hash || '',
tag: data?.name || '',
......@@ -191,7 +191,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg"
type="submit"
isLoading={ pending }
disabled={ !isValid }
disabled={ !isValid || !isDirty }
>
{ data ? 'Save changes' : 'Add address' }
</Button>
......
......@@ -35,7 +35,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderModalContent = useCallback(() => {
const addressString = isMobile ? [ address.slice(0, 4), address.slice(-4) ].join('...') : address;
return (
<Text>Address <Text fontWeight="600" as="span"> { addressString || 'address' }</Text> will be deleted</Text>
<Text>Address <Text fontWeight="700" as="span"> { addressString || 'address' }</Text> will be deleted</Text>
);
}, [ address, isMobile ]);
......
import { HStack, VStack, Text, Icon, useColorModeValue, Flex } from '@chakra-ui/react';
import { HStack, VStack, Text, Icon, Flex } from '@chakra-ui/react';
import React from 'react';
import type { TWatchlistItem } from 'types/client/account';
......@@ -12,12 +12,10 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenLogo from 'ui/shared/TokenLogo';
const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
const mainTextColor = useColorModeValue('gray.700', 'gray.50');
const infoItemsPaddingLeft = { base: 0, lg: 8 };
const infoItemsPaddingLeft = { base: 1, lg: 8 };
return (
<VStack spacing={ 2 } align="stretch" fontWeight={ 500 } color="gray.700">
<VStack spacing={ 2 } align="stretch" fontWeight={ 500 }>
<AddressSnippet address={ item.address_hash }/>
<Flex fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft } flexWrap="wrap" alignItems="center" rowGap={ 1 }>
{ appConfig.network.currency.address && (
......@@ -40,8 +38,8 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
</Flex>
{ item.tokens_count && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<Icon as={ TokensIcon } marginRight="10px" w="17px" h="16px"/>
<Text color={ mainTextColor }>{ `Tokens:${ nbsp }` + item.tokens_count }</Text>
<Icon as={ TokensIcon } mr={ 2 } w="17px" h="16px"/>
<Text>{ `Tokens:${ nbsp }` + item.tokens_count }</Text>
{ /* api does not provide token prices */ }
{ /* <Text variant="secondary">{ `${ nbsp }($${ item.tokensUSD } USD)` }</Text> */ }
<Text variant="secondary">{ `${ nbsp }(N/A)` }</Text>
......@@ -50,8 +48,8 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
{ /* api does not provide token prices */ }
{ /* { item.address_balance && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<Icon as={ WalletIcon } marginRight="10px" w="16px" h="16px"/>
<Text color={ mainTextColor }>{ `Net worth:${ nbsp }` }</Text>
<Icon as={ WalletIcon } mr={ 2 } w="16px" h="16px"/>
<Text>{ `Net worth:${ nbsp }` }</Text>
<Link href="#">{ `$${ item.totalUSD } USD` }</Link>
</HStack>
) } */ }
......
......@@ -2776,6 +2776,15 @@
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@rollup/pluginutils@^5.0.0":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33"
integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==
dependencies:
"@types/estree" "^1.0.0"
estree-walker "^2.0.2"
picomatch "^2.3.1"
"@rushstack/eslint-patch@^1.1.3":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728"
......@@ -2968,7 +2977,7 @@
"@svgr/babel-plugin-transform-react-native-svg" "^6.5.1"
"@svgr/babel-plugin-transform-svg-component" "^6.5.1"
"@svgr/core@^6.5.1":
"@svgr/core@^6.4.0", "@svgr/core@^6.5.1":
version "6.5.1"
resolved "https://registry.yarnpkg.com/@svgr/core/-/core-6.5.1.tgz#d3e8aa9dbe3fbd747f9ee4282c1c77a27410488a"
integrity sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==
......@@ -3357,6 +3366,11 @@
"@types/d3-transition" "*"
"@types/d3-zoom" "*"
"@types/estree@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
"@types/geojson@*":
version "7946.0.10"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249"
......@@ -3511,6 +3525,13 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
"@types/ws@^8.5.3":
version "8.5.3"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==
dependencies:
"@types/node" "*"
"@types/yargs-parser@*":
version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
......@@ -5378,7 +5399,7 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-walker@^2.0.1:
estree-walker@^2.0.1, estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
......@@ -8782,6 +8803,14 @@ v8-to-istanbul@^9.0.1:
"@types/istanbul-lib-coverage" "^2.0.1"
convert-source-map "^1.6.0"
vite-plugin-svgr@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/vite-plugin-svgr/-/vite-plugin-svgr-2.2.2.tgz#c5c9cb573bf455bb079550531847ddc5d2e122af"
integrity sha512-u8Ac27uZmDHTVGawpAhvLMJMuzbGeZGhe61TGeHoRQLxVhmQfIYCefa0iLbjC0ui1zFo6XZnS8EkzPITCYp85g==
dependencies:
"@rollup/pluginutils" "^5.0.0"
"@svgr/core" "^6.4.0"
vite-tsconfig-paths@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-3.5.2.tgz#fd3232f93c426311d7e0d581187d8b63fff55fbc"
......@@ -8949,7 +8978,7 @@ ws@7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
ws@^8.9.0:
ws@^8.11.0, ws@^8.9.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
......
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