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 = {
erc20a, items: [
erc20b, erc20a,
erc20c, erc20b,
erc721a, erc20c,
erc721b, ],
erc721c, };
erc1155withoutName,
erc1155a, export const erc721List = {
erc1155b, items: [
]; erc721a,
erc721b,
export const longValuesList = [ erc721c,
erc20LongSymbol, ],
erc721LongSymbol, };
erc1155LongId,
]; export const erc1155List = {
items: [
erc1155withoutName,
erc1155a,
erc1155b,
],
};
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,23 +227,25 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -225,23 +227,25 @@ 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 }/>
<Tooltip label="Reset filter"> { isMobile ? <HashStringShorten hash={ tokenFilter }/> : tokenFilter }
<Flex> <Tooltip label="Reset filter">
<Icon <Flex>
as={ crossIcon } <Icon
boxSize={ 6 } as={ crossIcon }
ml={ 1 } boxSize={ 6 }
color={ resetTokenIconColor } ml={ 1 }
cursor="pointer" color={ resetTokenIconColor }
_hover={{ color: resetTokenIconHoverColor }} cursor="pointer"
onClick={ resetTokenFilter } _hover={{ color: resetTokenIconHoverColor }}
/> onClick={ resetTokenFilter }
</Flex> />
</Tooltip> </Flex>
</Tooltip>
</Flex>
</Flex> </Flex>
); );
......
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,32 +20,50 @@ const nextPageParams = { ...@@ -22,32 +20,50 @@ const nextPageParams = {
value: 1, value: 1,
}; };
test('erc20 +@mobile +@dark-mode', async({ mount, page }) => { const test = base.extend({
page: async({ page }, use) => {
const response20 = {
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,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({
status: 200,
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 = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc20' }, query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' },
isReady: true, isReady: true,
}, },
}; };
const response20 = {
items: [ tokenBalanceMock.erc20a, tokenBalanceMock.erc20b, tokenBalanceMock.erc20c, tokenBalanceMock.erc20d ],
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-20', (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 }}/>
...@@ -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 given function signature, or if no data was supplied at all and there is no receive Ether function. `The fallback function is executed on a call to the contract if none of the other functions match
The fallback function always receives data, but in order to also receive Ether it must be marked payable.` } the given function signature, or if no data was supplied at all and there is no receive Ether function.
placement="top" The fallback function always receives data, but in order to also receive Ether it must be marked payable.`
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={
This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). `The receive function is executed on a call to the contract with empty calldata.
If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).
If neither a receive Ether nor a payable fallback function is present, If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
the contract cannot receive Ether through regular transactions and throws an exception.` } If neither a receive Ether nor a payable fallback function is present,
placement="top" the contract cannot receive Ether through regular transactions and throws an exception.`
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 content = METHODS[method] || null; const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) {
return;
}
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> <>
<Controller <ContractVerificationFormRow>
name="constructor_args" <Controller
control={ control } name="autodetect_constructor_args"
render={ renderControl } control={ control }
/> render={ renderControl }
</ContractVerificationFormRow> defaultValue={ true }
/>
</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 loadOptions = React.useCallback(async(inputValue: string) => {
return options
.filter(({ label }) => {
if (!inputValue) {
return !label.toLowerCase().includes('nightly');
}
const handleCheckboxChange = React.useCallback(() => { return label.toLowerCase().includes(inputValue.toLowerCase());
if (isNightly) { })
const field = getValues('compiler'); .slice(0, OPTIONS_LIMIT);
field.value.includes('nightly') && resetField('compiler', { defaultValue: null }); }, [ options ]);
}
setIsNightly(prev => !prev);
}, [ getValues, isNightly, resetField ]);
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) => (
<FileSnippet <Box key={ file.name + file.lastModified + index }>
key={ file.name + file.lastModified } <FileSnippet
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' }> { () => (
Upload file{ multiple ? 's' : '' } <Flex
</Button> 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' : '' }
</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 }
position="absolute" tooltipProps={ TOOLTIP_PROPS }
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }} boxSize={ 6 }
right="10px" color={ infoColor }
cursor="pointer" position="absolute"
> top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
<Icon right="10px"
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>
<RoutedTabs { tokenQuery.isLoading ? <SkeletonTabs/> : (
tabs={ tabs } <RoutedTabs
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } } tabs={ tabs }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null } tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
stickyEnabled={ !isMobile } rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
/> 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">
<AddressLink type="transaction" hash={ txHash }/> <Address display="inline-flex" fontWeight={ 600 } lineHeight="30px">
</Address> <AddressLink type="transaction" hash={ txHash }/>
{ timestamp && <Text color="gray.500" fontWeight="400" mt="10px">{ timeAgo }</Text> } </Address>
{ 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