Commit 96e2058c authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into invalid-api-token

parents 1e65df56 a85dd443
...@@ -30,6 +30,7 @@ NEXT_PUBLIC_MARKETPLACE_APP_LIST=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_APP_L ...@@ -30,6 +30,7 @@ NEXT_PUBLIC_MARKETPLACE_APP_LIST=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_APP_L
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM__ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM__
NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_URL__ NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_URL__
NEXT_PUBLIC_LOGOUT_RETURN_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_RETURN_URL__ NEXT_PUBLIC_LOGOUT_RETURN_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_RETURN_URL__
NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__
# api config # api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__ NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
......
...@@ -2,8 +2,8 @@ name: Run E2E tests k8s ...@@ -2,8 +2,8 @@ name: Run E2E tests k8s
on: on:
# push: # push:
# pull_request: pull_request:
workflow_dispatch workflow_dispatch:
env: env:
K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }} K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }}
......
...@@ -3,8 +3,6 @@ name: Deploy review environment ...@@ -3,8 +3,6 @@ name: Deploy review environment
on: on:
pull_request: pull_request:
# push: # push:
# branches-ignore:
# - 'main'
workflow_dispatch: workflow_dispatch:
env: env:
......
...@@ -70,6 +70,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -70,6 +70,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` *(optional)* | Verification type in the network | `mining` | | NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` *(optional)* | Verification type in the network | `mining` |
| NEXT_PUBLIC_LOGOUT_URL | `string` *(optional)* | Account logout url | `https://blockscoutcom.us.auth0.com/v2/logout` | | NEXT_PUBLIC_LOGOUT_URL | `string` *(optional)* | Account logout url | `https://blockscoutcom.us.auth0.com/v2/logout` |
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` *(optional)* | Account logout return url | `https://blockscout.com/poa/core/auth/logout` | | NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` *(optional)* | Account logout return url | `https://blockscout.com/poa/core/auth/logout` |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` *(optional)* | List of charts displayed on the home page | `['daily_txs','coin_price','market_cup']` |
### App configuration ### App configuration
......
/* eslint-disable no-restricted-properties */ /* eslint-disable no-restricted-properties */
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import type { FeaturedNetwork, NetworkExplorer, PreDefinedNetwork } from 'types/networks'; import type { FeaturedNetwork, NetworkExplorer, PreDefinedNetwork } from 'types/networks';
import type { ChainIndicatorId } from 'ui/home/indicators/types';
const getEnvValue = (env: string | undefined) => env?.replaceAll('\'', '"'); const getEnvValue = (env: string | undefined) => env?.replaceAll('\'', '"');
const parseEnvJson = <DataType>(env: string | undefined): DataType | null => { const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
...@@ -86,6 +87,9 @@ const config = Object.freeze({ ...@@ -86,6 +87,9 @@ const config = Object.freeze({
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com', socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''), basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
}, },
homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [],
},
}); });
export default config; export default config;
# ui config
NEXT_PUBLIC_FEATURED_NETWORKS=[{'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_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx'}}]
# network config
NEXT_PUBLIC_NETWORK_NAME=Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=ethereum
NEXT_PUBLIC_NETWORK_TYPE=goerli
NEXT_PUBLIC_NETWORK_ID=420
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_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/'}]
# api config
NEXT_PUBLIC_API_BASE_PATH=/eth/goerli
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=[{'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_FEATURED_NETWORKS=[{'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_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=POA NEXT_PUBLIC_NETWORK_NAME=POA
......
...@@ -8,18 +8,6 @@ blockscout: ...@@ -8,18 +8,6 @@ blockscout:
_default: ENC[AES256_GCM,data:+q7GvdSeRQ0aY4grlQrdWUvxT9tzsRoii3W/ECwpVKs13lw84bGV+rbiEJXBYoOAw7SDz1Hr+KMy9SQzHvj12w==,iv:Xjo9+Nca6eDTZtUcDPAPfXYSAoP2gYmDX+JanCyPPkY=,tag:kQxLFsfN5YXft8ihgNvA+Q==,type:str] _default: ENC[AES256_GCM,data:+q7GvdSeRQ0aY4grlQrdWUvxT9tzsRoii3W/ECwpVKs13lw84bGV+rbiEJXBYoOAw7SDz1Hr+KMy9SQzHvj12w==,iv:Xjo9+Nca6eDTZtUcDPAPfXYSAoP2gYmDX+JanCyPPkY=,tag:kQxLFsfN5YXft8ihgNvA+Q==,type:str]
MAILSLURP_EMAIL_ID: MAILSLURP_EMAIL_ID:
_default: ENC[AES256_GCM,data:tGjukx3Q+NaWpvfXPXmY6dphdEdbO531q4NMqsOa4XCudfpt,iv:6S2jTCoJCfhkRHg4aA2UR1U4fMsDhNV44+r7ofnHLTA=,tag:nr+0zaM45+UBUwOpmVD1hg==,type:str] _default: ENC[AES256_GCM,data:tGjukx3Q+NaWpvfXPXmY6dphdEdbO531q4NMqsOa4XCudfpt,iv:6S2jTCoJCfhkRHg4aA2UR1U4fMsDhNV44+r7ofnHLTA=,tag:nr+0zaM45+UBUwOpmVD1hg==,type:str]
ACCOUNT_AUTH0_DOMAIN:
_default: ENC[AES256_GCM,data:C6TAdc/RZ+qFOan+tWnTsfIDcYgxCNjuKOU=,iv:tEcHTumHxA9LreqLSwjcTTCP3igk/VQrJn/OWd+YQ4c=,tag:XOlWi3XHBZyENtLsv3afmw==,type:str]
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:lojxfDVJBi1Sc6KCL28tln6RJ5leg0jmaTbSHbBd1H4=,iv:l1Iq/YsZKkFuorQgKd+KzAIkqbXHv6I4eid+7LDnyOA=,tag:/c7GBciDCruz+dAOfxncww==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:v6LJLRwibjc6QgF1t6QNzy/FfL/i2G0mD2X6oxf0o4Q6A8cwovE03Wd5nXWTSyWrN95rJm0IQY7bjU3fXpDw0p/u4vu8dMpfM2bZMMOalXP4pA==,iv:VtNoyCEsesWn0PyWRXzLgbMcUa+voKa1sjsUY26RKnw=,tag:t02c9NVe4aGGo5feOP1OWQ==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:JQylcEDhJUOqXqBCycJ8XLwz6wpa3Uz3p5MhCEVolLnKkYoDFogGuFt1YkKt7EEtbZjqvSjIa0xQhpezX5hvypU9KxRxoVosUQ4=,iv:dbXG4N4t5jQgx68l0r5iLh5FGGg2O/vqbSDpyxzEauI=,tag:upkfr/3h35rZ93oBNJ3Y/w==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str]
ACCOUNT_SENDGRID_API_KEY: ACCOUNT_SENDGRID_API_KEY:
_default: ENC[AES256_GCM,data:bNzimXY1TDUHgLijvgcJ8mw+9RXvCLchxhum77KY6ss5NUdoWuNuQjpC7M819XRPqhBmZkC2qBSX1ZRhcNQCB1OqGkRq,iv:JFmeCxsO2lkyScN8iVb8B/356SbP7KCMFffIPjGtN0o=,tag:A21hzDObJMAUhHQRWB7Z9w==,type:str] _default: ENC[AES256_GCM,data:bNzimXY1TDUHgLijvgcJ8mw+9RXvCLchxhum77KY6ss5NUdoWuNuQjpC7M819XRPqhBmZkC2qBSX1ZRhcNQCB1OqGkRq,iv:JFmeCxsO2lkyScN8iVb8B/356SbP7KCMFffIPjGtN0o=,tag:A21hzDObJMAUhHQRWB7Z9w==,type:str]
ACCOUNT_SENDGRID_SENDER: ACCOUNT_SENDGRID_SENDER:
...@@ -39,9 +27,21 @@ blockscout: ...@@ -39,9 +27,21 @@ blockscout:
DATABASE_URL: DATABASE_URL:
_default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str] _default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str]
ACCOUNT_DATABASE_URL: ACCOUNT_DATABASE_URL:
_default: ENC[AES256_GCM,data:48q2fFA3s1HGSm7YB5QV8B0eiI2PZHPgOsUS84R8x+8GecrFTenifbcHD6AivFNKovLuClUy2aFuEtEvJDOfelY=,iv:5ZLl62Yj/AXwyg+pdBLUv3EyYXSTOiSCqGYOv3rBx94=,tag:FUiy0haST4mdrqe2VlvrLg==,type:str] _default: ENC[AES256_GCM,data:HF5y8ezV5TiLqeh98WDp4rXQeUfSBETyWVHOyNZNy5pt4MdiKTdFBLauOJpD6YHWynMFsd8IJLRNLrBn4qGe3RfwprR6v3WN9Q==,iv:B9AJXO7EJexsPgDHb5s5tzpadVYoZ79fyaL8NOYXSEw=,tag:lyNKQ13Q+pn+3DrAXIzIKQ==,type:str]
ACCOUNT_REDIS_URL: ACCOUNT_REDIS_URL:
_default: ENC[AES256_GCM,data:MtAI4U/4mLdIW0d4ap5+0wTufO7hvOD/ETPnqpGJ7tGFXmirK2w/M5UdQd8M/EsH/4zk3rlM4WDGRVwMzCTiugG1FxEdIcKOA6127jXgSUrE,iv:zgrAoj7437b0TJTstU0S7UVHGt4fKnmP3wlc2qbURa0=,tag:vT3NL8PLLxB9z/dmkE/jaw==,type:str] _default: ENC[AES256_GCM,data:MtAI4U/4mLdIW0d4ap5+0wTufO7hvOD/ETPnqpGJ7tGFXmirK2w/M5UdQd8M/EsH/4zk3rlM4WDGRVwMzCTiugG1FxEdIcKOA6127jXgSUrE,iv:zgrAoj7437b0TJTstU0S7UVHGt4fKnmP3wlc2qbURa0=,tag:vT3NL8PLLxB9z/dmkE/jaw==,type:str]
ACCOUNT_AUTH0_DOMAIN:
_default: ENC[AES256_GCM,data:zRZL/kIX2R4AbanIjN7s/qFu5od1onFhYBg=,iv:4OSMUjvxUHevuXSNAnTj/DTFoZA5mC4UIerGiIgDwQc=,tag:J5I4CC9AIVXDmhkXG4X5wQ==,type:str]
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:iYnptgxESZs216/m0ArCciXfirPTxVjt5urKTATyYv3uadokC54ofzMZagG/ZVvNY48+Uxh4YGzwySyLOOM0SA==,iv:UQoZf3tesEjAJ7E5YruoZmVclsYfS5EmSo4z90wfHfE=,tag:er+hARrkCrjoQugqvRr4ow==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:mTY6sjNKZ+VEvH47eyyoUHt//beWvuxyreu+1WrmMvkdkwR6jgXnSXh+1pTuiG8e3it96Egxraz0hjZxlkY9kSmE91/dfoqKTns=,iv:op8OuOXXeBmmUnVkCL14j4HrfjHHoN3n2XZqLdg03Mw=,tag:bbFQYi0Aa1sUdfoUjuQ/+w==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:IZFfi6pn+hy7g0wnEtP9TYHH1fNiC2gqgRHVdgm4C9smPerEvS0pq9dBwVY=,iv:BxbSInFQ6GE2loTv+IzdYr25PlyzdWZI1wdT6r+uvBg=,tag:psujCU372k59OGmlRdH9Fg==,type:str]
scVerifier: scVerifier:
environment: environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY: SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
...@@ -57,6 +57,12 @@ postgres: ...@@ -57,6 +57,12 @@ postgres:
POSTGRES_DB: POSTGRES_DB:
_default: ENC[AES256_GCM,data:QRCvQVzKOfOApg==,iv:eOdxyjmv6Zj5xINGXO0dD/GcDgD1J8e4OzLMyOneoDs=,tag:EBnJ7OT3u7vcBIbviRKaHg==,type:str] _default: ENC[AES256_GCM,data:QRCvQVzKOfOApg==,iv:eOdxyjmv6Zj5xINGXO0dD/GcDgD1J8e4OzLMyOneoDs=,tag:EBnJ7OT3u7vcBIbviRKaHg==,type:str]
geth: geth:
jwt:
token: ENC[AES256_GCM,data:+E6k/3vTav9yqAbOStmC5Rwx9mV80l8iAqQYGWJNZFTUfqXQKNavaVJW7AfzixZC4zaj8c0dbdsj1bv9rlJljG0q,iv:zW9spad1AdbkAjYQYAAflzsNhaH+LAIkjHpvIPHVGbs=,tag:oqvgkqVroMxSzwmna9ra5w==,type:str]
client:
environment:
JWT_TOKEN:
_default: ENC[AES256_GCM,data:T46y6KmBcEslaodBMRTNZya51uQiK4I3olCvdxCw3qZlgoOxRUvSgKLLoRNyD/d6w6PwgJA4oU7N5hmNGQ+I26Vc,iv:F+evxDb/sn6raub1LjZPGk2VFoQBVuBvkpgJ2YCJjw8=,tag:K0QV4weIEcdy1AXcpHSxQQ==,type:str]
files: files:
list: list:
genesis.json: ENC[AES256_GCM,data:nSFncQhigaTtb7YSeXiUvua2LTOGlluVDywxQR5NnhvIBZCZniIxHsEhXKF9rBYceOMfBPKY5MTLIyxYisZeaJHL71A/uXQerDEKkECpzhwUSTdNEA9gUEqmpUjf7ChAWjybWw6NCM2jVhG3bnZwTwmuzeCSek85q4J503j9HVEkGXAU52A2+yoov41c+xoCEOj7W6Ka/f7yJYkyppoeS+4AIxBFcJ5hI0pIf1VqDcSrJf68JO1pPzcmp9eK3CK2CIpD8uxV6CseBFEqlbllmgAVwSUJ4i8XhYEO2hMyULm/LkW52U31/tq89yctSkp5TB62sZEHyDxXcq6jFSqd4840o9VedyX4YuEVtLBlXdzerc4Q2tHORQv5u2BLW7ktA/5vj8d9XnblQBDKrqxtUEgvSj9T/Xm4zyKjIhJBAAwUd5rF4j8dNbUGpsBm3G/Lx1+DqrS8cyxXgumtmfsklEVTWBra2dwt/tlTpF+utOwT7KtqXo2dVNvL6tUWrWGQqCt+fgeRKw8NuHWgU9Nm6baQ0yhWIM3XaCEuuJqHLdelYgQiKVEaTvlw7my7XwQsunJCQ98Q2eO61v7WFsg1m5s3Wyts8R2EnMWrnDlGro//eUHFlxDiIQGXmWgnz597lrLTXiuZKNI4pvPsAggBQxIeUcYzICy5uLRPWAiDpfshYt/fRQhIxmrVaJCufJrq4IcUdJYKjAcpnGHqZMB6quZiUPCPYg5Lvz7w1zoIVLn4ANWVuh7J1husNH13SwrDUXEm+k/WYCkDZm5B/K9IzkihY8nNXbvxHEXUClpb943Xw8hpvZZj0BfP/ye7ywUzHGlo3OmFAdE7Fbjc9eELxgYirONoaHy9GkjWPjinTVVlvitakqzx4dGJ+U2adSB01WAj63rn4ZidaC/eUpcPe7E2OeqmpkLc/kLM9g3gEe/MCiqsXXUYtm0Idpbrf+rRTAotoVOi3fKr1inlJY6n+9UzKAxNkTIkiGUQ1E9g0s3Kn4Z83apuy5+Us8gA9xNfWNGcOO105ZUYKyow4Xb5ggV8t+UTXPOZQp97Qz/6q0VC2kY9bx991SrJLoaK+ChLQ0/t/LcLjZxKRPDU2YGOfjrPDO3iscDkZgX/8IvH56jxUXAYmNrxOgbb9encYIrMDYrsKTaj5kXETp2M3sTdHwk6d78PAteYCvgQb44gu5JtknIq2LLXb/2Ue45O4HnrB+AsICPb+oGAxIqFkHvWuOIRZLqap8wbazszBlqsrX8ABegjvReXiG0rX1VeQIXNcG3/tKPHNKrDlFJykaiC1w==,iv:pyrxyy1bbv1UiGZ2y1jer9UqOwVkZGb4GwawJoCGJuM=,tag:aVt5dASuZglsUs9Yydzjaw==,type:str] genesis.json: ENC[AES256_GCM,data:nSFncQhigaTtb7YSeXiUvua2LTOGlluVDywxQR5NnhvIBZCZniIxHsEhXKF9rBYceOMfBPKY5MTLIyxYisZeaJHL71A/uXQerDEKkECpzhwUSTdNEA9gUEqmpUjf7ChAWjybWw6NCM2jVhG3bnZwTwmuzeCSek85q4J503j9HVEkGXAU52A2+yoov41c+xoCEOj7W6Ka/f7yJYkyppoeS+4AIxBFcJ5hI0pIf1VqDcSrJf68JO1pPzcmp9eK3CK2CIpD8uxV6CseBFEqlbllmgAVwSUJ4i8XhYEO2hMyULm/LkW52U31/tq89yctSkp5TB62sZEHyDxXcq6jFSqd4840o9VedyX4YuEVtLBlXdzerc4Q2tHORQv5u2BLW7ktA/5vj8d9XnblQBDKrqxtUEgvSj9T/Xm4zyKjIhJBAAwUd5rF4j8dNbUGpsBm3G/Lx1+DqrS8cyxXgumtmfsklEVTWBra2dwt/tlTpF+utOwT7KtqXo2dVNvL6tUWrWGQqCt+fgeRKw8NuHWgU9Nm6baQ0yhWIM3XaCEuuJqHLdelYgQiKVEaTvlw7my7XwQsunJCQ98Q2eO61v7WFsg1m5s3Wyts8R2EnMWrnDlGro//eUHFlxDiIQGXmWgnz597lrLTXiuZKNI4pvPsAggBQxIeUcYzICy5uLRPWAiDpfshYt/fRQhIxmrVaJCufJrq4IcUdJYKjAcpnGHqZMB6quZiUPCPYg5Lvz7w1zoIVLn4ANWVuh7J1husNH13SwrDUXEm+k/WYCkDZm5B/K9IzkihY8nNXbvxHEXUClpb943Xw8hpvZZj0BfP/ye7ywUzHGlo3OmFAdE7Fbjc9eELxgYirONoaHy9GkjWPjinTVVlvitakqzx4dGJ+U2adSB01WAj63rn4ZidaC/eUpcPe7E2OeqmpkLc/kLM9g3gEe/MCiqsXXUYtm0Idpbrf+rRTAotoVOi3fKr1inlJY6n+9UzKAxNkTIkiGUQ1E9g0s3Kn4Z83apuy5+Us8gA9xNfWNGcOO105ZUYKyow4Xb5ggV8t+UTXPOZQp97Qz/6q0VC2kY9bx991SrJLoaK+ChLQ0/t/LcLjZxKRPDU2YGOfjrPDO3iscDkZgX/8IvH56jxUXAYmNrxOgbb9encYIrMDYrsKTaj5kXETp2M3sTdHwk6d78PAteYCvgQb44gu5JtknIq2LLXb/2Ue45O4HnrB+AsICPb+oGAxIqFkHvWuOIRZLqap8wbazszBlqsrX8ABegjvReXiG0rX1VeQIXNcG3/tKPHNKrDlFJykaiC1w==,iv:pyrxyy1bbv1UiGZ2y1jer9UqOwVkZGb4GwawJoCGJuM=,tag:aVt5dASuZglsUs9Yydzjaw==,type:str]
...@@ -65,7 +71,7 @@ geth: ...@@ -65,7 +71,7 @@ geth:
frontend: frontend:
environment: environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS: NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,type:str] _default: ENC[AES256_GCM,data:D/pLeRn7C40/rc7nNDCuK5dTuADPrHZl+J8hGC9xhwNRKZnkHySxkSzV,iv:z98n49jakK1mvlJpJ75BXj5gGyDHpBcAoCbaM0FmEA8=,tag:pLTa5kHmSi+bCN+8jeMhGQ==,type:str]
NEXT_PUBLIC_SENTRY_DSN: NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str] _default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI: SENTRY_CSP_REPORT_URI:
...@@ -78,8 +84,8 @@ sops: ...@@ -78,8 +84,8 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2022-11-02T07:59:42Z" lastmodified: "2022-11-14T10:04:29Z"
mac: ENC[AES256_GCM,data:OrV/dUWOtL23UFQLeIsKsGluTmse42d/4sgFMDs3UXdACsZu8twMt29Y/WaPHyq8Tpn5iYzhBLU6SCUmHxEhBNVzKBd5uCUbav1faS/zW6fSd9bEP7rmbUjaJGHliBkG3T4VCSZn53jR/OMNbSynIxZ0kRpVHr+RTcalaH7dLQ8=,iv:J3gXjFgFyZoPqL+VEnjkuKzA9UIIyK3UsvPWacBZKsY=,tag:/zY1jVjIm5BsDNJlfsg5uw==,type:str] mac: ENC[AES256_GCM,data:LitRw7GOBq9mHXXiWmfmcpw0Tbn8KESGZjFjDAlYWIlYZWVzxIeApXWNin/HoFrwIwnmpn2bpyjG1yWu3FR/3CLkkekUSsZ7k4P7kBlspQyUNSqFxWfDM+2qwpJ7qUNfQ05oGJkceLTsYec5kH2Xe39fs8RQife5uoord1FkW7A=,iv:M4nSUT77FUCCTm72yteJCZNJivYCs0LLr2irRGgrq3c=,tag:HaMxRKNEgNmB3sIw93yCMw==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
...@@ -62,58 +62,84 @@ blockscout: ...@@ -62,58 +62,84 @@ blockscout:
app: blockscout app: blockscout
# Blockscout environment variables # Blockscout environment variables
environment: environment:
ENV:
_default: test
RESOURCE_MODE:
_default: account
PUBLIC:
_default: 'false'
PORT:
_default: 4000
PORT_PG:
_default: 5432
PORT_NETWORK_HTTP:
_default: 8545
PORT_NETWORK_WS:
_default: 8546
ETHEREUM_JSONRPC_VARIANT:
_default: geth
ETHEREUM_JSONRPC_TRACE_URL: ETHEREUM_JSONRPC_TRACE_URL:
_default: http://geth-svc:8545 _default: http://geth-svc.eth-goerli.svc.cluster.local:8545
ETHEREUM_JSONRPC_HTTP_URL: ETHEREUM_JSONRPC_HTTP_URL:
_default: http://geth-svc:8545 _default: http://geth-svc.eth-goerli.svc.cluster.local:8545
ETHEREUM_JSONRPC_WS_URL: ETHEREUM_JSONRPC_WS_URL:
_default: ws://geth-svc:8546 _default: ws://geth-svc.eth-goerli.svc.cluster.local:8546
COIN: BLOCKSCOUT_VERSION:
_default: DAI _default: v4.1.8-beta
MIX_ENV:
_default: prod
ECTO_USE_SSL: ECTO_USE_SSL:
_default: 'false' _default: 'false'
ETHEREUM_JSONRPC_VARIANT:
_default: geth
HEART_BEAT_TIMEOUT:
_default: 30
SHOW_PRICE_CHART:
_default: false
CACHE_BLOCK_COUNT_PERIOD:
_default: 7200
PORT:
_default: 4000
SUBNETWORK:
_default: Ethereum
HEALTHY_BLOCKS_PERIOD:
_default: 60
NETWORK:
_default: (Goerli)
NETWORK_ICON:
_default: _network_icon.html
COIN:
_default: ETH
COIN_NAME:
_default: ETH
LOGO:
_default: /images/goerli_logo.svg
HISTORY_FETCH_INTERVAL:
_default: 60
TXS_HISTORIAN_INIT_LAG:
_default: 0
TXS_STATS_DAYS_TO_COMPILE_AT_INIT:
_default: 1
COIN_BALANCE_HISTORY_DAYS:
_default: 90
GAS_PRICE_ORACLE_NUM_OF_BLOCKS:
_default: 200
GAS_PRICE_ORACLE_SAFELOW_PERCENTILE:
_default: 35
GAS_PRICE_ORACLE_AVERAGE_PERCENTILE:
_default: 60
GAS_PRICE_ORACLE_FAST_PERCENTILE:
_default: 90
GAS_PRICE_ORACLE_CACHE_PERIOD:
_default: 300
POOL_SIZE:
_default: 20
DISPLAY_TOKEN_ICONS:
_default: 'true'
FETCH_REWARDS_WAY:
_default: manual
INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER:
_default: 'true'
SHOW_TESTNET_LABEL:
_default: 'true'
CHAIN_ID:
_default: 5
ENABLE_RUST_VERIFICATION_SERVICE:
_default: 'true'
RUST_VERIFICATION_SERVICE_URL: RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043 _default: http://sc-verifier-svc:8043
INDEXER_MEMORY_LIMIT:
_default: 3
ACCOUNT_ENABLED: ACCOUNT_ENABLED:
_default: 'true' _default: 'true'
DISABLE_REALTIME_INDEXER: API_V2_ENABLED:
_default: 'false'
SOCKET_ROOT:
_default: "/"
NETWORK_PATH:
_default: "/"
API_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_default: 'true' _default: 'true'
API_BASE_PATH:
_default: "/"
APPS_MENU: APPS_MENU:
_default: 'true' _default: 'true'
EXTERNAL_APPS: APPS:
_default: '[{"title": "Marketplace", "url": "/apps"}]' _default: '[{"title": "Marketplace", "url": "/apps", "embedded?": true}]'
JSON_RPC:
_default: https://sokol.poa.network
API_V2_ENABLED:
_default: 'true'
postgres: postgres:
enabled: true enabled: true
...@@ -142,7 +168,7 @@ postgres: ...@@ -142,7 +168,7 @@ postgres:
_default: 'trust' _default: 'trust'
# enable geth deploy # enable geth deploy
geth: geth:
enabled: true enabled: false
image: image:
_default: ethereum/client-go:stable _default: ethereum/client-go:stable
replicas: replicas:
...@@ -150,22 +176,25 @@ geth: ...@@ -150,22 +176,25 @@ geth:
portHttp: 8545 portHttp: 8545
portWs: 8546 portWs: 8546
portAuth: 8551 portAuth: 8551
command: '["sh","./root/init.sh"]' command: '["geth"]'
args: '["--fakepow", "--dev", "--dev.period=1", "--datadir=/root/.ethereum/devnet", "--keystore=/root/.ethereum/devnet/keystore", "--password=/root/password.txt", "--unlock=0", "--unlock=1", "--mine", "--miner.threads=1", "--miner.etherbase=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "--ipcpath=/root/geth.ipc", "--http", "--http.vhosts=*", "--http.addr=0.0.0.0", "--http.port=8545", "--http.api=eth,net,web3,debug,txpool", "--ws", "--ws.origins=*", "--ws.addr=0.0.0.0", "--ws.port=8546", "--ws.api=eth,net,web3,debug,txpool", "--graphql", "--graphql.corsdomain=*", "--allow-insecure-unlock", "--rpc.allow-unprotected-txs", "--http.corsdomain=*", "--vmdebug", "--networkid=1337", "--rpc.txfeecap=0"]' args: '["--goerli", "--datadir=/.ethereum", "--gcmode=archive", "--http", "--http.vhosts=*", "--http.addr=0.0.0.0", "--http.port=8545", "--http.api=eth,net,web3,debug,txpool", "--ws", "--ws.origins=*", "--ws.addr=0.0.0.0", "--ws.port=8546", "--ws.api=eth,net,web3,debug,txpool", "--allow-insecure-unlock", "--rpc.allow-unprotected-txs", "--http.corsdomain=*", "--vmdebug", "--rpc.txfeecap=0"]'
environment: {} environment: {}
persistence: persistence:
enabled: false enabled: true
mountPath: /geth
storageClass: gp3-new
storage: 1000Gi
resources: resources:
limits: limits:
memory: memory:
_default: "2Gi" _default: "8Gi"
cpu: cpu:
_default: "0.2" _default: "2"
requests: requests:
memory: memory:
_default: "2Gi" _default: "8Gi"
cpu: cpu:
_default: "0.2" _default: "2"
# node label # node label
nodeSelector: nodeSelector:
enabled: true enabled: true
...@@ -182,9 +211,63 @@ geth: ...@@ -182,9 +211,63 @@ geth:
tls: tls:
enabled: false enabled: false
jwt: jwt:
enabled: false enabled: true
mountPath: /geth/geth/jwtsecret
files: files:
enabled: false
# enable client deploy (Prysm, lighthouse, nimbus, etc.)
client:
enabled: true enabled: true
image:
_default: gcr.io/prysmaticlabs/prysm/beacon-chain:stable
# command: '["sh","./root/init.sh"]'
args: '["--goerli", "--datadir=/data", "--jwt-secret=/geth/geth/jwtsecret", "--rpc-host=0.0.0.0", "--grpc-gateway-host=0.0.0.0", "--monitoring-host=0.0.0.0", "--execution-endpoint=http://geth-svc:8545", "--checkpoint-sync-url=https://goerli.checkpoint-sync.ethdevops.io", "--genesis-beacon-api-url=https://goerli.checkpoint-sync.ethdevops.io"]'
ports:
port-tcp:
number: 13000
protocol: TCP
port-udp:
number: 12000
protocol: UDP
port-rpc:
number: 4000
protocol: TCP
port-rpc-gtw:
number: 3500
protocol: TCP
port-monitoring:
number: 8080
protocol: TCP
persistence:
enabled: false
mountPath: path
storageClass: gp3-new
storage: 100Gi
files:
enabled: false
list: {}
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: false
host:
# enable https
tls:
enabled: false
environment: {}
resources:
limits:
memory:
_default: "6Gi"
cpu:
_default: "3"
requests:
memory:
_default: "6Gi"
cpu:
_default: "3"
# enable Smart-contract-verifier deploy # enable Smart-contract-verifier deploy
scVerifier: scVerifier:
enabled: true enabled: true
...@@ -316,51 +399,58 @@ frontend: ...@@ -316,51 +399,58 @@ frontend:
enabled: true enabled: true
app: blockscout app: blockscout
environment: environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION: # ui config
_default: v4.1.8-beta NEXT_PUBLIC_FEATURED_NETWORKS:
NEXT_PUBLIC_FOOTER_GITHUB_LINK: _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'}]"
_default: https://github.com/blockscout/blockscout NEXT_PUBLIC_NETWORK_EXPLORERS:
NEXT_PUBLIC_FOOTER_TWITTER_LINK: _default: "[{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io','paths':{'tx':'/tx'}}]"
_default: https://www.twitter.com/blockscoutcom # network config
NEXT_PUBLIC_APP_ENV:
_default: preview
NEXT_PUBLIC_APP_INSTANCE:
_default: unknown
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME: NEXT_PUBLIC_NETWORK_NAME:
_default: Sokol _default: Ethereum
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA _default: Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME: NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa _default: ethereum
NEXT_PUBLIC_NETWORK_TYPE: NEXT_PUBLIC_NETWORK_TYPE:
_default: poa_core _default: goerli
NEXT_PUBLIC_NETWORK_ID: NEXT_PUBLIC_NETWORK_ID:
_default: 77 _default: 5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol _default: Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA _default: ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18 _default: 18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
_default: validation _default: validation
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_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
NEXT_PUBLIC_API_BASE_PATH:
_default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
_default: https://airtable.com/shrqUAcjgGJ4jU88C _default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST: 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/'}]" _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/'}]"
# api config
NEXT_PUBLIC_API_BASE_PATH:
_default: /
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV:
_default: preview
NEXT_PUBLIC_APP_INSTANCE:
_default: unknown
NEXT_PUBLIC_API_HOST:
_default: blockscout.com/eth/goerli
NEXT_PUBLIC_APP_HOST:
_default: blockscout.com/eth/goerli
NEXT_PUBLIC_LOGOUT_URL: NEXT_PUBLIC_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout _default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL: NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout.com/auth/logout _default: https://blockscout.com/auth/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cup']"
...@@ -60,7 +60,7 @@ geth: ...@@ -60,7 +60,7 @@ geth:
files: files:
list: list:
genesis.json: ENC[AES256_GCM,data:nSFncQhigaTtb7YSeXiUvua2LTOGlluVDywxQR5NnhvIBZCZniIxHsEhXKF9rBYceOMfBPKY5MTLIyxYisZeaJHL71A/uXQerDEKkECpzhwUSTdNEA9gUEqmpUjf7ChAWjybWw6NCM2jVhG3bnZwTwmuzeCSek85q4J503j9HVEkGXAU52A2+yoov41c+xoCEOj7W6Ka/f7yJYkyppoeS+4AIxBFcJ5hI0pIf1VqDcSrJf68JO1pPzcmp9eK3CK2CIpD8uxV6CseBFEqlbllmgAVwSUJ4i8XhYEO2hMyULm/LkW52U31/tq89yctSkp5TB62sZEHyDxXcq6jFSqd4840o9VedyX4YuEVtLBlXdzerc4Q2tHORQv5u2BLW7ktA/5vj8d9XnblQBDKrqxtUEgvSj9T/Xm4zyKjIhJBAAwUd5rF4j8dNbUGpsBm3G/Lx1+DqrS8cyxXgumtmfsklEVTWBra2dwt/tlTpF+utOwT7KtqXo2dVNvL6tUWrWGQqCt+fgeRKw8NuHWgU9Nm6baQ0yhWIM3XaCEuuJqHLdelYgQiKVEaTvlw7my7XwQsunJCQ98Q2eO61v7WFsg1m5s3Wyts8R2EnMWrnDlGro//eUHFlxDiIQGXmWgnz597lrLTXiuZKNI4pvPsAggBQxIeUcYzICy5uLRPWAiDpfshYt/fRQhIxmrVaJCufJrq4IcUdJYKjAcpnGHqZMB6quZiUPCPYg5Lvz7w1zoIVLn4ANWVuh7J1husNH13SwrDUXEm+k/WYCkDZm5B/K9IzkihY8nNXbvxHEXUClpb943Xw8hpvZZj0BfP/ye7ywUzHGlo3OmFAdE7Fbjc9eELxgYirONoaHy9GkjWPjinTVVlvitakqzx4dGJ+U2adSB01WAj63rn4ZidaC/eUpcPe7E2OeqmpkLc/kLM9g3gEe/MCiqsXXUYtm0Idpbrf+rRTAotoVOi3fKr1inlJY6n+9UzKAxNkTIkiGUQ1E9g0s3Kn4Z83apuy5+Us8gA9xNfWNGcOO105ZUYKyow4Xb5ggV8t+UTXPOZQp97Qz/6q0VC2kY9bx991SrJLoaK+ChLQ0/t/LcLjZxKRPDU2YGOfjrPDO3iscDkZgX/8IvH56jxUXAYmNrxOgbb9encYIrMDYrsKTaj5kXETp2M3sTdHwk6d78PAteYCvgQb44gu5JtknIq2LLXb/2Ue45O4HnrB+AsICPb+oGAxIqFkHvWuOIRZLqap8wbazszBlqsrX8ABegjvReXiG0rX1VeQIXNcG3/tKPHNKrDlFJykaiC1w==,iv:pyrxyy1bbv1UiGZ2y1jer9UqOwVkZGb4GwawJoCGJuM=,tag:aVt5dASuZglsUs9Yydzjaw==,type:str] genesis.json: ENC[AES256_GCM,data:nSFncQhigaTtb7YSeXiUvua2LTOGlluVDywxQR5NnhvIBZCZniIxHsEhXKF9rBYceOMfBPKY5MTLIyxYisZeaJHL71A/uXQerDEKkECpzhwUSTdNEA9gUEqmpUjf7ChAWjybWw6NCM2jVhG3bnZwTwmuzeCSek85q4J503j9HVEkGXAU52A2+yoov41c+xoCEOj7W6Ka/f7yJYkyppoeS+4AIxBFcJ5hI0pIf1VqDcSrJf68JO1pPzcmp9eK3CK2CIpD8uxV6CseBFEqlbllmgAVwSUJ4i8XhYEO2hMyULm/LkW52U31/tq89yctSkp5TB62sZEHyDxXcq6jFSqd4840o9VedyX4YuEVtLBlXdzerc4Q2tHORQv5u2BLW7ktA/5vj8d9XnblQBDKrqxtUEgvSj9T/Xm4zyKjIhJBAAwUd5rF4j8dNbUGpsBm3G/Lx1+DqrS8cyxXgumtmfsklEVTWBra2dwt/tlTpF+utOwT7KtqXo2dVNvL6tUWrWGQqCt+fgeRKw8NuHWgU9Nm6baQ0yhWIM3XaCEuuJqHLdelYgQiKVEaTvlw7my7XwQsunJCQ98Q2eO61v7WFsg1m5s3Wyts8R2EnMWrnDlGro//eUHFlxDiIQGXmWgnz597lrLTXiuZKNI4pvPsAggBQxIeUcYzICy5uLRPWAiDpfshYt/fRQhIxmrVaJCufJrq4IcUdJYKjAcpnGHqZMB6quZiUPCPYg5Lvz7w1zoIVLn4ANWVuh7J1husNH13SwrDUXEm+k/WYCkDZm5B/K9IzkihY8nNXbvxHEXUClpb943Xw8hpvZZj0BfP/ye7ywUzHGlo3OmFAdE7Fbjc9eELxgYirONoaHy9GkjWPjinTVVlvitakqzx4dGJ+U2adSB01WAj63rn4ZidaC/eUpcPe7E2OeqmpkLc/kLM9g3gEe/MCiqsXXUYtm0Idpbrf+rRTAotoVOi3fKr1inlJY6n+9UzKAxNkTIkiGUQ1E9g0s3Kn4Z83apuy5+Us8gA9xNfWNGcOO105ZUYKyow4Xb5ggV8t+UTXPOZQp97Qz/6q0VC2kY9bx991SrJLoaK+ChLQ0/t/LcLjZxKRPDU2YGOfjrPDO3iscDkZgX/8IvH56jxUXAYmNrxOgbb9encYIrMDYrsKTaj5kXETp2M3sTdHwk6d78PAteYCvgQb44gu5JtknIq2LLXb/2Ue45O4HnrB+AsICPb+oGAxIqFkHvWuOIRZLqap8wbazszBlqsrX8ABegjvReXiG0rX1VeQIXNcG3/tKPHNKrDlFJykaiC1w==,iv:pyrxyy1bbv1UiGZ2y1jer9UqOwVkZGb4GwawJoCGJuM=,tag:aVt5dASuZglsUs9Yydzjaw==,type:str]
init.sh: ENC[AES256_GCM,data:2qHHfwC63yJlxqaqmQQ69CxtcYZiEgdLdr/Hcxk8eKEou16bXL1NxpxfW8MWsfZyB4S3QNj05V6WSpZAw9ITG/GnXOKocZqKbZPmmuwQpLoekC37EsdUETb+tFvIunh9xRmgst4/ByXEyjC9pZk8SN0Qr9cGQl3VMzeGSkRndaE+qnlFxHRO5ELbDyaLMcyLWaQncescCSvIDpyOPNN7NwS3PYW+Nq3rvh4y+2FBoHUPUMIq46q94+ROnFk5TQsUI5/sVdw/5fuJklcTeqxuo4pPFLAgtkXkab0k2Og0eMI4It1xIrwpfJnxKwbjd0zljKo8tNzjpPZKHMfYchPX6uGH9fnwvXcH7nKUUdtSWrWa8R9a5AyO7SJ0hfzewMzvOWspihJW3JXFTvfesoG/cMMBDg3icsD/0HyxVBQP2riSZsou3hzDFAOVoNLAQTNRVXckSxk9zbx0aOy3DlWefJhIFj2ZmF4sliwxMe+Oy/So6PfiRLsP9v6/OTv6sriExtHdAx6xeA+dbYCO9ruW+5gNB2UUi3DQvpvejaxysJygaHvYCVDGAUKCxDAvQPrUQ/r5+UaE+GmqflDzhd9lhAptL/xL8tW8R+v9RZdMO/G9kxWMhKWiHH6QqgdYGOvanwCxAGtSvKhqf6fiWnNMr6wKG4qAFk6cMNUsLHtnC1qLYhZAfBa1/aK5cG0IbygZKurC+9G8x4SmOqUpW8h23XRsNd8peCRuSZ2uSGqss/1XvngfW0avzGiXMHW9,iv:ldcn5mGmfGmSmq30mUen6diWK6EAEnYF6NQTXXwY8yA=,tag:kSuu7AGzzmSEM5AzjIw5RQ==,type:str] init.sh: ENC[AES256_GCM,data:WtSrMR0dcOWU8aUtUqaRxHoG7k+821SrsBN/AgdQsDAwGJ21gP2iuT1hZn4t4qocv1qWnwtrrYwgDIX3MZUL5Ee0/SZ8oNEI49yaseA07OsPvs2AekHpOJBp4PJpPqN7C41Sb+pw9l3NUWnAG2LlCFwH8pfuszpKp/4sM5Vjn3KHSnEZ3VlddTfu+pa/Q65p6230ONemq0Fvp8r5ULznvcphNA63c4FK+xREVoPFXQQRvjnFvW4RdgUJriBxUc6r0LR6sWsfGZK+GeYID3N7J8J3FjSXl/yPTf5PjwtDsMJ9EjEiD1M9Ea5ssd32+Bx/XW2HR6oqApF2q9W2g55zC5o+72QREm1nDOt+/oB2uL5DUHB7FFgRwTHLTnjEF4LrmHD08kgzUghT1jks+XltIUt4MvPxSkhIN8QaNiLLkeJc2dxtcw2SZCTxrQmTgksqjf93gYZqbF7hrSwhyI9/yJOXBfLDCj/Ymzj7TaJDQgmabEgOAqtx+jTuAmJ3zKoLPw4+/9NX/IZi16x2gXg2zCWOlp6fH+4RRI6RHxcCRu0JQXO3q1TGVGZ8JC665oxcTDlXH7EFOiwo9e3r283WUrOHQ5CeOQTDV7SVVXmRwIUguOvH1BeU7penFeh+WirwquLykT5KQCt7zfbmmHpWSJ9uKVyX8d/LBydKCc1OAMMnp1zbE0cFX+4/Gyhlnn5JU6hvjOHohFvXn0b5hySY8seOvnOQCamQIOjDr85bHJGRRJRYJfMjghg3Sg==,iv:Q/dEbbeUwhkkTHZ8Ic5FHTZ4r9bCU1rDg6J9MyxzDiQ=,tag:XEPJ6r9+sQeGsh4VftLVVw==,type:str]
password.txt: "" password.txt: ""
frontend: frontend:
environment: environment:
...@@ -78,8 +78,8 @@ sops: ...@@ -78,8 +78,8 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2022-11-01T14:58:11Z" lastmodified: "2022-11-14T20:05:57Z"
mac: ENC[AES256_GCM,data:3AK4GRnUnAcQrdJ9JrdhSFqMYmYhE2RGiP+NPvO+mqBGDH26pRjN3lkNhHi/uObEQWiQZJLzLEOhSPl0/oDVYRTSGEeEiIlViEm/S5PuD57uFx6ogS9Iz88G/3hnc3HpTAIg2+NVwE7wF1/NK75WlivB1pUGk7OrazhZ+Fhyn5k=,iv:94YFEq/dmS07CqsKFZ2NAKMj3LqqyUB4+2XtHI3UiLg=,tag:4uti1G9eRoL2AwF0n7avdQ==,type:str] mac: ENC[AES256_GCM,data:m6N+QIkvPbzWVHkBQ17Wr97BpHY2395e5F7z9lZRcWu+L+MCVrRzCo4IeSKFFty5GqGeduouuMioYuGZeczDxSsWxTPXvyx+sH5n7Q0lp4gf9TkJqVL/O1NU8JSVrBvOy8B9F6vPLi7KwOW2xAFdFdGggTxG8Lk0DNWPlM8lmTo=,iv:fa0w2pIh075WxxfXPCf+zbSz/x1g+lqiVFeTlTvrEYw=,tag:BgELhzRsL6SxW8o8HH0C9g==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
...@@ -147,11 +147,21 @@ geth: ...@@ -147,11 +147,21 @@ geth:
_default: ethereum/client-go:stable _default: ethereum/client-go:stable
replicas: replicas:
app: 1 app: 1
portHttp: 8545 ports:
portWs: 8546 http:
portAuth: 8551 number: 8545
command: '["sh","./root/init.sh"]' protocol: TCP
args: '["--fakepow", "--dev", "--dev.period=1", "--datadir=/root/.ethereum/devnet", "--keystore=/root/.ethereum/devnet/keystore", "--password=/root/password.txt", "--unlock=0", "--unlock=1", "--mine", "--miner.threads=1", "--miner.etherbase=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "--ipcpath=/root/geth.ipc", "--http", "--http.vhosts=*", "--http.addr=0.0.0.0", "--http.port=8545", "--http.api=eth,net,web3,debug,txpool", "--ws", "--ws.origins=*", "--ws.addr=0.0.0.0", "--ws.port=8546", "--ws.api=eth,net,web3,debug,txpool", "--graphql", "--graphql.corsdomain=*", "--allow-insecure-unlock", "--rpc.allow-unprotected-txs", "--http.corsdomain=*", "--vmdebug", "--networkid=1337", "--rpc.txfeecap=0"]' ws:
number: 8546
protocol: TCP
auth:
number: 8551
protocol: TCP
command:
- /bin/sh
- -c
- |-
/root/init.sh --fakepow --dev --dev.period=1 --datadir=/root/.ethereum/devnet --keystore=/root/.ethereum/devnet/keystore --password=/root/password.txt --unlock=0 --unlock=1 --mine --miner.threads=1 --miner.etherbase=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 --ipcpath=/root/geth.ipc --http --http.vhosts=* --http.addr=0.0.0.0 --http.port=8545 --http.api=eth,net,web3,debug,txpool --ws --ws.origins=* --ws.addr=0.0.0.0 --ws.port=8546 --ws.api=eth,net,web3,debug,txpool --graphql --graphql.corsdomain=* --allow-insecure-unlock --rpc.allow-unprotected-txs --http.corsdomain=* --vmdebug --networkid=1337 --rpc.txfeecap=0
environment: {} environment: {}
persistence: persistence:
enabled: false enabled: false
...@@ -363,3 +373,5 @@ frontend: ...@@ -363,3 +373,5 @@ frontend:
_default: https://blockscoutcom.us.auth0.com/v2/logout _default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL: NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout.com/auth/logout _default: https://blockscout.com/auth/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cup']"
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2 19.4c3.972 0 7.2-3.228 7.2-7.2S16.172 5 12.2 5A7.206 7.206 0 0 0 5 12.2c0 3.972 3.228 7.2 7.2 7.2Zm-5.574-4.332 2.926-.023c.325 1.173.871 2.311 1.614 3.333a6.28 6.28 0 0 1-4.54-3.31Zm3.67-4.842h3.646a9.853 9.853 0 0 1 .023 3.867l-3.67.023a9.544 9.544 0 0 1 0-3.89Zm1.823 7.885a9.323 9.323 0 0 1-1.591-3.066l3.193-.023a9.813 9.813 0 0 1-1.602 3.089Zm.929.302a10.24 10.24 0 0 0 1.637-3.403l3.124-.023a6.286 6.286 0 0 1-4.761 3.426ZM18.47 12.2c0 .65-.105 1.277-.279 1.858l-3.286.023c.232-1.277.22-2.578-.023-3.855h3.263c.209.615.325 1.289.325 1.974Zm-.72-2.903h-3.09a10.675 10.675 0 0 0-1.613-3.31 6.244 6.244 0 0 1 4.703 3.31Zm-4.053 0H10.54a9.593 9.593 0 0 1 1.58-3.008 9.595 9.595 0 0 1 1.58 3.008Zm-2.532-3.275a10.317 10.317 0 0 0-1.59 3.275H6.648a6.249 6.249 0 0 1 4.517-3.275Zm-1.811 4.204a10.685 10.685 0 0 0-.012 3.89l-3.1.023a6.173 6.173 0 0 1 .012-3.914h3.1Z" fill="currentColor"/>
</svg>
export function shortenNumberWithLetter(
x: number,
params?: {
unitSeparator: string;
},
_options?: Intl.NumberFormatOptions,
) {
const options = _options || { maximumFractionDigits: 2 };
const unitSeparator = params?.unitSeparator || '';
if (x > 1_000_000_000) {
return (x / 1_000_000_000).toLocaleString('en', options) + unitSeparator + 'B';
}
if (x > 1_000_000) {
return (x / 1_000_000).toLocaleString('en', options) + unitSeparator + 'M';
}
if (x > 1_000) {
return (x / 1_000).toLocaleString('en', options) + unitSeparator + 'K';
}
return x.toLocaleString('en', options);
}
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { pick, omit } from 'lodash'; import { pick, omit } from 'lodash';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
...@@ -5,32 +6,31 @@ import React, { useCallback } from 'react'; ...@@ -5,32 +6,31 @@ import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { BlockFilters } from 'types/api/block'; import type { BlockFilters } from 'types/api/block';
import type { PaginationParams } from 'types/api/pagination'; 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 { TTxsFilters } from 'types/api/txsFilters';
import type { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
const PAGINATION_FIELDS: Array<keyof PaginationParams> = [ 'block_number', 'index', 'items_count' ]; interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string;
interface ResponseWithPagination { queryName: QueryName;
next_page_params: PaginationParams | null; queryIds?: Array<string>;
filters?: TTxsFilters | BlockFilters;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
} }
export default function useQueryWithPages<Response extends ResponseWithPagination>( export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({ queryName, filters, options, apiPath, queryIds }: Params<QueryName>) {
apiPath: string, const paginationFields = PAGINATION_FIELDS[queryName];
queryName: QueryKeys,
filters?: TTxsFilters | BlockFilters,
) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const [ page, setPage ] = React.useState(1); const [ page, setPage ] = React.useState(1);
const currPageParams = pick(router.query, PAGINATION_FIELDS); const currPageParams = pick(router.query, paginationFields);
const [ pageParams, setPageParams ] = React.useState<Array<Partial<PaginationParams>>>([ {} ]); const [ pageParams, setPageParams ] = React.useState<Array<PaginationParams<QueryName>>>([ ]);
const fetch = useFetch(); const fetch = useFetch();
const queryResult = useQuery<unknown, unknown, Response>( const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>(
[ queryName, { page, filters } ], [ queryName, ...(queryIds || []), { page, filters } ],
async() => { async() => {
const params: Array<string> = []; const params: Array<string> = [];
...@@ -44,7 +44,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio ...@@ -44,7 +44,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`); return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`);
}, },
{ staleTime: Infinity }, { staleTime: Infinity, ...options },
); );
const { data } = queryResult; const { data } = queryResult;
...@@ -53,9 +53,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio ...@@ -53,9 +53,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
// we hide next page button if no next_page_params // we hide next page button if no next_page_params
return; return;
} }
// api adds filters into next-page-params now const nextPageParams = data.next_page_params;
// later filters will be removed from response
const nextPageParams = pick(data.next_page_params, PAGINATION_FIELDS);
if (page >= pageParams.length && data?.next_page_params) { if (page >= pageParams.length && data?.next_page_params) {
setPageParams(prev => [ ...prev, nextPageParams ]); setPageParams(prev => [ ...prev, nextPageParams ]);
} }
...@@ -66,18 +64,18 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio ...@@ -66,18 +64,18 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
animateScroll.scrollToTop({ duration: 0 }); animateScroll.scrollToTop({ duration: 0 });
setPage(prev => prev + 1); setPage(prev => prev + 1);
}); });
}, [ data, page, pageParams, router ]); }, [ data?.next_page_params, page, pageParams.length, router ]);
const onPrevPageClick = useCallback(() => { const onPrevPageClick = useCallback(() => {
// returning to the first page // returning to the first page
// we dont have pagination params for the first page // we dont have pagination params for the first page
let nextPageQuery: typeof router.query; let nextPageQuery: typeof router.query;
if (page === 2) { if (page === 2) {
nextPageQuery = omit(router.query, PAGINATION_FIELDS); nextPageQuery = omit(router.query, paginationFields);
} else { } else {
const nextPageParams = { ...pageParams[page - 2] }; const nextPageParams = { ...pageParams[page - 2] };
nextPageQuery = { ...router.query }; nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString()); nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
} }
router.query = nextPageQuery; router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
...@@ -86,16 +84,16 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio ...@@ -86,16 +84,16 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
setPage(prev => prev - 1); setPage(prev => prev - 1);
page === 2 && queryClient.clear(); page === 2 && queryClient.clear();
}); });
}, [ router, page, pageParams, queryClient ]); }, [ router, page, paginationFields, pageParams, queryClient ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
queryClient.clear(); queryClient.clear();
router.push({ pathname: router.pathname, query: omit(router.query, PAGINATION_FIELDS) }, undefined, { shallow: true }).then(() => { router.push({ pathname: router.pathname, query: omit(router.query, paginationFields) }, undefined, { shallow: true }).then(() => {
animateScroll.scrollToTop({ duration: 0 }); animateScroll.scrollToTop({ duration: 0 });
setPage(1); setPage(1);
setPageParams([ {} ]); setPageParams([ ]);
}); });
}, [ router, queryClient ]); }, [ queryClient, router, paginationFields ]);
const hasPaginationParams = Object.keys(currPageParams).length > 0; const hasPaginationParams = Object.keys(currPageParams).length > 0;
const nextPageParams = data?.next_page_params; const nextPageParams = data?.next_page_params;
......
...@@ -3,7 +3,7 @@ import appConfig from 'configs/app/config'; ...@@ -3,7 +3,7 @@ import appConfig from 'configs/app/config';
import { ROUTES } from './routes'; import { ROUTES } from './routes';
import type { RouteName } from './routes'; import type { RouteName } from './routes';
const PATH_PARAM_REGEXP = /\/\[(\w+)\]/g; const PATH_PARAM_REGEXP = /\/:(\w+)/g;
export default function link( export default function link(
routeName: RouteName, routeName: RouteName,
......
...@@ -7,16 +7,16 @@ const paths = { ...@@ -7,16 +7,16 @@ const paths = {
custom_abi: `/account/custom_abi`, custom_abi: `/account/custom_abi`,
profile: `/auth/profile`, profile: `/auth/profile`,
txs: `/txs`, txs: `/txs`,
tx: `/tx/[id]`, tx: `/tx/:id`,
blocks: `/blocks`, blocks: `/blocks`,
block: `/block/[id]`, block: `/block/:id`,
tokens: `/tokens`, tokens: `/tokens`,
token_index: `/token/[hash]`, token_index: `/token/:hash`,
token_instance_item: `/token/[hash]/instance/[id]`, token_instance_item: `/token/:hash/instance/:id`,
address_index: `/address/[id]`, address_index: `/address/:id`,
address_contract_verification: `/address/[id]/contract_verifications/new`, address_contract_verification: `/address/:id/contract_verifications/new`,
apps: `/apps`, apps: `/apps`,
app_index: `/apps/[id]`, app_index: `/apps/:id`,
search_results: `/search-results`, search_results: `/search-results`,
other: `/search-results`, other: `/search-results`,
auth: `/auth/auth0`, auth: `/auth/auth0`,
......
...@@ -11,7 +11,14 @@ import appConfig from 'configs/app/config'; ...@@ -11,7 +11,14 @@ import appConfig from 'configs/app/config';
// title: 'Anyblock', // title: 'Anyblock',
// baseUrl: 'https://explorer.anyblock.tools', // baseUrl: 'https://explorer.anyblock.tools',
// paths: { // paths: {
// tx: '/ethereum/poa/core/tx', // tx: '/ethereum/ethereum/goerli/transaction',
// },
// },
// {
// title: 'Etherscan',
// baseUrl: 'https://goerli.etherscan.io/',
// paths: {
// tx: '/tx',
// }, // },
// }, // },
// ]).replaceAll('"', '\''); // ]).replaceAll('"', '\'');
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from './types';
import Block from 'ui/pages/Block';
import getSeo from './getSeo';
type Props = {
pageParams: PageParams;
}
const BlockNextPage: NextPage<Props> = ({ pageParams }: Props) => {
const { title, description } = getSeo(pageParams);
return (
<>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Block/>
</>
);
};
export default BlockNextPage;
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import Blocks from 'ui/pages/Blocks';
import getSeo from './getSeo';
const BlocksNextPage: NextPage = () => {
const { title } = getSeo();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Blocks/>
</>
);
};
export default BlocksNextPage;
export type PageParams = unknown
...@@ -3,13 +3,15 @@ import type { GetServerSideProps, GetServerSidePropsResult } from 'next'; ...@@ -3,13 +3,15 @@ import type { GetServerSideProps, GetServerSidePropsResult } from 'next';
export type Props = { export type Props = {
cookies: string; cookies: string;
referrer: string; referrer: string;
id?: string;
} }
export const getServerSideProps: GetServerSideProps = async({ req }): Promise<GetServerSidePropsResult<Props>> => { export const getServerSideProps: GetServerSideProps = async({ req, query }): Promise<GetServerSidePropsResult<Props>> => {
return { return {
props: { props: {
cookies: req.headers.cookie || '', cookies: req.headers.cookie || '',
referrer: req.headers.referer || '', referrer: req.headers.referer || '',
id: query.id?.toString() || '',
}, },
}; };
}; };
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from './types';
import Transaction from 'ui/pages/Transaction';
import getSeo from './getSeo';
type Props = {
pageParams: PageParams;
}
const TransactionNextPage: NextPage<Props> = ({ pageParams }: Props) => {
const { title, description } = getSeo(pageParams);
return (
<>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Transaction/>
</>
);
};
export default TransactionNextPage;
...@@ -4,9 +4,12 @@ import type { NewBlockSocketResponse } from 'types/api/block'; ...@@ -4,9 +4,12 @@ import type { NewBlockSocketResponse } from 'types/api/block';
export type SocketMessageParams = SocketMessage.NewBlock | export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus | SocketMessage.BlocksIndexStatus |
SocketMessage.TxStatusUpdate; SocketMessage.TxStatusUpdate |
SocketMessage.NewTx |
SocketMessage.NewPendingTx |
SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string, Payload extends object> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
channel: Channel | undefined; channel: Channel | undefined;
event: Event; event: Event;
handler: (payload: Payload) => void; handler: (payload: Payload) => void;
...@@ -17,4 +20,7 @@ export namespace SocketMessage { ...@@ -17,4 +20,7 @@ export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>; export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', {finished: boolean; ratio: string}>; export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', {finished: boolean; ratio: string}>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>; export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -6,7 +6,7 @@ import notEmpty from 'lib/notEmpty'; ...@@ -6,7 +6,7 @@ import notEmpty from 'lib/notEmpty';
import { useSocket } from './context'; import { useSocket } from './context';
interface Params { interface Params {
topic: string; topic: string | undefined;
params?: object; params?: object;
isDisabled: boolean; isDisabled: boolean;
onJoin?: (channel: Channel, message: unknown) => void; onJoin?: (channel: Channel, message: unknown) => void;
...@@ -47,7 +47,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on ...@@ -47,7 +47,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
}, [ channel, isDisabled ]); }, [ channel, isDisabled ]);
useEffect(() => { useEffect(() => {
if (socket === null || isDisabled) { if (socket === null || isDisabled || !topic) {
return; return;
} }
......
...@@ -7,7 +7,7 @@ export default function useSocketMessage({ channel, event, handler }: SocketMess ...@@ -7,7 +7,7 @@ export default function useSocketMessage({ channel, event, handler }: SocketMess
handlerRef.current = handler; handlerRef.current = handler;
useEffect(() => { useEffect(() => {
if (channel === undefined) { if (channel === undefined || event === undefined) {
return; return;
} }
......
import type { TransactionsResponse } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort'; import type { Sort } from 'types/client/txs-sort';
import compareBns from 'lib/bigint/compareBns'; import compareBns from 'lib/bigint/compareBns';
export default function sortTxs(txs: TransactionsResponse['items'], sorting?: Sort) { const sortTxs = (sorting?: Sort) => (tx1: Transaction, tx2: Transaction) => {
let sortedTxs;
switch (sorting) { switch (sorting) {
case 'val-desc': case 'val-desc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx1.value, tx2.value)); return compareBns(tx1.value, tx2.value);
break;
case 'val-asc': case 'val-asc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx2.value, tx1.value)); return compareBns(tx2.value, tx1.value);
break;
case 'fee-desc': case 'fee-desc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx1.fee.value, tx2.fee.value)); return compareBns(tx1.fee.value, tx2.fee.value);
break;
case 'fee-asc': case 'fee-asc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx2.fee.value, tx1.fee.value)); return compareBns(tx2.fee.value, tx1.fee.value);
break;
default: default:
sortedTxs = txs; return 0;
} }
};
return sortedTxs; export default sortTxs;
}
...@@ -13,6 +13,10 @@ class MyDocument extends Document { ...@@ -13,6 +13,10 @@ class MyDocument extends Document {
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&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link rel="icon" sizes="32x32" type="image/png" href="/static/favicon-32x32.png"/> <link rel="icon" sizes="32x32" type="image/png" href="/static/favicon-32x32.png"/>
<link rel="icon" sizes="16x16" type="image/png"href="/static/favicon-16x16.png"/> <link rel="icon" sizes="16x16" type="image/png"href="/static/favicon-16x16.png"/>
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"/> <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"/>
......
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats/charts/market';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats/charts/transactions';
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';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/transactions/${ req.query.id }/token-transfers${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/tx/types'; import type { PageParams } from 'lib/next/block/types';
import BlockNextPage from 'lib/next/block/BlockNextPage'; import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block';
type Props = { const BlockPage: NextPage<PageParams> = ({ id }: PageParams) => {
pageParams: PageParams; const { title, description } = getSeo({ id });
}
const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
return ( return (
<BlockNextPage pageParams={ pageParams }/> <>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Block/>
</>
); );
}; };
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react'; import React from 'react';
import BlocksNextPage from 'lib/next/blocks/BlocksNextPage'; import getSeo from 'lib/next/blocks/getSeo';
import Blocks from 'ui/pages/Blocks';
const BlockPage: NextPage = () => { const BlockPage: NextPage = () => {
const { title } = getSeo();
return ( return (
<BlocksNextPage/> <>
<Head>
<title>{ title }</title>
</Head>
<Blocks/>
</>
); );
}; };
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/tx/types'; import type { PageParams } from 'lib/next/tx/types';
import TransactionNextPage from 'lib/next/tx/TransactionNextPage'; import getSeo from 'lib/next/tx/getSeo';
import Transaction from 'ui/pages/Transaction';
type Props = { const TransactionPage: NextPage<PageParams> = ({ id }: PageParams) => {
pageParams: PageParams; const { title, description } = getSeo({ id });
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return ( return (
<TransactionNextPage pageParams={ pageParams }/> <>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Transaction/>
</>
); );
}; };
......
...@@ -4,7 +4,8 @@ const borders = { ...@@ -4,7 +4,8 @@ const borders = {
sm: '4px', sm: '4px',
base: '8px', base: '8px',
md: '12px', md: '12px',
lg: '24px', lg: '16px',
xl: '24px',
full: '9999px', full: '9999px',
}, },
}; };
......
import type { AddressParam } from 'types/api/addressParams'; import type { AddressParam } from 'types/api/addressParams';
import type { PaginationParams } from 'types/api/pagination';
import type { Reward } from 'types/api/reward'; import type { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -34,12 +33,18 @@ export interface Block { ...@@ -34,12 +33,18 @@ export interface Block {
export interface BlocksResponse { export interface BlocksResponse {
items: Array<Block>; items: Array<Block>;
next_page_params: PaginationParams | null; next_page_params: {
block_number: number;
items_count: number;
} | null;
} }
export interface BlockTransactionsResponse { export interface BlockTransactionsResponse {
items: Array<Transaction>; items: Array<Transaction>;
next_page_params: PaginationParams | null; next_page_params: {
block_number: number;
items_count: number;
} | null;
} }
export interface NewBlockSocketResponse { export interface NewBlockSocketResponse {
......
export interface ChartTransactionItem {
date: string;
tx_count: number;
}
export interface ChartMarketItem {
date: string;
closing_price: string;
}
export interface ChartTransactionResponse {
chart_data: Array<ChartTransactionItem>;
}
export interface ChartMarketResponse {
available_supply: string;
chart_data: Array<ChartMarketItem>;
}
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { PaginationParams } from './pagination';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward' export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
...@@ -20,7 +19,10 @@ export interface InternalTransaction { ...@@ -20,7 +19,10 @@ export interface InternalTransaction {
export interface InternalTransactionsResponse { export interface InternalTransactionsResponse {
items: Array<InternalTransaction>; items: Array<InternalTransaction>;
next_page_params: PaginationParams & { next_page_params: {
block_number: number;
index: number;
items_count: number;
transaction_hash: string; transaction_hash: string;
transaction_index: number; transaction_index: number;
}; };
......
export interface PaginationParams { import type { BlocksResponse, BlockTransactionsResponse } from 'types/api/block';
block_number: number; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
index?: number; import type { LogsResponse } from 'types/api/log';
items_count: number; import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending } from 'types/api/transaction';
import { QueryKeys } from 'types/client/queries';
import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
export type PaginatedQueryKeys =
QueryKeys.blocks |
QueryKeys.blockTxs |
QueryKeys.txsValidate |
QueryKeys.txsPending |
QueryKeys.txInternals |
QueryKeys.txLogs |
QueryKeys.txTokenTransfers;
export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.blocks ? BlocksResponse :
Q extends QueryKeys.blockTxs ? BlockTransactionsResponse :
Q extends QueryKeys.txsValidate ? TransactionsResponseValidated :
Q extends QueryKeys.txsPending ? TransactionsResponsePending :
Q extends QueryKeys.txInternals ? InternalTransactionsResponse :
Q extends QueryKeys.txLogs ? LogsResponse :
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse :
never
export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params'];
type PaginationFields = {
[K in PaginatedQueryKeys]: Array<KeysOfObjectOrNull<PaginatedResponse<K>['next_page_params']>>
} }
export const PAGINATION_FIELDS: PaginationFields = {
[QueryKeys.blocks]: [ 'block_number', 'items_count' ],
[QueryKeys.blockTxs]: [ 'block_number', 'items_count' ],
[QueryKeys.txsValidate]: [ 'block_number', 'items_count', 'filter', 'index' ],
[QueryKeys.txsPending]: [ 'filter', 'hash', 'inserted_at' ],
[QueryKeys.txInternals]: [ 'block_number', 'items_count', 'transaction_hash', 'index', 'transaction_index' ],
[QueryKeys.txTokenTransfers]: [ 'block_number', 'items_count', 'transaction_hash', 'index' ],
[QueryKeys.txLogs]: [ 'items_count', 'transaction_hash', 'index' ],
};
export type Stats = {
total_blocks: string;
total_addresses: string;
total_transactions: string;
average_block_time: number;
coin_price: string;
total_gas_used: string;
transactions_today: string;
gas_used_today: string;
gas_prices: {average: number; fast: number; slow: number};
static_gas_price: string;
market_cap: string;
}
...@@ -37,3 +37,13 @@ interface TokenTransferBase { ...@@ -37,3 +37,13 @@ interface TokenTransferBase {
from: AddressParam; from: AddressParam;
to: AddressParam; to: AddressParam;
} }
export interface TokenTransferResponse {
items: Array<TokenTransfer>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
} | null;
}
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { DecodedInput } from './decodedInput'; import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee'; import type { Fee } from './fee';
import type { PaginationParams } from './pagination';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = { export type TransactionRevertReason = {
...@@ -18,7 +17,7 @@ export interface Transaction { ...@@ -18,7 +17,7 @@ export interface Transaction {
timestamp: string | null; timestamp: string | null;
confirmation_duration: Array<number>; confirmation_duration: Array<number>;
from: AddressParam; from: AddressParam;
to: AddressParam; to: AddressParam | null;
created_contract: AddressParam; created_contract: AddressParam;
value: string; value: string;
fee: Fee; fee: Fee;
...@@ -39,14 +38,30 @@ export interface Transaction { ...@@ -39,14 +38,30 @@ export interface Transaction {
token_transfers: Array<TokenTransfer> | null; token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean; token_transfers_overflow: boolean;
exchange_rate: string; exchange_rate: string;
method: string; method: string | null;
tx_types: Array<TransactionType>; tx_types: Array<TransactionType>;
tx_tag: string | null; tx_tag: string | null;
} }
export interface TransactionsResponse { export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
export interface TransactionsResponseValidated {
items: Array<Transaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
filter: 'validated';
} | null;
}
export interface TransactionsResponsePending {
items: Array<Transaction>; items: Array<Transaction>;
next_page_params: PaginationParams | null; next_page_params: {
inserted_at: string;
hash: string;
filter: 'pending';
} | null;
} }
export type TransactionType = 'token_transfer' | 'contract_creation' | 'contract_call' | 'token_creation' | 'coin_transfer' export type TransactionType = 'token_transfer' | 'contract_creation' | 'contract_call' | 'token_creation' | 'coin_transfer'
export enum QueryKeys { export enum QueryKeys {
csrf = 'csrf', csrf = 'csrf',
profile = 'profile', profile = 'profile',
transactions = 'transactions', txsValidate = 'txs-validated',
txsPending = 'txs-pending',
stats='stats',
tx = 'tx', tx = 'tx',
txInternals = 'tx-internals', txInternals = 'tx-internals',
txLog = 'tx-log', txLogs = 'tx-logs',
txRawTrace = 'tx-raw-trace', txRawTrace = 'tx-raw-trace',
txTokenTransfers = 'tx-token-transfers',
blockTxs = 'block-transactions', blockTxs = 'block-transactions',
block = 'block', block = 'block',
blocks = 'blocks', blocks = 'blocks',
chartsTxs = 'charts-txs',
chartsMarket = 'charts-market',
} }
export type KeysOfObjectOrNull<T> = T extends null ? never : keyof T;
...@@ -12,6 +12,7 @@ const BlockTxs = () => { ...@@ -12,6 +12,7 @@ const BlockTxs = () => {
<TxsContent <TxsContent
queryName={ QueryKeys.blockTxs } queryName={ QueryKeys.blockTxs }
apiPath={ `/node-api/blocks/${ router.query.id }/transactions` } apiPath={ `/node-api/blocks/${ router.query.id }/transactions` }
showBlockInfo={ false }
/> />
); );
}; };
......
...@@ -25,7 +25,12 @@ const BlocksContent = ({ type }: Props) => { ...@@ -25,7 +25,12 @@ const BlocksContent = ({ type }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const { data, isLoading, isError, pagination } = useQueryWithPages<BlocksResponse>('/node-api/blocks', QueryKeys.blocks, { type }); const { data, isLoading, isError, pagination } = useQueryWithPages({
apiPath: '/node-api/blocks',
queryName: QueryKeys.blocks,
filters: { type },
});
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.blocks, { page: pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => { queryClient.setQueryData([ QueryKeys.blocks, { page: pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => {
...@@ -87,21 +92,27 @@ const BlocksContent = ({ type }: Props) => { ...@@ -87,21 +92,27 @@ const BlocksContent = ({ type }: Props) => {
<> <>
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> } { socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show> <Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show>
<Hide below="lg" key="content-desktop"><BlocksTable data={ data.items }/></Hide> <Hide below="lg" key="content-desktop"><BlocksTable data={ data.items } top={ isPaginatorHidden ? 0 : 80 } page={ pagination.page }/></Hide>
</> </>
); );
})(); })();
const totalText = data?.items.length ?
<Text mb={{ base: 0, lg: 6 }}>Total of { data.items[0].height.toLocaleString() } blocks</Text> :
null;
return ( return (
<> <>
{ data ? { data ?
<Text mb={{ base: 0, lg: 6 }}>Total of { data.items[0].height.toLocaleString() } blocks</Text> : totalText :
<Skeleton h="24px" w="200px" mb={{ base: 0, lg: 6 }}/> <Skeleton h="24px" w="200px" mb={{ base: 0, lg: 6 }}/>
} }
<ActionBar> { !isPaginatorHidden && (
<Pagination ml="auto" { ...pagination }/> <ActionBar>
</ActionBar> <Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ content } { content }
</> </>
); );
......
...@@ -12,13 +12,15 @@ import { default as Thead } from 'ui/shared/TheadSticky'; ...@@ -12,13 +12,15 @@ import { default as Thead } from 'ui/shared/TheadSticky';
interface Props { interface Props {
data: Array<Block>; data: Array<Block>;
top: number;
page: number;
} }
const BlocksTable = ({ data }: Props) => { const BlocksTable = ({ data, top, page }: Props) => {
return ( return (
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }> <Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
<Thead top={ 80 }> <Thead top={ top }>
<Tr> <Tr>
<Th width="125px">Block</Th> <Th width="125px">Block</Th>
<Th width="120px">Size</Th> <Th width="120px">Size</Th>
...@@ -31,8 +33,7 @@ const BlocksTable = ({ data }: Props) => { ...@@ -31,8 +33,7 @@ const BlocksTable = ({ data }: Props) => {
</Thead> </Thead>
<Tbody> <Tbody>
<AnimatePresence initial={ false }> <AnimatePresence initial={ false }>
{ /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ } { data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement={ page === 1 }/>) }
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement/>) }
</AnimatePresence> </AnimatePresence>
</Tbody> </Tbody>
</Table> </Table>
......
...@@ -32,12 +32,12 @@ const EthereumChart = () => { ...@@ -32,12 +32,12 @@ const EthereumChart = () => {
const data: TimeChartData = [ const data: TimeChartData = [
{ {
name: 'Daily Transactions', name: 'Daily txs',
color: useToken('colors', 'blue.500'), color: useToken('colors', 'blue.500'),
items: ethTxsData.slice(range[0], range[1]).map((d) => ({ ...d, date: new Date(d.date) })), items: ethTxsData.slice(range[0], range[1]).map((d) => ({ ...d, date: new Date(d.date) })),
}, },
{ {
name: 'ERC-20 Token Transfers', name: 'ERC-20 tr.',
color: useToken('colors', 'green.500'), color: useToken('colors', 'green.500'),
items: ethTokenTransferData.slice(range[0], range[1]).map((d) => ({ ...d, date: new Date(d.date) })), items: ethTokenTransferData.slice(range[0], range[1]).map((d) => ({ ...d, date: new Date(d.date) })),
}, },
...@@ -139,7 +139,6 @@ const EthereumChart = () => { ...@@ -139,7 +139,6 @@ const EthereumChart = () => {
anchorEl={ overlayRef.current } anchorEl={ overlayRef.current }
width={ innerWidth } width={ innerWidth }
height={ innerHeight } height={ innerHeight }
margin={ CHART_MARGIN }
xScale={ xScale } xScale={ xScale }
yScale={ yScale } yScale={ yScale }
data={ filteredData } data={ filteredData }
......
...@@ -5,7 +5,7 @@ import ethTxsData from 'data/charts_eth_txs.json'; ...@@ -5,7 +5,7 @@ import ethTxsData from 'data/charts_eth_txs.json';
import ChartLine from 'ui/shared/chart/ChartLine'; import ChartLine from 'ui/shared/chart/ChartLine';
import useChartSize from 'ui/shared/chart/useChartSize'; import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController'; import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import { BlueLinearGradient } from 'ui/shared/chart/utils/gradients'; import { BlueLineGradient } from 'ui/shared/chart/utils/gradients';
const CHART_MARGIN = { bottom: 0, left: 0, right: 0, top: 0 }; const CHART_MARGIN = { bottom: 0, left: 0, right: 0, top: 0 };
const DATA = ethTxsData.slice(-30).map((d) => ({ ...d, date: new Date(d.date) })); const DATA = ethTxsData.slice(-30).map((d) => ({ ...d, date: new Date(d.date) }));
...@@ -23,13 +23,13 @@ const SplineChartExample = () => { ...@@ -23,13 +23,13 @@ const SplineChartExample = () => {
return ( return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref }> <svg width={ width || '100%' } height={ height || '100%' } ref={ ref }>
<defs> <defs>
<BlueLinearGradient.defs/> <BlueLineGradient.defs/>
</defs> </defs>
<ChartLine <ChartLine
data={ DATA } data={ DATA }
xScale={ xScale } xScale={ xScale }
yScale={ yScale } yScale={ yScale }
stroke={ `url(#${ BlueLinearGradient.id })` } stroke={ `url(#${ BlueLineGradient.id })` }
animation="left" animation="left"
strokeWidth={ 3 } strokeWidth={ 3 }
/> />
......
import React from 'react';
import type { ChainIndicatorChartData } from './types';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartLine from 'ui/shared/chart/ChartLine';
import ChartOverlay from 'ui/shared/chart/ChartOverlay';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import { BlueLineGradient } from 'ui/shared/chart/utils/gradients';
interface Props {
data: ChainIndicatorChartData;
caption?: string;
}
const CHART_MARGIN = { bottom: 0, left: 10, right: 10, top: 0 };
const ChainIndicatorChart = ({ data }: Props) => {
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
const { xScale, yScale } = useTimeChartController({
data,
width: innerWidth,
height: innerHeight,
});
return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref } cursor="pointer">
<defs>
<BlueLineGradient.defs/>
</defs>
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ width ? 1 : 0 }>
<ChartArea
data={ data[0].items }
color={ data[0].color }
xScale={ xScale }
yScale={ yScale }
/>
<ChartLine
data={ data[0].items }
xScale={ xScale }
yScale={ yScale }
stroke={ `url(#${ BlueLineGradient.id })` }
animation="left"
strokeWidth={ 3 }
/>
<ChartOverlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }>
<ChartTooltip
anchorEl={ overlayRef.current }
width={ innerWidth }
height={ innerHeight }
xScale={ xScale }
yScale={ yScale }
data={ data }
/>
</ChartOverlay>
</g>
</svg>
);
};
export default React.memo(ChainIndicatorChart);
import { Flex } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { ChainIndicatorChartData } from './types';
import ChartLineLoader from 'ui/shared/chart/ChartLineLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ChainIndicatorChart from './ChainIndicatorChart';
type Props = UseQueryResult<ChainIndicatorChartData>;
const ChainIndicatorChartContainer = ({ data, isError, isLoading }: Props) => {
const content = (() => {
if (isLoading) {
return <ChartLineLoader mt="auto"/>;
}
if (isError) {
return <DataFetchAlert/>;
}
return <ChainIndicatorChart data={ data }/>;
})();
return <Flex h={{ base: '150px', lg: '250px' }} alignItems="flex-start">{ content }</Flex>;
};
export default React.memo(ChainIndicatorChartContainer);
import { Text, Flex, Box, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { ChainIndicatorId } from './types';
import type { Stats } from 'types/api/stats';
import useIsMobile from 'lib/hooks/useIsMobile';
interface Props {
id: ChainIndicatorId;
title: string;
value: (stats: Stats) => string;
icon: React.ReactNode;
isSelected: boolean;
onClick: (id: ChainIndicatorId) => void;
stats: UseQueryResult<Stats>;
}
const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => {
const bgColor = useColorModeValue('white', 'gray.900');
const isMobile = useIsMobile();
const handleClick = React.useCallback(() => {
onClick(id);
}, [ id, onClick ]);
const valueContent = (() => {
if (isMobile) {
return null;
}
if (stats.isLoading) {
return <Skeleton h={ 3 } w="70px" my={ 1.5 }/>;
}
if (stats.isError) {
return <Text variant="secondary" fontWeight={ 400 }>no data</Text>;
}
return <Text variant="secondary" fontWeight={ 600 }>{ value(stats.data) }</Text>;
})();
return (
<Flex
alignItems="center"
columnGap={ 3 }
p={ 4 }
as="li"
borderRadius="md"
cursor="pointer"
onClick={ handleClick }
bgColor={ isSelected ? bgColor : 'inherit' }
boxShadow={ isSelected ? 'lg' : 'none' }
zIndex={ isSelected ? 1 : 'initial' }
_hover={{
bgColor,
zIndex: 1,
}}
>
{ icon }
<Box>
<Text fontFamily="heading" fontWeight={ 500 }>{ title }</Text>
{ valueContent }
</Box>
</Flex>
);
};
export default React.memo(ChainIndicatorItem);
import { Box, Flex, Icon, Skeleton, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Stats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import infoIcon from 'icons/info.svg';
import useFetch from 'lib/hooks/useFetch';
import ChainIndicatorChartContainer from './ChainIndicatorChartContainer';
import ChainIndicatorItem from './ChainIndicatorItem';
import useFetchChartData from './useFetchChartData';
import INDICATORS from './utils/indicators';
const indicators = INDICATORS
.filter(({ id }) => appConfig.homepage.charts.includes(id))
.sort((a, b) => {
if (appConfig.homepage.charts.indexOf(a.id) > appConfig.homepage.charts.indexOf(b.id)) {
return 1;
}
if (appConfig.homepage.charts.indexOf(a.id) < appConfig.homepage.charts.indexOf(b.id)) {
return -1;
}
return 0;
});
const ChainIndicators = () => {
const [ selectedIndicator, selectIndicator ] = React.useState(indicators[0]?.id);
const indicator = indicators.find(({ id }) => id === selectedIndicator);
const queryResult = useFetchChartData(indicator);
const fetch = useFetch();
const statsQueryResult = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
() => fetch('/node-api/stats'),
);
const bgColor = useColorModeValue('white', 'gray.900');
const listBgColor = useColorModeValue('gray.50', 'black');
if (indicators.length === 0) {
return null;
}
const valueTitle = (() => {
if (statsQueryResult.isLoading) {
return <Skeleton h="48px" w="215px" mt={ 3 } mb={ 4 }/>;
}
if (statsQueryResult.isError) {
return <Text mt={ 3 } mb={ 4 }>There is no data</Text>;
}
return (
<Text fontWeight={ 600 } fontFamily="heading" fontSize="48px" lineHeight="48px" mt={ 3 } mb={ 4 }>
{ indicator?.value(statsQueryResult.data) }
</Text>
);
})();
return (
<Flex
p={{ base: 0, lg: 8 }}
borderRadius={{ base: 'none', lg: 'lg' }}
boxShadow={{ base: 'none', lg: 'xl' }}
bgColor={ bgColor }
columnGap={ 12 }
rowGap={ 0 }
flexDir={{ base: 'column', lg: 'row' }}
w="100%"
alignItems="stretch"
>
<Flex flexGrow={ 1 } flexDir="column" order={{ base: 2, lg: 1 }} p={{ base: 6, lg: 0 }}>
<Flex alignItems="center">
<Text fontWeight={ 500 } fontFamily="heading" fontSize="lg">{ indicator?.title }</Text>
{ indicator?.hint && (
<Tooltip label={ indicator.hint } maxW="300px">
<Box display="inline-flex" cursor="pointer" ml={ 1 }>
<Icon as={ infoIcon } boxSize={ 4 }/>
</Box>
</Tooltip>
) }
</Flex>
{ valueTitle }
<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 }}>
{ indicators.map((indicator) => (
<ChainIndicatorItem
key={ indicator.id }
{ ...indicator }
isSelected={ selectedIndicator === indicator.id }
onClick={ selectIndicator }
stats={ statsQueryResult }
/>
)) }
</Flex>
) }
</Flex>
);
};
export default ChainIndicators;
import type { ChartTransactionResponse, ChartMarketResponse } 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';
export type ChartsQueryKeys = QueryKeys.chartsTxs | QueryKeys.chartsMarket;
export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cup';
export interface TChainIndicator<Q extends ChartsQueryKeys> {
id: ChainIndicatorId;
title: string;
value: (stats: Stats) => string;
icon: React.ReactNode;
hint?: string;
api: {
queryName: Q;
path: string;
dataFn: (response: ChartsResponse<Q>) => ChainIndicatorChartData;
};
}
export type ChartsResponse<Q extends ChartsQueryKeys> =
Q extends QueryKeys.chartsTxs ? ChartTransactionResponse :
Q extends QueryKeys.chartsMarket ? ChartMarketResponse :
never;
export type ChainIndicatorChartData = Array<TimeChartDataItem>;
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 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> {
const fetch = useFetch();
type ResponseType = ChartsResponse<NotUndefined<typeof indicator>['api']['queryName']>;
const queryResult = useQuery<unknown, unknown, ResponseType>(
[ indicator?.api.queryName ],
() => fetch(indicator?.api.path || ''),
{ enabled: Boolean(indicator) },
);
return React.useMemo(() => {
return {
...queryResult,
data: queryResult.data && indicator ? indicator.api.dataFn(queryResult.data) : queryResult.data,
} as UseQueryResult<ChainIndicatorChartData>;
}, [ indicator, queryResult ]);
}
import { Icon } from '@chakra-ui/react';
import React from 'react';
import type { TChainIndicator } from '../types';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import globeIcon from 'icons/globe.svg';
import txIcon from 'icons/transactions.svg';
import { shortenNumberWithLetter } from 'lib/formatters';
import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import TokenLogo from 'ui/shared/TokenLogo';
const CHART_COLOR = '#439AE2';
const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
id: 'daily_txs',
title: 'Daily transactions',
value: (stats) => shortenNumberWithLetter(Number(stats.transactions_today), undefined, { maximumFractionDigits: 2 }),
icon: <Icon as={ txIcon } boxSize={ 6 } bgColor="#56ACD1" borderRadius="base" color="white"/>,
hint: `The total daily number of transactions on the blockchain for the last month.`,
api: {
queryName: QueryKeys.chartsTxs,
path: '/node-api/stats/charts/transactions',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: item.tx_count }))
.sort(sortByDateDesc),
name: 'Tx/day',
color: CHART_COLOR,
valueFormatter: (x) => shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 2 }),
} ]),
},
};
const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
id: 'coin_price',
title: `${ appConfig.network.currency.symbol } price`,
value: (stats) => '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
icon: <TokenLogo hash={ appConfig.network.nativeTokenAddress || '' } name={ appConfig.network.currency.name } boxSize={ 6 }/>,
hint: `${ appConfig.network.currency.symbol } token daily price in USD.`,
api: {
queryName: QueryKeys.chartsMarket,
path: '/node-api/stats/charts/market',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
.sort(sortByDateDesc),
name: `${ appConfig.network.currency.symbol } price`,
color: CHART_COLOR,
valueFormatter: (x) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
} ]),
},
};
const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
id: 'market_cup',
title: 'Market cap',
value: (stats) => '$' + shortenNumberWithLetter(Number(stats.market_cap), undefined, { maximumFractionDigits: 0 }),
icon: <Icon as={ globeIcon } boxSize={ 6 } bgColor="#6A5DCC" borderRadius="base" color="white"/>,
// eslint-disable-next-line max-len
hint: 'The total market value of a cryptocurrency\'s circulating supply. It is analogous to the free-float capitalization in the stock market. Market Cap = Current Price x Circulating Supply.',
api: {
queryName: QueryKeys.chartsMarket,
path: '/node-api/stats/charts/market',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
.sort(sortByDateDesc),
name: 'Market cap',
color: CHART_COLOR,
valueFormatter: (x) => '$' + shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 0 }),
} ]),
},
};
const INDICATORS = [
dailyTxsIndicator,
coinPriceIndicator,
marketPriceIndicator,
];
export default INDICATORS;
...@@ -6,6 +6,7 @@ import React from 'react'; ...@@ -6,6 +6,7 @@ import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -48,11 +49,11 @@ const Home = () => { ...@@ -48,11 +49,11 @@ const Home = () => {
return ( return (
<Page> <Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px"> <VStack gap={ 4 } alignItems="flex-start" maxW="1000px">
<PageTitle text={ <PageTitle text={
`Home Page for ${ appConfig.network.name } network` `Home Page for ${ appConfig.network.name } network`
}/> }/>
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button> <ChainIndicators/>
{ /* will be deleted when we move to new CI */ } { /* will be deleted when we move to new CI */ }
{ isFormVisible && ( { isFormVisible && (
<> <>
...@@ -70,6 +71,7 @@ const Home = () => { ...@@ -70,6 +71,7 @@ const Home = () => {
<Button onClick={ handleSetTokenClick }>Set cookie</Button> <Button onClick={ handleSetTokenClick }>Set cookie</Button>
</> </>
) } ) }
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button>
</VStack> </VStack>
</Page> </Page>
); );
......
...@@ -19,10 +19,12 @@ import TxDetails from 'ui/tx/TxDetails'; ...@@ -19,10 +19,12 @@ import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals'; import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace'; import TxRawTrace from 'ui/tx/TxRawTrace';
import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
// import TxState from 'ui/tx/TxState'; // import TxState from 'ui/tx/TxState';
const TABS: Array<RoutedTab> = [ const TABS: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <TxDetails/> }, { id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> },
{ id: 'internal', title: 'Internal txn', component: <TxInternals/> }, { id: 'internal', title: 'Internal txn', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> }, { id: 'logs', title: 'Logs', component: <TxLogs/> },
// will be implemented later, api is not ready // will be implemented later, api is not ready
......
...@@ -2,18 +2,34 @@ import { ...@@ -2,18 +2,34 @@ import {
Icon, Icon,
Center, Center,
useColorModeValue, useColorModeValue,
chakra,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import infoIcon from 'icons/info.svg'; import infoIcon from 'icons/info.svg';
const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick?: () => void}, ref: React.ForwardedRef<HTMLDivElement>) => { interface Props {
isOpen?: boolean;
className?: string;
onClick?: () => void;
}
const AdditionalInfoButton = ({ isOpen, onClick, className }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
const infoBgColor = useColorModeValue('blue.50', 'gray.600'); const infoBgColor = useColorModeValue('blue.50', 'gray.600');
const infoColor = useColorModeValue('blue.600', 'blue.300'); const infoColor = useColorModeValue('blue.600', 'blue.300');
return ( return (
<Center ref={ ref } background={ isOpen ? infoBgColor : 'unset' } borderRadius="8px" w="24px" h="24px" onClick={ onClick } cursor="pointer"> <Center
className={ className }
ref={ ref }
background={ isOpen ? infoBgColor : 'unset' }
borderRadius="8px"
w="24px"
h="24px"
onClick={ onClick }
cursor="pointer"
>
<Icon <Icon
as={ infoIcon } as={ infoIcon }
boxSize={ 5 } boxSize={ 5 }
...@@ -24,4 +40,4 @@ const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick? ...@@ -24,4 +40,4 @@ const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick?
); );
}; };
export default React.forwardRef(TxAdditionalInfoButton); export default chakra(React.forwardRef(AdditionalInfoButton));
...@@ -4,8 +4,8 @@ import React from 'react'; ...@@ -4,8 +4,8 @@ import React from 'react';
import infoIcon from 'icons/info.svg'; import infoIcon from 'icons/info.svg';
interface Props extends HTMLChakraProps<'div'> { interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
title: string; title: React.ReactNode;
hint: string; hint: string;
children: React.ReactNode; children: React.ReactNode;
} }
......
import { Tag, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
baseAddress: string;
addressFrom: string;
className?: string;
}
const InOutTag = ({ baseAddress, addressFrom, className }: Props) => {
const isOut = addressFrom === baseAddress;
const colorScheme = isOut ? 'orange' : 'green';
return <Tag className={ className } colorScheme={ colorScheme }>{ isOut ? 'OUT' : 'IN' }</Tag>;
};
export default React.memo(chakra(InOutTag));
import { Image, chakra } from '@chakra-ui/react'; import { Image, Center, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
const EmptyElement = () => null; const EmptyElement = ({ className, letter }: { className?: string; letter: string }) => {
const bgColor = useColorModeValue('gray.200', 'gray.600');
const color = useColorModeValue('gray.400', 'gray.200');
return (
<Center
className={ className }
fontWeight={ 600 }
bgColor={ bgColor }
color={ color }
borderRadius="base"
>
{ letter.toUpperCase() }
</Center>
);
};
interface Props { interface Props {
hash: string; hash: string;
...@@ -12,15 +27,22 @@ interface Props { ...@@ -12,15 +27,22 @@ interface Props {
} }
const TokenLogo = ({ hash, name, className }: Props) => { const TokenLogo = ({ hash, name, className }: Props) => {
const logoSrc = appConfig.network.assetsPathname ? ` const logoSrc = appConfig.network.assetsPathname ? [
https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/',
${ appConfig.network.assetsPathname } appConfig.network.assetsPathname,
/assets/ '/assets/',
${ hash } hash,
/logo.png '/logo.png',
` : undefined; ].join('') : undefined;
return <Image className={ className } src={ logoSrc } alt={ `${ name || 'token' } logo` } fallback={ <EmptyElement/> }/>; return (
<Image
className={ className }
src={ logoSrc }
alt={ `${ name || 'token' } logo` }
fallback={ <EmptyElement className={ className } letter={ name?.slice(0, 1) || 'U' }/> }
/>
);
}; };
export default React.memo(chakra(TokenLogo)); export default React.memo(chakra(TokenLogo));
import { Center, Link, Text, chakra } from '@chakra-ui/react'; import { Flex, Text, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import link from 'lib/link/link'; import AddressLink from 'ui/shared/address/AddressLink';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
...@@ -12,16 +12,12 @@ interface Props { ...@@ -12,16 +12,12 @@ interface Props {
} }
const TokenSnippet = ({ symbol, hash, name, className }: Props) => { const TokenSnippet = ({ symbol, hash, name, className }: Props) => {
const url = link('token_index', { hash });
return ( return (
<Center className={ className } columnGap={ 1 }> <Flex className={ className } alignItems="center" columnGap={ 1 } w="100%">
<TokenLogo boxSize={ 5 } hash={ hash } name={ name }/> <TokenLogo boxSize={ 5 } hash={ hash } name={ name }/>
<Link href={ url } target="_blank"> <AddressLink hash={ hash } alias={ name } type="token"/>
{ name }
</Link>
{ symbol && <Text variant="secondary">({ symbol })</Text> } { symbol && <Text variant="secondary">({ symbol })</Text> }
</Center> </Flex>
); );
}; };
......
import { Hide, Show, Text, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { QueryKeys } from 'types/client/queries';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
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 TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferSkeletonMobile from 'ui/shared/TokenTransfer/TokenTransferSkeletonMobile';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
interface Props {
isLoading?: boolean;
isDisabled?: boolean;
path: string;
queryName: QueryKeys.txTokenTransfers;
queryIds?: Array<string>;
baseAddress?: string;
showTxInfo?: boolean;
txHash?: string;
}
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true, txHash }: Props) => {
const { isError, isLoading, data, pagination } = useQueryWithPages({
apiPath: path,
queryName,
queryIds,
options: { enabled: !isDisabled },
});
const isPaginatorHidden = pagination.page === 1 && !pagination.hasNextPage;
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 }/>
</Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (!data.items?.length) {
return <Text as="span">There are no token transfers</Text>;
}
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ isPaginatorHidden ? 0 : 80 }/>
</Hide>
<Show below="lg">
<TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>
</Show>
</>
);
})();
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>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ content }
</>
);
};
export default React.memo(TokenTransfer);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import TokenTransferListItem from 'ui/shared/TokenTransfer/TokenTransferListItem';
interface Props {
data: Array<TokenTransfer>;
baseAddress?: string;
showTxInfo?: boolean;
}
const TokenTransferList = ({ data, baseAddress, showTxInfo }: Props) => {
return (
<Box>
{ data.map((item, index) => <TokenTransferListItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>) }
</Box>
);
};
export default TokenTransferList;
import { Text, Flex, Tag, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import eastArrowIcon from 'icons/arrows/east.svg';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
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 { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & {
baseAddress?: string;
showTxInfo?: boolean;
}
const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => {
const value = (() => {
if (!('value' in total)) {
return null;
}
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`;
return (
<AccountListItemMobile rowGap={ 3 }>
<Flex w="100%" flexWrap="wrap" rowGap={ 1 } position="relative">
<TokenSnippet hash={ token.address } w="auto" maxW="calc(100% - 140px)" name={ token.name || 'Unnamed token' }/>
<Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</Tag>
{ showTxInfo && <AdditionalInfoButton position="absolute" top={ 0 } right={ 0 }/> }
</Flex>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
{ showTxInfo && (
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Txn hash</Text>
<Address display="inline-flex" maxW="100%">
<AddressLink type="transaction" hash={ txHash }/>
</Address>
</Flex>
) }
<Flex w="100%" columnGap={ 3 }>
<Address width={ addressWidth }>
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address>
{ baseAddress ?
<InOutTag baseAddress={ baseAddress } addressFrom={ from.hash } w="50px" textAlign="center"/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
}
<Address width={ addressWidth }>
<AddressIcon hash={ to.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash }/>
</Address>
</Flex>
{ value && (
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text>
<Text variant="secondary">{ value }</Text>
</Flex>
) }
</AccountListItemMobile>
);
};
export default React.memo(TokenTransferListItem);
import { Box, Icon, Link } from '@chakra-ui/react';
import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
hash: string;
id: string;
}
const TokenTransferNft = ({ hash, id }: Props) => {
return (
<Link href={ link('token_instance_item', { hash, id }) } overflow="hidden" whiteSpace="nowrap" display="flex" alignItems="center" w="100%">
<Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/>
<Box maxW="calc(100% - 34px)">
<HashStringShortenDynamic hash={ id } fontWeight={ 500 }/>
</Box>
</Link>
);
};
export default React.memo(TokenTransferNft);
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TokenTransferSkeletonMobile = ({ showTxInfo, txHash }: { showTxInfo?: boolean; txHash?: string }) => {
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
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="50px"/>
{ showTxInfo && <Skeleton w="24px" ml="auto"/> }
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px"/>
<Skeleton w="24px"/>
<Skeleton w="90px"/>
</Flex>
{ showTxInfo && (
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
) }
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w="50px" mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="45px"/>
<Skeleton w="90px"/>
</Flex>
</Flex>
)) }
</Box>
</>
);
};
export default TokenTransferSkeletonMobile;
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokenTransferTableItem from 'ui/shared/TokenTransfer/TokenTransferTableItem';
interface Props {
data: Array<TokenTransfer>;
baseAddress?: string;
showTxInfo?: boolean;
top: number;
}
const TokenTransferTable = ({ data, baseAddress, showTxInfo, top }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
{ showTxInfo && <Th width="44px"></Th> }
<Th width="185px">Token</Th>
<Th width="160px">Token ID</Th>
{ showTxInfo && <Th width="25%">Txn hash</Th> }
<Th width="25%">From</Th>
{ baseAddress && <Th width="50px" px={ 0 }/> }
<Th width="25%">To</Th>
<Th width="25%" isNumeric>Value</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(TokenTransferTable);
import { Tr, Td, Tag, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
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 { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & {
baseAddress?: string;
showTxInfo?: boolean;
}
const TxInternalTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => {
const value = (() => {
if (!('value' in total)) {
return '-';
}
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
return (
<Tr alignItems="top">
{ showTxInfo && (
<Td>
<AdditionalInfoButton/>
</Td>
) }
<Td>
<Flex flexDir="column" alignItems="flex-start">
<TokenSnippet hash={ token.address } name={ token.name || 'Unnamed token' } lineHeight="30px"/>
<Tag mt={ 1 }>{ token.type }</Tag>
<Tag colorScheme="orange" mt={ 2 }>{ getTokenTransferTypeText(type) }</Tag>
</Flex>
</Td>
<Td lineHeight="30px">
{ 'token_id' in total ? <TokenTransferNft hash={ token.address } id={ total.token_id }/> : '-' }
</Td>
{ showTxInfo && (
<Td>
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px">
<AddressLink type="transaction" hash={ txHash }/>
</Address>
</Td>
) }
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address>
</Td>
{ baseAddress && (
<Td px={ 0 }>
<InOutTag baseAddress={ baseAddress } addressFrom={ from.hash } w="50px" textAlign="center" mt="3px"/>
</Td>
) }
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon hash={ to.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 }/>
</Address>
</Td>
<Td isNumeric verticalAlign="top" lineHeight="30px">
{ value }
</Td>
</Tr>
);
};
export default React.memo(TxInternalTableItem);
import type { TokenTransfer } from 'types/api/tokenTransfer';
export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => {
if (Array.isArray(item.total)) {
item.total.forEach((total) => {
result.push({ ...item, total });
});
} else {
result.push(item);
}
return result;
};
export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
switch (type) {
case 'token_minting':
return 'Token minting';
case 'token_burning':
return 'Token burning';
case 'token_spawning':
return 'Token creating';
case 'token_transfer':
return 'Token transfer';
}
};
...@@ -7,7 +7,7 @@ import HashStringShorten from 'ui/shared/HashStringShorten'; ...@@ -7,7 +7,7 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { interface Props {
type?: 'address' | 'transaction' | 'token' | 'block'; type?: 'address' | 'transaction' | 'token' | 'block' | 'token_instance_item';
alias?: string | null; alias?: string | null;
className?: string; className?: string;
hash: string; hash: string;
...@@ -23,6 +23,8 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, ...@@ -23,6 +23,8 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
url = link('tx', { id: id || hash }); url = link('tx', { id: id || hash });
} else if (type === 'token') { } else if (type === 'token') {
url = link('token_index', { hash: id || hash }); url = link('token_index', { hash: id || hash });
} else if (type === 'token_instance_item') {
url = link('token_instance_item', { hash, id });
} else if (type === 'block') { } else if (type === 'block') {
url = link('block', { id: id || hash }); url = link('block', { id: id || hash });
} else { } else {
...@@ -39,11 +41,11 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, ...@@ -39,11 +41,11 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
} }
switch (truncation) { switch (truncation) {
case 'constant': case 'constant':
return <HashStringShorten hash={ hash }/>; return <HashStringShorten hash={ id || hash }/>;
case 'dynamic': case 'dynamic':
return <HashStringShortenDynamic hash={ hash } fontWeight={ fontWeight }/>; return <HashStringShortenDynamic hash={ id || hash } fontWeight={ fontWeight }/>;
case 'none': case 'none':
return <span>{ hash }</span>; return <span>{ id || hash }</span>;
} }
})(); })();
......
import { useColorModeValue, useToken } from '@chakra-ui/react';
import * as d3 from 'd3'; import * as d3 from 'd3';
import React from 'react'; import React from 'react';
...@@ -13,6 +14,8 @@ interface Props extends React.SVGProps<SVGPathElement> { ...@@ -13,6 +14,8 @@ interface Props extends React.SVGProps<SVGPathElement> {
const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }: Props) => { const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }: Props) => {
const ref = React.useRef(null); const ref = React.useRef(null);
const gradientStopColor = useToken('colors', useColorModeValue('whiteAlpha.200', 'blackAlpha.100'));
React.useEffect(() => { React.useEffect(() => {
if (disableAnimation) { if (disableAnimation) {
d3.select(ref.current).attr('opacity', 1); d3.select(ref.current).attr('opacity', 1);
...@@ -39,8 +42,8 @@ const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }: ...@@ -39,8 +42,8 @@ const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }:
{ color && ( { color && (
<defs> <defs>
<linearGradient id={ `gradient-${ color }` } x1="0%" x2="0%" y1="0%" y2="100%"> <linearGradient id={ `gradient-${ color }` } x1="0%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stopColor={ color } stopOpacity={ 0.8 }/> <stop offset="2%" stopColor={ color }/>
<stop offset="100%" stopColor={ color } stopOpacity={ 0.02 }/> <stop offset="78%" stopColor={ gradientStopColor }/>
</linearGradient> </linearGradient>
</defs> </defs>
) } ) }
......
...@@ -69,6 +69,7 @@ const ChartLine = ({ xScale, yScale, data, animation, ...props }: Props) => { ...@@ -69,6 +69,7 @@ const ChartLine = ({ xScale, yScale, data, animation, ...props }: Props) => {
ref={ ref } ref={ ref }
d={ line(data) || undefined } d={ line(data) || undefined }
strokeWidth={ 1 } strokeWidth={ 1 }
strokeLinecap="round"
fill="none" fill="none"
opacity={ 0 } opacity={ 0 }
{ ...props } { ...props }
......
import { useColorModeValue, useToken, chakra } from '@chakra-ui/react';
import React from 'react';
// eslint-disable-next-line max-len
const d = 'M2 87.8491C2 87.8491 33.0576 108.005 66.5621 87.8491C100.067 67.693 104.693 112.847 115.444 112.847C126.196 112.847 127.564 -14.2956 150.132 4.10659C172.701 22.5087 204.973 118.132 231.009 87.8491C257.044 57.5664 282.524 27.2837 300.355 57.5664C318.185 87.8491 419.225 111.026 439.651 57.5664C460.077 4.10659 479.504 244.505 516.708 244.505C553.911 244.505 560.47 122.168 589.929 144.014C619.388 165.861 604.48 198.172 633.774 198.172C663.068 198.172 704.562 89 704.562 89';
const INCREMENT = 3;
const ChartLineLoader = ({ className }: { className?: string }) => {
const ref = React.useRef<SVGPathElement>(null);
const raf = React.useRef<number>();
const offset = React.useRef(0);
const lineBgColor = useToken('colors', useColorModeValue('gray.200', 'gray.500'));
const lineFgColor = useToken('colors', useColorModeValue('gray.400', 'gray.300'));
const gradientStopColor = useToken('colors', useColorModeValue('whiteAlpha.200', 'blackAlpha.100'));
React.useEffect(() => {
const length = ref.current?.getTotalLength() || 0;
ref.current?.setAttribute('stroke-dasharray', `${ length },${ length }`);
const animatePath = () => {
ref.current?.setAttribute('stroke-dashoffset', `${ length - offset.current }`);
const nextOffset = offset.current + INCREMENT <= length ? offset.current + INCREMENT : 0;
offset.current = nextOffset;
raf.current = window.requestAnimationFrame(animatePath);
};
raf.current = window.requestAnimationFrame(animatePath);
return () => {
raf.current && window.cancelAnimationFrame(raf.current);
};
}, []);
return (
<svg className={ className } width="100%" viewBox="0 0 707 272" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="chart_line_loader" x1="0" y1="0" x2="0" y2="272" gradientUnits="userSpaceOnUse">
<stop offset="0.02" stopColor="#D9D9D9"/>
<stop offset="0.78" stopColor={ gradientStopColor }/>
</linearGradient>
</defs>
<path
// eslint-disable-next-line max-len
d="M2 87.8491C2 87.8491 33.0576 108.005 66.5621 87.8491C100.067 67.693 104.693 112.847 115.444 112.847C126.196 112.847 127.564 -14.2956 150.132 4.10659C172.701 22.5087 204.973 118.132 231.009 87.8491C257.044 57.5664 282.524 27.2837 300.355 57.5664C318.185 87.8491 419.225 111.026 439.651 57.5664C460.077 4.10659 479.504 244.505 516.708 244.505C553.911 244.505 560.47 122.168 589.929 144.014C619.388 165.861 604.48 198.172 633.774 198.172C663.068 198.172 704.562 89 704.562 83.4575L702.467 231.992V268H0V85.5Z"
fill="url(#chart_line_loader)"
transform="translate(0,2)"
/>
<path
d={ d }
stroke={ lineBgColor }
strokeWidth="4"
strokeLinecap="round"
fill="none"
/>
<path
ref={ ref }
d={ d }
stroke={ lineFgColor }
strokeWidth="4"
strokeLinecap="round"
strokeDasharray="10000,100000"
strokeDashoffset="-10000"
fill="none"
/>
</svg>
);
};
export default chakra(ChartLineLoader);
...@@ -2,30 +2,35 @@ import { useToken, useColorModeValue } from '@chakra-ui/react'; ...@@ -2,30 +2,35 @@ import { useToken, useColorModeValue } from '@chakra-ui/react';
import * as d3 from 'd3'; import * as d3 from 'd3';
import React from 'react'; import React from 'react';
import type { TimeChartItem, ChartMargin, TimeChartData } from 'ui/shared/chart/types'; import type { TimeChartItem, TimeChartData } from 'ui/shared/chart/types';
import computeTooltipPosition from 'ui/shared/chart/utils/computeTooltipPosition';
import type { Pointer } from 'ui/shared/chart/utils/pointerTracker';
import { trackPointer } from 'ui/shared/chart/utils/pointerTracker';
interface Props { interface Props {
width?: number; width?: number;
height?: number; height?: number;
margin?: ChartMargin;
data: TimeChartData; data: TimeChartData;
xScale: d3.ScaleTime<number, number>; xScale: d3.ScaleTime<number, number>;
yScale: d3.ScaleLinear<number, number>; yScale: d3.ScaleLinear<number, number>;
anchorEl: SVGRectElement | null; anchorEl: SVGRectElement | null;
} }
const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, anchorEl, ...props }: Props) => { const TEXT_LINE_HEIGHT = 12;
const margin = React.useMemo(() => ({ const PADDING = 16;
top: 0, bottom: 0, left: 0, right: 0, const LINE_SPACE = 10;
..._margin, const POINT_SIZE = 16;
}), [ _margin ]);
const lineColor = useToken('colors', 'red.500'); const ChartTooltip = ({ xScale, yScale, width, height, data, anchorEl, ...props }: Props) => {
const textColor = useToken('colors', useColorModeValue('white', 'black')); const lineColor = useToken('colors', 'gray.400');
const bgColor = useToken('colors', useColorModeValue('gray.900', 'gray.400')); const titleColor = useToken('colors', 'blue.100');
const textColor = useToken('colors', 'white');
const markerBgColor = useToken('colors', useColorModeValue('black', 'white'));
const markerBorderColor = useToken('colors', useColorModeValue('white', 'black'));
const bgColor = useToken('colors', 'blackAlpha.900');
const ref = React.useRef(null); const ref = React.useRef(null);
const isPressed = React.useRef(false);
const drawLine = React.useCallback( const drawLine = React.useCallback(
(x: number) => { (x: number) => {
...@@ -40,38 +45,45 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, an ...@@ -40,38 +45,45 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, an
); );
const drawContent = React.useCallback( const drawContent = React.useCallback(
(x: number) => { (x: number, y: number) => {
const tooltipContent = d3.select(ref.current).select('.ChartTooltip__content'); const tooltipContent = d3.select(ref.current).select('.ChartTooltip__content');
tooltipContent.attr('transform', (cur, i, nodes) => { tooltipContent.attr('transform', (cur, i, nodes) => {
const OFFSET = 8;
const node = nodes[i] as SVGGElement | null; const node = nodes[i] as SVGGElement | null;
const nodeWidth = node?.getBoundingClientRect()?.width || 0; const { width: nodeWidth, height: nodeHeight } = node?.getBoundingClientRect() || { width: 0, height: 0 };
const translateX = nodeWidth + x + OFFSET > (width || 0) ? x - nodeWidth - OFFSET : x + OFFSET; const [ translateX, translateY ] = computeTooltipPosition({
return `translate(${ translateX }, ${ margin.top + 30 })`; canvasWidth: width || 0,
canvasHeight: height || 0,
nodeWidth,
nodeHeight,
pointX: x,
pointY: y,
offset: POINT_SIZE,
});
return `translate(${ translateX }, ${ translateY })`;
}); });
tooltipContent tooltipContent
.select('.ChartTooltip__contentTitle') .select('.ChartTooltip__contentDate')
.text(d3.timeFormat('%b %d, %Y')(xScale.invert(x))); .text(d3.timeFormat('%e %b %Y')(xScale.invert(x)));
}, },
[ xScale, margin, width ], [ xScale, width, height ],
); );
const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => { const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => {
d3.selectAll('.ChartTooltip__value') d3.selectAll('.ChartTooltip__value')
.filter((td, tIndex) => tIndex === i) .filter((td, tIndex) => tIndex === i)
.text(d.value.toLocaleString()); .text(data[i].valueFormatter?.(d.value) || d.value.toLocaleString());
}, []); }, [ data ]);
const drawCircles = React.useCallback((event: MouseEvent) => { const drawPoints = React.useCallback((x: number) => {
const [ x ] = d3.pointer(event, anchorEl);
const xDate = xScale.invert(x); const xDate = xScale.invert(x);
const bisectDate = d3.bisector<TimeChartItem, unknown>((d) => d.date).left; const bisectDate = d3.bisector<TimeChartItem, unknown>((d) => d.date).left;
let baseXPos = 0; let baseXPos = 0;
let baseYPos = 0;
d3.select(ref.current) d3.select(ref.current)
.selectAll('.ChartTooltip__linePoint') .selectAll('.ChartTooltip__point')
.attr('transform', (cur, i) => { .attr('transform', (cur, i) => {
const index = bisectDate(data[i].items, xDate, 1); const index = bisectDate(data[i].items, xDate, 1);
const d0 = data[i].items[index - 1]; const d0 = data[i].items[index - 1];
...@@ -84,97 +96,162 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, an ...@@ -84,97 +96,162 @@ const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, an
} }
const xPos = xScale(d.date); const xPos = xScale(d.date);
const yPos = yScale(d.value);
if (i === 0) { if (i === 0) {
baseXPos = xPos; baseXPos = xPos;
baseYPos = yPos;
} }
const yPos = yScale(d.value);
updateDisplayedValue(d, i); updateDisplayedValue(d, i);
return `translate(${ xPos }, ${ yPos })`; return `translate(${ xPos }, ${ yPos })`;
}); });
return baseXPos; return [ baseXPos, baseYPos ];
}, [ anchorEl, data, updateDisplayedValue, xScale, yScale ]); }, [ data, updateDisplayedValue, xScale, yScale ]);
const draw = React.useCallback((pointer: Pointer) => {
if (pointer.point) {
const [ baseXPos, baseYPos ] = drawPoints(pointer.point[0]);
drawLine(baseXPos);
drawContent(baseXPos, baseYPos);
}
}, [ drawPoints, drawLine, drawContent ]);
const showContent = React.useCallback(() => {
d3.select(ref.current).attr('opacity', 1);
d3.select(ref.current)
.selectAll('.ChartTooltip__point')
.attr('opacity', 1);
}, []);
const hideContent = React.useCallback(() => {
d3.select(ref.current).attr('opacity', 0);
}, []);
const createPointerTracker = React.useCallback((event: PointerEvent, isSubsequentCall?: boolean) => {
let isShown = false;
let isPressed = event.pointerType === 'mouse' && event.type === 'pointerdown' && !isSubsequentCall;
const followPoints = React.useCallback((event: MouseEvent) => { if (isPressed) {
const baseXPos = drawCircles(event); hideContent();
drawLine(baseXPos); }
drawContent(baseXPos);
}, [ drawCircles, drawLine, drawContent ]); trackPointer(event, {
move: (pointer) => {
if (!pointer.point || isPressed) {
return;
}
draw(pointer);
if (!isShown) {
showContent();
isShown = true;
}
},
out: () => {
hideContent();
isShown = false;
},
end: (tracker) => {
hideContent();
const isOutside = tracker.sourceEvent?.offsetX && width && (tracker.sourceEvent.offsetX > width || tracker.sourceEvent.offsetX < 0);
if (!isOutside && isPressed) {
window.setTimeout(() => {
createPointerTracker(event, true);
}, 0);
}
isShown = false;
isPressed = false;
},
});
}, [ draw, hideContent, showContent, width ]);
React.useEffect(() => { React.useEffect(() => {
const anchorD3 = d3.select(anchorEl); const anchorD3 = d3.select(anchorEl);
anchorD3 anchorD3
.on('mousedown.tooltip', () => { .on('touchmove.tooltip', (event: PointerEvent) => event.preventDefault()) // prevent scrolling
isPressed.current = true; .on('pointerenter.tooltip pointerdown.tooltip', (event: PointerEvent) => {
d3.select(ref.current).attr('opacity', 0); createPointerTracker(event);
})
.on('mouseup.tooltip', () => {
isPressed.current = false;
})
.on('mouseout.tooltip', () => {
d3.select(ref.current).attr('opacity', 0);
})
.on('mouseover.tooltip', () => {
d3.select(ref.current).attr('opacity', 1);
})
.on('mousemove.tooltip', (event: MouseEvent) => {
if (!isPressed.current) {
d3.select(ref.current).attr('opacity', 1);
d3.select(ref.current)
.selectAll('.ChartTooltip__linePoint')
.attr('opacity', 1);
followPoints(event);
}
}); });
d3.select('body').on('mouseup.tooltip', function(event) {
const isOutside = event.target !== anchorD3.node();
if (isOutside) {
isPressed.current = false;
}
});
return () => { return () => {
anchorD3.on('mousedown.tooltip mouseup.tooltip mouseout.tooltip mouseover.tooltip mousemove.tooltip', null); anchorD3.on('touchmove.tooltip pointerenter.tooltip pointerdown.tooltip', null);
d3.select('body').on('mouseup.tooltip', null);
}; };
}, [ anchorEl, followPoints ]); }, [ anchorEl, createPointerTracker, draw, hideContent, showContent ]);
return ( return (
<g ref={ ref } opacity={ 0 } { ...props }> <g ref={ ref } opacity={ 0 } { ...props }>
<line className="ChartTooltip__line" stroke={ lineColor }/> <line className="ChartTooltip__line" stroke={ lineColor } strokeDasharray="3"/>
{ data.map(({ name }) => (
<circle
key={ name }
className="ChartTooltip__point"
r={ POINT_SIZE / 2 }
opacity={ 0 }
fill={ markerBgColor }
stroke={ markerBorderColor }
strokeWidth={ 4 }
/>
)) }
<g className="ChartTooltip__content"> <g className="ChartTooltip__content">
<rect className="ChartTooltip__contentBg" rx={ 8 } ry={ 8 } fill={ bgColor } width={ 125 } height={ data.length * 22 + 34 }/> <rect
<text className="ChartTooltip__contentBg"
className="ChartTooltip__contentTitle" rx={ 12 }
transform="translate(8,20)" ry={ 12 }
fontSize="12px" fill={ bgColor }
fontWeight="bold" width={ 200 }
fill={ textColor } height={ 2 * PADDING + (data.length + 1) * TEXT_LINE_HEIGHT + data.length * LINE_SPACE }
pointerEvents="none"
/> />
<g> <g transform={ `translate(${ PADDING },${ PADDING })` }>
{ data.map(({ name, color }, index) => ( <text
<g key={ name } className="ChartTooltip__contentLine" transform={ `translate(12,${ 40 + index * 20 })` }> className="ChartTooltip__contentTitle"
<circle r={ 4 } fill={ color }/> transform="translate(0,0)"
<text fontSize="12px"
transform="translate(10,4)" fontWeight="500"
className="ChartTooltip__value" fill={ titleColor }
fontSize="12px" dominantBaseline="hanging"
fill={ textColor } >
pointerEvents="none" Date
/> </text>
</g> <text
)) } className="ChartTooltip__contentDate"
transform="translate(80,0)"
fontSize="12px"
fontWeight="500"
fill={ textColor }
dominantBaseline="hanging"
/>
</g> </g>
{ data.map(({ name }, index) => (
<g
key={ name }
transform={ `translate(${ PADDING },${ PADDING + (index + 1) * (LINE_SPACE + TEXT_LINE_HEIGHT) })` }
>
<text
className="ChartTooltip__contentTitle"
transform="translate(0,0)"
fontSize="12px"
fontWeight="500"
fill={ titleColor }
dominantBaseline="hanging"
>
{ name }
</text>
<text
transform="translate(80,0)"
className="ChartTooltip__value"
fontSize="12px"
fill={ textColor }
dominantBaseline="hanging"
/>
</g>
)) }
</g> </g>
{ data.map(({ name, color }) => (
<circle key={ name } className="ChartTooltip__linePoint" r={ 4 } opacity={ 0 } fill={ color } stroke="#FFF" strokeWidth={ 1 }/>
)) }
</g> </g>
); );
}; };
......
...@@ -19,6 +19,7 @@ export interface TimeChartDataItem { ...@@ -19,6 +19,7 @@ export interface TimeChartDataItem {
items: Array<TimeChartItem>; items: Array<TimeChartItem>;
name: string; name: string;
color?: string; color?: string;
valueFormatter?: (value: number) => string;
} }
export type TimeChartData = Array<TimeChartDataItem>; export type TimeChartData = Array<TimeChartDataItem>;
import _clamp from 'lodash/clamp';
interface Params {
pointX: number;
pointY: number;
offset: number;
nodeWidth: number;
nodeHeight: number;
canvasWidth: number;
canvasHeight: number;
}
export default function computeTooltipPosition({ pointX, pointY, canvasWidth, canvasHeight, nodeWidth, nodeHeight, offset }: Params): [ number, number ] {
// right
if (pointX + offset + nodeWidth <= canvasWidth) {
const x = pointX + offset;
const y = _clamp(pointY - nodeHeight / 2, 0, canvasHeight - nodeHeight);
return [ x, y ];
}
// left
if (nodeWidth + offset <= pointX) {
const x = pointX - offset - nodeWidth;
const y = _clamp(pointY - nodeHeight / 2, 0, canvasHeight - nodeHeight);
return [ x, y ];
}
// top
if (nodeHeight + offset <= pointY) {
const x = _clamp(pointX - nodeWidth / 2, 0, canvasWidth - nodeWidth);
const y = pointY - offset - nodeHeight;
return [ x, y ];
}
// bottom
if (pointY + offset + nodeHeight <= canvasHeight) {
const x = _clamp(pointX - nodeWidth / 2, 0, canvasWidth - nodeWidth);
const y = pointY + offset;
return [ x, y ];
}
const x = _clamp(pointX / 2, 0, canvasWidth - nodeWidth);
const y = _clamp(pointY / 2, 0, canvasHeight - nodeHeight);
return [ x, y ];
}
import React from 'react'; import React from 'react';
export const BlueLinearGradient = { export const BlueLineGradient = {
id: 'blue-linear-gradient', id: 'blue-linear-gradient',
defs: () => ( defs: () => (
<linearGradient id="blue-linear-gradient"> <linearGradient id="blue-linear-gradient">
......
import * as d3 from 'd3';
export interface Pointer {
id: number;
point: [number, number] | null;
prev: [number, number] | null;
sourceEvent?: PointerEvent;
}
export interface PointerOptions {
start?: (tracker: Pointer) => void;
move?: (tracker: Pointer) => void;
out?: (tracker: Pointer) => void;
end?: (tracker: Pointer) => void;
}
export function trackPointer(event: PointerEvent, { start, move, out, end }: PointerOptions) {
const tracker: Pointer = {
id: event.pointerId,
point: null,
prev: null,
};
const id = event.pointerId;
const target = event.target as Element;
tracker.point = d3.pointer(event, target);
target.setPointerCapture(id);
d3.select(target)
.on(`pointerup.${ id } pointercancel.${ id } lostpointercapture.${ id }`, (sourceEvent: PointerEvent) => {
if (sourceEvent.pointerId !== id) {
return;
}
tracker.sourceEvent = sourceEvent;
d3.select(target).on(`.${ id }`, null);
target.releasePointerCapture(id);
end?.(tracker);
})
.on(`pointermove.${ id }`, (sourceEvent) => {
if (sourceEvent.pointerId !== id) {
return;
}
tracker.sourceEvent = sourceEvent;
tracker.prev = tracker.point;
tracker.point = d3.pointer(sourceEvent, target);
move?.(tracker);
})
.on(`pointerout.${ id }`, (e) => {
if (e.pointerId !== id) {
return;
}
tracker.sourceEvent = e;
tracker.point = null;
out?.(tracker);
});
start?.(tracker);
return [ 'pointerup', 'pointercancel', 'lostpointercapture', 'pointermove', 'pointerout' ].map((event) => `${ event }.${ id }`);
}
import type { TimeChartItem } from '../types';
export const sortByDateDesc = (a: TimeChartItem, b: TimeChartItem) => {
return a.date.getTime() - b.date.getTime();
};
import { Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer as TTokenTransfer } from 'types/api/tokenTransfer';
import TokenTransfer from 'ui/tx/TokenTransfer';
interface Props {
items: Array<TTokenTransfer>;
}
function getItemsNum(items: Array<TTokenTransfer>) {
const nonErc1155items = items.filter((item) => item.token.type !== 'ERC-1155').length;
const erc1155items = items
.filter((item) => item.token.type === 'ERC-1155')
.map((item) => {
if (Array.isArray(item.total)) {
return item.total.length;
}
return 1;
})
.reduce((sum, item) => sum + item, 0);
return nonErc1155items + erc1155items;
}
const TokenTransferList = ({ items }: Props) => {
const itemsNum = getItemsNum(items);
const hasScroll = itemsNum > 5;
const gradientStartColor = useColorModeValue('whiteAlpha.600', 'blackAlpha.600');
const gradientEndColor = useColorModeValue('whiteAlpha.900', 'blackAlpha.900');
return (
<Flex
flexDirection="column"
alignItems="flex-start"
rowGap={ 5 }
w="100%"
_after={ hasScroll ? {
position: 'absolute',
content: '""',
bottom: 0,
left: 0,
right: '20px',
height: '48px',
bgGradient: `linear(to-b, ${ gradientStartColor } 37.5%, ${ gradientEndColor } 77.5%)`,
} : undefined }
maxH={ hasScroll ? '200px' : 'auto' }
overflowY={ hasScroll ? 'scroll' : 'auto' }
pr={ hasScroll ? 5 : 0 }
pb={ hasScroll ? 10 : 0 }
>
{ items.map((item, index) => <TokenTransfer key={ index } { ...item }/>) }
</Flex>
);
};
export default React.memo(TokenTransferList);
...@@ -25,19 +25,12 @@ import TextSeparator from 'ui/shared/TextSeparator'; ...@@ -25,19 +25,12 @@ import TextSeparator from 'ui/shared/TextSeparator';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization'; import Utilization from 'ui/shared/Utilization';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton'; import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxRevertReason from 'ui/tx/details/TxRevertReason'; import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TokenTransferList from 'ui/tx/TokenTransferList';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData'; import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TOKEN_TRANSFERS = [
{ title: 'Tokens Transferred', hint: 'List of tokens transferred in the transaction.', type: 'token_transfer' },
{ title: 'Tokens Minted', hint: 'List of tokens minted in the transaction.', type: 'token_minting' },
{ title: 'Tokens Burnt', hint: 'List of tokens burnt in the transaction.', type: 'token_burning' },
{ title: 'Tokens Created', hint: 'List of tokens created in the transaction.', type: 'token_spawning' },
];
const TxDetails = () => { const TxDetails = () => {
const { data, isLoading, isError, socketStatus } = useFetchTxInfo(); const { data, isLoading, isError, socketStatus } = useFetchTxInfo();
...@@ -65,10 +58,11 @@ const TxDetails = () => { ...@@ -65,10 +58,11 @@ const TxDetails = () => {
...data.from.watchlist_names || [], ...data.from.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>); ].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const toAddress = data.to && data.to.hash ? data.to : data.created_contract;
const addressToTags = [ const addressToTags = [
...data.to.private_tags || [], ...toAddress.private_tags || [],
...data.to.public_tags || [], ...toAddress.public_tags || [],
...data.to.watchlist_names || [], ...toAddress.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>); ].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
return ( return (
...@@ -150,25 +144,34 @@ const TxDetails = () => { ...@@ -150,25 +144,34 @@ const TxDetails = () => {
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title={ data.to.is_contract ? 'Interacted with contract' : 'To' } title={ toAddress.is_contract ? 'Interacted with contract' : 'To' }
hint="Address (external or contract) receiving the transaction." hint="Address (external or contract) receiving the transaction."
flexWrap={{ base: 'wrap', lg: 'nowrap' }} flexWrap={{ base: 'wrap', lg: 'nowrap' }}
columnGap={ 3 } columnGap={ 3 }
> >
<Address> { data.to && data.to.hash ? (
<AddressIcon hash={ data.to.hash }/> <Address>
<AddressLink ml={ 2 } hash={ data.to.hash }/> <AddressIcon hash={ toAddress.hash }/>
<CopyToClipboard text={ data.to.hash }/> <AddressLink ml={ 2 } hash={ toAddress.hash }/>
</Address> <CopyToClipboard text={ toAddress.hash }/>
{ data.to.name && <Text>{ data.to.name }</Text> } </Address>
{ data.to.is_contract && data.result === 'success' && ( ) : (
<Flex width="100%" whiteSpace="pre">
<span>[Contract </span>
<AddressLink hash={ toAddress.hash }/>
<span> created]</span>
<CopyToClipboard text={ toAddress.hash }/>
</Flex>
) }
{ toAddress.name && <Text>{ toAddress.name }</Text> }
{ toAddress.is_contract && data.result === 'success' && (
<Tooltip label="Contract execution completed"> <Tooltip label="Contract execution completed">
<chakra.span display="inline-flex"> <chakra.span display="inline-flex">
<Icon as={ successIcon } boxSize={ 4 } color="green.500" cursor="pointer"/> <Icon as={ successIcon } boxSize={ 4 } color="green.500" cursor="pointer"/>
</chakra.span> </chakra.span>
</Tooltip> </Tooltip>
) } ) }
{ data.to.is_contract && Boolean(data.status) && data.result !== 'success' && ( { toAddress.is_contract && Boolean(data.status) && data.result !== 'success' && (
<Tooltip label="Error occured during contract execution"> <Tooltip label="Error occured during contract execution">
<chakra.span display="inline-flex"> <chakra.span display="inline-flex">
<Icon as={ errorIcon } boxSize={ 4 } color="red.500" cursor="pointer"/> <Icon as={ errorIcon } boxSize={ 4 } color="red.500" cursor="pointer"/>
...@@ -181,22 +184,7 @@ const TxDetails = () => { ...@@ -181,22 +184,7 @@ const TxDetails = () => {
</Flex> </Flex>
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
{ TOKEN_TRANSFERS.map(({ title, hint, type }) => { { data.token_transfers && <TxDetailsTokenTransfers data={ data.token_transfers } txHash={ data.hash }/> }
const items = data.token_transfers?.filter((token) => token.type === type) || [];
if (items.length === 0) {
return null;
}
return (
<DetailsInfoItem
key={ type }
title={ title }
hint={ hint }
position="relative"
>
<TokenTransferList items={ items }/>
</DetailsInfoItem>
);
}) }
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/> <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
<DetailsInfoItem <DetailsInfoItem
......
import { Box, Flex, Text, Show, Hide } from '@chakra-ui/react'; import { Box, Text, Show, Hide } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { InternalTransactionsResponse, TxInternalsType, InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransaction } from 'types/api/internalTransaction';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterInput from 'ui/shared/FilterInput'; // import FilterInput from 'ui/shared/FilterInput';
import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter'; // import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import Pagination from 'ui/shared/Pagination';
import TxInternalsList from 'ui/tx/internals/TxInternalsList'; import TxInternalsList from 'ui/tx/internals/TxInternalsList';
import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDesktop'; import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDesktop';
import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile'; import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile';
...@@ -62,34 +62,34 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT ...@@ -62,34 +62,34 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT
} }
}; };
const searchFn = (searchTerm: string) => (item: InternalTransaction): boolean => { // const searchFn = (searchTerm: string) => (item: InternalTransaction): boolean => {
const formattedSearchTerm = searchTerm.toLowerCase(); // const formattedSearchTerm = searchTerm.toLowerCase();
return item.type.toLowerCase().includes(formattedSearchTerm) || // return item.type.toLowerCase().includes(formattedSearchTerm) ||
item.from.hash.toLowerCase().includes(formattedSearchTerm) || // item.from.hash.toLowerCase().includes(formattedSearchTerm) ||
item.to.hash.toLowerCase().includes(formattedSearchTerm); // item.to.hash.toLowerCase().includes(formattedSearchTerm);
}; // };
const TxInternals = () => { const TxInternals = () => {
const router = useRouter(); // filters are not implemented yet in api
const fetch = useFetch(); // const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
// const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>(); const [ sort, setSort ] = React.useState<Sort>();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>( const { data, isLoading, isError, pagination } = useQueryWithPages({
[ QueryKeys.txInternals, router.query.id ], apiPath: `/node-api/transactions/${ txInfo.data?.hash }/internal-transactions`,
async() => await fetch(`/node-api/transactions/${ router.query.id }/internal-transactions`), queryName: QueryKeys.txInternals,
{ queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status), options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
}, },
); });
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => { // const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => {
setFilters(nextValue); // setFilters(nextValue);
}, []); // }, []);
const handleSortToggle = React.useCallback((field: SortField) => { const handleSortToggle = React.useCallback((field: SortField) => {
return () => { return () => {
...@@ -120,8 +120,9 @@ const TxInternals = () => { ...@@ -120,8 +120,9 @@ const TxInternals = () => {
const content = (() => { const content = (() => {
const filteredData = data.items const filteredData = data.items
.filter(({ type }) => filters.length > 0 ? filters.includes(type) : true) .slice()
.filter(searchFn(searchTerm)) // .filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
// .filter(searchFn(searchTerm))
.sort(sortFn(sort)); .sort(sortFn(sort));
if (filteredData.length === 0) { if (filteredData.length === 0) {
...@@ -130,15 +131,20 @@ const TxInternals = () => { ...@@ -130,15 +131,20 @@ const TxInternals = () => {
return isMobile ? return isMobile ?
<TxInternalsList data={ filteredData }/> : <TxInternalsList data={ filteredData }/> :
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle }/>; <TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle } top={ isPaginatorHidden ? 0 : 80 }/>;
})(); })();
return ( return (
<Box> <Box>
<Flex mb={ 6 }> { !isPaginatorHidden && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ /* <Flex mb={ 6 }>
<TxInternalsFilter onFilterChange={ handleFilterChange } defaultFilters={ filters } appliedFiltersNum={ filters.length }/> <TxInternalsFilter onFilterChange={ handleFilterChange } defaultFilters={ filters } appliedFiltersNum={ filters.length }/>
<FilterInput onChange={ setSearchTerm } maxW="360px" ml={ 3 } size="xs" placeholder="Search by addresses, hash, method..."/> <FilterInput onChange={ setSearchTerm } maxW="360px" ml={ 3 } size="xs" placeholder="Search by addresses, hash, method..."/>
</Flex> </Flex> */ }
{ content } { content }
</Box> </Box>
); );
......
import { Box, Text } from '@chakra-ui/react'; import { Box, Text } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { LogsResponse } from 'types/api/log';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import useFetch from 'lib/hooks/useFetch'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import TxLogItem from 'ui/tx/logs/TxLogItem'; import TxLogItem from 'ui/tx/logs/TxLogItem';
import TxLogSkeleton from 'ui/tx/logs/TxLogSkeleton'; import TxLogSkeleton from 'ui/tx/logs/TxLogSkeleton';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
...@@ -16,17 +15,16 @@ import TxSocketAlert from 'ui/tx/TxSocketAlert'; ...@@ -16,17 +15,16 @@ import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxLogs = () => { const TxLogs = () => {
const router = useRouter();
const fetch = useFetch();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>( const { data, isLoading, isError, pagination } = useQueryWithPages({
[ QueryKeys.txLog, router.query.id ], apiPath: `/node-api/transactions/${ txInfo.data?.hash }/logs`,
async() => await fetch(`/node-api/transactions/${ router.query.id }/logs`), queryName: QueryKeys.txLogs,
{ queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status), options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
}, },
); });
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) { if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
...@@ -51,6 +49,11 @@ const TxLogs = () => { ...@@ -51,6 +49,11 @@ const TxLogs = () => {
return ( return (
<Box> <Box>
{ !isPaginatorHidden && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ data.items.map((item, index) => <TxLogItem key={ index } { ...item }/>) } { data.items.map((item, index) => <TxLogItem key={ index } { ...item }/>) }
</Box> </Box>
); );
......
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxTokenTransfer = () => {
const { isError, isLoading, data, socketStatus } = useFetchTxInfo({ updateDelay: 5 * SECOND });
if (!isLoading && !isError && !data.status) {
return socketStatus ? <TxSocketAlert status={ socketStatus }/> : <TxPendingAlert/>;
}
if (isError) {
return <DataFetchAlert/>;
}
const path = `/node-api/transactions/${ data?.hash }/token-transfers`;
return (
<TokenTransfer
isLoading={ isLoading }
isDisabled={ !data?.status || !data?.hash }
path={ path }
queryName={ QueryKeys.txTokenTransfers }
queryIds={ data?.hash ? [ data.hash ] : undefined }
showTxInfo={ false }
txHash={ data?.hash || '' }
/>
);
};
export default TxTokenTransfer;
...@@ -12,21 +12,20 @@ import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet'; ...@@ -12,21 +12,20 @@ import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
type Props = TTokenTransfer; type Props = TTokenTransfer;
const TokenTransfer = ({ token, total, to, from }: Props) => { const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => {
const isColumnLayout = token.type === 'ERC-1155' && Array.isArray(total); const isColumnLayout = token.type === 'ERC-1155' && Array.isArray(total);
const tokenSnippet = <TokenSnippet symbol={ token.symbol } hash={ token.address } name={ token.name } ml={ 3 }/>;
const content = (() => { const content = (() => {
switch (token.type) { switch (token.type) {
case 'ERC-20': { case 'ERC-20': {
const payload = total as Erc20TotalPayload; const payload = total as Erc20TotalPayload;
return ( return (
<Flex> <Flex flexWrap="wrap" columnGap={ 3 } rowGap={ 2 }>
<Text fontWeight={ 500 } as="span">For:{ space } <Text fontWeight={ 500 } as="span">For:{ space }
<CurrencyValue value={ payload.value } exchangeRate={ token.exchange_rate } fontWeight={ 600 }/> <CurrencyValue value={ payload.value } exchangeRate={ token.exchange_rate } fontWeight={ 600 }/>
</Text> </Text>
{ tokenSnippet } <TokenSnippet symbol={ token.symbol } hash={ token.address } name={ token.name } w="auto" flexGrow="1"/>
</Flex> </Flex>
); );
} }
...@@ -79,4 +78,4 @@ const TokenTransfer = ({ token, total, to, from }: Props) => { ...@@ -79,4 +78,4 @@ const TokenTransfer = ({ token, total, to, from }: Props) => {
); );
}; };
export default React.memo(TokenTransfer); export default React.memo(TxDetailsTokenTransfer);
import { Icon, Link, GridItem, Show, Flex } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import tokenIcon from 'icons/token.svg';
import link from 'lib/link/link';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TxDetailsTokenTransfer from './TxDetailsTokenTransfer';
interface Props {
data: Array<TokenTransfer>;
txHash: string;
}
const TOKEN_TRANSFERS_TYPES = [
{ title: 'Tokens transferred', hint: 'List of tokens transferred in the transaction.', type: 'token_transfer' },
{ title: 'Tokens minted', hint: 'List of tokens minted in the transaction.', type: 'token_minting' },
{ title: 'Tokens burnt', hint: 'List of tokens burnt in the transaction.', type: 'token_burning' },
{ title: 'Tokens created', hint: 'List of tokens created in the transaction.', type: 'token_spawning' },
];
const VISIBLE_ITEMS_NUM = 3;
const TxDetailsTokenTransfers = ({ data, txHash }: Props) => {
const viewAllUrl = link('tx', { id: txHash }, { tab: 'token_transfers' });
const formattedData = data.reduce(flattenTotal, []);
const transferGroups = TOKEN_TRANSFERS_TYPES.map((group) => ({
...group,
items: formattedData?.filter((token) => token.type === group.type) || [],
}));
const showViewAllLink = transferGroups.some(({ items }) => items.length > VISIBLE_ITEMS_NUM);
return (
<>
{ transferGroups.map(({ title, hint, type, items }) => {
if (items.length === 0) {
return null;
}
return (
<DetailsInfoItem
key={ type }
title={ title }
hint={ hint }
position="relative"
>
<Flex
flexDirection="column"
alignItems="flex-start"
rowGap={ 5 }
w="100%"
>
{ items.slice(0, VISIBLE_ITEMS_NUM).map((item, index) => <TxDetailsTokenTransfer key={ index } { ...item }/>) }
</Flex>
</DetailsInfoItem>
);
}) }
{ showViewAllLink && (
<>
<Show above="lg"><GridItem></GridItem></Show>
<GridItem fontSize="sm" alignItems="center" display="inline-flex" pl={{ base: '28px', lg: 0 }}>
<Icon as={ tokenIcon } boxSize={ 6 }/>
<NextLink href={ viewAllUrl } passHref>
<Link>View all</Link>
</NextLink>
</GridItem>
</>
) }
</>
);
};
export default React.memo(TxDetailsTokenTransfers);
...@@ -7,7 +7,7 @@ import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem'; ...@@ -7,7 +7,7 @@ import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem';
const TxInternalsList = ({ data }: { data: Array<InternalTransaction>}) => { const TxInternalsList = ({ data }: { data: Array<InternalTransaction>}) => {
return ( return (
<Box mt={ 6 }> <Box>
{ data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item }/>) } { data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item }/>) }
</Box> </Box>
); );
......
import { Skeleton, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
const TxInternalsSkeletonDesktop = () => { const TxInternalsSkeletonDesktop = () => {
return ( return (
<> <SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/>
</>
); );
}; };
......
...@@ -5,47 +5,41 @@ const TxInternalsSkeletonMobile = () => { ...@@ -5,47 +5,41 @@ const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
<> <Box>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }> { Array.from(Array(2)).map((item, index) => (
<Skeleton w="36px" flexShrink={ 0 }/> <Flex
<Skeleton w="100%"/> key={ index }
</Flex> rowGap={ 3 }
<Box> flexDirection="column"
{ Array.from(Array(2)).map((item, index) => ( paddingY={ 6 }
<Flex borderTopWidth="1px"
key={ index } borderColor={ borderColor }
rowGap={ 3 } _last={{
flexDirection="column" borderBottomWidth: '1px',
paddingY={ 6 } }}
borderTopWidth="1px" >
borderColor={ borderColor } <Flex h={ 6 }>
_last={{ <Skeleton w="100px" mr={ 2 }/>
borderBottomWidth: '1px', <Skeleton w="90px"/>
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="90px"/>
</Flex>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w={ 6 } mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="60px"/>
</Flex>
</Flex> </Flex>
)) } <Flex h={ 6 }>
</Box> <SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
</> <Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w={ 6 } mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="60px"/>
</Flex>
</Flex>
)) }
</Box>
); );
}; };
......
...@@ -13,14 +13,15 @@ interface Props { ...@@ -13,14 +13,15 @@ interface Props {
data: Array<InternalTransaction>; data: Array<InternalTransaction>;
sort: Sort | undefined; sort: Sort | undefined;
onSortToggle: (field: SortField) => () => void; onSortToggle: (field: SortField) => () => void;
top: number;
} }
const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => { const TxInternalsTable = ({ data, sort, onSortToggle, top }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)'; const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return ( return (
<Table variant="simple" size="sm" mt={ 6 }> <Table variant="simple" size="sm">
<Thead top={ 0 }> <Thead top={ top }>
<Tr> <Tr>
<Th width="28%">Type</Th> <Th width="28%">Type</Th>
<Th width="20%">From</Th> <Th width="20%">From</Th>
......
...@@ -28,16 +28,16 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -28,16 +28,16 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit:
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex> </Flex>
</Td> </Td>
<Td> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon hash={ from.hash }/> <AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address> </Address>
</Td> </Td>
<Td px={ 0 }> <Td px={ 0 } verticalAlign="middle">
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td> </Td>
<Td> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon hash={ to.hash }/> <AddressIcon hash={ to.hash }/>
<AddressLink hash={ to.hash } alias={ to.name } fontWeight="500" ml={ 2 }/> <AddressLink hash={ to.hash } alias={ to.name } fontWeight="500" ml={ 2 }/>
......
import { Text, Box, Show, Hide } from '@chakra-ui/react'; import { Text, Box, Show, Hide } from '@chakra-ui/react';
import React, { useState, useCallback } from 'react'; import React from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { QueryKeys } from 'types/client/queries'; import type { QueryKeys } from 'types/client/queries';
import type { Sort } from 'types/client/txs-sort';
import * as cookies from 'lib/cookies';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsHeader from './TxsHeader'; import TxsHeader from './TxsHeader';
import TxsListItem from './TxsListItem';
import TxsNewItemNotice from './TxsNewItemNotice';
import TxsSkeletonDesktop from './TxsSkeletonDesktop'; import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile'; import TxsSkeletonMobile from './TxsSkeletonMobile';
import TxsWithSort from './TxsWithSort'; import TxsTable from './TxsTable';
import useTxsSort from './useTxsSort';
type Props = { type Props = {
queryName: QueryKeys; queryName: QueryKeys.txsPending | QueryKeys.txsValidate | QueryKeys.blockTxs;
showDescription?: boolean; showDescription?: boolean;
stateFilter?: TTxsFilters['filter']; stateFilter?: TTxsFilters['filter'];
apiPath: string; apiPath: string;
showBlockInfo?: boolean;
} }
const TxsContent = ({ const TxsContent = ({
...@@ -27,63 +28,53 @@ const TxsContent = ({ ...@@ -27,63 +28,53 @@ const TxsContent = ({
showDescription, showDescription,
stateFilter, stateFilter,
apiPath, apiPath,
showBlockInfo = true,
}: Props) => { }: Props) => {
const [ sorting, setSorting ] = useState<Sort>(cookies.get(cookies.NAMES.TXS_SORT) as Sort || '');
// const [ filters, setFilters ] = useState<Partial<TTxsFilters>>({ type: [], method: [] });
const sort = useCallback((field: 'val' | 'fee') => () => {
setSorting((prevVal) => {
let newVal: Sort = '';
if (field === 'val') {
if (prevVal === 'val-asc') {
newVal = '';
} else if (prevVal === 'val-desc') {
newVal = 'val-asc';
} else {
newVal = 'val-desc';
}
}
if (field === 'fee') {
if (prevVal === 'fee-asc') {
newVal = '';
} else if (prevVal === 'fee-desc') {
newVal = 'fee-asc';
} else {
newVal = 'fee-desc';
}
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
}, [ ]);
const { const {
data,
isLoading,
isError,
pagination, pagination,
} = useQueryWithPages<TransactionsResponse>(apiPath, queryName, stateFilter && { filter: stateFilter }); ...queryResult
} = useQueryWithPages({
apiPath,
queryName,
filters: stateFilter ? { filter: stateFilter } : undefined,
});
// } = useQueryWithPages({ ...filters, filter: stateFilter, apiPath }); // } = useQueryWithPages({ ...filters, filter: stateFilter, apiPath });
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(queryResult);
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const content = (() => { const content = (() => {
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const txs = data?.items; if (isLoading) {
return (
if (!isLoading && !txs?.length) { <>
return <Text as="span">There are no transactions.</Text>; <Show below="lg" ssr={ false }><TxsSkeletonMobile showBlockInfo={ showBlockInfo }/></Show>
<Hide below="lg" ssr={ false }><TxsSkeletonDesktop showBlockInfo={ showBlockInfo }/></Hide>
</>
);
} }
if (!isLoading && txs) { const txs = data.items;
return <TxsWithSort txs={ txs } sorting={ sorting } sort={ sort }/>;
if (!txs.length) {
return <Text as="span">There are no transactions.</Text>;
} }
return ( return (
<> <>
<Show below="lg" ssr={ false }><TxsSkeletonMobile/></Show> <Show below="lg" ssr={ false }>
<Hide below="lg" ssr={ false }><TxsSkeletonDesktop/></Hide> <Box>
<TxsNewItemNotice>
{ ({ content }) => <Box>{ content }</Box> }
</TxsNewItemNotice>
{ txs.map(tx => <TxsListItem tx={ tx } key={ tx.hash } showBlockInfo={ showBlockInfo }/>) }
</Box>
</Show>
<Hide below="lg" ssr={ false }>
<TxsTable txs={ txs } sort={ setSortByField } sorting={ sorting } showBlockInfo={ showBlockInfo } top={ isPaginatorHidden ? 0 : 80 }/>
</Hide>
</> </>
); );
})(); })();
...@@ -91,7 +82,7 @@ const TxsContent = ({ ...@@ -91,7 +82,7 @@ const TxsContent = ({
return ( return (
<> <>
{ showDescription && <Box mb={{ base: 6, lg: 12 }}>Only the first 10,000 elements are displayed</Box> } { showDescription && <Box mb={{ base: 6, lg: 12 }}>Only the first 10,000 elements are displayed</Box> }
<TxsHeader mt={ -6 } sorting={ sorting } setSorting={ setSorting } paginationProps={ pagination }/> <TxsHeader mt={ -6 } sorting={ sorting } setSorting={ setSortByValue } paginationProps={ pagination } showPagination={ !isPaginatorHidden }/>
{ content } { content }
</> </>
); );
......
...@@ -14,14 +14,19 @@ import TxsSorting from 'ui/txs/TxsSorting'; ...@@ -14,14 +14,19 @@ import TxsSorting from 'ui/txs/TxsSorting';
type Props = { type Props = {
sorting: Sort; sorting: Sort;
setSorting: (val: Sort | ((val: Sort) => Sort)) => void; setSorting: (val: Sort) => void;
paginationProps: PaginationProps; paginationProps: PaginationProps;
className?: string; className?: string;
showPagination?: boolean;
} }
const TxsHeader = ({ sorting, setSorting, paginationProps, className }: Props) => { const TxsHeader = ({ sorting, setSorting, paginationProps, className, showPagination = true }: Props) => {
const isMobile = useIsMobile(false); const isMobile = useIsMobile(false);
if (!showPagination && !isMobile) {
return null;
}
return ( return (
<ActionBar className={ className }> <ActionBar className={ className }>
<HStack> <HStack>
...@@ -47,7 +52,7 @@ const TxsHeader = ({ sorting, setSorting, paginationProps, className }: Props) = ...@@ -47,7 +52,7 @@ const TxsHeader = ({ sorting, setSorting, paginationProps, className }: Props) =
placeholder="Search by addresses, hash, method..." placeholder="Search by addresses, hash, method..."
/> */ } /> */ }
</HStack> </HStack>
<Pagination { ...paginationProps }/> { showPagination && <Pagination { ...paginationProps }/> }
</ActionBar> </ActionBar>
); );
}; };
......
...@@ -20,19 +20,20 @@ import transactionIcon from 'icons/transactions.svg'; ...@@ -20,19 +20,20 @@ import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import getValueWithUnit from 'lib/getValueWithUnit'; import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from 'ui/txs/TxType'; import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx }: {tx: Transaction}) => { const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boolean}) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('blue.600', 'blue.300'); const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
return ( return (
<> <>
...@@ -42,7 +43,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => { ...@@ -42,7 +43,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
{ tx.tx_types.map(item => <TxType key={ item } type={ item }/>) } { tx.tx_types.map(item => <TxType key={ item } type={ item }/>) }
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/> <TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</HStack> </HStack>
<TxAdditionalInfoButton onClick={ onOpen }/> <AdditionalInfoButton onClick={ onOpen }/>
</Flex> </Flex>
<Flex justifyContent="space-between" lineHeight="24px" mt={ 3 }> <Flex justifyContent="space-between" lineHeight="24px" mt={ 3 }>
<Flex> <Flex>
...@@ -76,7 +77,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => { ...@@ -76,7 +77,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
{ tx.method } { tx.method }
</Text> </Text>
</Flex> </Flex>
{ tx.block !== null && ( { showBlockInfo && tx.block !== null && (
<Box mt={ 2 }> <Box mt={ 2 }>
<Text as="span">Block </Text> <Text as="span">Block </Text>
<Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link>
...@@ -99,10 +100,10 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => { ...@@ -99,10 +100,10 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
color="gray.500" color="gray.500"
/> />
<Address width="calc((100%-40px)/2)"> <Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.to.hash }/> <AddressIcon hash={ dataTo.hash }/>
<AddressLink <AddressLink
hash={ tx.to.hash } hash={ dataTo.hash }
alias={ tx.to.name } alias={ dataTo.name }
fontWeight="500" fontWeight="500"
ml={ 2 } ml={ 2 }
/> />
......
import { Alert, Spinner, Text, Link, chakra } from '@chakra-ui/react';
import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import React from 'react';
import { ROUTES } from 'lib/link/routes';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
interface InjectedProps {
content: React.ReactNode;
}
interface Props {
children: (props: InjectedProps) => JSX.Element;
className?: string;
}
function getSocketParams(router: NextRouter) {
if (router.pathname === ROUTES.txs.pattern && router.query.tab === 'validated' && !router.query.block_number) {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
if (router.pathname === ROUTES.txs.pattern && router.query.tab === 'pending' && !router.query.block_number) {
return { topic: 'transactions:new_pending_transaction' as const, event: 'pending_transaction' as const };
}
return {};
}
function assertIsNewTxResponse(response: unknown): response is { transaction: number } {
return typeof response === 'object' && response !== null && 'transaction' in response;
}
function assertIsNewPendingTxResponse(response: unknown): response is { pending_transaction: number } {
return typeof response === 'object' && response !== null && 'pending_transaction' in response;
}
const TxsNewItemNotice = ({ children, className }: Props) => {
const router = useRouter();
const [ num, setNum ] = React.useState(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const { topic, event } = getSocketParams(router);
const handleClick = React.useCallback(() => {
window.location.reload();
}, []);
const handleNewTxMessage = React.useCallback((response: { transaction: number } | { pending_transaction: number } | unknown) => {
if (assertIsNewTxResponse(response)) {
setNum((prev) => prev + response.transaction);
}
if (assertIsNewPendingTxResponse(response)) {
setNum((prev) => prev + response.pending_transaction);
}
}, []);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new transactions.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please click here to refresh the page.');
}, []);
const channel = useSocketChannel({
topic,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: !topic,
});
useSocketMessage({
channel,
event,
handler: handleNewTxMessage,
});
if (!topic && !event) {
return null;
}
const content = (() => {
if (socketAlert) {
return (
<Alert
className={ className }
status="warning"
p={ 4 }
borderRadius={ 0 }
onClick={ handleClick }
cursor="pointer"
>
{ socketAlert }
</Alert>
);
}
if (!num) {
return null;
}
return (
<Alert className={ className } status="warning" p={ 4 } fontWeight={ 400 }>
<Spinner size="sm" mr={ 3 }/>
<Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text>
<Link onClick={ handleClick }>Show in list</Link>
</Alert>
);
})();
return children({ content });
};
export default chakra(TxsNewItemNotice);
...@@ -3,10 +3,17 @@ import React from 'react'; ...@@ -3,10 +3,17 @@ import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
const TxsInternalsSkeletonDesktop = () => { interface Props {
showBlockInfo: boolean;
}
const TxsInternalsSkeletonDesktop = ({ showBlockInfo }: Props) => {
return ( return (
<Box mb={ 8 }> <Box mb={ 8 }>
<SkeletonTable columns={ [ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] }/> <SkeletonTable columns={ showBlockInfo ?
[ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] :
[ '32px', '20%', '18%', '15%', '292px', '18%', '18%' ]
}/>
</Box> </Box>
); );
}; };
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react'; import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
const TxInternalsSkeletonMobile = () => { interface Props {
showBlockInfo: boolean;
}
const TxInternalsSkeletonMobile = ({ showBlockInfo }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
...@@ -25,7 +29,7 @@ const TxInternalsSkeletonMobile = () => { ...@@ -25,7 +29,7 @@ const TxInternalsSkeletonMobile = () => {
<Skeleton w="100%" h="30px" mt={ 3 }/> <Skeleton w="100%" h="30px" mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 3 }/> <Skeleton w="50%" h={ 6 } mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/> <Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="100%" h={ 6 } mt={ 6 }/> { showBlockInfo && <Skeleton w="100%" h={ 6 } mt={ 6 }/> }
<Skeleton w="50%" h={ 6 } mt={ 2 }/> <Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/> <Skeleton w="50%" h={ 6 } mt={ 2 }/>
</Flex> </Flex>
......
...@@ -11,13 +11,12 @@ import React from 'react'; ...@@ -11,13 +11,12 @@ import React from 'react';
import type { Sort } from 'types/client/txs-sort'; import type { Sort } from 'types/client/txs-sort';
import * as cookies from 'lib/cookies';
import SortButton from 'ui/shared/SortButton'; import SortButton from 'ui/shared/SortButton';
interface Props { interface Props {
isActive: boolean; isActive: boolean;
sorting: Sort; sorting: Sort;
setSorting: (val: Sort | ((val: Sort) => Sort)) => void; setSorting: (val: Sort) => void;
} }
const SORT_OPTIONS = [ const SORT_OPTIONS = [
...@@ -32,14 +31,8 @@ const TxsSorting = ({ isActive, sorting, setSorting }: Props) => { ...@@ -32,14 +31,8 @@ const TxsSorting = ({ isActive, sorting, setSorting }: Props) => {
const { isOpen, onToggle } = useDisclosure(); const { isOpen, onToggle } = useDisclosure();
const setSortingFromMenu = React.useCallback((val: string | Array<string>) => { const setSortingFromMenu = React.useCallback((val: string | Array<string>) => {
setSorting((prevVal: Sort) => { const value = val as Sort | Array<Sort>;
let newVal: Sort = ''; setSorting(Array.isArray(value) ? value[0] : value);
if (val !== prevVal) {
newVal = val as Sort;
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
}, [ setSorting ]); }, [ setSorting ]);
return ( return (
......
...@@ -9,12 +9,23 @@ type Props = { ...@@ -9,12 +9,23 @@ type Props = {
} }
const TxsTab = ({ tab }: Props) => { const TxsTab = ({ tab }: Props) => {
if (tab === 'validated') {
return (
<TxsContent
queryName={ QueryKeys.txsValidate }
showDescription
stateFilter="validated"
apiPath="/node-api/transactions"
/>
);
}
return ( return (
<TxsContent <TxsContent
queryName={ QueryKeys.transactions } queryName={ QueryKeys.txsPending }
showDescription={ tab === 'validated' } stateFilter="pending"
stateFilter={ tab }
apiPath="/node-api/transactions" apiPath="/node-api/transactions"
showBlockInfo={ false }
/> />
); );
}; };
......
import { Link, Table, Tbody, Tr, Th, Icon } from '@chakra-ui/react'; import { Link, Table, Tbody, Tr, Th, Td, Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -8,24 +8,27 @@ import appConfig from 'configs/app/config'; ...@@ -8,24 +8,27 @@ import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import TheadSticky from 'ui/shared/TheadSticky'; import TheadSticky from 'ui/shared/TheadSticky';
import TxsNewItemNotice from './TxsNewItemNotice';
import TxsTableItem from './TxsTableItem'; import TxsTableItem from './TxsTableItem';
type Props = { type Props = {
txs: Array<Transaction>; txs: Array<Transaction>;
sort: (field: 'val' | 'fee') => () => void; sort: (field: 'val' | 'fee') => () => void;
sorting?: Sort; sorting?: Sort;
top: number;
showBlockInfo: boolean;
} }
const TxsTable = ({ txs, sort, sorting }: Props) => { const TxsTable = ({ txs, sort, sorting, top, showBlockInfo }: Props) => {
return ( return (
<Table variant="simple" minWidth="810px" size="xs"> <Table variant="simple" minWidth="810px" size="xs">
<TheadSticky top={ 80 }> <TheadSticky top={ top }>
<Tr> <Tr>
<Th width="54px"></Th> <Th width="54px"></Th>
<Th width="20%">Type</Th> <Th width="20%">Type</Th>
<Th width="18%">Txn hash</Th> <Th width="18%">Txn hash</Th>
<Th width="15%">Method</Th> <Th width="15%">Method</Th>
<Th width="11%">Block</Th> { showBlockInfo && <Th width="11%">Block</Th> }
<Th width={{ xl: '128px', base: '66px' }}>From</Th> <Th width={{ xl: '128px', base: '66px' }}>From</Th>
<Th width={{ xl: '36px', base: '0' }}></Th> <Th width={{ xl: '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '66px' }}>To</Th> <Th width={{ xl: '128px', base: '66px' }}>To</Th>
...@@ -46,10 +49,14 @@ const TxsTable = ({ txs, sort, sorting }: Props) => { ...@@ -46,10 +49,14 @@ const TxsTable = ({ txs, sort, sorting }: Props) => {
</Tr> </Tr>
</TheadSticky> </TheadSticky>
<Tbody> <Tbody>
<TxsNewItemNotice borderRadius={ 0 }>
{ ({ content }) => <Tr><Td colSpan={ 10 } p={ 0 }>{ content }</Td></Tr> }
</TxsNewItemNotice>
{ txs.map((item) => ( { txs.map((item) => (
<TxsTableItem <TxsTableItem
key={ item.hash } key={ item.hash }
tx={ item } tx={ item }
showBlockInfo={ showBlockInfo }
/> />
)) } )) }
</Tbody> </Tbody>
......
...@@ -23,6 +23,7 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -23,6 +23,7 @@ import type { Transaction } from 'types/api/transaction';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -30,11 +31,10 @@ import CurrencyValue from 'ui/shared/CurrencyValue'; ...@@ -30,11 +31,10 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from './TxType'; import TxType from './TxType';
const TxsTableItem = ({ tx }: {tx: Transaction}) => { const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boolean }) => {
const addressFrom = ( const addressFrom = (
<Address> <Address>
<Tooltip label={ tx.from.implementation_name }> <Tooltip label={ tx.from.implementation_name }>
...@@ -44,12 +44,14 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => { ...@@ -44,12 +44,14 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
</Address> </Address>
); );
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const addressTo = ( const addressTo = (
<Address> <Address>
<Tooltip label={ tx.to.implementation_name }> <Tooltip label={ dataTo.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.to.hash }/></Box> <Box display="flex"><AddressIcon hash={ dataTo.hash }/></Box>
</Tooltip> </Tooltip>
<AddressLink hash={ tx.to.hash } alias={ tx.to.name } fontWeight="500" ml={ 2 } truncation="constant"/> <AddressLink hash={ dataTo.hash } alias={ dataTo.name } fontWeight="500" ml={ 2 } truncation="constant"/>
</Address> </Address>
); );
...@@ -61,7 +63,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => { ...@@ -61,7 +63,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
{ ({ isOpen }) => ( { ({ isOpen }) => (
<> <>
<PopoverTrigger> <PopoverTrigger>
<TxAdditionalInfoButton isOpen={ isOpen }/> <AdditionalInfoButton isOpen={ isOpen }/>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent border="1px solid" borderColor={ infoBorderColor }> <PopoverContent border="1px solid" borderColor={ infoBorderColor }>
<PopoverBody> <PopoverBody>
...@@ -92,17 +94,19 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => { ...@@ -92,17 +94,19 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
<TruncatedTextTooltip label={ tx.method }> { tx.method ? (
<Tag <TruncatedTextTooltip label={ tx.method }>
colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' } <Tag colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }>
> { tx.method }
{ tx.method } </Tag>
</Tag> </TruncatedTextTooltip>
</TruncatedTextTooltip> ) : '-' }
</Td>
<Td>
{ tx.block && <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> }
</Td> </Td>
{ showBlockInfo && (
<Td>
{ tx.block && <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> }
</Td>
) }
<Show above="xl" ssr={ false }> <Show above="xl" ssr={ false }>
<Td> <Td>
{ addressFrom } { addressFrom }
......
import { Box, Show, Hide } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import sortTxs from 'lib/tx/sortTxs';
import TxsListItem from './TxsListItem';
import TxsTable from './TxsTable';
type Props = {
txs: TransactionsResponse['items'];
sorting?: Sort;
sort: (field: 'val' | 'fee') => () => void;
}
const TxsWithSort = ({
txs,
sorting,
sort,
}: Props) => {
const [ sortedTxs, setSortedTxs ] = useState<TransactionsResponse['items']>(sortTxs(txs, sorting));
useEffect(() => {
setSortedTxs(sortTxs(txs, sorting));
}, [ sorting, txs ]);
return (
<>
<Show below="lg" ssr={ false }><Box>{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Box></Show>
<Hide below="lg" ssr={ false }><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Hide>
</>
);
};
export default TxsWithSort;
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { BlockTransactionsResponse } from 'types/api/block';
import type { TransactionsResponsePending, TransactionsResponseValidated } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import * as cookies from 'lib/cookies';
import sortTxs from 'lib/tx/sortTxs';
type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse;
type HookResult = UseQueryResult<TxsResponse> & {
sorting: Sort;
setSortByField: (field: 'val' | 'fee') => () => void;
setSortByValue: (value: Sort) => void;
}
export default function useTxsSort(
queryResult: UseQueryResult<TxsResponse>,
): HookResult {
const [ sorting, setSorting ] = React.useState<Sort>(cookies.get(cookies.NAMES.TXS_SORT) as Sort);
const setSortByField = React.useCallback((field: 'val' | 'fee') => () => {
setSorting((prevVal) => {
let newVal: Sort = '';
if (field === 'val') {
if (prevVal === 'val-asc') {
newVal = '';
} else if (prevVal === 'val-desc') {
newVal = 'val-asc';
} else {
newVal = 'val-desc';
}
}
if (field === 'fee') {
if (prevVal === 'fee-asc') {
newVal = '';
} else if (prevVal === 'fee-desc') {
newVal = 'fee-asc';
} else {
newVal = 'fee-desc';
}
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
}, [ ]);
const setSortByValue = React.useCallback((value: Sort) => {
setSorting((prevVal: Sort) => {
let newVal: Sort = '';
if (value !== prevVal) {
newVal = value as Sort;
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
}, []);
return React.useMemo(() => {
if (queryResult.isError || queryResult.isLoading) {
return { ...queryResult, setSortByField, setSortByValue, sorting };
}
return {
...queryResult,
data: { ...queryResult.data, items: queryResult.data.items.slice().sort(sortTxs(sorting)) },
setSortByField,
setSortByValue,
sorting,
};
}, [ queryResult, setSortByField, setSortByValue, sorting ]);
}
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