Commit ae65e565 authored by tom's avatar tom

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

parents 26702ee2 02bc55b3
...@@ -2,8 +2,8 @@ name: Run E2E tests k8s ...@@ -2,8 +2,8 @@ name: Run E2E tests k8s
on: on:
# push: # push:
# pull_request: pull_request:
workflow_dispatch workflow_dispatch:
env: env:
K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }} K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }}
......
...@@ -3,8 +3,6 @@ name: Deploy review environment ...@@ -3,8 +3,6 @@ name: Deploy review environment
on: on:
pull_request: pull_request:
# push: # push:
# branches-ignore:
# - 'main'
workflow_dispatch: workflow_dispatch:
env: env:
......
/// <reference types="next-react-svg" />
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -8,18 +8,6 @@ blockscout: ...@@ -8,18 +8,6 @@ blockscout:
_default: ENC[AES256_GCM,data:+q7GvdSeRQ0aY4grlQrdWUvxT9tzsRoii3W/ECwpVKs13lw84bGV+rbiEJXBYoOAw7SDz1Hr+KMy9SQzHvj12w==,iv:Xjo9+Nca6eDTZtUcDPAPfXYSAoP2gYmDX+JanCyPPkY=,tag:kQxLFsfN5YXft8ihgNvA+Q==,type:str] _default: ENC[AES256_GCM,data:+q7GvdSeRQ0aY4grlQrdWUvxT9tzsRoii3W/ECwpVKs13lw84bGV+rbiEJXBYoOAw7SDz1Hr+KMy9SQzHvj12w==,iv:Xjo9+Nca6eDTZtUcDPAPfXYSAoP2gYmDX+JanCyPPkY=,tag:kQxLFsfN5YXft8ihgNvA+Q==,type:str]
MAILSLURP_EMAIL_ID: MAILSLURP_EMAIL_ID:
_default: ENC[AES256_GCM,data:tGjukx3Q+NaWpvfXPXmY6dphdEdbO531q4NMqsOa4XCudfpt,iv:6S2jTCoJCfhkRHg4aA2UR1U4fMsDhNV44+r7ofnHLTA=,tag:nr+0zaM45+UBUwOpmVD1hg==,type:str] _default: ENC[AES256_GCM,data:tGjukx3Q+NaWpvfXPXmY6dphdEdbO531q4NMqsOa4XCudfpt,iv:6S2jTCoJCfhkRHg4aA2UR1U4fMsDhNV44+r7ofnHLTA=,tag:nr+0zaM45+UBUwOpmVD1hg==,type:str]
ACCOUNT_AUTH0_DOMAIN:
_default: ENC[AES256_GCM,data:C6TAdc/RZ+qFOan+tWnTsfIDcYgxCNjuKOU=,iv:tEcHTumHxA9LreqLSwjcTTCP3igk/VQrJn/OWd+YQ4c=,tag:XOlWi3XHBZyENtLsv3afmw==,type:str]
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:lojxfDVJBi1Sc6KCL28tln6RJ5leg0jmaTbSHbBd1H4=,iv:l1Iq/YsZKkFuorQgKd+KzAIkqbXHv6I4eid+7LDnyOA=,tag:/c7GBciDCruz+dAOfxncww==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:Nmajob/xrOfx3sZgGHWw4/VnSgoyehubNdhJSqasxLFInJeZgxCW/QXpu5KG6j1fDdALLOT89Es+m0IO9VrG8A==,iv:RxYWdOo5G6t0FFwHQfhvUTiCOsxRzJJT0Y1nc0F5sDc=,tag:6v6+yHaHgwnw0cDSD1hXwg==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:v6LJLRwibjc6QgF1t6QNzy/FfL/i2G0mD2X6oxf0o4Q6A8cwovE03Wd5nXWTSyWrN95rJm0IQY7bjU3fXpDw0p/u4vu8dMpfM2bZMMOalXP4pA==,iv:VtNoyCEsesWn0PyWRXzLgbMcUa+voKa1sjsUY26RKnw=,tag:t02c9NVe4aGGo5feOP1OWQ==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:JQylcEDhJUOqXqBCycJ8XLwz6wpa3Uz3p5MhCEVolLnKkYoDFogGuFt1YkKt7EEtbZjqvSjIa0xQhpezX5hvypU9KxRxoVosUQ4=,iv:dbXG4N4t5jQgx68l0r5iLh5FGGg2O/vqbSDpyxzEauI=,tag:upkfr/3h35rZ93oBNJ3Y/w==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:6vj0qvxktXptFDX4Tq3kGIyhaJN7Adcwfgb3qWEYH78pWtmFprwfKNdPa/4=,iv:Eb3NcZP9oSmh0ZfY8vCWwR5Ciwe7Lr/QJmcNWXiir9E=,tag:uXoFnMfwo/GD9IPuvs/9Wg==,type:str]
ACCOUNT_SENDGRID_API_KEY: ACCOUNT_SENDGRID_API_KEY:
_default: ENC[AES256_GCM,data:bNzimXY1TDUHgLijvgcJ8mw+9RXvCLchxhum77KY6ss5NUdoWuNuQjpC7M819XRPqhBmZkC2qBSX1ZRhcNQCB1OqGkRq,iv:JFmeCxsO2lkyScN8iVb8B/356SbP7KCMFffIPjGtN0o=,tag:A21hzDObJMAUhHQRWB7Z9w==,type:str] _default: ENC[AES256_GCM,data:bNzimXY1TDUHgLijvgcJ8mw+9RXvCLchxhum77KY6ss5NUdoWuNuQjpC7M819XRPqhBmZkC2qBSX1ZRhcNQCB1OqGkRq,iv:JFmeCxsO2lkyScN8iVb8B/356SbP7KCMFffIPjGtN0o=,tag:A21hzDObJMAUhHQRWB7Z9w==,type:str]
ACCOUNT_SENDGRID_SENDER: ACCOUNT_SENDGRID_SENDER:
...@@ -39,9 +27,21 @@ blockscout: ...@@ -39,9 +27,21 @@ blockscout:
DATABASE_URL: DATABASE_URL:
_default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str] _default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str]
ACCOUNT_DATABASE_URL: ACCOUNT_DATABASE_URL:
_default: ENC[AES256_GCM,data:48q2fFA3s1HGSm7YB5QV8B0eiI2PZHPgOsUS84R8x+8GecrFTenifbcHD6AivFNKovLuClUy2aFuEtEvJDOfelY=,iv:5ZLl62Yj/AXwyg+pdBLUv3EyYXSTOiSCqGYOv3rBx94=,tag:FUiy0haST4mdrqe2VlvrLg==,type:str] _default: ENC[AES256_GCM,data:HF5y8ezV5TiLqeh98WDp4rXQeUfSBETyWVHOyNZNy5pt4MdiKTdFBLauOJpD6YHWynMFsd8IJLRNLrBn4qGe3RfwprR6v3WN9Q==,iv:B9AJXO7EJexsPgDHb5s5tzpadVYoZ79fyaL8NOYXSEw=,tag:lyNKQ13Q+pn+3DrAXIzIKQ==,type:str]
ACCOUNT_REDIS_URL: ACCOUNT_REDIS_URL:
_default: ENC[AES256_GCM,data:MtAI4U/4mLdIW0d4ap5+0wTufO7hvOD/ETPnqpGJ7tGFXmirK2w/M5UdQd8M/EsH/4zk3rlM4WDGRVwMzCTiugG1FxEdIcKOA6127jXgSUrE,iv:zgrAoj7437b0TJTstU0S7UVHGt4fKnmP3wlc2qbURa0=,tag:vT3NL8PLLxB9z/dmkE/jaw==,type:str] _default: ENC[AES256_GCM,data:MtAI4U/4mLdIW0d4ap5+0wTufO7hvOD/ETPnqpGJ7tGFXmirK2w/M5UdQd8M/EsH/4zk3rlM4WDGRVwMzCTiugG1FxEdIcKOA6127jXgSUrE,iv:zgrAoj7437b0TJTstU0S7UVHGt4fKnmP3wlc2qbURa0=,tag:vT3NL8PLLxB9z/dmkE/jaw==,type:str]
ACCOUNT_AUTH0_DOMAIN:
_default: ENC[AES256_GCM,data:zRZL/kIX2R4AbanIjN7s/qFu5od1onFhYBg=,iv:4OSMUjvxUHevuXSNAnTj/DTFoZA5mC4UIerGiIgDwQc=,tag:J5I4CC9AIVXDmhkXG4X5wQ==,type:str]
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:iYnptgxESZs216/m0ArCciXfirPTxVjt5urKTATyYv3uadokC54ofzMZagG/ZVvNY48+Uxh4YGzwySyLOOM0SA==,iv:UQoZf3tesEjAJ7E5YruoZmVclsYfS5EmSo4z90wfHfE=,tag:er+hARrkCrjoQugqvRr4ow==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
_default: ENC[AES256_GCM,data:mTY6sjNKZ+VEvH47eyyoUHt//beWvuxyreu+1WrmMvkdkwR6jgXnSXh+1pTuiG8e3it96Egxraz0hjZxlkY9kSmE91/dfoqKTns=,iv:op8OuOXXeBmmUnVkCL14j4HrfjHHoN3n2XZqLdg03Mw=,tag:bbFQYi0Aa1sUdfoUjuQ/+w==,type:str]
ACCOUNT_AUTH0_LOGOUT_URL:
_default: ENC[AES256_GCM,data:IZFfi6pn+hy7g0wnEtP9TYHH1fNiC2gqgRHVdgm4C9smPerEvS0pq9dBwVY=,iv:BxbSInFQ6GE2loTv+IzdYr25PlyzdWZI1wdT6r+uvBg=,tag:psujCU372k59OGmlRdH9Fg==,type:str]
scVerifier: scVerifier:
environment: environment:
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY: SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ACCESS_KEY:
...@@ -57,6 +57,12 @@ postgres: ...@@ -57,6 +57,12 @@ postgres:
POSTGRES_DB: POSTGRES_DB:
_default: ENC[AES256_GCM,data:QRCvQVzKOfOApg==,iv:eOdxyjmv6Zj5xINGXO0dD/GcDgD1J8e4OzLMyOneoDs=,tag:EBnJ7OT3u7vcBIbviRKaHg==,type:str] _default: ENC[AES256_GCM,data:QRCvQVzKOfOApg==,iv:eOdxyjmv6Zj5xINGXO0dD/GcDgD1J8e4OzLMyOneoDs=,tag:EBnJ7OT3u7vcBIbviRKaHg==,type:str]
geth: geth:
jwt:
token: ENC[AES256_GCM,data:+E6k/3vTav9yqAbOStmC5Rwx9mV80l8iAqQYGWJNZFTUfqXQKNavaVJW7AfzixZC4zaj8c0dbdsj1bv9rlJljG0q,iv:zW9spad1AdbkAjYQYAAflzsNhaH+LAIkjHpvIPHVGbs=,tag:oqvgkqVroMxSzwmna9ra5w==,type:str]
client:
environment:
JWT_TOKEN:
_default: ENC[AES256_GCM,data:T46y6KmBcEslaodBMRTNZya51uQiK4I3olCvdxCw3qZlgoOxRUvSgKLLoRNyD/d6w6PwgJA4oU7N5hmNGQ+I26Vc,iv:F+evxDb/sn6raub1LjZPGk2VFoQBVuBvkpgJ2YCJjw8=,tag:K0QV4weIEcdy1AXcpHSxQQ==,type:str]
files: files:
list: list:
genesis.json: ENC[AES256_GCM,data:nSFncQhigaTtb7YSeXiUvua2LTOGlluVDywxQR5NnhvIBZCZniIxHsEhXKF9rBYceOMfBPKY5MTLIyxYisZeaJHL71A/uXQerDEKkECpzhwUSTdNEA9gUEqmpUjf7ChAWjybWw6NCM2jVhG3bnZwTwmuzeCSek85q4J503j9HVEkGXAU52A2+yoov41c+xoCEOj7W6Ka/f7yJYkyppoeS+4AIxBFcJ5hI0pIf1VqDcSrJf68JO1pPzcmp9eK3CK2CIpD8uxV6CseBFEqlbllmgAVwSUJ4i8XhYEO2hMyULm/LkW52U31/tq89yctSkp5TB62sZEHyDxXcq6jFSqd4840o9VedyX4YuEVtLBlXdzerc4Q2tHORQv5u2BLW7ktA/5vj8d9XnblQBDKrqxtUEgvSj9T/Xm4zyKjIhJBAAwUd5rF4j8dNbUGpsBm3G/Lx1+DqrS8cyxXgumtmfsklEVTWBra2dwt/tlTpF+utOwT7KtqXo2dVNvL6tUWrWGQqCt+fgeRKw8NuHWgU9Nm6baQ0yhWIM3XaCEuuJqHLdelYgQiKVEaTvlw7my7XwQsunJCQ98Q2eO61v7WFsg1m5s3Wyts8R2EnMWrnDlGro//eUHFlxDiIQGXmWgnz597lrLTXiuZKNI4pvPsAggBQxIeUcYzICy5uLRPWAiDpfshYt/fRQhIxmrVaJCufJrq4IcUdJYKjAcpnGHqZMB6quZiUPCPYg5Lvz7w1zoIVLn4ANWVuh7J1husNH13SwrDUXEm+k/WYCkDZm5B/K9IzkihY8nNXbvxHEXUClpb943Xw8hpvZZj0BfP/ye7ywUzHGlo3OmFAdE7Fbjc9eELxgYirONoaHy9GkjWPjinTVVlvitakqzx4dGJ+U2adSB01WAj63rn4ZidaC/eUpcPe7E2OeqmpkLc/kLM9g3gEe/MCiqsXXUYtm0Idpbrf+rRTAotoVOi3fKr1inlJY6n+9UzKAxNkTIkiGUQ1E9g0s3Kn4Z83apuy5+Us8gA9xNfWNGcOO105ZUYKyow4Xb5ggV8t+UTXPOZQp97Qz/6q0VC2kY9bx991SrJLoaK+ChLQ0/t/LcLjZxKRPDU2YGOfjrPDO3iscDkZgX/8IvH56jxUXAYmNrxOgbb9encYIrMDYrsKTaj5kXETp2M3sTdHwk6d78PAteYCvgQb44gu5JtknIq2LLXb/2Ue45O4HnrB+AsICPb+oGAxIqFkHvWuOIRZLqap8wbazszBlqsrX8ABegjvReXiG0rX1VeQIXNcG3/tKPHNKrDlFJykaiC1w==,iv:pyrxyy1bbv1UiGZ2y1jer9UqOwVkZGb4GwawJoCGJuM=,tag:aVt5dASuZglsUs9Yydzjaw==,type:str] genesis.json: ENC[AES256_GCM,data:nSFncQhigaTtb7YSeXiUvua2LTOGlluVDywxQR5NnhvIBZCZniIxHsEhXKF9rBYceOMfBPKY5MTLIyxYisZeaJHL71A/uXQerDEKkECpzhwUSTdNEA9gUEqmpUjf7ChAWjybWw6NCM2jVhG3bnZwTwmuzeCSek85q4J503j9HVEkGXAU52A2+yoov41c+xoCEOj7W6Ka/f7yJYkyppoeS+4AIxBFcJ5hI0pIf1VqDcSrJf68JO1pPzcmp9eK3CK2CIpD8uxV6CseBFEqlbllmgAVwSUJ4i8XhYEO2hMyULm/LkW52U31/tq89yctSkp5TB62sZEHyDxXcq6jFSqd4840o9VedyX4YuEVtLBlXdzerc4Q2tHORQv5u2BLW7ktA/5vj8d9XnblQBDKrqxtUEgvSj9T/Xm4zyKjIhJBAAwUd5rF4j8dNbUGpsBm3G/Lx1+DqrS8cyxXgumtmfsklEVTWBra2dwt/tlTpF+utOwT7KtqXo2dVNvL6tUWrWGQqCt+fgeRKw8NuHWgU9Nm6baQ0yhWIM3XaCEuuJqHLdelYgQiKVEaTvlw7my7XwQsunJCQ98Q2eO61v7WFsg1m5s3Wyts8R2EnMWrnDlGro//eUHFlxDiIQGXmWgnz597lrLTXiuZKNI4pvPsAggBQxIeUcYzICy5uLRPWAiDpfshYt/fRQhIxmrVaJCufJrq4IcUdJYKjAcpnGHqZMB6quZiUPCPYg5Lvz7w1zoIVLn4ANWVuh7J1husNH13SwrDUXEm+k/WYCkDZm5B/K9IzkihY8nNXbvxHEXUClpb943Xw8hpvZZj0BfP/ye7ywUzHGlo3OmFAdE7Fbjc9eELxgYirONoaHy9GkjWPjinTVVlvitakqzx4dGJ+U2adSB01WAj63rn4ZidaC/eUpcPe7E2OeqmpkLc/kLM9g3gEe/MCiqsXXUYtm0Idpbrf+rRTAotoVOi3fKr1inlJY6n+9UzKAxNkTIkiGUQ1E9g0s3Kn4Z83apuy5+Us8gA9xNfWNGcOO105ZUYKyow4Xb5ggV8t+UTXPOZQp97Qz/6q0VC2kY9bx991SrJLoaK+ChLQ0/t/LcLjZxKRPDU2YGOfjrPDO3iscDkZgX/8IvH56jxUXAYmNrxOgbb9encYIrMDYrsKTaj5kXETp2M3sTdHwk6d78PAteYCvgQb44gu5JtknIq2LLXb/2Ue45O4HnrB+AsICPb+oGAxIqFkHvWuOIRZLqap8wbazszBlqsrX8ABegjvReXiG0rX1VeQIXNcG3/tKPHNKrDlFJykaiC1w==,iv:pyrxyy1bbv1UiGZ2y1jer9UqOwVkZGb4GwawJoCGJuM=,tag:aVt5dASuZglsUs9Yydzjaw==,type:str]
...@@ -65,7 +71,7 @@ geth: ...@@ -65,7 +71,7 @@ geth:
frontend: frontend:
environment: environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS: NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,type:str] _default: ENC[AES256_GCM,data:D/pLeRn7C40/rc7nNDCuK5dTuADPrHZl+J8hGC9xhwNRKZnkHySxkSzV,iv:z98n49jakK1mvlJpJ75BXj5gGyDHpBcAoCbaM0FmEA8=,tag:pLTa5kHmSi+bCN+8jeMhGQ==,type:str]
NEXT_PUBLIC_SENTRY_DSN: NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str] _default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI: SENTRY_CSP_REPORT_URI:
...@@ -78,8 +84,8 @@ sops: ...@@ -78,8 +84,8 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2022-11-02T07:59:42Z" lastmodified: "2022-11-14T10:04:29Z"
mac: ENC[AES256_GCM,data:OrV/dUWOtL23UFQLeIsKsGluTmse42d/4sgFMDs3UXdACsZu8twMt29Y/WaPHyq8Tpn5iYzhBLU6SCUmHxEhBNVzKBd5uCUbav1faS/zW6fSd9bEP7rmbUjaJGHliBkG3T4VCSZn53jR/OMNbSynIxZ0kRpVHr+RTcalaH7dLQ8=,iv:J3gXjFgFyZoPqL+VEnjkuKzA9UIIyK3UsvPWacBZKsY=,tag:/zY1jVjIm5BsDNJlfsg5uw==,type:str] mac: ENC[AES256_GCM,data:LitRw7GOBq9mHXXiWmfmcpw0Tbn8KESGZjFjDAlYWIlYZWVzxIeApXWNin/HoFrwIwnmpn2bpyjG1yWu3FR/3CLkkekUSsZ7k4P7kBlspQyUNSqFxWfDM+2qwpJ7qUNfQ05oGJkceLTsYec5kH2Xe39fs8RQife5uoord1FkW7A=,iv:M4nSUT77FUCCTm72yteJCZNJivYCs0LLr2irRGgrq3c=,tag:HaMxRKNEgNmB3sIw93yCMw==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
...@@ -62,58 +62,84 @@ blockscout: ...@@ -62,58 +62,84 @@ blockscout:
app: blockscout app: blockscout
# Blockscout environment variables # Blockscout environment variables
environment: environment:
ENV:
_default: test
RESOURCE_MODE:
_default: account
PUBLIC:
_default: 'false'
PORT:
_default: 4000
PORT_PG:
_default: 5432
PORT_NETWORK_HTTP:
_default: 8545
PORT_NETWORK_WS:
_default: 8546
ETHEREUM_JSONRPC_VARIANT:
_default: geth
ETHEREUM_JSONRPC_TRACE_URL: ETHEREUM_JSONRPC_TRACE_URL:
_default: http://geth-svc:8545 _default: http://geth-svc.eth-goerli.svc.cluster.local:8545
ETHEREUM_JSONRPC_HTTP_URL: ETHEREUM_JSONRPC_HTTP_URL:
_default: http://geth-svc:8545 _default: http://geth-svc.eth-goerli.svc.cluster.local:8545
ETHEREUM_JSONRPC_WS_URL: ETHEREUM_JSONRPC_WS_URL:
_default: ws://geth-svc:8546 _default: ws://geth-svc.eth-goerli.svc.cluster.local:8546
COIN: BLOCKSCOUT_VERSION:
_default: DAI _default: v4.1.8-beta
MIX_ENV:
_default: prod
ECTO_USE_SSL: ECTO_USE_SSL:
_default: 'false' _default: 'false'
ETHEREUM_JSONRPC_VARIANT:
_default: geth
HEART_BEAT_TIMEOUT:
_default: 30
SHOW_PRICE_CHART:
_default: false
CACHE_BLOCK_COUNT_PERIOD:
_default: 7200
PORT:
_default: 4000
SUBNETWORK:
_default: Ethereum
HEALTHY_BLOCKS_PERIOD:
_default: 60
NETWORK:
_default: (Goerli)
NETWORK_ICON:
_default: _network_icon.html
COIN:
_default: ETH
COIN_NAME:
_default: ETH
LOGO:
_default: /images/goerli_logo.svg
HISTORY_FETCH_INTERVAL:
_default: 60
TXS_HISTORIAN_INIT_LAG:
_default: 0
TXS_STATS_DAYS_TO_COMPILE_AT_INIT:
_default: 1
COIN_BALANCE_HISTORY_DAYS:
_default: 90
GAS_PRICE_ORACLE_NUM_OF_BLOCKS:
_default: 200
GAS_PRICE_ORACLE_SAFELOW_PERCENTILE:
_default: 35
GAS_PRICE_ORACLE_AVERAGE_PERCENTILE:
_default: 60
GAS_PRICE_ORACLE_FAST_PERCENTILE:
_default: 90
GAS_PRICE_ORACLE_CACHE_PERIOD:
_default: 300
POOL_SIZE:
_default: 20
DISPLAY_TOKEN_ICONS:
_default: 'true'
FETCH_REWARDS_WAY:
_default: manual
INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER:
_default: 'true'
SHOW_TESTNET_LABEL:
_default: 'true'
CHAIN_ID:
_default: 5
ENABLE_RUST_VERIFICATION_SERVICE:
_default: 'true'
RUST_VERIFICATION_SERVICE_URL: RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043 _default: http://sc-verifier-svc:8043
INDEXER_MEMORY_LIMIT:
_default: 3
ACCOUNT_ENABLED: ACCOUNT_ENABLED:
_default: 'true' _default: 'true'
DISABLE_REALTIME_INDEXER: API_V2_ENABLED:
_default: 'false'
SOCKET_ROOT:
_default: "/"
NETWORK_PATH:
_default: "/"
API_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_default: 'true' _default: 'true'
API_BASE_PATH:
_default: "/"
APPS_MENU: APPS_MENU:
_default: 'true' _default: 'true'
EXTERNAL_APPS: APPS:
_default: '[{"title": "Marketplace", "url": "/apps"}]' _default: '[{"title": "Marketplace", "url": "/apps", "embedded?": true}]'
JSON_RPC:
_default: https://sokol.poa.network
API_V2_ENABLED:
_default: 'true'
postgres: postgres:
enabled: true enabled: true
...@@ -142,7 +168,7 @@ postgres: ...@@ -142,7 +168,7 @@ postgres:
_default: 'trust' _default: 'trust'
# enable geth deploy # enable geth deploy
geth: geth:
enabled: true enabled: false
image: image:
_default: ethereum/client-go:stable _default: ethereum/client-go:stable
replicas: replicas:
...@@ -150,22 +176,25 @@ geth: ...@@ -150,22 +176,25 @@ geth:
portHttp: 8545 portHttp: 8545
portWs: 8546 portWs: 8546
portAuth: 8551 portAuth: 8551
command: '["sh","./root/init.sh"]' command: '["geth"]'
args: '["--fakepow", "--dev", "--dev.period=1", "--datadir=/root/.ethereum/devnet", "--keystore=/root/.ethereum/devnet/keystore", "--password=/root/password.txt", "--unlock=0", "--unlock=1", "--mine", "--miner.threads=1", "--miner.etherbase=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", "--ipcpath=/root/geth.ipc", "--http", "--http.vhosts=*", "--http.addr=0.0.0.0", "--http.port=8545", "--http.api=eth,net,web3,debug,txpool", "--ws", "--ws.origins=*", "--ws.addr=0.0.0.0", "--ws.port=8546", "--ws.api=eth,net,web3,debug,txpool", "--graphql", "--graphql.corsdomain=*", "--allow-insecure-unlock", "--rpc.allow-unprotected-txs", "--http.corsdomain=*", "--vmdebug", "--networkid=1337", "--rpc.txfeecap=0"]' args: '["--goerli", "--datadir=/.ethereum", "--gcmode=archive", "--http", "--http.vhosts=*", "--http.addr=0.0.0.0", "--http.port=8545", "--http.api=eth,net,web3,debug,txpool", "--ws", "--ws.origins=*", "--ws.addr=0.0.0.0", "--ws.port=8546", "--ws.api=eth,net,web3,debug,txpool", "--allow-insecure-unlock", "--rpc.allow-unprotected-txs", "--http.corsdomain=*", "--vmdebug", "--rpc.txfeecap=0"]'
environment: {} environment: {}
persistence: persistence:
enabled: false enabled: true
mountPath: /geth
storageClass: gp3-new
storage: 1000Gi
resources: resources:
limits: limits:
memory: memory:
_default: "2Gi" _default: "8Gi"
cpu: cpu:
_default: "0.2" _default: "2"
requests: requests:
memory: memory:
_default: "2Gi" _default: "8Gi"
cpu: cpu:
_default: "0.2" _default: "2"
# node label # node label
nodeSelector: nodeSelector:
enabled: true enabled: true
...@@ -182,9 +211,63 @@ geth: ...@@ -182,9 +211,63 @@ geth:
tls: tls:
enabled: false enabled: false
jwt: jwt:
enabled: false enabled: true
mountPath: /geth/geth/jwtsecret
files: files:
enabled: false
# enable client deploy (Prysm, lighthouse, nimbus, etc.)
client:
enabled: true enabled: true
image:
_default: gcr.io/prysmaticlabs/prysm/beacon-chain:stable
# command: '["sh","./root/init.sh"]'
args: '["--goerli", "--datadir=/data", "--jwt-secret=/geth/geth/jwtsecret", "--rpc-host=0.0.0.0", "--grpc-gateway-host=0.0.0.0", "--monitoring-host=0.0.0.0", "--execution-endpoint=http://geth-svc:8545", "--checkpoint-sync-url=https://goerli.checkpoint-sync.ethdevops.io", "--genesis-beacon-api-url=https://goerli.checkpoint-sync.ethdevops.io"]'
ports:
port-tcp:
number: 13000
protocol: TCP
port-udp:
number: 12000
protocol: UDP
port-rpc:
number: 4000
protocol: TCP
port-rpc-gtw:
number: 3500
protocol: TCP
port-monitoring:
number: 8080
protocol: TCP
persistence:
enabled: false
mountPath: path
storageClass: gp3-new
storage: 100Gi
files:
enabled: false
list: {}
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: false
host:
# enable https
tls:
enabled: false
environment: {}
resources:
limits:
memory:
_default: "6Gi"
cpu:
_default: "3"
requests:
memory:
_default: "6Gi"
cpu:
_default: "3"
# enable Smart-contract-verifier deploy # enable Smart-contract-verifier deploy
scVerifier: scVerifier:
enabled: true enabled: true
...@@ -316,50 +399,55 @@ frontend: ...@@ -316,50 +399,55 @@ frontend:
enabled: true enabled: true
app: blockscout app: blockscout
environment: environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION: # ui config
_default: v4.1.8-beta NEXT_PUBLIC_FEATURED_NETWORKS:
NEXT_PUBLIC_FOOTER_GITHUB_LINK: _default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]"
_default: https://github.com/blockscout/blockscout NEXT_PUBLIC_NETWORK_EXPLORERS:
NEXT_PUBLIC_FOOTER_TWITTER_LINK: _default: "[{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io','paths':{'tx':'/tx'}}]"
_default: https://www.twitter.com/blockscoutcom # network config
NEXT_PUBLIC_APP_ENV:
_default: preview
NEXT_PUBLIC_APP_INSTANCE:
_default: unknown
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME: NEXT_PUBLIC_NETWORK_NAME:
_default: Sokol _default: Ethereum
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA _default: Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME: NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa _default: ethereum
NEXT_PUBLIC_NETWORK_TYPE: NEXT_PUBLIC_NETWORK_TYPE:
_default: poa_core _default: goerli
NEXT_PUBLIC_NETWORK_ID: NEXT_PUBLIC_NETWORK_ID:
_default: 77 _default: 5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol _default: Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA _default: ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18 _default: 18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
_default: validation _default: validation
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]"
NEXT_PUBLIC_API_HOST:
_default: blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
_default: https://airtable.com/shrqUAcjgGJ4jU88C _default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST: NEXT_PUBLIC_MARKETPLACE_APP_LIST:
_default: "[{'author': 'Blockscout', 'id': 'token-approval-tracker', 'title': 'Token Approval Tracker', 'logo': 'https://approval-tracker.vercel.app/icon-192.png', 'categories': ['security', 'tools'], 'shortDescription': 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', 'site': 'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker', 'description': 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', 'url': 'https://approval-tracker.vercel.app/'}]" _default: "[{'author': 'Blockscout', 'id': 'token-approval-tracker', 'title': 'Token Approval Tracker', 'logo': 'https://approval-tracker.vercel.app/icon-192.png', 'categories': ['security', 'tools'], 'shortDescription': 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', 'site': 'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker', 'description': 'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.', 'url': 'https://approval-tracker.vercel.app/'}]"
# api config
NEXT_PUBLIC_API_BASE_PATH:
_default: /
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV:
_default: preview
NEXT_PUBLIC_APP_INSTANCE:
_default: unknown
NEXT_PUBLIC_API_HOST:
_default: blockscout.com/eth/goerli
NEXT_PUBLIC_APP_HOST:
_default: blockscout.com/eth/goerli
NEXT_PUBLIC_LOGOUT_URL: NEXT_PUBLIC_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout _default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL: NEXT_PUBLIC_LOGOUT_RETURN_URL:
......
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { pick, omit } from 'lodash'; import { pick, omit } from 'lodash';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
...@@ -17,11 +18,15 @@ interface ResponseWithPagination { ...@@ -17,11 +18,15 @@ interface ResponseWithPagination {
next_page_params: PaginationParams | null; next_page_params: PaginationParams | null;
} }
export default function useQueryWithPages<Response extends ResponseWithPagination>( interface Params<Response> {
apiPath: string, apiPath: string;
queryName: QueryKeys, queryName: QueryKeys;
filters?: TTxsFilters | BlockFilters, queryIds?: Array<string>;
) { filters?: TTxsFilters | BlockFilters;
options?: Omit<UseQueryOptions<unknown, unknown, Response>, 'queryKey' | 'queryFn'>;
}
export default function useQueryWithPages<Response extends ResponseWithPagination>({ queryName, filters, options, apiPath, queryIds }: Params<Response>) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const [ page, setPage ] = React.useState(1); const [ page, setPage ] = React.useState(1);
...@@ -30,7 +35,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio ...@@ -30,7 +35,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
const fetch = useFetch(); const fetch = useFetch();
const queryResult = useQuery<unknown, unknown, Response>( const queryResult = useQuery<unknown, unknown, Response>(
[ queryName, { page, filters } ], [ queryName, ...(queryIds || []), { page, filters } ],
async() => { async() => {
const params: Array<string> = []; const params: Array<string> = [];
...@@ -44,7 +49,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio ...@@ -44,7 +49,7 @@ export default function useQueryWithPages<Response extends ResponseWithPaginatio
return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`); return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`);
}, },
{ staleTime: Infinity }, { staleTime: Infinity, ...options },
); );
const { data } = queryResult; const { data } = queryResult;
......
...@@ -32,5 +32,5 @@ export default function link( ...@@ -32,5 +32,5 @@ export default function link(
url.searchParams.append(key, value); url.searchParams.append(key, value);
}); });
return url.pathname; return url.toString();
} }
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;
export type PageParams = unknown
...@@ -3,13 +3,15 @@ import type { GetServerSideProps, GetServerSidePropsResult } from 'next'; ...@@ -3,13 +3,15 @@ import type { GetServerSideProps, GetServerSidePropsResult } from 'next';
export type Props = { export type Props = {
cookies: string; cookies: string;
referrer: string; referrer: string;
id?: string;
} }
export const getServerSideProps: GetServerSideProps = async({ req }): Promise<GetServerSidePropsResult<Props>> => { export const getServerSideProps: GetServerSideProps = async({ req, query }): Promise<GetServerSidePropsResult<Props>> => {
return { return {
props: { props: {
cookies: req.headers.cookie || '', cookies: req.headers.cookie || '',
referrer: req.headers.referer || '', referrer: req.headers.referer || '',
id: query.id?.toString() || '',
}, },
}; };
}; };
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from './types';
import Transaction from 'ui/pages/Transaction';
import getSeo from './getSeo';
type Props = {
pageParams: PageParams;
}
const TransactionNextPage: NextPage<Props> = ({ pageParams }: Props) => {
const { title, description } = getSeo(pageParams);
return (
<>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Transaction/>
</>
);
};
export default TransactionNextPage;
...@@ -4,9 +4,12 @@ import type { NewBlockSocketResponse } from 'types/api/block'; ...@@ -4,9 +4,12 @@ import type { NewBlockSocketResponse } from 'types/api/block';
export type SocketMessageParams = SocketMessage.NewBlock | export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus | SocketMessage.BlocksIndexStatus |
SocketMessage.TxStatusUpdate; SocketMessage.TxStatusUpdate |
SocketMessage.NewTx |
SocketMessage.NewPendingTx |
SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string, Payload extends object> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
channel: Channel | undefined; channel: Channel | undefined;
event: Event; event: Event;
handler: (payload: Payload) => void; handler: (payload: Payload) => void;
...@@ -17,4 +20,7 @@ export namespace SocketMessage { ...@@ -17,4 +20,7 @@ export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>; export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', {finished: boolean; ratio: string}>; export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', {finished: boolean; ratio: string}>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>; export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -6,7 +6,7 @@ import notEmpty from 'lib/notEmpty'; ...@@ -6,7 +6,7 @@ import notEmpty from 'lib/notEmpty';
import { useSocket } from './context'; import { useSocket } from './context';
interface Params { interface Params {
topic: string; topic: string | undefined;
params?: object; params?: object;
isDisabled: boolean; isDisabled: boolean;
onJoin?: (channel: Channel, message: unknown) => void; onJoin?: (channel: Channel, message: unknown) => void;
...@@ -47,7 +47,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on ...@@ -47,7 +47,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
}, [ channel, isDisabled ]); }, [ channel, isDisabled ]);
useEffect(() => { useEffect(() => {
if (socket === null || isDisabled) { if (socket === null || isDisabled || !topic) {
return; return;
} }
......
...@@ -7,7 +7,7 @@ export default function useSocketMessage({ channel, event, handler }: SocketMess ...@@ -7,7 +7,7 @@ export default function useSocketMessage({ channel, event, handler }: SocketMess
handlerRef.current = handler; handlerRef.current = handler;
useEffect(() => { useEffect(() => {
if (channel === undefined) { if (channel === undefined || event === undefined) {
return; return;
} }
......
const withReactSvg = require('next-react-svg');
const path = require('path'); const path = require('path');
const headers = require('./configs/nextjs/headers'); const headers = require('./configs/nextjs/headers');
...@@ -15,6 +14,12 @@ const moduleExports = { ...@@ -15,6 +14,12 @@ const moduleExports = {
__SENTRY_TRACING__: false, __SENTRY_TRACING__: false,
}), }),
); );
config.module.rules.push(
{
test: /\.svg$/,
use: [ '@svgr/webpack' ],
},
);
return config; return config;
}, },
...@@ -28,4 +33,4 @@ const moduleExports = { ...@@ -28,4 +33,4 @@ const moduleExports = {
output: 'standalone', output: 'standalone',
}; };
module.exports = withReactSvg(moduleExports); module.exports = moduleExports;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/transactions/${ req.query.id }/token-transfers${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/tx/types'; import type { PageParams } from 'lib/next/block/types';
import BlockNextPage from 'lib/next/block/BlockNextPage'; import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block';
type Props = { const BlockPage: NextPage<PageParams> = ({ id }: PageParams) => {
pageParams: PageParams; const { title, description } = getSeo({ id });
}
const BlockPage: NextPage<Props> = ({ pageParams }: Props) => {
return ( return (
<BlockNextPage pageParams={ pageParams }/> <>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Block/>
</>
); );
}; };
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react'; import React from 'react';
import BlocksNextPage from 'lib/next/blocks/BlocksNextPage'; import getSeo from 'lib/next/blocks/getSeo';
import Blocks from 'ui/pages/Blocks';
const BlockPage: NextPage = () => { const BlockPage: NextPage = () => {
const { title } = getSeo();
return ( return (
<BlocksNextPage/> <>
<Head>
<title>{ title }</title>
</Head>
<Blocks/>
</>
); );
}; };
......
...@@ -2,20 +2,17 @@ import type { NextPage } from 'next'; ...@@ -2,20 +2,17 @@ import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import Blocks from 'ui/pages/Blocks'; import Graph from 'ui/pages/Graph';
import getSeo from './getSeo'; const GraphPage: NextPage = () => {
const BlocksNextPage: NextPage = () => {
const { title } = getSeo();
return ( return (
<> <>
<Head> <Head><title>Graph Page</title></Head>
<title>{ title }</title> <Graph/>
</Head>
<Blocks/>
</> </>
); );
}; };
export default BlocksNextPage; export default GraphPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/tx/types'; import type { PageParams } from 'lib/next/tx/types';
import TransactionNextPage from 'lib/next/tx/TransactionNextPage'; import getSeo from 'lib/next/tx/getSeo';
import Transaction from 'ui/pages/Transaction';
type Props = { const TransactionPage: NextPage<PageParams> = ({ id }: PageParams) => {
pageParams: PageParams; const { title, description } = getSeo({ id });
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return ( return (
<TransactionNextPage pageParams={ pageParams }/> <>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Transaction/>
</>
); );
}; };
......
...@@ -9,6 +9,10 @@ const global = (props: StyleFunctionProps) => ({ ...@@ -9,6 +9,10 @@ const global = (props: StyleFunctionProps) => ({
...getDefaultTransitionProps(), ...getDefaultTransitionProps(),
'-webkit-tap-highlight-color': 'transparent', '-webkit-tap-highlight-color': 'transparent',
}, },
'svg *::selection': {
color: 'none',
background: 'none',
},
form: { form: {
w: '100%', w: '100%',
}, },
......
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { PaginationParams } from './pagination';
import type { TokenInfoGeneric } from './tokenInfo'; import type { TokenInfoGeneric } from './tokenInfo';
export type Erc20TotalPayload = { export type Erc20TotalPayload = {
...@@ -37,3 +38,8 @@ interface TokenTransferBase { ...@@ -37,3 +38,8 @@ interface TokenTransferBase {
from: AddressParam; from: AddressParam;
to: AddressParam; to: AddressParam;
} }
export interface TokenTransferResponse {
items: Array<TokenTransfer>;
next_page_params: PaginationParams | null;
}
...@@ -6,6 +6,7 @@ export enum QueryKeys { ...@@ -6,6 +6,7 @@ export enum QueryKeys {
txInternals = 'tx-internals', txInternals = 'tx-internals',
txLog = 'tx-log', txLog = 'tx-log',
txRawTrace = 'tx-raw-trace', txRawTrace = 'tx-raw-trace',
txTokenTransfers = 'tx-token-transfers',
blockTxs = 'block-transactions', blockTxs = 'block-transactions',
block = 'block', block = 'block',
blocks = 'blocks', blocks = 'blocks',
......
...@@ -2,6 +2,7 @@ import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, Alert } from '@chakra-u ...@@ -2,6 +2,7 @@ import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, Alert } from '@chakra-u
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import NextLink from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
...@@ -109,9 +110,11 @@ const BlockDetails = () => { ...@@ -109,9 +110,11 @@ const BlockDetails = () => {
title="Transactions" title="Transactions"
hint="The number of transactions in the block." hint="The number of transactions in the block."
> >
<Link href={ link('block', { id: router.query.id }, { tab: 'txs' }) }> <NextLink href={ link('block', { id: router.query.id }, { tab: 'txs' }) } passHref>
<Link>
{ data.tx_count } transactions { data.tx_count } transactions
</Link> </Link>
</NextLink>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title={ appConfig.network.verificationType === 'validation' ? 'Validated by' : 'Mined by' } title={ appConfig.network.verificationType === 'validation' ? 'Validated by' : 'Mined by' }
......
...@@ -25,7 +25,11 @@ const BlocksContent = ({ type }: Props) => { ...@@ -25,7 +25,11 @@ const BlocksContent = ({ type }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const { data, isLoading, isError, pagination } = useQueryWithPages<BlocksResponse>('/node-api/blocks', QueryKeys.blocks, { type }); const { data, isLoading, isError, pagination } = useQueryWithPages<BlocksResponse>({
apiPath: '/node-api/blocks',
queryName: QueryKeys.blocks,
filters: { type },
});
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.blocks, { page: pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => { queryClient.setQueryData([ QueryKeys.blocks, { page: pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => {
......
import { useToken, Button, Box } from '@chakra-ui/react';
import React from 'react';
import type { TimeChartData } from 'ui/shared/chart/types';
import ethTokenTransferData from 'data/charts_eth_token_transfer.json';
import ethTxsData from 'data/charts_eth_txs.json';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis';
import ChartGridLine from 'ui/shared/chart/ChartGridLine';
import ChartLegend from 'ui/shared/chart/ChartLegend';
import ChartLine from 'ui/shared/chart/ChartLine';
import ChartOverlay from 'ui/shared/chart/ChartOverlay';
import ChartSelectionX from 'ui/shared/chart/ChartSelectionX';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
// import useBrushX from 'ui/shared/chart/useBrushX';
import useChartLegend from 'ui/shared/chart/useChartLegend';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 };
const CHART_OFFSET = {
y: 26, // legend height
};
const EthereumChart = () => {
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN, CHART_OFFSET);
const [ range, setRange ] = React.useState<[number, number]>([ 0, Infinity ]);
const data: TimeChartData = [
{
name: 'Daily Transactions',
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',
color: useToken('colors', 'green.500'),
items: ethTokenTransferData.slice(range[0], range[1]).map((d) => ({ ...d, date: new Date(d.date) })),
},
];
const { selectedLines, handleLegendItemClick } = useChartLegend(data.length);
const filteredData = data.filter((_, index) => selectedLines.includes(index));
const { yTickFormat, xScale, yScale } = useTimeChartController({
data: filteredData.length === 0 ? data : filteredData,
width: innerWidth,
height: innerHeight,
});
const handleRangeSelect = React.useCallback((nextRange: [number, number]) => {
setRange([ range[0] + nextRange[0], range[0] + nextRange[1] ]);
}, [ range ]);
const handleZoomReset = React.useCallback(() => {
setRange([ 0, Infinity ]);
}, [ ]);
// uncomment if we need brush the chart
// const brushLimits = React.useMemo(() => (
// [ [ 0, innerHeight ], [ innerWidth, height ] ] as [[number, number], [number, number]]
// ), [ height, innerHeight, innerWidth ]);
// useBrushX({ anchor: ref.current, limits: brushLimits, setRange });
return (
<Box display="inline-block" position="relative" width="100%" height="100%">
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref }>
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ width ? 1 : 0 }>
{ /* BASE GRID LINE */ }
<ChartGridLine
type="horizontal"
scale={ yScale }
ticks={ 1 }
size={ innerWidth }
disableAnimation
/>
{ /* GIRD LINES */ }
<ChartGridLine
type="vertical"
scale={ xScale }
ticks={ 5 }
size={ innerHeight }
transform={ `translate(0, ${ innerHeight })` }
disableAnimation
/>
<ChartGridLine
type="horizontal"
scale={ yScale }
ticks={ 5 }
size={ innerWidth }
disableAnimation
/>
{ /* GRAPH */ }
{ filteredData.map((d) => (
<ChartLine
key={ d.name }
data={ d.items }
xScale={ xScale }
yScale={ yScale }
stroke={ d.color }
animation="left"
/>
)) }
{ filteredData.map((d) => (
<ChartArea
key={ d.name }
data={ d.items }
color={ d.color }
xScale={ xScale }
yScale={ yScale }
/>
)) }
{ /* AXISES */ }
<ChartAxis
type="left"
scale={ yScale }
ticks={ 5 }
tickFormat={ yTickFormat }
disableAnimation
/>
<ChartOverlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }>
<ChartAxis
type="bottom"
scale={ xScale }
transform={ `translate(0, ${ innerHeight })` }
ticks={ 5 }
anchorEl={ overlayRef.current }
disableAnimation
/>
{ filteredData.length > 0 && (
<ChartTooltip
anchorEl={ overlayRef.current }
width={ innerWidth }
height={ innerHeight }
margin={ CHART_MARGIN }
xScale={ xScale }
yScale={ yScale }
data={ filteredData }
/>
) }
{ filteredData.length > 0 && (
<ChartSelectionX
anchorEl={ overlayRef.current }
height={ innerHeight }
scale={ xScale }
data={ filteredData }
onSelect={ handleRangeSelect }
/>
) }
</ChartOverlay>
</g>
</svg>
{ (range[0] !== 0 || range[1] !== Infinity) && (
<Button
size="sm"
variant="outline"
position="absolute"
top={ `${ CHART_MARGIN?.top || 0 }px` }
right={ `${ CHART_MARGIN?.right || 0 }px` }
onClick={ handleZoomReset }
>
Reset zoom
</Button>
) }
<ChartLegend data={ data } selectedIndexes={ selectedLines } onClick={ handleLegendItemClick }/>
</Box>
);
};
export default React.memo(EthereumChart);
import { useToken } from '@chakra-ui/react';
import React from 'react';
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';
const CHART_MARGIN = { bottom: 0, left: 0, right: 0, top: 0 };
const DATA = ethTxsData.slice(-30).map((d) => ({ ...d, date: new Date(d.date) }));
const SplineChartExample = () => {
const ref = React.useRef<SVGSVGElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
const color = useToken('colors', 'blue.500');
const { xScale, yScale } = useTimeChartController({
data: [ { items: DATA, name: 'spline', color } ],
width: innerWidth,
height: innerHeight,
});
return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref }>
<defs>
<BlueLinearGradient.defs/>
</defs>
<ChartLine
data={ DATA }
xScale={ xScale }
yScale={ yScale }
stroke={ `url(#${ BlueLinearGradient.id })` }
animation="left"
strokeWidth={ 3 }
/>
</svg>
);
};
export default SplineChartExample;
import { Box, Heading } from '@chakra-ui/react';
import React from 'react';
import EthereumChart from 'ui/charts/EthereumChart';
import SplineChartExample from 'ui/charts/SplineChartExample';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const Graph = () => {
return (
<Page>
<PageTitle text="Charts"/>
<Heading as="h2" size="sm" fontWeight="500" mb={ 3 }>Ethereum Daily Transactions & ERC-20 Token Transfer Chart</Heading>
<Box w="100%" h="400px">
<EthereumChart/>
</Box>
<Heading as="h2" size="sm" fontWeight="500" mb={ 3 } mt="80px">Ethereum Daily Transactions For Last Month</Heading>
<Box w="240px" h="150px">
<SplineChartExample/>
</Box>
</Page>
);
};
export default Graph;
...@@ -19,10 +19,12 @@ import TxDetails from 'ui/tx/TxDetails'; ...@@ -19,10 +19,12 @@ import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals'; import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace'; import TxRawTrace from 'ui/tx/TxRawTrace';
import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
// import TxState from 'ui/tx/TxState'; // import TxState from 'ui/tx/TxState';
const TABS: Array<RoutedTab> = [ const TABS: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <TxDetails/> }, { id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> },
{ id: 'internal', title: 'Internal txn', component: <TxInternals/> }, { id: 'internal', title: 'Internal txn', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> }, { id: 'logs', title: 'Logs', component: <TxLogs/> },
// will be implemented later, api is not ready // will be implemented later, api is not ready
......
...@@ -2,18 +2,34 @@ import { ...@@ -2,18 +2,34 @@ import {
Icon, Icon,
Center, Center,
useColorModeValue, useColorModeValue,
chakra,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import infoIcon from 'icons/info.svg'; import infoIcon from 'icons/info.svg';
const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick?: () => void}, ref: React.ForwardedRef<HTMLDivElement>) => { interface Props {
isOpen?: boolean;
className?: string;
onClick?: () => void;
}
const AdditionalInfoButton = ({ isOpen, onClick, className }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
const infoBgColor = useColorModeValue('blue.50', 'gray.600'); const infoBgColor = useColorModeValue('blue.50', 'gray.600');
const infoColor = useColorModeValue('blue.600', 'blue.300'); const infoColor = useColorModeValue('blue.600', 'blue.300');
return ( return (
<Center ref={ ref } background={ isOpen ? infoBgColor : 'unset' } borderRadius="8px" w="24px" h="24px" onClick={ onClick } cursor="pointer"> <Center
className={ className }
ref={ ref }
background={ isOpen ? infoBgColor : 'unset' }
borderRadius="8px"
w="24px"
h="24px"
onClick={ onClick }
cursor="pointer"
>
<Icon <Icon
as={ infoIcon } as={ infoIcon }
boxSize={ 5 } boxSize={ 5 }
...@@ -24,4 +40,4 @@ const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick? ...@@ -24,4 +40,4 @@ const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick?
); );
}; };
export default React.forwardRef(TxAdditionalInfoButton); export default chakra(React.forwardRef(AdditionalInfoButton));
...@@ -4,8 +4,8 @@ import React from 'react'; ...@@ -4,8 +4,8 @@ import React from 'react';
import infoIcon from 'icons/info.svg'; import infoIcon from 'icons/info.svg';
interface Props extends HTMLChakraProps<'div'> { interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
title: string; title: React.ReactNode;
hint: string; hint: string;
children: React.ReactNode; children: React.ReactNode;
} }
......
import { Tag, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
baseAddress: string;
addressFrom: string;
className?: string;
}
const InOutTag = ({ baseAddress, addressFrom, className }: Props) => {
const isOut = addressFrom === baseAddress;
const colorScheme = isOut ? 'orange' : 'green';
return <Tag className={ className } colorScheme={ colorScheme }>{ isOut ? 'OUT' : 'IN' }</Tag>;
};
export default React.memo(chakra(InOutTag));
...@@ -38,7 +38,7 @@ const Page = ({ ...@@ -38,7 +38,7 @@ const Page = ({
) : children; ) : children;
return ( return (
<SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2/websocket?vsn=2.0.0` }> <SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2` }>
<ScrollDirectionContext.Provider value={ directionContext }> <ScrollDirectionContext.Provider value={ directionContext }>
<Flex w="100%" minH="100vh" alignItems="stretch"> <Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/> <NavigationDesktop/>
......
import { Image, chakra } from '@chakra-ui/react'; import { Image, Center, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
const EmptyElement = () => null; const EmptyElement = ({ className, letter }: { className?: string; letter: string }) => {
const bgColor = useColorModeValue('gray.200', 'gray.600');
const color = useColorModeValue('gray.400', 'gray.200');
return (
<Center
className={ className }
fontWeight={ 600 }
bgColor={ bgColor }
color={ color }
borderRadius="base"
>
{ letter.toUpperCase() }
</Center>
);
};
interface Props { interface Props {
hash: string; hash: string;
...@@ -12,15 +27,22 @@ interface Props { ...@@ -12,15 +27,22 @@ interface Props {
} }
const TokenLogo = ({ hash, name, className }: Props) => { const TokenLogo = ({ hash, name, className }: Props) => {
const logoSrc = appConfig.network.assetsPathname ? ` const logoSrc = appConfig.network.assetsPathname ? [
https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/',
${ appConfig.network.assetsPathname } appConfig.network.assetsPathname,
/assets/ '/assets/',
${ hash } hash,
/logo.png '/logo.png',
` : undefined; ].join('') : undefined;
return <Image className={ className } src={ logoSrc } alt={ `${ name || 'token' } logo` } fallback={ <EmptyElement/> }/>; return (
<Image
className={ className }
src={ logoSrc }
alt={ `${ name || 'token' } logo` }
fallback={ <EmptyElement className={ className } letter={ name?.slice(0, 1) || 'U' }/> }
/>
);
}; };
export default React.memo(chakra(TokenLogo)); export default React.memo(chakra(TokenLogo));
import { Center, Link, Text, chakra } from '@chakra-ui/react'; import { Flex, Text, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import link from 'lib/link/link'; import AddressLink from 'ui/shared/address/AddressLink';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
...@@ -12,16 +12,12 @@ interface Props { ...@@ -12,16 +12,12 @@ interface Props {
} }
const TokenSnippet = ({ symbol, hash, name, className }: Props) => { const TokenSnippet = ({ symbol, hash, name, className }: Props) => {
const url = link('token_index', { hash });
return ( return (
<Center className={ className } columnGap={ 1 }> <Flex className={ className } alignItems="center" columnGap={ 1 } w="100%">
<TokenLogo boxSize={ 5 } hash={ hash } name={ name }/> <TokenLogo boxSize={ 5 } hash={ hash } name={ name }/>
<Link href={ url } target="_blank"> <AddressLink hash={ hash } alias={ name } type="token"/>
{ name }
</Link>
{ symbol && <Text variant="secondary">({ symbol })</Text> } { symbol && <Text variant="secondary">({ symbol })</Text> }
</Center> </Flex>
); );
}; };
......
import { Hide, Show, Text, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
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;
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<TokenTransferResponse>({
apiPath: path,
queryName,
queryIds,
options: { enabled: !isDisabled },
});
const isPaginatorHidden = pagination.page === 1 && !pagination.hasNextPage;
const content = (() => {
if (isLoading || isLoadingProp) {
return (
<>
<Hide below="lg">
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="340px"/> }
<SkeletonTable columns={ showTxInfo ?
[ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] :
[ '185px', '25%', '25%', '25%', '25%' ] }
/>
</Hide>
<Show below="lg">
<TokenTransferSkeletonMobile showTxInfo={ showTxInfo } txHash={ txHash }/>
</Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (!data.items?.length) {
return <Text as="span">There are no token transfers</Text>;
}
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ isPaginatorHidden ? 0 : 80 }/>
</Hide>
<Show below="lg">
<TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>
</Show>
</>
);
})();
return (
<>
{ txHash && (data?.items.length || 0 > 0) && (
<Flex mb={ isPaginatorHidden ? 6 : 0 } w="100%">
<Text as="span" fontWeight={ 600 } whiteSpace="pre">Token transfers for by txn hash: </Text>
<HashStringShorten hash={ txHash }/>
</Flex>
) }
{ isPaginatorHidden ? null : (
<ActionBar>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ content }
</>
);
};
export default React.memo(TokenTransfer);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import TokenTransferListItem from 'ui/shared/TokenTransfer/TokenTransferListItem';
interface Props {
data: Array<TokenTransfer>;
baseAddress?: string;
showTxInfo?: boolean;
}
const TokenTransferList = ({ data, baseAddress, showTxInfo }: Props) => {
return (
<Box>
{ data.map((item, index) => <TokenTransferListItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>) }
</Box>
);
};
export default TokenTransferList;
import { Text, Flex, Tag, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import eastArrowIcon from 'icons/arrows/east.svg';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TokenSnippet from 'ui/shared/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & {
baseAddress?: string;
showTxInfo?: boolean;
}
const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => {
const value = (() => {
if (!('value' in total)) {
return null;
}
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`;
return (
<AccountListItemMobile rowGap={ 3 }>
<Flex w="100%" flexWrap="wrap" rowGap={ 1 } position="relative">
<TokenSnippet hash={ token.address } w="auto" maxW="calc(100% - 140px)" name={ token.name || 'Unnamed token' }/>
<Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</Tag>
{ showTxInfo && <AdditionalInfoButton position="absolute" top={ 0 } right={ 0 }/> }
</Flex>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
{ showTxInfo && (
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Txn hash</Text>
<Address display="inline-flex" maxW="100%">
<AddressLink type="transaction" hash={ txHash }/>
</Address>
</Flex>
) }
<Flex w="100%" columnGap={ 3 }>
<Address width={ addressWidth }>
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address>
{ baseAddress ?
<InOutTag baseAddress={ baseAddress } addressFrom={ from.hash } w="50px" textAlign="center"/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
}
<Address width={ addressWidth }>
<AddressIcon hash={ to.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash }/>
</Address>
</Flex>
{ value && (
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text>
<Text variant="secondary">{ value }</Text>
</Flex>
) }
</AccountListItemMobile>
);
};
export default React.memo(TokenTransferListItem);
import { Box, Icon, Link } from '@chakra-ui/react';
import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
hash: string;
id: string;
}
const TokenTransferNft = ({ hash, id }: Props) => {
return (
<Link href={ link('token_instance_item', { hash, id }) } overflow="hidden" whiteSpace="nowrap" display="flex" alignItems="center" w="100%">
<Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/>
<Box maxW="calc(100% - 34px)">
<HashStringShortenDynamic hash={ id } fontWeight={ 500 }/>
</Box>
</Link>
);
};
export default React.memo(TokenTransferNft);
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TokenTransferSkeletonMobile = ({ showTxInfo, txHash }: { showTxInfo?: boolean; txHash?: string }) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="100%"/> }
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="50px"/>
{ showTxInfo && <Skeleton w="24px" ml="auto"/> }
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px"/>
<Skeleton w="24px"/>
<Skeleton w="90px"/>
</Flex>
{ showTxInfo && (
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
) }
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w="50px" mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="45px"/>
<Skeleton w="90px"/>
</Flex>
</Flex>
)) }
</Box>
</>
);
};
export default TokenTransferSkeletonMobile;
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokenTransferTableItem from 'ui/shared/TokenTransfer/TokenTransferTableItem';
interface Props {
data: Array<TokenTransfer>;
baseAddress?: string;
showTxInfo?: boolean;
top: number;
}
const TokenTransferTable = ({ data, baseAddress, showTxInfo, top }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
{ showTxInfo && <Th width="44px"></Th> }
<Th width="185px">Token</Th>
<Th width="160px">Token ID</Th>
{ showTxInfo && <Th width="25%">Txn hash</Th> }
<Th width="25%">From</Th>
{ baseAddress && <Th width="50px" px={ 0 }/> }
<Th width="25%">To</Th>
<Th width="25%" isNumeric>Value</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(TokenTransferTable);
import { Tr, Td, Tag, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TokenSnippet from 'ui/shared/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & {
baseAddress?: string;
showTxInfo?: boolean;
}
const TxInternalTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => {
const value = (() => {
if (!('value' in total)) {
return '-';
}
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})();
return (
<Tr alignItems="top">
{ showTxInfo && (
<Td>
<AdditionalInfoButton/>
</Td>
) }
<Td>
<Flex flexDir="column" alignItems="flex-start">
<TokenSnippet hash={ token.address } name={ token.name || 'Unnamed token' } lineHeight="30px"/>
<Tag mt={ 1 }>{ token.type }</Tag>
<Tag colorScheme="orange" mt={ 2 }>{ getTokenTransferTypeText(type) }</Tag>
</Flex>
</Td>
<Td lineHeight="30px">
{ 'token_id' in total ? <TokenTransferNft hash={ token.address } id={ total.token_id }/> : '-' }
</Td>
{ showTxInfo && (
<Td>
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px">
<AddressLink type="transaction" hash={ txHash }/>
</Address>
</Td>
) }
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address>
</Td>
{ baseAddress && (
<Td px={ 0 }>
<InOutTag baseAddress={ baseAddress } addressFrom={ from.hash } w="50px" textAlign="center" mt="3px"/>
</Td>
) }
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon hash={ to.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 }/>
</Address>
</Td>
<Td isNumeric verticalAlign="top" lineHeight="30px">
{ value }
</Td>
</Tr>
);
};
export default React.memo(TxInternalTableItem);
import type { TokenTransfer } from 'types/api/tokenTransfer';
export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => {
if (Array.isArray(item.total)) {
item.total.forEach((total) => {
result.push({ ...item, total });
});
} else {
result.push(item);
}
return result;
};
export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
switch (type) {
case 'token_minting':
return 'Token minting';
case 'token_burning':
return 'Token burning';
case 'token_spawning':
return 'Token creating';
case 'token_transfer':
return 'Token transfer';
}
};
...@@ -7,7 +7,7 @@ import HashStringShorten from 'ui/shared/HashStringShorten'; ...@@ -7,7 +7,7 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { interface Props {
type?: 'address' | 'transaction' | 'token' | 'block'; type?: 'address' | 'transaction' | 'token' | 'block' | 'token_instance_item';
alias?: string | null; alias?: string | null;
className?: string; className?: string;
hash: string; hash: string;
...@@ -23,6 +23,8 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, ...@@ -23,6 +23,8 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
url = link('tx', { id: id || hash }); url = link('tx', { id: id || hash });
} else if (type === 'token') { } else if (type === 'token') {
url = link('token_index', { hash: id || hash }); url = link('token_index', { hash: id || hash });
} else if (type === 'token_instance_item') {
url = link('token_instance_item', { hash, id });
} else if (type === 'block') { } else if (type === 'block') {
url = link('block', { id: id || hash }); url = link('block', { id: id || hash });
} else { } else {
...@@ -39,11 +41,11 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, ...@@ -39,11 +41,11 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
} }
switch (truncation) { switch (truncation) {
case 'constant': case 'constant':
return <HashStringShorten hash={ hash }/>; return <HashStringShorten hash={ id || hash }/>;
case 'dynamic': case 'dynamic':
return <HashStringShortenDynamic hash={ hash } fontWeight={ fontWeight }/>; return <HashStringShortenDynamic hash={ id || hash } fontWeight={ fontWeight }/>;
case 'none': case 'none':
return <span>{ hash }</span>; return <span>{ id || hash }</span>;
} }
})(); })();
......
import * as d3 from 'd3';
import React from 'react';
import type { TimeChartItem } from 'ui/shared/chart/types';
interface Props extends React.SVGProps<SVGPathElement> {
xScale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
yScale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
color?: string;
data: Array<TimeChartItem>;
disableAnimation?: boolean;
}
const ChartArea = ({ xScale, yScale, color, data, disableAnimation, ...props }: Props) => {
const ref = React.useRef(null);
React.useEffect(() => {
if (disableAnimation) {
d3.select(ref.current).attr('opacity', 1);
return;
}
d3.select(ref.current).transition()
.duration(750)
.ease(d3.easeBackIn)
.attr('opacity', 1);
}, [ disableAnimation ]);
const d = React.useMemo(() => {
const area = d3.area<TimeChartItem>()
.x(({ date }) => xScale(date))
.y1(({ value }) => yScale(value))
.y0(() => yScale(yScale.domain()[0]))
.curve(d3.curveNatural);
return area(data) || undefined;
}, [ xScale, yScale, data ]);
return (
<>
<path ref={ ref } d={ d } fill={ color ? `url(#gradient-${ color })` : 'none' } opacity={ 0 } { ...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 }/>
</linearGradient>
</defs>
) }
</>
);
};
export default React.memo(ChartArea);
import { useColorModeValue, useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
interface Props extends Omit<React.SVGProps<SVGGElement>, 'scale'> {
type: 'left' | 'bottom';
scale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
disableAnimation?: boolean;
ticks: number;
tickFormat?: (domainValue: d3.AxisDomain, index: number) => string;
anchorEl?: SVGRectElement | null;
}
const ChartAxis = ({ type, scale, ticks, tickFormat, disableAnimation, anchorEl, ...props }: Props) => {
const ref = React.useRef<SVGGElement>(null);
const textColorToken = useColorModeValue('blackAlpha.500', 'whiteAlpha.500');
const textColor = useToken('colors', textColorToken);
React.useEffect(() => {
if (!ref.current) {
return;
}
const axisGenerator = type === 'left' ? d3.axisLeft : d3.axisBottom;
const axis = tickFormat ?
axisGenerator(scale).ticks(ticks).tickFormat(tickFormat) :
axisGenerator(scale).ticks(ticks);
const axisGroup = d3.select(ref.current);
if (disableAnimation) {
axisGroup.call(axis);
} else {
axisGroup.transition().duration(750).ease(d3.easeLinear).call(axis);
}
axisGroup.select('.domain').remove();
axisGroup.selectAll('line').remove();
axisGroup.selectAll('text')
.attr('opacity', 1)
.attr('color', textColor)
.attr('font-size', '0.75rem');
}, [ scale, ticks, tickFormat, disableAnimation, type, textColor ]);
React.useEffect(() => {
if (!anchorEl) {
return;
}
const anchorD3 = d3.select(anchorEl);
anchorD3
.on('mouseout.axisX', () => {
d3.select(ref.current)
.selectAll('text')
.style('font-weight', 'normal');
})
.on('mousemove.axisX', (event) => {
const [ x ] = d3.pointer(event, anchorEl);
const xDate = scale.invert(x);
const textElements = d3.select(ref.current).selectAll('text');
const data = textElements.data();
const index = d3.bisector((d) => d).left(data, xDate);
textElements
.style('font-weight', (d, i) => i === index - 1 ? 'bold' : 'normal');
});
return () => {
anchorD3.on('mouseout.axisX mousemove.axisX', null);
};
}, [ anchorEl, scale ]);
return <g ref={ ref } { ...props }/>;
};
export default React.memo(ChartAxis);
import { useColorModeValue, useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
interface Props extends Omit<React.SVGProps<SVGGElement>, 'scale'> {
type: 'vertical' | 'horizontal';
scale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
disableAnimation?: boolean;
size: number;
ticks: number;
}
const ChartGridLine = ({ type, scale, ticks, size, disableAnimation, ...props }: Props) => {
const ref = React.useRef<SVGGElement>(null);
const strokeColorToken = useColorModeValue('blackAlpha.300', 'whiteAlpha.300');
const strokeColor = useToken('colors', strokeColorToken);
React.useEffect(() => {
if (!ref.current) {
return;
}
const axisGenerator = type === 'vertical' ? d3.axisBottom : d3.axisLeft;
const axis = axisGenerator(scale).ticks(ticks).tickSize(-size);
const gridGroup = d3.select(ref.current);
if (disableAnimation) {
gridGroup.call(axis);
} else {
gridGroup.transition().duration(750).ease(d3.easeLinear).call(axis);
}
gridGroup.select('.domain').remove();
gridGroup.selectAll('text').remove();
gridGroup.selectAll('line').attr('stroke', strokeColor);
}, [ scale, ticks, size, disableAnimation, type, strokeColor ]);
return <g ref={ ref } { ...props }/>;
};
export default React.memo(ChartGridLine);
import { Box, Circle, Text } from '@chakra-ui/react';
import React from 'react';
import type { TimeChartData } from 'ui/shared/chart/types';
interface Props {
data: TimeChartData;
selectedIndexes: Array<number>;
onClick: (index: number) => void;
}
const ChartLegend = ({ data, selectedIndexes, onClick }: Props) => {
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLDivElement>) => {
const itemIndex = (event.currentTarget as HTMLDivElement).getAttribute('data-index');
onClick(Number(itemIndex));
}, [ onClick ]);
return (
<Box display="flex" columnGap={ 3 } mt={ 2 }>
{ data.map(({ name, color }, index) => {
const isSelected = selectedIndexes.includes(index);
return (
<Box
key={ name }
data-index={ index }
display="flex"
columnGap={ 1 }
alignItems="center"
onClick={ handleItemClick }
cursor="pointer"
>
<Circle
size={ 2 }
bgColor={ isSelected ? color : 'transparent' }
borderWidth={ 2 }
borderColor={ color }
/>
<Text fontSize="xs">
{ name }
</Text>
</Box>
);
}) }
</Box>
);
};
export default React.memo(ChartLegend);
import * as d3 from 'd3';
import React from 'react';
import type { TimeChartItem } from 'ui/shared/chart/types';
interface Props extends React.SVGProps<SVGPathElement> {
xScale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
yScale: d3.ScaleTime<number, number> | d3.ScaleLinear<number, number>;
data: Array<TimeChartItem>;
animation: 'left' | 'fadeIn' | 'none';
}
const ChartLine = ({ xScale, yScale, data, animation, ...props }: Props) => {
const ref = React.useRef<SVGPathElement>(null);
// Define different types of animation that we can use
const animateLeft = React.useCallback(() => {
const totalLength = ref.current?.getTotalLength() || 0;
d3.select(ref.current)
.attr('opacity', 1)
.attr('stroke-dasharray', `${ totalLength },${ totalLength }`)
.attr('stroke-dashoffset', totalLength)
.transition()
.duration(750)
.ease(d3.easeLinear)
.attr('stroke-dashoffset', 0);
}, []);
const animateFadeIn = React.useCallback(() => {
d3.select(ref.current)
.transition()
.duration(750)
.ease(d3.easeLinear)
.attr('opacity', 1);
}, []);
const noneAnimation = React.useCallback(() => {
d3.select(ref.current).attr('opacity', 1);
}, []);
React.useEffect(() => {
const ANIMATIONS = {
left: animateLeft,
fadeIn: animateFadeIn,
none: noneAnimation,
};
const animationFn = ANIMATIONS[animation];
window.setTimeout(animationFn, 100);
}, [ animateLeft, animateFadeIn, noneAnimation, animation ]);
// Recalculate line length if scale has changed
React.useEffect(() => {
if (animation === 'left') {
const totalLength = ref.current?.getTotalLength();
d3.select(ref.current).attr(
'stroke-dasharray',
`${ totalLength },${ totalLength }`,
);
}
}, [ xScale, yScale, animation ]);
const line = d3.line<TimeChartItem>()
.x((d) => xScale(d.date))
.y((d) => yScale(d.value))
.curve(d3.curveNatural);
return (
<path
ref={ ref }
d={ line(data) || undefined }
strokeWidth={ 1 }
fill="none"
opacity={ 0 }
{ ...props }
/>
);
};
export default React.memo(ChartLine);
import React from 'react';
interface Props {
width: number;
height: number;
children: React.ReactNode;
}
const ChartOverlay = ({ width, height, children }: Props, ref: React.ForwardedRef<SVGRectElement>) => {
return (
<g className="ChartOverlay">
{ children }
<rect ref={ ref } width={ width } height={ height } opacity={ 0 }/>
</g>
);
};
export default React.forwardRef(ChartOverlay);
import { useToken } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
import type { TimeChartData, TimeChartItem } from 'ui/shared/chart/types';
const SELECTION_THRESHOLD = 1;
interface Props {
height: number;
anchorEl?: SVGRectElement | null;
scale: d3.ScaleTime<number, number>;
data: TimeChartData;
onSelect: (range: [number, number]) => void;
}
const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => {
const borderColor = useToken('colors', 'blue.200');
const ref = React.useRef(null);
const isPressed = React.useRef(false);
const startX = React.useRef<number>();
const endX = React.useRef<number>();
const startIndex = React.useRef<number>(0);
const getIndexByX = React.useCallback((x: number) => {
const xDate = scale.invert(x);
const bisectDate = d3.bisector<TimeChartItem, unknown>((d) => d.date).left;
return bisectDate(data[0].items, xDate, 1);
}, [ data, scale ]);
const drawSelection = React.useCallback((x0: number, x1: number) => {
const diffX = x1 - x0;
d3.select(ref.current)
.attr('opacity', 1);
d3.select(ref.current)
.select('.ChartSelectionX__line_left')
.attr('x1', x0)
.attr('x2', x0);
d3.select(ref.current)
.select('.ChartSelectionX__line_right')
.attr('x1', x1)
.attr('x2', x1);
d3.select(ref.current)
.select('.ChartSelectionX__rect')
.attr('x', diffX > 0 ? x0 : diffX + x0)
.attr('width', Math.abs(diffX));
}, []);
const handelMouseUp = React.useCallback(() => {
isPressed.current = false;
startX.current = undefined;
d3.select(ref.current).attr('opacity', 0);
if (!endX.current) {
return;
}
const index = getIndexByX(endX.current);
if (Math.abs(index - startIndex.current) > SELECTION_THRESHOLD) {
onSelect([ Math.min(index, startIndex.current), Math.max(index, startIndex.current) ]);
}
}, [ getIndexByX, onSelect ]);
React.useEffect(() => {
if (!anchorEl) {
return;
}
const anchorD3 = d3.select(anchorEl);
anchorD3
.on('mousedown.selectionX', (event: MouseEvent) => {
const [ x ] = d3.pointer(event, anchorEl);
isPressed.current = true;
startX.current = x;
const index = getIndexByX(x);
startIndex.current = index;
})
.on('mouseup.selectionX', handelMouseUp)
.on('mousemove.selectionX', (event: MouseEvent) => {
if (isPressed.current) {
const [ x ] = d3.pointer(event, anchorEl);
startX.current && drawSelection(startX.current, x);
endX.current = x;
}
});
d3.select('body').on('mouseup.selectionX', function(event) {
const isOutside = startX.current !== undefined && event.target !== anchorD3.node();
if (isOutside) {
handelMouseUp();
}
});
return () => {
anchorD3.on('mousedown.selectionX mouseup.selectionX mousemove.selectionX', null);
d3.select('body').on('mouseup.selectionX', null);
};
}, [ anchorEl, drawSelection, getIndexByX, handelMouseUp ]);
return (
<g className="ChartSelectionX" ref={ ref } opacity={ 0 }>
<rect className="ChartSelectionX__rect" width={ 0 } height={ height } fill="rgba(66, 153, 225, 0.1)"/>
<line className="ChartSelectionX__line ChartSelectionX__line_left" x1={ 0 } x2={ 0 } y1={ 0 } y2={ height } stroke={ borderColor }/>
<line className="ChartSelectionX__line ChartSelectionX__line_right" x1={ 0 } x2={ 0 } y1={ 0 } y2={ height } stroke={ borderColor }/>
</g>
);
};
export default React.memo(ChartSelectionX);
import { useToken, useColorModeValue } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
import type { TimeChartItem, ChartMargin, TimeChartData } from 'ui/shared/chart/types';
interface Props {
width?: number;
height?: number;
margin?: ChartMargin;
data: TimeChartData;
xScale: d3.ScaleTime<number, number>;
yScale: d3.ScaleLinear<number, number>;
anchorEl: SVGRectElement | null;
}
const ChartTooltip = ({ xScale, yScale, width, height, data, margin: _margin, anchorEl, ...props }: Props) => {
const margin = React.useMemo(() => ({
top: 0, bottom: 0, left: 0, right: 0,
..._margin,
}), [ _margin ]);
const lineColor = useToken('colors', 'red.500');
const textColor = useToken('colors', useColorModeValue('white', 'black'));
const bgColor = useToken('colors', useColorModeValue('gray.900', 'gray.400'));
const ref = React.useRef(null);
const isPressed = React.useRef(false);
const drawLine = React.useCallback(
(x: number) => {
d3.select(ref.current)
.select('.ChartTooltip__line')
.attr('x1', x)
.attr('x2', x)
.attr('y1', 0)
.attr('y2', height || 0);
},
[ ref, height ],
);
const drawContent = React.useCallback(
(x: number) => {
const tooltipContent = d3.select(ref.current).select('.ChartTooltip__content');
tooltipContent.attr('transform', (cur, i, nodes) => {
const OFFSET = 8;
const node = nodes[i] as SVGGElement | null;
const nodeWidth = node?.getBoundingClientRect()?.width || 0;
const translateX = nodeWidth + x + OFFSET > (width || 0) ? x - nodeWidth - OFFSET : x + OFFSET;
return `translate(${ translateX }, ${ margin.top + 30 })`;
});
tooltipContent
.select('.ChartTooltip__contentTitle')
.text(d3.timeFormat('%b %d, %Y')(xScale.invert(x)));
},
[ xScale, margin, width ],
);
const updateDisplayedValue = React.useCallback((d: TimeChartItem, i: number) => {
d3.selectAll('.ChartTooltip__value')
.filter((td, tIndex) => tIndex === i)
.text(d.value.toLocaleString());
}, []);
const drawCircles = React.useCallback((event: MouseEvent) => {
const [ x ] = d3.pointer(event, anchorEl);
const xDate = xScale.invert(x);
const bisectDate = d3.bisector<TimeChartItem, unknown>((d) => d.date).left;
let baseXPos = 0;
d3.select(ref.current)
.selectAll('.ChartTooltip__linePoint')
.attr('transform', (cur, i) => {
const index = bisectDate(data[i].items, xDate, 1);
const d0 = data[i].items[index - 1];
const d1 = data[i].items[index];
const d = xDate.getTime() - d0?.date.getTime() > d1?.date.getTime() - xDate.getTime() ? d1 : d0;
if (d.date === undefined && d.value === undefined) {
// move point out of container
return 'translate(-100,-100)';
}
const xPos = xScale(d.date);
if (i === 0) {
baseXPos = xPos;
}
const yPos = yScale(d.value);
updateDisplayedValue(d, i);
return `translate(${ xPos }, ${ yPos })`;
});
return baseXPos;
}, [ anchorEl, data, updateDisplayedValue, xScale, yScale ]);
const followPoints = React.useCallback((event: MouseEvent) => {
const baseXPos = drawCircles(event);
drawLine(baseXPos);
drawContent(baseXPos);
}, [ drawCircles, drawLine, drawContent ]);
React.useEffect(() => {
const anchorD3 = d3.select(anchorEl);
anchorD3
.on('mousedown.tooltip', () => {
isPressed.current = true;
d3.select(ref.current).attr('opacity', 0);
})
.on('mouseup.tooltip', () => {
isPressed.current = false;
})
.on('mouseout.tooltip', () => {
d3.select(ref.current).attr('opacity', 0);
})
.on('mouseover.tooltip', () => {
d3.select(ref.current).attr('opacity', 1);
})
.on('mousemove.tooltip', (event: MouseEvent) => {
if (!isPressed.current) {
d3.select(ref.current).attr('opacity', 1);
d3.select(ref.current)
.selectAll('.ChartTooltip__linePoint')
.attr('opacity', 1);
followPoints(event);
}
});
d3.select('body').on('mouseup.tooltip', function(event) {
const isOutside = event.target !== anchorD3.node();
if (isOutside) {
isPressed.current = false;
}
});
return () => {
anchorD3.on('mousedown.tooltip mouseup.tooltip mouseout.tooltip mouseover.tooltip mousemove.tooltip', null);
d3.select('body').on('mouseup.tooltip', null);
};
}, [ anchorEl, followPoints ]);
return (
<g ref={ ref } opacity={ 0 } { ...props }>
<line className="ChartTooltip__line" stroke={ lineColor }/>
<g className="ChartTooltip__content">
<rect className="ChartTooltip__contentBg" rx={ 8 } ry={ 8 } fill={ bgColor } width={ 125 } height={ data.length * 22 + 34 }/>
<text
className="ChartTooltip__contentTitle"
transform="translate(8,20)"
fontSize="12px"
fontWeight="bold"
fill={ textColor }
pointerEvents="none"
/>
<g>
{ data.map(({ name, color }, index) => (
<g key={ name } className="ChartTooltip__contentLine" transform={ `translate(12,${ 40 + index * 20 })` }>
<circle r={ 4 } fill={ color }/>
<text
transform="translate(10,4)"
className="ChartTooltip__value"
fontSize="12px"
fill={ textColor }
pointerEvents="none"
/>
</g>
)) }
</g>
</g>
{ data.map(({ name, color }) => (
<circle key={ name } className="ChartTooltip__linePoint" r={ 4 } opacity={ 0 } fill={ color } stroke="#FFF" strokeWidth={ 1 }/>
)) }
</g>
);
};
export default React.memo(ChartTooltip);
export interface TimeChartItem {
date: Date;
value: number;
}
export interface ChartMargin {
top?: number;
right?: number;
bottom?: number;
left?: number;
}
export interface ChartOffset {
x?: number;
y?: number;
}
export interface TimeChartDataItem {
items: Array<TimeChartItem>;
name: string;
color?: string;
}
export type TimeChartData = Array<TimeChartDataItem>;
import { useToken, useColorModeValue } from '@chakra-ui/react';
import * as d3 from 'd3';
import React from 'react';
interface Props {
limits: [[number, number], [number, number]];
anchor: SVGSVGElement | null;
setRange: (range: [number, number]) => void;
}
export default function useBrushX({ limits, anchor, setRange }: Props) {
const brushRef = React.useRef<d3.BrushBehavior<unknown>>();
const brushSelectionBg = useToken('colors', useColorModeValue('blackAlpha.400', 'whiteAlpha.500'));
React.useEffect(() => {
if (!anchor || brushRef.current || limits[1][0] === 0) {
return;
}
const svgEl = d3.select(anchor).select('g');
brushRef.current = d3.brushX()
.extent(limits);
brushRef.current.on('end', (event) => {
setRange(event.selection);
});
const gBrush = svgEl?.append('g')
.attr('class', 'ChartBrush')
.call(brushRef.current);
gBrush.select('.selection')
.attr('stroke', 'none')
.attr('fill', brushSelectionBg);
}, [ anchor, brushSelectionBg, limits, setRange ]);
}
import _range from 'lodash/range';
import React from 'react';
export default function useChartLegend(dataLength: number) {
const [ selectedLines, setSelectedLines ] = React.useState<Array<number>>(_range(dataLength));
const handleLegendItemClick = React.useCallback((index: number) => {
const nextSelectedLines = selectedLines.includes(index) ? selectedLines.filter((item) => item !== index) : [ ...selectedLines, index ];
setSelectedLines(nextSelectedLines);
}, [ selectedLines ]);
return {
selectedLines,
handleLegendItemClick,
};
}
import _debounce from 'lodash/debounce';
import React from 'react';
import type { ChartMargin, ChartOffset } from 'ui/shared/chart/types';
export default function useChartSize(svgEl: SVGSVGElement | null, margin?: ChartMargin, offsets?: ChartOffset) {
const [ rect, setRect ] = React.useState<{ width: number; height: number}>({ width: 0, height: 0 });
const calculateRect = React.useCallback(() => {
const rect = svgEl?.getBoundingClientRect();
return { width: rect?.width || 0, height: rect?.height || 0 };
}, [ svgEl ]);
React.useEffect(() => {
setRect(calculateRect());
}, [ calculateRect ]);
React.useEffect(() => {
let timeoutId: number;
const resizeHandler = _debounce(() => {
setRect({ width: 0, height: 0 });
timeoutId = window.setTimeout(() => {
setRect(calculateRect());
}, 0);
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(document.body);
return function cleanup() {
resizeObserver.unobserve(document.body);
window.clearTimeout(timeoutId);
};
}, [ calculateRect ]);
return React.useMemo(() => {
return {
width: Math.max(rect.width - (offsets?.x || 0), 0),
height: Math.max(rect.height - (offsets?.y || 0), 0),
innerWidth: Math.max(rect.width - (offsets?.x || 0) - (margin?.left || 0) - (margin?.right || 0), 0),
innerHeight: Math.max(rect.height - (offsets?.y || 0) - (margin?.bottom || 0) - (margin?.top || 0), 0),
};
}, [ margin?.bottom, margin?.left, margin?.right, margin?.top, offsets?.x, offsets?.y, rect.height, rect.width ]);
}
import * as d3 from 'd3';
import { useMemo } from 'react';
import type { TimeChartData } from 'ui/shared/chart/types';
interface Props {
data: TimeChartData;
width: number;
height: number;
}
export default function useTimeChartController({ data, width, height }: Props) {
const xMin = useMemo(
() => d3.min(data, ({ items }) => d3.min(items, ({ date }) => date)) || new Date(),
[ data ],
);
const xMax = useMemo(
() => d3.max(data, ({ items }) => d3.max(items, ({ date }) => date)) || new Date(),
[ data ],
);
const xScale = useMemo(
() => d3.scaleTime().domain([ xMin, xMax ]).range([ 0, width ]),
[ xMin, xMax, width ],
);
const yMin = useMemo(
() => d3.min(data, ({ items }) => d3.min(items, ({ value }) => value)) || 0,
[ data ],
);
const yMax = useMemo(
() => d3.max(data, ({ items }) => d3.max(items, ({ value }) => value)) || 0,
[ data ],
);
const yScale = useMemo(() => {
const indention = (yMax - yMin) * 0.3;
return d3.scaleLinear()
.domain([ yMin >= 0 && yMin - indention <= 0 ? 0 : yMin - indention, yMax + indention ])
.range([ height, 0 ]);
}, [ height, yMin, yMax ]);
const yScaleForAxis = useMemo(
() => d3.scaleBand().domain([ String(yMin), String(yMax) ]).range([ height, 0 ]),
[ height, yMin, yMax ],
);
const xTickFormat = (d: d3.AxisDomain) => d.toLocaleString();
const yTickFormat = (d: d3.AxisDomain) => d.toLocaleString();
return {
xTickFormat,
yTickFormat,
xScale,
yScale,
yScaleForAxis,
};
}
import React from 'react';
export const BlueLinearGradient = {
id: 'blue-linear-gradient',
defs: () => (
<linearGradient id="blue-linear-gradient">
<stop offset="0%" stopColor="#4299E1"/>
<stop offset="100%" stopColor="#00B5D8"/>
</linearGradient>
),
};
export const RainbowGradient = {
id: 'rainbow-gradient',
defs: () => (
<linearGradient id="rainbow-gradient">
<stop offset="0%" stopColor="rgba(255, 0, 0, 1)"/>
<stop offset="10%" stopColor="rgba(255, 154, 0, 1)"/>
<stop offset="20%" stopColor="rgba(208, 222, 33, 1)"/>
<stop offset="30%" stopColor="rgba(79, 220, 74, 1)"/>
<stop offset="40%" stopColor="rgba(63, 218, 216, 1)"/>
<stop offset="50%" stopColor="rgba(47, 201, 226, 1)"/>
<stop offset="60%" stopColor="rgba(28, 127, 238, 1)"/>
<stop offset="70%" stopColor="rgba(95, 21, 242, 1)"/>
<stop offset="80%" stopColor="rgba(186, 12, 248, 1)"/>
<stop offset="90%" stopColor="rgba(251, 7, 217, 1)"/>
<stop offset="100%" stopColor="rgba(255, 0, 0, 1)"/>
</linearGradient>
),
};
...@@ -6,23 +6,34 @@ import type { PreDefinedNetwork } from 'types/networks'; ...@@ -6,23 +6,34 @@ import type { PreDefinedNetwork } from 'types/networks';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import blockscoutLogo from 'icons/logo.svg'; import blockscoutLogo from 'icons/logo.svg';
import artisLogo from 'icons/networks/logos/artis.svg';
import astarLogo from 'icons/networks/logos/astar.svg';
import etcLogo from 'icons/networks/logos/etc.svg';
import ethLogo from 'icons/networks/logos/eth.svg';
import gnosisLogo from 'icons/networks/logos/gnosis.svg';
import luksoLogo from 'icons/networks/logos/lukso.svg';
import poaLogo from 'icons/networks/logos/poa.svg';
import rskLogo from 'icons/networks/logos/rsk.svg';
import shibuyaLogo from 'icons/networks/logos/shibuya.svg';
import shidenLogo from 'icons/networks/logos/shiden.svg';
import sokolLogo from 'icons/networks/logos/sokol.svg';
import link from 'lib/link/link'; import link from 'lib/link/link';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
// predefined network logos // predefined network logos
const LOGOS: Partial<Record<PreDefinedNetwork, React.FunctionComponent<React.SVGAttributes<SVGElement>>>> = { const LOGOS: Partial<Record<PreDefinedNetwork, React.FunctionComponent<React.SVGAttributes<SVGElement>>>> = {
xdai_mainnet: require('icons/networks/logos/gnosis.svg'), xdai_mainnet: gnosisLogo,
eth_mainnet: require('icons/networks/logos/eth.svg'), eth_mainnet: ethLogo,
etc_mainnet: require('icons/networks/logos/etc.svg'), etc_mainnet: etcLogo,
poa_core: require('icons/networks/logos/poa.svg'), poa_core: poaLogo,
rsk_mainnet: require('icons/networks/logos/rsk.svg'), rsk_mainnet: rskLogo,
xdai_testnet: require('icons/networks/logos/gnosis.svg'), xdai_testnet: gnosisLogo,
poa_sokol: require('icons/networks/logos/sokol.svg'), poa_sokol: sokolLogo,
artis_sigma1: require('icons/networks/logos/artis.svg'), artis_sigma1: artisLogo,
lukso_l14: require('icons/networks/logos/lukso.svg'), lukso_l14: luksoLogo,
astar: require('icons/networks/logos/astar.svg'), astar: astarLogo,
shiden: require('icons/networks/logos/shiden.svg'), shiden: shidenLogo,
shibuya: require('icons/networks/logos/shibuya.svg'), shibuya: shibuyaLogo,
}; };
interface Props { interface Props {
......
import { Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer as TTokenTransfer } from 'types/api/tokenTransfer';
import TokenTransfer from 'ui/tx/TokenTransfer';
interface Props {
items: Array<TTokenTransfer>;
}
function getItemsNum(items: Array<TTokenTransfer>) {
const nonErc1155items = items.filter((item) => item.token.type !== 'ERC-1155').length;
const erc1155items = items
.filter((item) => item.token.type === 'ERC-1155')
.map((item) => {
if (Array.isArray(item.total)) {
return item.total.length;
}
return 1;
})
.reduce((sum, item) => sum + item, 0);
return nonErc1155items + erc1155items;
}
const TokenTransferList = ({ items }: Props) => {
const itemsNum = getItemsNum(items);
const hasScroll = itemsNum > 5;
const gradientStartColor = useColorModeValue('whiteAlpha.600', 'blackAlpha.600');
const gradientEndColor = useColorModeValue('whiteAlpha.900', 'blackAlpha.900');
return (
<Flex
flexDirection="column"
alignItems="flex-start"
rowGap={ 5 }
w="100%"
_after={ hasScroll ? {
position: 'absolute',
content: '""',
bottom: 0,
left: 0,
right: '20px',
height: '48px',
bgGradient: `linear(to-b, ${ gradientStartColor } 37.5%, ${ gradientEndColor } 77.5%)`,
} : undefined }
maxH={ hasScroll ? '200px' : 'auto' }
overflowY={ hasScroll ? 'scroll' : 'auto' }
pr={ hasScroll ? 5 : 0 }
pb={ hasScroll ? 10 : 0 }
>
{ items.map((item, index) => <TokenTransfer key={ index } { ...item }/>) }
</Flex>
);
};
export default React.memo(TokenTransferList);
...@@ -25,19 +25,12 @@ import TextSeparator from 'ui/shared/TextSeparator'; ...@@ -25,19 +25,12 @@ import TextSeparator from 'ui/shared/TextSeparator';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization'; import Utilization from 'ui/shared/Utilization';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton'; import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxRevertReason from 'ui/tx/details/TxRevertReason'; import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TokenTransferList from 'ui/tx/TokenTransferList';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData'; import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TOKEN_TRANSFERS = [
{ title: 'Tokens Transferred', hint: 'List of tokens transferred in the transaction.', type: 'token_transfer' },
{ title: 'Tokens Minted', hint: 'List of tokens minted in the transaction.', type: 'token_minting' },
{ title: 'Tokens Burnt', hint: 'List of tokens burnt in the transaction.', type: 'token_burning' },
{ title: 'Tokens Created', hint: 'List of tokens created in the transaction.', type: 'token_spawning' },
];
const TxDetails = () => { const TxDetails = () => {
const { data, isLoading, isError, socketStatus } = useFetchTxInfo(); const { data, isLoading, isError, socketStatus } = useFetchTxInfo();
...@@ -191,22 +184,7 @@ const TxDetails = () => { ...@@ -191,22 +184,7 @@ const TxDetails = () => {
</Flex> </Flex>
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
{ TOKEN_TRANSFERS.map(({ title, hint, type }) => { { data.token_transfers && <TxDetailsTokenTransfers data={ data.token_transfers } txHash={ data.hash }/> }
const items = data.token_transfers?.filter((token) => token.type === type) || [];
if (items.length === 0) {
return null;
}
return (
<DetailsInfoItem
key={ type }
title={ title }
hint={ hint }
position="relative"
>
<TokenTransferList items={ items }/>
</DetailsInfoItem>
);
}) }
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/> <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
<DetailsInfoItem <DetailsInfoItem
......
import { Box, Flex, Text, Show, Hide } from '@chakra-ui/react'; import { Box, Text, Show, Hide } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { InternalTransactionsResponse, TxInternalsType, InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse, InternalTransaction } from 'types/api/internalTransaction';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
...@@ -12,8 +12,8 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -12,8 +12,8 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterInput from 'ui/shared/FilterInput'; // import FilterInput from 'ui/shared/FilterInput';
import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter'; // import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import TxInternalsList from 'ui/tx/internals/TxInternalsList'; import TxInternalsList from 'ui/tx/internals/TxInternalsList';
import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDesktop'; import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDesktop';
import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile'; import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile';
...@@ -62,19 +62,20 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT ...@@ -62,19 +62,20 @@ const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalT
} }
}; };
const searchFn = (searchTerm: string) => (item: InternalTransaction): boolean => { // const searchFn = (searchTerm: string) => (item: InternalTransaction): boolean => {
const formattedSearchTerm = searchTerm.toLowerCase(); // const formattedSearchTerm = searchTerm.toLowerCase();
return item.type.toLowerCase().includes(formattedSearchTerm) || // return item.type.toLowerCase().includes(formattedSearchTerm) ||
item.from.hash.toLowerCase().includes(formattedSearchTerm) || // item.from.hash.toLowerCase().includes(formattedSearchTerm) ||
item.to.hash.toLowerCase().includes(formattedSearchTerm); // item.to.hash.toLowerCase().includes(formattedSearchTerm);
}; // };
const TxInternals = () => { const TxInternals = () => {
const router = useRouter(); const router = useRouter();
const fetch = useFetch(); const fetch = useFetch();
const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]); // filters are not implemented yet in api
const [ searchTerm, setSearchTerm ] = React.useState<string>(''); // const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
// const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>(); const [ sort, setSort ] = React.useState<Sort>();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>(
...@@ -87,9 +88,9 @@ const TxInternals = () => { ...@@ -87,9 +88,9 @@ const TxInternals = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => { // const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => {
setFilters(nextValue); // setFilters(nextValue);
}, []); // }, []);
const handleSortToggle = React.useCallback((field: SortField) => { const handleSortToggle = React.useCallback((field: SortField) => {
return () => { return () => {
...@@ -120,8 +121,9 @@ const TxInternals = () => { ...@@ -120,8 +121,9 @@ const TxInternals = () => {
const content = (() => { const content = (() => {
const filteredData = data.items const filteredData = data.items
.filter(({ type }) => filters.length > 0 ? filters.includes(type) : true) .slice()
.filter(searchFn(searchTerm)) // .filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
// .filter(searchFn(searchTerm))
.sort(sortFn(sort)); .sort(sortFn(sort));
if (filteredData.length === 0) { if (filteredData.length === 0) {
...@@ -135,10 +137,10 @@ const TxInternals = () => { ...@@ -135,10 +137,10 @@ const TxInternals = () => {
return ( return (
<Box> <Box>
<Flex mb={ 6 }> { /* <Flex mb={ 6 }>
<TxInternalsFilter onFilterChange={ handleFilterChange } defaultFilters={ filters } appliedFiltersNum={ filters.length }/> <TxInternalsFilter onFilterChange={ handleFilterChange } defaultFilters={ filters } appliedFiltersNum={ filters.length }/>
<FilterInput onChange={ setSearchTerm } maxW="360px" ml={ 3 } size="xs" placeholder="Search by addresses, hash, method..."/> <FilterInput onChange={ setSearchTerm } maxW="360px" ml={ 3 } size="xs" placeholder="Search by addresses, hash, method..."/>
</Flex> </Flex> */ }
{ content } { content }
</Box> </Box>
); );
......
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxTokenTransfer = () => {
const { isError, isLoading, data, socketStatus } = useFetchTxInfo({ updateDelay: 5 * SECOND });
if (!isLoading && !isError && !data.status) {
return socketStatus ? <TxSocketAlert status={ socketStatus }/> : <TxPendingAlert/>;
}
if (isError) {
return <DataFetchAlert/>;
}
const path = `/node-api/transactions/${ data?.hash }/token-transfers`;
return (
<TokenTransfer
isLoading={ isLoading }
isDisabled={ !data?.status || !data?.hash }
path={ path }
queryName={ QueryKeys.txTokenTransfers }
queryIds={ data?.hash ? [ data.hash ] : undefined }
showTxInfo={ false }
txHash={ data?.hash || '' }
/>
);
};
export default TxTokenTransfer;
...@@ -12,21 +12,20 @@ import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet'; ...@@ -12,21 +12,20 @@ import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
type Props = TTokenTransfer; type Props = TTokenTransfer;
const TokenTransfer = ({ token, total, to, from }: Props) => { const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => {
const isColumnLayout = token.type === 'ERC-1155' && Array.isArray(total); const isColumnLayout = token.type === 'ERC-1155' && Array.isArray(total);
const tokenSnippet = <TokenSnippet symbol={ token.symbol } hash={ token.address } name={ token.name } ml={ 3 }/>;
const content = (() => { const content = (() => {
switch (token.type) { switch (token.type) {
case 'ERC-20': { case 'ERC-20': {
const payload = total as Erc20TotalPayload; const payload = total as Erc20TotalPayload;
return ( return (
<Flex> <Flex flexWrap="wrap" columnGap={ 3 } rowGap={ 2 }>
<Text fontWeight={ 500 } as="span">For:{ space } <Text fontWeight={ 500 } as="span">For:{ space }
<CurrencyValue value={ payload.value } exchangeRate={ token.exchange_rate } fontWeight={ 600 }/> <CurrencyValue value={ payload.value } exchangeRate={ token.exchange_rate } fontWeight={ 600 }/>
</Text> </Text>
{ tokenSnippet } <TokenSnippet symbol={ token.symbol } hash={ token.address } name={ token.name } w="auto" flexGrow="1"/>
</Flex> </Flex>
); );
} }
...@@ -79,4 +78,4 @@ const TokenTransfer = ({ token, total, to, from }: Props) => { ...@@ -79,4 +78,4 @@ const TokenTransfer = ({ token, total, to, from }: Props) => {
); );
}; };
export default React.memo(TokenTransfer); export default React.memo(TxDetailsTokenTransfer);
import { Icon, Link, GridItem, Show, Flex } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import tokenIcon from 'icons/token.svg';
import link from 'lib/link/link';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TxDetailsTokenTransfer from './TxDetailsTokenTransfer';
interface Props {
data: Array<TokenTransfer>;
txHash: string;
}
const TOKEN_TRANSFERS_TYPES = [
{ title: 'Tokens transferred', hint: 'List of tokens transferred in the transaction.', type: 'token_transfer' },
{ title: 'Tokens minted', hint: 'List of tokens minted in the transaction.', type: 'token_minting' },
{ title: 'Tokens burnt', hint: 'List of tokens burnt in the transaction.', type: 'token_burning' },
{ title: 'Tokens created', hint: 'List of tokens created in the transaction.', type: 'token_spawning' },
];
const VISIBLE_ITEMS_NUM = 3;
const TxDetailsTokenTransfers = ({ data, txHash }: Props) => {
const viewAllUrl = link('tx', { id: txHash }, { tab: 'token_transfers' });
const formattedData = data.reduce(flattenTotal, []);
const transferGroups = TOKEN_TRANSFERS_TYPES.map((group) => ({
...group,
items: formattedData?.filter((token) => token.type === group.type) || [],
}));
const showViewAllLink = transferGroups.some(({ items }) => items.length > VISIBLE_ITEMS_NUM);
return (
<>
{ transferGroups.map(({ title, hint, type, items }) => {
if (items.length === 0) {
return null;
}
return (
<DetailsInfoItem
key={ type }
title={ title }
hint={ hint }
position="relative"
>
<Flex
flexDirection="column"
alignItems="flex-start"
rowGap={ 5 }
w="100%"
>
{ items.slice(0, VISIBLE_ITEMS_NUM).map((item, index) => <TxDetailsTokenTransfer key={ index } { ...item }/>) }
</Flex>
</DetailsInfoItem>
);
}) }
{ showViewAllLink && (
<>
<Show above="lg"><GridItem></GridItem></Show>
<GridItem fontSize="sm" alignItems="center" display="inline-flex" pl={{ base: '28px', lg: 0 }}>
<Icon as={ tokenIcon } boxSize={ 6 }/>
<NextLink href={ viewAllUrl } passHref>
<Link>View all</Link>
</NextLink>
</GridItem>
</>
) }
</>
);
};
export default React.memo(TxDetailsTokenTransfers);
...@@ -28,16 +28,16 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -28,16 +28,16 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit:
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex> </Flex>
</Td> </Td>
<Td> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon hash={ from.hash }/> <AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address> </Address>
</Td> </Td>
<Td px={ 0 }> <Td px={ 0 } verticalAlign="middle">
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td> </Td>
<Td> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon hash={ to.hash }/> <AddressIcon hash={ to.hash }/>
<AddressLink hash={ to.hash } alias={ to.name } fontWeight="500" ml={ 2 }/> <AddressLink hash={ to.hash } alias={ to.name } fontWeight="500" ml={ 2 }/>
......
...@@ -62,7 +62,11 @@ const TxsContent = ({ ...@@ -62,7 +62,11 @@ const TxsContent = ({
isLoading, isLoading,
isError, isError,
pagination, pagination,
} = useQueryWithPages<TransactionsResponse>(apiPath, queryName, stateFilter && { filter: stateFilter }); } = useQueryWithPages<TransactionsResponse>({
apiPath,
queryName,
filters: stateFilter ? { filter: stateFilter } : undefined,
});
// } = useQueryWithPages({ ...filters, filter: stateFilter, apiPath }); // } = useQueryWithPages({ ...filters, filter: stateFilter, apiPath });
const content = (() => { const content = (() => {
......
...@@ -20,12 +20,12 @@ import transactionIcon from 'icons/transactions.svg'; ...@@ -20,12 +20,12 @@ import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import getValueWithUnit from 'lib/getValueWithUnit'; import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from 'ui/txs/TxType'; import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx }: {tx: Transaction}) => { const TxsListItem = ({ tx }: {tx: Transaction}) => {
...@@ -43,7 +43,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => { ...@@ -43,7 +43,7 @@ const TxsListItem = ({ tx }: {tx: Transaction}) => {
{ tx.tx_types.map(item => <TxType key={ item } type={ item }/>) } { tx.tx_types.map(item => <TxType key={ item } type={ item }/>) }
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/> <TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</HStack> </HStack>
<TxAdditionalInfoButton onClick={ onOpen }/> <AdditionalInfoButton onClick={ onOpen }/>
</Flex> </Flex>
<Flex justifyContent="space-between" lineHeight="24px" mt={ 3 }> <Flex justifyContent="space-between" lineHeight="24px" mt={ 3 }>
<Flex> <Flex>
......
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);
...@@ -14,7 +14,7 @@ const TxsTab = ({ tab }: Props) => { ...@@ -14,7 +14,7 @@ const TxsTab = ({ tab }: Props) => {
queryName={ QueryKeys.transactions } queryName={ QueryKeys.transactions }
showDescription={ tab === 'validated' } showDescription={ tab === 'validated' }
stateFilter={ tab } stateFilter={ tab }
apiPath="/api/transactions" apiPath="/node-api/transactions"
/> />
); );
}; };
......
import { Link, Table, Tbody, Tr, Th, Icon } from '@chakra-ui/react'; import { Link, Table, Tbody, Tr, Th, Td, Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -8,6 +8,7 @@ import appConfig from 'configs/app/config'; ...@@ -8,6 +8,7 @@ import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import TheadSticky from 'ui/shared/TheadSticky'; import TheadSticky from 'ui/shared/TheadSticky';
import TxsNewItemNotice from './TxsNewItemNotice';
import TxsTableItem from './TxsTableItem'; import TxsTableItem from './TxsTableItem';
type Props = { type Props = {
...@@ -46,6 +47,9 @@ const TxsTable = ({ txs, sort, sorting }: Props) => { ...@@ -46,6 +47,9 @@ const TxsTable = ({ txs, sort, sorting }: Props) => {
</Tr> </Tr>
</TheadSticky> </TheadSticky>
<Tbody> <Tbody>
<TxsNewItemNotice borderRadius={ 0 }>
{ ({ content }) => <Tr><Td colSpan={ 10 } p={ 0 }>{ content }</Td></Tr> }
</TxsNewItemNotice>
{ txs.map((item) => ( { txs.map((item) => (
<TxsTableItem <TxsTableItem
key={ item.hash } key={ item.hash }
......
...@@ -23,6 +23,7 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -23,6 +23,7 @@ import type { Transaction } from 'types/api/transaction';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -30,7 +31,6 @@ import CurrencyValue from 'ui/shared/CurrencyValue'; ...@@ -30,7 +31,6 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from './TxType'; import TxType from './TxType';
...@@ -63,7 +63,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => { ...@@ -63,7 +63,7 @@ const TxsTableItem = ({ tx }: {tx: Transaction}) => {
{ ({ isOpen }) => ( { ({ isOpen }) => (
<> <>
<PopoverTrigger> <PopoverTrigger>
<TxAdditionalInfoButton isOpen={ isOpen }/> <AdditionalInfoButton isOpen={ isOpen }/>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent border="1px solid" borderColor={ infoBorderColor }> <PopoverContent border="1px solid" borderColor={ infoBorderColor }>
<PopoverBody> <PopoverBody>
......
...@@ -7,6 +7,7 @@ import type { Sort } from 'types/client/txs-sort'; ...@@ -7,6 +7,7 @@ import type { Sort } from 'types/client/txs-sort';
import sortTxs from 'lib/tx/sortTxs'; import sortTxs from 'lib/tx/sortTxs';
import TxsListItem from './TxsListItem'; import TxsListItem from './TxsListItem';
import TxsNewItemNotice from './TxsNewItemNotice';
import TxsTable from './TxsTable'; import TxsTable from './TxsTable';
type Props = { type Props = {
...@@ -28,7 +29,14 @@ const TxsWithSort = ({ ...@@ -28,7 +29,14 @@ const TxsWithSort = ({
return ( return (
<> <>
<Show below="lg" ssr={ false }><Box>{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Box></Show> <Show below="lg" ssr={ false }>
<Box>
<TxsNewItemNotice>
{ ({ content }) => <Box>{ content }</Box> }
</TxsNewItemNotice>
{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }
</Box>
</Show>
<Hide below="lg" ssr={ false }><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Hide> <Hide below="lg" ssr={ false }><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Hide>
</> </>
); );
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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