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
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_RETURN_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_RETURN_URL__
NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__
# api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
......
......@@ -2,8 +2,8 @@ name: Run E2E tests k8s
on:
# push:
# pull_request:
workflow_dispatch
pull_request:
workflow_dispatch:
env:
K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }}
......
......@@ -3,8 +3,6 @@ name: Deploy review environment
on:
pull_request:
# push:
# branches-ignore:
# - 'main'
workflow_dispatch:
env:
......
......@@ -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_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_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
......
/* eslint-disable no-restricted-properties */
import type { AppItemOverview } from 'types/client/apps';
import type { FeaturedNetwork, NetworkExplorer, PreDefinedNetwork } from 'types/networks';
import type { ChainIndicatorId } from 'ui/home/indicators/types';
const getEnvValue = (env: string | undefined) => env?.replaceAll('\'', '"');
const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
......@@ -86,6 +87,9 @@ const config = Object.freeze({
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
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;
# 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 @@
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
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_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
NEXT_PUBLIC_NETWORK_NAME=POA
......
......@@ -8,18 +8,6 @@ blockscout:
_default: ENC[AES256_GCM,data:+q7GvdSeRQ0aY4grlQrdWUvxT9tzsRoii3W/ECwpVKs13lw84bGV+rbiEJXBYoOAw7SDz1Hr+KMy9SQzHvj12w==,iv:Xjo9+Nca6eDTZtUcDPAPfXYSAoP2gYmDX+JanCyPPkY=,tag:kQxLFsfN5YXft8ihgNvA+Q==,type:str]
MAILSLURP_EMAIL_ID:
_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:
_default: ENC[AES256_GCM,data:bNzimXY1TDUHgLijvgcJ8mw+9RXvCLchxhum77KY6ss5NUdoWuNuQjpC7M819XRPqhBmZkC2qBSX1ZRhcNQCB1OqGkRq,iv:JFmeCxsO2lkyScN8iVb8B/356SbP7KCMFffIPjGtN0o=,tag:A21hzDObJMAUhHQRWB7Z9w==,type:str]
ACCOUNT_SENDGRID_SENDER:
......@@ -39,9 +27,21 @@ blockscout:
DATABASE_URL:
_default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str]
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:
_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:
environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
......@@ -57,6 +57,12 @@ postgres:
POSTGRES_DB:
_default: ENC[AES256_GCM,data:QRCvQVzKOfOApg==,iv:eOdxyjmv6Zj5xINGXO0dD/GcDgD1J8e4OzLMyOneoDs=,tag:EBnJ7OT3u7vcBIbviRKaHg==,type:str]
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:
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]
......@@ -65,7 +71,7 @@ geth:
frontend:
environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,type:str]
_default: ENC[AES256_GCM,data:D/pLeRn7C40/rc7nNDCuK5dTuADPrHZl+J8hGC9xhwNRKZnkHySxkSzV,iv:z98n49jakK1mvlJpJ75BXj5gGyDHpBcAoCbaM0FmEA8=,tag:pLTa5kHmSi+bCN+8jeMhGQ==,type:str]
NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
......@@ -78,8 +84,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-11-02T07:59:42Z"
mac: ENC[AES256_GCM,data:OrV/dUWOtL23UFQLeIsKsGluTmse42d/4sgFMDs3UXdACsZu8twMt29Y/WaPHyq8Tpn5iYzhBLU6SCUmHxEhBNVzKBd5uCUbav1faS/zW6fSd9bEP7rmbUjaJGHliBkG3T4VCSZn53jR/OMNbSynIxZ0kRpVHr+RTcalaH7dLQ8=,iv:J3gXjFgFyZoPqL+VEnjkuKzA9UIIyK3UsvPWacBZKsY=,tag:/zY1jVjIm5BsDNJlfsg5uw==,type:str]
lastmodified: "2022-11-14T10:04:29Z"
mac: ENC[AES256_GCM,data:LitRw7GOBq9mHXXiWmfmcpw0Tbn8KESGZjFjDAlYWIlYZWVzxIeApXWNin/HoFrwIwnmpn2bpyjG1yWu3FR/3CLkkekUSsZ7k4P7kBlspQyUNSqFxWfDM+2qwpJ7qUNfQ05oGJkceLTsYec5kH2Xe39fs8RQife5uoord1FkW7A=,iv:M4nSUT77FUCCTm72yteJCZNJivYCs0LLr2irRGgrq3c=,tag:HaMxRKNEgNmB3sIw93yCMw==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
This diff is collapsed.
......@@ -60,7 +60,7 @@ geth:
files:
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]
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: ""
frontend:
environment:
......@@ -78,8 +78,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-11-01T14:58:11Z"
mac: ENC[AES256_GCM,data:3AK4GRnUnAcQrdJ9JrdhSFqMYmYhE2RGiP+NPvO+mqBGDH26pRjN3lkNhHi/uObEQWiQZJLzLEOhSPl0/oDVYRTSGEeEiIlViEm/S5PuD57uFx6ogS9Iz88G/3hnc3HpTAIg2+NVwE7wF1/NK75WlivB1pUGk7OrazhZ+Fhyn5k=,iv:94YFEq/dmS07CqsKFZ2NAKMj3LqqyUB4+2XtHI3UiLg=,tag:4uti1G9eRoL2AwF0n7avdQ==,type:str]
lastmodified: "2022-11-14T20:05:57Z"
mac: ENC[AES256_GCM,data:m6N+QIkvPbzWVHkBQ17Wr97BpHY2395e5F7z9lZRcWu+L+MCVrRzCo4IeSKFFty5GqGeduouuMioYuGZeczDxSsWxTPXvyx+sH5n7Q0lp4gf9TkJqVL/O1NU8JSVrBvOy8B9F6vPLi7KwOW2xAFdFdGggTxG8Lk0DNWPlM8lmTo=,iv:fa0w2pIh075WxxfXPCf+zbSz/x1g+lqiVFeTlTvrEYw=,tag:BgELhzRsL6SxW8o8HH0C9g==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -147,11 +147,21 @@ geth:
_default: ethereum/client-go:stable
replicas:
app: 1
portHttp: 8545
portWs: 8546
portAuth: 8551
command: '["sh","./root/init.sh"]'
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"]'
ports:
http:
number: 8545
protocol: TCP
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: {}
persistence:
enabled: false
......@@ -363,3 +373,5 @@ frontend:
_default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL:
_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 { pick, omit } from 'lodash';
import { useRouter } from 'next/router';
......@@ -5,32 +6,31 @@ import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll';
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 { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
const PAGINATION_FIELDS: Array<keyof PaginationParams> = [ 'block_number', 'index', 'items_count' ];
interface ResponseWithPagination {
next_page_params: PaginationParams | null;
interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string;
queryName: QueryName;
queryIds?: Array<string>;
filters?: TTxsFilters | BlockFilters;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
}
export default function useQueryWithPages<Response extends ResponseWithPagination>(
apiPath: string,
queryName: QueryKeys,
filters?: TTxsFilters | BlockFilters,
) {
export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({ queryName, filters, options, apiPath, queryIds }: Params<QueryName>) {
const paginationFields = PAGINATION_FIELDS[queryName];
const queryClient = useQueryClient();
const router = useRouter();
const [ page, setPage ] = React.useState(1);
const currPageParams = pick(router.query, PAGINATION_FIELDS);
const [ pageParams, setPageParams ] = React.useState<Array<Partial<PaginationParams>>>([ {} ]);
const currPageParams = pick(router.query, paginationFields);
const [ pageParams, setPageParams ] = React.useState<Array<PaginationParams<QueryName>>>([ ]);
const fetch = useFetch();
const queryResult = useQuery<unknown, unknown, Response>(
[ queryName, { page, filters } ],
const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>(
[ queryName, ...(queryIds || []), { page, filters } ],
async() => {
const params: Array<string> = [];
......@@ -44,7 +44,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`);
},
{ staleTime: Infinity },
{ staleTime: Infinity, ...options },
);
const { data } = queryResult;
......@@ -53,9 +53,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
// we hide next page button if no next_page_params
return;
}
// api adds filters into next-page-params now
// later filters will be removed from response
const nextPageParams = pick(data.next_page_params, PAGINATION_FIELDS);
const nextPageParams = data.next_page_params;
if (page >= pageParams.length && data?.next_page_params) {
setPageParams(prev => [ ...prev, nextPageParams ]);
}
......@@ -66,18 +64,18 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
animateScroll.scrollToTop({ duration: 0 });
setPage(prev => prev + 1);
});
}, [ data, page, pageParams, router ]);
}, [ data?.next_page_params, page, pageParams.length, router ]);
const onPrevPageClick = useCallback(() => {
// returning to the first page
// we dont have pagination params for the first page
let nextPageQuery: typeof router.query;
if (page === 2) {
nextPageQuery = omit(router.query, PAGINATION_FIELDS);
nextPageQuery = omit(router.query, paginationFields);
} else {
const nextPageParams = { ...pageParams[page - 2] };
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.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
......@@ -86,16 +84,16 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
setPage(prev => prev - 1);
page === 2 && queryClient.clear();
});
}, [ router, page, pageParams, queryClient ]);
}, [ router, page, paginationFields, pageParams, queryClient ]);
const resetPage = useCallback(() => {
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 });
setPage(1);
setPageParams([ {} ]);
setPageParams([ ]);
});
}, [ router, queryClient ]);
}, [ queryClient, router, paginationFields ]);
const hasPaginationParams = Object.keys(currPageParams).length > 0;
const nextPageParams = data?.next_page_params;
......
......@@ -3,7 +3,7 @@ import appConfig from 'configs/app/config';
import { ROUTES } from './routes';
import type { RouteName } from './routes';
const PATH_PARAM_REGEXP = /\/\[(\w+)\]/g;
const PATH_PARAM_REGEXP = /\/:(\w+)/g;
export default function link(
routeName: RouteName,
......
......@@ -7,16 +7,16 @@ const paths = {
custom_abi: `/account/custom_abi`,
profile: `/auth/profile`,
txs: `/txs`,
tx: `/tx/[id]`,
tx: `/tx/:id`,
blocks: `/blocks`,
block: `/block/[id]`,
block: `/block/:id`,
tokens: `/tokens`,
token_index: `/token/[hash]`,
token_instance_item: `/token/[hash]/instance/[id]`,
address_index: `/address/[id]`,
address_contract_verification: `/address/[id]/contract_verifications/new`,
token_index: `/token/:hash`,
token_instance_item: `/token/:hash/instance/:id`,
address_index: `/address/:id`,
address_contract_verification: `/address/:id/contract_verifications/new`,
apps: `/apps`,
app_index: `/apps/[id]`,
app_index: `/apps/:id`,
search_results: `/search-results`,
other: `/search-results`,
auth: `/auth/auth0`,
......
......@@ -11,7 +11,14 @@ import appConfig from 'configs/app/config';
// title: 'Anyblock',
// baseUrl: 'https://explorer.anyblock.tools',
// paths: {
// tx: '/ethereum/poa/core/tx',
// tx: '/ethereum/ethereum/goerli/transaction',
// },
// },
// {
// title: 'Etherscan',
// baseUrl: 'https://goerli.etherscan.io/',
// paths: {
// tx: '/tx',
// },
// },
// ]).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';
export type Props = {
cookies: 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 {
props: {
cookies: req.headers.cookie || '',
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';
export type SocketMessageParams = SocketMessage.NewBlock |
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;
event: Event;
handler: (payload: Payload) => void;
......@@ -17,4 +20,7 @@ export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', {finished: boolean; ratio: string}>;
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';
import { useSocket } from './context';
interface Params {
topic: string;
topic: string | undefined;
params?: object;
isDisabled: boolean;
onJoin?: (channel: Channel, message: unknown) => void;
......@@ -47,7 +47,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
}, [ channel, isDisabled ]);
useEffect(() => {
if (socket === null || isDisabled) {
if (socket === null || isDisabled || !topic) {
return;
}
......
......@@ -7,7 +7,7 @@ export default function useSocketMessage({ channel, event, handler }: SocketMess
handlerRef.current = handler;
useEffect(() => {
if (channel === undefined) {
if (channel === undefined || event === undefined) {
return;
}
......
import type { TransactionsResponse } from 'types/api/transaction';
import type { Transaction } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import compareBns from 'lib/bigint/compareBns';
export default function sortTxs(txs: TransactionsResponse['items'], sorting?: Sort) {
let sortedTxs;
const sortTxs = (sorting?: Sort) => (tx1: Transaction, tx2: Transaction) => {
switch (sorting) {
case 'val-desc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx1.value, tx2.value));
break;
return compareBns(tx1.value, tx2.value);
case 'val-asc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx2.value, tx1.value));
break;
return compareBns(tx2.value, tx1.value);
case 'fee-desc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx1.fee.value, tx2.fee.value));
break;
return compareBns(tx1.fee.value, tx2.fee.value);
case 'fee-asc':
sortedTxs = [ ...txs ].sort((tx1, tx2) => compareBns(tx2.fee.value, tx1.fee.value));
break;
return compareBns(tx2.fee.value, tx1.fee.value);
default:
sortedTxs = txs;
return 0;
}
};
return sortedTxs;
}
export default sortTxs;
......@@ -13,6 +13,10 @@ class MyDocument extends Document {
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
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="16x16" type="image/png"href="/static/favicon-16x16.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 Head from 'next/head';
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 = {
pageParams: PageParams;
}
const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
const BlockPage: NextPage<PageParams> = ({ id }: PageParams) => {
const { title, description } = getSeo({ id });
return (
<BlockNextPage pageParams={ pageParams }/>
<>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Block/>
</>
);
};
......
import type { NextPage } from 'next';
import Head from 'next/head';
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 { title } = getSeo();
return (
<BlocksNextPage/>
<>
<Head>
<title>{ title }</title>
</Head>
<Blocks/>
</>
);
};
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
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 = {
pageParams: PageParams;
}
const TransactionPage: NextPage<PageParams> = ({ id }: PageParams) => {
const { title, description } = getSeo({ id });
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<TransactionNextPage pageParams={ pageParams }/>
<>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Transaction/>
</>
);
};
......
......@@ -4,7 +4,8 @@ const borders = {
sm: '4px',
base: '8px',
md: '12px',
lg: '24px',
lg: '16px',
xl: '24px',
full: '9999px',
},
};
......
import type { AddressParam } from 'types/api/addressParams';
import type { PaginationParams } from 'types/api/pagination';
import type { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction';
......@@ -34,12 +33,18 @@ export interface Block {
export interface BlocksResponse {
items: Array<Block>;
next_page_params: PaginationParams | null;
next_page_params: {
block_number: number;
items_count: number;
} | null;
}
export interface BlockTransactionsResponse {
items: Array<Transaction>;
next_page_params: PaginationParams | null;
next_page_params: {
block_number: number;
items_count: number;
} | null;
}
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 { PaginationParams } from './pagination';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
......@@ -20,7 +19,10 @@ export interface InternalTransaction {
export interface InternalTransactionsResponse {
items: Array<InternalTransaction>;
next_page_params: PaginationParams & {
next_page_params: {
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
transaction_index: number;
};
......
export interface PaginationParams {
block_number: number;
index?: number;
items_count: number;
import type { BlocksResponse, BlockTransactionsResponse } from 'types/api/block';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponse } from 'types/api/log';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import type { 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 {
from: 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 { DecodedInput } from './decodedInput';
import type { Fee } from './fee';
import type { PaginationParams } from './pagination';
import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = {
......@@ -18,7 +17,7 @@ export interface Transaction {
timestamp: string | null;
confirmation_duration: Array<number>;
from: AddressParam;
to: AddressParam;
to: AddressParam | null;
created_contract: AddressParam;
value: string;
fee: Fee;
......@@ -39,14 +38,30 @@ export interface Transaction {
token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean;
exchange_rate: string;
method: string;
method: string | null;
tx_types: Array<TransactionType>;
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>;
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 enum QueryKeys {
csrf = 'csrf',
profile = 'profile',
transactions = 'transactions',
txsValidate = 'txs-validated',
txsPending = 'txs-pending',
stats='stats',
tx = 'tx',
txInternals = 'tx-internals',
txLog = 'tx-log',
txLogs = 'tx-logs',
txRawTrace = 'tx-raw-trace',
txTokenTransfers = 'tx-token-transfers',
blockTxs = 'block-transactions',
block = 'block',
blocks = 'blocks',
chartsTxs = 'charts-txs',
chartsMarket = 'charts-market',
}
export type KeysOfObjectOrNull<T> = T extends null ? never : keyof T;
......@@ -12,6 +12,7 @@ const BlockTxs = () => {
<TxsContent
queryName={ QueryKeys.blockTxs }
apiPath={ `/node-api/blocks/${ router.query.id }/transactions` }
showBlockInfo={ false }
/>
);
};
......
......@@ -25,7 +25,12 @@ const BlocksContent = ({ type }: Props) => {
const queryClient = useQueryClient();
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) => {
queryClient.setQueryData([ QueryKeys.blocks, { page: pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => {
......@@ -87,21 +92,27 @@ const BlocksContent = ({ type }: Props) => {
<>
{ 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>
<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 (
<>
{ 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 }}/>
}
<ActionBar>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
{ !isPaginatorHidden && (
<ActionBar>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ content }
</>
);
......
......@@ -12,13 +12,15 @@ import { default as Thead } from 'ui/shared/TheadSticky';
interface Props {
data: Array<Block>;
top: number;
page: number;
}
const BlocksTable = ({ data }: Props) => {
const BlocksTable = ({ data, top, page }: Props) => {
return (
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
<Thead top={ 80 }>
<Thead top={ top }>
<Tr>
<Th width="125px">Block</Th>
<Th width="120px">Size</Th>
......@@ -31,8 +33,7 @@ const BlocksTable = ({ data }: Props) => {
</Thead>
<Tbody>
<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/>) }
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement={ page === 1 }/>) }
</AnimatePresence>
</Tbody>
</Table>
......
......@@ -32,12 +32,12 @@ const EthereumChart = () => {
const data: TimeChartData = [
{
name: 'Daily Transactions',
name: 'Daily txs',
color: useToken('colors', 'blue.500'),
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'),
items: ethTokenTransferData.slice(range[0], range[1]).map((d) => ({ ...d, date: new Date(d.date) })),
},
......@@ -139,7 +139,6 @@ const EthereumChart = () => {
anchorEl={ overlayRef.current }
width={ innerWidth }
height={ innerHeight }
margin={ CHART_MARGIN }
xScale={ xScale }
yScale={ yScale }
data={ filteredData }
......
......@@ -5,7 +5,7 @@ import ethTxsData from 'data/charts_eth_txs.json';
import ChartLine from 'ui/shared/chart/ChartLine';
import useChartSize from 'ui/shared/chart/useChartSize';
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 DATA = ethTxsData.slice(-30).map((d) => ({ ...d, date: new Date(d.date) }));
......@@ -23,13 +23,13 @@ const SplineChartExample = () => {
return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref }>
<defs>
<BlueLinearGradient.defs/>
<BlueLineGradient.defs/>
</defs>
<ChartLine
data={ DATA }
xScale={ xScale }
yScale={ yScale }
stroke={ `url(#${ BlueLinearGradient.id })` }
stroke={ `url(#${ BlueLineGradient.id })` }
animation="left"
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';
import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies';
import useToast from 'lib/hooks/useToast';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -48,11 +49,11 @@ const Home = () => {
return (
<Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<VStack gap={ 4 } alignItems="flex-start" maxW="1000px">
<PageTitle text={
`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 */ }
{ isFormVisible && (
<>
......@@ -70,6 +71,7 @@ const Home = () => {
<Button onClick={ handleSetTokenClick }>Set cookie</Button>
</>
) }
<Button colorScheme="red" onClick={ checkSentry }>Check Sentry</Button>
</VStack>
</Page>
);
......
......@@ -19,10 +19,12 @@ import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace';
import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
// import TxState from 'ui/tx/TxState';
const TABS: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> },
{ id: 'internal', title: 'Internal txn', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> },
// will be implemented later, api is not ready
......
......@@ -2,18 +2,34 @@ import {
Icon,
Center,
useColorModeValue,
chakra,
} from '@chakra-ui/react';
import React from 'react';
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 infoColor = useColorModeValue('blue.600', 'blue.300');
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
as={ infoIcon }
boxSize={ 5 }
......@@ -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';
import infoIcon from 'icons/info.svg';
interface Props extends HTMLChakraProps<'div'> {
title: string;
interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
title: React.ReactNode;
hint: string;
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 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 {
hash: string;
......@@ -12,15 +27,22 @@ interface Props {
}
const TokenLogo = ({ hash, name, className }: Props) => {
const logoSrc = appConfig.network.assetsPathname ? `
https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/
${ appConfig.network.assetsPathname }
/assets/
${ hash }
/logo.png
` : undefined;
const logoSrc = appConfig.network.assetsPathname ? [
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/',
appConfig.network.assetsPathname,
'/assets/',
hash,
'/logo.png',
].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));
import { Center, Link, Text, chakra } from '@chakra-ui/react';
import { Flex, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
......@@ -12,16 +12,12 @@ interface Props {
}
const TokenSnippet = ({ symbol, hash, name, className }: Props) => {
const url = link('token_index', { hash });
return (
<Center className={ className } columnGap={ 1 }>
<Flex className={ className } alignItems="center" columnGap={ 1 } w="100%">
<TokenLogo boxSize={ 5 } hash={ hash } name={ name }/>
<Link href={ url } target="_blank">
{ name }
</Link>
<AddressLink hash={ hash } alias={ name } type="token"/>
{ 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';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
type?: 'address' | 'transaction' | 'token' | 'block';
type?: 'address' | 'transaction' | 'token' | 'block' | 'token_instance_item';
alias?: string | null;
className?: string;
hash: string;
......@@ -23,6 +23,8 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
url = link('tx', { id: id || hash });
} else if (type === 'token') {
url = link('token_index', { hash: id || hash });
} else if (type === 'token_instance_item') {
url = link('token_instance_item', { hash, id });
} else if (type === 'block') {
url = link('block', { id: id || hash });
} else {
......@@ -39,11 +41,11 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
}
switch (truncation) {
case 'constant':
return <HashStringShorten hash={ hash }/>;
return <HashStringShorten hash={ id || hash }/>;
case 'dynamic':
return <HashStringShortenDynamic hash={ hash } fontWeight={ fontWeight }/>;
return <HashStringShortenDynamic hash={ id || hash } fontWeight={ fontWeight }/>;
case 'none':
return <span>{ hash }</span>;
return <span>{ id || hash }</span>;
}
})();
......
import { useColorModeValue, useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
......@@ -13,6 +14,8 @@ interface Props extends React.SVGProps<SVGPathElement> {
const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }: Props) => {
const ref = React.useRef(null);
const gradientStopColor = useToken('colors', useColorModeValue('whiteAlpha.200', 'blackAlpha.100'));
React.useEffect(() => {
if (disableAnimation) {
d3.select(ref.current).attr('opacity', 1);
......@@ -39,8 +42,8 @@ const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }:
{ color && (
<defs>
<linearGradient id={ `gradient-${ color }` } x1="0%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stopColor={ color } stopOpacity={ 0.8 }/>
<stop offset="100%" stopColor={ color } stopOpacity={ 0.02 }/>
<stop offset="2%" stopColor={ color }/>
<stop offset="78%" stopColor={ gradientStopColor }/>
</linearGradient>
</defs>
) }
......
......@@ -69,6 +69,7 @@ const ChartLine = ({ xScale, yScale, data, animation, ...props }: Props) => {
ref={ ref }
d={ line(data) || undefined }
strokeWidth={ 1 }
strokeLinecap="round"
fill="none"
opacity={ 0 }
{ ...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);
This diff is collapsed.
......@@ -19,6 +19,7 @@ export interface TimeChartDataItem {
items: Array<TimeChartItem>;
name: string;
color?: string;
valueFormatter?: (value: number) => string;
}
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';
export const BlueLinearGradient = {
export const BlueLineGradient = {
id: 'blue-linear-gradient',
defs: () => (
<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';
import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TokenTransferList from 'ui/tx/TokenTransferList';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
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 { data, isLoading, isError, socketStatus } = useFetchTxInfo();
......@@ -65,10 +58,11 @@ const TxDetails = () => {
...data.from.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const toAddress = data.to && data.to.hash ? data.to : data.created_contract;
const addressToTags = [
...data.to.private_tags || [],
...data.to.public_tags || [],
...data.to.watchlist_names || [],
...toAddress.private_tags || [],
...toAddress.public_tags || [],
...toAddress.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
return (
......@@ -150,25 +144,34 @@ const TxDetails = () => {
) }
</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."
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
columnGap={ 3 }
>
<Address>
<AddressIcon hash={ data.to.hash }/>
<AddressLink ml={ 2 } hash={ data.to.hash }/>
<CopyToClipboard text={ data.to.hash }/>
</Address>
{ data.to.name && <Text>{ data.to.name }</Text> }
{ data.to.is_contract && data.result === 'success' && (
{ data.to && data.to.hash ? (
<Address>
<AddressIcon hash={ toAddress.hash }/>
<AddressLink ml={ 2 } hash={ toAddress.hash }/>
<CopyToClipboard text={ toAddress.hash }/>
</Address>
) : (
<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">
<chakra.span display="inline-flex">
<Icon as={ successIcon } boxSize={ 4 } color="green.500" cursor="pointer"/>
</chakra.span>
</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">
<chakra.span display="inline-flex">
<Icon as={ errorIcon } boxSize={ 4 } color="red.500" cursor="pointer"/>
......@@ -181,22 +184,7 @@ const TxDetails = () => {
</Flex>
) }
</DetailsInfoItem>
{ TOKEN_TRANSFERS.map(({ title, hint, type }) => {
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>
);
}) }
{ data.token_transfers && <TxDetailsTokenTransfers data={ data.token_transfers } txHash={ data.hash }/> }
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
<DetailsInfoItem
......
import { Box, Flex, Text, Show, Hide } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { Box, Text, Show, Hide } from '@chakra-ui/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 { SECOND } from 'lib/consts';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterInput from 'ui/shared/FilterInput';
import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
// import FilterInput from 'ui/shared/FilterInput';
// import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import Pagination from 'ui/shared/Pagination';
import TxInternalsList from 'ui/tx/internals/TxInternalsList';
import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDesktop';
import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile';
......@@ -62,34 +62,34 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT
}
};
const searchFn = (searchTerm: string) => (item: InternalTransaction): boolean => {
const formattedSearchTerm = searchTerm.toLowerCase();
return item.type.toLowerCase().includes(formattedSearchTerm) ||
item.from.hash.toLowerCase().includes(formattedSearchTerm) ||
item.to.hash.toLowerCase().includes(formattedSearchTerm);
};
// const searchFn = (searchTerm: string) => (item: InternalTransaction): boolean => {
// const formattedSearchTerm = searchTerm.toLowerCase();
// return item.type.toLowerCase().includes(formattedSearchTerm) ||
// item.from.hash.toLowerCase().includes(formattedSearchTerm) ||
// item.to.hash.toLowerCase().includes(formattedSearchTerm);
// };
const TxInternals = () => {
const router = useRouter();
const fetch = useFetch();
const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
const [ searchTerm, setSearchTerm ] = React.useState<string>('');
// filters are not implemented yet in api
// const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
// const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>(
[ QueryKeys.txInternals, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/internal-transactions`),
{
enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status),
const { data, isLoading, isError, pagination } = useQueryWithPages({
apiPath: `/node-api/transactions/${ txInfo.data?.hash }/internal-transactions`,
queryName: QueryKeys.txInternals,
queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
},
);
});
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const isMobile = useIsMobile();
const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => {
setFilters(nextValue);
}, []);
// const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => {
// setFilters(nextValue);
// }, []);
const handleSortToggle = React.useCallback((field: SortField) => {
return () => {
......@@ -120,8 +120,9 @@ const TxInternals = () => {
const content = (() => {
const filteredData = data.items
.filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
.filter(searchFn(searchTerm))
.slice()
// .filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
// .filter(searchFn(searchTerm))
.sort(sortFn(sort));
if (filteredData.length === 0) {
......@@ -130,15 +131,20 @@ const TxInternals = () => {
return isMobile ?
<TxInternalsList data={ filteredData }/> :
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle }/>;
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle } top={ isPaginatorHidden ? 0 : 80 }/>;
})();
return (
<Box>
<Flex mb={ 6 }>
{ !isPaginatorHidden && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ /* <Flex mb={ 6 }>
<TxInternalsFilter onFilterChange={ handleFilterChange } defaultFilters={ filters } appliedFiltersNum={ filters.length }/>
<FilterInput onChange={ setSearchTerm } maxW="360px" ml={ 3 } size="xs" placeholder="Search by addresses, hash, method..."/>
</Flex>
</Flex> */ }
{ content }
</Box>
);
......
import { Box, Text } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { LogsResponse } from 'types/api/log';
import { QueryKeys } from 'types/client/queries';
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 Pagination from 'ui/shared/Pagination';
import TxLogItem from 'ui/tx/logs/TxLogItem';
import TxLogSkeleton from 'ui/tx/logs/TxLogSkeleton';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
......@@ -16,17 +15,16 @@ import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxLogs = () => {
const router = useRouter();
const fetch = useFetch();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>(
[ QueryKeys.txLog, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/logs`),
{
enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status),
const { data, isLoading, isError, pagination } = useQueryWithPages({
apiPath: `/node-api/transactions/${ txInfo.data?.hash }/logs`,
queryName: QueryKeys.txLogs,
queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
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) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
......@@ -51,6 +49,11 @@ const TxLogs = () => {
return (
<Box>
{ !isPaginatorHidden && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ data.items.map((item, index) => <TxLogItem key={ index } { ...item }/>) }
</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';
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 tokenSnippet = <TokenSnippet symbol={ token.symbol } hash={ token.address } name={ token.name } ml={ 3 }/>;
const content = (() => {
switch (token.type) {
case 'ERC-20': {
const payload = total as Erc20TotalPayload;
return (
<Flex>
<Flex flexWrap="wrap" columnGap={ 3 } rowGap={ 2 }>
<Text fontWeight={ 500 } as="span">For:{ space }
<CurrencyValue value={ payload.value } exchangeRate={ token.exchange_rate } fontWeight={ 600 }/>
</Text>
{ tokenSnippet }
<TokenSnippet symbol={ token.symbol } hash={ token.address } name={ token.name } w="auto" flexGrow="1"/>
</Flex>
);
}
......@@ -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';
const TxInternalsList = ({ data }: { data: Array<InternalTransaction>}) => {
return (
<Box mt={ 6 }>
<Box>
{ data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item }/>) }
</Box>
);
......
import { Skeleton, Flex } from '@chakra-ui/react';
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxInternalsSkeletonDesktop = () => {
return (
<>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/>
</>
<SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/>
);
};
......
......@@ -5,47 +5,41 @@ const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
<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 }>
<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>
<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 }>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="90px"/>
</Flex>
)) }
</Box>
</>
<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>
)) }
</Box>
);
};
......
......@@ -13,14 +13,15 @@ interface Props {
data: Array<InternalTransaction>;
sort: Sort | undefined;
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)';
return (
<Table variant="simple" size="sm" mt={ 6 }>
<Thead top={ 0 }>
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="28%">Type</Th>
<Th width="20%">From</Th>
......
......@@ -28,16 +28,16 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit:
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex>
</Td>
<Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address>
</Td>
<Td px={ 0 }>
<Td px={ 0 } verticalAlign="middle">
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td>
<Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ to.hash }/>
<AddressLink hash={ to.hash } alias={ to.name } fontWeight="500" ml={ 2 }/>
......
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 { 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 DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsHeader from './TxsHeader';
import TxsListItem from './TxsListItem';
import TxsNewItemNotice from './TxsNewItemNotice';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
import TxsWithSort from './TxsWithSort';
import TxsTable from './TxsTable';
import useTxsSort from './useTxsSort';
type Props = {
queryName: QueryKeys;
queryName: QueryKeys.txsPending | QueryKeys.txsValidate | QueryKeys.blockTxs;
showDescription?: boolean;
stateFilter?: TTxsFilters['filter'];
apiPath: string;
showBlockInfo?: boolean;
}
const TxsContent = ({
......@@ -27,63 +28,53 @@ const TxsContent = ({
showDescription,
stateFilter,
apiPath,
showBlockInfo = true,
}: 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 {
data,
isLoading,
isError,
pagination,
} = useQueryWithPages<TransactionsResponse>(apiPath, queryName, stateFilter && { filter: stateFilter });
...queryResult
} = useQueryWithPages({
apiPath,
queryName,
filters: stateFilter ? { filter: stateFilter } : undefined,
});
// } = 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 = (() => {
if (isError) {
return <DataFetchAlert/>;
}
const txs = data?.items;
if (!isLoading && !txs?.length) {
return <Text as="span">There are no transactions.</Text>;
if (isLoading) {
return (
<>
<Show below="lg" ssr={ false }><TxsSkeletonMobile showBlockInfo={ showBlockInfo }/></Show>
<Hide below="lg" ssr={ false }><TxsSkeletonDesktop showBlockInfo={ showBlockInfo }/></Hide>
</>
);
}
if (!isLoading && txs) {
return <TxsWithSort txs={ txs } sorting={ sorting } sort={ sort }/>;
const txs = data.items;
if (!txs.length) {
return <Text as="span">There are no transactions.</Text>;
}
return (
<>
<Show below="lg" ssr={ false }><TxsSkeletonMobile/></Show>
<Hide below="lg" ssr={ false }><TxsSkeletonDesktop/></Hide>
<Show below="lg" ssr={ false }>
<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 = ({
return (
<>
{ 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 }
</>
);
......
......@@ -14,14 +14,19 @@ import TxsSorting from 'ui/txs/TxsSorting';
type Props = {
sorting: Sort;
setSorting: (val: Sort | ((val: Sort) => Sort)) => void;
setSorting: (val: Sort) => void;
paginationProps: PaginationProps;
className?: string;
showPagination?: boolean;
}
const TxsHeader = ({ sorting, setSorting, paginationProps, className }: Props) => {
const TxsHeader = ({ sorting, setSorting, paginationProps, className, showPagination = true }: Props) => {
const isMobile = useIsMobile(false);
if (!showPagination && !isMobile) {
return null;
}
return (
<ActionBar className={ className }>
<HStack>
......@@ -47,7 +52,7 @@ const TxsHeader = ({ sorting, setSorting, paginationProps, className }: Props) =
placeholder="Search by addresses, hash, method..."
/> */ }
</HStack>
<Pagination { ...paginationProps }/>
{ showPagination && <Pagination { ...paginationProps }/> }
</ActionBar>
);
};
......
......@@ -20,19 +20,20 @@ import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link';
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 TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
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 iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
return (
<>
......@@ -42,7 +43,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
{ tx.tx_types.map(item => <TxType key={ item } type={ item }/>) }
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</HStack>
<TxAdditionalInfoButton onClick={ onOpen }/>
<AdditionalInfoButton onClick={ onOpen }/>
</Flex>
<Flex justifyContent="space-between" lineHeight="24px" mt={ 3 }>
<Flex>
......@@ -76,7 +77,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
{ tx.method }
</Text>
</Flex>
{ tx.block !== null && (
{ showBlockInfo && tx.block !== null && (
<Box mt={ 2 }>
<Text as="span">Block </Text>
<Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link>
......@@ -99,10 +100,10 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
color="gray.500"
/>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.to.hash }/>
<AddressIcon hash={ dataTo.hash }/>
<AddressLink
hash={ tx.to.hash }
alias={ tx.to.name }
hash={ dataTo.hash }
alias={ dataTo.name }
fontWeight="500"
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';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxsInternalsSkeletonDesktop = () => {
interface Props {
showBlockInfo: boolean;
}
const TxsInternalsSkeletonDesktop = ({ showBlockInfo }: Props) => {
return (
<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>
);
};
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = () => {
interface Props {
showBlockInfo: boolean;
}
const TxInternalsSkeletonMobile = ({ showBlockInfo }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
......@@ -25,7 +29,7 @@ const TxInternalsSkeletonMobile = () => {
<Skeleton w="100%" h="30px" mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 3 }/>
<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 }/>
</Flex>
......
......@@ -11,13 +11,12 @@ import React from 'react';
import type { Sort } from 'types/client/txs-sort';
import * as cookies from 'lib/cookies';
import SortButton from 'ui/shared/SortButton';
interface Props {
isActive: boolean;
sorting: Sort;
setSorting: (val: Sort | ((val: Sort) => Sort)) => void;
setSorting: (val: Sort) => void;
}
const SORT_OPTIONS = [
......@@ -32,14 +31,8 @@ const TxsSorting = ({ isActive, sorting, setSorting }: Props) => {
const { isOpen, onToggle } = useDisclosure();
const setSortingFromMenu = React.useCallback((val: string | Array<string>) => {
setSorting((prevVal: Sort) => {
let newVal: Sort = '';
if (val !== prevVal) {
newVal = val as Sort;
}
cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal;
});
const value = val as Sort | Array<Sort>;
setSorting(Array.isArray(value) ? value[0] : value);
}, [ setSorting ]);
return (
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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