Commit 743d82d0 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into token-instance-transfers

parents 1ccbf441 428ec75e
...@@ -9,6 +9,7 @@ jobs: ...@@ -9,6 +9,7 @@ jobs:
jest_tests: jest_tests:
name: Run tests with Jest name: Run tests with Jest
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ false }} # disable since there are no jest test yet
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
......
...@@ -161,6 +161,26 @@ ...@@ -161,6 +161,26 @@
"instanceLimit": 1 "instanceLimit": 1
} }
}, },
{
"type": "shell",
"command": "npx playwright show-report",
"problemMatcher": [],
"label": "pw: report",
"detail": "serve test report",
"presentation": {
"reveal": "always",
"panel": "shared",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiBlue",
"id": "output"
},
"runOptions": {
"instanceLimit": 1
}
},
// JEST TESTS // JEST TESTS
{ {
......
const PATHS = require('../../lib/link/paths.json');
const oldUrls = [ const oldUrls = [
{ {
oldPath: '/account/tag_transaction', oldPath: '/account/tag_transaction',
newPath: `${ PATHS.private_tags }?tab=tx`, newPath: '/account/tag_address?tab=tx',
}, },
{ {
oldPath: '/pending-transactions', oldPath: '/pending-transactions',
newPath: `${ PATHS.txs }?tab=pending`, newPath: '/txs?tab=pending',
}, },
{ {
oldPath: '/tx/:id/internal-transactions', oldPath: '/tx/:hash/internal-transactions',
newPath: `${ PATHS.tx }?tab=internal`, newPath: '/tx/:hash?tab=internal',
}, },
{ {
oldPath: '/tx/:id/logs', oldPath: '/tx/:hash/logs',
newPath: `${ PATHS.tx }?tab=logs`, newPath: '/tx/:hash?tab=logs',
}, },
{ {
oldPath: '/tx/:id/raw-trace', oldPath: '/tx/:hash/raw-trace',
newPath: `${ PATHS.tx }?tab=raw_trace`, newPath: '/tx/:hash?tab=raw_trace',
}, },
{ {
oldPath: '/tx/:id/state', oldPath: '/tx/:hash/state',
newPath: `${ PATHS.tx }?tab=state`, newPath: '/tx/:hash?tab=state',
}, },
{ {
oldPath: '/uncles', oldPath: '/uncles',
newPath: `${ PATHS.blocks }?tab=uncles`, newPath: '/blocks?tab=uncles',
}, },
{ {
oldPath: '/reorgs', oldPath: '/reorgs',
newPath: `${ PATHS.blocks }?tab=reorgs`, newPath: '/blocks?tab=reorgs',
}, },
{ {
oldPath: '/block/:id/transactions', oldPath: '/block/:height/transactions',
newPath: `${ PATHS.block }`, newPath: '/block/:height?tab=txs',
}, },
{ {
oldPath: '/address/:id/transactions', oldPath: '/address/:hash/transactions',
newPath: `${ PATHS.address_index }`, newPath: '/address/:hash',
}, },
{ {
oldPath: '/address/:id/token-transfers', oldPath: '/address/:hash/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers`, newPath: '/address/:hash?tab=token_transfers',
}, },
{ {
oldPath: '/address/:id/tokens', oldPath: '/address/:hash/tokens',
newPath: `${ PATHS.address_index }?tab=tokens`, newPath: '/address/:hash?tab=tokens',
}, },
{ {
oldPath: '/address/:id/internal-transactions', oldPath: '/address/:hash/internal-transactions',
newPath: `${ PATHS.address_index }?tab=internal_txns`, newPath: '/address/:hash?tab=internal_txns',
}, },
{ {
oldPath: '/address/:id/coin-balances', oldPath: '/address/:hash/coin-balances',
newPath: `${ PATHS.address_index }?tab=coin_balance_history`, newPath: '/address/:hash?tab=coin_balance_history',
}, },
{ {
oldPath: '/address/:id/validations', oldPath: '/address/:hash/validations',
newPath: `${ PATHS.address_index }?tab=blocks_validated`, newPath: '/address/:hash?tab=blocks_validated',
}, },
{ {
oldPath: '/address/:id/tokens/:hash/token-transfers', oldPath: '/address/:hash/tokens/:token_hash/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers&token=:hash`, newPath: '/address/:hash?tab=token_transfers&token=:token_hash',
}, },
// contract verification // contract verification
{ {
oldPath: '/address/:id/contract_verifications/new', oldPath: '/address/:hash/contract_verifications/new',
newPath: `${ PATHS.address_contract_verification }`, newPath: '/address/:hash/contract_verification',
}, },
{ {
oldPath: '/address/:id/verify-via-flattened-code/new', oldPath: '/address/:hash/verify-via-flattened-code/new',
newPath: `${ PATHS.address_contract_verification }?method=flatten_source_code`, newPath: '/address/:hash/contract_verification?method=flatten_source_code',
}, },
{ {
oldPath: '/address/:id/verify-via-standard-json-input/new', oldPath: '/address/:hash/verify-via-standard-json-input/new',
newPath: `${ PATHS.address_contract_verification }?method=standard_input`, newPath: '/address/:hash/contract_verification?method=standard_input',
}, },
{ {
oldPath: '/address/:id/verify-via-metadata-json/new', oldPath: '/address/:hash/verify-via-metadata-json/new',
newPath: `${ PATHS.address_contract_verification }?method=sourcify`, newPath: '/address/:hash/contract_verification?method=sourcify',
}, },
{ {
oldPath: '/address/:id/verify-via-multi-part-files/new', oldPath: '/address/:hash/verify-via-multi-part-files/new',
newPath: `${ PATHS.address_contract_verification }?method=multi_part_file`, newPath: '/address/:hash/contract_verification?method=multi_part_file',
}, },
{ {
oldPath: '/address/:id/verify-vyper-contract/new', oldPath: '/address/:hash/verify-vyper-contract/new',
newPath: `${ PATHS.address_contract_verification }?method=vyper_contract`, newPath: '/address/:hash/contract_verification?method=vyper_contract',
}, },
]; ];
......
...@@ -126,14 +126,16 @@ frontend: ...@@ -126,14 +126,16 @@ frontend:
_default: ENC[AES256_GCM,data:cRcbMzOW2AFyDz/lqv4T9SpDCabXdBKLFN9dbq/rFg4=,iv:G9afggfvZ+BpuE5u31KDVfIxhlP38IE28Yt9pMQsd6E=,tag:t1kkjXPolc/58fuQVULLfA==,type:str] _default: ENC[AES256_GCM,data:cRcbMzOW2AFyDz/lqv4T9SpDCabXdBKLFN9dbq/rFg4=,iv:G9afggfvZ+BpuE5u31KDVfIxhlP38IE28Yt9pMQsd6E=,tag:t1kkjXPolc/58fuQVULLfA==,type:str]
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_default: ENC[AES256_GCM,data:LhVaS9L3ujRwznCe6D+edYU5XT4GLXm4jJ4lKGsrzyA=,iv:ibEJAfSrm3ZWtVJ5Du9MagbC6/Tv5L3xQQRjeQ1BGDA=,tag:IqtgpmJPhmHnSgMuIfI/0A==,type:str] _default: ENC[AES256_GCM,data:LhVaS9L3ujRwznCe6D+edYU5XT4GLXm4jJ4lKGsrzyA=,iv:ibEJAfSrm3ZWtVJ5Du9MagbC6/Tv5L3xQQRjeQ1BGDA=,tag:IqtgpmJPhmHnSgMuIfI/0A==,type:str]
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY:
_default: ENC[AES256_GCM,data:JZ+dOLHGXe2vzb380jPuw5weEp5UXPLWlYj2JsCIRZ4bdV3agTbGIw==,iv:gyzp3Bkhlw3JX2/mg1r8IWruY1b57esLrv09+jGkZUM=,tag:0N/XzMJM1hAVp+xlLCJupA==,type:str]
sops: sops:
kms: [] kms: []
gcp_kms: [] gcp_kms: []
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-01-30T18:25:02Z" lastmodified: "2023-02-14T08:03:14Z"
mac: ENC[AES256_GCM,data:osscQyqUAUatyEzeIHCj10+uj1vCzKJC9W0IyChPKR+KQSEW0I2l20nFcSMCb9sGWVNXzQCfltsDTXzHM5nIuzP2qn/qVmwntQcKB4ibGzaE1gshVQA6deQ28UvGkMjkNTKszqtONgc02G1Kl0d7yiigx+jAQDJNT94amwO/OPs=,iv:6g4T3KJTH0u96y+QBjjJPwjuU4+5psd1cIw+AddvWdE=,tag:Z59JO8iZwfbx80wACXk6sA==,type:str] mac: ENC[AES256_GCM,data:B2AkQb4I83dP3UUitRCcrUfzm3nWmcknIUoMWHyYaG9jasnccbr8zZatYdpbvKFcELVTtjhYk6ly5Sx7+6sk2PZm6o7dN3yHG5lSWmnZqNXkwo42GIk/F6vzDdLutZsu8HH8pWHd9y5R272CIPOOh4+Ur0OtwiGgj3Bp1od76qM=,iv:j7aIPflH0FsYhE/iylvBh5nDmVdghhxAFvaeXlR560k=,tag:/oe6OeitIHaZ4TgM7w/0pg==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
...@@ -81,7 +81,7 @@ blockscout: ...@@ -81,7 +81,7 @@ blockscout:
# # _default: ws://geth-svc:8546 # # _default: ws://geth-svc:8546
# _default: ws://geth-svc.goerli.svc.cluster.local:8546 # _default: ws://geth-svc.goerli.svc.cluster.local:8546
BLOCKSCOUT_VERSION: BLOCKSCOUT_VERSION:
_default: v4.1.8-beta _default: v5.1.0-beta
ECTO_USE_SSL: ECTO_USE_SSL:
_default: 'false' _default: 'false'
ETHEREUM_JSONRPC_VARIANT: ETHEREUM_JSONRPC_VARIANT:
...@@ -116,24 +116,12 @@ blockscout: ...@@ -116,24 +116,12 @@ blockscout:
_default: 1 _default: 1
COIN_BALANCE_HISTORY_DAYS: COIN_BALANCE_HISTORY_DAYS:
_default: 90 _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: POOL_SIZE:
_default: 100 _default: 100
DISPLAY_TOKEN_ICONS: DISPLAY_TOKEN_ICONS:
_default: 'true' _default: 'true'
FETCH_REWARDS_WAY: FETCH_REWARDS_WAY:
_default: manual _default: manual
INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER:
_default: 'true'
SHOW_TESTNET_LABEL: SHOW_TESTNET_LABEL:
_default: 'true' _default: 'true'
CHAIN_ID: CHAIN_ID:
...@@ -141,7 +129,7 @@ blockscout: ...@@ -141,7 +129,7 @@ blockscout:
ENABLE_RUST_VERIFICATION_SERVICE: ENABLE_RUST_VERIFICATION_SERVICE:
_default: 'true' _default: 'true'
RUST_VERIFICATION_SERVICE_URL: RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:80 _default: http://sc-verifier-svc:8050
INDEXER_MEMORY_LIMIT: INDEXER_MEMORY_LIMIT:
_default: 5 _default: 5
ACCOUNT_ENABLED: ACCOUNT_ENABLED:
...@@ -154,6 +142,30 @@ blockscout: ...@@ -154,6 +142,30 @@ blockscout:
_default: '[{"title": "Marketplace", "url": "/apps", "embedded?": true}]' _default: '[{"title": "Marketplace", "url": "/apps", "embedded?": true}]'
SESSION_COOKIE_DOMAIN: SESSION_COOKIE_DOMAIN:
_default: blockscout-main.test.aws-k8s.blockscout.com _default: blockscout-main.test.aws-k8s.blockscout.com
ETHEREUM_JSONRPC_DEBUG_TRACE_TRANSACTION_TIMEOUT:
_default: '20s'
INDEXER_INTERNAL_TRANSACTIONS_BATCH_SIZE:
_default: 15
INDEXER_DISABLE_EMPTY_BLOCK_SANITIZER:
_default: 'true'
INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER:
_default: 'true'
INDEXER_RECEIPTS_BATCH_SIZE:
_default: 50
INDEXER_COIN_BALANCES_BATCH_SIZE:
_default: 50
DISABLE_EXCHANGE_RATES:
_default: 'true'
DISABLE_INDEXER:
_default: 'false'
FIRST_BLOCK:
_default: '8446041'
LAST_BLOCK:
_default: '8446041'
TRACE_FIRST_BLOCK:
_default: '8446041'
TRACE_LAST_BLOCK:
_default: '8446041'
postgres: postgres:
enabled: true enabled: true
......
...@@ -72,14 +72,16 @@ frontend: ...@@ -72,14 +72,16 @@ frontend:
_default: ENC[AES256_GCM,data:4cTeqxQnGcpzPK4bMqxZpLgMeFSSDbajN/fmb1UunH8=,iv:bPTQfahGfWF1OfArvYQeSQItMa0Ymkt6eUfDZFBQSOY=,tag:8xHe5AUkbH/rl6cOfkVAKg==,type:str] _default: ENC[AES256_GCM,data:4cTeqxQnGcpzPK4bMqxZpLgMeFSSDbajN/fmb1UunH8=,iv:bPTQfahGfWF1OfArvYQeSQItMa0Ymkt6eUfDZFBQSOY=,tag:8xHe5AUkbH/rl6cOfkVAKg==,type:str]
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_default: ENC[AES256_GCM,data:x1CNJWk9wmqxjKWzD62pIb+scdzt5V22SPrXjvmsIR0=,iv:UjcwWfuGk3HDazHT5OcruevkQX/qAXiaHu6uVoJrSmE=,tag:NcCDR3tRULpiGJRpwBK0GQ==,type:str] _default: ENC[AES256_GCM,data:x1CNJWk9wmqxjKWzD62pIb+scdzt5V22SPrXjvmsIR0=,iv:UjcwWfuGk3HDazHT5OcruevkQX/qAXiaHu6uVoJrSmE=,tag:NcCDR3tRULpiGJRpwBK0GQ==,type:str]
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY:
_default: ENC[AES256_GCM,data:MCne89QeuJCrC/xIsYm+8n2dEPIdrd7UTfKm5H3w34nLCrCP5+e4ZA==,iv:NAEBjnkHCuiojGBD49hJGFd5R1jvS6VOEbqMmWQ4mWc=,tag:zYRe8AYbjNwV64gEoGcI3A==,type:str]
sops: sops:
kms: [] kms: []
gcp_kms: [] gcp_kms: []
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-01-30T18:24:47Z" lastmodified: "2023-02-14T08:03:25Z"
mac: ENC[AES256_GCM,data:KYXWcGq3Irq7styUQke+YFlpClE5LzmPLb7tIJcWmTTnksQKZcLibPJIoEBbPLjznJeKpY/AU777CXStihtZPcWeey7NO8VWk2LcVI1+r+/jzj1Gu0bqfIXIqTXFRy46QBjNsbRYMlZRyWlXSKlXXx4Ahp86sHu5nOHJ3oMe1FY=,iv:70iEvTB2J8lZuQzatWg4OKBN2/vYVoNabdJtbp7X8Y8=,tag:SYI8NKMOq67tYPmSWVQiMA==,type:str] mac: ENC[AES256_GCM,data:xGhF8Zc6BEh0TeAlp6UBTGZ6DNIL/d+nO7xNj2xvuHDmxElg816d8jxErbkgJzoNhs5DGbQXmp3pAUL4JPg8x5T+pn0hodvSb7c6zOlfTwumRC+6R6vC+ZOlxbzut4dSvtWxsPmrncWZLg6hyOilps7qvkLCj2d5UC+U7c1uQjc=,iv:OzLsfii8F3VFSSkUaY7ZjolgBbt8oXEtOYTHHMbJtPU=,tag:0rLPkbDmOSxOufCk9iZTDQ==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.4 2.933a.156.156 0 0 0-.155.156v10.844a.467.467 0 0 1-.934 0V3.09A1.089 1.089 0 0 1 6.401 2h7.474a.467.467 0 0 1 .322.137l4.355 4.355a.467.467 0 0 1 .137.33v7.111a.467.467 0 0 1-.933 0V7.29h-3.89a.467.467 0 0 1-.466-.467V2.933h-7Zm7.933.66v2.763h2.763l-2.763-2.763Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M6.4 2.933a.156.156 0 0 0-.155.156v10.844a.467.467 0 0 1-.934 0V3.09A1.089 1.089 0 0 1 6.401 2h7.474a.467.467 0 0 1 .322.137l4.355 4.355a.467.467 0 0 1 .137.33v7.111a.467.467 0 0 1-.933 0V7.29h-3.89a.467.467 0 0 1-.466-.467v-3.89h-7Zm7.933.66v2.763h2.763l-2.763-2.763Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.224 18.185v.628c0 .308-.04.565-.12.771-.067.19-.181.352-.328.465a.794.794 0 0 1-.48.152.8.8 0 0 1-.48-.152 1.026 1.026 0 0 1-.326-.465 2.159 2.159 0 0 1-.12-.77v-.629c0-.31.04-.566.12-.77.068-.19.181-.352.327-.466a.788.788 0 0 1 .48-.155c.18 0 .34.052.479.155.146.113.26.275.329.465.08.205.12.461.12.771Zm.82.625v-.617c0-.454-.07-.844-.21-1.17a1.674 1.674 0 0 0-.602-.758c-.258-.176-.57-.265-.935-.265-.363 0-.675.089-.939.265-.26.173-.47.437-.6.755-.14.326-.21.717-.21 1.173v.616c0 .452.07.841.21 1.17.139.327.339.578.6.754.264.174.576.26.94.26s.676-.086.935-.26c.262-.176.462-.427.601-.753.14-.33.21-.72.21-1.17Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M13.224 18.185v.628c0 .308-.04.565-.12.771-.067.19-.181.352-.328.465a.794.794 0 0 1-.48.152.8.8 0 0 1-.48-.152 1.026 1.026 0 0 1-.326-.465 2.159 2.159 0 0 1-.12-.77v-.629c0-.31.04-.566.12-.77.068-.19.181-.352.327-.466a.788.788 0 0 1 .48-.155c.18 0 .34.052.479.155.146.113.26.275.329.465.08.205.12.461.12.771Zm.82.625v-.617c0-.454-.07-.844-.21-1.17a1.674 1.674 0 0 0-.602-.758c-.258-.176-.57-.265-.935-.265-.363 0-.675.089-.939.265a1.65 1.65 0 0 0-.6.755c-.14.326-.21.717-.21 1.173v.616c0 .452.07.841.21 1.17.139.327.339.578.6.754.264.174.576.26.94.26s.676-.086.935-.26c.262-.176.462-.427.601-.753.14-.33.21-.72.21-1.17Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.22 16.056c.31-.206.671-.306 1.077-.306.406 0 .77.1 1.075.308.306.205.545.51.693.868.155.364.229.788.229 1.267v.616c0 .477-.075.901-.23 1.268v.001a1.923 1.923 0 0 1-.691.862c-.307.207-.67.304-1.076.304-.404 0-.766-.097-1.077-.303h-.001a1.923 1.923 0 0 1-.692-.863c-.156-.367-.23-.793-.23-1.268v-.617c0-.48.074-.905.23-1.27a1.9 1.9 0 0 1 .693-.866Zm1.077.194c-.32 0-.583.078-.8.223a1.403 1.403 0 0 0-.509.642l-.001.004c-.123.287-.19.642-.19 1.074v.616c0 .426.066.781.19 1.073.123.287.294.498.51.643.216.143.479.219.8.219.324 0 .586-.077.797-.219.216-.145.387-.355.51-.643.123-.292.19-.647.19-1.073v-.616c0-.429-.067-.784-.19-1.073v-.002a1.425 1.425 0 0 0-.511-.646h-.001c-.21-.144-.472-.222-.795-.222Zm-.33.898a.78.78 0 0 0-.242.35l-.002.007c-.065.167-.103.39-.103.68v.635c-.006.234.03.466.106.68a.78.78 0 0 0 .24.349c.102.07.215.104.325.102h.011c.11.002.223-.031.325-.102a.768.768 0 0 0 .242-.349l.002-.006c.066-.168.103-.392.103-.68v-.629c0-.29-.037-.514-.102-.68l-.003-.007a.767.767 0 0 0-.244-.35.535.535 0 0 0-.329-.104h-.005a.537.537 0 0 0-.324.104Zm.332-.604a1.037 1.037 0 0 0-.63.203l-.007.005a1.276 1.276 0 0 0-.406.575 2.39 2.39 0 0 0-.136.858v.625a2.41 2.41 0 0 0 .134.857v.001c.083.23.223.433.408.578l.01.008c.185.13.4.2.624.197.224.004.44-.066.625-.198l.008-.006c.187-.144.328-.346.41-.576.093-.243.135-.532.135-.858v-.628a2.39 2.39 0 0 0-.135-.858 1.265 1.265 0 0 0-.41-.576l-.004-.003a1.034 1.034 0 0 0-.626-.204Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M11.22 16.056c.31-.206.671-.306 1.077-.306.406 0 .77.1 1.075.308.306.205.545.51.693.868.155.364.229.788.229 1.267v.616c0 .477-.075.901-.23 1.268v.001a1.923 1.923 0 0 1-.691.862c-.307.207-.67.304-1.076.304-.404 0-.766-.097-1.077-.303h-.001a1.923 1.923 0 0 1-.692-.863c-.156-.367-.23-.793-.23-1.268v-.617c0-.48.074-.905.23-1.27a1.9 1.9 0 0 1 .693-.866Zm1.077.194c-.32 0-.583.078-.8.223a1.403 1.403 0 0 0-.509.642l-.001.004c-.123.287-.19.642-.19 1.074v.616c0 .426.066.781.19 1.073.123.287.294.498.51.643.216.143.479.219.8.219.324 0 .586-.077.797-.219.216-.145.387-.355.51-.643.123-.292.19-.647.19-1.073v-.616c0-.429-.067-.784-.19-1.073v-.002a1.425 1.425 0 0 0-.511-.646h-.001c-.21-.144-.472-.222-.795-.222Zm-.33.898a.78.78 0 0 0-.242.35l-.002.007c-.065.167-.103.39-.103.68v.635c-.006.234.03.466.106.68a.78.78 0 0 0 .24.349c.102.07.215.104.325.102h.011a.55.55 0 0 0 .325-.102.768.768 0 0 0 .242-.349l.002-.006c.066-.168.103-.392.103-.68v-.629c0-.29-.037-.514-.102-.68l-.003-.007a.767.767 0 0 0-.244-.35.535.535 0 0 0-.329-.104h-.005a.537.537 0 0 0-.324.104Zm.332-.604a1.037 1.037 0 0 0-.63.203l-.007.005a1.276 1.276 0 0 0-.406.575 2.39 2.39 0 0 0-.136.858v.625a2.41 2.41 0 0 0 .134.857v.001c.083.23.223.433.408.578l.01.008c.185.13.4.2.624.197.224.004.44-.066.625-.198l.008-.006c.187-.144.328-.346.41-.576.093-.243.135-.532.135-.858v-.628a2.39 2.39 0 0 0-.135-.858 1.265 1.265 0 0 0-.41-.576l-.004-.003a1.034 1.034 0 0 0-.626-.204Z" fill="currentColor"/>
<path d="M17.06 21v-1h1.301a.5.5 0 0 1 .5.5v.5H17.06Zm-.699 0a.5.5 0 0 1-.5-.5v-3.9a.6.6 0 0 1 1.199 0V21h-.699Z" fill="currentColor"/> <path d="M17.06 21v-1h1.301a.5.5 0 0 1 .5.5v.5H17.06Zm-.699 0a.5.5 0 0 1-.5-.5v-3.9a.6.6 0 0 1 1.199 0V21h-.699Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.861 21v-.5a.5.5 0 0 0-.5-.5H17.06v-3.4a.6.6 0 0 0-1.199 0v3.9a.5.5 0 0 0 .5.5h2.5Zm-1.701-1.1v-3.3a.7.7 0 0 0-1.399 0v3.9a.6.6 0 0 0 .6.6h2.6v-.6a.6.6 0 0 0-.6-.6H17.16Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M18.861 21v-.5a.5.5 0 0 0-.5-.5H17.06v-3.4a.6.6 0 0 0-1.199 0v3.9a.5.5 0 0 0 .5.5h2.5Zm-1.701-1.1v-3.3a.7.7 0 0 0-1.399 0v3.9a.6.6 0 0 0 .6.6h2.6v-.6a.6.6 0 0 0-.6-.6H17.16Z" fill="currentColor"/>
<path d="M6.468 17.532c0-.192.067-.316.163-.398.104-.09.286-.166.562-.166.332 0 .655.108.921.307a.484.484 0 0 0 .579-.776A2.516 2.516 0 0 0 7.195 16h-.001c-.45 0-.873.125-1.192.399-.328.28-.502.68-.502 1.133 0 .244.067.464.196.651.124.182.29.309.452.401.29.165.657.262.949.34l.053.013c.339.09.589.163.76.267.135.083.17.15.17.264 0 .25-.086.352-.19.419-.138.089-.372.145-.696.145a1.548 1.548 0 0 1-.92-.307.484.484 0 0 0-.58.776c.433.322.958.498 1.498.499h.002c.402 0 .853-.064 1.218-.298.4-.256.636-.677.636-1.234 0-.532-.287-.878-.635-1.09-.31-.189-.699-.292-1.003-.373l-.012-.003c-.344-.091-.597-.16-.772-.26a.39.39 0 0 1-.132-.106c-.013-.018-.026-.045-.026-.104Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6.468 17.532c0-.192.067-.316.163-.398.104-.09.286-.166.562-.166.332 0 .655.108.921.307a.484.484 0 0 0 .579-.776A2.516 2.516 0 0 0 7.195 16h-.001c-.45 0-.873.125-1.192.399-.328.28-.502.68-.502 1.133 0 .244.067.464.196.651.124.182.29.309.452.401.29.165.657.262.949.34l.053.013c.339.09.589.163.76.267.135.083.17.15.17.264 0 .25-.086.352-.19.419-.138.089-.372.145-.696.145a1.548 1.548 0 0 1-.92-.307.484.484 0 0 0-.58.776A2.52 2.52 0 0 0 7.192 21h.002c.402 0 .853-.064 1.218-.298.4-.256.636-.677.636-1.234 0-.532-.287-.878-.635-1.09-.31-.189-.699-.292-1.003-.373l-.012-.003c-.344-.091-.597-.16-.772-.26a.39.39 0 0 1-.132-.106c-.013-.018-.026-.045-.026-.104Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.4 2.933a.156.156 0 0 0-.155.156v10.844a.467.467 0 0 1-.934 0V3.09A1.089 1.089 0 0 1 6.401 2h7.474a.467.467 0 0 1 .322.137l4.355 4.355a.467.467 0 0 1 .137.33v7.111a.467.467 0 0 1-.933 0V7.29h-3.89a.467.467 0 0 1-.466-.467V2.933h-7Zm7.933.66v2.763h2.763l-2.763-2.763Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M6.4 2.933a.156.156 0 0 0-.155.156v10.844a.467.467 0 0 1-.934 0V3.09A1.089 1.089 0 0 1 6.401 2h7.474a.467.467 0 0 1 .322.137l4.355 4.355a.467.467 0 0 1 .137.33v7.111a.467.467 0 0 1-.933 0V7.29h-3.89a.467.467 0 0 1-.466-.467v-3.89h-7Zm7.933.66v2.763h2.763l-2.763-2.763Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.06 20h1.301a.5.5 0 0 1 .5.5v.5h-2.5a.5.5 0 0 1-.5-.5v-3.9a.6.6 0 0 1 1.199 0V20Z" fill="currentColor"/> <path d="M17.06 20h1.301a.5.5 0 0 1 .5.5v.5h-2.5a.5.5 0 0 1-.5-.5v-3.9a.6.6 0 0 1 1.199 0V20Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.861 21v-.5a.5.5 0 0 0-.5-.5H17.06v-3.4a.6.6 0 0 0-1.199 0v3.9a.5.5 0 0 0 .5.5h2.5Zm-1.701-1.1v-3.3a.7.7 0 0 0-1.399 0v3.9a.6.6 0 0 0 .6.6h2.6v-.6a.6.6 0 0 0-.6-.6H17.16Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M18.861 21v-.5a.5.5 0 0 0-.5-.5H17.06v-3.4a.6.6 0 0 0-1.199 0v3.9a.5.5 0 0 0 .5.5h2.5Zm-1.701-1.1v-3.3a.7.7 0 0 0-1.399 0v3.9a.6.6 0 0 0 .6.6h2.6v-.6a.6.6 0 0 0-.6-.6H17.16Z" fill="currentColor"/>
<path d="M11.18 15.85a.63.63 0 0 1 .63.63v3.131c0 .177.043.291.11.365.066.071.156.113.29.115.135-.002.222-.044.282-.113l.002-.002c.067-.074.11-.188.11-.364V16.48a.63.63 0 0 1 1.26 0v3.288h-.007a1.44 1.44 0 0 1-.213.641l-.001.002a1.592 1.592 0 0 1-.592.543l-.002.001a1.74 1.74 0 0 1-.682.189v.006h-.313v-.006a1.788 1.788 0 0 1-.688-.188l-.003-.002a1.592 1.592 0 0 1-.592-.543v-.002a1.44 1.44 0 0 1-.214-.64h-.007v-3.29a.63.63 0 0 1 .63-.629ZM7.286 16.285l.397 1.229.484-1.3a.558.558 0 0 1 1.046.388l-.983 2.656v1.304a.588.588 0 1 1-1.177 0v-1.304l-.953-2.56a.628.628 0 1 1 1.186-.413Z" fill="currentColor"/> <path d="M11.18 15.85a.63.63 0 0 1 .63.63v3.131c0 .177.043.291.11.365a.378.378 0 0 0 .29.115c.135-.002.222-.044.282-.113l.002-.002c.067-.074.11-.188.11-.364V16.48a.63.63 0 0 1 1.26 0v3.288h-.007a1.44 1.44 0 0 1-.213.641l-.001.002a1.592 1.592 0 0 1-.592.543l-.002.001a1.74 1.74 0 0 1-.682.189v.006h-.313v-.006a1.788 1.788 0 0 1-.688-.188l-.003-.002a1.592 1.592 0 0 1-.592-.543v-.002a1.44 1.44 0 0 1-.214-.64h-.007v-3.29a.63.63 0 0 1 .63-.629Zm-3.894.435.397 1.229.484-1.3a.558.558 0 0 1 1.046.388l-.983 2.656v1.304a.588.588 0 1 1-1.177 0v-1.304l-.953-2.56a.628.628 0 1 1 1.186-.413Z" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.346 13.736c.607 1.073-.75 1.406-1.386 1.467-.965.094-1.165-.464-.976-1.166a1.262 1.262 0 0 1 1.085-.951 1.342 1.342 0 0 1 1.277.65Zm3.787-1.984c-.705 5.417 8.8 4.29 8.618 8.6.938-1.223 1.34-4.55-1.42-6.262-2.458-1.526-5.662-.69-7.198-2.338Zm5.474-2.042c-.061-.058-.125-.114-.187-.17.063.057.125.117.187.17Z" fill="currentColor"/>
<path d="m25.817 13.658-.006-.009a3.601 3.601 0 0 0-.292-.458 2.358 2.358 0 0 0-1.303-.886 5.545 5.545 0 0 0-1.06-.167c-.364-.027-.734-.042-1.108-.061-.75-.042-1.518-.12-2.268-.334a6.304 6.304 0 0 1-1.11-.425 4.745 4.745 0 0 1-.973-.71c-.577-.54-1.031-1.151-1.486-1.745a20.606 20.606 0 0 0-1.378-1.714 6.285 6.285 0 0 0-1.687-1.324 5.812 5.812 0 0 0-2.101-.602 4.947 4.947 0 0 1 2.244.275 5.934 5.934 0 0 1 1.947 1.242c.367.345.712.714 1.032 1.104 2.38-.47 4.312-.052 5.796.76l.034.016a7.31 7.31 0 0 1 1.507 1.092c.316.292.61.606.88.942l.021.027c.877 1.115 1.31 2.274 1.31 2.977Z" fill="currentColor"/>
<path d="m25.816 13.658-.005-.011.005.01ZM11.165 5.92c.608.088 1.228.331 1.627.795.4.463.546 1.066.662 1.64.093.444.168.897.342 1.318.084.205.208.385.311.579.086.161.241.306.301.478a.154.154 0 0 1-.018.153c-.212.235-.783-.026-1-.132a3.254 3.254 0 0 1-.982-.753c-.866-.966-1.313-2.355-1.286-3.62a4.22 4.22 0 0 1 .043-.458Zm10.161 10.887c-1.312 3.679 4.64 6.147 2.41 9.888 2.289-.95 3.375-3.817 2.425-6.092-.83-1.998-3.286-2.726-4.835-3.796Zm-7.873 4.809c.358-.27.749-.493 1.164-.663.42-.17.854-.295 1.299-.377.882-.169 1.755-.21 2.488-.507.362-.142.695-.35.983-.612.279-.26.492-.583.623-.942.133-.378.188-.78.16-1.181a4.348 4.348 0 0 0-.289-1.256 2.883 2.883 0 0 1 .734 2.614 2.71 2.71 0 0 1-.715 1.294c-.35.346-.77.61-1.234.773-.441.155-.902.25-1.37.282-.45.038-.884.048-1.312.074a8.433 8.433 0 0 0-2.53.502Zm8.397 6.469c-.133.105-.265.217-.41.315-.147.097-.3.183-.459.257-.33.162-.695.245-1.063.242-.997-.02-1.702-.765-2.115-1.608-.281-.574-.475-1.195-.809-1.742-.477-.783-1.294-1.413-2.25-1.296-.39.049-.755.225-.972.565-.57.888.248 2.132 1.292 1.956a1.14 1.14 0 0 0 .259-.072.936.936 0 0 0 .23-.14 1.05 1.05 0 0 0 .317-.461c.069-.188.084-.391.044-.587a.806.806 0 0 0-.335-.502c.2.094.356.263.435.47.082.215.104.447.061.672a1.332 1.332 0 0 1-.298.635 1.304 1.304 0 0 1-.281.24 1.638 1.638 0 0 1-1.064.234 1.946 1.946 0 0 1-.947-.413c-.322-.256-.562-.591-.854-.88a4.057 4.057 0 0 0-1.164-.854c-.3-.132-.615-.23-.938-.291a6.176 6.176 0 0 0-.49-.08c-.075-.007-.443-.089-.494-.041a10.29 10.29 0 0 1 1.65-1.243 8.013 8.013 0 0 1 1.935-.832 5.497 5.497 0 0 1 2.164-.166c.374.045.74.14 1.088.282.365.147.702.356.995.618.291.275.526.604.692.97.15.34.262.698.334 1.064.214 1.095.135 2.793 1.563 3.044.074.014.15.025.225.032l.233.006c.16-.012.32-.035.477-.07.326-.076.644-.185.948-.324Z" fill="currentColor"/>
<path d="m13.553 26.89-.037-.029.037.03Zm-1.281-15.456a1.59 1.59 0 0 1-.267.554 2.167 2.167 0 0 1-.889.683c-.315.137-.65.225-.99.262-.075.01-.152.015-.226.02a1.01 1.01 0 0 0-.94.754 2.57 2.57 0 0 0-.057.317c-.034.277-.04.565-.07.914a5.428 5.428 0 0 1-.505 1.706c-.34.718-.72 1.296-.632 2.123.058.537.332.896.696 1.268.656.674 2.125.975 1.797 2.637-.198.991-1.835 2.032-4.135 2.395.229-.035-.294-.919-.326-.975-.247-.388-.517-.754-.713-1.175-.384-.816-.562-1.76-.405-2.655.166-.942.86-1.664 1.436-2.383.687-.856 1.407-1.978 1.566-3.089.037-.27.064-.606.124-.942.057-.371.173-.731.343-1.066.116-.22.269-.417.452-.585a.585.585 0 0 0 .112-.712L4.976 4.86l5.267 6.53a.656.656 0 0 0 1.01.022.448.448 0 0 0 .013-.565c-.344-.442-.708-.892-1.06-1.335L8.88 7.864l-2.66-3.29L2.592 0 6.56 4.278l2.832 3.146L10.806 9c.469.53.937 1.044 1.406 1.601l.077.094.017.146a1.74 1.74 0 0 1-.034.593Zm1.214 15.413a3.535 3.535 0 0 1-.674-.689c.207.247.432.477.674.689Z" fill="currentColor"/>
</svg>
...@@ -4,14 +4,14 @@ import appConfig from 'configs/app/config'; ...@@ -4,14 +4,14 @@ import appConfig from 'configs/app/config';
import isNeedProxy from './isNeedProxy'; import isNeedProxy from './isNeedProxy';
import { RESOURCES } from './resources'; import { RESOURCES } from './resources';
import type { ApiResource, ResourceName } from './resources'; import type { ApiResource, ResourceName, ResourcePathParams } from './resources';
export default function buildUrl( export default function buildUrl<R extends ResourceName>(
_resource: ApiResource | ResourceName, resourceName: R,
pathParams?: Record<string, string | undefined>, pathParams?: ResourcePathParams<R>,
queryParams?: Record<string, string | Array<string> | number | undefined>, queryParams?: Record<string, string | Array<string> | number | undefined>,
) { ): string {
const resource: ApiResource = typeof _resource === 'string' ? RESOURCES[_resource] : _resource; const resource: ApiResource = RESOURCES[resourceName];
const baseUrl = isNeedProxy() ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint); const baseUrl = isNeedProxy() ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath; const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path; const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
......
...@@ -13,9 +13,10 @@ export default function fetchFactory( ...@@ -13,9 +13,10 @@ export default function fetchFactory(
// first arg can be only a string // first arg can be only a string
// FIXME migrate to RequestInfo later if needed // FIXME migrate to RequestInfo later if needed
return function fetch(url: string, init?: RequestInit): Promise<Response> { return function fetch(url: string, init?: RequestInit): Promise<Response> {
const incomingContentType = _req.headers['content-type'];
const headers = { const headers = {
accept: 'application/json', accept: 'application/json',
'content-type': 'application/json', 'content-type': incomingContentType?.match(/^multipart\/form-data/) ? incomingContentType : 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`, cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
}; };
...@@ -25,10 +26,23 @@ export default function fetchFactory( ...@@ -25,10 +26,23 @@ export default function fetchFactory(
req: _req, req: _req,
}); });
const body = (() => {
const _body = init?.body;
if (!_body) {
return;
}
if (typeof _body === 'string') {
return _body;
}
return JSON.stringify(_body);
})();
return nodeFetch(url, { return nodeFetch(url, {
...init, ...init,
headers, headers,
body: init?.body ? JSON.stringify(init.body) : undefined, body,
}); });
}; };
} }
...@@ -2,7 +2,6 @@ import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, Ap ...@@ -2,7 +2,6 @@ import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, Ap
import type { import type {
Address, Address,
AddressCounters, AddressCounters,
AddressTokenBalance,
AddressTransactionsResponse, AddressTransactionsResponse,
AddressTokenTransferResponse, AddressTokenTransferResponse,
AddressCoinBalanceHistoryResponse, AddressCoinBalanceHistoryResponse,
...@@ -17,15 +16,22 @@ import type { ...@@ -17,15 +16,22 @@ import type {
import type { AddressesResponse } from 'types/api/addresses'; import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { IndexingStatus } from 'types/api/indexingStatus'; import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search'; import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo'; import type {
import type { TokensResponse, TokensFilters, TokenInstance, TokenInstanceTransfersCount, TokenInstanceTransferResponse } from 'types/api/tokens'; TokenCounters,
TokenInfo,
TokenHolders,
TokenInventoryResponse,
TokenInstance,
TokenInstanceTransfersCount,
} from 'types/api/token';
import type { TokensResponse, TokensFilters, TokenInstanceTransferResponse } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
...@@ -38,6 +44,7 @@ export interface ApiResource { ...@@ -38,6 +44,7 @@ export interface ApiResource {
path: string; path: string;
endpoint?: string; endpoint?: string;
basePath?: string; basePath?: string;
pathParams?: Array<string>;
} }
export const RESOURCES = { export const RESOURCES = {
...@@ -50,21 +57,27 @@ export const RESOURCES = { ...@@ -50,21 +57,27 @@ export const RESOURCES = {
}, },
custom_abi: { custom_abi: {
path: '/api/account/v1/user/custom_abis/:id?', path: '/api/account/v1/user/custom_abis/:id?',
pathParams: [ 'id' as const ],
}, },
watchlist: { watchlist: {
path: '/api/account/v1/user/watchlist/:id?', path: '/api/account/v1/user/watchlist/:id?',
pathParams: [ 'id' as const ],
}, },
public_tags: { public_tags: {
path: '/api/account/v1/user/public_tags/:id?', path: '/api/account/v1/user/public_tags/:id?',
pathParams: [ 'id' as const ],
}, },
private_tags_address: { private_tags_address: {
path: '/api/account/v1/user/tags/address/:id?', path: '/api/account/v1/user/tags/address/:id?',
pathParams: [ 'id' as const ],
}, },
private_tags_tx: { private_tags_tx: {
path: '/api/account/v1/user/tags/transaction/:id?', path: '/api/account/v1/user/tags/transaction/:id?',
pathParams: [ 'id' as const ],
}, },
api_keys: { api_keys: {
path: '/api/account/v1/user/api_keys/:id?', path: '/api/account/v1/user/api_keys/:id?',
pathParams: [ 'id' as const ],
}, },
// STATS // STATS
...@@ -80,6 +93,7 @@ export const RESOURCES = { ...@@ -80,6 +93,7 @@ export const RESOURCES = {
}, },
stats_line: { stats_line: {
path: '/api/v1/lines/:id', path: '/api/v1/lines/:id',
pathParams: [ 'id' as const ],
endpoint: appConfig.statsApi.endpoint, endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath, basePath: appConfig.statsApi.basePath,
}, },
...@@ -98,10 +112,12 @@ export const RESOURCES = { ...@@ -98,10 +112,12 @@ export const RESOURCES = {
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
block: { block: {
path: '/api/v2/blocks/:id', path: '/api/v2/blocks/:height',
pathParams: [ 'height' as const ],
}, },
block_txs: { block_txs: {
path: '/api/v2/blocks/:id/transactions', path: '/api/v2/blocks/:height/transactions',
pathParams: [ 'height' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -116,25 +132,30 @@ export const RESOURCES = { ...@@ -116,25 +132,30 @@ export const RESOURCES = {
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
}, },
tx: { tx: {
path: '/api/v2/transactions/:id', path: '/api/v2/transactions/:hash',
pathParams: [ 'hash' as const ],
}, },
tx_internal_txs: { tx_internal_txs: {
path: '/api/v2/transactions/:id/internal-transactions', path: '/api/v2/transactions/:hash/internal-transactions',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const, 'transaction_index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx_logs: { tx_logs: {
path: '/api/v2/transactions/:id/logs', path: '/api/v2/transactions/:hash/logs',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'transaction_hash' as const, 'index' as const ], paginationFields: [ 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx_token_transfers: { tx_token_transfers: {
path: '/api/v2/transactions/:id/token-transfers', path: '/api/v2/transactions/:hash/token-transfers',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
tx_raw_trace: { tx_raw_trace: {
path: '/api/v2/transactions/:id/raw-trace', path: '/api/v2/transactions/:hash/raw-trace',
pathParams: [ 'hash' as const ],
}, },
// ADDRESSES // ADDRESSES
...@@ -146,90 +167,124 @@ export const RESOURCES = { ...@@ -146,90 +167,124 @@ export const RESOURCES = {
// ADDRESS // ADDRESS
address: { address: {
path: '/api/v2/addresses/:id', path: '/api/v2/addresses/:hash',
pathParams: [ 'hash' as const ],
}, },
address_counters: { address_counters: {
path: '/api/v2/addresses/:id/counters', path: '/api/v2/addresses/:hash/counters',
}, pathParams: [ 'hash' as const ],
address_token_balances: {
path: '/api/v2/addresses/:id/token-balances',
}, },
// this resource doesn't have pagination, so causing huge problems on some addresses page
// address_token_balances: {
// path: '/api/v2/addresses/:hash/token-balances',
// },
address_txs: { address_txs: {
path: '/api/v2/addresses/:id/transactions', path: '/api/v2/addresses/:hash/transactions',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [ 'filter' as const ], filterFields: [ 'filter' as const ],
}, },
address_internal_txs: { address_internal_txs: {
path: '/api/v2/addresses/:id/internal-transactions', path: '/api/v2/addresses/:hash/internal-transactions',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const ], filterFields: [ 'filter' as const ],
}, },
address_token_transfers: { address_token_transfers: {
path: '/api/v2/addresses/:id/token-transfers', path: '/api/v2/addresses/:hash/token-transfers',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'token' as const ], filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
}, },
address_blocks_validated: { address_blocks_validated: {
path: '/api/v2/addresses/:id/blocks-validated', path: '/api/v2/addresses/:hash/blocks-validated',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'block_number' as const ], paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_coin_balance: { address_coin_balance: {
path: '/api/v2/addresses/:id/coin-balance-history', path: '/api/v2/addresses/:hash/coin-balance-history',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'block_number' as const ], paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_coin_balance_chart: { address_coin_balance_chart: {
path: '/api/v2/addresses/:id/coin-balance-history-by-day', path: '/api/v2/addresses/:hash/coin-balance-history-by-day',
pathParams: [ 'hash' as const ],
}, },
address_logs: { address_logs: {
path: '/api/v2/addresses/:id/logs', path: '/api/v2/addresses/:hash/logs',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'transaction_index' as const, 'index' as const, 'block_number' as const ], paginationFields: [ 'items_count' as const, 'transaction_index' as const, 'index' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_tokens: { address_tokens: {
path: '/api/v2/addresses/:id/tokens', path: '/api/v2/addresses/:hash/tokens',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const ], paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
// CONTRACT // CONTRACT
contract: { contract: {
path: '/api/v2/smart-contracts/:id', path: '/api/v2/smart-contracts/:hash',
pathParams: [ 'hash' as const ],
}, },
contract_methods_read: { contract_methods_read: {
path: '/api/v2/smart-contracts/:id/methods-read', path: '/api/v2/smart-contracts/:hash/methods-read',
pathParams: [ 'hash' as const ],
}, },
contract_methods_read_proxy: { contract_methods_read_proxy: {
path: '/api/v2/smart-contracts/:id/methods-read-proxy', path: '/api/v2/smart-contracts/:hash/methods-read-proxy',
pathParams: [ 'hash' as const ],
}, },
contract_method_query: { contract_method_query: {
path: '/api/v2/smart-contracts/:id/query-read-method', path: '/api/v2/smart-contracts/:hash/query-read-method',
pathParams: [ 'hash' as const ],
}, },
contract_methods_write: { contract_methods_write: {
path: '/api/v2/smart-contracts/:id/methods-write', path: '/api/v2/smart-contracts/:hash/methods-write',
pathParams: [ 'hash' as const ],
}, },
contract_methods_write_proxy: { contract_methods_write_proxy: {
path: '/api/v2/smart-contracts/:id/methods-write-proxy', path: '/api/v2/smart-contracts/:hash/methods-write-proxy',
pathParams: [ 'hash' as const ],
},
contract_verification_config: {
path: '/api/v2/smart-contracts/verification/config',
},
contract_verification_via: {
path: '/api/v2/smart-contracts/:hash/verification/via/:method',
pathParams: [ 'hash' as const, 'method' as const ],
}, },
// TOKEN // TOKEN
token: { token: {
path: '/api/v2/tokens/:hash', path: '/api/v2/tokens/:hash',
pathParams: [ 'hash' as const ],
}, },
token_counters: { token_counters: {
path: '/api/v2/tokens/:hash/counters', path: '/api/v2/tokens/:hash/counters',
pathParams: [ 'hash' as const ],
}, },
token_holders: { token_holders: {
path: '/api/v2/tokens/:hash/holders', path: '/api/v2/tokens/:hash/holders',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'value' as const ], paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [], filterFields: [],
}, },
token_transfers: { token_transfers: {
path: '/api/v2/tokens/:hash/transfers', path: '/api/v2/tokens/:hash/transfers',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
token_inventory: {
path: '/api/v2/tokens/:hash/instances',
pathParams: [ 'hash' as const ],
paginationFields: [ 'unique_token' as const ],
filterFields: [],
},
tokens: { tokens: {
path: '/api/v2/tokens', path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ], paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ],
...@@ -239,12 +294,15 @@ export const RESOURCES = { ...@@ -239,12 +294,15 @@ export const RESOURCES = {
// TOKEN INSTANCE // TOKEN INSTANCE
token_instance: { token_instance: {
path: '/api/v2/tokens/:hash/instances/:id', path: '/api/v2/tokens/:hash/instances/:id',
pathParams: [ 'hash' as const, 'id' as const ],
}, },
token_instance_transfers_count: { token_instance_transfers_count: {
path: '/api/v2/tokens/:hash/instances/:id/transfers-count', path: '/api/v2/tokens/:hash/instances/:id/transfers-count',
pathParams: [ 'hash' as const, 'id' as const ],
}, },
token_instance_transfers: { token_instance_transfers: {
path: '/api/v2/tokens/:hash/instances/:id/transfers', path: '/api/v2/tokens/:hash/instances/:id/transfers',
pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -307,6 +365,15 @@ export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R] ...@@ -307,6 +365,15 @@ export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R]
export const resourceKey = (x: keyof typeof RESOURCES) => x; export const resourceKey = (x: keyof typeof RESOURCES) => x;
type ResourcePathParamName<Resource extends ResourceName> =
typeof RESOURCES[Resource] extends { pathParams: Array<string> } ?
ArrayElement<typeof RESOURCES[Resource]['pathParams']> :
string;
export type ResourcePathParams<Resource extends ResourceName> = typeof RESOURCES[Resource] extends { pathParams: Array<string> } ?
Record<ResourcePathParamName<Resource>, string | undefined> :
never;
export interface ResourceError<T = unknown> { export interface ResourceError<T = unknown> {
payload?: T; payload?: T;
status: Response['status']; status: Response['status'];
...@@ -322,7 +389,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -322,7 +389,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_logs' | 'address_tokens' |
'token_transfers' | 'token_holders' | 'tokens' | 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
'token_instance_transfers'; 'token_instance_transfers';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -358,7 +425,6 @@ Q extends 'tx_raw_trace' ? RawTracesResponse : ...@@ -358,7 +425,6 @@ Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'addresses' ? AddressesResponse : Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address : Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters : Q extends 'address_counters' ? AddressCounters :
Q extends 'address_token_balances' ? Array<AddressTokenBalance> :
Q extends 'address_txs' ? AddressTransactionsResponse : Q extends 'address_txs' ? AddressTransactionsResponse :
Q extends 'address_internal_txs' ? AddressInternalTxsResponse : Q extends 'address_internal_txs' ? AddressInternalTxsResponse :
Q extends 'address_token_transfers' ? AddressTokenTransferResponse : Q extends 'address_token_transfers' ? AddressTokenTransferResponse :
...@@ -374,6 +440,7 @@ Q extends 'token_holders' ? TokenHolders : ...@@ -374,6 +440,7 @@ Q extends 'token_holders' ? TokenHolders :
Q extends 'token_instance' ? TokenInstance : Q extends 'token_instance' ? TokenInstance :
Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount : Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount :
Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse : Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse : Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
...@@ -382,6 +449,7 @@ Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> : ...@@ -382,6 +449,7 @@ Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> : Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> :
Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> : Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> :
Q extends 'visualize_sol2uml' ? VisualizedContract : Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
...@@ -6,10 +6,10 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -6,10 +6,10 @@ import useFetch from 'lib/hooks/useFetch';
import buildUrl from './buildUrl'; import buildUrl from './buildUrl';
import { RESOURCES } from './resources'; import { RESOURCES } from './resources';
import type { ApiResource } from './resources'; import type { ApiResource, ResourceName, ResourcePathParams } from './resources';
export interface Params { export interface Params<R extends ResourceName> {
pathParams?: Record<string, string | undefined>; pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | Array<string> | number | undefined>; queryParams?: Record<string, string | Array<string> | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>; fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
} }
...@@ -17,12 +17,12 @@ export interface Params { ...@@ -17,12 +17,12 @@ export interface Params {
export default function useApiFetch() { export default function useApiFetch() {
const fetch = useFetch(); const fetch = useFetch();
return React.useCallback(<R extends keyof typeof RESOURCES, SuccessType = unknown, ErrorType = unknown>( return React.useCallback(<R extends ResourceName, SuccessType = unknown, ErrorType = unknown>(
resourceName: R, resourceName: R,
{ pathParams, queryParams, fetchParams }: Params = {}, { pathParams, queryParams, fetchParams }: Params<R> = {},
) => { ) => {
const resource: ApiResource = RESOURCES[resourceName]; const resource: ApiResource = RESOURCES[resourceName];
const url = buildUrl(resource, pathParams, queryParams); const url = buildUrl(resourceName, pathParams, queryParams);
return fetch<SuccessType, ErrorType>(url, { return fetch<SuccessType, ErrorType>(url, {
credentials: 'include', credentials: 'include',
...(resource.endpoint && appConfig.host === 'localhost' ? { ...(resource.endpoint && appConfig.host === 'localhost' ? {
......
...@@ -5,7 +5,7 @@ import type { ResourceError, ResourceName, ResourcePayload } from './resources'; ...@@ -5,7 +5,7 @@ import type { ResourceError, ResourceName, ResourcePayload } from './resources';
import type { Params as ApiFetchParams } from './useApiFetch'; import type { Params as ApiFetchParams } from './useApiFetch';
import useApiFetch from './useApiFetch'; import useApiFetch from './useApiFetch';
export interface Params<R extends ResourceName, E = unknown> extends ApiFetchParams { export interface Params<R extends ResourceName, E = unknown> extends ApiFetchParams<R> {
queryOptions?: Omit<UseQueryOptions<unknown, ResourceError<E>, ResourcePayload<R>>, 'queryKey' | 'queryFn'>; queryOptions?: Omit<UseQueryOptions<unknown, ResourceError<E>, ResourcePayload<R>>, 'queryKey' | 'queryFn'>;
} }
......
import type BigNumber from 'bignumber.js';
export default function sumBnReducer(result: BigNumber, item: BigNumber) {
return result.plus(item);
}
...@@ -12,3 +12,6 @@ export const DAY = 24 * HOUR; ...@@ -12,3 +12,6 @@ export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY; export const WEEK = 7 * DAY;
export const MONTH = 30 * DAY; export const MONTH = 30 * DAY;
export const YEAR = 365 * DAY; export const YEAR = 365 * DAY;
export const Kb = 1_000;
export const Mb = 1_000 * Kb;
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { ZERO } from 'lib/consts';
interface Params { interface Params {
value: string; value: string;
exchangeRate?: string | null; exchangeRate?: string | null;
...@@ -13,10 +15,11 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal ...@@ -13,10 +15,11 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat(); const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
let usdResult: string | undefined; let usdResult: string | undefined;
let usdBn = ZERO;
if (exchangeRate) { if (exchangeRate) {
const exchangeRateBn = new BigNumber(exchangeRate); const exchangeRateBn = new BigNumber(exchangeRate);
const usdBn = valueCurr.times(exchangeRateBn); usdBn = valueCurr.times(exchangeRateBn);
if (accuracyUsd && !usdBn.isEqualTo(0)) { if (accuracyUsd && !usdBn.isEqualTo(0)) {
const usdBnDp = usdBn.dp(accuracyUsd); const usdBnDp = usdBn.dp(accuracyUsd);
usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat(); usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat();
...@@ -25,5 +28,5 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal ...@@ -25,5 +28,5 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal
} }
} }
return { valueStr: valueResult, usd: usdResult }; return { valueStr: valueResult, usd: usdResult, usdBn };
} }
...@@ -11,7 +11,7 @@ export interface Params { ...@@ -11,7 +11,7 @@ export interface Params {
method?: RequestInit['method']; method?: RequestInit['method'];
headers?: RequestInit['headers']; headers?: RequestInit['headers'];
signal?: RequestInit['signal']; signal?: RequestInit['signal'];
body?: Record<string, unknown>; body?: Record<string, unknown> | FormData;
credentials?: RequestCredentials; credentials?: RequestCredentials;
} }
...@@ -20,13 +20,27 @@ export default function useFetch() { ...@@ -20,13 +20,27 @@ export default function useFetch() {
const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {}; const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => { return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => {
const hasBody = params?.method && ![ 'GET', 'HEAD' ].includes(params.method); const _body = params?.body;
const isFormData = _body instanceof FormData;
const isBodyAllowed = params?.method && ![ 'GET', 'HEAD' ].includes(params.method);
const body: FormData | string | undefined = (() => {
if (!isBodyAllowed) {
return;
}
if (isFormData) {
return _body;
}
return JSON.stringify({ ..._body, _csrf_token: token });
})();
const reqParams = { const reqParams = {
...params, ...params,
body: hasBody ? JSON.stringify({ ...params.body, _csrf_token: token }) : undefined, body,
headers: { headers: {
...(hasBody ? { 'Content-type': 'application/json' } : undefined), ...(isBodyAllowed && !isFormData ? { 'Content-type': 'application/json' } : undefined),
...params?.headers, ...params?.headers,
}, },
}; };
......
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import link from 'lib/link/link'; import appConfig from 'configs/app/config';
export default function useLoginUrl() { export default function useLoginUrl() {
const router = useRouter(); const router = useRouter();
return link('auth', {}, { path: router.asPath }); return appConfig.authUrl + route({ pathname: '/auth/auth0', query: { path: router.asPath } });
} }
import { useRouter } from 'next/router';
import type { Route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -14,40 +16,76 @@ import tokensIcon from 'icons/token.svg'; ...@@ -14,40 +16,76 @@ import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg'; import transactionsIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import watchlistIcon from 'icons/watchlist.svg'; import watchlistIcon from 'icons/watchlist.svg';
import link from 'lib/link/link';
import useCurrentRoute from 'lib/link/useCurrentRoute';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
export default function useNavItems() { export interface NavItem {
text: string;
nextRoute: Route;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
isActive?: boolean;
isNewUi?: boolean;
}
interface ReturnType {
mainNavItems: Array<NavItem>;
accountNavItems: Array<NavItem>;
profileItem: NavItem;
}
export default function useNavItems(): ReturnType {
const isMarketplaceFilled = appConfig.marketplaceAppList.length > 0; const isMarketplaceFilled = appConfig.marketplaceAppList.length > 0;
const currentRoute = useCurrentRoute()(); const router = useRouter();
const pathname = router.pathname;
return React.useMemo(() => { return React.useMemo(() => {
const mainNavItems = [ const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: true }, { text: 'Blocks', nextRoute: { pathname: '/blocks' as const }, icon: blocksIcon, isActive: pathname.startsWith('/block'), isNewUi: true },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: true }, { text: 'Transactions', nextRoute: { pathname: '/txs' as const }, icon: transactionsIcon, isActive: pathname.startsWith('/tx'), isNewUi: true },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute.startsWith('token'), isNewUi: true }, { text: 'Tokens', nextRoute: { pathname: '/tokens' as const }, icon: tokensIcon, isActive: pathname.startsWith('/token'), isNewUi: true },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: true }, { text: 'Accounts', nextRoute: { pathname: '/accounts' as const }, icon: walletIcon, isActive: pathname === '/accounts', isNewUi: true },
isMarketplaceFilled ? isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute.startsWith('app'), isNewUi: true } : null, { text: 'Apps', nextRoute: { pathname: '/apps' as const }, icon: appsIcon, isActive: pathname.startsWith('/app'), isNewUi: true } : null,
{ text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: true }, { text: 'Charts & stats', nextRoute: { pathname: '/stats' as const }, icon: statsIcon, isActive: pathname === '/stats', isNewUi: true },
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other' // there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
// examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/ // examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/
// at this stage custom menu items is under development, we will implement it later // at this stage custom menu items is under development, we will implement it later
// { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' }, // { text: 'Other', url: link('other'), icon: gearIcon, isActive: pathname === 'other' },
].filter(notEmpty); ].filter(notEmpty);
const accountNavItems = [ const accountNavItems = [
{ text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist', isNewUi: true }, {
{ text: 'Private tags', url: link('private_tags'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags'), isNewUi: true }, text: 'Watchlist',
{ text: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags', isNewUi: true }, nextRoute: { pathname: '/account/watchlist' as const },
{ text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys', isNewUi: true }, icon: watchlistIcon,
{ text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi', isNewUi: true }, isActive: pathname === '/account/watchlist',
isNewUi: true,
},
{
text: 'Private tags',
nextRoute: { pathname: '/account/tag_address' as const },
icon: privateTagIcon,
isActive: pathname === '/account/tag_address',
isNewUi: true,
},
{
text: 'Public tags',
nextRoute: { pathname: '/account/public_tags_request' as const },
icon: publicTagIcon, isActive: pathname === '/account/public_tags_request', isNewUi: true,
},
{ text: 'API keys', nextRoute: { pathname: '/account/api_key' as const }, icon: apiKeysIcon, isActive: pathname === '/account/api_key', isNewUi: true },
{
text: 'Custom ABI',
nextRoute: { pathname: '/account/custom_abi' as const },
icon: abiIcon,
isActive: pathname === '/account/custom_abi',
isNewUi: true,
},
]; ];
const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile', isNewUi: true }; const profileItem = {
text: 'My profile', nextRoute: { pathname: '/auth/profile' as const }, icon: profileIcon, isActive: pathname === '/auth/profile', isNewUi: true };
return { mainNavItems, accountNavItems, profileItem }; return { mainNavItems, accountNavItems, profileItem };
}, [ isMarketplaceFilled, currentRoute ]); }, [ isMarketplaceFilled, pathname ]);
} }
...@@ -3,14 +3,13 @@ import { useRouter } from 'next/router'; ...@@ -3,14 +3,13 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useGradualIncrement from 'lib/hooks/useGradualIncrement'; import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import { ROUTES } from 'lib/link/routes';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
function getSocketParams(router: NextRouter) { function getSocketParams(router: NextRouter) {
if ( if (
router.pathname === ROUTES.txs.pattern && router.pathname === '/txs' &&
(router.query.tab === 'validated' || router.query.tab === undefined) && (router.query.tab === 'validated' || router.query.tab === undefined) &&
!router.query.block_number && !router.query.block_number &&
!router.query.page !router.query.page
...@@ -18,12 +17,12 @@ function getSocketParams(router: NextRouter) { ...@@ -18,12 +17,12 @@ function getSocketParams(router: NextRouter) {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const }; return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
} }
if (router.pathname === ROUTES.network_index.pattern) { if (router.pathname === '/') {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const }; return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
} }
if ( if (
router.pathname === ROUTES.txs.pattern && router.pathname === '/txs' &&
router.query.tab === 'pending' && router.query.tab === 'pending' &&
!router.query.block_number && !router.query.block_number &&
!router.query.page !router.query.page
......
export const ACCOUNT_ROUTES: Array<RouteName> = [ 'watchlist', 'private_tags', 'public_tags', 'api_keys', 'custom_abi' ];
import type { RouteName } from 'lib/link/routes';
export default function isAccountRoute(route: RouteName) {
return ACCOUNT_ROUTES.includes(route);
}
import link from './link';
it('makes correct link if there are no params in path', () => {
const result = link('api_keys');
expect(result).toBe('https://blockscout.com/account/api_key');
});
it('makes correct link if there are params in path', () => {
const result = link('token_instance_item', { id: '42', hash: '0x67e90a54AeEA85f21949c645082FE95d77BC1E70' });
expect(result).toBe('https://blockscout.com/token/0x67e90a54AeEA85f21949c645082FE95d77BC1E70/instance/42');
});
it('makes correct link with query params', () => {
const result = link('tx', { id: '0x4eb3b3b35d4c4757629bee32fc7a28b5dece693af8e7a383cf4cd6debe97ecf2' }, { tab: 'index', foo: 'bar' });
expect(result).toBe('https://blockscout.com/tx/0x4eb3b3b35d4c4757629bee32fc7a28b5dece693af8e7a383cf4cd6debe97ecf2?tab=index&foo=bar');
});
import appConfig from 'configs/app/config';
import { ROUTES } from './routes';
import type { RouteName } from './routes';
const PATH_PARAM_REGEXP = /\/:(\w+)/g;
export default function link(
routeName: RouteName,
urlParams?: Record<string, Array<string> | string | undefined>,
queryParams?: Record<string, string>,
): string {
const route = ROUTES[routeName];
if (!route) {
return '';
}
const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
let paramValue = urlParams?.[paramName];
if (Array.isArray(paramValue)) {
// FIXME we don't have yet params as array, but typescript says that we could
// dunno know how to manage it, fix me if you find an issue
paramValue = paramValue.join(',');
}
return paramValue ? `/${ paramValue }` : '';
});
const baseUrl = routeName === 'auth' ? appConfig.authUrl : appConfig.baseUrl;
const url = new URL(path, baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
url.searchParams.append(key, value);
});
return url.toString();
}
{
"network_index": "/",
"watchlist": "/account/watchlist",
"private_tags": "/account/tag_address",
"public_tags": "/account/public_tags_request",
"api_keys": "/account/api_key",
"custom_abi": "/account/custom_abi",
"profile": "/auth/profile",
"txs": "/txs",
"tx": "/tx/:id",
"blocks": "/blocks",
"block": "/block/:id",
"tokens": "/tokens",
"token_index": "/token/:hash",
"token_instance_item": "/token/:hash/instance/:id",
"address_index": "/address/:id",
"address_contract_verification": "/address/:id/contract_verification",
"accounts": "/accounts",
"apps": "/apps",
"app_index": "/apps/:id",
"search_results": "/search-results",
"auth": "/auth/auth0",
"stats": "/stats",
"visualize_sol2uml": "/visualize/sol2uml",
"csv_export": "/csv-export"
}
import appConfig from 'configs/app/config';
import PATHS from './paths.json';
export interface Route {
pattern: string;
crossNetworkNavigation?: boolean; // route will not change when switching networks
}
export type RouteName = keyof typeof ROUTES;
export const ROUTES = {
// NETWORK MAIN PAGE
network_index: {
pattern: PATHS.network_index,
crossNetworkNavigation: true,
},
// ACCOUNT
watchlist: {
pattern: PATHS.watchlist,
},
private_tags: {
pattern: PATHS.private_tags,
},
public_tags: {
pattern: PATHS.public_tags,
},
api_keys: {
pattern: PATHS.api_keys,
},
custom_abi: {
pattern: PATHS.custom_abi,
},
profile: {
pattern: PATHS.profile,
},
// TRANSACTIONS
txs: {
pattern: PATHS.txs,
crossNetworkNavigation: true,
},
tx: {
pattern: PATHS.tx,
},
// BLOCKS
blocks: {
pattern: PATHS.blocks,
crossNetworkNavigation: true,
},
block: {
pattern: PATHS.block,
},
// TOKENS
tokens: {
pattern: PATHS.tokens,
crossNetworkNavigation: true,
},
token_index: {
pattern: PATHS.token_index,
crossNetworkNavigation: true,
},
token_instance_item: {
pattern: PATHS.token_instance_item,
},
// ADDRESSES
address_index: {
pattern: PATHS.address_index,
crossNetworkNavigation: true,
},
address_contract_verification: {
pattern: PATHS.address_contract_verification,
crossNetworkNavigation: true,
},
// ACCOUNTS
accounts: {
pattern: PATHS.accounts,
crossNetworkNavigation: true,
},
// APPS
apps: {
pattern: PATHS.apps,
},
app_index: {
pattern: PATHS.app_index,
},
stats: {
pattern: PATHS.stats,
},
// SEARCH
search_results: {
pattern: PATHS.search_results,
},
// VISUALIZE
visualize_sol2uml: {
pattern: PATHS.visualize_sol2uml,
},
csv_export: {
pattern: PATHS.csv_export,
},
// AUTH
auth: {
pattern: PATHS.auth,
},
};
// !!! for development purpose only !!!
// don't wanna strict ROUTES to type "Record<string, Route>"
// otherwise we lose benefit of using "keyof typeof ROUTES" for possible route names (it will be any string then)
// but we still want typescript to tell us if routes follow its interface
// so we do this simple type-checking here
//
// another option is to create common enum with all possible route names and use it across the project
// but it is a little bit overwhelming as it seems right now
function checkRoutes(route: Record<string, Route>) {
return route;
}
if (appConfig.isDev) {
checkRoutes(ROUTES);
}
import { useRouter } from 'next/router';
import React from 'react';
import type { RouteName } from 'lib/link/routes';
import { ROUTES } from 'lib/link/routes';
const PATH_PARAM_REGEXP = /\/:(\w+)/g;
export default function useCurrentRoute() {
const { route: nextRoute } = useRouter();
return React.useCallback((): RouteName => {
for (const routeName in ROUTES) {
const route = ROUTES[routeName as RouteName];
const formattedRoute = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
return `/[${ paramName }]`;
});
if (formattedRoute === nextRoute) {
return routeName as RouteName;
}
}
return 'network_index';
}, [ nextRoute ]);
}
import type { PageParams } from './types'; import type { RoutedQuery } from 'nextjs-routes';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params: PageParams) { export default function getSeo(params: RoutedQuery<'/address/[hash]'>) {
const networkTitle = getNetworkTitle(); const networkTitle = getNetworkTitle();
return { return {
title: params ? `${ params.id } - ${ networkTitle }` : '', title: params ? `${ params.hash } - ${ networkTitle }` : '',
description: params ? description: params ?
`View the account balance, transactions, and other data for ${ params.id } on the ${ networkTitle }` : `View the account balance, transactions, and other data for ${ params.hash } on the ${ networkTitle }` :
'', '',
}; };
} }
export type PageParams = {
id: string;
}
import type { PageParams } from './types'; import type { RoutedQuery } from 'nextjs-routes';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) { export default function getSeo(params?: RoutedQuery<'/block/[height]'>) {
const networkTitle = getNetworkTitle(); const networkTitle = getNetworkTitle();
return { return {
title: params ? `Block ${ params.id } - ${ networkTitle }` : '', title: params ? `Block ${ params.height } - ${ networkTitle }` : '',
description: params ? `View the transactions, token transfers, and uncles for block number ${ params.id }` : '', description: params ? `View the transactions, token transfers, and uncles for block number ${ params.height }` : '',
}; };
} }
export type PageParams = {
id: string;
}
...@@ -4,6 +4,8 @@ export type Props = { ...@@ -4,6 +4,8 @@ export type Props = {
cookies: string; cookies: string;
referrer: string; referrer: string;
id?: string; id?: string;
height?: string;
hash?: string;
} }
export const getServerSideProps: GetServerSideProps<Props> = async({ req, query }) => { export const getServerSideProps: GetServerSideProps<Props> = async({ req, query }) => {
...@@ -12,6 +14,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async({ req, query ...@@ -12,6 +14,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async({ req, query
cookies: req.headers.cookie || '', cookies: req.headers.cookie || '',
referrer: req.headers.referer || '', referrer: req.headers.referer || '',
id: query.id?.toString() || '', id: query.id?.toString() || '',
height: query.height?.toString() || '',
hash: query.hash?.toString() || '',
}, },
}; };
}; };
import type { PageParams } from './types'; import type { RoutedQuery } from 'nextjs-routes';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) { export default function getSeo(params?: RoutedQuery<'/tx/[hash]'>) {
const networkTitle = getNetworkTitle(); const networkTitle = getNetworkTitle();
return { return {
title: params ? `Transaction ${ params.id } - ${ networkTitle }` : '', title: params ? `Transaction ${ params.hash } - ${ networkTitle }` : '',
description: params ? `View transaction ${ params.id } on ${ networkTitle }` : '', description: params ? `View transaction ${ params.hash } on ${ networkTitle }` : '',
}; };
} }
export type PageParams = {
id: string;
}
export default function getQueryParamString(param: string | Array<string> | undefined): string {
if (Array.isArray(param)) {
return param.join(',');
}
return param || '';
}
// https://hexdocs.pm/phoenix/js/
import type { SocketConnectOption } from 'phoenix'; import type { SocketConnectOption } from 'phoenix';
import { Socket } from 'phoenix'; import { Socket } from 'phoenix';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
......
...@@ -2,6 +2,7 @@ import type { Channel } from 'phoenix'; ...@@ -2,6 +2,7 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -19,6 +20,7 @@ SocketMessage.AddressTxs | ...@@ -19,6 +20,7 @@ SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending | SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer | SocketMessage.AddressTokenTransfer |
SocketMessage.TokenTransfers | SocketMessage.TokenTransfers |
SocketMessage.ContractVerification |
SocketMessage.Unknown; SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
...@@ -43,6 +45,7 @@ export namespace SocketMessage { ...@@ -43,6 +45,7 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }> export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -60,7 +60,11 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on ...@@ -60,7 +60,11 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
} else { } else {
ch = socket.channel(topic); ch = socket.channel(topic);
CHANNEL_REGISTRY[topic] = ch; CHANNEL_REGISTRY[topic] = ch;
ch.join().receive('ok', (message) => onJoinRef.current?.(ch, message)); ch.join()
.receive('ok', (message) => onJoinRef.current?.(ch, message))
.receive('error', () => {
onSocketError?.();
});
} }
setChannel(ch); setChannel(ch);
...@@ -70,7 +74,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on ...@@ -70,7 +74,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
delete CHANNEL_REGISTRY[topic]; delete CHANNEL_REGISTRY[topic];
setChannel(undefined); setChannel(undefined);
}; };
}, [ socket, topic, params, isDisabled ]); }, [ socket, topic, params, isDisabled, onSocketError ]);
return channel; return channel;
} }
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [ const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' }, { title: 'ERC-20', id: 'ERC-20' },
......
import type { NextRequest } from 'next/server'; import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { route } from 'nextjs-routes';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { NAMES } from 'lib/cookies'; import { NAMES } from 'lib/cookies';
import getCspPolicy from 'lib/csp/getCspPolicy'; import getCspPolicy from 'lib/csp/getCspPolicy';
import link from 'lib/link/link';
const cspPolicy = getCspPolicy(); const cspPolicy = getCspPolicy();
...@@ -22,7 +22,7 @@ export function middleware(req: NextRequest) { ...@@ -22,7 +22,7 @@ export function middleware(req: NextRequest) {
const apiToken = req.cookies.get(NAMES.API_TOKEN); const apiToken = req.cookies.get(NAMES.API_TOKEN);
if ((isAccountRoute || isProfileRoute) && !apiToken && appConfig.isAccountSupported) { if ((isAccountRoute || isProfileRoute) && !apiToken && appConfig.isAccountSupported) {
const authUrl = link('auth', undefined, { path: req.nextUrl.pathname }); const authUrl = appConfig.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } });
return NextResponse.redirect(authUrl); return NextResponse.redirect(authUrl);
} }
......
...@@ -80,20 +80,26 @@ export const erc1155LongId: AddressTokenBalance = { ...@@ -80,20 +80,26 @@ export const erc1155LongId: AddressTokenBalance = {
value: '42', value: '42',
}; };
export const baseList = [ export const erc20List = {
items: [
erc20a, erc20a,
erc20b, erc20b,
erc20c, erc20c,
],
};
export const erc721List = {
items: [
erc721a, erc721a,
erc721b, erc721b,
erc721c, erc721c,
],
};
export const erc1155List = {
items: [
erc1155withoutName, erc1155withoutName,
erc1155a, erc1155a,
erc1155b, erc1155b,
]; ],
};
export const longValuesList = [
erc20LongSymbol,
erc721LongSymbol,
erc1155LongId,
];
import type { TokenHolders } from 'types/api/tokenInfo'; import type { TokenHolders } from 'types/api/token';
import { withName, withoutName } from 'mocks/address/address'; import { withName, withoutName } from 'mocks/address/address';
......
import type { TokenCounters, TokenInfo } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo } from 'types/api/token';
export const tokenInfo: TokenInfo = { export const tokenInfo: TokenInfo = {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
......
import type { TokenInstance } from 'types/api/tokens'; import type { TokenInstance } from 'types/api/token';
import * as addressMock from '../address/address'; import * as addressMock from '../address/address';
import { tokenInfoERC721a } from './tokenInfo'; import { tokenInfoERC721a } from './tokenInfo';
......
...@@ -64,6 +64,7 @@ export const base: Transaction = { ...@@ -64,6 +64,7 @@ export const base: Transaction = {
], ],
type: 2, type: 2,
value: '42000000000000000000', value: '42000000000000000000',
actions: [],
}; };
export const withContractCreation: Transaction = { export const withContractCreation: Transaction = {
...@@ -170,3 +171,72 @@ export const pending: Transaction = { ...@@ -170,3 +171,72 @@ export const pending: Transaction = {
type: null, type: null,
value: '0', value: '0',
}; };
export const withActionsUniswap: Transaction = {
...base,
actions: [
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '7143.488560357232097378',
amount1: '10',
symbol0: 'XFT',
symbol1: 'Ether',
},
protocol: 'uniswap_v3',
type: 'mint',
},
{
data: {
address: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88',
ids: [
'53699',
'53700123456',
'42',
],
name: 'Uniswap V3: Positions NFT',
symbol: 'UNI-V3-POS',
to: '0x6d872Fb5F5B2B1f71fA9AadE159bc3976c1946B7',
},
protocol: 'uniswap_v3',
type: 'mint_nft',
},
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '42876.488560357232',
amount1: '345.908098203434',
symbol0: 'SHAVUHA',
symbol1: 'BOB',
},
protocol: 'uniswap_v3',
type: 'swap',
},
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '42',
amount1: '0.523523223232',
symbol0: 'VIC',
symbol1: 'USDT',
},
protocol: 'uniswap_v3',
type: 'burn',
},
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '42',
amount1: '0.523523223232',
symbol0: 'BOB',
symbol1: 'UNI',
},
protocol: 'uniswap_v3',
type: 'collect',
},
],
};
const withRoutes = require('nextjs-routes/config')({
outDir: 'types',
});
const path = require('path'); const path = require('path');
const headers = require('./configs/nextjs/headers'); const headers = require('./configs/nextjs/headers');
...@@ -33,4 +36,4 @@ const moduleExports = { ...@@ -33,4 +36,4 @@ const moduleExports = {
output: 'standalone', output: 'standalone',
}; };
module.exports = moduleExports; module.exports = withRoutes(moduleExports);
...@@ -35,10 +35,13 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -35,10 +35,13 @@ function MyApp({ Component, pageProps }: AppProps) {
}, },
})); }));
const renderErrorScreen = React.useCallback(() => { const renderErrorScreen = React.useCallback((error?: Error) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusCode = (error?.cause as any)?.status || 500;
return ( return (
<AppError <AppError
statusCode={ 500 } statusCode={ statusCode }
height="100vh" height="100vh"
display="flex" display="flex"
flexDirection="column" flexDirection="column"
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/address/types';
import getSeo from 'lib/next/address/getSeo'; import getSeo from 'lib/next/address/getSeo';
import ContractVerification from 'ui/pages/ContractVerification'; import ContractVerification from 'ui/pages/ContractVerification';
const ContractVerificationPage: NextPage<PageParams> = ({ id }: PageParams) => { const ContractVerificationPage: NextPage<RoutedQuery<'/address/[hash]/contract_verification'>> =
const { title, description } = getSeo({ id }); ({ hash }: RoutedQuery<'/address/[hash]/contract_verification'>) => {
const { title, description } = getSeo({ hash });
return ( return (
<> <>
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/address/types';
import getSeo from 'lib/next/address/getSeo'; import getSeo from 'lib/next/address/getSeo';
import Address from 'ui/pages/Address'; import Address from 'ui/pages/Address';
const AddressPage: NextPage<PageParams> = ({ id }: PageParams) => { const AddressPage: NextPage<RoutedQuery<'/address/[hash]'>> = ({ hash }: RoutedQuery<'/address/[hash]'>) => {
const { title, description } = getSeo({ id }); const { title, description } = getSeo({ hash });
return ( return (
<> <>
......
...@@ -16,7 +16,7 @@ const AppPage: NextPage = () => { ...@@ -16,7 +16,7 @@ const AppPage: NextPage = () => {
const [ isLoading, setIsLoading ] = useState(true); const [ isLoading, setIsLoading ] = useState(true);
const [ app, setApp ] = useState<AppItemOverview | undefined>(undefined); const [ app, setApp ] = useState<AppItemOverview | undefined>(undefined);
const { id }: { id?: string } = router.query; const id = router.query.id;
useEffect(() => { useEffect(() => {
if (!id) { if (!id) {
......
import type { NextPage } from 'next';
const Auth0Page: NextPage = () => {
return null;
};
export default Auth0Page;
export async function getServerSideProps() {
return {
notFound: true,
};
}
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/block/types';
import getSeo from 'lib/next/block/getSeo'; import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block'; import Block from 'ui/pages/Block';
const BlockPage: NextPage<PageParams> = ({ id }: PageParams) => { const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => {
const { title, description } = getSeo({ id }); const { title, description } = getSeo({ height });
return ( return (
<> <>
<Head> <Head>
......
import type { NextPage } from 'next';
const CsvExportPage: NextPage = () => {
return null;
};
export default CsvExportPage;
export async function getServerSideProps() {
return {
notFound: true,
};
}
import type { GetServerSideProps, NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SearchRedirectResult } from 'types/api/search'; import type { SearchRedirectResult } from 'types/api/search';
import buildUrlNode from 'lib/api/buildUrlNode'; import buildUrlNode from 'lib/api/buildUrlNode';
import fetchFactory from 'lib/api/nodeFetch'; import fetchFactory from 'lib/api/nodeFetch';
import link from 'lib/link/link';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props } from 'lib/next/getServerSideProps'; import type { Props } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps'; import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
...@@ -43,13 +43,13 @@ export const getServerSideProps: GetServerSideProps<Props> = async({ req, res, r ...@@ -43,13 +43,13 @@ export const getServerSideProps: GetServerSideProps<Props> = async({ req, res, r
const redirectUrl = (() => { const redirectUrl = (() => {
switch (payload.type) { switch (payload.type) {
case 'block': { case 'block': {
return link('block', { id: q }); return route({ pathname: '/block/[height]', query: { height: q } });
} }
case 'address': { case 'address': {
return link('address_index', { id: payload.parameter || q }); return route({ pathname: '/address/[hash]', query: { hash: payload.parameter || q } });
} }
case 'transaction': { case 'transaction': {
return link('tx', { id: q }); return route({ pathname: '/tx/[hash]', query: { hash: q } });
} }
} }
})(); })();
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import getSeo from 'lib/next/tx/getSeo'; import getSeo from 'lib/next/tx/getSeo';
import Transaction from 'ui/pages/Transaction'; import Transaction from 'ui/pages/Transaction';
const TransactionPage: NextPage<PageParams> = ({ id }: PageParams) => { const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQuery<'/tx/[hash]'>) => {
const { title, description } = getSeo({ id }); const { title, description } = getSeo({ hash });
return ( return (
<> <>
......
import './fonts.css'; import './fonts.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks'; import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import _defaultsDeep from 'lodash/defaultsDeep';
import MockDate from 'mockdate'; import MockDate from 'mockdate';
import * as router from 'next/router'; import * as router from 'next/router';
const NEXT_ROUTER_MOCK = { const NEXT_ROUTER_MOCK = {
query: {}, query: {},
pathname: '',
}; };
beforeMount(async({ hooksConfig }) => { beforeMount(async({ hooksConfig }) => {
// Before mount, redefine useRouter to return mock value from test. // Before mount, redefine useRouter to return mock value from test.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: I really want to redefine this property :) // @ts-ignore: I really want to redefine this property :)
router.useRouter = () => hooksConfig?.router || NEXT_ROUTER_MOCK; router.useRouter = () => _defaultsDeep(hooksConfig?.router, NEXT_ROUTER_MOCK);
// set current date // set current date
MockDate.set('2022-11-11T12:00:00Z'); MockDate.set('2022-11-11T12:00:00Z');
......
import { compile } from 'path-to-regexp'; import { compile } from 'path-to-regexp';
import type { ResourceName } from 'lib/api/resources'; import type { ResourceName, ResourcePathParams } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources'; import { RESOURCES } from 'lib/api/resources';
export default function buildApiUrl(resourceName: ResourceName, pathParams?: Record<string, string>) { export default function buildApiUrl<R extends ResourceName>(resourceName: R, pathParams?: ResourcePathParams<R>) {
const resource = RESOURCES[resourceName]; const resource = RESOURCES[resourceName];
return compile('/node-api/proxy/poa/core' + resource.path)(pathParams); return compile('/node-api/proxy/poa/core' + resource.path)(pathParams);
} }
import { formAnatomy as parts } from '@chakra-ui/anatomy'; import { formAnatomy as parts } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system'; import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { getColor } from '@chakra-ui/theme-tools'; import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors'; import getDefaultFormColors from '../utils/getDefaultFormColors';
import FancySelect from './FancySelect'; import FancySelect from './FancySelect';
...@@ -81,11 +81,18 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction ...@@ -81,11 +81,18 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles, 'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles,
[` [`
input[disabled] + label, input[disabled] + label,
textarea[disabled] + label,
&[aria-disabled=true] label &[aria-disabled=true] label
`]: { `]: {
backgroundColor: 'transparent', backgroundColor: 'transparent',
}, },
// in textarea bg of label could not be transparent; it should match the background color of input but without alpha
// so we have to use non-standard colors here
'textarea[disabled] + label': {
backgroundColor: mode('#ececec', '#232425')(props),
},
'textarea[disabled] + label[data-in-modal=true]': {
backgroundColor: mode('#ececec', '#292b34')(props),
},
// indicator styles // indicator styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': { 'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': {
......
...@@ -22,7 +22,7 @@ const baseStyleDialog = defineStyle((props) => { ...@@ -22,7 +22,7 @@ const baseStyleDialog = defineStyle((props) => {
const baseStyleDialogContainer = defineStyle({ const baseStyleDialogContainer = defineStyle({
'::-webkit-scrollbar': { display: 'none' }, '::-webkit-scrollbar': { display: 'none' },
'scrollbar-width': 'none', 'scrollbar-width': 'none',
'@supports (height: -webkit-fill-available)': { height: '-webkit-fill-available' }, // '@supports (height: -webkit-fill-available)': { height: '-webkit-fill-available' },
}); });
const baseStyleHeader = defineStyle((props) => ({ const baseStyleHeader = defineStyle((props) => ({
......
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es6",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
......
...@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction';
import type { AddressTag, WatchlistName } from './addressParams'; import type { AddressTag, WatchlistName } from './addressParams';
import type { Block } from './block'; import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { TokenInfo, TokenType } from './tokenInfo'; import type { TokenInfo, TokenType } from './token';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer'; import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
export interface Address { export interface Address {
......
...@@ -113,3 +113,34 @@ export interface SmartContractQueryMethodReadError { ...@@ -113,3 +113,34 @@ export interface SmartContractQueryMethodReadError {
} }
export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError; export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError;
// VERIFICATION
export type SmartContractVerificationMethod = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part' | 'vyper-code' | 'vyper-multi-part';
export interface SmartContractVerificationConfigRaw {
solidity_compiler_versions: Array<string>;
solidity_evm_versions: Array<string>;
verification_options: Array<string>;
vyper_compiler_versions: Array<string>;
vyper_evm_versions: Array<string>;
}
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
verification_options: Array<SmartContractVerificationMethod>;
}
export type SmartContractVerificationResponse = {
status: 'error';
errors: SmartContractVerificationError;
} | {
status: 'success';
}
export interface SmartContractVerificationError {
contract_source_code?: Array<string>;
files?: Array<string>;
compiler_version?: Array<string>;
constructor_arguments?: Array<string>;
name?: Array<string>;
}
...@@ -34,3 +34,28 @@ export type TokenHoldersPagination = { ...@@ -34,3 +34,28 @@ export type TokenHoldersPagination = {
items_count: number; items_count: number;
value: string; value: string;
} }
export interface TokenInstance {
is_unique: boolean;
id: string;
holder_address_hash: string | null;
image_url: string | null;
animation_url: string | null;
external_app_url: string | null;
metadata: unknown;
owner: AddressParam;
token: TokenInfo;
}
export interface TokenInstanceTransfersCount {
transfers_count: number;
}
export interface TokenInventoryResponse {
items: Array<TokenInstance>;
next_page_params: TokenInventoryPagination;
}
export type TokenInventoryPagination = {
unique_token: number;
}
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric, TokenType } from './tokenInfo'; import type { TokenInfoGeneric, TokenType } from './token';
export type Erc20TotalPayload = { export type Erc20TotalPayload = {
decimals: string | null; decimals: string | null;
......
import type { AddressParam } from './addressParams'; import type { TokenInfo, TokenType } from './token';
import type { TokenInfo, TokenType } from './tokenInfo';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
export type TokensResponse = { export type TokensResponse = {
...@@ -13,22 +12,6 @@ export type TokensResponse = { ...@@ -13,22 +12,6 @@ export type TokensResponse = {
export type TokensFilters = { filter: string; type: Array<TokenType> | undefined }; export type TokensFilters = { filter: string; type: Array<TokenType> | undefined };
export interface TokenInstance {
is_unique: boolean;
id: string;
holder_address_hash: string | null;
image_url: string | null;
animation_url: string | null;
external_app_url: string | null;
metadata: unknown;
owner: AddressParam;
token: TokenInfo;
}
export interface TokenInstanceTransfersCount {
transfers_count: number;
}
export interface TokenInstanceTransferResponse { export interface TokenInstanceTransferResponse {
items: Array<TokenTransfer>; items: Array<TokenTransfer>;
next_page_params: TokenInstanceTransferPagination | null; next_page_params: TokenInstanceTransferPagination | null;
......
...@@ -3,6 +3,7 @@ import type { BlockTransactionsResponse } from './block'; ...@@ -3,6 +3,7 @@ import type { BlockTransactionsResponse } from './block';
import type { DecodedInput } from './decodedInput'; import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee'; import type { Fee } from './fee';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
import type { TxAction } from './txAction';
export type TransactionRevertReason = { export type TransactionRevertReason = {
raw: string; raw: string;
...@@ -48,6 +49,7 @@ export type Transaction = ( ...@@ -48,6 +49,7 @@ export type Transaction = (
method: string | null; method: string | null;
tx_types: Array<TransactionType>; tx_types: Array<TransactionType>;
tx_tag: string | null; tx_tag: string | null;
actions: Array<TxAction>;
} }
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
......
export interface TxActionGeneral {
type: 'mint' | 'burn' | 'collect' | 'swap';
data: {
amount0: string;
symbol0: string;
address0: string;
amount1: string;
symbol1: string;
address1: string;
};
}
export interface TxActionNft {
type: 'mint_nft';
data: {
name: string;
symbol: string;
address: string;
to: string;
ids: Array<string>;
};
}
export type TxAction = {
protocol: 'uniswap_v3';
} & (TxActionGeneral | TxActionNft)
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
// This file will be automatically regenerated when your Next.js server is running.
// nextjs-routes version: 1.0.8
/* eslint-disable */
// prettier-ignore
declare module "nextjs-routes" {
export type Route =
| StaticRoute<"/account/api_key">
| StaticRoute<"/account/custom_abi">
| StaticRoute<"/account/public_tags_request">
| StaticRoute<"/account/tag_address">
| StaticRoute<"/account/watchlist">
| StaticRoute<"/accounts">
| DynamicRoute<"/address/[hash]/contract_verification", { "hash": string }>
| DynamicRoute<"/address/[hash]", { "hash": string }>
| StaticRoute<"/api/csrf">
| StaticRoute<"/api/proxy">
| DynamicRoute<"/apps/[id]", { "id": string }>
| StaticRoute<"/apps">
| StaticRoute<"/auth/auth0">
| StaticRoute<"/auth/profile">
| DynamicRoute<"/block/[height]", { "height": string }>
| StaticRoute<"/blocks">
| StaticRoute<"/csv-export">
| StaticRoute<"/graph">
| StaticRoute<"/">
| StaticRoute<"/login">
| StaticRoute<"/search-results">
| StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }>
| DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }>
| StaticRoute<"/tokens">
| DynamicRoute<"/tx/[hash]", { "hash": string }>
| StaticRoute<"/txs">
| StaticRoute<"/visualize/sol2uml">;
interface StaticRoute<Pathname> {
pathname: Pathname;
query?: Query | undefined;
hash?: string | null | undefined;
}
interface DynamicRoute<Pathname, Parameters> {
pathname: Pathname;
query: Parameters & Query;
hash?: string | null | undefined;
}
interface Query {
[key: string]: string | string[] | undefined;
};
export type RoutedQuery<P extends Route["pathname"]> = Extract<
Route,
{ pathname: P }
>["query"];
export type Locale = undefined;
/**
* A typesafe utility function for generating paths in your application.
*
* route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar".
*/
export declare function route(r: Route): string;
}
// prettier-ignore
declare module "next/link" {
import type { Route } from "nextjs-routes";
import type { LinkProps as NextLinkProps } from "next/dist/client/link";
import type {
AnchorHTMLAttributes,
DetailedReactHTMLElement,
MouseEventHandler,
PropsWithChildren,
} from "react";
export * from "next/dist/client/link";
type Query = { query?: { [key: string]: string | string[] | undefined } };
type StaticRoute = Exclude<Route, { query: any }>["pathname"];
export interface LinkProps
extends Omit<NextLinkProps, "href" | "locale">,
AnchorHTMLAttributes<HTMLAnchorElement> {
href: Route | StaticRoute | Query;
locale?: false;
}
type LinkReactElement = DetailedReactHTMLElement<
{
onMouseEnter?: MouseEventHandler<Element> | undefined;
onClick: MouseEventHandler;
href?: string | undefined;
ref?: any;
},
HTMLElement
>;
declare function Link(props: PropsWithChildren<LinkProps>): LinkReactElement;
export default Link;
}
// prettier-ignore
declare module "next/router" {
import type { Locale, Route, RoutedQuery } from "nextjs-routes";
import type { NextRouter as Router } from "next/dist/client/router";
export * from "next/dist/client/router";
export { default } from "next/dist/client/router";
type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
type StaticRoute = Exclude<Route, { query: any }>["pathname"];
type Query = { query?: { [key: string]: string | string[] | undefined } };
interface TransitionOptions extends Omit<NextTransitionOptions, "locale"> {
locale?: false;
}
export type NextRouter<P extends Route["pathname"] = Route["pathname"]> =
Extract<Route, { pathname: P }> &
Omit<
Router,
| "push"
| "replace"
| "locale"
| "locales"
| "defaultLocale"
| "domainLocales"
> & {
defaultLocale?: undefined;
domainLocales?: undefined;
locale?: Locale;
locales?: undefined;
push(
url: Route | StaticRoute | Query,
as?: string,
options?: TransitionOptions
): Promise<boolean>;
replace(
url: Route | StaticRoute | Query,
as?: string,
options?: TransitionOptions
): Promise<boolean>;
route: P;
};
export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
}
...@@ -31,10 +31,10 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -31,10 +31,10 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const addressHash = String(router.query?.id); const addressHash = String(router.query.hash);
const query = useQueryWithPages({ const query = useQueryWithPages({
resourceName: 'address_blocks_validated', resourceName: 'address_blocks_validated',
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
scrollRef, scrollRef,
}); });
...@@ -46,7 +46,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -46,7 +46,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
setSocketAlert(false); setSocketAlert(false);
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_blocks_validated', { pathParams: { id: addressHash } }), getResourceKey('address_blocks_validated', { pathParams: { hash: addressHash } }),
(prevData: AddressBlocksValidatedResponse | undefined) => { (prevData: AddressBlocksValidatedResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
......
...@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressCoinBalance from './AddressCoinBalance'; import AddressCoinBalance from './AddressCoinBalance';
const addressHash = 'hash'; const addressHash = 'hash';
const BALANCE_HISTORY_API_URL = buildApiUrl('address_coin_balance', { id: addressHash }); const BALANCE_HISTORY_API_URL = buildApiUrl('address_coin_balance', { hash: addressHash });
const BALANCE_HISTORY_CHART_API_URL = buildApiUrl('address_coin_balance_chart', { id: addressHash }); const BALANCE_HISTORY_CHART_API_URL = buildApiUrl('address_coin_balance_chart', { hash: addressHash });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: addressHash }, query: { hash: addressHash },
}, },
}; };
......
...@@ -7,6 +7,7 @@ import type { AddressCoinBalanceHistoryResponse } from 'types/api/address'; ...@@ -7,6 +7,7 @@ import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import SocketAlert from 'ui/shared/SocketAlert'; import SocketAlert from 'ui/shared/SocketAlert';
...@@ -20,10 +21,10 @@ const AddressCoinBalance = () => { ...@@ -20,10 +21,10 @@ const AddressCoinBalance = () => {
const router = useRouter(); const router = useRouter();
const scrollRef = React.useRef<HTMLDivElement>(null); const scrollRef = React.useRef<HTMLDivElement>(null);
const addressHash = String(router.query?.id); const addressHash = getQueryParamString(router.query.hash);
const coinBalanceQuery = useQueryWithPages({ const coinBalanceQuery = useQueryWithPages({
resourceName: 'address_coin_balance', resourceName: 'address_coin_balance',
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
scrollRef, scrollRef,
}); });
...@@ -35,7 +36,7 @@ const AddressCoinBalance = () => { ...@@ -35,7 +36,7 @@ const AddressCoinBalance = () => {
setSocketAlert(false); setSocketAlert(false);
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_coin_balance', { pathParams: { id: addressHash } }), getResourceKey('address_coin_balance', { pathParams: { hash: addressHash } }),
(prevData: AddressCoinBalanceHistoryResponse | undefined) => { (prevData: AddressCoinBalanceHistoryResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
......
import { chakra, Icon, Link, Tooltip, Hide } from '@chakra-ui/react'; import { chakra, Icon, Link, Tooltip, Hide } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import svgFileIcon from 'icons/files/csv.svg'; import svgFileIcon from 'icons/files/csv.svg';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
interface Props { interface Props {
address: string; address: string;
...@@ -20,7 +20,9 @@ const AddressCsvExportLink = ({ className, address, type }: Props) => { ...@@ -20,7 +20,9 @@ const AddressCsvExportLink = ({ className, address, type }: Props) => {
className={ className } className={ className }
display="inline-flex" display="inline-flex"
alignItems="center" alignItems="center"
href={ link('csv_export', undefined, { type, address }) } whiteSpace="nowrap"
href={ route({ pathname: '/csv-export', query: { type, address } }) }
flexShrink={ 0 }
> >
<Icon as={ svgFileIcon } boxSize={{ base: '30px', lg: 6 }}/> <Icon as={ svgFileIcon } boxSize={{ base: '30px', lg: 6 }}/>
<Hide ssr={ false } below="lg"><chakra.span ml={ 1 }>Download CSV</chakra.span></Hide> <Hide ssr={ false } below="lg"><chakra.span ml={ 1 }>Download CSV</chakra.span></Hide>
......
...@@ -8,7 +8,7 @@ import type { Address } from 'types/api/address'; ...@@ -8,7 +8,7 @@ import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters'; import * as countersMock from 'mocks/address/counters';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokensMock from 'mocks/address/tokens';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -16,12 +16,14 @@ import AddressDetails from './AddressDetails'; ...@@ -16,12 +16,14 @@ import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage'; import MockAddressPage from './testUtils/MockAddressPage';
const ADDRESS_HASH = addressMock.hash; const ADDRESS_HASH = addressMock.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH }); const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_COUNTERS = buildApiUrl('address_counters', { id: ADDRESS_HASH }); const API_URL_COUNTERS = buildApiUrl('address_counters', { hash: ADDRESS_HASH });
const API_URL_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH }); const API_URL_TOKENS_ERC20 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-20';
const API_URL_TOKENS_ERC721 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-721';
const API_URL_TOKENS_ER1155 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-1155';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH }, query: { hash: ADDRESS_HASH },
}, },
}; };
...@@ -54,10 +56,18 @@ test('token', async({ mount, page }) => { ...@@ -54,10 +56,18 @@ test('token', async({ mount, page }) => {
status: 200, status: 200,
body: JSON.stringify(countersMock.forToken), body: JSON.stringify(countersMock.forToken),
})); }));
await page.route(API_URL_TOKEN_BALANCES, (route) => route.fulfill({ await page.route(API_URL_TOKENS_ERC20, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(tokenBalanceMock.baseList), body: JSON.stringify(tokensMock.erc20List),
})); }), { times: 1 });
await page.route(API_URL_TOKENS_ERC721, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc721List),
}), { times: 1 });
await page.route(API_URL_TOKENS_ER1155, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc1155List),
}), { times: 1 });
await page.evaluate(() => { await page.evaluate(() => {
window.ethereum = { } as MetaMaskInpageProvider; window.ethereum = { } as MetaMaskInpageProvider;
......
import { Box, Flex, Text, Icon, Grid } from '@chakra-ui/react'; import { Box, Flex, Text, Icon, Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Address as TAddress } from 'types/api/address'; import type { Address as TAddress } from 'types/api/address';
...@@ -9,7 +10,7 @@ import appConfig from 'configs/app/config'; ...@@ -9,7 +10,7 @@ import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link'; import getQueryParamString from 'lib/router/getQueryParamString';
import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
...@@ -32,12 +33,12 @@ interface Props { ...@@ -32,12 +33,12 @@ interface Props {
const AddressDetails = ({ addressQuery, scrollRef }: Props) => { const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter(); const router = useRouter();
const addressHash = router.query.id?.toString(); const addressHash = getQueryParamString(router.query.hash);
const countersQuery = useApiQuery('address_counters', { const countersQuery = useApiQuery('address_counters', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data), enabled: Boolean(addressHash) && Boolean(addressQuery.data),
}, },
}); });
...@@ -90,7 +91,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -90,7 +91,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap"> <Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
<Text fontSize="sm">Verify with other explorers</Text> <Text fontSize="sm">Verify with other explorers</Text>
{ explorers.map((explorer) => { { explorers.map((explorer) => {
const url = new URL(explorer.paths.address + '/' + router.query.id, explorer.baseUrl); const url = new URL(explorer.paths.address + '/' + addressHash, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>; return <LinkExternal key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>;
}) } }) }
</Flex> </Flex>
...@@ -118,7 +119,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -118,7 +119,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
hint="Implementation address of the proxy contract." hint="Implementation address of the proxy contract."
columnGap={ 1 } columnGap={ 1 }
> >
<LinkInternal href={ link('address_index', { id: data.implementation_address }) } overflow="hidden"> <LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: data.implementation_address } }) } overflow="hidden">
{ data.implementation_name || <HashStringShortenDynamic hash={ data.implementation_address }/> } { data.implementation_name || <HashStringShortenDynamic hash={ data.implementation_address }/> }
</LinkInternal> </LinkInternal>
{ data.implementation_name && ( { data.implementation_name && (
...@@ -183,7 +184,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -183,7 +184,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
py={{ base: '2px', lg: 1 }} py={{ base: '2px', lg: 1 }}
> >
<LinkInternal <LinkInternal
href={ link('block', { id: String(data.block_number_balance_updated_at) }) } href={ route({ pathname: '/block/[height]', query: { height: String(data.block_number_balance_updated_at) } }) }
display="flex" display="flex"
alignItems="center" alignItems="center"
> >
......
...@@ -9,10 +9,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -9,10 +9,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressInternalTxs from './AddressInternalTxs'; import AddressInternalTxs from './AddressInternalTxs';
const ADDRESS_HASH = internalTxsMock.base.from.hash; const ADDRESS_HASH = internalTxsMock.base.from.hash;
const API_URL_TX_INTERNALS = buildApiUrl('address_internal_txs', { id: ADDRESS_HASH }); const API_URL_TX_INTERNALS = buildApiUrl('address_internal_txs', { hash: ADDRESS_HASH });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH }, query: { hash: ADDRESS_HASH },
}, },
}; };
......
import { Text, Show, Hide } from '@chakra-ui/react'; import { Text, Show, Hide } from '@chakra-ui/react';
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,6 +8,7 @@ import { AddressFromToFilterValues } from 'types/api/address'; ...@@ -9,6 +8,7 @@ import { AddressFromToFilterValues } from 'types/api/address';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressIntTxsSkeletonDesktop from 'ui/address/internals/AddressIntTxsSkeletonDesktop'; import AddressIntTxsSkeletonDesktop from 'ui/address/internals/AddressIntTxsSkeletonDesktop';
import AddressIntTxsSkeletonMobile from 'ui/address/internals/AddressIntTxsSkeletonMobile'; import AddressIntTxsSkeletonMobile from 'ui/address/internals/AddressIntTxsSkeletonMobile';
import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable'; import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable';
...@@ -27,13 +27,11 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -27,13 +27,11 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
const router = useRouter(); const router = useRouter();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter)); const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const queryId = router.query.id; const hash = getQueryParamString(router.query.hash);
const queryIdArray = castArray(queryId);
const queryIdStr = queryIdArray[0];
const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_internal_txs', resourceName: 'address_internal_txs',
pathParams: { id: queryIdStr }, pathParams: { hash },
filters: { filter: filterValue }, filters: { filter: filterValue },
scrollRef, scrollRef,
}); });
...@@ -70,10 +68,10 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -70,10 +68,10 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
content = ( content = (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ queryIdStr }/> <AddressIntTxsList data={ data.items } currentAddress={ hash }/>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<AddressIntTxsTable data={ data.items } currentAddress={ queryIdStr }/> <AddressIntTxsTable data={ data.items } currentAddress={ hash }/>
</Hide> </Hide>
</> </>
); );
...@@ -87,7 +85,7 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -87,7 +85,7 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
onFilterChange={ handleFilterChange } onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) } isActive={ Boolean(filterValue) }
/> />
<AddressCsvExportLink address={ queryIdStr } type="internal-transactions" ml={{ base: 2, lg: 'auto' }}/> <AddressCsvExportLink address={ hash } type="internal-transactions" ml={{ base: 2, lg: 'auto' }}/>
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> } { isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar> </ActionBar>
{ content } { content }
......
...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; ...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import LogItem from 'ui/shared/logs/LogItem'; import LogItem from 'ui/shared/logs/LogItem';
...@@ -12,10 +13,10 @@ import Pagination from 'ui/shared/Pagination'; ...@@ -12,10 +13,10 @@ import Pagination from 'ui/shared/Pagination';
const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter(); const router = useRouter();
const addressHash = String(router.query?.id); const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_logs', resourceName: 'address_logs',
pathParams: { id: addressHash }, pathParams: { hash },
scrollRef, scrollRef,
}); });
......
...@@ -8,12 +8,12 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,12 +8,12 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokenTransfers from './AddressTokenTransfers'; import AddressTokenTransfers from './AddressTokenTransfers';
const API_URL = buildApiUrl('address_token_transfers', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) + const API_URL = buildApiUrl('address_token_transfers', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859'; '?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token_hash: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
}, },
}; };
......
...@@ -6,7 +6,7 @@ import React from 'react'; ...@@ -6,7 +6,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address'; import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import crossIcon from 'icons/cross.svg'; import crossIcon from 'icons/cross.svg';
...@@ -16,12 +16,14 @@ import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; ...@@ -16,12 +16,14 @@ import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import TOKEN_TYPE from 'lib/token/tokenTypes'; import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
...@@ -69,12 +71,12 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -69,12 +71,12 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const currentAddress = router.query.id?.toString(); const currentAddress = getQueryParamString(router.query.hash);
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0); const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const tokenFilter = router.query.token ? router.query.token.toString() : undefined; const tokenFilter = getQueryParamString(router.query.token_hash) || undefined;
const [ filters, setFilters ] = React.useState<Filters>( const [ filters, setFilters ] = React.useState<Filters>(
{ {
...@@ -85,7 +87,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -85,7 +87,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_token_transfers', resourceName: 'address_token_transfers',
pathParams: { id: currentAddress }, pathParams: { hash: currentAddress },
filters: tokenFilter ? { token: tokenFilter } : filters, filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef, scrollRef,
}); });
...@@ -117,7 +119,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -117,7 +119,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
} }
} else { } else {
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_token_transfers', { pathParams: { id: router.query.id?.toString() }, queryParams: { ...filters } }), getResourceKey('address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }),
(prevData: AddressTokenTransferResponse | undefined) => { (prevData: AddressTokenTransferResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
...@@ -147,7 +149,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -147,7 +149,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
}, []); }, []);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `addresses:${ (router.query.id as string).toLowerCase() }`, topic: `addresses:${ currentAddress.toLowerCase() }`,
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: pagination.page !== 1 || Boolean(tokenFilter), isDisabled: pagination.page !== 1 || Boolean(tokenFilter),
...@@ -160,7 +162,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -160,7 +162,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
}); });
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0); const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length; const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress;
const content = (() => { const content = (() => {
if (isLoading) { if (isLoading) {
...@@ -225,10 +227,11 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -225,10 +227,11 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
})(); })();
const tokenFilterComponent = tokenFilter && ( const tokenFilterComponent = tokenFilter && (
<Flex alignItems="center" py={ 1 } flexWrap="wrap" mb={{ base: isPaginationVisible ? 6 : 3, lg: 0 }}> <Flex alignItems="center" flexWrap="wrap" mb={{ base: isActionBarHidden ? 3 : 6, lg: 0 }} mr={ 4 }>
Filtered by token <Text whiteSpace="nowrap" mr={ 2 } py={ 1 }>Filtered by token</Text>
<TokenLogo hash={ tokenFilter } boxSize={ 6 } mx={ 2 }/> <Flex alignItems="center" py={ 1 }>
{ isMobile ? tokenFilter.slice(0, 4) + '...' + tokenFilter.slice(-4) : tokenFilter } <TokenLogo hash={ tokenFilter } boxSize={ 6 } mr={ 2 }/>
{ isMobile ? <HashStringShorten hash={ tokenFilter }/> : tokenFilter }
<Tooltip label="Reset filter"> <Tooltip label="Reset filter">
<Flex> <Flex>
<Icon <Icon
...@@ -243,6 +246,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -243,6 +246,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
</Flex> </Flex>
</Tooltip> </Tooltip>
</Flex> </Flex>
</Flex>
); );
return ( return (
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { withName } from 'mocks/address/address'; import { withName } from 'mocks/address/address';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokensMock from 'mocks/address/tokens';
import { baseList } from 'mocks/address/tokenBalance';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokens from './AddressTokens'; import AddressTokens from './AddressTokens';
const ADDRESS_HASH = withName.hash; const ADDRESS_HASH = withName.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH }); const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_ADDRESS_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH }); const API_URL_TOKENS = buildApiUrl('address_tokens', { hash: ADDRESS_HASH });
const API_URL_TOKENS = buildApiUrl('address_tokens', { id: ADDRESS_HASH });
const nextPageParams = { const nextPageParams = {
items_count: 50, items_count: 50,
...@@ -22,16 +20,18 @@ const nextPageParams = { ...@@ -22,16 +20,18 @@ const nextPageParams = {
value: 1, value: 1,
}; };
test('erc20 +@mobile +@dark-mode', async({ mount, page }) => { const test = base.extend({
const hooksConfig = { page: async({ page }, use) => {
router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc20' },
isReady: true,
},
};
const response20 = { const response20 = {
items: [ tokenBalanceMock.erc20a, tokenBalanceMock.erc20b, tokenBalanceMock.erc20c, tokenBalanceMock.erc20d ], items: [ tokensMock.erc20a, tokensMock.erc20b, tokensMock.erc20c, tokensMock.erc20d ],
next_page_params: nextPageParams,
};
const response721 = {
items: [ tokensMock.erc721a, tokensMock.erc721b, tokensMock.erc721c ],
next_page_params: nextPageParams,
};
const response1155 = {
items: [ tokensMock.erc1155a, tokensMock.erc1155b ],
next_page_params: nextPageParams, next_page_params: nextPageParams,
}; };
...@@ -39,14 +39,30 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -39,14 +39,30 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => {
status: 200, status: 200,
body: JSON.stringify(withName), body: JSON.stringify(withName),
})); }));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({ await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(response20), body: JSON.stringify(response20),
})); }));
await page.route(API_URL_TOKENS + '?type=ERC-721', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response721),
}));
await page.route(API_URL_TOKENS + '?type=ERC-1155', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response1155),
}));
use(page);
},
});
test('erc20 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' },
isReady: true,
},
};
const component = await mount( const component = await mount(
<TestApp> <TestApp>
...@@ -59,32 +75,14 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -59,32 +75,14 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc721 +@mobile +@dark-mode', async({ mount, page }) => { test('erc721 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc721' }, query: { hash: ADDRESS_HASH, tab: 'tokens_erc721' },
isReady: true, isReady: true,
}, },
}; };
const response20 = {
items: [ tokenBalanceMock.erc721a, tokenBalanceMock.erc721b, tokenBalanceMock.erc721c ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-721', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
...@@ -96,32 +94,14 @@ test('erc721 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -96,32 +94,14 @@ test('erc721 +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc1155 +@mobile +@dark-mode', async({ mount, page }) => { test('erc1155 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc1155' }, query: { hash: ADDRESS_HASH, tab: 'tokens_erc1155' },
isReady: true, isReady: true,
}, },
}; };
const response20 = {
items: [ tokenBalanceMock.erc1155a, tokenBalanceMock.erc1155b ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-1155', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
......
...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
...@@ -36,7 +36,7 @@ const AddressTokens = () => { ...@@ -36,7 +36,7 @@ const AddressTokens = () => {
const tokensQuery = useQueryWithPages({ const tokensQuery = useQueryWithPages({
resourceName: 'address_tokens', resourceName: 'address_tokens',
pathParams: { id: router.query.id?.toString() }, pathParams: { hash: router.query.hash?.toString() },
filters: { type: tokenType }, filters: { type: tokenType },
scrollRef, scrollRef,
}); });
......
...@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTxs from './AddressTxs'; import AddressTxs from './AddressTxs';
const API_URL = buildApiUrl('address_txs', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }); const API_URL = buildApiUrl('address_txs', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' },
}, },
}; };
......
...@@ -10,6 +10,7 @@ import { getResourceKey } from 'lib/api/useApiQuery'; ...@@ -10,6 +10,7 @@ import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
...@@ -31,13 +32,13 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -31,13 +32,13 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
const [ newItemsCount, setNewItemsCount ] = React.useState(0); const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const currentAddress = router.query.id?.toString(); const currentAddress = getQueryParamString(router.query.hash);
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter)); const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const addressTxsQuery = useQueryWithPages({ const addressTxsQuery = useQueryWithPages({
resourceName: 'address_txs', resourceName: 'address_txs',
pathParams: { id: currentAddress }, pathParams: { hash: currentAddress },
filters: { filter: filterValue }, filters: { filter: filterValue },
scrollRef, scrollRef,
}); });
...@@ -63,7 +64,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -63,7 +64,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
} }
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { id: router.query.id?.toString() }, queryParams: { filter: filterValue } }), getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }),
(prevData: AddressTransactionsResponse | undefined) => { (prevData: AddressTransactionsResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
......
import { Text, Flex } from '@chakra-ui/react'; import { Text, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
...@@ -7,7 +8,6 @@ import type { Block } from 'types/api/block'; ...@@ -7,7 +8,6 @@ import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
...@@ -17,7 +17,7 @@ type Props = Block & { ...@@ -17,7 +17,7 @@ type Props = Block & {
}; };
const AddressBlocksValidatedListItem = (props: Props) => { const AddressBlocksValidatedListItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.height) }); const blockUrl = route({ pathname: '/block/[height]', query: { height: props.height.toString() } });
const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1); const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1);
const totalReward = getBlockTotalReward(props); const totalReward = getBlockTotalReward(props);
......
import { Td, Tr, Text, Box, Flex } from '@chakra-ui/react'; import { Td, Tr, Text, Box, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
...@@ -15,7 +15,7 @@ type Props = Block & { ...@@ -15,7 +15,7 @@ type Props = Block & {
}; };
const AddressBlocksValidatedTableItem = (props: Props) => { const AddressBlocksValidatedTableItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.height) }); const blockUrl = route({ pathname: '/block/[height]', query: { height: String(props.height) } });
const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1); const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1);
const totalReward = getBlockTotalReward(props); const totalReward = getBlockTotalReward(props);
......
...@@ -11,7 +11,7 @@ interface Props { ...@@ -11,7 +11,7 @@ interface Props {
const AddressCoinBalanceChart = ({ addressHash }: Props) => { const AddressCoinBalanceChart = ({ addressHash }: Props) => {
const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', { const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
}); });
const items = React.useMemo(() => data?.map(({ date, value }) => ({ const items = React.useMemo(() => data?.map(({ date, value }) => ({
......
import { Text, Stat, StatHelpText, StatArrow, Flex } from '@chakra-ui/react'; import { Text, Stat, StatHelpText, StatArrow, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
...@@ -7,7 +8,6 @@ import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; ...@@ -7,7 +8,6 @@ import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { WEI, ZERO } from 'lib/consts'; import { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -18,7 +18,7 @@ type Props = AddressCoinBalanceHistoryItem & { ...@@ -18,7 +18,7 @@ type Props = AddressCoinBalanceHistoryItem & {
}; };
const AddressCoinBalanceListItem = (props: Props) => { const AddressCoinBalanceListItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.block_number) }); const blockUrl = route({ pathname: '/block/[height]', query: { height: String(props.block_number) } });
const deltaBn = BigNumber(props.delta).div(WEI); const deltaBn = BigNumber(props.delta).div(WEI);
const isPositiveDelta = deltaBn.gte(ZERO); const isPositiveDelta = deltaBn.gte(ZERO);
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1); const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
......
import { Td, Tr, Text, Stat, StatHelpText, StatArrow } from '@chakra-ui/react'; import { Td, Tr, Text, Stat, StatHelpText, StatArrow } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import { WEI, ZERO } from 'lib/consts'; import { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -16,7 +16,7 @@ type Props = AddressCoinBalanceHistoryItem & { ...@@ -16,7 +16,7 @@ type Props = AddressCoinBalanceHistoryItem & {
}; };
const AddressCoinBalanceTableItem = (props: Props) => { const AddressCoinBalanceTableItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.block_number) }); const blockUrl = route({ pathname: '/block/[height]', query: { height: String(props.block_number) } });
const deltaBn = BigNumber(props.delta).div(WEI); const deltaBn = BigNumber(props.delta).div(WEI);
const isPositiveDelta = deltaBn.gte(ZERO); const isPositiveDelta = deltaBn.gte(ZERO);
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1); const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
......
...@@ -8,10 +8,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,10 +8,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import ContractCode from './ContractCode'; import ContractCode from './ContractCode';
const addressHash = 'hash'; const addressHash = 'hash';
const CONTRACT_API_URL = buildApiUrl('contract', { id: addressHash }); const CONTRACT_API_URL = buildApiUrl('contract', { hash: addressHash });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: addressHash }, query: { hash: addressHash },
}, },
}; };
......
import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react'; import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link'; import getQueryParamString from 'lib/router/getQueryParamString';
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';
...@@ -24,9 +25,9 @@ const InfoItem = ({ label, value }: { label: string; value: string }) => ( ...@@ -24,9 +25,9 @@ const InfoItem = ({ label, value }: { label: string; value: string }) => (
const ContractCode = () => { const ContractCode = () => {
const router = useRouter(); const router = useRouter();
const addressHash = router.query.id?.toString(); const addressHash = getQueryParamString(router.query.hash);
const { data, isLoading, isError } = useApiQuery('contract', { const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash), enabled: Boolean(addressHash),
refetchOnMount: false, refetchOnMount: false,
...@@ -60,7 +61,7 @@ const ContractCode = () => { ...@@ -60,7 +61,7 @@ const ContractCode = () => {
ml="auto" ml="auto"
mr={ 3 } mr={ 3 }
as="a" as="a"
href={ link('address_contract_verification', { id: addressHash }) } href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash } }) }
> >
Verify & publish Verify & publish
</Button> </Button>
...@@ -74,7 +75,7 @@ const ContractCode = () => { ...@@ -74,7 +75,7 @@ const ContractCode = () => {
const decoded = data.decoded_constructor_args const decoded = data.decoded_constructor_args
.map(([ value, { name, type } ], index) => { .map(([ value, { name, type } ], index) => {
const valueEl = type === 'address' ? const valueEl = type === 'address' ?
<LinkInternal href={ link('address_index', { id: value }) }>{ value }</LinkInternal> : <LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: value } }) }>{ value }</LinkInternal> :
<span>{ value }</span>; <span>{ value }</span>;
return ( return (
<Box key={ index }> <Box key={ index }>
...@@ -101,7 +102,7 @@ const ContractCode = () => { ...@@ -101,7 +102,7 @@ const ContractCode = () => {
return data.external_libraries.map((item) => ( return data.external_libraries.map((item) => (
<Box key={ item.address_hash }> <Box key={ item.address_hash }>
<chakra.span fontWeight={ 500 }>{ item.name }: </chakra.span> <chakra.span fontWeight={ 500 }>{ item.name }: </chakra.span>
<LinkInternal href={ link('address_index', { id: item.address_hash }, { tab: 'contract' }) }>{ item.address_hash }</LinkInternal> <LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: item.address_hash, tab: 'contract' } }) }>{ item.address_hash }</LinkInternal>
</Box> </Box>
)); ));
})(); })();
...@@ -129,7 +130,7 @@ const ContractCode = () => { ...@@ -129,7 +130,7 @@ const ContractCode = () => {
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/> <AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address> </Address>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span> <chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<LinkInternal href={ link('address_contract_verification', { id: data.verified_twin_address_hash }) }>Verify & Publish</LinkInternal> <LinkInternal href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash } }) }>Verify & Publish</LinkInternal>
<span> page</span> <span> page</span>
</Alert> </Alert>
) } ) }
......
...@@ -14,7 +14,7 @@ interface Props { ...@@ -14,7 +14,7 @@ interface Props {
const ContractImplementationAddress = ({ hash }: Props) => { const ContractImplementationAddress = ({ hash }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const data = queryClient.getQueryData<TAddress>(getResourceKey('address', { const data = queryClient.getQueryData<TAddress>(getResourceKey('address', {
pathParams: { id: hash }, pathParams: { hash },
})); }));
if (!data?.implementation_address) { if (!data?.implementation_address) {
......
import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Flex, Icon, Link, Tooltip } from '@chakra-ui/react'; import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Flex, Link } from '@chakra-ui/react';
import _range from 'lodash/range'; import _range from 'lodash/range';
import React from 'react'; import React from 'react';
import type { SmartContractMethod } from 'types/api/contract'; import type { SmartContractMethod } from 'types/api/contract';
import infoIcon from 'icons/info.svg'; import Hint from 'ui/shared/Hint';
interface Props<T extends SmartContractMethod> { interface Props<T extends SmartContractMethod> {
data: Array<T>; data: Array<T>;
...@@ -46,32 +46,22 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC ...@@ -46,32 +46,22 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name } { index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name }
</Box> </Box>
{ item.type === 'fallback' && ( { item.type === 'fallback' && (
<Tooltip <Hint
label={ `The fallback function is executed on a call to the contract if none of the other functions match label={
`The fallback function is executed on a call to the contract if none of the other functions match
the given function signature, or if no data was supplied at all and there is no receive Ether function. the given function signature, or if no data was supplied at all and there is no receive Ether function.
The fallback function always receives data, but in order to also receive Ether it must be marked payable.` } The fallback function always receives data, but in order to also receive Ether it must be marked payable.`
placement="top" }/>
maxW="320px"
>
<Box cursor="pointer" display="inherit">
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
) } ) }
{ item.type === 'receive' && ( { item.type === 'receive' && (
<Tooltip <Hint
label={ `The receive function is executed on a call to the contract with empty calldata. label={
`The receive function is executed on a call to the contract with empty calldata.
This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).
If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
If neither a receive Ether nor a payable fallback function is present, If neither a receive Ether nor a payable fallback function is present,
the contract cannot receive Ether through regular transactions and throws an exception.` } the contract cannot receive Ether through regular transactions and throws an exception.`
placement="top" }/>
maxW="320px"
>
<Box cursor="pointer" display="inherit">
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
) } ) }
<AccordionIcon/> <AccordionIcon/>
</AccordionButton> </AccordionButton>
......
...@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import ContractRead from './ContractRead'; import ContractRead from './ContractRead';
const addressHash = 'hash'; const addressHash = 'hash';
const CONTRACT_READ_METHODS_API_URL = buildApiUrl('contract_methods_read', { id: addressHash }) + '?is_custom_abi=false'; const CONTRACT_READ_METHODS_API_URL = buildApiUrl('contract_methods_read', { hash: addressHash }) + '?is_custom_abi=false';
const CONTRACT_QUERY_METHOD_API_URL = buildApiUrl('contract_method_query', { id: addressHash }); const CONTRACT_QUERY_METHOD_API_URL = buildApiUrl('contract_method_query', { hash: addressHash });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: addressHash }, query: { hash: addressHash },
}, },
}; };
......
...@@ -7,6 +7,7 @@ import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'type ...@@ -7,6 +7,7 @@ import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'type
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion'; import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -28,21 +29,21 @@ const ContractRead = ({ isProxy, isCustomAbi }: Props) => { ...@@ -28,21 +29,21 @@ const ContractRead = ({ isProxy, isCustomAbi }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { address: userAddress } = useAccount(); const { address: userAddress } = useAccount();
const addressHash = router.query.id?.toString(); const addressHash = getQueryParamString(router.query.hash);
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', { const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
queryParams: { queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false', is_custom_abi: isCustomAbi ? 'true' : 'false',
}, },
queryOptions: { queryOptions: {
enabled: Boolean(router.query.id), enabled: Boolean(addressHash),
}, },
}); });
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array<string | Array<string>>) => { const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array<string | Array<string>>) => {
return apiFetch<'contract_method_query', SmartContractQueryMethodRead>('contract_method_query', { return apiFetch<'contract_method_query', SmartContractQueryMethodRead>('contract_method_query', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body: { body: {
......
import { Box, chakra, Flex, Text, Tooltip } from '@chakra-ui/react'; import { Box, chakra, Flex, Text, Tooltip } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SmartContract } from 'types/api/contract'; import type { SmartContract } from 'types/api/contract';
import link from 'lib/link/link';
import CodeEditor from 'ui/shared/CodeEditor'; import CodeEditor from 'ui/shared/CodeEditor';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -28,7 +28,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -28,7 +28,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
const diagramLink = hasSol2Yml && address ? ( const diagramLink = hasSol2Yml && address ? (
<Tooltip label="Visualize contract code using Sol2Uml JS library"> <Tooltip label="Visualize contract code using Sol2Uml JS library">
<LinkInternal <LinkInternal
href={ link('visualize_sol2uml', undefined, { address }) } href={ route({ pathname: '/visualize/sol2uml', query: { address } }) }
ml="auto" ml="auto"
mr={ 3 } mr={ 3 }
> >
......
...@@ -8,10 +8,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,10 +8,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import ContractWrite from './ContractWrite'; import ContractWrite from './ContractWrite';
const addressHash = 'hash'; const addressHash = 'hash';
const CONTRACT_WRITE_METHODS_API_URL = buildApiUrl('contract_methods_write', { id: addressHash }) + '?is_custom_abi=false'; const CONTRACT_WRITE_METHODS_API_URL = buildApiUrl('contract_methods_write', { hash: addressHash }) + '?is_custom_abi=false';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: addressHash }, query: { hash: addressHash },
}, },
}; };
......
...@@ -7,6 +7,7 @@ import type { SmartContractWriteMethod } from 'types/api/contract'; ...@@ -7,6 +7,7 @@ import type { SmartContractWriteMethod } from 'types/api/contract';
import config from 'configs/app/config'; import config from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion'; import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -27,12 +28,12 @@ interface Props { ...@@ -27,12 +28,12 @@ interface Props {
const ContractWrite = ({ isProxy, isCustomAbi }: Props) => { const ContractWrite = ({ isProxy, isCustomAbi }: Props) => {
const router = useRouter(); const router = useRouter();
const addressHash = router.query.id?.toString(); const addressHash = getQueryParamString(router.query.hash);
const { data: signer } = useSigner(); const { data: signer } = useSigner();
const { isConnected } = useAccount(); const { isConnected } = useAccount();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', { const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
queryParams: { queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false', is_custom_abi: isCustomAbi ? 'true' : 'false',
}, },
......
import { Box, chakra, Spinner } from '@chakra-ui/react'; import { Box, chakra, Spinner } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { ContractMethodWriteResult } from './types'; import type { ContractMethodWriteResult } from './types';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
interface Props { interface Props {
...@@ -30,9 +30,9 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => { ...@@ -30,9 +30,9 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => {
const isErrorResult = 'message' in result; const isErrorResult = 'message' in result;
const txLink = ( const txLink = txHash ? (
<LinkInternal href={ link('tx', { id: txHash }) }>View transaction details</LinkInternal> <LinkInternal href={ route({ pathname: '/tx/[hash]', query: { hash: txHash } }) }>View transaction details</LinkInternal>
); ) : null;
const content = (() => { const content = (() => {
if (isErrorResult) { if (isErrorResult) {
......
...@@ -28,9 +28,9 @@ export function ContractContextProvider({ children }: ProviderProps) { ...@@ -28,9 +28,9 @@ export function ContractContextProvider({ children }: ProviderProps) {
const { data: signer } = useSigner(); const { data: signer } = useSigner();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const addressHash = router.query.id?.toString(); const addressHash = router.query.hash?.toString();
const { data: contractInfo } = useApiQuery('contract', { const { data: contractInfo } = useApiQuery('contract', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash), enabled: Boolean(addressHash),
refetchOnMount: false, refetchOnMount: false,
...@@ -38,11 +38,11 @@ export function ContractContextProvider({ children }: ProviderProps) { ...@@ -38,11 +38,11 @@ export function ContractContextProvider({ children }: ProviderProps) {
}); });
const addressInfo = queryClient.getQueryData<Address>(getResourceKey('address', { const addressInfo = queryClient.getQueryData<Address>(getResourceKey('address', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
})); }));
const { data: proxyInfo } = useApiQuery('contract', { const { data: proxyInfo } = useApiQuery('contract', {
pathParams: { id: addressInfo?.implementation_address || '' }, pathParams: { hash: addressInfo?.implementation_address || '' },
queryOptions: { queryOptions: {
enabled: Boolean(addressInfo?.implementation_address), enabled: Boolean(addressInfo?.implementation_address),
refetchOnMount: false, refetchOnMount: false,
......
import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react'; import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import metamaskIcon from 'icons/metamask.svg'; import metamaskIcon from 'icons/metamask.svg';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
......
...@@ -26,7 +26,7 @@ const AddressBalance = ({ data }: Props) => { ...@@ -26,7 +26,7 @@ const AddressBalance = ({ data }: Props) => {
} }
setLastBlockNumber(blockNumber); setLastBlockNumber(blockNumber);
const queryKey = getResourceKey('address', { pathParams: { id: data.hash } }); const queryKey = getResourceKey('address', { pathParams: { hash: data.hash } });
queryClient.setQueryData(queryKey, (prevData: Address | undefined) => { queryClient.setQueryData(queryKey, (prevData: Address | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
......
import { Skeleton } from '@chakra-ui/react'; import { Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { AddressCounters } from 'types/api/address'; import type { AddressCounters } from 'types/api/address';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
interface Props { interface Props {
...@@ -42,7 +42,7 @@ const AddressCounterItem = ({ prop, query, address, onClick }: Props) => { ...@@ -42,7 +42,7 @@ const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
return <span>0</span>; return <span>0</span>;
} }
return ( return (
<LinkInternal href={ link('address_index', { id: address }, { tab: PROP_TO_TAB[prop] }) } onClick={ onClick }> <LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: address, tab: PROP_TO_TAB[prop] } }) } onClick={ onClick }>
{ Number(data).toLocaleString() } { Number(data).toLocaleString() }
</LinkInternal> </LinkInternal>
); );
......
...@@ -41,10 +41,10 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => { ...@@ -41,10 +41,10 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
}, [ addModalProps, deleteModalProps, isAdded, isAuth, loginUrl ]); }, [ addModalProps, deleteModalProps, isAdded, isAuth, loginUrl ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => { const handleAddOrDeleteSuccess = React.useCallback(async() => {
const queryKey = getResourceKey('address', { pathParams: { id: router.query.id?.toString() } }); const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } });
await queryClient.refetchQueries({ queryKey }); await queryClient.refetchQueries({ queryKey });
addModalProps.onClose(); addModalProps.onClose();
}, [ addModalProps, queryClient, router.query.id ]); }, [ addModalProps, queryClient, router.query.hash ]);
const handleAddModalClose = React.useCallback(() => { const handleAddModalClose = React.useCallback(() => {
addModalProps.onClose(); addModalProps.onClose();
......
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import link from 'lib/link/link';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -19,7 +19,7 @@ const AddressNameInfo = ({ data }: Props) => { ...@@ -19,7 +19,7 @@ const AddressNameInfo = ({ data }: Props) => {
title="Token name" title="Token name"
hint="Token name and symbol" hint="Token name and symbol"
> >
<LinkInternal href={ link('token_index', { hash: data.token.address }) }> <LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }>
{ data.token.name }{ symbol } { data.token.name }{ symbol }
</LinkInternal> </LinkInternal>
</DetailsInfoItem> </DetailsInfoItem>
......
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react'; import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransaction } from 'types/api/internalTransaction';
...@@ -7,7 +8,6 @@ import type { InternalTransaction } from 'types/api/internalTransaction'; ...@@ -7,7 +8,6 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
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';
...@@ -50,7 +50,7 @@ const TxInternalsListItem = ({ ...@@ -50,7 +50,7 @@ const TxInternalsListItem = ({
</Flex> </Flex>
<HStack spacing={ 1 }> <HStack spacing={ 1 }>
<Text fontSize="sm" fontWeight={ 500 }>Block</Text> <Text fontSize="sm" fontWeight={ 500 }>Block</Text>
<LinkInternal href={ link('block', { id: block.toString() }) }>{ block }</LinkInternal> <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: block.toString() } }) }>{ block }</LinkInternal>
</HStack> </HStack>
<Box w="100%" display="flex" columnGap={ 3 }> <Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
......
import { Tr, Td, Tag, Icon, Box, Flex, Text } from '@chakra-ui/react'; import { Tr, Td, Tag, Icon, Box, Flex, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransaction } from 'types/api/internalTransaction';
...@@ -7,7 +8,6 @@ import type { InternalTransaction } from 'types/api/internalTransaction'; ...@@ -7,7 +8,6 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
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';
...@@ -58,7 +58,7 @@ const AddressIntTxsTableItem = ({ ...@@ -58,7 +58,7 @@ const AddressIntTxsTableItem = ({
</Flex> </Flex>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkInternal href={ link('block', { id: block.toString() }) }>{ block }</LinkInternal> <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: block.toString() } }) }>{ block }</LinkInternal>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
......
...@@ -7,8 +7,8 @@ const MockAddressPage = ({ children }: { children: JSX.Element }): JSX.Element = ...@@ -7,8 +7,8 @@ const MockAddressPage = ({ children }: { children: JSX.Element }): JSX.Element =
const router = useRouter(); const router = useRouter();
const { data } = useApiQuery('address', { const { data } = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() }, pathParams: { hash: router.query.hash?.toString() },
queryOptions: { enabled: Boolean(router.query.id) }, queryOptions: { enabled: Boolean(router.query.hash) },
}); });
if (!data) { if (!data) {
......
...@@ -3,7 +3,7 @@ import { test as base, expect, devices } from '@playwright/experimental-ct-react ...@@ -3,7 +3,7 @@ import { test as base, expect, devices } from '@playwright/experimental-ct-react
import React from 'react'; import React from 'react';
import * as coinBalanceMock from 'mocks/address/coinBalanceHistory'; import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokensMock from 'mocks/address/tokens';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -12,11 +12,13 @@ import MockAddressPage from 'ui/address/testUtils/MockAddressPage'; ...@@ -12,11 +12,13 @@ import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
import TokenSelect from './TokenSelect'; import TokenSelect from './TokenSelect';
const ASSET_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0xb2a90505dc6680a7a695f7975d0d32EeF610f456/logo.png'; const ASSET_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0xb2a90505dc6680a7a695f7975d0d32EeF610f456/logo.png';
const TOKENS_API_URL = buildApiUrl('address_token_balances', { id: '1' }); const TOKENS_ERC20_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-20';
const ADDRESS_API_URL = buildApiUrl('address', { id: '1' }); const TOKENS_ERC721_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-721';
const TOKENS_ER1155_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-1155';
const ADDRESS_API_URL = buildApiUrl('address', { hash: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: '1' }, query: { hash: '1' },
}, },
}; };
const CLIPPING_AREA = { x: 0, y: 0, width: 360, height: 500 }; const CLIPPING_AREA = { x: 0, y: 0, width: 360, height: 500 };
...@@ -33,9 +35,17 @@ const test = base.extend({ ...@@ -33,9 +35,17 @@ const test = base.extend({
status: 200, status: 200,
body: JSON.stringify({ hash: '1' }), body: JSON.stringify({ hash: '1' }),
}), { times: 1 }); }), { times: 1 });
await page.route(TOKENS_API_URL, async(route) => route.fulfill({ await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(tokenBalanceMock.baseList), body: JSON.stringify(tokensMock.erc20List),
}), { times: 1 });
await page.route(TOKENS_ERC721_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc721List),
}), { times: 1 });
await page.route(TOKENS_ER1155_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc1155List),
}), { times: 1 }); }), { times: 1 });
use(page); use(page);
...@@ -137,9 +147,17 @@ base('long values', async({ mount, page }) => { ...@@ -137,9 +147,17 @@ base('long values', async({ mount, page }) => {
status: 200, status: 200,
body: JSON.stringify({ hash: '1' }), body: JSON.stringify({ hash: '1' }),
}), { times: 1 }); }), { times: 1 });
await page.route(TOKENS_API_URL, async(route) => route.fulfill({ await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokensMock.erc20LongSymbol ] }),
}), { times: 1 });
await page.route(TOKENS_ERC721_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokensMock.erc721LongSymbol ] }),
}), { times: 1 });
await page.route(TOKENS_ER1155_API_URL, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(tokenBalanceMock.longValuesList), body: JSON.stringify({ items: [ tokensMock.erc1155LongId ] }),
}), { times: 1 }); }), { times: 1 });
await mount( await mount(
...@@ -175,13 +193,15 @@ test.describe('socket', () => { ...@@ -175,13 +193,15 @@ test.describe('socket', () => {
{ hooksConfig }, { hooksConfig },
); );
await page.route(TOKENS_API_URL, (route) => route.fulfill({ await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify([ body: JSON.stringify({
...tokenBalanceMock.baseList, items: [
tokenBalanceMock.erc20d, ...tokensMock.erc20List.items,
]), tokensMock.erc20d,
})); ],
}),
}), { times: 1 });
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1'); const channel = await socketServer.joinChannel(socket, 'addresses:1');
...@@ -206,13 +226,15 @@ test.describe('socket', () => { ...@@ -206,13 +226,15 @@ test.describe('socket', () => {
{ hooksConfig }, { hooksConfig },
); );
await page.route(TOKENS_API_URL, (route) => route.fulfill({ await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify([ body: JSON.stringify({
...tokenBalanceMock.baseList, items: [
tokenBalanceMock.erc20d, ...tokensMock.erc20List.items,
]), tokensMock.erc20d,
})); ],
}),
}), { times: 1 });
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1'); const channel = await socketServer.joinChannel(socket, 'addresses:1');
......
import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react'; import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQueryClient, useIsFetching } from '@tanstack/react-query'; import { useQueryClient, useIsFetching } from '@tanstack/react-query';
import _sumBy from 'lodash/sumBy';
import NextLink from 'next/link'; import NextLink from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -8,12 +9,13 @@ import type { SocketMessage } from 'lib/socket/types'; ...@@ -8,12 +9,13 @@ import type { SocketMessage } from 'lib/socket/types';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import useFetchTokens from '../utils/useFetchTokens';
import TokenSelectDesktop from './TokenSelectDesktop'; import TokenSelectDesktop from './TokenSelectDesktop';
import TokenSelectMobile from './TokenSelectMobile'; import TokenSelectMobile from './TokenSelectMobile';
...@@ -27,17 +29,14 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -27,17 +29,14 @@ const TokenSelect = ({ onClick }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>(); const [ blockNumber, setBlockNumber ] = React.useState<number>();
const addressHash = router.query.id?.toString(); const addressHash = getQueryParamString(router.query.hash);
const addressResourceKey = getResourceKey('address', { pathParams: { id: addressHash } }); const addressResourceKey = getResourceKey('address', { pathParams: { hash: addressHash } });
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey); const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
const { data, isError, isLoading, refetch } = useApiQuery('address_token_balances', { const { data, isError, isLoading, refetch } = useFetchTokens({ hash: addressQueryData?.hash });
pathParams: { id: addressQueryData?.hash }, const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } });
queryOptions: { enabled: Boolean(addressQueryData) }, const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey });
});
const balancesResourceKey = getResourceKey('address_token_balances', { pathParams: { id: addressQueryData?.hash } });
const balancesIsFetching = useIsFetching({ queryKey: balancesResourceKey });
const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => { const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => {
if (payload.block_number !== blockNumber) { if (payload.block_number !== blockNumber) {
...@@ -71,19 +70,20 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -71,19 +70,20 @@ const TokenSelect = ({ onClick }: Props) => {
return <Skeleton h={ 8 } w="160px"/>; return <Skeleton h={ 8 } w="160px"/>;
} }
if (isError || data.length === 0) { const hasTokens = _sumBy(Object.values(data), ({ items }) => items.length) > 0;
if (isError || !hasTokens) {
return <Box py="6px">0</Box>; return <Box py="6px">0</Box>;
} }
return ( return (
<Flex columnGap={ 3 } mt={{ base: '6px', lg: 0 }}> <Flex columnGap={ 3 } mt={{ base: '6px', lg: 0 }}>
{ isMobile ? { isMobile ?
<TokenSelectMobile data={ data } isLoading={ balancesIsFetching === 1 }/> : <TokenSelectMobile data={ data } isLoading={ tokensIsFetching === 1 }/> :
<TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/> <TokenSelectDesktop data={ data } isLoading={ tokensIsFetching === 1 }/>
} }
<Tooltip label="Show all tokens"> <Tooltip label="Show all tokens">
<Box> <Box>
<NextLink href={ link('address_index', { id: addressHash }, { tab: 'tokens' }) } passHref> <NextLink href={{ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'tokens' } }} passHref>
<IconButton <IconButton
aria-label="Show all tokens" aria-label="Show all tokens"
variant="outline" variant="outline"
......
import { Box, Button, Icon, Skeleton, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Button, Icon, Skeleton, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { FormattedData } from './types';
import arrowIcon from 'icons/arrows/east-mini.svg'; import arrowIcon from 'icons/arrows/east-mini.svg';
import tokensIcon from 'icons/tokens.svg'; import tokensIcon from 'icons/tokens.svg';
import type { EnhancedData } from '../utils/tokenUtils'; import { getTokensTotalInfo } from '../utils/tokenUtils';
import { getTokenBalanceTotal } from '../utils/tokenUtils';
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
isLoading: boolean; isLoading: boolean;
onClick: () => void; onClick: () => void;
data: Array<EnhancedData>; data: FormattedData;
} }
const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => { const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const totalBn = getTokenBalanceTotal(data); const { usd, num, isOverflow } = getTokensTotalInfo(data);
const skeletonBgColor = useColorModeValue('white', 'black'); const skeletonBgColor = useColorModeValue('white', 'black');
const prefix = isOverflow ? '>' : '';
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
if (isLoading && !isOpen) { if (isLoading && !isOpen) {
return; return;
...@@ -36,8 +39,8 @@ const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: Rea ...@@ -36,8 +39,8 @@ const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: Rea
aria-label="Token select" aria-label="Token select"
> >
<Icon as={ tokensIcon } boxSize={ 4 } mr={ 2 }/> <Icon as={ tokensIcon } boxSize={ 4 } mr={ 2 }/>
<Text fontWeight={ 600 }>{ data.length }</Text> <Text fontWeight={ 600 }>{ prefix }{ num }</Text>
<Text whiteSpace="pre" variant="secondary" fontWeight={ 400 }> (${ totalBn.toFormat(2) })</Text> <Text whiteSpace="pre" variant="secondary" fontWeight={ 400 }> ({ prefix }${ usd.toFormat(2) })</Text>
<Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 3 }/> <Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 3 }/>
</Button> </Button>
{ isLoading && !isOpen && <Skeleton h="100%" w="100%" position="absolute" top={ 0 } left={ 0 } bgColor={ skeletonBgColor }/> } { isLoading && !isOpen && <Skeleton h="100%" w="100%" position="absolute" top={ 0 } left={ 0 } bgColor={ skeletonBgColor }/> }
......
import { useColorModeValue, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; import { useColorModeValue, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { FormattedData } from './types';
import TokenSelectButton from './TokenSelectButton'; import TokenSelectButton from './TokenSelectButton';
import TokenSelectMenu from './TokenSelectMenu'; import TokenSelectMenu from './TokenSelectMenu';
import useTokenSelect from './useTokenSelect'; import useTokenSelect from './useTokenSelect';
interface Props { interface Props {
data: Array<AddressTokenBalance>; data: FormattedData;
isLoading: boolean; isLoading: boolean;
} }
...@@ -22,7 +22,7 @@ const TokenSelectDesktop = ({ data, isLoading }: Props) => { ...@@ -22,7 +22,7 @@ const TokenSelectDesktop = ({ data, isLoading }: Props) => {
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger> <PopoverTrigger>
<TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.modifiedData } isLoading={ isLoading }/> <TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.data } isLoading={ isLoading }/>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w="355px" maxH="450px" overflowY="scroll"> <PopoverContent w="355px" maxH="450px" overflowY="scroll">
<PopoverBody px={ 4 } py={ 6 } bgColor={ bgColor } boxShadow="2xl" > <PopoverBody px={ 4 } py={ 6 } bgColor={ bgColor } boxShadow="2xl" >
......
import { chakra, Flex, Text, useColorModeValue } from '@chakra-ui/react'; import { chakra, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import link from 'lib/link/link';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import type { EnhancedData } from '../utils/tokenUtils'; import type { TokenEnhancedData } from '../utils/tokenUtils';
interface Props { interface Props {
data: EnhancedData; data: TokenEnhancedData;
} }
const TokenSelectItem = ({ data }: Props) => { const TokenSelectItem = ({ data }: Props) => {
...@@ -45,7 +45,7 @@ const TokenSelectItem = ({ data }: Props) => { ...@@ -45,7 +45,7 @@ const TokenSelectItem = ({ data }: Props) => {
})(); })();
// TODO add filter param when token page is ready // TODO add filter param when token page is ready
const url = link('token_index', { hash: data.token.address }); const url = route({ pathname: '/token/[hash]', query: { hash: data.token.address } });
return ( return (
<Flex <Flex
......
import { Icon, Text, Box, Input, InputGroup, InputLeftElement, useColorModeValue, Flex, Link } from '@chakra-ui/react'; import { Icon, Text, Box, Input, InputGroup, InputLeftElement, useColorModeValue, Flex, Link } from '@chakra-ui/react';
import type { Dictionary } from 'lodash'; import _sumBy from 'lodash/sumBy';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { FormattedData } from './types';
import type { TokenType } from 'types/api/token';
import arrowIcon from 'icons/arrows/east.svg'; import arrowIcon from 'icons/arrows/east.svg';
import searchIcon from 'icons/search.svg'; import searchIcon from 'icons/search.svg';
import type { Sort, EnhancedData } from '../utils/tokenUtils'; import type { Sort } from '../utils/tokenUtils';
import { sortTokenGroups, sortingFns } from '../utils/tokenUtils'; import { sortTokenGroups, sortingFns } from '../utils/tokenUtils';
import TokenSelectItem from './TokenSelectItem'; import TokenSelectItem from './TokenSelectItem';
...@@ -16,16 +17,17 @@ interface Props { ...@@ -16,16 +17,17 @@ interface Props {
searchTerm: string; searchTerm: string;
erc20sort: Sort; erc20sort: Sort;
erc1155sort: Sort; erc1155sort: Sort;
modifiedData: Array<EnhancedData>; filteredData: FormattedData;
groupedData: Dictionary<Array<EnhancedData>>;
onInputChange: (event: ChangeEvent<HTMLInputElement>) => void; onInputChange: (event: ChangeEvent<HTMLInputElement>) => void;
onSortClick: (event: React.SyntheticEvent) => void; onSortClick: (event: React.SyntheticEvent) => void;
} }
const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, onInputChange, onSortClick, searchTerm }: Props) => { const TokenSelectMenu = ({ erc20sort, erc1155sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200'); const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
const hasFilteredResult = _sumBy(Object.values(filteredData), ({ items }) => items.length) > 0;
return ( return (
<> <>
<InputGroup size="xs" mb={ 5 }> <InputGroup size="xs" mb={ 5 }>
...@@ -41,7 +43,12 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, on ...@@ -41,7 +43,12 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, on
/> />
</InputGroup> </InputGroup>
<Flex flexDir="column" rowGap={ 6 }> <Flex flexDir="column" rowGap={ 6 }>
{ Object.entries(groupedData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => { { Object.entries(filteredData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => {
if (tokenInfo.items.length === 0) {
return null;
}
const type = tokenType as TokenType; const type = tokenType as TokenType;
const arrowTransform = (type === 'ERC-1155' && erc1155sort === 'desc') || (type === 'ERC-20' && erc20sort === 'desc') ? const arrowTransform = (type === 'ERC-1155' && erc1155sort === 'desc') || (type === 'ERC-20' && erc20sort === 'desc') ?
'rotate(90deg)' : 'rotate(90deg)' :
...@@ -56,24 +63,26 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, on ...@@ -56,24 +63,26 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, on
return 'desc'; return 'desc';
} }
})(); })();
const hasSort = type === 'ERC-1155' || (type === 'ERC-20' && tokenInfo.some(({ usd }) => usd)); const hasSort = type === 'ERC-1155' || (type === 'ERC-20' && tokenInfo.items.some(({ usd }) => usd));
const numPrefix = tokenInfo.isOverflow ? '>' : '';
return ( return (
<Box key={ type }> <Box key={ type }>
<Flex justifyContent="space-between"> <Flex justifyContent="space-between">
<Text mb={ 3 } color="gray.500" fontWeight={ 600 } fontSize="sm">{ type } tokens ({ tokenInfo.length })</Text> <Text mb={ 3 } color="gray.500" fontWeight={ 600 } fontSize="sm">{ type } tokens ({ numPrefix }{ tokenInfo.items.length })</Text>
{ hasSort && ( { hasSort && (
<Link data-type={ type } onClick={ onSortClick } aria-label={ `Sort ${ type } tokens` }> <Link data-type={ type } onClick={ onSortClick } aria-label={ `Sort ${ type } tokens` }>
<Icon as={ arrowIcon } boxSize={ 5 } transform={ arrowTransform } transitionDuration="faster"/> <Icon as={ arrowIcon } boxSize={ 5 } transform={ arrowTransform } transitionDuration="faster"/>
</Link> </Link>
) } ) }
</Flex> </Flex>
{ tokenInfo.sort(sortingFns[type](sortDirection)).map((data) => <TokenSelectItem key={ data.token.address + data.token_id } data={ data }/>) } { tokenInfo.items.sort(sortingFns[type](sortDirection)).map((data) =>
<TokenSelectItem key={ data.token.address + data.token_id } data={ data }/>) }
</Box> </Box>
); );
}) } }) }
</Flex> </Flex>
{ modifiedData.length === 0 && searchTerm && <Text fontSize="sm">Could not find any matches.</Text> } { Boolean(searchTerm) && !hasFilteredResult && <Text fontSize="sm">Could not find any matches.</Text> }
</> </>
); );
}; };
......
import { useDisclosure, Modal, ModalContent, ModalCloseButton } from '@chakra-ui/react'; import { useDisclosure, Modal, ModalContent, ModalCloseButton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { FormattedData } from './types';
import TokenSelectButton from './TokenSelectButton'; import TokenSelectButton from './TokenSelectButton';
import TokenSelectMenu from './TokenSelectMenu'; import TokenSelectMenu from './TokenSelectMenu';
import useTokenSelect from './useTokenSelect'; import useTokenSelect from './useTokenSelect';
interface Props { interface Props {
data: Array<AddressTokenBalance>; data: FormattedData;
isLoading: boolean; isLoading: boolean;
} }
...@@ -18,7 +18,7 @@ const TokenSelectMobile = ({ data, isLoading }: Props) => { ...@@ -18,7 +18,7 @@ const TokenSelectMobile = ({ data, isLoading }: Props) => {
return ( return (
<> <>
<TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.modifiedData } isLoading={ isLoading }/> <TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.data } isLoading={ isLoading }/>
<Modal isOpen={ isOpen } onClose={ onClose } size="full"> <Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent> <ModalContent>
<ModalCloseButton/> <ModalCloseButton/>
......
import type { TokenType } from 'types/api/token';
import type { TokenEnhancedData } from 'ui/address/utils/tokenUtils';
export type FormattedData = Record<TokenType, FormattedDataItem>;
export interface FormattedDataItem {
items: Array<TokenEnhancedData>;
isOverflow: boolean;
}
import _groupBy from 'lodash/groupBy'; import _mapValues from 'lodash/mapValues';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { FormattedData } from './types';
import type { Sort } from '../utils/tokenUtils'; import type { Sort } from '../utils/tokenUtils';
import { calculateUsdValue, filterTokens } from '../utils/tokenUtils'; import { filterTokens } from '../utils/tokenUtils';
export default function useData(data: Array<AddressTokenBalance>) { export default function useTokenSelect(data: FormattedData) {
const [ searchTerm, setSearchTerm ] = React.useState(''); const [ searchTerm, setSearchTerm ] = React.useState('');
const [ erc1155sort, setErc1155Sort ] = React.useState<Sort>('desc'); const [ erc1155sort, setErc1155Sort ] = React.useState<Sort>('desc');
const [ erc20sort, setErc20Sort ] = React.useState<Sort>('desc'); const [ erc20sort, setErc20Sort ] = React.useState<Sort>('desc');
...@@ -26,12 +26,12 @@ export default function useData(data: Array<AddressTokenBalance>) { ...@@ -26,12 +26,12 @@ export default function useData(data: Array<AddressTokenBalance>) {
} }
}, []); }, []);
const modifiedData = React.useMemo(() => { const filteredData = React.useMemo(() => {
return data.filter(filterTokens(searchTerm.toLowerCase())).map(calculateUsdValue); return _mapValues(data, ({ items, isOverflow }) => ({
isOverflow,
items: items.filter(filterTokens(searchTerm.toLowerCase())),
}));
}, [ data, searchTerm ]); }, [ data, searchTerm ]);
const groupedData = React.useMemo(() => {
return _groupBy(modifiedData, 'token.type');
}, [ modifiedData ]);
return { return {
searchTerm, searchTerm,
...@@ -39,7 +39,7 @@ export default function useData(data: Array<AddressTokenBalance>) { ...@@ -39,7 +39,7 @@ export default function useData(data: Array<AddressTokenBalance>) {
erc1155sort, erc1155sort,
onInputChange, onInputChange,
onSortClick, onSortClick,
modifiedData, data,
groupedData, filteredData,
}; };
} }
import { Flex, Link, Text, LinkBox, LinkOverlay, useColorModeValue } from '@chakra-ui/react'; import { Flex, Link, Text, LinkBox, LinkOverlay, useColorModeValue } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import link from 'lib/link/link';
import NftImage from 'ui/shared/nft/NftImage'; import NftImage from 'ui/shared/nft/NftImage';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
...@@ -11,11 +11,11 @@ import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; ...@@ -11,11 +11,11 @@ import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = AddressTokenBalance; type Props = AddressTokenBalance;
const NFTItem = ({ token, token_id: tokenId }: Props) => { const NFTItem = ({ token, token_id: tokenId }: Props) => {
const tokenLink = link('token_index', { hash: token.address }); const tokenLink = route({ pathname: '/token/[hash]', query: { hash: token.address } });
return ( return (
<LinkBox <LinkBox
w={{ base: 'calc((100% - 12px)/2)', lg: '210px' }} w={{ base: '100%', lg: '210px' }}
border="1px solid" border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px" borderRadius="12px"
...@@ -25,13 +25,14 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => { ...@@ -25,13 +25,14 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => {
fontWeight={ 500 } fontWeight={ 500 }
lineHeight="20px" lineHeight="20px"
> >
<LinkOverlay href={ tokenLink }/> <LinkOverlay href={ tokenLink }>
<NftImage <NftImage
mb="18px" mb="18px"
url={ null } url={ null }
fallbackPadding="30px" fallbackPadding="30px"
cursor="pointer" cursor="pointer"
/> />
</LinkOverlay>
{ tokenId && ( { tokenId && (
<Flex mb={ 2 } ml={ 1 }> <Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text> <Text whiteSpace="pre" variant="secondary">ID# </Text>
...@@ -40,7 +41,7 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => { ...@@ -40,7 +41,7 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => {
overflow="hidden" overflow="hidden"
whiteSpace="nowrap" whiteSpace="nowrap"
textOverflow="ellipsis" textOverflow="ellipsis"
href={ link('token_instance_item', { hash: token.address, id: tokenId }) } href={ route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) }
> >
{ tokenId } { tokenId }
</Link> </Link>
......
import { Flex, Skeleton } from '@chakra-ui/react'; import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { ZERO } from 'lib/consts';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import { getTokenBalanceTotal, calculateUsdValue } from '../utils/tokenUtils'; import { getTokensTotalInfo } from '../utils/tokenUtils';
import useFetchTokens from '../utils/useFetchTokens';
import TokenBalancesItem from './TokenBalancesItem'; import TokenBalancesItem from './TokenBalancesItem';
const TokenBalances = () => { const TokenBalances = () => {
const router = useRouter(); const router = useRouter();
const hash = router.query.hash?.toString();
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() }, pathParams: { hash },
queryOptions: { enabled: Boolean(router.query.id) }, queryOptions: { enabled: Boolean(hash) },
}); });
const balancesQuery = useApiQuery('address_token_balances', { const tokenQuery = useFetchTokens({ hash });
pathParams: { id: addressQuery.data?.hash },
queryOptions: { enabled: Boolean(addressQuery.data) },
});
if (addressQuery.isError || balancesQuery.isError) { if (addressQuery.isError || tokenQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (addressQuery.isLoading || balancesQuery.isLoading) { if (addressQuery.isLoading || tokenQuery.isLoading) {
const item = <Skeleton w={{ base: '100%', lg: '240px' }} h="82px" borderRadius="16px"/>; const item = <Skeleton w={{ base: '100%', lg: '240px' }} h="82px" borderRadius="16px"/>;
return ( return (
<Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}> <Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}>
...@@ -40,7 +40,7 @@ const TokenBalances = () => { ...@@ -40,7 +40,7 @@ const TokenBalances = () => {
} }
const addressData = addressQuery.data; const addressData = addressQuery.data;
const { valueStr: nativeValue, usd: nativeUsd } = getCurrencyValue({ const { valueStr: nativeValue, usdBn: nativeUsd } = getCurrencyValue({
value: addressData.coin_balance || '0', value: addressData.coin_balance || '0',
accuracy: 8, accuracy: 8,
accuracyUsd: 2, accuracyUsd: 2,
...@@ -48,22 +48,25 @@ const TokenBalances = () => { ...@@ -48,22 +48,25 @@ const TokenBalances = () => {
decimals: String(appConfig.network.currency.decimals), decimals: String(appConfig.network.currency.decimals),
}); });
const tokenBalanceBn = getTokenBalanceTotal(balancesQuery.data.map(calculateUsdValue)).toFixed(2); const tokensInfo = getTokensTotalInfo(tokenQuery.data);
const prefix = tokensInfo.isOverflow ? '>' : '';
const totalUsd = nativeUsd ? BigNumber(nativeUsd).toNumber() + BigNumber(tokenBalanceBn).toNumber() : undefined; const totalUsd = nativeUsd.plus(tokensInfo.usd);
const tokensNumText = tokensInfo.num > 0 ?
` | ${ prefix }${ tokensInfo.num } ${ tokensInfo.num > 1 ? 'tokens' : 'token' }` :
'';
return ( return (
<Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}> <Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}>
<TokenBalancesItem name="Net Worth" value={ totalUsd ? `$${ totalUsd } USD` : 'N/A' }/> <TokenBalancesItem name="Net Worth" value={ addressData.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) } USD` : 'N/A' }/>
<TokenBalancesItem <TokenBalancesItem
name={ `${ appConfig.network.currency.symbol } Balance` } name={ `${ appConfig.network.currency.symbol } Balance` }
value={ (nativeUsd ? `$${ nativeUsd } USD | ` : '') + `${ nativeValue } ${ appConfig.network.currency.symbol }` } value={ (!nativeUsd.eq(ZERO) ? `$${ nativeUsd.toFormat(2) } USD | ` : '') + `${ nativeValue } ${ appConfig.network.currency.symbol }` }
/> />
<TokenBalancesItem <TokenBalancesItem
name="Tokens" name="Tokens"
value={ value={
`$${ tokenBalanceBn } USD ` + `${ prefix }$${ tokensInfo.usd.toFormat(2) } USD ` +
(balancesQuery.data.length ? ` | ${ balancesQuery.data.length } ${ balancesQuery.data.length === 1 ? 'token' : 'tokens' }` : '') tokensNumText
} }
/> />
</Flex> </Flex>
......
import { Flex, Skeleton, Text } from '@chakra-ui/react'; import { Grid, Skeleton, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -38,11 +38,18 @@ const TokensWithIds = ({ tokensQuery }: Props) => { ...@@ -38,11 +38,18 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
return ( return (
<> <>
{ bar } { bar }
<Flex columnGap={ 6 } rowGap={ 6 } flexWrap="wrap"> <Grid
<Skeleton w="210px" h="272px"/> w="100%"
<Skeleton w="210px" h="272px"/> columnGap={{ base: 3, lg: 6 }}
<Skeleton w="210px" h="272px"/> rowGap={{ base: 3, lg: 6 }}
</Flex> gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
</> </>
); );
} }
...@@ -54,9 +61,14 @@ const TokensWithIds = ({ tokensQuery }: Props) => { ...@@ -54,9 +61,14 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
return ( return (
<> <>
{ bar } { bar }
<Flex columnGap={{ base: 3, lg: 6 }} rowGap={{ base: 3, lg: 6 }} flexWrap="wrap"> <Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) } { data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) }
</Flex> </Grid>
</> </>
); );
}; };
......
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import fpAdd from 'lodash/fp/add';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import sumBnReducer from 'lib/bigint/sumBnReducer';
import { ZERO } from 'lib/consts'; import { ZERO } from 'lib/consts';
export type EnhancedData = AddressTokenBalance & { export type TokenEnhancedData = AddressTokenBalance & {
usd?: BigNumber ; usd?: BigNumber ;
} }
export type Sort = 'desc' | 'asc'; export type Sort = 'desc' | 'asc';
export type TokenSelectData = Record<TokenType, TokenSelectDataItem>;
export interface TokenSelectDataItem {
items: Array<TokenEnhancedData>;
isOverflow: boolean;
}
type TokenGroup = [string, TokenSelectDataItem];
const TOKEN_GROUPS_ORDER: Array<TokenType> = [ 'ERC-20', 'ERC-721', 'ERC-1155' ]; const TOKEN_GROUPS_ORDER: Array<TokenType> = [ 'ERC-20', 'ERC-721', 'ERC-1155' ];
type TokenGroup = [string, Array<AddressTokenBalance>];
export const sortTokenGroups = (groupA: TokenGroup, groupB: TokenGroup) => { export const sortTokenGroups = (groupA: TokenGroup, groupB: TokenGroup) => {
return TOKEN_GROUPS_ORDER.indexOf(groupA[0] as TokenType) > TOKEN_GROUPS_ORDER.indexOf(groupB[0] as TokenType) ? 1 : -1; return TOKEN_GROUPS_ORDER.indexOf(groupA[0] as TokenType) > TOKEN_GROUPS_ORDER.indexOf(groupB[0] as TokenType) ? 1 : -1;
...@@ -27,7 +38,7 @@ const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: Ad ...@@ -27,7 +38,7 @@ const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: Ad
return Number(dataA.value) > Number(dataB.value) ? 1 : -1; return Number(dataA.value) > Number(dataB.value) ? 1 : -1;
}; };
const sortErc20Tokens = (sort: Sort) => (dataA: EnhancedData, dataB: EnhancedData) => { const sortErc20Tokens = (sort: Sort) => (dataA: TokenEnhancedData, dataB: TokenEnhancedData) => {
if (!dataA.usd && !dataB.usd) { if (!dataA.usd && !dataB.usd) {
return 0; return 0;
} }
...@@ -63,7 +74,7 @@ export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBala ...@@ -63,7 +74,7 @@ export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBala
return token.name?.toLowerCase().includes(searchTerm); return token.name?.toLowerCase().includes(searchTerm);
}; };
export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => { export const calculateUsdValue = (data: AddressTokenBalance): TokenEnhancedData => {
if (data.token.type !== 'ERC-20') { if (data.token.type !== 'ERC-20') {
return data; return data;
} }
...@@ -80,6 +91,18 @@ export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => { ...@@ -80,6 +91,18 @@ export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => {
}; };
}; };
export const getTokenBalanceTotal = (data: Array<EnhancedData>) => { export const getTokensTotalInfo = (data: TokenSelectData) => {
return data.reduce((result, item) => !item.usd ? result : result.plus(BigNumber(item.usd)), ZERO); const usd = Object.values(data)
.map(({ items }) => items.reduce(usdValueReducer, ZERO))
.reduce(sumBnReducer, ZERO);
const num = Object.values(data)
.map(({ items }) => items.length)
.reduce(fpAdd, 0);
const isOverflow = Object.values(data).some(({ isOverflow }) => isOverflow);
return { usd, num, isOverflow };
}; };
const usdValueReducer = (result: BigNumber, item: TokenEnhancedData) => !item.usd ? result : result.plus(BigNumber(item.usd));
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { calculateUsdValue } from './tokenUtils';
interface Props {
hash?: string;
}
export default function useFetchTokens({ hash }: Props) {
const erc20query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-20' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const erc721query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-721' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const erc1155query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-1155' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const refetch = React.useCallback(() => {
erc20query.refetch();
erc721query.refetch();
erc1155query.refetch();
}, [ erc1155query, erc20query, erc721query ]);
const data = React.useMemo(() => {
return {
'ERC-20': {
items: erc20query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc20query.data?.next_page_params),
},
'ERC-721': {
items: erc721query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc721query.data?.next_page_params),
},
'ERC-1155': {
items: erc1155query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc1155query.data?.next_page_params),
},
};
}, [ erc1155query.data, erc20query.data, erc721query.data ]);
return {
isLoading: erc20query.isLoading || erc721query.isLoading || erc1155query.isLoading,
isError: erc20query.isError || erc721query.isError || erc1155query.isError,
data,
refetch,
};
}
...@@ -103,7 +103,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -103,7 +103,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
{ ...field } { ...field }
disabled={ true } disabled={ true }
/> />
<FormLabel>Auto-generated API key token</FormLabel> <FormLabel data-in-modal="true">Auto-generated API key token</FormLabel>
</FormControl> </FormControl>
); );
}, []); }, []);
......
...@@ -12,7 +12,7 @@ type Props = { ...@@ -12,7 +12,7 @@ type Props = {
data?: ApiKey; data?: ApiKey;
} }
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const ApiKeyModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const title = data ? 'Edit API key' : 'New API key'; const title = data ? 'Edit API key' : 'New API key';
const text = !data ? 'Add an application name to identify your API key. Click the button below to auto-generate the associated key.' : ''; const text = !data ? 'Add an application name to identify your API key. Click the button below to auto-generate the associated key.' : '';
...@@ -34,4 +34,4 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -34,4 +34,4 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
); );
}; };
export default AddressModal; export default ApiKeyModal;
...@@ -2,8 +2,6 @@ import { LinkOverlay } from '@chakra-ui/react'; ...@@ -2,8 +2,6 @@ import { LinkOverlay } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import link from 'lib/link/link';
type Props = { type Props = {
id: string; id: string;
url: string; url: string;
...@@ -17,7 +15,7 @@ const AppLink = ({ url, external, id, title }: Props) => { ...@@ -17,7 +15,7 @@ const AppLink = ({ url, external, id, title }: Props) => {
{ title } { title }
</LinkOverlay> </LinkOverlay>
) : ( ) : (
<NextLink href={ link('app_index', { id: id }) } passHref> <NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref>
<LinkOverlay> <LinkOverlay>
{ title } { title }
</LinkOverlay> </LinkOverlay>
......
...@@ -2,8 +2,6 @@ import { Button } from '@chakra-ui/react'; ...@@ -2,8 +2,6 @@ import { Button } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import link from 'lib/link/link';
type Props = { type Props = {
id: string; id: string;
url: string; url: string;
...@@ -29,7 +27,7 @@ const AppModalLink = ({ url, external, id }: Props) => { ...@@ -29,7 +27,7 @@ const AppModalLink = ({ url, external, id }: Props) => {
{ ...buttonProps } { ...buttonProps }
>Launch app</Button> >Launch app</Button>
) : ( ) : (
<NextLink href={ link('app_index', { id: id }) } passHref> <NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref>
<Button <Button
as="a" as="a"
{ ...buttonProps } { ...buttonProps }
......
...@@ -7,10 +7,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -7,10 +7,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import BlockDetails from './BlockDetails'; import BlockDetails from './BlockDetails';
const API_URL = buildApiUrl('block', { id: '1' }); const API_URL = buildApiUrl('block', { height: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: '1' }, query: { height: '1' },
}, },
}; };
......
...@@ -2,6 +2,7 @@ import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react ...@@ -2,6 +2,7 @@ import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
...@@ -13,8 +14,8 @@ import getBlockReward from 'lib/block/getBlockReward'; ...@@ -13,8 +14,8 @@ import getBlockReward from 'lib/block/getBlockReward';
import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts'; import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString';
import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton'; import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -30,10 +31,11 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -30,10 +31,11 @@ import Utilization from 'ui/shared/Utilization/Utilization';
const BlockDetails = () => { const BlockDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false); const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter(); const router = useRouter();
const height = getQueryParamString(router.query.height);
const { data, isLoading, isError, error } = useApiQuery('block', { const { data, isLoading, isError, error } = useApiQuery('block', {
pathParams: { id: router.query.id?.toString() }, pathParams: { height },
queryOptions: { enabled: Boolean(router.query.id) }, queryOptions: { enabled: Boolean(height) },
}); });
const handleCutClick = React.useCallback(() => { const handleCutClick = React.useCallback(() => {
...@@ -46,11 +48,10 @@ const BlockDetails = () => { ...@@ -46,11 +48,10 @@ const BlockDetails = () => {
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => { const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
const increment = direction === 'next' ? +1 : -1; const increment = direction === 'next' ? +1 : -1;
const nextId = String(Number(router.query.id) + increment); const nextId = String(Number(height) + increment);
const url = link('block', { id: nextId }); router.push({ pathname: '/block/[height]', query: { height: nextId } }, undefined);
router.push(url, undefined); }, [ height, router ]);
}, [ router ]);
if (isLoading) { if (isLoading) {
return <BlockDetailsSkeleton/>; return <BlockDetailsSkeleton/>;
...@@ -116,7 +117,7 @@ const BlockDetails = () => { ...@@ -116,7 +117,7 @@ const BlockDetails = () => {
title="Transactions" title="Transactions"
hint="The number of transactions in the block." hint="The number of transactions in the block."
> >
<LinkInternal href={ link('block', { id: router.query.id }, { tab: 'txs' }) }> <LinkInternal href={ route({ pathname: '/block/[height]', query: { height, tab: 'txs' } }) }>
{ data.tx_count } transactions { data.tx_count } transactions
</LinkInternal> </LinkInternal>
</DetailsInfoItem> </DetailsInfoItem>
......
import { Flex, Spinner, Text, Box, Icon } from '@chakra-ui/react'; import { Flex, Spinner, Text, Box, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
...@@ -9,7 +10,6 @@ import appConfig from 'configs/app/config'; ...@@ -9,7 +10,6 @@ import appConfig from 'configs/app/config';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -36,7 +36,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -36,7 +36,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
{ isPending && <Spinner size="sm"/> } { isPending && <Spinner size="sm"/> }
<LinkInternal <LinkInternal
fontWeight={ 600 } fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) } href={ route({ pathname: '/block/[height]', query: { height: String(data.height) } }) }
> >
{ data.height } { data.height }
</LinkInternal> </LinkInternal>
...@@ -54,7 +54,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -54,7 +54,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text> <Text fontWeight={ 500 }>Txn</Text>
{ data.tx_count > 0 ? ( { data.tx_count > 0 ? (
<LinkInternal href={ link('block', { id: String(data.height) }, { tab: 'txs' }) }> <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count } { data.tx_count }
</LinkInternal> </LinkInternal>
) : ) :
......
import { Tr, Td, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react'; import { Tr, Td, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
...@@ -8,7 +9,6 @@ import type { Block } from 'types/api/block'; ...@@ -8,7 +9,6 @@ import type { Block } from 'types/api/block';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
import link from 'lib/link/link';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
...@@ -41,7 +41,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -41,7 +41,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations"> <Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<LinkInternal <LinkInternal
fontWeight={ 600 } fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) } href={ route({ pathname: '/block/[height]', query: { height: String(data.height) } }) }
> >
{ data.height } { data.height }
</LinkInternal> </LinkInternal>
...@@ -55,7 +55,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -55,7 +55,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Td> </Td>
<Td isNumeric fontSize="sm"> <Td isNumeric fontSize="sm">
{ data.tx_count > 0 ? ( { data.tx_count > 0 ? (
<LinkInternal href={ link('block', { id: String(data.height) }, { tab: 'txs' }) }> <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count } { data.tx_count }
</LinkInternal> </LinkInternal>
) : data.tx_count } ) : data.tx_count }
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import ContractVerificationForm from './ContractVerificationForm'; import ContractVerificationForm from './ContractVerificationForm';
...@@ -11,10 +13,49 @@ const hooksConfig = { ...@@ -11,10 +13,49 @@ const hooksConfig = {
}, },
}; };
const hash = '0x2F99338637F027CFB7494E46B49987457beCC6E3';
const formConfig: SmartContractVerificationConfig = {
solidity_compiler_versions: [
'v0.8.17+commit.8df45f5f',
'v0.8.16+commit.07a7930e',
'v0.8.15+commit.e14f2714',
'v0.8.18-nightly.2022.11.23+commit.eb2f874e',
'v0.8.17-nightly.2022.8.24+commit.22a0c46e',
'v0.8.16-nightly.2022.7.6+commit.b6f11b33',
],
solidity_evm_versions: [
'default',
'london',
'berlin',
],
verification_options: [
'flattened-code',
'standard-input',
'sourcify',
'multi-part',
'vyper-code',
],
vyper_compiler_versions: [
'v0.3.7+commit.6020b8bb',
'v0.3.1+commit.0463ea4c',
'v0.3.0+commit.8a23feb',
'v0.2.16+commit.59e1bdd',
'v0.2.3+commit.006968f',
'v0.2.2+commit.337c2ef',
'v0.1.0-beta.17+commit.0671b7b',
],
vyper_evm_versions: [
'byzantium',
'constantinople',
'petersburg',
'istanbul',
],
};
test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) => { test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -30,7 +71,7 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) = ...@@ -30,7 +71,7 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) =
test('standard input json method', async({ mount, page }) => { test('standard input json method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -43,7 +84,7 @@ test('standard input json method', async({ mount, page }) => { ...@@ -43,7 +84,7 @@ test('standard input json method', async({ mount, page }) => {
test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -62,7 +103,7 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -62,7 +103,7 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
test('multi-part files method', async({ mount, page }) => { test('multi-part files method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -75,7 +116,7 @@ test('multi-part files method', async({ mount, page }) => { ...@@ -75,7 +116,7 @@ test('multi-part files method', async({ mount, page }) => {
test('vyper contract method', async({ mount, page }) => { test('vyper contract method', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractVerificationForm/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
...@@ -4,54 +4,139 @@ import React from 'react'; ...@@ -4,54 +4,139 @@ import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { FormFields, VerificationMethod } from './types'; import type { FormFields } from './types';
import type { SocketMessage } from 'lib/socket/types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import delay from 'lib/delay'; import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ContractVerificationFieldMethod, { VERIFICATION_METHODS } from './fields/ContractVerificationFieldMethod'; import ContractVerificationFieldMethod from './fields/ContractVerificationFieldMethod';
import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode'; import ContractVerificationFlattenSourceCode from './methods/ContractVerificationFlattenSourceCode';
import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile'; import ContractVerificationMultiPartFile from './methods/ContractVerificationMultiPartFile';
import ContractVerificationSourcify from './methods/ContractVerificationSourcify'; import ContractVerificationSourcify from './methods/ContractVerificationSourcify';
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput'; import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract'; import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors } from './utils';
const METHODS = { const METHOD_COMPONENTS = {
flatten_source_code: <ContractVerificationFlattenSourceCode/>, 'flattened-code': <ContractVerificationFlattenSourceCode/>,
standard_input: <ContractVerificationStandardInput/>, 'standard-input': <ContractVerificationStandardInput/>,
sourcify: <ContractVerificationSourcify/>, sourcify: <ContractVerificationSourcify/>,
multi_part_file: <ContractVerificationMultiPartFile/>, 'multi-part': <ContractVerificationMultiPartFile/>,
vyper_contract: <ContractVerificationVyperContract/>, 'vyper-code': <ContractVerificationVyperContract/>,
'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
}; };
const ContractVerificationForm = () => { interface Props {
const router = useRouter(); method?: SmartContractVerificationMethod;
const methodFromQuery = router.query.method?.toString() as VerificationMethod; config: SmartContractVerificationConfig;
hash: string;
}
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: { defaultValues: {
method: VERIFICATION_METHODS.includes(methodFromQuery) ? methodFromQuery : undefined, method: methodFromQuery,
}, },
}); });
const { control, handleSubmit, watch, formState } = formApi; const { control, handleSubmit, watch, formState, setError } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const apiFetch = useApiFetch();
const toast = useToast();
const router = useRouter();
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => { const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
// eslint-disable-next-line no-console const body = prepareRequestBody(data);
console.log('__>__', data);
await delay(5_000);
}, []);
const method = watch('method'); try {
await apiFetch('contract_verification_via', {
pathParams: { method: data.method, hash },
fetchParams: {
method: 'POST',
body,
},
});
} catch (error) {
return;
}
return new Promise((resolve) => {
submitPromiseResolver.current = resolve;
});
}, [ apiFetch, hash ]);
const handleNewSocketMessage: SocketMessage.ContractVerification['handler'] = React.useCallback((payload) => {
if (payload.status === 'error') {
const errors = formatSocketErrors(payload.errors);
errors.forEach(([ field, error ]) => setError(field, error));
submitPromiseResolver.current?.(null);
return;
}
toast({
position: 'top-right',
title: 'Success',
description: 'Contract is successfully verified.',
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { shallow: true });
},
});
}, [ hash, router, setError, toast ]);
const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) {
return;
}
const content = METHODS[method] || null; submitPromiseResolver.current?.(null);
const toastId = 'socket-error';
!toast.isActive(toastId) && toast({
id: toastId,
position: 'top-right',
title: 'Error',
description: 'There was an error with socket connection. Try again later.',
status: 'error',
variant: 'subtle',
isClosable: true,
});
}, [ formState.isSubmitting, toast ]);
const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: false,
});
useSocketMessage({
channel,
event: 'verification_result',
handler: handleNewSocketMessage,
});
const method = watch('method');
const content = METHOD_COMPONENTS[method] || null;
return ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
<chakra.form <chakra.form
noValidate noValidate
onSubmit={ handleSubmit(onFormSubmit) } onSubmit={ handleSubmit(onFormSubmit) }
mt={ 12 }
> >
<ContractVerificationFieldMethod control={ control } isDisabled={ Boolean(method) }/> <ContractVerificationFieldMethod
control={ control }
isDisabled={ Boolean(method) }
methods={ config.verification_options }
/>
{ content } { content }
{ Boolean(method) && ( { Boolean(method) && (
<Button <Button
......
...@@ -7,27 +7,39 @@ import type { FormFields } from '../types'; ...@@ -7,27 +7,39 @@ import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput'; import CheckboxInput from 'ui/shared/CheckboxInput';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationFieldConstructorArgs from './ContractVerificationFieldConstructorArgs';
const ContractVerificationFieldConstArgs = () => { const ContractVerificationFieldAutodetectArgs = () => {
const { formState, control } = useFormContext<FormFields>(); const [ isOn, setIsOn ] = React.useState(true);
const { formState, control, resetField } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'constructor_args'>}) => ( const handleCheckboxChange = React.useCallback(() => {
<CheckboxInput<FormFields, 'constructor_args'> !isOn && resetField('constructor_args');
setIsOn(prev => !prev);
}, [ isOn, resetField ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'autodetect_constructor_args'>}) => (
<CheckboxInput<FormFields, 'autodetect_constructor_args'>
text="Try to fetch constructor arguments automatically" text="Try to fetch constructor arguments automatically"
field={ field } field={ field }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
onChange={ handleCheckboxChange }
/> />
), [ formState.isSubmitting ]); ), [ formState.isSubmitting, handleCheckboxChange ]);
return ( return (
<>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <Controller
name="constructor_args" name="autodetect_constructor_args"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
defaultValue={ true }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
{ !isOn && <ContractVerificationFieldConstructorArgs/> }
</>
); );
}; };
export default React.memo(ContractVerificationFieldConstArgs); export default React.memo(ContractVerificationFieldAutodetectArgs);
...@@ -5,6 +5,7 @@ import { useFormContext, Controller } from 'react-hook-form'; ...@@ -5,6 +5,7 @@ import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -27,7 +28,8 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => { ...@@ -27,7 +28,8 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => {
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
required required
/> />
<InputPlaceholder text="Contract code" error={ error }/> <InputPlaceholder text="Contract code"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl> </FormControl>
); );
}, [ formState.errors, formState.isSubmitting ]); }, [ formState.errors, formState.isSubmitting ]);
......
import { Code, Checkbox } from '@chakra-ui/react'; import { Code } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form'; import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect'; import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const COMPILERS = [ const OPTIONS_LIMIT = 50;
'v0.8.17+commit.8df45f5f',
'v0.8.16+commit.07a7930e',
'v0.8.15+commit.e14f2714',
];
const COMPILERS_NIGHTLY = [
'v0.8.18-nightly.2022.11.23+commit.eb2f874e',
'v0.8.17-nightly.2022.8.24+commit.22a0c46e',
'v0.8.16-nightly.2022.7.6+commit.b6f11b33',
];
interface Props { interface Props {
isVyper?: boolean; isVyper?: boolean;
} }
const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
const [ isNightly, setIsNightly ] = React.useState(false); const { formState, control } = useFormContext<FormFields>();
const { formState, control, getValues, resetField } = useFormContext<FormFields>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const options = React.useMemo(() => ( const options = React.useMemo(() => (
[ (isVyper ? config?.vyper_compiler_versions : config?.solidity_compiler_versions)?.map((option) => ({ label: option, value: option })) || []
...COMPILERS, ...(isNightly ? COMPILERS_NIGHTLY : []), ), [ config?.solidity_compiler_versions, config?.vyper_compiler_versions, isVyper ]);
].map((option) => ({ label: option, value: option }))
), [ isNightly ]);
const handleCheckboxChange = React.useCallback(() => { const loadOptions = React.useCallback(async(inputValue: string) => {
if (isNightly) { return options
const field = getValues('compiler'); .filter(({ label }) => {
field.value.includes('nightly') && resetField('compiler', { defaultValue: null }); if (!inputValue) {
return !label.toLowerCase().includes('nightly');
} }
setIsNightly(prev => !prev);
}, [ getValues, isNightly, resetField ]); return label.toLowerCase().includes(inputValue.toLowerCase());
})
.slice(0, OPTIONS_LIMIT);
}, [ options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => {
const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined; const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined;
...@@ -51,36 +47,26 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { ...@@ -51,36 +47,26 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
return ( return (
<FancySelect <FancySelect
{ ...field } { ...field }
options={ options } loadOptions={ loadOptions }
defaultOptions
size={ isMobile ? 'md' : 'lg' } size={ isMobile ? 'md' : 'lg' }
placeholder="Compiler" placeholder="Compiler"
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
error={ error } error={ error }
isRequired isRequired
isAsync
/> />
); );
}, [ formState.errors, formState.isSubmitting, isMobile, options ]); }, [ formState.errors, formState.isSubmitting, isMobile, loadOptions ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<>
<Controller <Controller
name="compiler" name="compiler"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }} rules={{ required: true }}
/> />
{ !isVyper && (
<Checkbox
size="lg"
mt={ 3 }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
>
Include nightly builds
</Checkbox>
) }
</>
{ isVyper ? null : ( { isVyper ? null : (
<> <>
<span>The compiler version is specified in </span> <span>The compiler version is specified in </span>
......
...@@ -5,32 +5,39 @@ import { Controller, useFormContext } from 'react-hook-form'; ...@@ -5,32 +5,39 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import FieldError from 'ui/shared/forms/FieldError';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldAbiEncodedArgs = () => { const ContractVerificationFieldConstructorArgs = () => {
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'abi_encoded_args'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'constructor_args'>}) => {
const error = 'constructor_args' in formState.errors ? formState.errors.constructor_args : undefined;
return ( return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }}> <FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Textarea <Textarea
{ ...field } { ...field }
maxLength={ 255 } maxLength={ 255 }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
isInvalid={ Boolean(error) }
required
/> />
<InputPlaceholder text="ABI-encoded Constructor Arguments"/> <InputPlaceholder text="ABI-encoded Constructor Arguments"/>
{ error?.message && <FieldError message={ error?.message }/> }
</FormControl> </FormControl>
); );
}, [ formState.isSubmitting ]); }, [ formState.errors, formState.isSubmitting ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <Controller
name="abi_encoded_args" name="constructor_args"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }}
/> />
<> <>
<span>Add arguments in </span> <span>Add arguments in </span>
...@@ -44,4 +51,4 @@ const ContractVerificationFieldAbiEncodedArgs = () => { ...@@ -44,4 +51,4 @@ const ContractVerificationFieldAbiEncodedArgs = () => {
); );
}; };
export default React.memo(ContractVerificationFieldAbiEncodedArgs); export default React.memo(ContractVerificationFieldConstructorArgs);
import { Link } from '@chakra-ui/react'; import { Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form'; import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect'; import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const VERSIONS = [ interface Props {
'default', isVyper?: boolean;
'london', }
'berlin',
];
const ContractVerificationFieldEvmVersion = () => { const ContractVerificationFieldEvmVersion = ({ isVyper }: Props) => {
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const options = React.useMemo(() => ( const options = React.useMemo(() => (
VERSIONS.map((option) => ({ label: option, value: option })) (isVyper ? config?.vyper_evm_versions : config?.solidity_evm_versions)?.map((option) => ({ label: option, value: option })) || []
), [ ]); ), [ config?.solidity_evm_versions, config?.vyper_evm_versions, isVyper ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'evm_version'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'evm_version'>}) => {
const error = 'evm_version' in formState.errors ? formState.errors.evm_version : undefined; const error = 'evm_version' in formState.errors ? formState.errors.evm_version : undefined;
......
...@@ -20,32 +20,26 @@ import React from 'react'; ...@@ -20,32 +20,26 @@ import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form'; import type { ControllerRenderProps, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import type { FormFields, VerificationMethod } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract';
import infoIcon from 'icons/info.svg'; import infoIcon from 'icons/info.svg';
export const VERIFICATION_METHODS: Array<VerificationMethod> = [
'flatten_source_code',
'standard_input',
'sourcify',
'multi_part_file',
'vyper_contract',
];
interface Props { interface Props {
control: Control<FormFields>; control: Control<FormFields>;
isDisabled?: boolean; isDisabled?: boolean;
methods: SmartContractVerificationConfig['verification_options'];
} }
const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => { const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => {
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(); const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean();
const tooltipBg = useColorModeValue('gray.700', 'gray.900'); const tooltipBg = useColorModeValue('gray.700', 'gray.900');
const renderItem = React.useCallback((method: VerificationMethod) => { const renderItem = React.useCallback((method: SmartContractVerificationMethod) => {
switch (method) { switch (method) {
case 'flatten_source_code': case 'flattened-code':
return 'Via flattened source code'; return 'Via flattened source code';
case 'standard_input': case 'standard-input':
return ( return (
<> <>
<span>Via standard </span> <span>Via standard </span>
...@@ -91,10 +85,12 @@ const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => { ...@@ -91,10 +85,12 @@ const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => {
</Popover> </Popover>
</> </>
); );
case 'multi_part_file': case 'multi-part':
return 'Via multi-part files'; return 'Via multi-part files';
case 'vyper_contract': case 'vyper-code':
return 'Vyper contract'; return 'Vyper contract';
case 'vyper-multi-part':
return 'Via multi-part Vyper files';
default: default:
break; break;
...@@ -103,15 +99,15 @@ const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => { ...@@ -103,15 +99,15 @@ const ContractVerificationFieldMethod = ({ control, isDisabled }: Props) => {
const renderRadioGroup = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => { const renderRadioGroup = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
return ( return (
<RadioGroup defaultValue="add" colorScheme="blue" isDisabled={ isDisabled } { ...field }> <RadioGroup defaultValue="add" colorScheme="blue" isDisabled={ isDisabled } isFocusable={ !isDisabled } { ...field } >
<Stack spacing={ 4 }> <Stack spacing={ 4 }>
{ VERIFICATION_METHODS.map((method) => { { methods.map((method) => {
return <Radio key={ method } value={ method } size="lg">{ renderItem(method) }</Radio>; return <Radio key={ method } value={ method } size="lg">{ renderItem(method) }</Radio>;
}) } }) }
</Stack> </Stack>
</RadioGroup> </RadioGroup>
); );
}, [ isDisabled, renderItem ]); }, [ isDisabled, methods, renderItem ]);
return ( return (
<section> <section>
......
...@@ -11,7 +11,7 @@ import InputPlaceholder from 'ui/shared/InputPlaceholder'; ...@@ -11,7 +11,7 @@ import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
const ContractVerificationFieldOptimization = () => { const ContractVerificationFieldOptimization = () => {
const [ isEnabled, setIsEnabled ] = React.useState(false); const [ isEnabled, setIsEnabled ] = React.useState(true);
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const handleCheckboxChange = React.useCallback(() => { const handleCheckboxChange = React.useCallback(() => {
...@@ -49,6 +49,7 @@ const ContractVerificationFieldOptimization = () => { ...@@ -49,6 +49,7 @@ const ContractVerificationFieldOptimization = () => {
name="is_optimization_enabled" name="is_optimization_enabled"
control={ control } control={ control }
render={ renderCheckboxControl } render={ renderCheckboxControl }
defaultValue={ true }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
{ isEnabled && ( { isEnabled && (
...@@ -58,7 +59,7 @@ const ContractVerificationFieldOptimization = () => { ...@@ -58,7 +59,7 @@ const ContractVerificationFieldOptimization = () => {
control={ control } control={ control }
render={ renderInputControl } render={ renderInputControl }
rules={{ required: true }} rules={{ required: true }}
defaultValue="" defaultValue="200"
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
) } ) }
......
import { Text, Button, Box, chakra } from '@chakra-ui/react'; import { Text, Button, Box, chakra, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { Mb } from 'lib/consts';
// import DragAndDropArea from 'ui/shared/forms/DragAndDropArea';
import FieldError from 'ui/shared/forms/FieldError';
import FileInput from 'ui/shared/forms/FileInput'; import FileInput from 'ui/shared/forms/FileInput';
import FileSnippet from 'ui/shared/forms/FileSnippet'; import FileSnippet from 'ui/shared/forms/FileSnippet';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
type FileTypes = '.sol' | '.yul' | '.json' | '.vy'
interface Props { interface Props {
accept?: string; fileTypes: Array<FileTypes>;
multiple?: boolean; multiple?: boolean;
title: string; title: string;
className?: string; className?: string;
hint: string; hint: string;
} }
const ContractVerificationFieldSources = ({ accept, multiple, title, className, hint }: Props) => { const ContractVerificationFieldSources = ({ fileTypes, multiple, title, className, hint }: Props) => {
const { setValue, getValues, control, formState } = useFormContext<FormFields>(); const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>();
const error = 'sources' in formState.errors ? formState.errors.sources : undefined; const error = 'sources' in formState.errors ? formState.errors.sources : undefined;
const commonError = !error?.type?.startsWith('file_') ? error : undefined;
const fileError = error?.type?.startsWith('file_') ? error : undefined;
const handleFileRemove = React.useCallback((index?: number) => { const handleFileRemove = React.useCallback((index?: number) => {
if (index === undefined) { if (index === undefined) {
...@@ -31,41 +38,89 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className, ...@@ -31,41 +38,89 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className,
const value = getValues('sources').slice(); const value = getValues('sources').slice();
value.splice(index, 1); value.splice(index, 1);
setValue('sources', value); setValue('sources', value);
clearErrors('sources');
}, [ getValues, setValue ]); }, [ getValues, clearErrors, setValue ]);
const renderFiles = React.useCallback((files: Array<File>) => { const renderFiles = React.useCallback((files: Array<File>) => {
const errorList = fileError?.message?.split(';');
return ( return (
<Box display="grid" gridTemplateColumns={{ base: '1fr', lg: '1fr 1fr' }} columnGap={ 3 } rowGap={ 3 }> <Box display="grid" gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }} columnGap={ 3 } rowGap={ 3 }>
{ files.map((file, index) => ( { files.map((file, index) => (
<Box key={ file.name + file.lastModified + index }>
<FileSnippet <FileSnippet
key={ file.name + file.lastModified }
file={ file } file={ file }
maxW="initial" maxW="initial"
onRemove={ handleFileRemove } onRemove={ handleFileRemove }
index={ index } index={ index }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
/> />
{ errorList?.[index] && <FieldError message={ errorList?.[index] } mt={ 1 } px={ 3 }/> }
</Box>
)) } )) }
</Box> </Box>
); );
}, [ formState.isSubmitting, handleFileRemove ]); }, [ formState.isSubmitting, handleFileRemove, fileError ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => ( const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => (
<> <>
<FileInput<FormFields, 'sources'> accept={ accept } multiple={ multiple } field={ field }> <FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
<Button variant="outline" size="sm" display={ field.value && field.value.length > 0 ? 'none' : 'block' }> { () => (
<Flex
flexDir="column"
alignItems="flex-start"
rowGap={ 2 }
w="100%"
display={ field.value && field.value.length > 0 && !multiple ? 'none' : 'block' }
mb={ field.value && field.value.length > 0 ? 2 : 0 }
>
<Button
variant="outline"
size="sm"
// mb={ 2 }
>
Upload file{ multiple ? 's' : '' } Upload file{ multiple ? 's' : '' }
</Button> </Button>
{ /* design is not ready */ }
{ /* <DragAndDropArea onDrop={ onChange }/> */ }
</Flex>
) }
</FileInput> </FileInput>
{ field.value && field.value.length > 0 && renderFiles(field.value) } { field.value && field.value.length > 0 && renderFiles(field.value) }
{ error && ( { commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
<Box fontSize="sm" mt={ 2 } color="error">
{ error.type === 'required' ? 'Field is required' : error.message }
</Box>
) }
</> </>
), [ accept, error, multiple, renderFiles ]); ), [ fileTypes, commonError, multiple, renderFiles ]);
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (Array.isArray(value)) {
const errorText = `Wrong file type. Allowed files types are ${ fileTypes.join(',') }.`;
const errors = value.map(({ name }) => fileTypes.some((ext) => name.endsWith(ext)) ? '' : errorText);
if (errors.some((item) => item !== '')) {
return errors.join(';');
}
}
return true;
}, [ fileTypes ]);
const validateFileSize = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (Array.isArray(value)) {
const FILE_SIZE_LIMIT = 20 * Mb;
const errors = value.map(({ size }) => size > FILE_SIZE_LIMIT ? 'File is too big. Maximum size is 20 Mb.' : '');
if (errors.some((item) => item !== '')) {
return errors.join(';');
}
}
return true;
}, []);
const rules = React.useMemo(() => ({
required: true,
validate: {
file_type: validateFileType,
file_size: validateFileSize,
},
}), [ validateFileSize, validateFileType ]);
return ( return (
<> <>
...@@ -77,7 +132,7 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className, ...@@ -77,7 +132,7 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className,
name="sources" name="sources"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }} rules={ rules }
/> />
{ hint ? <span>{ hint }</span> : null } { hint ? <span>{ hint }</span> : null }
</ContractVerificationFormRow> </ContractVerificationFormRow>
......
import React from 'react'; import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode'; import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler'; import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstArgs from '../fields/ContractVerificationFieldConstArgs';
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion'; import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldIsYul from '../fields/ContractVerificationFieldIsYul'; import ContractVerificationFieldIsYul from '../fields/ContractVerificationFieldIsYul';
import ContractVerificationFieldLibraries from '../fields/ContractVerificationFieldLibraries'; import ContractVerificationFieldLibraries from '../fields/ContractVerificationFieldLibraries';
...@@ -19,7 +19,7 @@ const ContractVerificationFlattenSourceCode = () => { ...@@ -19,7 +19,7 @@ const ContractVerificationFlattenSourceCode = () => {
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
<ContractVerificationFieldCode/> <ContractVerificationFieldCode/>
<ContractVerificationFieldConstArgs/> <ContractVerificationFieldAutodetectArgs/>
<ContractVerificationFieldLibraries/> <ContractVerificationFieldLibraries/>
</ContractVerificationMethod> </ContractVerificationMethod>
); );
......
...@@ -7,6 +7,8 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi ...@@ -7,6 +7,8 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi
import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization'; import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources'; import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.sol' as const, '.yul' as const ];
const ContractVerificationMultiPartFile = () => { const ContractVerificationMultiPartFile = () => {
return ( return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification"> <ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification">
...@@ -14,7 +16,7 @@ const ContractVerificationMultiPartFile = () => { ...@@ -14,7 +16,7 @@ const ContractVerificationMultiPartFile = () => {
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
<ContractVerificationFieldSources <ContractVerificationFieldSources
accept=".sol,.yul" fileTypes={ FILE_TYPES }
multiple multiple
title="Sources *.sol or *.yul files" title="Sources *.sol or *.yul files"
hint="Upload all Solidity or Yul contract source files." hint="Upload all Solidity or Yul contract source files."
......
...@@ -3,11 +3,13 @@ import React from 'react'; ...@@ -3,11 +3,13 @@ import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources'; import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const, '.sol' as const ];
const ContractVerificationSourcify = () => { const ContractVerificationSourcify = () => {
return ( return (
<ContractVerificationMethod title="New Smart Contract Verification"> <ContractVerificationMethod title="New Smart Contract Verification">
<ContractVerificationFieldSources <ContractVerificationFieldSources
accept=".json" fileTypes={ FILE_TYPES }
multiple multiple
title="Sources and Metadata JSON" mt={ 0 } title="Sources and Metadata JSON" mt={ 0 }
hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation." hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation."
......
import React from 'react'; import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler'; import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstArgs from '../fields/ContractVerificationFieldConstArgs';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName'; import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources'; import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const ];
const ContractVerificationStandardInput = () => { const ContractVerificationStandardInput = () => {
return ( return (
<ContractVerificationMethod title="New Smart Contract Verification"> <ContractVerificationMethod title="New Smart Contract Verification">
<ContractVerificationFieldName/> <ContractVerificationFieldName/>
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldSources <ContractVerificationFieldSources
accept=".json" fileTypes={ FILE_TYPES }
title="Standard Input JSON" title="Standard Input JSON"
hint="Upload the standard input JSON file created during contract compilation." hint="Upload the standard input JSON file created during contract compilation."
/> />
<ContractVerificationFieldConstArgs/> <ContractVerificationFieldAutodetectArgs/>
</ContractVerificationMethod> </ContractVerificationMethod>
); );
}; };
......
import React from 'react'; import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAbiEncodedArgs from '../fields/ContractVerificationFieldAbiEncodedArgs';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode'; import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler'; import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstructorArgs from '../fields/ContractVerificationFieldConstructorArgs';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName'; import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
const ContractVerificationVyperContract = () => { const ContractVerificationVyperContract = () => {
...@@ -12,7 +12,7 @@ const ContractVerificationVyperContract = () => { ...@@ -12,7 +12,7 @@ const ContractVerificationVyperContract = () => {
<ContractVerificationFieldName hint="Must match the name specified in the code."/> <ContractVerificationFieldName hint="Must match the name specified in the code."/>
<ContractVerificationFieldCompiler isVyper/> <ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldCode isVyper/> <ContractVerificationFieldCode isVyper/>
<ContractVerificationFieldAbiEncodedArgs/> <ContractVerificationFieldConstructorArgs/>
</ContractVerificationMethod> </ContractVerificationMethod>
); );
}; };
......
import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.vy' as const ];
const ContractVerificationVyperMultiPartFile = () => {
return (
<ContractVerificationMethod title="New Vyper Smart Contract Verification">
<ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldEvmVersion isVyper/>
<ContractVerificationFieldSources
fileTypes={ FILE_TYPES }
multiple
title="Sources *.vy files"
hint="Upload all Vyper contract source files."
/>
</ContractVerificationMethod>
);
};
export default React.memo(ContractVerificationVyperMultiPartFile);
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/FancySelect/types';
export type VerificationMethod = 'flatten_source_code' | 'standard_input' | 'sourcify' | 'multi_part_file' | 'vyper_contract'
export interface ContractLibrary { export interface ContractLibrary {
name: string; name: string;
address: string; address: string;
} }
export interface FormFieldsFlattenSourceCode { export interface FormFieldsFlattenSourceCode {
method: 'flatten_source_code'; method: 'flattened-code';
is_yul: boolean; is_yul: boolean;
name: string; name: string;
compiler: Option; compiler: Option;
...@@ -15,15 +13,18 @@ export interface FormFieldsFlattenSourceCode { ...@@ -15,15 +13,18 @@ export interface FormFieldsFlattenSourceCode {
is_optimization_enabled: boolean; is_optimization_enabled: boolean;
optimization_runs: string; optimization_runs: string;
code: string; code: string;
constructor_args: boolean; autodetect_constructor_args: boolean;
constructor_args: string;
libraries: Array<ContractLibrary>; libraries: Array<ContractLibrary>;
} }
export interface FormFieldsStandardInput { export interface FormFieldsStandardInput {
method: 'standard_input'; method: 'standard-input';
name: string; name: string;
compiler: Option; compiler: Option;
sources: Array<File>; sources: Array<File>;
autodetect_constructor_args: boolean;
constructor_args: string;
} }
export interface FormFieldsSourcify { export interface FormFieldsSourcify {
...@@ -32,21 +33,29 @@ export interface FormFieldsSourcify { ...@@ -32,21 +33,29 @@ export interface FormFieldsSourcify {
} }
export interface FormFieldsMultiPartFile { export interface FormFieldsMultiPartFile {
method: 'multi_part_file'; method: 'multi-part';
compiler: Option; compiler: Option;
evm_version: Option; evm_version: Option;
is_optimization_enabled: boolean; is_optimization_enabled: boolean;
optimization_runs: string; optimization_runs: string;
sources: Array<File>; sources: Array<File>;
libraries: Array<ContractLibrary>;
} }
export interface FormFieldsVyperContract { export interface FormFieldsVyperContract {
method: 'vyper_contract'; method: 'vyper-code';
name: string; name: string;
compiler: Option; compiler: Option;
code: string; code: string;
abi_encoded_args: string; constructor_args: string;
}
export interface FormFieldsVyperMultiPartFile {
method: 'vyper-multi-part';
compiler: Option;
evm_version: Option;
sources: Array<File>;
} }
export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify | export type FormFields = FormFieldsFlattenSourceCode | FormFieldsStandardInput | FormFieldsSourcify |
FormFieldsMultiPartFile | FormFieldsVyperContract; FormFieldsMultiPartFile | FormFieldsVyperContract | FormFieldsVyperMultiPartFile;
import type { FieldPath, ErrorOption } from 'react-hook-form';
import type { ContractLibrary, FormFields } from './types';
import type { SmartContractVerificationMethod, SmartContractVerificationError } from 'types/api/contract';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMethod> = [
'flattened-code',
'standard-input',
'sourcify',
'multi-part',
'vyper-code',
'vyper-multi-part',
];
export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod {
return method && SUPPORTED_VERIFICATION_METHODS.includes(method as SmartContractVerificationMethod) ? true : false;
}
export function sortVerificationMethods(methodA: SmartContractVerificationMethod, methodB: SmartContractVerificationMethod) {
const indexA = SUPPORTED_VERIFICATION_METHODS.indexOf(methodA);
const indexB = SUPPORTED_VERIFICATION_METHODS.indexOf(methodB);
if (indexA > indexB) {
return 1;
}
if (indexA < indexB) {
return -1;
}
return 0;
}
export function prepareRequestBody(data: FormFields): FetchParams['body'] {
switch (data.method) {
case 'flattened-code': {
return {
compiler_version: data.compiler?.value,
source_code: data.code,
is_optimization_enabled: data.is_optimization_enabled,
is_yul_contract: data.is_yul,
optimization_runs: data.optimization_runs,
contract_name: data.name,
libraries: reduceLibrariesArray(data.libraries),
evm_version: data.evm_version?.value,
autodetect_constructor_args: data.autodetect_constructor_args,
constructor_args: data.constructor_args,
};
}
case 'standard-input': {
const body = new FormData();
body.set('compiler_version', data.compiler?.value);
body.set('contract_name', data.name);
body.set('autodetect_constructor_args', String(Boolean(data.autodetect_constructor_args)));
body.set('constructor_args', data.constructor_args);
addFilesToFormData(body, data.sources);
return body;
}
case 'sourcify': {
const body = new FormData();
addFilesToFormData(body, data.sources);
return body;
}
case 'multi-part': {
const body = new FormData();
body.set('compiler_version', data.compiler?.value);
body.set('evm_version', data.evm_version?.value);
body.set('is_optimization_enabled', String(Boolean(data.is_optimization_enabled)));
data.is_optimization_enabled && body.set('optimization_runs', data.optimization_runs);
const libraries = reduceLibrariesArray(data.libraries);
libraries && body.set('libraries', JSON.stringify(libraries));
addFilesToFormData(body, data.sources);
return body;
}
case 'vyper-code': {
return {
compiler_version: data.compiler?.value,
source_code: data.code,
contract_name: data.name,
constructor_args: data.constructor_args,
};
}
case 'vyper-multi-part': {
const body = new FormData();
body.set('compiler_version', data.compiler?.value);
body.set('evm_version', data.evm_version?.value);
addFilesToFormData(body, data.sources);
return body;
}
default: {
return {};
}
}
}
function reduceLibrariesArray(libraries: Array<ContractLibrary> | undefined) {
if (!libraries || libraries.length === 0) {
return;
}
return libraries.reduce<Record<string, string>>((result, item) => {
result[item.name] = item.address;
return result;
}, {});
}
function addFilesToFormData(body: FormData, files: Array<File> | undefined) {
if (!files) {
return;
}
for (let index = 0; index < files.length; index++) {
const file = files[index];
body.set(`files[${ index }]`, file, file.name);
}
}
const API_ERROR_TO_FORM_FIELD: Record<keyof SmartContractVerificationError, FieldPath<FormFields>> = {
contract_source_code: 'code',
files: 'sources',
compiler_version: 'compiler',
constructor_arguments: 'constructor_args',
name: 'name',
};
export function formatSocketErrors(errors: SmartContractVerificationError): Array<[FieldPath<FormFields>, ErrorOption]> {
return Object.entries(errors).map(([ key, value ]) => {
return [ API_ERROR_TO_FORM_FIELD[key as keyof SmartContractVerificationError], { message: value.join(',') } ];
});
}
import { Box, Heading, Flex, Text, VStack, Skeleton } from '@chakra-ui/react'; import { Box, Heading, Flex, Text, VStack, Skeleton } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
...@@ -9,7 +10,6 @@ import type { Block } from 'types/api/block'; ...@@ -9,7 +10,6 @@ import type { Block } from 'types/api/block';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -98,7 +98,7 @@ const LatestBlocks = () => { ...@@ -98,7 +98,7 @@ const LatestBlocks = () => {
</AnimatePresence> </AnimatePresence>
</VStack> </VStack>
<Flex justifyContent="center"> <Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ link('blocks') }>View all blocks</LinkInternal> <LinkInternal fontSize="sm" href={ route({ pathname: '/blocks' }) }>View all blocks</LinkInternal>
</Flex> </Flex>
</> </>
); );
......
...@@ -8,13 +8,13 @@ import { ...@@ -8,13 +8,13 @@ import {
Text, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import link from 'lib/link/link';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -44,7 +44,7 @@ const LatestBlocksItem = ({ block, h }: Props) => { ...@@ -44,7 +44,7 @@ const LatestBlocksItem = ({ block, h }: Props) => {
<HStack spacing={ 2 }> <HStack spacing={ 2 }>
<Icon as={ blockIcon } boxSize="30px" color="link"/> <Icon as={ blockIcon } boxSize="30px" color="link"/>
<LinkInternal <LinkInternal
href={ link('block', { id: String(block.height) }) } href={ route({ pathname: '/block/[height]', query: { height: String(block.height) } }) }
fontSize="xl" fontSize="xl"
fontWeight="500" fontWeight="500"
> >
......
import { test as base, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { ROUTES } from 'lib/link/routes';
import * as statsMock from 'mocks/stats/index'; import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx'; import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
...@@ -42,7 +41,7 @@ test.describe('socket', () => { ...@@ -42,7 +41,7 @@ test.describe('socket', () => {
const hooksConfig = { const hooksConfig = {
router: { router: {
pathname: ROUTES.network_index.pattern, pathname: '/',
query: {}, query: {},
}, },
}; };
......
import { Box, Heading, Flex, Text, Skeleton } from '@chakra-ui/react'; import { Box, Heading, Flex, Text, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket'; import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
...@@ -34,7 +34,7 @@ const LatestTransactions = () => { ...@@ -34,7 +34,7 @@ const LatestTransactions = () => {
} }
if (data) { if (data) {
const txsUrl = link('txs'); const txsUrl = route({ pathname: '/txs' });
content = ( content = (
<> <>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/> <SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/>
......
import { Grid } from '@chakra-ui/react'; import { Grid } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -8,7 +9,6 @@ import gasIcon from 'icons/gas.svg'; ...@@ -8,7 +9,6 @@ import gasIcon from 'icons/gas.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link';
import StatsGasPrices from './StatsGasPrices'; import StatsGasPrices from './StatsGasPrices';
import StatsItem from './StatsItem'; import StatsItem from './StatsItem';
...@@ -45,7 +45,7 @@ const Stats = () => { ...@@ -45,7 +45,7 @@ const Stats = () => {
icon={ blockIcon } icon={ blockIcon }
title="Total blocks" title="Total blocks"
value={ Number(data.total_blocks).toLocaleString() } value={ Number(data.total_blocks).toLocaleString() }
url={ link('blocks') } url={ route({ pathname: '/blocks' }) }
/> />
{ hasAvgBlockTime && ( { hasAvgBlockTime && (
<StatsItem <StatsItem
...@@ -58,7 +58,7 @@ const Stats = () => { ...@@ -58,7 +58,7 @@ const Stats = () => {
icon={ txIcon } icon={ txIcon }
title="Total transactions" title="Total transactions"
value={ Number(data.total_transactions).toLocaleString() } value={ Number(data.total_transactions).toLocaleString() }
url={ link('txs') } url={ route({ pathname: '/txs' }) }
/> />
<StatsItem <StatsItem
icon={ walletIcon } icon={ walletIcon }
......
import { Box, Flex, Icon, Text, useColorModeValue, chakra, Tooltip, LightMode } from '@chakra-ui/react'; import type { TooltipProps } from '@chakra-ui/react';
import { Flex, Icon, Text, useColorModeValue, chakra, LightMode } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import infoIcon from 'icons/info.svg';
import breakpoints from 'theme/foundations/breakpoints'; import breakpoints from 'theme/foundations/breakpoints';
import Hint from 'ui/shared/Hint';
type Props = { type Props = {
icon: React.FC<React.SVGAttributes<SVGElement>>; icon: React.FC<React.SVGAttributes<SVGElement>>;
...@@ -15,6 +16,14 @@ type Props = { ...@@ -15,6 +16,14 @@ type Props = {
const LARGEST_BREAKPOINT = '1240px'; const LARGEST_BREAKPOINT = '1240px';
const TOOLTIP_PROPS: Partial<TooltipProps> = {
hasArrow: false,
borderRadius: 'md',
placement: 'bottom-end',
offset: [ 0, 0 ],
bgColor: 'blackAlpha.900',
};
const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) => { const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const sxContainer = {} as any; const sxContainer = {} as any;
...@@ -55,20 +64,15 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) ...@@ -55,20 +64,15 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props)
</Flex> </Flex>
{ tooltipLabel && ( { tooltipLabel && (
<LightMode> <LightMode>
<Tooltip label={ tooltipLabel } hasArrow={ false } borderRadius="12px" placement="bottom-end" offset={ [ 0, 0 ] } bgColor="blackAlpha.900"> <Hint
<Box label={ tooltipLabel }
tooltipProps={ TOOLTIP_PROPS }
boxSize={ 6 }
color={ infoColor }
position="absolute" position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }} top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
right="10px" right="10px"
cursor="pointer"
>
<Icon
as={ infoIcon }
boxSize={ 6 }
color={ infoColor }
/> />
</Box>
</Tooltip>
</LightMode> </LightMode>
) } ) }
</Flex> </Flex>
......
import { Box, Flex, Icon, Skeleton, Text, Tooltip, useColorModeValue } from '@chakra-ui/react'; import { Flex, Skeleton, Text, 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';
import infoIcon from 'icons/info.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import Hint from 'ui/shared/Hint';
import ChainIndicatorChartContainer from './ChainIndicatorChartContainer'; import ChainIndicatorChartContainer from './ChainIndicatorChartContainer';
import ChainIndicatorItem from './ChainIndicatorItem'; import ChainIndicatorItem from './ChainIndicatorItem';
...@@ -72,13 +72,7 @@ const ChainIndicators = () => { ...@@ -72,13 +72,7 @@ const ChainIndicators = () => {
<Flex flexGrow={ 1 } flexDir="column" order={{ base: 2, lg: 1 }} p={{ base: 6, lg: 0 }}> <Flex flexGrow={ 1 } flexDir="column" order={{ base: 2, lg: 1 }} p={{ base: 6, lg: 0 }}>
<Flex alignItems="center"> <Flex alignItems="center">
<Text fontWeight={ 500 } fontFamily="heading" fontSize="lg">{ indicator?.title }</Text> <Text fontWeight={ 500 } fontFamily="heading" fontSize="lg">{ indicator?.title }</Text>
{ indicator?.hint && ( { indicator?.hint && <Hint label={ indicator.hint } ml={ 1 }/> }
<Tooltip label={ indicator.hint } maxW="300px">
<Box display="inline-flex" cursor="pointer" ml={ 1 }>
<Icon as={ infoIcon } boxSize={ 4 }/>
</Box>
</Tooltip>
) }
</Flex> </Flex>
{ valueTitle } { valueTitle }
<ChainIndicatorChartContainer { ...queryResult }/> <ChainIndicatorChartContainer { ...queryResult }/>
......
...@@ -2,13 +2,14 @@ import { Flex, Skeleton, Tag, Box, Icon } from '@chakra-ui/react'; ...@@ -2,13 +2,14 @@ import { Flex, Skeleton, Tag, Box, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import iconSuccess from 'icons/status/success.svg'; import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
...@@ -44,10 +45,11 @@ const AddressPageContent = () => { ...@@ -44,10 +45,11 @@ const AddressPageContent = () => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
const tabsScrollRef = React.useRef<HTMLDivElement>(null); const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const hash = getQueryParamString(router.query.hash);
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() }, pathParams: { hash },
queryOptions: { enabled: Boolean(router.query.id) }, queryOptions: { enabled: Boolean(hash) },
}); });
const tags = [ const tags = [
......
...@@ -6,6 +6,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; ...@@ -6,6 +6,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import BlockDetails from 'ui/block/BlockDetails'; import BlockDetails from 'ui/block/BlockDetails';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
...@@ -24,17 +25,19 @@ const BlockPageContent = () => { ...@@ -24,17 +25,19 @@ const BlockPageContent = () => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const appProps = useAppContext(); const appProps = useAppContext();
const height = getQueryParamString(router.query.height);
const tab = getQueryParamString(router.query.tab);
const blockTxsQuery = useQueryWithPages({ const blockTxsQuery = useQueryWithPages({
resourceName: 'block_txs', resourceName: 'block_txs',
pathParams: { id: router.query.id?.toString() }, pathParams: { height },
options: { options: {
enabled: Boolean(router.query.id && router.query.tab === 'txs'), enabled: Boolean(height && tab === 'txs'),
}, },
}); });
if (!router.query.id) { if (!height) {
return null; throw new Error('Block not found', { cause: { status: 404 } });
} }
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
...@@ -42,7 +45,7 @@ const BlockPageContent = () => { ...@@ -42,7 +45,7 @@ const BlockPageContent = () => {
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> }, { id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
]; ];
const hasPagination = !isMobile && router.query.tab === 'txs' && blockTxsQuery.isPaginationVisible; const hasPagination = !isMobile && tab === 'txs' && blockTxsQuery.isPaginationVisible;
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/blocks'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/blocks');
...@@ -50,7 +53,7 @@ const BlockPageContent = () => { ...@@ -50,7 +53,7 @@ const BlockPageContent = () => {
<Page> <Page>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
text={ `Block #${ router.query.id }` } text={ `Block #${ height }` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined } backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to blocks list" backLinkLabel="Back to blocks list"
/> />
......
...@@ -2,13 +2,19 @@ import { Text } from '@chakra-ui/react'; ...@@ -2,13 +2,19 @@ import { Text } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SmartContractVerificationConfigRaw, SmartContractVerificationMethod } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
import link from 'lib/link/link'; import getQueryParamString from 'lib/router/getQueryParamString';
import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm'; import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm';
import { isValidVerificationMethod, sortVerificationMethods } from 'ui/contractVerification/utils';
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 ContentLoader from 'ui/shared/ContentLoader';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -20,17 +26,67 @@ const ContractVerification = () => { ...@@ -20,17 +26,67 @@ const ContractVerification = () => {
const hasGoBackLink = referrer && referrer.includes('/address'); const hasGoBackLink = referrer && referrer.includes('/address');
const router = useRouter(); const router = useRouter();
const hash = router.query.id?.toString(); const hash = getQueryParamString(router.query.hash);
const method = router.query.id?.toString(); const method = getQueryParamString(router.query.method) as SmartContractVerificationMethod;
const contractQuery = useApiQuery('contract', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
},
});
if (contractQuery.isError && contractQuery.error.status === 404) {
throw Error('Not found', { cause: contractQuery.error as unknown as Error });
}
const configQuery = useApiQuery('contract_verification_config', {
queryOptions: {
select: (data: unknown) => {
const _data = data as SmartContractVerificationConfigRaw;
return {
..._data,
verification_options: _data.verification_options.filter(isValidVerificationMethod).sort(sortVerificationMethods),
};
},
enabled: Boolean(hash),
},
});
React.useEffect(() => { React.useEffect(() => {
if (method && hash) { if (method && hash) {
router.replace(link('address_contract_verification', { id: hash }), undefined, { scroll: false, shallow: true }); router.replace({ pathname: '/address/[hash]/contract_verification', query: { hash } }, undefined, { scroll: false, shallow: true });
} }
// onMount only // onMount only
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]); }, [ ]);
const isVerifiedContract = contractQuery.data?.is_verified;
React.useEffect(() => {
if (isVerifiedContract) {
router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { scroll: false, shallow: true });
}
}, [ hash, isVerifiedContract, router ]);
const content = (() => {
if (configQuery.isError || !hash || contractQuery.isError) {
return <DataFetchAlert/>;
}
if (configQuery.isLoading || contractQuery.isLoading || isVerifiedContract) {
return <ContentLoader/>;
}
return (
<ContractVerificationForm
method={ method && configQuery.data.verification_options.includes(method) ? method : undefined }
config={ configQuery.data }
hash={ hash }
/>
);
})();
return ( return (
<Page> <Page>
<PageTitle <PageTitle
...@@ -39,7 +95,7 @@ const ContractVerification = () => { ...@@ -39,7 +95,7 @@ const ContractVerification = () => {
backLinkLabel="Back to contract" backLinkLabel="Back to contract"
/> />
{ hash && ( { hash && (
<Address> <Address mb={ 12 }>
<AddressIcon address={{ hash, is_contract: true, implementation_name: null }} flexShrink={ 0 }/> <AddressIcon address={{ hash, is_contract: true, implementation_name: null }} flexShrink={ 0 }/>
<Text fontFamily="heading" ml={ 2 } fontWeight={ 500 } fontSize="lg" w={{ base: '100%', lg: 'auto' }} whiteSpace="nowrap" overflow="hidden"> <Text fontFamily="heading" ml={ 2 } fontWeight={ 500 } fontSize="lg" w={{ base: '100%', lg: 'auto' }} whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ hash }/> <HashStringShortenDynamic hash={ hash }/>
...@@ -47,7 +103,7 @@ const ContractVerification = () => { ...@@ -47,7 +103,7 @@ const ContractVerification = () => {
<CopyToClipboard text={ hash }/> <CopyToClipboard text={ hash }/>
</Address> </Address>
) } ) }
<ContractVerificationForm/> { content }
</Page> </Page>
); );
}; };
......
import { Box, Center, useColorMode } from '@chakra-ui/react'; import { Box, Center, useColorMode } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import link from 'lib/link/link';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
...@@ -26,9 +26,9 @@ const MarketplaceApp = ({ app, isLoading }: Props) => { ...@@ -26,9 +26,9 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
if (app && !isFrameLoading) { if (app && !isFrameLoading) {
const message = { const message = {
blockscoutColorMode: colorMode, blockscoutColorMode: colorMode,
blockscoutRootUrl: link('network_index'), blockscoutRootUrl: appConfig.baseUrl + route({ pathname: '/' }),
blockscoutAddressExplorerUrl: link('address_index'), blockscoutAddressExplorerUrl: appConfig.baseUrl + route({ pathname: '/address/[hash]', query: { hash: '' } }),
blockscoutTransactionExplorerUrl: link('tx'), blockscoutTransactionExplorerUrl: appConfig.baseUrl + route({ pathname: '/tx/[hash]', query: { hash: '' } }),
blockscoutNetworkName: appConfig.network.name, blockscoutNetworkName: appConfig.network.name,
blockscoutNetworkId: Number(appConfig.network.id), blockscoutNetworkId: Number(appConfig.network.id),
blockscoutNetworkCurrency: appConfig.network.currency, blockscoutNetworkCurrency: appConfig.network.currency,
......
...@@ -12,7 +12,7 @@ import Token from './Token'; ...@@ -12,7 +12,7 @@ import Token from './Token';
const TOKEN_API_URL = buildApiUrl('token', { hash: '1' }); const TOKEN_API_URL = buildApiUrl('token', { hash: '1' });
const TOKEN_COUNTERS_API_URL = buildApiUrl('token_counters', { hash: '1' }); const TOKEN_COUNTERS_API_URL = buildApiUrl('token_counters', { hash: '1' });
const TOKEN_TRANSFERS_API_URL = buildApiUrl('token_transfers', { hash: '1' }); const TOKEN_TRANSFERS_API_URL = buildApiUrl('token_transfers', { hash: '1' });
const ADDRESS_API_URL = buildApiUrl('address', { id: '1' }); const ADDRESS_API_URL = buildApiUrl('address', { hash: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: 1, tab: 'token_transfers' }, query: { hash: 1, tab: 'token_transfers' },
......
...@@ -16,13 +16,15 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -16,13 +16,15 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import TokenContractInfo from 'ui/token/TokenContractInfo'; import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails'; import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders'; import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenInventory from 'ui/token/TokenInventory';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
export type TokenTabs = 'token_transfers' | 'holders' export type TokenTabs = 'token_transfers' | 'holders' | 'inventory';
const TokenPageContent = () => { const TokenPageContent = () => {
const router = useRouter(); const router = useRouter();
...@@ -72,11 +74,24 @@ const TokenPageContent = () => { ...@@ -72,11 +74,24 @@ const TokenPageContent = () => {
}, },
}); });
const inventoryQuery = useQueryWithPages({
resourceName: 'token_inventory',
pathParams: { hash: router.query.hash?.toString() },
scrollRef,
options: {
enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && tokenQuery.data),
},
});
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery }/> }, { id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> }, { id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
]; ];
if (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') {
tabs.push({ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> });
}
let hasPagination; let hasPagination;
let pagination; let pagination;
...@@ -90,6 +105,11 @@ const TokenPageContent = () => { ...@@ -90,6 +105,11 @@ const TokenPageContent = () => {
pagination = holdersQuery.pagination; pagination = holdersQuery.pagination;
} }
if (router.query.tab === 'inventory') {
hasPagination = inventoryQuery.isPaginationVisible;
pagination = inventoryQuery.pagination;
}
const tokenSymbolText = tokenQuery.data?.symbol ? ` (${ trimTokenSymbol(tokenQuery.data.symbol) })` : ''; const tokenSymbolText = tokenQuery.data?.symbol ? ` (${ trimTokenSymbol(tokenQuery.data.symbol) })` : '';
return ( return (
...@@ -118,12 +138,14 @@ const TokenPageContent = () => { ...@@ -118,12 +138,14 @@ const TokenPageContent = () => {
{ /* should stay before tabs to scroll up whith pagination */ } { /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
{ tokenQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } } tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null } rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
) }
</Page> </Page>
); );
}; };
......
...@@ -7,6 +7,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; ...@@ -7,6 +7,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import networkExplorers from 'lib/networks/networkExplorers'; import networkExplorers from 'lib/networks/networkExplorers';
import getQueryParamString from 'lib/router/getQueryParamString';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
...@@ -34,21 +35,22 @@ const TransactionPageContent = () => { ...@@ -34,21 +35,22 @@ const TransactionPageContent = () => {
const appProps = useAppContext(); const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/txs'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/txs');
const hash = getQueryParamString(router.query.hash);
const { data } = useApiQuery('tx', { const { data } = useApiQuery('tx', {
pathParams: { id: router.query.id?.toString() }, pathParams: { hash },
queryOptions: { enabled: Boolean(router.query.id) }, queryOptions: { enabled: Boolean(hash) },
}); });
const explorersLinks = networkExplorers const explorersLinks = networkExplorers
.filter((explorer) => explorer.paths.tx) .filter((explorer) => explorer.paths.tx)
.map((explorer) => { .map((explorer) => {
const url = new URL(explorer.paths.tx + '/' + router.query.id, explorer.baseUrl); const url = new URL(explorer.paths.tx + '/' + hash, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } title={ `Open in ${ explorer.title }` } href={ url.toString() }/>; return <LinkExternal key={ explorer.baseUrl } title={ `Open in ${ explorer.title }` } href={ url.toString() }/>;
}); });
const additionals = ( const additionals = (
<Flex justifyContent="space-between" alignItems="center" flexGrow={ 1 }> <Flex justifyContent="space-between" alignItems="center" flexGrow={ 1 } flexWrap="wrap">
{ data?.tx_tag && <Tag my={ 2 }>{ data.tx_tag }</Tag> } { data?.tx_tag && <Tag my={ 2 }>{ data.tx_tag }</Tag> }
{ explorersLinks.length > 0 && ( { explorersLinks.length > 0 && (
<Flex <Flex
......
import { Text, Flex, Icon, Box, chakra } from '@chakra-ui/react'; import { Text, Flex, Icon, Box, chakra } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SearchResultItem } from 'types/api/search'; import type { SearchResultItem } from 'types/api/search';
...@@ -6,7 +7,6 @@ import type { SearchResultItem } from 'types/api/search'; ...@@ -6,7 +7,6 @@ import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
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';
...@@ -31,7 +31,7 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => { ...@@ -31,7 +31,7 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
return ( return (
<Flex alignItems="flex-start"> <Flex alignItems="flex-start">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/> <TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<LinkInternal ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 } wordBreak="break-all"> <LinkInternal ml={ 2 } href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) } fontWeight={ 700 } wordBreak="break-all">
<chakra.span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/> <chakra.span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</LinkInternal> </LinkInternal>
</Flex> </Flex>
...@@ -56,7 +56,7 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => { ...@@ -56,7 +56,7 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
return ( return (
<Flex alignItems="center"> <Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/> <Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<LinkInternal fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }> <LinkInternal fontWeight={ 700 } href={ route({ pathname: '/block/[height]', query: { height: String(data.block_number) } }) }>
<Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box> <Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
</LinkInternal> </LinkInternal>
</Flex> </Flex>
......
import { Tr, Td, Text, Flex, Icon, Box } from '@chakra-ui/react'; import { Tr, Td, Text, Flex, Icon, Box } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SearchResultItem } from 'types/api/search'; import type { SearchResultItem } from 'types/api/search';
...@@ -6,7 +7,6 @@ import type { SearchResultItem } from 'types/api/search'; ...@@ -6,7 +7,6 @@ import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
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';
...@@ -31,7 +31,7 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => { ...@@ -31,7 +31,7 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Td fontSize="sm"> <Td fontSize="sm">
<Flex alignItems="center"> <Flex alignItems="center">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/> <TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<LinkInternal ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 } wordBreak="break-all"> <LinkInternal ml={ 2 } href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) } fontWeight={ 700 } wordBreak="break-all">
<span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/> <span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</LinkInternal> </LinkInternal>
</Flex> </Flex>
...@@ -54,7 +54,12 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => { ...@@ -54,7 +54,12 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Td fontSize="sm"> <Td fontSize="sm">
<Flex alignItems="center" overflow="hidden"> <Flex alignItems="center" overflow="hidden">
<AddressIcon address={{ hash: data.address, is_contract: data.type === 'contract', implementation_name: null }} mr={ 2 } flexShrink={ 0 }/> <AddressIcon address={{ hash: data.address, is_contract: data.type === 'contract', implementation_name: null }} mr={ 2 } flexShrink={ 0 }/>
<LinkInternal href={ link('address_index', { id: data.address }) } fontWeight={ 700 } overflow="hidden" whiteSpace="nowrap"> <LinkInternal
href={ route({ pathname: '/address/[hash]', query: { hash: data.address } }) }
fontWeight={ 700 }
overflow="hidden"
whiteSpace="nowrap"
>
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block"> <Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block">
<HashStringShortenDynamic hash={ data.address }/> <HashStringShortenDynamic hash={ data.address }/>
</Box> </Box>
...@@ -88,7 +93,7 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => { ...@@ -88,7 +93,7 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Td fontSize="sm"> <Td fontSize="sm">
<Flex alignItems="center"> <Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/> <Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<LinkInternal fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }> <LinkInternal fontWeight={ 700 } href={ route({ pathname: '/block/[height]', query: { height: String(data.block_number) } }) }>
<Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box> <Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
</LinkInternal> </LinkInternal>
</Flex> </Flex>
......
...@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressParam } from 'types/api/addressParams'; import type { AddressParam } from 'types/api/addressParams';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask'; import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
......
import { Box, Button, Heading, Icon, Text, chakra } from '@chakra-ui/react'; import { Box, Button, Heading, Icon, Text, chakra } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import icon404 from 'icons/error-pages/404.svg'; import icon404 from 'icons/error-pages/404.svg';
import icon422 from 'icons/error-pages/422.svg'; import icon422 from 'icons/error-pages/422.svg';
import icon500 from 'icons/error-pages/500.svg'; import icon500 from 'icons/error-pages/500.svg';
import link from 'lib/link/link';
interface Props { interface Props {
statusCode: number; statusCode: number;
...@@ -42,7 +42,7 @@ const AppError = ({ statusCode, className }: Props) => { ...@@ -42,7 +42,7 @@ const AppError = ({ statusCode, className }: Props) => {
size="lg" size="lg"
variant="outline" variant="outline"
as="a" as="a"
href={ link('network_index') } href={ route({ pathname: '/' }) }
> >
Back to home Back to home
</Button> </Button>
......
import { IconButton, Tooltip, useClipboard, chakra } from '@chakra-ui/react'; import { IconButton, Tooltip, useClipboard, chakra, useDisclosure } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import CopyIcon from 'icons/copy.svg'; import CopyIcon from 'icons/copy.svg';
...@@ -6,6 +6,8 @@ import CopyIcon from 'icons/copy.svg'; ...@@ -6,6 +6,8 @@ import CopyIcon from 'icons/copy.svg';
const CopyToClipboard = ({ text, className }: {text: string; className?: string}) => { const CopyToClipboard = ({ text, className }: {text: string; className?: string}) => {
const { hasCopied, onCopy } = useClipboard(text, 3000); const { hasCopied, onCopy } = useClipboard(text, 3000);
const [ copied, setCopied ] = useState(false); const [ copied, setCopied ] = useState(false);
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
useEffect(() => { useEffect(() => {
if (hasCopied) { if (hasCopied) {
...@@ -15,8 +17,13 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string} ...@@ -15,8 +17,13 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
} }
}, [ hasCopied ]); }, [ hasCopied ]);
const handleClick = React.useCallback(() => {
onToggle();
onCopy();
}, [ onCopy, onToggle ]);
return ( return (
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } closeOnClick={ false }> <Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen }>
<IconButton <IconButton
aria-label="copy" aria-label="copy"
icon={ <CopyIcon/> } icon={ <CopyIcon/> }
...@@ -25,11 +32,13 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string} ...@@ -25,11 +32,13 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
variant="simple" variant="simple"
display="inline-block" display="inline-block"
flexShrink={ 0 } flexShrink={ 0 }
onClick={ onCopy } onClick={ handleClick }
className={ className } className={ className }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
/> />
</Tooltip> </Tooltip>
); );
}; };
export default chakra(CopyToClipboard); export default React.memo(chakra(CopyToClipboard));
import { GridItem, Icon, Flex, Tooltip, Box, Text } from '@chakra-ui/react'; import { GridItem, Flex, Text } from '@chakra-ui/react';
import type { HTMLChakraProps } from '@chakra-ui/system'; import type { HTMLChakraProps } from '@chakra-ui/system';
import React from 'react'; import React from 'react';
import infoIcon from 'icons/info.svg'; import Hint from 'ui/shared/Hint';
interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> { interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
title: React.ReactNode; title: React.ReactNode;
hint: string; hint: string;
children: React.ReactNode; children: React.ReactNode;
note?: string;
} }
const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => { const DetailsInfoItem = ({ title, hint, note, children, id, ...styles }: Props) => {
return ( return (
<> <>
<GridItem py={{ base: 1, lg: 2 }} lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}> <GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}>
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="flex-start">
<Tooltip <Hint label={ hint }/>
label={ hint } <Text fontWeight={{ base: 700, lg: 500 }}>
placement="top" { title }
maxW="320px" { note && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">{ note }</Text> }
> </Text>
<Box cursor="pointer" display="inherit">
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
<Text fontWeight={{ base: 700, lg: 500 }}>{ title }</Text>
</Flex> </Flex>
</GridItem> </GridItem>
<GridItem <GridItem
......
import { FormControl, useToken, useColorMode } from '@chakra-ui/react'; import { FormControl, useToken, useColorMode } from '@chakra-ui/react';
import type { Size, CSSObjectWithLabel, OptionsOrGroups, GroupBase, SingleValue, MultiValue } from 'chakra-react-select'; import type { CSSObjectWithLabel, GroupBase, SingleValue, MultiValue, AsyncProps, Props as SelectProps } from 'chakra-react-select';
import { Select } from 'chakra-react-select'; import { Select, AsyncSelect } from 'chakra-react-select';
import React from 'react'; import React from 'react';
import type { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form'; import type { FieldError, FieldErrorsImpl, Merge } from 'react-hook-form';
...@@ -9,20 +9,23 @@ import type { Option } from './types'; ...@@ -9,20 +9,23 @@ import type { Option } from './types';
import { getChakraStyles } from 'ui/shared/FancySelect/utils'; import { getChakraStyles } from 'ui/shared/FancySelect/utils';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props { interface CommonProps {
size?: Size; // we don't have styles for sm select/input with floating label yet
options: OptionsOrGroups<Option, GroupBase<Option>>;
placeholder: string;
name: string;
onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void;
onBlur?: () => void;
isDisabled?: boolean;
isRequired?: boolean;
error?: Merge<FieldError, FieldErrorsImpl<Option>> | undefined; error?: Merge<FieldError, FieldErrorsImpl<Option>> | undefined;
value?: SingleValue<Option> | MultiValue<Option>;
} }
const FancySelect = ({ size = 'md', options, placeholder, name, onChange, onBlur, isDisabled, isRequired, error, value }: Props) => { interface RegularSelectProps extends SelectProps<Option, boolean, GroupBase<Option>>, CommonProps {
isAsync?: false;
onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void;
}
interface AsyncSelectProps extends AsyncProps<Option, boolean, GroupBase<Option>>, CommonProps {
isAsync: true;
onChange: (newValue: SingleValue<Option> | MultiValue<Option>) => void;
}
type Props = RegularSelectProps | AsyncSelectProps;
const FancySelect = (props: Props) => {
const menuZIndex = useToken('zIndices', 'dropdown'); const menuZIndex = useToken('zIndices', 'dropdown');
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
...@@ -32,34 +35,30 @@ const FancySelect = ({ size = 'md', options, placeholder, name, onChange, onBlur ...@@ -32,34 +35,30 @@ const FancySelect = ({ size = 'md', options, placeholder, name, onChange, onBlur
const chakraStyles = React.useMemo(() => getChakraStyles(colorMode), [ colorMode ]); const chakraStyles = React.useMemo(() => getChakraStyles(colorMode), [ colorMode ]);
const SelectComponent = props.isAsync ? AsyncSelect : Select;
return ( return (
<FormControl <FormControl
variant="floating" variant="floating"
size={ size } size={ props.size || 'md' }
isRequired={ isRequired } isRequired={ props.isRequired }
{ ...(error ? { 'aria-invalid': true } : {}) } { ...(props.error ? { 'aria-invalid': true } : {}) }
{ ...(isDisabled ? { 'aria-disabled': true } : {}) } { ...(props.isDisabled ? { 'aria-disabled': true } : {}) }
{ ...(value ? { 'data-active': true } : {}) } { ...(props.value ? { 'data-active': true } : {}) }
> >
<Select <SelectComponent
{ ...props }
size={ props.size || 'md' }
menuPortalTarget={ window.document.body } menuPortalTarget={ window.document.body }
placeholder="" placeholder=""
name={ name }
options={ options }
size={ size }
styles={ styles } styles={ styles }
chakraStyles={ chakraStyles } chakraStyles={ chakraStyles }
isInvalid={ Boolean(props.error) }
useBasicStyles useBasicStyles
onChange={ onChange }
onBlur={ onBlur }
isDisabled={ isDisabled }
isRequired={ isRequired }
isInvalid={ Boolean(error) }
value={ value }
/> />
<InputPlaceholder <InputPlaceholder
text={ placeholder } text={ typeof props.placeholder === 'string' ? props.placeholder : '' }
error={ error } error={ props.error }
isFancy isFancy
/> />
</FormControl> </FormControl>
......
import type { TooltipProps } from '@chakra-ui/react';
import { chakra, IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import InfoIcon from 'icons/info.svg';
interface Props {
label: string | React.ReactNode;
className?: string;
tooltipProps?: Partial<TooltipProps>;
}
const Hint = ({ label, className, tooltipProps }: Props) => {
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
const handleClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
onToggle();
}, [ onToggle ]);
return (
<Tooltip
label={ label }
placement="top"
maxW="320px"
isOpen={ isOpen }
{ ...tooltipProps }
>
<IconButton
colorScheme="none"
aria-label="hint"
icon={ <InfoIcon/> }
boxSize={ 5 }
variant="simple"
display="inline-block"
flexShrink={ 0 }
className={ className }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
onClick={ handleClick }
/>
</Tooltip>
);
};
export default React.memo(chakra(Hint));
...@@ -7,9 +7,10 @@ interface Props { ...@@ -7,9 +7,10 @@ interface Props {
error?: Partial<FieldError>; error?: Partial<FieldError>;
className?: string; className?: string;
isFancy?: boolean; isFancy?: boolean;
isInModal?: boolean;
} }
const InputPlaceholder = ({ text, error, className, isFancy }: Props) => { const InputPlaceholder = ({ text, error, className, isFancy, isInModal }: Props) => {
let errorMessage = error?.message; let errorMessage = error?.message;
if (!errorMessage && error?.type === 'pattern') { if (!errorMessage && error?.type === 'pattern') {
...@@ -20,6 +21,7 @@ const InputPlaceholder = ({ text, error, className, isFancy }: Props) => { ...@@ -20,6 +21,7 @@ const InputPlaceholder = ({ text, error, className, isFancy }: Props) => {
<FormLabel <FormLabel
className={ className } className={ className }
{ ...(isFancy ? { 'data-fancy': true } : {}) } { ...(isFancy ? { 'data-fancy': true } : {}) }
{ ...(isInModal ? { 'data-in-modal': true } : {}) }
> >
<chakra.span>{ text }</chakra.span> <chakra.span>{ text }</chakra.span>
{ errorMessage && <chakra.span order={ 3 } whiteSpace="pre"> - { errorMessage }</chakra.span> } { errorMessage && <chakra.span order={ 3 } whiteSpace="pre"> - { errorMessage }</chakra.span> }
......
import type { LinkProps } from '@chakra-ui/react'; import type { LinkProps } from '@chakra-ui/react';
import { Link } from '@chakra-ui/react'; import { Link } from '@chakra-ui/react';
import type { LinkProps as NextLinkProps } from 'next/link';
import NextLink from 'next/link'; import NextLink from 'next/link';
import type { LegacyRef } from 'react'; import type { LegacyRef } from 'react';
import React from 'react'; import React from 'react';
...@@ -11,7 +12,7 @@ const LinkInternal = (props: LinkProps, ref: LegacyRef<HTMLAnchorElement>) => { ...@@ -11,7 +12,7 @@ const LinkInternal = (props: LinkProps, ref: LegacyRef<HTMLAnchorElement>) => {
} }
return ( return (
<NextLink href={ props.href } passHref target={ props.target }> <NextLink href={ props.href as NextLinkProps['href'] } passHref target={ props.target }>
<Link { ...props } ref={ ref }/> <Link { ...props } ref={ ref }/>
</NextLink> </NextLink>
); );
......
...@@ -35,7 +35,7 @@ const Page = ({ ...@@ -35,7 +35,7 @@ const Page = ({
return <PageContent isHomePage={ isHomePage }>{ content }</PageContent>; return <PageContent isHomePage={ isHomePage }>{ content }</PageContent>;
} }
return isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ 500 }/>; return isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode }/>;
}, [ isHomePage, wrapChildren ]); }, [ isHomePage, wrapChildren ]);
const renderedChildren = wrapChildren ? ( const renderedChildren = wrapChildren ? (
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
chakra, chakra,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system'; import type { StyleProps } from '@chakra-ui/styled-system';
import _pickBy from 'lodash/pickBy';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
...@@ -51,8 +52,9 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -51,8 +52,9 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index]; const nextTab = tabs[index];
const queryForPathname = _pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`));
router.push( router.push(
{ pathname: router.asPath.split('?')[0], query: { tab: nextTab.id } }, { pathname: router.pathname, query: { ...queryForPathname, tab: nextTab.id } },
undefined, undefined,
{ shallow: true }, { shallow: true },
); );
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { ROUTES } from 'lib/link/routes';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import SocketNewItemsNotice from './SocketNewItemsNotice'; import SocketNewItemsNotice from './SocketNewItemsNotice';
const hooksConfig = { const hooksConfig = {
router: { router: {
pathname: ROUTES.txs.pattern, pathname: '/tx/[hash]',
query: {}, query: {},
}, },
}; };
......
...@@ -10,13 +10,14 @@ interface Props { ...@@ -10,13 +10,14 @@ interface Props {
name?: string | null; name?: string | null;
className?: string; className?: string;
logoSize?: number; logoSize?: number;
isDisabled?: boolean;
} }
const TokenSnippet = ({ symbol, hash, name, className, logoSize = 6 }: Props) => { const TokenSnippet = ({ symbol, hash, name, className, logoSize = 6, isDisabled }: Props) => {
return ( return (
<Flex className={ className } alignItems="center" columnGap={ 2 } w="100%"> <Flex className={ className } alignItems="center" columnGap={ 2 } w="100%">
<TokenLogo boxSize={ logoSize } hash={ hash } name={ name }/> <TokenLogo boxSize={ logoSize } hash={ hash } name={ name }/>
<AddressLink hash={ hash } alias={ name } type="token"/> <AddressLink hash={ hash } alias={ name } type="token" isDisabled={ isDisabled }/>
{ symbol && <Text variant="secondary">({ symbol })</Text> } { symbol && <Text variant="secondary">({ symbol })</Text> }
</Flex> </Flex>
); );
......
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
import React from 'react'; import React from 'react';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import PopoverFilter from 'ui/shared/filters/PopoverFilter'; import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter'; import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
......
import { Box, Icon, chakra } from '@chakra-ui/react'; import { Box, Icon, chakra } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg'; import nftPlaceholder from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -18,7 +18,7 @@ const TokenTransferNft = ({ hash, id, className, isDisabled }: Props) => { ...@@ -18,7 +18,7 @@ const TokenTransferNft = ({ hash, id, className, isDisabled }: Props) => {
return ( return (
<Component <Component
href={ isDisabled ? undefined : link('token_instance_item', { hash, id }) } href={ isDisabled ? undefined : route({ pathname: '/token/[hash]/instance/[id]', query: { hash, id } }) }
overflow="hidden" overflow="hidden"
whiteSpace="nowrap" whiteSpace="nowrap"
display="flex" display="flex"
......
...@@ -30,7 +30,7 @@ const TokenTransferTable = ({ ...@@ -30,7 +30,7 @@ const TokenTransferTable = ({
}: Props) => { }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table variant="simple" size="sm" minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
{ showTxInfo && <Th width="44px"></Th> } { showTxInfo && <Th width="44px"></Th> }
......
import { chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react'; import { chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import type { HTMLAttributeAnchorTarget } from 'react'; import type { HTMLAttributeAnchorTarget } from 'react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -27,7 +27,7 @@ type AddressTokenTxProps = { ...@@ -27,7 +27,7 @@ type AddressTokenTxProps = {
type BlockProps = { type BlockProps = {
type: 'block'; type: 'block';
hash: string; hash: string;
id: string; height: string;
} }
type AddressTokenProps = { type AddressTokenProps = {
...@@ -44,15 +44,15 @@ const AddressLink = (props: Props) => { ...@@ -44,15 +44,15 @@ const AddressLink = (props: Props) => {
let url; let url;
if (type === 'transaction') { if (type === 'transaction') {
url = link('tx', { id: hash }); url = route({ pathname: '/tx/[hash]', query: { hash } });
} else if (type === 'token') { } else if (type === 'token') {
url = link('token_index', { hash: hash }); url = route({ pathname: '/token/[hash]', query: { hash } });
} else if (type === 'block') { } else if (type === 'block') {
url = link('block', { id: props.id }); url = route({ pathname: '/block/[height]', query: { height: props.height } });
} else if (type === 'address_token') { } else if (type === 'address_token') {
url = link('address_index', { id: hash }, { tab: 'token_transfers', token: props.tokenHash, scroll_to_tabs: 'true' }); url = route({ pathname: '/address/[hash]', query: { hash, tab: 'token_transfers', token_hash: props.tokenHash, scroll_to_tabs: 'true' } });
} else { } else {
url = link('address_index', { id: hash }); url = route({ pathname: '/address/[hash]', query: { hash } });
} }
const content = (() => { const content = (() => {
......
import { CheckboxGroup, Checkbox, Text } from '@chakra-ui/react'; import { CheckboxGroup, Checkbox, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import TOKEN_TYPE from 'lib/token/tokenTypes'; import TOKEN_TYPE from 'lib/token/tokenTypes';
......
import { Box } from '@chakra-ui/react';
import type { DragEvent } from 'react';
import React from 'react';
import { getAllFileEntries, convertFileEntryToFile } from './utils/files';
interface Props {
onDrop: (files: Array<File>) => void;
}
const DragAndDropArea = ({ onDrop }: Props) => {
const [ isDragOver, setIsDragOver ] = React.useState(false);
const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
const fileEntries = await getAllFileEntries(event.dataTransfer.items);
const files = await Promise.all(fileEntries.map(convertFileEntryToFile));
onDrop(files);
setIsDragOver(false);
}, [ onDrop ]);
const handleDragOver = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
}, []);
const handleDragEnter = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(true);
}, []);
const handleDragLeave = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDragOver(false);
}, []);
return (
<Box
w="100%"
h="200px"
bgColor="lightpink"
opacity={ isDragOver ? 0.8 : 1 }
onDrop={ handleDrop }
onDragOver={ handleDragOver }
onDragEnter={ handleDragEnter }
onDragLeave={ handleDragLeave }
/>
);
};
export default React.memo(DragAndDropArea);
import { Box, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
message: string;
className?: string;
}
const FieldError = ({ message, className }: Props) => {
return <Box className={ className } color="error" fontSize="sm" mt={ 2 } wordBreak="break-all">{ message }</Box>;
};
export default chakra(FieldError);
...@@ -3,8 +3,12 @@ import type { ChangeEvent } from 'react'; ...@@ -3,8 +3,12 @@ import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, FieldValues, Path } from 'react-hook-form'; import type { ControllerRenderProps, FieldValues, Path } from 'react-hook-form';
interface InjectedProps {
onChange: (files: Array<File>) => void;
}
interface Props<V extends FieldValues, N extends Path<V>> { interface Props<V extends FieldValues, N extends Path<V>> {
children: React.ReactNode; children: React.ReactNode | ((props: InjectedProps) => React.ReactNode);
field: ControllerRenderProps<V, N>; field: ControllerRenderProps<V, N>;
accept?: string; accept?: string;
multiple?: boolean; multiple?: boolean;
...@@ -13,6 +17,10 @@ interface Props<V extends FieldValues, N extends Path<V>> { ...@@ -13,6 +17,10 @@ interface Props<V extends FieldValues, N extends Path<V>> {
const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ children, accept, multiple, field }: Props<Values, Names>) => { const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ children, accept, multiple, field }: Props<Values, Names>) => {
const ref = React.useRef<HTMLInputElement>(null); const ref = React.useRef<HTMLInputElement>(null);
const onChange = React.useCallback((files: Array<File>) => {
field.onChange([ ...(field.value || []), ...files ]);
}, [ field ]);
const handleInputChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleInputChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
const fileList = event.target.files; const fileList = event.target.files;
if (!fileList) { if (!fileList) {
...@@ -20,9 +28,9 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi ...@@ -20,9 +28,9 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi
} }
const files = Array.from(fileList); const files = Array.from(fileList);
field.onChange(files); onChange(files);
field.onBlur(); field.onBlur();
}, [ field ]); }, [ onChange, field ]);
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
ref.current?.click(); ref.current?.click();
...@@ -32,6 +40,12 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi ...@@ -32,6 +40,12 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi
field.onBlur(); field.onBlur();
}, [ field ]); }, [ field ]);
const injectedProps = React.useMemo(() => ({
onChange,
}), [ onChange ]);
const content = typeof children === 'function' ? children(injectedProps) : children;
return ( return (
<InputGroup onClick={ handleClick } onBlur={ handleInputBlur }> <InputGroup onClick={ handleClick } onBlur={ handleInputBlur }>
<VisuallyHiddenInput <VisuallyHiddenInput
...@@ -42,7 +56,7 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi ...@@ -42,7 +56,7 @@ const FileInput = <Values extends FieldValues, Names extends Path<Values>>({ chi
multiple={ multiple } multiple={ multiple }
name={ field.name } name={ field.name }
/> />
{ children } { content }
</InputGroup> </InputGroup>
); );
}; };
......
// Function to get all files in drop directory
export async function getAllFileEntries(dataTransferItemList: DataTransferItemList): Promise<Array<FileSystemFileEntry>> {
const fileEntries: Array<FileSystemFileEntry> = [];
// Use BFS to traverse entire directory/file structure
const queue: Array<FileSystemFileEntry | FileSystemDirectoryEntry> = [];
// Unfortunately dataTransferItemList is not iterable i.e. no forEach
for (let i = 0; i < dataTransferItemList.length; i++) {
// Note webkitGetAsEntry a non-standard feature and may change
// Usage is necessary for handling directories
// + typescript types are kinda wrong - https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
const item = dataTransferItemList[i].webkitGetAsEntry() as FileSystemFileEntry | FileSystemDirectoryEntry | null;
item && queue.push(item);
}
while (queue.length > 0) {
const entry = queue.shift();
if (entry?.isFile) {
fileEntries.push(entry as FileSystemFileEntry);
} else if (entry?.isDirectory && 'createReader' in entry) {
queue.push(...await readAllDirectoryEntries(entry.createReader()));
}
}
return fileEntries;
}
// Get all the entries (files or sub-directories) in a directory
// by calling readEntries until it returns empty array
async function readAllDirectoryEntries(directoryReader: DirectoryReader) {
const entries: Array<FileSystemFileEntry> = [];
let readEntries = await readEntriesPromise(directoryReader);
while (readEntries && readEntries.length > 0) {
entries.push(...readEntries);
readEntries = await readEntriesPromise(directoryReader);
}
return entries;
}
// Wrap readEntries in a promise to make working with readEntries easier
// readEntries will return only some of the entries in a directory
// e.g. Chrome returns at most 100 entries at a time
async function readEntriesPromise(directoryReader: DirectoryReader): Promise<Array<FileSystemFileEntry> | undefined> {
try {
return await new Promise((resolve, reject) => {
directoryReader.readEntries(
(fileEntry) => {
resolve(fileEntry as Array<FileSystemFileEntry>);
},
reject,
);
});
} catch (err) {}
}
export function convertFileEntryToFile(entry: FileSystemFileEntry): Promise<File> {
return new Promise((resolve) => {
entry.file(async(file: File) => {
// const newFile = new File([ file ], entry.fullPath, { lastModified: file.lastModified, type: file.type });
resolve(file);
});
});
}
import { Text, Grid, GridItem, Tooltip, Button, useColorModeValue, Alert, Link } from '@chakra-ui/react'; import { Text, Grid, GridItem, Tooltip, Button, useColorModeValue, Alert, Link } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Log } from 'types/api/log'; import type { Log } from 'types/api/log';
// import searchIcon from 'icons/search.svg'; // import searchIcon from 'icons/search.svg';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
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';
...@@ -47,7 +47,7 @@ const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash ...@@ -47,7 +47,7 @@ const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash
<GridItem colSpan={{ base: 1, lg: 2 }}> <GridItem colSpan={{ base: 1, lg: 2 }}>
<Alert status="warning" display="inline-table" whiteSpace="normal"> <Alert status="warning" display="inline-table" whiteSpace="normal">
To see accurate decoded input data, the contract must be verified.{ space } To see accurate decoded input data, the contract must be verified.{ space }
<Link href={ link('address_contract_verification', { id: address.hash }) }>Verify the contract here</Link> <Link href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: address.hash } }) }>Verify the contract here</Link>
</Alert> </Alert>
</GridItem> </GridItem>
) } ) }
......
import { Link, Icon, Text, HStack, Tooltip, Box } from '@chakra-ui/react'; import { Link, Icon, Text, HStack, Tooltip, Box } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { NavItem } from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import useColors from './useColors'; import useColors from './useColors';
interface Props { type Props = NavItem & {
isCollapsed?: boolean; isCollapsed?: boolean;
isActive?: boolean;
url: string;
text: string;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
px?: string | number; px?: string | number;
isNewUi: boolean;
} }
const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props) => { const NavLink = ({ text, nextRoute, icon, isCollapsed, isActive, px, isNewUi }: Props) => {
const colors = useColors(); const colors = useColors();
const isExpanded = isCollapsed === false; const isExpanded = isCollapsed === false;
const content = ( const content = (
<Link <Link
{ ...(isNewUi ? {} : { href: url }) } { ...(isNewUi ? {} : { href: route(nextRoute) }) }
w={{ base: '100%', lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }} w={{ base: '100%', lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }}
px={ px || { base: 3, lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 } } px={ px || { base: 3, lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 } }
py={ 2.5 } py={ 2.5 }
...@@ -68,7 +65,7 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props) ...@@ -68,7 +65,7 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props)
{ /* why not NextLink in all cases? since prev UI and new one are hosting in the same domain and global routing is managed by nginx */ } { /* why not NextLink in all cases? since prev UI and new one are hosting in the same domain and global routing is managed by nginx */ }
{ /* we have to hard reload page on every transition between urls from different part of the app */ } { /* we have to hard reload page on every transition between urls from different part of the app */ }
{ isNewUi ? ( { isNewUi ? (
<NextLink href={ url } passHref> <NextLink href={ nextRoute } passHref>
{ content } { content }
</NextLink> </NextLink>
) : content } ) : content }
......
...@@ -12,6 +12,7 @@ const hooksConfig = { ...@@ -12,6 +12,7 @@ const hooksConfig = {
router: { router: {
route: '/blocks', route: '/blocks',
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' },
pathname: '/blocks',
}, },
}; };
......
import { Icon, Box, Image, useColorModeValue } from '@chakra-ui/react'; import { Icon, Box, Image, useColorModeValue } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import smallLogoPlaceholder from 'icons/networks/icons/placeholder.svg'; import smallLogoPlaceholder from 'icons/networks/icons/placeholder.svg';
import logoPlaceholder from 'icons/networks/logos/blockscout.svg'; import logoPlaceholder from 'icons/networks/logos/blockscout.svg';
import link from 'lib/link/link';
import ASSETS from 'lib/networks/networkAssets'; import ASSETS from 'lib/networks/networkAssets';
interface Props { interface Props {
...@@ -14,7 +14,7 @@ interface Props { ...@@ -14,7 +14,7 @@ interface Props {
const NetworkLogo = ({ isCollapsed, onClick }: Props) => { const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
const logoColor = useColorModeValue('blue.600', 'white'); const logoColor = useColorModeValue('blue.600', 'white');
const href = link('network_index'); const href = route({ pathname: '/' });
const [ isLogoError, setLogoError ] = React.useState(false); const [ isLogoError, setLogoError ] = React.useState(false);
const [ isSmallLogoError, setSmallLogoError ] = React.useState(false); const [ isSmallLogoError, setSmallLogoError ] = React.useState(false);
......
...@@ -12,6 +12,7 @@ test('no auth', async({ mount, page }) => { ...@@ -12,6 +12,7 @@ test('no auth', async({ mount, page }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
asPath: '/', asPath: '/',
pathname: '/',
}, },
}; };
const component = await mount( const component = await mount(
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; import { Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import _debounce from 'lodash/debounce';
import { route } from 'nextjs-routes';
import type { FormEvent, FocusEvent } from 'react'; import type { FormEvent, FocusEvent } from 'react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import SearchBarInput from './SearchBarInput'; import SearchBarInput from './SearchBarInput';
import SearchBarSuggest from './SearchBarSuggest'; import SearchBarSuggest from './SearchBarSuggest';
...@@ -26,7 +26,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -26,7 +26,7 @@ const SearchBar = ({ isHomepage }: Props) => {
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (searchTerm) { if (searchTerm) {
const url = link('search_results', undefined, { q: searchTerm }); const url = route({ pathname: '/search-results', query: { q: searchTerm } });
window.location.assign(url); window.location.assign(url);
} }
}, [ searchTerm ]); }, [ searchTerm ]);
......
import { chakra, Text, Flex, useColorModeValue, Icon, Box } from '@chakra-ui/react'; import { chakra, Text, Flex, useColorModeValue, Icon, Box } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SearchResultItem } from 'types/api/search'; import type { SearchResultItem } from 'types/api/search';
...@@ -6,7 +7,6 @@ import type { SearchResultItem } from 'types/api/search'; ...@@ -6,7 +7,6 @@ import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText'; import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
...@@ -22,17 +22,17 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm }: Props) => { ...@@ -22,17 +22,17 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm }: Props) => {
const url = (() => { const url = (() => {
switch (data.type) { switch (data.type) {
case 'token': { case 'token': {
return link('token_index', { hash: data.address }); return route({ pathname: '/token/[hash]', query: { hash: data.address } });
} }
case 'contract': case 'contract':
case 'address': { case 'address': {
return link('address_index', { id: data.address }); return route({ pathname: '/address/[hash]', query: { hash: data.address } });
} }
case 'transaction': { case 'transaction': {
return link('tx', { id: data.tx_hash }); return route({ pathname: '/tx/[hash]', query: { hash: data.tx_hash } });
} }
case 'block': { case 'block': {
return link('block', { id: String(data.block_number) }); return route({ pathname: '/block/[height]', query: { height: String(data.block_number) } });
} }
} }
})(); })();
......
...@@ -28,7 +28,7 @@ function composeSources(contract: SmartContract | undefined) { ...@@ -28,7 +28,7 @@ function composeSources(contract: SmartContract | undefined) {
const Sol2UmlDiagram = ({ addressHash }: Props) => { const Sol2UmlDiagram = ({ addressHash }: Props) => {
const contractQuery = useApiQuery<'contract', ResourceError>('contract', { const contractQuery = useApiQuery<'contract', ResourceError>('contract', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash), enabled: Boolean(addressHash),
refetchOnMount: false, refetchOnMount: false,
......
...@@ -3,7 +3,7 @@ import type { UseQueryResult } from '@tanstack/react-query'; ...@@ -3,7 +3,7 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
...@@ -16,7 +16,7 @@ const TokenContractInfo = ({ tokenQuery }: Props) => { ...@@ -16,7 +16,7 @@ const TokenContractInfo = ({ tokenQuery }: Props) => {
const router = useRouter(); const router = useRouter();
const contractQuery = useApiQuery('address', { const contractQuery = useApiQuery('address', {
pathParams: { id: router.query.hash?.toString() }, pathParams: { hash: router.query.hash?.toString() },
queryOptions: { enabled: Boolean(router.query.hash) }, queryOptions: { enabled: Boolean(router.query.hash) },
}); });
......
...@@ -4,11 +4,10 @@ import { useRouter } from 'next/router'; ...@@ -4,11 +4,10 @@ import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { scroller } from 'react-scroll'; import { scroller } from 'react-scroll';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import link from 'lib/link/link';
import type { TokenTabs } from 'ui/pages/Token'; import type { TokenTabs } from 'ui/pages/Token';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow'; import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
...@@ -26,9 +25,8 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -26,9 +25,8 @@ const TokenDetails = ({ tokenQuery }: Props) => {
}); });
const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => { const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => {
const newLink = link('token_index', { hash: router.query.hash }, { tab: tab });
router.push( router.push(
newLink, { pathname: '/token/[hash]', query: { hash: router.query.hash?.toString() || '', tab } },
undefined, undefined,
{ shallow: true }, { shallow: true },
); );
......
...@@ -2,7 +2,7 @@ import { Text } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { TokenHolders, TokenInfo } from 'types/api/tokenInfo'; import type { TokenHolders, TokenInfo } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo'; import type { TokenHolder, TokenInfo } from 'types/api/token';
import TokenHoldersListItem from './TokenHoldersListItem'; import TokenHoldersListItem from './TokenHoldersListItem';
......
...@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo'; import type { TokenHolder, TokenInfo } from 'types/api/token';
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 { Table, Tbody, Tr, Th } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo'; import type { TokenHolder, TokenInfo } from 'types/api/token';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import TokenHoldersTableItem from 'ui/token/TokenHolders/TokenHoldersTableItem'; import TokenHoldersTableItem from 'ui/token/TokenHolders/TokenHoldersTableItem';
......
...@@ -2,7 +2,7 @@ import { Tr, Td } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Tr, Td } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo'; import type { TokenHolder, TokenInfo } from 'types/api/token';
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 { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { base as tokenInstanse } from 'mocks/tokens/tokenInstance';
import TestApp from 'playwright/TestApp';
import TokenInventory from './TokenInventory';
test('base view +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 0 }}/>
<TokenInventory
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
inventoryQuery={{
data: {
items: [ tokenInstanse, tokenInstanse, tokenInstanse ],
next_page_params: { unique_token: 1 },
},
isPaginationVisible: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
pagination: { page: 1 },
}}
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Grid, Text, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { TokenInventoryResponse } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import TokenInventoryItem from './TokenInventoryItem';
type Props = {
inventoryQuery: UseQueryResult<TokenInventoryResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokenInventory = ({ inventoryQuery }: Props) => {
const isMobile = useIsMobile();
if (inventoryQuery.isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && inventoryQuery.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...inventoryQuery.pagination }/>
</ActionBar>
);
if (inventoryQuery.isLoading) {
return (
<>
{ bar }
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
</>
);
}
const items = inventoryQuery.data.items;
if (!items?.length) {
return <Text as="span">There are no tokens.</Text>;
}
return (
<>
{ bar }
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ items.map((item) => <TokenInventoryItem key={ item.token.address } item={ item }/>) }
</Grid></>
);
};
export default TokenInventory;
import { Flex, Link, Text, LinkBox, LinkOverlay, useColorModeValue, Hide } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { TokenInstance } from 'types/api/token';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import NftMedia from 'ui/shared/nft/NftMedia';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = { item: TokenInstance };
const NFTItem = ({ item }: Props) => {
const tokenLink = route({ pathname: '/token/[hash]/instance/[id]', query: { hash: item.token.address, id: item.id } });
return (
<LinkBox
w={{ base: '100%', lg: '210px' }}
border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px"
p="10px"
_hover={{ boxShadow: 'md' }}
fontSize="sm"
fontWeight={ 500 }
lineHeight="20px"
>
<LinkOverlay href={ tokenLink }>
<NftMedia
mb="18px"
imageUrl={ item.image_url }
animationUrl={ item.animation_url }
/>
</LinkOverlay>
{ item.id && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text>
<TruncatedTextTooltip label={ item.id }>
<Link
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ item.id }
</Link>
</TruncatedTextTooltip>
</Flex>
) }
{ item.owner && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary" mr={ 2 } lineHeight="24px">Owner</Text>
<Address>
<Hide below="lg" ssr={ false }><AddressIcon address={ item.owner } mr={ 1 }/></Hide>
<AddressLink hash={ item.owner.hash } alias={ item.owner.name } type="address" truncation="constant"/>
</Address>
</Flex>
) }
</LinkBox>
);
};
export default NFTItem;
...@@ -25,7 +25,7 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket ...@@ -25,7 +25,7 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="40%">Txn hash</Th> <Th width={ token.type === 'ERC-1155' ? '60%' : '80%' }>Txn hash</Th>
<Th width="164px">Method</Th> <Th width="164px">Method</Th>
<Th width="148px">From</Th> <Th width="148px">From</Th>
<Th width="36px" px={ 0 }/> <Th width="36px" px={ 0 }/>
......
import { Tr, Td, Tag, Text, Icon } from '@chakra-ui/react'; import { Tr, Td, Tag, Text, Icon, Grid } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -36,10 +36,12 @@ const TokenTransferTableItem = ({ ...@@ -36,10 +36,12 @@ const TokenTransferTableItem = ({
return ( return (
<Tr alignItems="top"> <Tr alignItems="top">
<Td> <Td>
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px"> <Grid alignItems="center" gridTemplateColumns="auto 130px" width="fit-content">
<Address display="inline-flex" fontWeight={ 600 } lineHeight="30px">
<AddressLink type="transaction" hash={ txHash }/> <AddressLink type="transaction" hash={ txHash }/>
</Address> </Address>
{ timestamp && <Text color="gray.500" fontWeight="400" mt="10px">{ timeAgo }</Text> } { timestamp && <Text color="gray.500" fontWeight="400" ml="10px">{ timeAgo }</Text> }
</Grid>
</Td> </Td>
<Td> <Td>
{ method ? <Tag colorScheme="gray">{ method }</Tag> : '-' } { method ? <Tag colorScheme="gray">{ method }</Tag> : '-' }
......
...@@ -8,7 +8,7 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,7 +8,7 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import TokenInstanceDetails from './TokenInstanceDetails'; import TokenInstanceDetails from './TokenInstanceDetails';
const API_URL_ADDRESS = buildApiUrl('address', { id: tokenInstanceMock.base.token.address }); const API_URL_ADDRESS = buildApiUrl('address', { hash: tokenInstanceMock.base.token.address });
const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', { const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', {
id: tokenInstanceMock.base.id, id: tokenInstanceMock.base.id,
hash: tokenInstanceMock.base.token.address, hash: tokenInstanceMock.base.token.address,
......
import { Box, Flex, Grid } from '@chakra-ui/react'; import { Box, Flex, Grid } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInstance } from 'types/api/tokens'; import type { TokenInstance } from 'types/api/token';
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';
......
...@@ -14,7 +14,7 @@ interface Props { ...@@ -14,7 +14,7 @@ interface Props {
const TokenInstanceCreatorAddress = ({ hash }: Props) => { const TokenInstanceCreatorAddress = ({ hash }: Props) => {
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
pathParams: { id: hash }, pathParams: { hash },
}); });
if (addressQuery.isError) { if (addressQuery.isError) {
......
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow'; import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
...@@ -30,7 +30,7 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => { ...@@ -30,7 +30,7 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
} }
const url = transfersCountQuery.data.transfers_count > 0 ? const url = transfersCountQuery.data.transfers_count > 0 ?
link('token_instance_item', { id, hash }, { tab: 'token_transfers' }) : route({ pathname: '/token/[hash]/instance/[id]', query: { hash, id, tab: 'token_transfers' } }) :
undefined; undefined;
return ( return (
......
...@@ -2,7 +2,7 @@ import { Hide, HStack, Show, Text } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Hide, HStack, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
......
import { Flex, Text, Tag, HStack, Grid, GridItem } from '@chakra-ui/react'; import { Flex, Text, Tag, HStack, Grid, GridItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask'; import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
......
import { Table, Tbody, Th, Tr } from '@chakra-ui/react'; import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
......
import { Box, Flex, Td, Tr, Text, Tag } from '@chakra-ui/react'; import { Box, Flex, Td, Tr, Text, Tag } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask'; import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
......
import { Flex, Link, Text, Icon, Box } from '@chakra-ui/react'; import { Flex, Link, Text, Icon, Box } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import nftIcon from 'icons/nft_shield.svg'; import nftIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet'; import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
...@@ -17,7 +17,7 @@ interface Props { ...@@ -17,7 +17,7 @@ interface Props {
const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props) => { const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props) => {
const num = value === '1' ? '' : value; const num = value === '1' ? '' : value;
const url = link('token_instance_item', { hash: hash, id: tokenId }); const url = route({ pathname: '/token/[hash]/instance/[id]', query: { hash: hash, id: tokenId } });
return ( return (
<Flex alignItems="center" columnGap={ 3 } rowGap={ 2 } flexWrap="wrap"> <Flex alignItems="center" columnGap={ 3 } rowGap={ 2 } flexWrap="wrap">
......
...@@ -8,10 +8,10 @@ import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder'; ...@@ -8,10 +8,10 @@ import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder';
import TxDetails from './TxDetails'; import TxDetails from './TxDetails';
const API_URL = buildApiUrl('tx', { id: '1' }); const API_URL = buildApiUrl('tx', { hash: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: 1 }, query: { hash: 1 },
}, },
}; };
...@@ -124,3 +124,20 @@ test('pending', async({ mount, page }) => { ...@@ -124,3 +124,20 @@ test('pending', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with actions uniswap +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withActionsUniswap),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await insertAdPlaceholder(page);
await expect(component).toHaveScreenshot();
});
...@@ -39,6 +39,7 @@ import RawInputData from 'ui/shared/RawInputData'; ...@@ -39,6 +39,7 @@ import RawInputData from 'ui/shared/RawInputData';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
import TxDetailsActions from 'ui/tx/details/TxDetailsActions';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton'; import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers'; import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxRevertReason from 'ui/tx/details/TxRevertReason'; import TxRevertReason from 'ui/tx/details/TxRevertReason';
...@@ -89,6 +90,8 @@ const TxDetails = () => { ...@@ -89,6 +90,8 @@ const TxDetails = () => {
...toAddress.watchlist_names || [], ...toAddress.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>); ].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const actionsExist = data.actions && data.actions.length > 0;
const executionSuccessBadge = toAddress.is_contract && data.result === 'success' ? ( const executionSuccessBadge = toAddress.is_contract && data.result === 'success' ? (
<Tooltip label="Contract execution completed"> <Tooltip label="Contract execution completed">
<chakra.span display="inline-flex" ml={ 2 } mr={ 1 }> <chakra.span display="inline-flex" ml={ 2 } mr={ 1 }>
...@@ -104,6 +107,16 @@ const TxDetails = () => { ...@@ -104,6 +107,16 @@ const TxDetails = () => {
</Tooltip> </Tooltip>
) : null; ) : null;
const divider = (
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor="divider"
/>
);
return ( return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}> <Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}>
{ socketStatus && ( { socketStatus && (
...@@ -180,13 +193,16 @@ const TxDetails = () => { ...@@ -180,13 +193,16 @@ const TxDetails = () => {
<AdBanner/> <AdBanner/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<GridItem
colSpan={{ base: undefined, lg: 2 }} { divider }
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }} { actionsExist && (
borderBottom="1px solid" <>
borderColor="divider" <TxDetailsActions actions={ data.actions }/>
/> { divider }
</>
) }
<DetailsInfoItem <DetailsInfoItem
title="From" title="From"
hint="Address (external or contract) sending the transaction." hint="Address (external or contract) sending the transaction."
...@@ -237,13 +253,8 @@ const TxDetails = () => { ...@@ -237,13 +253,8 @@ const TxDetails = () => {
</DetailsInfoItem> </DetailsInfoItem>
{ data.token_transfers && <TxDetailsTokenTransfers data={ data.token_transfers } txHash={ data.hash }/> } { data.token_transfers && <TxDetailsTokenTransfers data={ data.token_transfers } txHash={ data.hash }/> }
<GridItem { divider }
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor="divider"
/>
<DetailsInfoItem <DetailsInfoItem
title="Value" title="Value"
hint="Value sent in the native token (and USD) if applicable." hint="Value sent in the native token (and USD) if applicable."
......
...@@ -9,11 +9,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -9,11 +9,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxInternals from './TxInternals'; import TxInternals from './TxInternals';
const TX_HASH = txMock.base.hash; const TX_HASH = txMock.base.hash;
const API_URL_TX = buildApiUrl('tx', { id: TX_HASH }); const API_URL_TX = buildApiUrl('tx', { hash: TX_HASH });
const API_URL_TX_INTERNALS = buildApiUrl('tx_internal_txs', { id: TX_HASH }); const API_URL_TX_INTERNALS = buildApiUrl('tx_internal_txs', { hash: TX_HASH });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: TX_HASH }, query: { hash: TX_HASH },
}, },
}; };
......
...@@ -76,7 +76,7 @@ const TxInternals = () => { ...@@ -76,7 +76,7 @@ const TxInternals = () => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'tx_internal_txs', resourceName: 'tx_internal_txs',
pathParams: { id: txInfo.data?.hash }, pathParams: { hash: txInfo.data?.hash },
options: { options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
}, },
......
...@@ -16,7 +16,7 @@ const TxLogs = () => { ...@@ -16,7 +16,7 @@ const TxLogs = () => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'tx_logs', resourceName: 'tx_logs',
pathParams: { id: txInfo.data?.hash }, pathParams: { hash: txInfo.data?.hash },
options: { options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
}, },
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import getQueryParamString from 'lib/router/getQueryParamString';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
...@@ -12,12 +13,13 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; ...@@ -12,12 +13,13 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxRawTrace = () => { const TxRawTrace = () => {
const router = useRouter(); const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useApiQuery('tx_raw_trace', { const { data, isLoading, isError } = useApiQuery('tx_raw_trace', {
pathParams: { id: router.query.id?.toString() }, pathParams: { hash },
queryOptions: { queryOptions: {
enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status), enabled: Boolean(hash) && Boolean(txInfo.data?.status),
}, },
}); });
......
...@@ -2,7 +2,7 @@ import { Hide, Show, Text } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Hide, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
...@@ -36,7 +36,7 @@ const TxTokenTransfer = () => { ...@@ -36,7 +36,7 @@ const TxTokenTransfer = () => {
const tokenTransferQuery = useQueryWithPages({ const tokenTransferQuery = useQueryWithPages({
resourceName: 'tx_token_transfers', resourceName: 'tx_token_transfers',
pathParams: { id: txsInfo.data?.hash.toString() }, pathParams: { hash: txsInfo.data?.hash.toString() },
options: { enabled: Boolean(txsInfo.data?.status && txsInfo.data?.hash) }, options: { enabled: Boolean(txsInfo.data?.status && txsInfo.data?.hash) },
filters: { type: typeFilter }, filters: { type: typeFilter },
}); });
......
import { Flex, Link, Icon, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import type { TxAction, TxActionGeneral } from 'types/api/txAction';
import appConfig from 'configs/app/config';
import uniswapIcon from 'icons/uniswap.svg';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
interface Props {
action: TxAction;
}
function getActionText(actionType: TxActionGeneral['type']) {
switch (actionType) {
case 'mint': return [ 'Add', 'Liquidity to' ];
case 'burn': return [ 'Remove', 'Liquidity from' ];
case 'collect': return [ 'Collect', 'From' ];
case 'swap': return [ 'Swap', 'On' ];
}
}
const TxDetailsAction = ({ action }: Props) => {
const { protocol, type, data } = action;
if (protocol !== 'uniswap_v3') {
return null;
}
switch (type) {
case 'mint':
case 'burn':
case 'collect':
case 'swap': {
const amount0 = BigNumber(data.amount0).toFormat();
const amount1 = BigNumber(data.amount1).toFormat();
const [ text0, text1 ] = getActionText(type);
return (
<Flex flexWrap="wrap" columnGap={ 1 } rowGap={ 2 } alignItems="center">
<chakra.span color="text_secondary">{ text0 }: </chakra.span>
<chakra.span fontWeight={ 600 }>{ amount0 }</chakra.span>
<TokenSnippet
name={ data.symbol0 === 'Ether' ? appConfig.network.currency.symbol : data.symbol0 }
hash={ data.symbol0 === 'Ether' ? appConfig.network.currency.address || '' : data.address1 }
w="auto"
columnGap={ 1 }
logoSize={ 5 }
isDisabled={ data.symbol0 === 'Ether' }
/>
<chakra.span color="text_secondary">{ type === 'swap' ? 'For' : 'And' }: </chakra.span>
<chakra.span fontWeight={ 600 }>{ amount1 }</chakra.span>
<TokenSnippet
name={ data.symbol1 === 'Ether' ? appConfig.network.currency.symbol : data.symbol1 }
hash={ data.symbol1 === 'Ether' ? appConfig.network.currency.address || '' : data.address1 }
w="auto"
columnGap={ 1 }
logoSize={ 5 }
isDisabled={ data.symbol1 === 'Ether' }
/>
<chakra.span color="text_secondary">{ text1 } </chakra.span>
<Flex columnGap={ 1 }>
<Icon as={ uniswapIcon } boxSize={ 5 } color="white" bgColor="#ff007a" borderRadius="full" p="2px"/>
<chakra.span color="text_secondary">Uniswap V3</chakra.span>
</Flex>
</Flex>
);
}
case 'mint_nft' : {
return (
<div>
<Flex rowGap={ 2 } flexWrap="wrap" alignItems="center" whiteSpace="pre-wrap">
<chakra.span>Mint of </chakra.span>
<TokenSnippet
name={ data.name }
hash={ data.address }
symbol={ trimTokenSymbol(data.symbol) }
w="auto"
columnGap={ 1 }
logoSize={ 5 }
rowGap={ 2 }
flexWrap="wrap"
/>
<chakra.span> to </chakra.span>
<AddressLink hash={ data.to } type="address" truncation="constant"/>
</Flex>
<Flex columnGap={ 1 } rowGap={ 2 } pl={ 3 } flexDirection="column" mt={ 2 }>
{
data.ids.map((id: string) => {
const url = route({ pathname: '/token/[hash]/instance/[id]', query: { hash: data.address, id } });
return (
<Flex key={ data.address + id } whiteSpace="pre-wrap">
<span>1 of </span>
<chakra.span color="text_secondary">Token ID [</chakra.span>
<Link href={ url }>{ id }</Link>
<chakra.span color="text_secondary">]</chakra.span>
</Flex>
);
})
}
</Flex>
</div>
);
}
default:
return null;
}
};
export default React.memo(TxDetailsAction);
import { Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { TxAction } from 'types/api/txAction';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TxDetailsAction from './TxDetailsAction';
interface Props {
actions: Array<TxAction>;
}
const TxDetailsActions = ({ actions }: Props) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const [ hasScroll, setHasScroll ] = React.useState(false);
const gradientStartColor = useColorModeValue('whiteAlpha.600', 'blackAlpha.600');
const gradientEndColor = useColorModeValue('whiteAlpha.900', 'blackAlpha.900');
React.useEffect(() => {
if (!containerRef.current) {
return;
}
setHasScroll(containerRef.current.scrollHeight > containerRef.current.clientHeight);
}, []);
return (
<DetailsInfoItem
title="Transaction Action"
hint="Highlighted events of the transaction"
note={ hasScroll ? 'Scroll to see more' : undefined }
position="relative"
>
<Flex
flexDirection="column"
alignItems="flex-start"
rowGap={ 5 }
w="100%"
maxH="200px"
overflowY="scroll"
ref={ containerRef }
_after={ hasScroll ? {
position: 'absolute',
content: '""',
bottom: 0,
left: 0,
right: '20px',
height: '48px',
bgGradient: `linear(to-b, ${ gradientStartColor } 37.5%, ${ gradientEndColor } 77.5%)`,
} : undefined }
pr={ hasScroll ? 5 : 0 }
pb={ hasScroll ? 10 : 0 }
>
{ actions.map((action, index: number) => <TxDetailsAction key={ index } action={ action }/>) }
</Flex>
</DetailsInfoItem>
);
};
export default React.memo(TxDetailsActions);
import { Icon, GridItem, Show, Flex } from '@chakra-ui/react'; import { Icon, GridItem, Show, Flex } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import tokenIcon from 'icons/token.svg'; import tokenIcon from 'icons/token.svg';
import link from 'lib/link/link';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers'; import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
...@@ -25,7 +25,7 @@ const TOKEN_TRANSFERS_TYPES = [ ...@@ -25,7 +25,7 @@ const TOKEN_TRANSFERS_TYPES = [
const VISIBLE_ITEMS_NUM = 3; const VISIBLE_ITEMS_NUM = 3;
const TxDetailsTokenTransfers = ({ data, txHash }: Props) => { const TxDetailsTokenTransfers = ({ data, txHash }: Props) => {
const viewAllUrl = link('tx', { id: txHash }, { tab: 'token_transfers' }); const viewAllUrl = route({ pathname: '/tx/[hash]', query: { hash: txHash, tab: 'token_transfers' } });
const formattedData = data.reduce(flattenTotal, []); const formattedData = data.reduce(flattenTotal, []);
const transferGroups = TOKEN_TRANSFERS_TYPES.map((group) => ({ const transferGroups = TOKEN_TRANSFERS_TYPES.map((group) => ({
......
...@@ -9,6 +9,7 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -9,6 +9,7 @@ import type { Transaction } from 'types/api/transaction';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import delay from 'lib/delay'; import delay from 'lib/delay';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -25,11 +26,12 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params ...@@ -25,11 +26,12 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>(); const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>();
const hash = getQueryParamString(router.query.hash);
const queryResult = useApiQuery<'tx', { status: number }>('tx', { const queryResult = useApiQuery<'tx', { status: number }>('tx', {
pathParams: { id: router.query.id?.toString() }, pathParams: { hash },
queryOptions: { queryOptions: {
enabled: Boolean(router.query.id), enabled: Boolean(hash),
refetchOnMount: false, refetchOnMount: false,
}, },
}); });
...@@ -38,10 +40,10 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params ...@@ -38,10 +40,10 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => { const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => {
updateDelay && await delay(updateDelay); updateDelay && await delay(updateDelay);
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: getResourceKey('tx', { pathParams: { id: router.query.id?.toString() } }), queryKey: getResourceKey('tx', { pathParams: { hash } }),
}); });
onTxStatusUpdate?.(); onTxStatusUpdate?.();
}, [ onTxStatusUpdate, queryClient, router.query.id, updateDelay ]); }, [ onTxStatusUpdate, queryClient, hash, updateDelay ]);
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
setSocketStatus('close'); setSocketStatus('close');
...@@ -52,7 +54,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params ...@@ -52,7 +54,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
}, []); }, []);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `transactions:${ router.query.id }`, topic: `transactions:${ hash }`,
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: isLoading || isError || data.status !== null, isDisabled: isLoading || isError || data.status !== null,
......
...@@ -12,7 +12,7 @@ interface Props { ...@@ -12,7 +12,7 @@ interface Props {
const TxAdditionalInfoContainer = ({ hash }: Props) => { const TxAdditionalInfoContainer = ({ hash }: Props) => {
const { data, isError, isLoading } = useApiQuery('tx', { const { data, isError, isLoading } = useApiQuery('tx', {
pathParams: { id: hash }, pathParams: { hash },
queryOptions: { queryOptions: {
refetchOnMount: false, refetchOnMount: false,
}, },
......
import { Box, Heading, Text, Flex } from '@chakra-ui/react'; import { Box, Heading, Text, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import getValueWithUnit from 'lib/getValueWithUnit'; import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
...@@ -90,7 +90,7 @@ const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => { ...@@ -90,7 +90,7 @@ const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => {
<Text fontWeight="600" as="span">{ tx.position }</Text> <Text fontWeight="600" as="span">{ tx.position }</Text>
</Box> </Box>
</Box> </Box>
<LinkInternal fontSize="sm" href={ link('tx', { id: tx.hash }) }>More details</LinkInternal> <LinkInternal fontSize="sm" href={ route({ pathname: '/tx/[hash]', query: { hash: tx.hash } }) }>More details</LinkInternal>
</> </>
); );
}; };
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
Icon, Icon,
Text, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -14,7 +15,6 @@ import rightArrowIcon from 'icons/arrows/east.svg'; ...@@ -14,7 +15,6 @@ import rightArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg'; import transactionIcon from 'icons/transactions.svg';
import getValueWithUnit from 'lib/getValueWithUnit'; import getValueWithUnit from 'lib/getValueWithUnit';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
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';
...@@ -88,7 +88,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: ...@@ -88,7 +88,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
{ showBlockInfo && tx.block !== null && ( { showBlockInfo && tx.block !== null && (
<Box mt={ 2 }> <Box mt={ 2 }>
<Text as="span">Block </Text> <Text as="span">Block </Text>
<LinkInternal href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</LinkInternal> <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: tx.block.toString() } }) }>{ tx.block }</LinkInternal>
</Box> </Box>
) } ) }
<Flex alignItems="center" height={ 6 } mt={ 6 }> <Flex alignItems="center" height={ 6 } mt={ 6 }>
......
...@@ -10,13 +10,13 @@ import { ...@@ -10,13 +10,13 @@ import {
Hide, Hide,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
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';
...@@ -99,7 +99,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -99,7 +99,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
</Td> </Td>
{ showBlockInfo && ( { showBlockInfo && (
<Td> <Td>
{ tx.block && <LinkInternal href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</LinkInternal> } { tx.block && <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: tx.block.toString() } }) }>{ tx.block }</LinkInternal> }
</Td> </Td>
) } ) }
<Show above="xl" ssr={ false }> <Show above="xl" ssr={ false }>
......
...@@ -4897,6 +4897,14 @@ anymatch@^3.0.3: ...@@ -4897,6 +4897,14 @@ anymatch@^3.0.3:
normalize-path "^3.0.0" normalize-path "^3.0.0"
picomatch "^2.0.4" picomatch "^2.0.4"
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
aproba@^1.0.3: aproba@^1.0.3:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
...@@ -5161,6 +5169,11 @@ bignumber.js@^9.1.0: ...@@ -5161,6 +5169,11 @@ bignumber.js@^9.1.0:
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.0.tgz#8d340146107fe3a6cb8d40699643c302e8773b62" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.0.tgz#8d340146107fe3a6cb8d40699643c302e8773b62"
integrity sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A== integrity sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bind-decorator@^1.0.11: bind-decorator@^1.0.11:
version "1.0.11" version "1.0.11"
resolved "https://registry.yarnpkg.com/bind-decorator/-/bind-decorator-1.0.11.tgz#e41bc06a1f65dd9cec476c91c5daf3978488252f" resolved "https://registry.yarnpkg.com/bind-decorator/-/bind-decorator-1.0.11.tgz#e41bc06a1f65dd9cec476c91c5daf3978488252f"
...@@ -5222,7 +5235,7 @@ brace-expansion@^2.0.1: ...@@ -5222,7 +5235,7 @@ brace-expansion@^2.0.1:
dependencies: dependencies:
balanced-match "^1.0.0" balanced-match "^1.0.0"
braces@^3.0.2: braces@^3.0.2, braces@~3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
...@@ -5415,6 +5428,21 @@ char-regex@^1.0.2: ...@@ -5415,6 +5428,21 @@ char-regex@^1.0.2:
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
ci-info@^3.2.0: ci-info@^3.2.0:
version "3.6.1" version "3.6.1"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.1.tgz#7594f1c95cb7fdfddee7af95a13af7dbc67afdcf" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.1.tgz#7594f1c95cb7fdfddee7af95a13af7dbc67afdcf"
...@@ -7421,7 +7449,7 @@ get-symbol-description@^1.0.0: ...@@ -7421,7 +7449,7 @@ get-symbol-description@^1.0.0:
call-bind "^1.0.2" call-bind "^1.0.2"
get-intrinsic "^1.1.1" get-intrinsic "^1.1.1"
glob-parent@^5.1.2: glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
...@@ -7778,6 +7806,13 @@ is-bigint@^1.0.1: ...@@ -7778,6 +7806,13 @@ is-bigint@^1.0.1:
dependencies: dependencies:
has-bigints "^1.0.1" has-bigints "^1.0.1"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-boolean-object@^1.1.0: is-boolean-object@^1.1.0:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
...@@ -7844,7 +7879,7 @@ is-generator-function@^1.0.7: ...@@ -7844,7 +7879,7 @@ is-generator-function@^1.0.7:
dependencies: dependencies:
has-tostringtag "^1.0.0" has-tostringtag "^1.0.0"
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
...@@ -9039,6 +9074,13 @@ next@12.2.5: ...@@ -9039,6 +9074,13 @@ next@12.2.5:
"@next/swc-win32-ia32-msvc" "12.2.5" "@next/swc-win32-ia32-msvc" "12.2.5"
"@next/swc-win32-x64-msvc" "12.2.5" "@next/swc-win32-x64-msvc" "12.2.5"
nextjs-routes@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/nextjs-routes/-/nextjs-routes-1.0.8.tgz#7604fe12dd0132c5a4c61e8d4a6c00c19cf615fb"
integrity sha512-EWBrzT0VS+SGcrhEbCUGnDlEAS6tjJSiecbSA7x75pBbcW9IYkYw7JxDMNqFhiSrvtYAL8yIWFwbQ8x0Jmz7wg==
dependencies:
chokidar "^3.5.3"
node-addon-api@^2.0.0: node-addon-api@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
...@@ -9080,7 +9122,7 @@ node-releases@^2.0.6: ...@@ -9080,7 +9122,7 @@ node-releases@^2.0.6:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
normalize-path@^3.0.0: normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
...@@ -9386,7 +9428,7 @@ picocolors@^1.0.0: ...@@ -9386,7 +9428,7 @@ picocolors@^1.0.0:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
...@@ -9931,6 +9973,13 @@ readable-stream@^4.0.0: ...@@ -9931,6 +9973,13 @@ readable-stream@^4.0.0:
events "^3.3.0" events "^3.3.0"
process "^0.11.10" process "^0.11.10"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
real-require@^0.1.0: real-require@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.1.0.tgz#736ac214caa20632847b7ca8c1056a0767df9381" resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.1.0.tgz#736ac214caa20632847b7ca8c1056a0767df9381"
......
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