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,
}); });
}; };
} }
This diff is collapsed.
...@@ -6,10 +6,10 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -6,10 +6,10 @@ import useFetch from 'lib/hooks/useFetch';
import buildUrl from './buildUrl'; import buildUrl from './buildUrl';
import { RESOURCES } from './resources'; import { RESOURCES } from './resources';
import type { ApiResource } from './resources'; import type { ApiResource, ResourceName, ResourcePathParams } from './resources';
export interface Params { export interface Params<R extends ResourceName> {
pathParams?: Record<string, string | undefined>; pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | Array<string> | number | undefined>; queryParams?: Record<string, string | Array<string> | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>; fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
} }
...@@ -17,12 +17,12 @@ export interface Params { ...@@ -17,12 +17,12 @@ export interface Params {
export default function useApiFetch() { export default function useApiFetch() {
const fetch = useFetch(); const fetch = useFetch();
return React.useCallback(<R extends keyof typeof RESOURCES, SuccessType = unknown, ErrorType = unknown>( return React.useCallback(<R extends ResourceName, SuccessType = unknown, ErrorType = unknown>(
resourceName: R, resourceName: R,
{ pathParams, queryParams, fetchParams }: Params = {}, { pathParams, queryParams, fetchParams }: Params<R> = {},
) => { ) => {
const resource: ApiResource = RESOURCES[resourceName]; const resource: ApiResource = RESOURCES[resourceName];
const url = buildUrl(resource, pathParams, queryParams); const url = buildUrl(resourceName, pathParams, queryParams);
return fetch<SuccessType, ErrorType>(url, { return fetch<SuccessType, ErrorType>(url, {
credentials: 'include', credentials: 'include',
...(resource.endpoint && appConfig.host === 'localhost' ? { ...(resource.endpoint && appConfig.host === 'localhost' ? {
......
...@@ -5,7 +5,7 @@ import type { ResourceError, ResourceName, ResourcePayload } from './resources'; ...@@ -5,7 +5,7 @@ import type { ResourceError, ResourceName, ResourcePayload } from './resources';
import type { Params as ApiFetchParams } from './useApiFetch'; import type { Params as ApiFetchParams } from './useApiFetch';
import useApiFetch from './useApiFetch'; import useApiFetch from './useApiFetch';
export interface Params<R extends ResourceName, E = unknown> extends ApiFetchParams { export interface Params<R extends ResourceName, E = unknown> extends ApiFetchParams<R> {
queryOptions?: Omit<UseQueryOptions<unknown, ResourceError<E>, ResourcePayload<R>>, 'queryKey' | 'queryFn'>; queryOptions?: Omit<UseQueryOptions<unknown, ResourceError<E>, ResourcePayload<R>>, 'queryKey' | 'queryFn'>;
} }
......
import type BigNumber from 'bignumber.js';
export default function sumBnReducer(result: BigNumber, item: BigNumber) {
return result.plus(item);
}
...@@ -12,3 +12,6 @@ export const DAY = 24 * HOUR; ...@@ -12,3 +12,6 @@ export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY; export const WEEK = 7 * DAY;
export const MONTH = 30 * DAY; export const MONTH = 30 * DAY;
export const YEAR = 365 * DAY; export const YEAR = 365 * DAY;
export const Kb = 1_000;
export const Mb = 1_000 * Kb;
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { ZERO } from 'lib/consts';
interface Params { interface Params {
value: string; value: string;
exchangeRate?: string | null; exchangeRate?: string | null;
...@@ -13,10 +15,11 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal ...@@ -13,10 +15,11 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat(); const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
let usdResult: string | undefined; let usdResult: string | undefined;
let usdBn = ZERO;
if (exchangeRate) { if (exchangeRate) {
const exchangeRateBn = new BigNumber(exchangeRate); const exchangeRateBn = new BigNumber(exchangeRate);
const usdBn = valueCurr.times(exchangeRateBn); usdBn = valueCurr.times(exchangeRateBn);
if (accuracyUsd && !usdBn.isEqualTo(0)) { if (accuracyUsd && !usdBn.isEqualTo(0)) {
const usdBnDp = usdBn.dp(accuracyUsd); const usdBnDp = usdBn.dp(accuracyUsd);
usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat(); usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat();
...@@ -25,5 +28,5 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal ...@@ -25,5 +28,5 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal
} }
} }
return { valueStr: valueResult, usd: usdResult }; return { valueStr: valueResult, usd: usdResult, usdBn };
} }
...@@ -11,7 +11,7 @@ export interface Params { ...@@ -11,7 +11,7 @@ export interface Params {
method?: RequestInit['method']; method?: RequestInit['method'];
headers?: RequestInit['headers']; headers?: RequestInit['headers'];
signal?: RequestInit['signal']; signal?: RequestInit['signal'];
body?: Record<string, unknown>; body?: Record<string, unknown> | FormData;
credentials?: RequestCredentials; credentials?: RequestCredentials;
} }
...@@ -20,13 +20,27 @@ export default function useFetch() { ...@@ -20,13 +20,27 @@ export default function useFetch() {
const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {}; const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => { return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => {
const hasBody = params?.method && ![ 'GET', 'HEAD' ].includes(params.method); const _body = params?.body;
const isFormData = _body instanceof FormData;
const isBodyAllowed = params?.method && ![ 'GET', 'HEAD' ].includes(params.method);
const body: FormData | string | undefined = (() => {
if (!isBodyAllowed) {
return;
}
if (isFormData) {
return _body;
}
return JSON.stringify({ ..._body, _csrf_token: token });
})();
const reqParams = { const reqParams = {
...params, ...params,
body: hasBody ? JSON.stringify({ ...params.body, _csrf_token: token }) : undefined, body,
headers: { headers: {
...(hasBody ? { 'Content-type': 'application/json' } : undefined), ...(isBodyAllowed && !isFormData ? { 'Content-type': 'application/json' } : undefined),
...params?.headers, ...params?.headers,
}, },
}; };
......
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import link from 'lib/link/link'; import appConfig from 'configs/app/config';
export default function useLoginUrl() { export default function useLoginUrl() {
const router = useRouter(); const router = useRouter();
return link('auth', {}, { path: router.asPath }); return appConfig.authUrl + route({ pathname: '/auth/auth0', query: { path: router.asPath } });
} }
import { useRouter } from 'next/router';
import type { Route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -14,40 +16,76 @@ import tokensIcon from 'icons/token.svg'; ...@@ -14,40 +16,76 @@ import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg'; import transactionsIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import watchlistIcon from 'icons/watchlist.svg'; import watchlistIcon from 'icons/watchlist.svg';
import link from 'lib/link/link';
import useCurrentRoute from 'lib/link/useCurrentRoute';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
export default function useNavItems() { export interface NavItem {
text: string;
nextRoute: Route;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
isActive?: boolean;
isNewUi?: boolean;
}
interface ReturnType {
mainNavItems: Array<NavItem>;
accountNavItems: Array<NavItem>;
profileItem: NavItem;
}
export default function useNavItems(): ReturnType {
const isMarketplaceFilled = appConfig.marketplaceAppList.length > 0; const isMarketplaceFilled = appConfig.marketplaceAppList.length > 0;
const currentRoute = useCurrentRoute()(); const router = useRouter();
const pathname = router.pathname;
return React.useMemo(() => { return React.useMemo(() => {
const mainNavItems = [ const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: true }, { text: 'Blocks', nextRoute: { pathname: '/blocks' as const }, icon: blocksIcon, isActive: pathname.startsWith('/block'), isNewUi: true },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: true }, { text: 'Transactions', nextRoute: { pathname: '/txs' as const }, icon: transactionsIcon, isActive: pathname.startsWith('/tx'), isNewUi: true },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute.startsWith('token'), isNewUi: true }, { text: 'Tokens', nextRoute: { pathname: '/tokens' as const }, icon: tokensIcon, isActive: pathname.startsWith('/token'), isNewUi: true },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: true }, { text: 'Accounts', nextRoute: { pathname: '/accounts' as const }, icon: walletIcon, isActive: pathname === '/accounts', isNewUi: true },
isMarketplaceFilled ? isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute.startsWith('app'), isNewUi: true } : null, { text: 'Apps', nextRoute: { pathname: '/apps' as const }, icon: appsIcon, isActive: pathname.startsWith('/app'), isNewUi: true } : null,
{ text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: true }, { text: 'Charts & stats', nextRoute: { pathname: '/stats' as const }, icon: statsIcon, isActive: pathname === '/stats', isNewUi: true },
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other' // there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
// examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/ // examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/
// at this stage custom menu items is under development, we will implement it later // at this stage custom menu items is under development, we will implement it later
// { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' }, // { text: 'Other', url: link('other'), icon: gearIcon, isActive: pathname === 'other' },
].filter(notEmpty); ].filter(notEmpty);
const accountNavItems = [ const accountNavItems = [
{ text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist', isNewUi: true }, {
{ text: 'Private tags', url: link('private_tags'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags'), isNewUi: true }, text: 'Watchlist',
{ text: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags', isNewUi: true }, nextRoute: { pathname: '/account/watchlist' as const },
{ text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys', isNewUi: true }, icon: watchlistIcon,
{ text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi', isNewUi: true }, isActive: pathname === '/account/watchlist',
isNewUi: true,
},
{
text: 'Private tags',
nextRoute: { pathname: '/account/tag_address' as const },
icon: privateTagIcon,
isActive: pathname === '/account/tag_address',
isNewUi: true,
},
{
text: 'Public tags',
nextRoute: { pathname: '/account/public_tags_request' as const },
icon: publicTagIcon, isActive: pathname === '/account/public_tags_request', isNewUi: true,
},
{ text: 'API keys', nextRoute: { pathname: '/account/api_key' as const }, icon: apiKeysIcon, isActive: pathname === '/account/api_key', isNewUi: true },
{
text: 'Custom ABI',
nextRoute: { pathname: '/account/custom_abi' as const },
icon: abiIcon,
isActive: pathname === '/account/custom_abi',
isNewUi: true,
},
]; ];
const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile', isNewUi: true }; const profileItem = {
text: 'My profile', nextRoute: { pathname: '/auth/profile' as const }, icon: profileIcon, isActive: pathname === '/auth/profile', isNewUi: true };
return { mainNavItems, accountNavItems, profileItem }; return { mainNavItems, accountNavItems, profileItem };
}, [ isMarketplaceFilled, currentRoute ]); }, [ isMarketplaceFilled, pathname ]);
} }
...@@ -3,14 +3,13 @@ import { useRouter } from 'next/router'; ...@@ -3,14 +3,13 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useGradualIncrement from 'lib/hooks/useGradualIncrement'; import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import { ROUTES } from 'lib/link/routes';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
function getSocketParams(router: NextRouter) { function getSocketParams(router: NextRouter) {
if ( if (
router.pathname === ROUTES.txs.pattern && router.pathname === '/txs' &&
(router.query.tab === 'validated' || router.query.tab === undefined) && (router.query.tab === 'validated' || router.query.tab === undefined) &&
!router.query.block_number && !router.query.block_number &&
!router.query.page !router.query.page
...@@ -18,12 +17,12 @@ function getSocketParams(router: NextRouter) { ...@@ -18,12 +17,12 @@ function getSocketParams(router: NextRouter) {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const }; return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
} }
if (router.pathname === ROUTES.network_index.pattern) { if (router.pathname === '/') {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const }; return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
} }
if ( if (
router.pathname === ROUTES.txs.pattern && router.pathname === '/txs' &&
router.query.tab === 'pending' && router.query.tab === 'pending' &&
!router.query.block_number && !router.query.block_number &&
!router.query.page !router.query.page
......
export const ACCOUNT_ROUTES: Array<RouteName> = [ 'watchlist', 'private_tags', 'public_tags', 'api_keys', 'custom_abi' ];
import type { RouteName } from 'lib/link/routes';
export default function isAccountRoute(route: RouteName) {
return ACCOUNT_ROUTES.includes(route);
}
import link from './link';
it('makes correct link if there are no params in path', () => {
const result = link('api_keys');
expect(result).toBe('https://blockscout.com/account/api_key');
});
it('makes correct link if there are params in path', () => {
const result = link('token_instance_item', { id: '42', hash: '0x67e90a54AeEA85f21949c645082FE95d77BC1E70' });
expect(result).toBe('https://blockscout.com/token/0x67e90a54AeEA85f21949c645082FE95d77BC1E70/instance/42');
});
it('makes correct link with query params', () => {
const result = link('tx', { id: '0x4eb3b3b35d4c4757629bee32fc7a28b5dece693af8e7a383cf4cd6debe97ecf2' }, { tab: 'index', foo: 'bar' });
expect(result).toBe('https://blockscout.com/tx/0x4eb3b3b35d4c4757629bee32fc7a28b5dece693af8e7a383cf4cd6debe97ecf2?tab=index&foo=bar');
});
import appConfig from 'configs/app/config';
import { ROUTES } from './routes';
import type { RouteName } from './routes';
const PATH_PARAM_REGEXP = /\/:(\w+)/g;
export default function link(
routeName: RouteName,
urlParams?: Record<string, Array<string> | string | undefined>,
queryParams?: Record<string, string>,
): string {
const route = ROUTES[routeName];
if (!route) {
return '';
}
const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
let paramValue = urlParams?.[paramName];
if (Array.isArray(paramValue)) {
// FIXME we don't have yet params as array, but typescript says that we could
// dunno know how to manage it, fix me if you find an issue
paramValue = paramValue.join(',');
}
return paramValue ? `/${ paramValue }` : '';
});
const baseUrl = routeName === 'auth' ? appConfig.authUrl : appConfig.baseUrl;
const url = new URL(path, baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
url.searchParams.append(key, value);
});
return url.toString();
}
{
"network_index": "/",
"watchlist": "/account/watchlist",
"private_tags": "/account/tag_address",
"public_tags": "/account/public_tags_request",
"api_keys": "/account/api_key",
"custom_abi": "/account/custom_abi",
"profile": "/auth/profile",
"txs": "/txs",
"tx": "/tx/:id",
"blocks": "/blocks",
"block": "/block/:id",
"tokens": "/tokens",
"token_index": "/token/:hash",
"token_instance_item": "/token/:hash/instance/:id",
"address_index": "/address/:id",
"address_contract_verification": "/address/:id/contract_verification",
"accounts": "/accounts",
"apps": "/apps",
"app_index": "/apps/:id",
"search_results": "/search-results",
"auth": "/auth/auth0",
"stats": "/stats",
"visualize_sol2uml": "/visualize/sol2uml",
"csv_export": "/csv-export"
}
import appConfig from 'configs/app/config';
import PATHS from './paths.json';
export interface Route {
pattern: string;
crossNetworkNavigation?: boolean; // route will not change when switching networks
}
export type RouteName = keyof typeof ROUTES;
export const ROUTES = {
// NETWORK MAIN PAGE
network_index: {
pattern: PATHS.network_index,
crossNetworkNavigation: true,
},
// ACCOUNT
watchlist: {
pattern: PATHS.watchlist,
},
private_tags: {
pattern: PATHS.private_tags,
},
public_tags: {
pattern: PATHS.public_tags,
},
api_keys: {
pattern: PATHS.api_keys,
},
custom_abi: {
pattern: PATHS.custom_abi,
},
profile: {
pattern: PATHS.profile,
},
// TRANSACTIONS
txs: {
pattern: PATHS.txs,
crossNetworkNavigation: true,
},
tx: {
pattern: PATHS.tx,
},
// BLOCKS
blocks: {
pattern: PATHS.blocks,
crossNetworkNavigation: true,
},
block: {
pattern: PATHS.block,
},
// TOKENS
tokens: {
pattern: PATHS.tokens,
crossNetworkNavigation: true,
},
token_index: {
pattern: PATHS.token_index,
crossNetworkNavigation: true,
},
token_instance_item: {
pattern: PATHS.token_instance_item,
},
// ADDRESSES
address_index: {
pattern: PATHS.address_index,
crossNetworkNavigation: true,
},
address_contract_verification: {
pattern: PATHS.address_contract_verification,
crossNetworkNavigation: true,
},
// ACCOUNTS
accounts: {
pattern: PATHS.accounts,
crossNetworkNavigation: true,
},
// APPS
apps: {
pattern: PATHS.apps,
},
app_index: {
pattern: PATHS.app_index,
},
stats: {
pattern: PATHS.stats,
},
// SEARCH
search_results: {
pattern: PATHS.search_results,
},
// VISUALIZE
visualize_sol2uml: {
pattern: PATHS.visualize_sol2uml,
},
csv_export: {
pattern: PATHS.csv_export,
},
// AUTH
auth: {
pattern: PATHS.auth,
},
};
// !!! for development purpose only !!!
// don't wanna strict ROUTES to type "Record<string, Route>"
// otherwise we lose benefit of using "keyof typeof ROUTES" for possible route names (it will be any string then)
// but we still want typescript to tell us if routes follow its interface
// so we do this simple type-checking here
//
// another option is to create common enum with all possible route names and use it across the project
// but it is a little bit overwhelming as it seems right now
function checkRoutes(route: Record<string, Route>) {
return route;
}
if (appConfig.isDev) {
checkRoutes(ROUTES);
}
import { useRouter } from 'next/router';
import React from 'react';
import type { RouteName } from 'lib/link/routes';
import { ROUTES } from 'lib/link/routes';
const PATH_PARAM_REGEXP = /\/:(\w+)/g;
export default function useCurrentRoute() {
const { route: nextRoute } = useRouter();
return React.useCallback((): RouteName => {
for (const routeName in ROUTES) {
const route = ROUTES[routeName as RouteName];
const formattedRoute = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
return `/[${ paramName }]`;
});
if (formattedRoute === nextRoute) {
return routeName as RouteName;
}
}
return 'network_index';
}, [ nextRoute ]);
}
import type { PageParams } from './types'; import type { RoutedQuery } from 'nextjs-routes';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params: PageParams) { export default function getSeo(params: RoutedQuery<'/address/[hash]'>) {
const networkTitle = getNetworkTitle(); const networkTitle = getNetworkTitle();
return { return {
title: params ? `${ params.id } - ${ networkTitle }` : '', title: params ? `${ params.hash } - ${ networkTitle }` : '',
description: params ? description: params ?
`View the account balance, transactions, and other data for ${ params.id } on the ${ networkTitle }` : `View the account balance, transactions, and other data for ${ params.hash } on the ${ networkTitle }` :
'', '',
}; };
} }
export type PageParams = {
id: string;
}
import type { PageParams } from './types'; import type { RoutedQuery } from 'nextjs-routes';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) { export default function getSeo(params?: RoutedQuery<'/block/[height]'>) {
const networkTitle = getNetworkTitle(); const networkTitle = getNetworkTitle();
return { return {
title: params ? `Block ${ params.id } - ${ networkTitle }` : '', title: params ? `Block ${ params.height } - ${ networkTitle }` : '',
description: params ? `View the transactions, token transfers, and uncles for block number ${ params.id }` : '', description: params ? `View the transactions, token transfers, and uncles for block number ${ params.height }` : '',
}; };
} }
export type PageParams = {
id: string;
}
...@@ -4,6 +4,8 @@ export type Props = { ...@@ -4,6 +4,8 @@ export type Props = {
cookies: string; cookies: string;
referrer: string; referrer: string;
id?: string; id?: string;
height?: string;
hash?: string;
} }
export const getServerSideProps: GetServerSideProps<Props> = async({ req, query }) => { export const getServerSideProps: GetServerSideProps<Props> = async({ req, query }) => {
...@@ -12,6 +14,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async({ req, query ...@@ -12,6 +14,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async({ req, query
cookies: req.headers.cookie || '', cookies: req.headers.cookie || '',
referrer: req.headers.referer || '', referrer: req.headers.referer || '',
id: query.id?.toString() || '', id: query.id?.toString() || '',
height: query.height?.toString() || '',
hash: query.hash?.toString() || '',
}, },
}; };
}; };
import type { PageParams } from './types'; import type { RoutedQuery } from 'nextjs-routes';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params?: PageParams) { export default function getSeo(params?: RoutedQuery<'/tx/[hash]'>) {
const networkTitle = getNetworkTitle(); const networkTitle = getNetworkTitle();
return { return {
title: params ? `Transaction ${ params.id } - ${ networkTitle }` : '', title: params ? `Transaction ${ params.hash } - ${ networkTitle }` : '',
description: params ? `View transaction ${ params.id } on ${ networkTitle }` : '', description: params ? `View transaction ${ params.hash } on ${ networkTitle }` : '',
}; };
} }
export type PageParams = {
id: string;
}
export default function getQueryParamString(param: string | Array<string> | undefined): string {
if (Array.isArray(param)) {
return param.join(',');
}
return param || '';
}
// https://hexdocs.pm/phoenix/js/
import type { SocketConnectOption } from 'phoenix'; import type { SocketConnectOption } from 'phoenix';
import { Socket } from 'phoenix'; import { Socket } from 'phoenix';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
......
...@@ -2,6 +2,7 @@ import type { Channel } from 'phoenix'; ...@@ -2,6 +2,7 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -19,6 +20,7 @@ SocketMessage.AddressTxs | ...@@ -19,6 +20,7 @@ SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending | SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer | SocketMessage.AddressTokenTransfer |
SocketMessage.TokenTransfers | SocketMessage.TokenTransfers |
SocketMessage.ContractVerification |
SocketMessage.Unknown; SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> { interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
...@@ -43,6 +45,7 @@ export namespace SocketMessage { ...@@ -43,6 +45,7 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }> export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -60,7 +60,11 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on ...@@ -60,7 +60,11 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
} else { } else {
ch = socket.channel(topic); ch = socket.channel(topic);
CHANNEL_REGISTRY[topic] = ch; CHANNEL_REGISTRY[topic] = ch;
ch.join().receive('ok', (message) => onJoinRef.current?.(ch, message)); ch.join()
.receive('ok', (message) => onJoinRef.current?.(ch, message))
.receive('error', () => {
onSocketError?.();
});
} }
setChannel(ch); setChannel(ch);
...@@ -70,7 +74,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on ...@@ -70,7 +74,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
delete CHANNEL_REGISTRY[topic]; delete CHANNEL_REGISTRY[topic];
setChannel(undefined); setChannel(undefined);
}; };
}, [ socket, topic, params, isDisabled ]); }, [ socket, topic, params, isDisabled, onSocketError ]);
return channel; return channel;
} }
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [ const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' }, { title: 'ERC-20', id: 'ERC-20' },
......
import type { NextRequest } from 'next/server'; import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { route } from 'nextjs-routes';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { NAMES } from 'lib/cookies'; import { NAMES } from 'lib/cookies';
import getCspPolicy from 'lib/csp/getCspPolicy'; import getCspPolicy from 'lib/csp/getCspPolicy';
import link from 'lib/link/link';
const cspPolicy = getCspPolicy(); const cspPolicy = getCspPolicy();
...@@ -22,7 +22,7 @@ export function middleware(req: NextRequest) { ...@@ -22,7 +22,7 @@ export function middleware(req: NextRequest) {
const apiToken = req.cookies.get(NAMES.API_TOKEN); const apiToken = req.cookies.get(NAMES.API_TOKEN);
if ((isAccountRoute || isProfileRoute) && !apiToken && appConfig.isAccountSupported) { if ((isAccountRoute || isProfileRoute) && !apiToken && appConfig.isAccountSupported) {
const authUrl = link('auth', undefined, { path: req.nextUrl.pathname }); const authUrl = appConfig.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } });
return NextResponse.redirect(authUrl); return NextResponse.redirect(authUrl);
} }
......
...@@ -80,20 +80,26 @@ export const erc1155LongId: AddressTokenBalance = { ...@@ -80,20 +80,26 @@ export const erc1155LongId: AddressTokenBalance = {
value: '42', value: '42',
}; };
export const baseList = [ export const erc20List = {
items: [
erc20a, erc20a,
erc20b, erc20b,
erc20c, erc20c,
],
};
export const erc721List = {
items: [
erc721a, erc721a,
erc721b, erc721b,
erc721c, erc721c,
],
};
export const erc1155List = {
items: [
erc1155withoutName, erc1155withoutName,
erc1155a, erc1155a,
erc1155b, erc1155b,
]; ],
};
export const longValuesList = [
erc20LongSymbol,
erc721LongSymbol,
erc1155LongId,
];
import type { TokenHolders } from 'types/api/tokenInfo'; import type { TokenHolders } from 'types/api/token';
import { withName, withoutName } from 'mocks/address/address'; import { withName, withoutName } from 'mocks/address/address';
......
import type { TokenCounters, TokenInfo } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo } from 'types/api/token';
export const tokenInfo: TokenInfo = { export const tokenInfo: TokenInfo = {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
......
import type { TokenInstance } from 'types/api/tokens'; import type { TokenInstance } from 'types/api/token';
import * as addressMock from '../address/address'; import * as addressMock from '../address/address';
import { tokenInfoERC721a } from './tokenInfo'; import { tokenInfoERC721a } from './tokenInfo';
......
...@@ -64,6 +64,7 @@ export const base: Transaction = { ...@@ -64,6 +64,7 @@ export const base: Transaction = {
], ],
type: 2, type: 2,
value: '42000000000000000000', value: '42000000000000000000',
actions: [],
}; };
export const withContractCreation: Transaction = { export const withContractCreation: Transaction = {
...@@ -170,3 +171,72 @@ export const pending: Transaction = { ...@@ -170,3 +171,72 @@ export const pending: Transaction = {
type: null, type: null,
value: '0', value: '0',
}; };
export const withActionsUniswap: Transaction = {
...base,
actions: [
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '7143.488560357232097378',
amount1: '10',
symbol0: 'XFT',
symbol1: 'Ether',
},
protocol: 'uniswap_v3',
type: 'mint',
},
{
data: {
address: '0xC36442b4a4522E871399CD717aBDD847Ab11FE88',
ids: [
'53699',
'53700123456',
'42',
],
name: 'Uniswap V3: Positions NFT',
symbol: 'UNI-V3-POS',
to: '0x6d872Fb5F5B2B1f71fA9AadE159bc3976c1946B7',
},
protocol: 'uniswap_v3',
type: 'mint_nft',
},
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '42876.488560357232',
amount1: '345.908098203434',
symbol0: 'SHAVUHA',
symbol1: 'BOB',
},
protocol: 'uniswap_v3',
type: 'swap',
},
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '42',
amount1: '0.523523223232',
symbol0: 'VIC',
symbol1: 'USDT',
},
protocol: 'uniswap_v3',
type: 'burn',
},
{
data: {
address0: '0x6f16598F00eDabEA92B4Cef4b6aa0d45c898A9AE',
address1: '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6',
amount0: '42',
amount1: '0.523523223232',
symbol0: 'BOB',
symbol1: 'UNI',
},
protocol: 'uniswap_v3',
type: 'collect',
},
],
};
const withRoutes = require('nextjs-routes/config')({
outDir: 'types',
});
const path = require('path'); const path = require('path');
const headers = require('./configs/nextjs/headers'); const headers = require('./configs/nextjs/headers');
...@@ -33,4 +36,4 @@ const moduleExports = { ...@@ -33,4 +36,4 @@ const moduleExports = {
output: 'standalone', output: 'standalone',
}; };
module.exports = moduleExports; module.exports = withRoutes(moduleExports);
...@@ -35,10 +35,13 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -35,10 +35,13 @@ function MyApp({ Component, pageProps }: AppProps) {
}, },
})); }));
const renderErrorScreen = React.useCallback(() => { const renderErrorScreen = React.useCallback((error?: Error) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusCode = (error?.cause as any)?.status || 500;
return ( return (
<AppError <AppError
statusCode={ 500 } statusCode={ statusCode }
height="100vh" height="100vh"
display="flex" display="flex"
flexDirection="column" flexDirection="column"
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/address/types';
import getSeo from 'lib/next/address/getSeo'; import getSeo from 'lib/next/address/getSeo';
import ContractVerification from 'ui/pages/ContractVerification'; import ContractVerification from 'ui/pages/ContractVerification';
const ContractVerificationPage: NextPage<PageParams> = ({ id }: PageParams) => { const ContractVerificationPage: NextPage<RoutedQuery<'/address/[hash]/contract_verification'>> =
const { title, description } = getSeo({ id }); ({ hash }: RoutedQuery<'/address/[hash]/contract_verification'>) => {
const { title, description } = getSeo({ hash });
return ( return (
<> <>
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/address/types';
import getSeo from 'lib/next/address/getSeo'; import getSeo from 'lib/next/address/getSeo';
import Address from 'ui/pages/Address'; import Address from 'ui/pages/Address';
const AddressPage: NextPage<PageParams> = ({ id }: PageParams) => { const AddressPage: NextPage<RoutedQuery<'/address/[hash]'>> = ({ hash }: RoutedQuery<'/address/[hash]'>) => {
const { title, description } = getSeo({ id }); const { title, description } = getSeo({ hash });
return ( return (
<> <>
......
...@@ -16,7 +16,7 @@ const AppPage: NextPage = () => { ...@@ -16,7 +16,7 @@ const AppPage: NextPage = () => {
const [ isLoading, setIsLoading ] = useState(true); const [ isLoading, setIsLoading ] = useState(true);
const [ app, setApp ] = useState<AppItemOverview | undefined>(undefined); const [ app, setApp ] = useState<AppItemOverview | undefined>(undefined);
const { id }: { id?: string } = router.query; const id = router.query.id;
useEffect(() => { useEffect(() => {
if (!id) { if (!id) {
......
import type { NextPage } from 'next';
const Auth0Page: NextPage = () => {
return null;
};
export default Auth0Page;
export async function getServerSideProps() {
return {
notFound: true,
};
}
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/block/types';
import getSeo from 'lib/next/block/getSeo'; import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block'; import Block from 'ui/pages/Block';
const BlockPage: NextPage<PageParams> = ({ id }: PageParams) => { const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => {
const { title, description } = getSeo({ id }); const { title, description } = getSeo({ height });
return ( return (
<> <>
<Head> <Head>
......
import type { NextPage } from 'next';
const CsvExportPage: NextPage = () => {
return null;
};
export default CsvExportPage;
export async function getServerSideProps() {
return {
notFound: true,
};
}
import type { GetServerSideProps, NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SearchRedirectResult } from 'types/api/search'; import type { SearchRedirectResult } from 'types/api/search';
import buildUrlNode from 'lib/api/buildUrlNode'; import buildUrlNode from 'lib/api/buildUrlNode';
import fetchFactory from 'lib/api/nodeFetch'; import fetchFactory from 'lib/api/nodeFetch';
import link from 'lib/link/link';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props } from 'lib/next/getServerSideProps'; import type { Props } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps'; import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
...@@ -43,13 +43,13 @@ export const getServerSideProps: GetServerSideProps<Props> = async({ req, res, r ...@@ -43,13 +43,13 @@ export const getServerSideProps: GetServerSideProps<Props> = async({ req, res, r
const redirectUrl = (() => { const redirectUrl = (() => {
switch (payload.type) { switch (payload.type) {
case 'block': { case 'block': {
return link('block', { id: q }); return route({ pathname: '/block/[height]', query: { height: q } });
} }
case 'address': { case 'address': {
return link('address_index', { id: payload.parameter || q }); return route({ pathname: '/address/[hash]', query: { hash: payload.parameter || q } });
} }
case 'transaction': { case 'transaction': {
return link('tx', { id: q }); return route({ pathname: '/tx/[hash]', query: { hash: q } });
} }
} }
})(); })();
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import getSeo from 'lib/next/tx/getSeo'; import getSeo from 'lib/next/tx/getSeo';
import Transaction from 'ui/pages/Transaction'; import Transaction from 'ui/pages/Transaction';
const TransactionPage: NextPage<PageParams> = ({ id }: PageParams) => { const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQuery<'/tx/[hash]'>) => {
const { title, description } = getSeo({ id }); const { title, description } = getSeo({ hash });
return ( return (
<> <>
......
import './fonts.css'; import './fonts.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks'; import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import _defaultsDeep from 'lodash/defaultsDeep';
import MockDate from 'mockdate'; import MockDate from 'mockdate';
import * as router from 'next/router'; import * as router from 'next/router';
const NEXT_ROUTER_MOCK = { const NEXT_ROUTER_MOCK = {
query: {}, query: {},
pathname: '',
}; };
beforeMount(async({ hooksConfig }) => { beforeMount(async({ hooksConfig }) => {
// Before mount, redefine useRouter to return mock value from test. // Before mount, redefine useRouter to return mock value from test.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: I really want to redefine this property :) // @ts-ignore: I really want to redefine this property :)
router.useRouter = () => hooksConfig?.router || NEXT_ROUTER_MOCK; router.useRouter = () => _defaultsDeep(hooksConfig?.router, NEXT_ROUTER_MOCK);
// set current date // set current date
MockDate.set('2022-11-11T12:00:00Z'); MockDate.set('2022-11-11T12:00:00Z');
......
import { compile } from 'path-to-regexp'; import { compile } from 'path-to-regexp';
import type { ResourceName } from 'lib/api/resources'; import type { ResourceName, ResourcePathParams } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources'; import { RESOURCES } from 'lib/api/resources';
export default function buildApiUrl(resourceName: ResourceName, pathParams?: Record<string, string>) { export default function buildApiUrl<R extends ResourceName>(resourceName: R, pathParams?: ResourcePathParams<R>) {
const resource = RESOURCES[resourceName]; const resource = RESOURCES[resourceName];
return compile('/node-api/proxy/poa/core' + resource.path)(pathParams); return compile('/node-api/proxy/poa/core' + resource.path)(pathParams);
} }
import { formAnatomy as parts } from '@chakra-ui/anatomy'; import { formAnatomy as parts } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system'; import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import type { StyleFunctionProps } from '@chakra-ui/theme-tools'; import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { getColor } from '@chakra-ui/theme-tools'; import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors'; import getDefaultFormColors from '../utils/getDefaultFormColors';
import FancySelect from './FancySelect'; import FancySelect from './FancySelect';
...@@ -81,11 +81,18 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction ...@@ -81,11 +81,18 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles, 'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles,
[` [`
input[disabled] + label, input[disabled] + label,
textarea[disabled] + label,
&[aria-disabled=true] label &[aria-disabled=true] label
`]: { `]: {
backgroundColor: 'transparent', backgroundColor: 'transparent',
}, },
// in textarea bg of label could not be transparent; it should match the background color of input but without alpha
// so we have to use non-standard colors here
'textarea[disabled] + label': {
backgroundColor: mode('#ececec', '#232425')(props),
},
'textarea[disabled] + label[data-in-modal=true]': {
backgroundColor: mode('#ececec', '#292b34')(props),
},
// indicator styles // indicator styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': { 'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': {
......
...@@ -22,7 +22,7 @@ const baseStyleDialog = defineStyle((props) => { ...@@ -22,7 +22,7 @@ const baseStyleDialog = defineStyle((props) => {
const baseStyleDialogContainer = defineStyle({ const baseStyleDialogContainer = defineStyle({
'::-webkit-scrollbar': { display: 'none' }, '::-webkit-scrollbar': { display: 'none' },
'scrollbar-width': 'none', 'scrollbar-width': 'none',
'@supports (height: -webkit-fill-available)': { height: '-webkit-fill-available' }, // '@supports (height: -webkit-fill-available)': { height: '-webkit-fill-available' },
}); });
const baseStyleHeader = defineStyle((props) => ({ const baseStyleHeader = defineStyle((props) => ({
......
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es6",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
......
...@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction';
import type { AddressTag, WatchlistName } from './addressParams'; import type { AddressTag, WatchlistName } from './addressParams';
import type { Block } from './block'; import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { TokenInfo, TokenType } from './tokenInfo'; import type { TokenInfo, TokenType } from './token';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer'; import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
export interface Address { export interface Address {
......
...@@ -113,3 +113,34 @@ export interface SmartContractQueryMethodReadError { ...@@ -113,3 +113,34 @@ export interface SmartContractQueryMethodReadError {
} }
export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError; export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError;
// VERIFICATION
export type SmartContractVerificationMethod = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part' | 'vyper-code' | 'vyper-multi-part';
export interface SmartContractVerificationConfigRaw {
solidity_compiler_versions: Array<string>;
solidity_evm_versions: Array<string>;
verification_options: Array<string>;
vyper_compiler_versions: Array<string>;
vyper_evm_versions: Array<string>;
}
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
verification_options: Array<SmartContractVerificationMethod>;
}
export type SmartContractVerificationResponse = {
status: 'error';
errors: SmartContractVerificationError;
} | {
status: 'success';
}
export interface SmartContractVerificationError {
contract_source_code?: Array<string>;
files?: Array<string>;
compiler_version?: Array<string>;
constructor_arguments?: Array<string>;
name?: Array<string>;
}
...@@ -34,3 +34,28 @@ export type TokenHoldersPagination = { ...@@ -34,3 +34,28 @@ export type TokenHoldersPagination = {
items_count: number; items_count: number;
value: string; value: string;
} }
export interface TokenInstance {
is_unique: boolean;
id: string;
holder_address_hash: string | null;
image_url: string | null;
animation_url: string | null;
external_app_url: string | null;
metadata: unknown;
owner: AddressParam;
token: TokenInfo;
}
export interface TokenInstanceTransfersCount {
transfers_count: number;
}
export interface TokenInventoryResponse {
items: Array<TokenInstance>;
next_page_params: TokenInventoryPagination;
}
export type TokenInventoryPagination = {
unique_token: number;
}
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric, TokenType } from './tokenInfo'; import type { TokenInfoGeneric, TokenType } from './token';
export type Erc20TotalPayload = { export type Erc20TotalPayload = {
decimals: string | null; decimals: string | null;
......
import type { AddressParam } from './addressParams'; import type { TokenInfo, TokenType } from './token';
import type { TokenInfo, TokenType } from './tokenInfo';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
export type TokensResponse = { export type TokensResponse = {
...@@ -13,22 +12,6 @@ export type TokensResponse = { ...@@ -13,22 +12,6 @@ export type TokensResponse = {
export type TokensFilters = { filter: string; type: Array<TokenType> | undefined }; export type TokensFilters = { filter: string; type: Array<TokenType> | undefined };
export interface TokenInstance {
is_unique: boolean;
id: string;
holder_address_hash: string | null;
image_url: string | null;
animation_url: string | null;
external_app_url: string | null;
metadata: unknown;
owner: AddressParam;
token: TokenInfo;
}
export interface TokenInstanceTransfersCount {
transfers_count: number;
}
export interface TokenInstanceTransferResponse { export interface TokenInstanceTransferResponse {
items: Array<TokenTransfer>; items: Array<TokenTransfer>;
next_page_params: TokenInstanceTransferPagination | null; next_page_params: TokenInstanceTransferPagination | null;
......
...@@ -3,6 +3,7 @@ import type { BlockTransactionsResponse } from './block'; ...@@ -3,6 +3,7 @@ import type { BlockTransactionsResponse } from './block';
import type { DecodedInput } from './decodedInput'; import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee'; import type { Fee } from './fee';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
import type { TxAction } from './txAction';
export type TransactionRevertReason = { export type TransactionRevertReason = {
raw: string; raw: string;
...@@ -48,6 +49,7 @@ export type Transaction = ( ...@@ -48,6 +49,7 @@ export type Transaction = (
method: string | null; method: string | null;
tx_types: Array<TransactionType>; tx_types: Array<TransactionType>;
tx_tag: string | null; tx_tag: string | null;
actions: Array<TxAction>;
} }
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
......
export interface TxActionGeneral {
type: 'mint' | 'burn' | 'collect' | 'swap';
data: {
amount0: string;
symbol0: string;
address0: string;
amount1: string;
symbol1: string;
address1: string;
};
}
export interface TxActionNft {
type: 'mint_nft';
data: {
name: string;
symbol: string;
address: string;
to: string;
ids: Array<string>;
};
}
export type TxAction = {
protocol: 'uniswap_v3';
} & (TxActionGeneral | TxActionNft)
// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
// This file will be automatically regenerated when your Next.js server is running.
// nextjs-routes version: 1.0.8
/* eslint-disable */
// prettier-ignore
declare module "nextjs-routes" {
export type Route =
| StaticRoute<"/account/api_key">
| StaticRoute<"/account/custom_abi">
| StaticRoute<"/account/public_tags_request">
| StaticRoute<"/account/tag_address">
| StaticRoute<"/account/watchlist">
| StaticRoute<"/accounts">
| DynamicRoute<"/address/[hash]/contract_verification", { "hash": string }>
| DynamicRoute<"/address/[hash]", { "hash": string }>
| StaticRoute<"/api/csrf">
| StaticRoute<"/api/proxy">
| DynamicRoute<"/apps/[id]", { "id": string }>
| StaticRoute<"/apps">
| StaticRoute<"/auth/auth0">
| StaticRoute<"/auth/profile">
| DynamicRoute<"/block/[height]", { "height": string }>
| StaticRoute<"/blocks">
| StaticRoute<"/csv-export">
| StaticRoute<"/graph">
| StaticRoute<"/">
| StaticRoute<"/login">
| StaticRoute<"/search-results">
| StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }>
| DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }>
| StaticRoute<"/tokens">
| DynamicRoute<"/tx/[hash]", { "hash": string }>
| StaticRoute<"/txs">
| StaticRoute<"/visualize/sol2uml">;
interface StaticRoute<Pathname> {
pathname: Pathname;
query?: Query | undefined;
hash?: string | null | undefined;
}
interface DynamicRoute<Pathname, Parameters> {
pathname: Pathname;
query: Parameters & Query;
hash?: string | null | undefined;
}
interface Query {
[key: string]: string | string[] | undefined;
};
export type RoutedQuery<P extends Route["pathname"]> = Extract<
Route,
{ pathname: P }
>["query"];
export type Locale = undefined;
/**
* A typesafe utility function for generating paths in your application.
*
* route({ pathname: "/foos/[foo]", query: { foo: "bar" }}) will produce "/foos/bar".
*/
export declare function route(r: Route): string;
}
// prettier-ignore
declare module "next/link" {
import type { Route } from "nextjs-routes";
import type { LinkProps as NextLinkProps } from "next/dist/client/link";
import type {
AnchorHTMLAttributes,
DetailedReactHTMLElement,
MouseEventHandler,
PropsWithChildren,
} from "react";
export * from "next/dist/client/link";
type Query = { query?: { [key: string]: string | string[] | undefined } };
type StaticRoute = Exclude<Route, { query: any }>["pathname"];
export interface LinkProps
extends Omit<NextLinkProps, "href" | "locale">,
AnchorHTMLAttributes<HTMLAnchorElement> {
href: Route | StaticRoute | Query;
locale?: false;
}
type LinkReactElement = DetailedReactHTMLElement<
{
onMouseEnter?: MouseEventHandler<Element> | undefined;
onClick: MouseEventHandler;
href?: string | undefined;
ref?: any;
},
HTMLElement
>;
declare function Link(props: PropsWithChildren<LinkProps>): LinkReactElement;
export default Link;
}
// prettier-ignore
declare module "next/router" {
import type { Locale, Route, RoutedQuery } from "nextjs-routes";
import type { NextRouter as Router } from "next/dist/client/router";
export * from "next/dist/client/router";
export { default } from "next/dist/client/router";
type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
type StaticRoute = Exclude<Route, { query: any }>["pathname"];
type Query = { query?: { [key: string]: string | string[] | undefined } };
interface TransitionOptions extends Omit<NextTransitionOptions, "locale"> {
locale?: false;
}
export type NextRouter<P extends Route["pathname"] = Route["pathname"]> =
Extract<Route, { pathname: P }> &
Omit<
Router,
| "push"
| "replace"
| "locale"
| "locales"
| "defaultLocale"
| "domainLocales"
> & {
defaultLocale?: undefined;
domainLocales?: undefined;
locale?: Locale;
locales?: undefined;
push(
url: Route | StaticRoute | Query,
as?: string,
options?: TransitionOptions
): Promise<boolean>;
replace(
url: Route | StaticRoute | Query,
as?: string,
options?: TransitionOptions
): Promise<boolean>;
route: P;
};
export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
}
...@@ -31,10 +31,10 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -31,10 +31,10 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const addressHash = String(router.query?.id); const addressHash = String(router.query.hash);
const query = useQueryWithPages({ const query = useQueryWithPages({
resourceName: 'address_blocks_validated', resourceName: 'address_blocks_validated',
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
scrollRef, scrollRef,
}); });
...@@ -46,7 +46,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -46,7 +46,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
setSocketAlert(false); setSocketAlert(false);
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_blocks_validated', { pathParams: { id: addressHash } }), getResourceKey('address_blocks_validated', { pathParams: { hash: addressHash } }),
(prevData: AddressBlocksValidatedResponse | undefined) => { (prevData: AddressBlocksValidatedResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
......
...@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressCoinBalance from './AddressCoinBalance'; import AddressCoinBalance from './AddressCoinBalance';
const addressHash = 'hash'; const addressHash = 'hash';
const BALANCE_HISTORY_API_URL = buildApiUrl('address_coin_balance', { id: addressHash }); const BALANCE_HISTORY_API_URL = buildApiUrl('address_coin_balance', { hash: addressHash });
const BALANCE_HISTORY_CHART_API_URL = buildApiUrl('address_coin_balance_chart', { id: addressHash }); const BALANCE_HISTORY_CHART_API_URL = buildApiUrl('address_coin_balance_chart', { hash: addressHash });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: addressHash }, query: { hash: addressHash },
}, },
}; };
......
...@@ -7,6 +7,7 @@ import type { AddressCoinBalanceHistoryResponse } from 'types/api/address'; ...@@ -7,6 +7,7 @@ import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import SocketAlert from 'ui/shared/SocketAlert'; import SocketAlert from 'ui/shared/SocketAlert';
...@@ -20,10 +21,10 @@ const AddressCoinBalance = () => { ...@@ -20,10 +21,10 @@ const AddressCoinBalance = () => {
const router = useRouter(); const router = useRouter();
const scrollRef = React.useRef<HTMLDivElement>(null); const scrollRef = React.useRef<HTMLDivElement>(null);
const addressHash = String(router.query?.id); const addressHash = getQueryParamString(router.query.hash);
const coinBalanceQuery = useQueryWithPages({ const coinBalanceQuery = useQueryWithPages({
resourceName: 'address_coin_balance', resourceName: 'address_coin_balance',
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
scrollRef, scrollRef,
}); });
...@@ -35,7 +36,7 @@ const AddressCoinBalance = () => { ...@@ -35,7 +36,7 @@ const AddressCoinBalance = () => {
setSocketAlert(false); setSocketAlert(false);
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_coin_balance', { pathParams: { id: addressHash } }), getResourceKey('address_coin_balance', { pathParams: { hash: addressHash } }),
(prevData: AddressCoinBalanceHistoryResponse | undefined) => { (prevData: AddressCoinBalanceHistoryResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
......
import { chakra, Icon, Link, Tooltip, Hide } from '@chakra-ui/react'; import { chakra, Icon, Link, Tooltip, Hide } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import svgFileIcon from 'icons/files/csv.svg'; import svgFileIcon from 'icons/files/csv.svg';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
interface Props { interface Props {
address: string; address: string;
...@@ -20,7 +20,9 @@ const AddressCsvExportLink = ({ className, address, type }: Props) => { ...@@ -20,7 +20,9 @@ const AddressCsvExportLink = ({ className, address, type }: Props) => {
className={ className } className={ className }
display="inline-flex" display="inline-flex"
alignItems="center" alignItems="center"
href={ link('csv_export', undefined, { type, address }) } whiteSpace="nowrap"
href={ route({ pathname: '/csv-export', query: { type, address } }) }
flexShrink={ 0 }
> >
<Icon as={ svgFileIcon } boxSize={{ base: '30px', lg: 6 }}/> <Icon as={ svgFileIcon } boxSize={{ base: '30px', lg: 6 }}/>
<Hide ssr={ false } below="lg"><chakra.span ml={ 1 }>Download CSV</chakra.span></Hide> <Hide ssr={ false } below="lg"><chakra.span ml={ 1 }>Download CSV</chakra.span></Hide>
......
...@@ -8,7 +8,7 @@ import type { Address } from 'types/api/address'; ...@@ -8,7 +8,7 @@ import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters'; import * as countersMock from 'mocks/address/counters';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokensMock from 'mocks/address/tokens';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -16,12 +16,14 @@ import AddressDetails from './AddressDetails'; ...@@ -16,12 +16,14 @@ import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage'; import MockAddressPage from './testUtils/MockAddressPage';
const ADDRESS_HASH = addressMock.hash; const ADDRESS_HASH = addressMock.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH }); const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_COUNTERS = buildApiUrl('address_counters', { id: ADDRESS_HASH }); const API_URL_COUNTERS = buildApiUrl('address_counters', { hash: ADDRESS_HASH });
const API_URL_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH }); const API_URL_TOKENS_ERC20 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-20';
const API_URL_TOKENS_ERC721 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-721';
const API_URL_TOKENS_ER1155 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-1155';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH }, query: { hash: ADDRESS_HASH },
}, },
}; };
...@@ -54,10 +56,18 @@ test('token', async({ mount, page }) => { ...@@ -54,10 +56,18 @@ test('token', async({ mount, page }) => {
status: 200, status: 200,
body: JSON.stringify(countersMock.forToken), body: JSON.stringify(countersMock.forToken),
})); }));
await page.route(API_URL_TOKEN_BALANCES, (route) => route.fulfill({ await page.route(API_URL_TOKENS_ERC20, async(route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(tokenBalanceMock.baseList), body: JSON.stringify(tokensMock.erc20List),
})); }), { times: 1 });
await page.route(API_URL_TOKENS_ERC721, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc721List),
}), { times: 1 });
await page.route(API_URL_TOKENS_ER1155, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc1155List),
}), { times: 1 });
await page.evaluate(() => { await page.evaluate(() => {
window.ethereum = { } as MetaMaskInpageProvider; window.ethereum = { } as MetaMaskInpageProvider;
......
import { Box, Flex, Text, Icon, Grid } from '@chakra-ui/react'; import { Box, Flex, Text, Icon, Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Address as TAddress } from 'types/api/address'; import type { Address as TAddress } from 'types/api/address';
...@@ -9,7 +10,7 @@ import appConfig from 'configs/app/config'; ...@@ -9,7 +10,7 @@ import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link'; import getQueryParamString from 'lib/router/getQueryParamString';
import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
...@@ -32,12 +33,12 @@ interface Props { ...@@ -32,12 +33,12 @@ interface Props {
const AddressDetails = ({ addressQuery, scrollRef }: Props) => { const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter(); const router = useRouter();
const addressHash = router.query.id?.toString(); const addressHash = getQueryParamString(router.query.hash);
const countersQuery = useApiQuery('address_counters', { const countersQuery = useApiQuery('address_counters', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data), enabled: Boolean(addressHash) && Boolean(addressQuery.data),
}, },
}); });
...@@ -90,7 +91,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -90,7 +91,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap"> <Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
<Text fontSize="sm">Verify with other explorers</Text> <Text fontSize="sm">Verify with other explorers</Text>
{ explorers.map((explorer) => { { explorers.map((explorer) => {
const url = new URL(explorer.paths.address + '/' + router.query.id, explorer.baseUrl); const url = new URL(explorer.paths.address + '/' + addressHash, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>; return <LinkExternal key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>;
}) } }) }
</Flex> </Flex>
...@@ -118,7 +119,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -118,7 +119,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
hint="Implementation address of the proxy contract." hint="Implementation address of the proxy contract."
columnGap={ 1 } columnGap={ 1 }
> >
<LinkInternal href={ link('address_index', { id: data.implementation_address }) } overflow="hidden"> <LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: data.implementation_address } }) } overflow="hidden">
{ data.implementation_name || <HashStringShortenDynamic hash={ data.implementation_address }/> } { data.implementation_name || <HashStringShortenDynamic hash={ data.implementation_address }/> }
</LinkInternal> </LinkInternal>
{ data.implementation_name && ( { data.implementation_name && (
...@@ -183,7 +184,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -183,7 +184,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
py={{ base: '2px', lg: 1 }} py={{ base: '2px', lg: 1 }}
> >
<LinkInternal <LinkInternal
href={ link('block', { id: String(data.block_number_balance_updated_at) }) } href={ route({ pathname: '/block/[height]', query: { height: String(data.block_number_balance_updated_at) } }) }
display="flex" display="flex"
alignItems="center" alignItems="center"
> >
......
...@@ -9,10 +9,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -9,10 +9,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressInternalTxs from './AddressInternalTxs'; import AddressInternalTxs from './AddressInternalTxs';
const ADDRESS_HASH = internalTxsMock.base.from.hash; const ADDRESS_HASH = internalTxsMock.base.from.hash;
const API_URL_TX_INTERNALS = buildApiUrl('address_internal_txs', { id: ADDRESS_HASH }); const API_URL_TX_INTERNALS = buildApiUrl('address_internal_txs', { hash: ADDRESS_HASH });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH }, query: { hash: ADDRESS_HASH },
}, },
}; };
......
import { Text, Show, Hide } from '@chakra-ui/react'; import { Text, Show, Hide } from '@chakra-ui/react';
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,6 +8,7 @@ import { AddressFromToFilterValues } from 'types/api/address'; ...@@ -9,6 +8,7 @@ import { AddressFromToFilterValues } from 'types/api/address';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressIntTxsSkeletonDesktop from 'ui/address/internals/AddressIntTxsSkeletonDesktop'; import AddressIntTxsSkeletonDesktop from 'ui/address/internals/AddressIntTxsSkeletonDesktop';
import AddressIntTxsSkeletonMobile from 'ui/address/internals/AddressIntTxsSkeletonMobile'; import AddressIntTxsSkeletonMobile from 'ui/address/internals/AddressIntTxsSkeletonMobile';
import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable'; import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable';
...@@ -27,13 +27,11 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -27,13 +27,11 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
const router = useRouter(); const router = useRouter();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter)); const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const queryId = router.query.id; const hash = getQueryParamString(router.query.hash);
const queryIdArray = castArray(queryId);
const queryIdStr = queryIdArray[0];
const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_internal_txs', resourceName: 'address_internal_txs',
pathParams: { id: queryIdStr }, pathParams: { hash },
filters: { filter: filterValue }, filters: { filter: filterValue },
scrollRef, scrollRef,
}); });
...@@ -70,10 +68,10 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -70,10 +68,10 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
content = ( content = (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ queryIdStr }/> <AddressIntTxsList data={ data.items } currentAddress={ hash }/>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<AddressIntTxsTable data={ data.items } currentAddress={ queryIdStr }/> <AddressIntTxsTable data={ data.items } currentAddress={ hash }/>
</Hide> </Hide>
</> </>
); );
...@@ -87,7 +85,7 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -87,7 +85,7 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
onFilterChange={ handleFilterChange } onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) } isActive={ Boolean(filterValue) }
/> />
<AddressCsvExportLink address={ queryIdStr } type="internal-transactions" ml={{ base: 2, lg: 'auto' }}/> <AddressCsvExportLink address={ hash } type="internal-transactions" ml={{ base: 2, lg: 'auto' }}/>
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> } { isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar> </ActionBar>
{ content } { content }
......
...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; ...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import LogItem from 'ui/shared/logs/LogItem'; import LogItem from 'ui/shared/logs/LogItem';
...@@ -12,10 +13,10 @@ import Pagination from 'ui/shared/Pagination'; ...@@ -12,10 +13,10 @@ import Pagination from 'ui/shared/Pagination';
const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter(); const router = useRouter();
const addressHash = String(router.query?.id); const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_logs', resourceName: 'address_logs',
pathParams: { id: addressHash }, pathParams: { hash },
scrollRef, scrollRef,
}); });
......
...@@ -8,12 +8,12 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,12 +8,12 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokenTransfers from './AddressTokenTransfers'; import AddressTokenTransfers from './AddressTokenTransfers';
const API_URL = buildApiUrl('address_token_transfers', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) + const API_URL = buildApiUrl('address_token_transfers', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859'; '?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token_hash: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
}, },
}; };
......
...@@ -6,7 +6,7 @@ import React from 'react'; ...@@ -6,7 +6,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address'; import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import crossIcon from 'icons/cross.svg'; import crossIcon from 'icons/cross.svg';
...@@ -16,12 +16,14 @@ import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; ...@@ -16,12 +16,14 @@ import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import TOKEN_TYPE from 'lib/token/tokenTypes'; import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
...@@ -69,12 +71,12 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -69,12 +71,12 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const currentAddress = router.query.id?.toString(); const currentAddress = getQueryParamString(router.query.hash);
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0); const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const tokenFilter = router.query.token ? router.query.token.toString() : undefined; const tokenFilter = getQueryParamString(router.query.token_hash) || undefined;
const [ filters, setFilters ] = React.useState<Filters>( const [ filters, setFilters ] = React.useState<Filters>(
{ {
...@@ -85,7 +87,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -85,7 +87,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_token_transfers', resourceName: 'address_token_transfers',
pathParams: { id: currentAddress }, pathParams: { hash: currentAddress },
filters: tokenFilter ? { token: tokenFilter } : filters, filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef, scrollRef,
}); });
...@@ -117,7 +119,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -117,7 +119,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
} }
} else { } else {
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_token_transfers', { pathParams: { id: router.query.id?.toString() }, queryParams: { ...filters } }), getResourceKey('address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }),
(prevData: AddressTokenTransferResponse | undefined) => { (prevData: AddressTokenTransferResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
...@@ -147,7 +149,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -147,7 +149,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
}, []); }, []);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `addresses:${ (router.query.id as string).toLowerCase() }`, topic: `addresses:${ currentAddress.toLowerCase() }`,
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: pagination.page !== 1 || Boolean(tokenFilter), isDisabled: pagination.page !== 1 || Boolean(tokenFilter),
...@@ -160,7 +162,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -160,7 +162,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
}); });
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0); const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length; const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress;
const content = (() => { const content = (() => {
if (isLoading) { if (isLoading) {
...@@ -225,10 +227,11 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -225,10 +227,11 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
})(); })();
const tokenFilterComponent = tokenFilter && ( const tokenFilterComponent = tokenFilter && (
<Flex alignItems="center" py={ 1 } flexWrap="wrap" mb={{ base: isPaginationVisible ? 6 : 3, lg: 0 }}> <Flex alignItems="center" flexWrap="wrap" mb={{ base: isActionBarHidden ? 3 : 6, lg: 0 }} mr={ 4 }>
Filtered by token <Text whiteSpace="nowrap" mr={ 2 } py={ 1 }>Filtered by token</Text>
<TokenLogo hash={ tokenFilter } boxSize={ 6 } mx={ 2 }/> <Flex alignItems="center" py={ 1 }>
{ isMobile ? tokenFilter.slice(0, 4) + '...' + tokenFilter.slice(-4) : tokenFilter } <TokenLogo hash={ tokenFilter } boxSize={ 6 } mr={ 2 }/>
{ isMobile ? <HashStringShorten hash={ tokenFilter }/> : tokenFilter }
<Tooltip label="Reset filter"> <Tooltip label="Reset filter">
<Flex> <Flex>
<Icon <Icon
...@@ -243,6 +246,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -243,6 +246,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
</Flex> </Flex>
</Tooltip> </Tooltip>
</Flex> </Flex>
</Flex>
); );
return ( return (
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { withName } from 'mocks/address/address'; import { withName } from 'mocks/address/address';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokensMock from 'mocks/address/tokens';
import { baseList } from 'mocks/address/tokenBalance';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokens from './AddressTokens'; import AddressTokens from './AddressTokens';
const ADDRESS_HASH = withName.hash; const ADDRESS_HASH = withName.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH }); const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_ADDRESS_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH }); const API_URL_TOKENS = buildApiUrl('address_tokens', { hash: ADDRESS_HASH });
const API_URL_TOKENS = buildApiUrl('address_tokens', { id: ADDRESS_HASH });
const nextPageParams = { const nextPageParams = {
items_count: 50, items_count: 50,
...@@ -22,16 +20,18 @@ const nextPageParams = { ...@@ -22,16 +20,18 @@ const nextPageParams = {
value: 1, value: 1,
}; };
test('erc20 +@mobile +@dark-mode', async({ mount, page }) => { const test = base.extend({
const hooksConfig = { page: async({ page }, use) => {
router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc20' },
isReady: true,
},
};
const response20 = { const response20 = {
items: [ tokenBalanceMock.erc20a, tokenBalanceMock.erc20b, tokenBalanceMock.erc20c, tokenBalanceMock.erc20d ], items: [ tokensMock.erc20a, tokensMock.erc20b, tokensMock.erc20c, tokensMock.erc20d ],
next_page_params: nextPageParams,
};
const response721 = {
items: [ tokensMock.erc721a, tokensMock.erc721b, tokensMock.erc721c ],
next_page_params: nextPageParams,
};
const response1155 = {
items: [ tokensMock.erc1155a, tokensMock.erc1155b ],
next_page_params: nextPageParams, next_page_params: nextPageParams,
}; };
...@@ -39,14 +39,30 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -39,14 +39,30 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => {
status: 200, status: 200,
body: JSON.stringify(withName), body: JSON.stringify(withName),
})); }));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({ await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(response20), body: JSON.stringify(response20),
})); }));
await page.route(API_URL_TOKENS + '?type=ERC-721', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response721),
}));
await page.route(API_URL_TOKENS + '?type=ERC-1155', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response1155),
}));
use(page);
},
});
test('erc20 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' },
isReady: true,
},
};
const component = await mount( const component = await mount(
<TestApp> <TestApp>
...@@ -59,32 +75,14 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -59,32 +75,14 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc721 +@mobile +@dark-mode', async({ mount, page }) => { test('erc721 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc721' }, query: { hash: ADDRESS_HASH, tab: 'tokens_erc721' },
isReady: true, isReady: true,
}, },
}; };
const response20 = {
items: [ tokenBalanceMock.erc721a, tokenBalanceMock.erc721b, tokenBalanceMock.erc721c ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-721', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
...@@ -96,32 +94,14 @@ test('erc721 +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -96,32 +94,14 @@ test('erc721 +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc1155 +@mobile +@dark-mode', async({ mount, page }) => { test('erc1155 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc1155' }, query: { hash: ADDRESS_HASH, tab: 'tokens_erc1155' },
isReady: true, isReady: true,
}, },
}; };
const response20 = {
items: [ tokenBalanceMock.erc1155a, tokenBalanceMock.erc1155b ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-1155', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
......
...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
...@@ -36,7 +36,7 @@ const AddressTokens = () => { ...@@ -36,7 +36,7 @@ const AddressTokens = () => {
const tokensQuery = useQueryWithPages({ const tokensQuery = useQueryWithPages({
resourceName: 'address_tokens', resourceName: 'address_tokens',
pathParams: { id: router.query.id?.toString() }, pathParams: { hash: router.query.hash?.toString() },
filters: { type: tokenType }, filters: { type: tokenType },
scrollRef, scrollRef,
}); });
......
...@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTxs from './AddressTxs'; import AddressTxs from './AddressTxs';
const API_URL = buildApiUrl('address_txs', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }); const API_URL = buildApiUrl('address_txs', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' },
}, },
}; };
......
...@@ -10,6 +10,7 @@ import { getResourceKey } from 'lib/api/useApiQuery'; ...@@ -10,6 +10,7 @@ import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
...@@ -31,13 +32,13 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -31,13 +32,13 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
const [ newItemsCount, setNewItemsCount ] = React.useState(0); const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const currentAddress = router.query.id?.toString(); const currentAddress = getQueryParamString(router.query.hash);
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter)); const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const addressTxsQuery = useQueryWithPages({ const addressTxsQuery = useQueryWithPages({
resourceName: 'address_txs', resourceName: 'address_txs',
pathParams: { id: currentAddress }, pathParams: { hash: currentAddress },
filters: { filter: filterValue }, filters: { filter: filterValue },
scrollRef, scrollRef,
}); });
...@@ -63,7 +64,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -63,7 +64,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
} }
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { id: router.query.id?.toString() }, queryParams: { filter: filterValue } }), getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }),
(prevData: AddressTransactionsResponse | undefined) => { (prevData: AddressTransactionsResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
......
import { Text, Flex } from '@chakra-ui/react'; import { Text, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
...@@ -7,7 +8,6 @@ import type { Block } from 'types/api/block'; ...@@ -7,7 +8,6 @@ import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
...@@ -17,7 +17,7 @@ type Props = Block & { ...@@ -17,7 +17,7 @@ type Props = Block & {
}; };
const AddressBlocksValidatedListItem = (props: Props) => { const AddressBlocksValidatedListItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.height) }); const blockUrl = route({ pathname: '/block/[height]', query: { height: props.height.toString() } });
const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1); const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1);
const totalReward = getBlockTotalReward(props); const totalReward = getBlockTotalReward(props);
......
import { Td, Tr, Text, Box, Flex } from '@chakra-ui/react'; import { Td, Tr, Text, Box, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
...@@ -15,7 +15,7 @@ type Props = Block & { ...@@ -15,7 +15,7 @@ type Props = Block & {
}; };
const AddressBlocksValidatedTableItem = (props: Props) => { const AddressBlocksValidatedTableItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.height) }); const blockUrl = route({ pathname: '/block/[height]', query: { height: String(props.height) } });
const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1); const timeAgo = useTimeAgoIncrement(props.timestamp, props.page === 1);
const totalReward = getBlockTotalReward(props); const totalReward = getBlockTotalReward(props);
......
...@@ -11,7 +11,7 @@ interface Props { ...@@ -11,7 +11,7 @@ interface Props {
const AddressCoinBalanceChart = ({ addressHash }: Props) => { const AddressCoinBalanceChart = ({ addressHash }: Props) => {
const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', { const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', {
pathParams: { id: addressHash }, pathParams: { hash: addressHash },
}); });
const items = React.useMemo(() => data?.map(({ date, value }) => ({ const items = React.useMemo(() => data?.map(({ date, value }) => ({
......
import { Text, Stat, StatHelpText, StatArrow, Flex } from '@chakra-ui/react'; import { Text, Stat, StatHelpText, StatArrow, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
...@@ -7,7 +8,6 @@ import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; ...@@ -7,7 +8,6 @@ import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { WEI, ZERO } from 'lib/consts'; import { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -18,7 +18,7 @@ type Props = AddressCoinBalanceHistoryItem & { ...@@ -18,7 +18,7 @@ type Props = AddressCoinBalanceHistoryItem & {
}; };
const AddressCoinBalanceListItem = (props: Props) => { const AddressCoinBalanceListItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.block_number) }); const blockUrl = route({ pathname: '/block/[height]', query: { height: String(props.block_number) } });
const deltaBn = BigNumber(props.delta).div(WEI); const deltaBn = BigNumber(props.delta).div(WEI);
const isPositiveDelta = deltaBn.gte(ZERO); const isPositiveDelta = deltaBn.gte(ZERO);
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1); const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment