Commit d7c9827c authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into verification-redesign

parents 5520b211 a4c20166
......@@ -9,6 +9,7 @@ jobs:
jest_tests:
name: Run tests with Jest
runs-on: ubuntu-latest
if: ${{ false }} # disable since there are no jest test yet
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
......
......@@ -44,8 +44,8 @@
}
},
{
"type": "npm",
"script": "dev:goerli",
"type": "shell",
"command": "NEXT_PUBLIC_API_HOST=${input:goerliApiHost} yarn dev:goerli",
"problemMatcher": [],
"label": "dev server: goerli",
"detail": "start local dev server for Goerli network",
......@@ -161,6 +161,26 @@
"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
{
......@@ -307,5 +327,15 @@
],
"default": ""
},
{
"type": "pickString",
"id": "goerliApiHost",
"description": "Choose API host:",
"options": [
"blockscout-main.test.aws-k8s.blockscout.com",
"eth-goerli.blockscout.com",
],
"default": ""
},
],
}
\ No newline at end of file
const PATHS = require('../../lib/link/paths.json');
const oldUrls = [
{
oldPath: '/account/tag_transaction',
newPath: `${ PATHS.private_tags }?tab=tx`,
newPath: '/account/tag_address?tab=tx',
},
{
oldPath: '/pending-transactions',
newPath: `${ PATHS.txs }?tab=pending`,
newPath: '/txs?tab=pending',
},
{
oldPath: '/tx/:id/internal-transactions',
newPath: `${ PATHS.tx }?tab=internal`,
oldPath: '/tx/:hash/internal-transactions',
newPath: '/tx/:hash?tab=internal',
},
{
oldPath: '/tx/:id/logs',
newPath: `${ PATHS.tx }?tab=logs`,
oldPath: '/tx/:hash/logs',
newPath: '/tx/:hash?tab=logs',
},
{
oldPath: '/tx/:id/raw-trace',
newPath: `${ PATHS.tx }?tab=raw_trace`,
oldPath: '/tx/:hash/raw-trace',
newPath: '/tx/:hash?tab=raw_trace',
},
{
oldPath: '/tx/:id/state',
newPath: `${ PATHS.tx }?tab=state`,
oldPath: '/tx/:hash/state',
newPath: '/tx/:hash?tab=state',
},
{
oldPath: '/uncles',
newPath: `${ PATHS.blocks }?tab=uncles`,
newPath: '/blocks?tab=uncles',
},
{
oldPath: '/reorgs',
newPath: `${ PATHS.blocks }?tab=reorgs`,
newPath: '/blocks?tab=reorgs',
},
{
oldPath: '/block/:id/transactions',
newPath: `${ PATHS.block }`,
oldPath: '/block/:height/transactions',
newPath: '/block/:height?tab=txs',
},
{
oldPath: '/address/:id/transactions',
newPath: `${ PATHS.address_index }`,
oldPath: '/address/:hash/transactions',
newPath: '/address/:hash',
},
{
oldPath: '/address/:id/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers`,
oldPath: '/address/:hash/token-transfers',
newPath: '/address/:hash?tab=token_transfers',
},
{
oldPath: '/address/:id/tokens',
newPath: `${ PATHS.address_index }?tab=tokens`,
oldPath: '/address/:hash/tokens',
newPath: '/address/:hash?tab=tokens',
},
{
oldPath: '/address/:id/internal-transactions',
newPath: `${ PATHS.address_index }?tab=internal_txns`,
oldPath: '/address/:hash/internal-transactions',
newPath: '/address/:hash?tab=internal_txns',
},
{
oldPath: '/address/:id/coin-balances',
newPath: `${ PATHS.address_index }?tab=coin_balance_history`,
oldPath: '/address/:hash/coin-balances',
newPath: '/address/:hash?tab=coin_balance_history',
},
{
oldPath: '/address/:id/validations',
newPath: `${ PATHS.address_index }?tab=blocks_validated`,
oldPath: '/address/:hash/validations',
newPath: '/address/:hash?tab=blocks_validated',
},
{
oldPath: '/address/:id/tokens/:hash/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers&token=:hash`,
oldPath: '/address/:hash/tokens/:token_hash/token-transfers',
newPath: '/address/:hash?tab=token_transfers&token=:token_hash',
},
// contract verification
{
oldPath: '/address/:id/contract_verifications/new',
newPath: `${ PATHS.address_contract_verification }`,
oldPath: '/address/:hash/contract_verifications/new',
newPath: '/address/:hash/contract_verification',
},
{
oldPath: '/address/:id/verify-via-flattened-code/new',
newPath: `${ PATHS.address_contract_verification }?method=flatten_source_code`,
oldPath: '/address/:hash/verify-via-flattened-code/new',
newPath: '/address/:hash/contract_verification?method=flatten_source_code',
},
{
oldPath: '/address/:id/verify-via-standard-json-input/new',
newPath: `${ PATHS.address_contract_verification }?method=standard_input`,
oldPath: '/address/:hash/verify-via-standard-json-input/new',
newPath: '/address/:hash/contract_verification?method=standard_input',
},
{
oldPath: '/address/:id/verify-via-metadata-json/new',
newPath: `${ PATHS.address_contract_verification }?method=sourcify`,
oldPath: '/address/:hash/verify-via-metadata-json/new',
newPath: '/address/:hash/contract_verification?method=sourcify',
},
{
oldPath: '/address/:id/verify-via-multi-part-files/new',
newPath: `${ PATHS.address_contract_verification }?method=multi_part_file`,
oldPath: '/address/:hash/verify-via-multi-part-files/new',
newPath: '/address/:hash/contract_verification?method=multi_part_file',
},
{
oldPath: '/address/:id/verify-vyper-contract/new',
newPath: `${ PATHS.address_contract_verification }?method=vyper_contract`,
oldPath: '/address/:hash/verify-vyper-contract/new',
newPath: '/address/:hash/contract_verification?method=vyper_contract',
},
];
......
......@@ -126,14 +126,16 @@ frontend:
_default: ENC[AES256_GCM,data:cRcbMzOW2AFyDz/lqv4T9SpDCabXdBKLFN9dbq/rFg4=,iv:G9afggfvZ+BpuE5u31KDVfIxhlP38IE28Yt9pMQsd6E=,tag:t1kkjXPolc/58fuQVULLfA==,type:str]
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_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:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-01-30T18:25:02Z"
mac: ENC[AES256_GCM,data:osscQyqUAUatyEzeIHCj10+uj1vCzKJC9W0IyChPKR+KQSEW0I2l20nFcSMCb9sGWVNXzQCfltsDTXzHM5nIuzP2qn/qVmwntQcKB4ibGzaE1gshVQA6deQ28UvGkMjkNTKszqtONgc02G1Kl0d7yiigx+jAQDJNT94amwO/OPs=,iv:6g4T3KJTH0u96y+QBjjJPwjuU4+5psd1cIw+AddvWdE=,tag:Z59JO8iZwfbx80wACXk6sA==,type:str]
lastmodified: "2023-02-14T08:03:14Z"
mac: ENC[AES256_GCM,data:B2AkQb4I83dP3UUitRCcrUfzm3nWmcknIUoMWHyYaG9jasnccbr8zZatYdpbvKFcELVTtjhYk6ly5Sx7+6sk2PZm6o7dN3yHG5lSWmnZqNXkwo42GIk/F6vzDdLutZsu8HH8pWHd9y5R272CIPOOh4+Ur0OtwiGgj3Bp1od76qM=,iv:j7aIPflH0FsYhE/iylvBh5nDmVdghhxAFvaeXlR560k=,tag:/oe6OeitIHaZ4TgM7w/0pg==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -81,7 +81,7 @@ blockscout:
# # _default: ws://geth-svc:8546
# _default: ws://geth-svc.goerli.svc.cluster.local:8546
BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
_default: v5.1.0-beta
ECTO_USE_SSL:
_default: 'false'
ETHEREUM_JSONRPC_VARIANT:
......@@ -116,24 +116,12 @@ blockscout:
_default: 1
COIN_BALANCE_HISTORY_DAYS:
_default: 90
GAS_PRICE_ORACLE_NUM_OF_BLOCKS:
_default: 200
GAS_PRICE_ORACLE_SAFELOW_PERCENTILE:
_default: 35
GAS_PRICE_ORACLE_AVERAGE_PERCENTILE:
_default: 60
GAS_PRICE_ORACLE_FAST_PERCENTILE:
_default: 90
GAS_PRICE_ORACLE_CACHE_PERIOD:
_default: 300
POOL_SIZE:
_default: 100
DISPLAY_TOKEN_ICONS:
_default: 'true'
FETCH_REWARDS_WAY:
_default: manual
INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER:
_default: 'true'
SHOW_TESTNET_LABEL:
_default: 'true'
CHAIN_ID:
......@@ -141,7 +129,7 @@ blockscout:
ENABLE_RUST_VERIFICATION_SERVICE:
_default: 'true'
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:80
_default: http://sc-verifier-svc:8050
INDEXER_MEMORY_LIMIT:
_default: 5
ACCOUNT_ENABLED:
......@@ -154,6 +142,30 @@ blockscout:
_default: '[{"title": "Marketplace", "url": "/apps", "embedded?": true}]'
SESSION_COOKIE_DOMAIN:
_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:
enabled: true
......
......@@ -72,14 +72,16 @@ frontend:
_default: ENC[AES256_GCM,data:4cTeqxQnGcpzPK4bMqxZpLgMeFSSDbajN/fmb1UunH8=,iv:bPTQfahGfWF1OfArvYQeSQItMa0Ymkt6eUfDZFBQSOY=,tag:8xHe5AUkbH/rl6cOfkVAKg==,type:str]
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_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:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-01-30T18:24:47Z"
mac: ENC[AES256_GCM,data:KYXWcGq3Irq7styUQke+YFlpClE5LzmPLb7tIJcWmTTnksQKZcLibPJIoEBbPLjznJeKpY/AU777CXStihtZPcWeey7NO8VWk2LcVI1+r+/jzj1Gu0bqfIXIqTXFRy46QBjNsbRYMlZRyWlXSKlXXx4Ahp86sHu5nOHJ3oMe1FY=,iv:70iEvTB2J8lZuQzatWg4OKBN2/vYVoNabdJtbp7X8Y8=,tag:SYI8NKMOq67tYPmSWVQiMA==,type:str]
lastmodified: "2023-02-14T08:03:25Z"
mac: ENC[AES256_GCM,data:xGhF8Zc6BEh0TeAlp6UBTGZ6DNIL/d+nO7xNj2xvuHDmxElg816d8jxErbkgJzoNhs5DGbQXmp3pAUL4JPg8x5T+pn0hodvSb7c6zOlfTwumRC+6R6vC+ZOlxbzut4dSvtWxsPmrncWZLg6hyOilps7qvkLCj2d5UC+U7c1uQjc=,iv:OzLsfii8F3VFSSkUaY7ZjolgBbt8oXEtOYTHHMbJtPU=,tag:0rLPkbDmOSxOufCk9iZTDQ==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -4,14 +4,14 @@ import appConfig from 'configs/app/config';
import isNeedProxy from './isNeedProxy';
import { RESOURCES } from './resources';
import type { ApiResource, ResourceName } from './resources';
import type { ApiResource, ResourceName, ResourcePathParams } from './resources';
export default function buildUrl(
_resource: ApiResource | ResourceName,
pathParams?: Record<string, string | undefined>,
export default function buildUrl<R extends ResourceName>(
resourceName: R,
pathParams?: ResourcePathParams<R>,
queryParams?: Record<string, string | Array<string> | number | undefined>,
) {
const resource: ApiResource = typeof _resource === 'string' ? RESOURCES[_resource] : _resource;
): string {
const resource: ApiResource = RESOURCES[resourceName];
const baseUrl = isNeedProxy() ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
......
......@@ -2,7 +2,6 @@ import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, Ap
import type {
Address,
AddressCounters,
AddressTokenBalance,
AddressTransactionsResponse,
AddressTokenTransferResponse,
AddressCoinBalanceHistoryResponse,
......@@ -24,8 +23,15 @@ import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo';
import type { TokensResponse, TokensFilters } from 'types/api/tokens';
import type {
TokenCounters,
TokenInfo,
TokenHolders,
TokenInventoryResponse,
TokenInstance,
TokenInstanceTransfersCount,
} from 'types/api/token';
import type { TokensResponse, TokensFilters, TokenInstanceTransferResponse } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
......@@ -38,6 +44,7 @@ export interface ApiResource {
path: string;
endpoint?: string;
basePath?: string;
pathParams?: Array<string>;
}
export const RESOURCES = {
......@@ -50,21 +57,27 @@ export const RESOURCES = {
},
custom_abi: {
path: '/api/account/v1/user/custom_abis/:id?',
pathParams: [ 'id' as const ],
},
watchlist: {
path: '/api/account/v1/user/watchlist/:id?',
pathParams: [ 'id' as const ],
},
public_tags: {
path: '/api/account/v1/user/public_tags/:id?',
pathParams: [ 'id' as const ],
},
private_tags_address: {
path: '/api/account/v1/user/tags/address/:id?',
pathParams: [ 'id' as const ],
},
private_tags_tx: {
path: '/api/account/v1/user/tags/transaction/:id?',
pathParams: [ 'id' as const ],
},
api_keys: {
path: '/api/account/v1/user/api_keys/:id?',
pathParams: [ 'id' as const ],
},
// STATS
......@@ -80,6 +93,7 @@ export const RESOURCES = {
},
stats_line: {
path: '/api/v1/lines/:id',
pathParams: [ 'id' as const ],
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
......@@ -98,10 +112,12 @@ export const RESOURCES = {
filterFields: [ 'type' as const ],
},
block: {
path: '/api/v2/blocks/:id',
path: '/api/v2/blocks/:height',
pathParams: [ 'height' as const ],
},
block_txs: {
path: '/api/v2/blocks/:id/transactions',
path: '/api/v2/blocks/:height/transactions',
pathParams: [ 'height' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
......@@ -116,25 +132,30 @@ export const RESOURCES = {
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
},
tx: {
path: '/api/v2/transactions/:id',
path: '/api/v2/transactions/:hash',
pathParams: [ 'hash' as const ],
},
tx_internal_txs: {
path: '/api/v2/transactions/:id/internal-transactions',
path: '/api/v2/transactions/:hash/internal-transactions',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ ],
},
tx_logs: {
path: '/api/v2/transactions/:id/logs',
path: '/api/v2/transactions/:hash/logs',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ ],
},
tx_token_transfers: {
path: '/api/v2/transactions/:id/token-transfers',
path: '/api/v2/transactions/:hash/token-transfers',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ 'type' as const ],
},
tx_raw_trace: {
path: '/api/v2/transactions/:id/raw-trace',
path: '/api/v2/transactions/:hash/raw-trace',
pathParams: [ 'hash' as const ],
},
// ADDRESSES
......@@ -146,102 +167,146 @@ export const RESOURCES = {
// ADDRESS
address: {
path: '/api/v2/addresses/:id',
path: '/api/v2/addresses/:hash',
pathParams: [ 'hash' as const ],
},
address_counters: {
path: '/api/v2/addresses/:id/counters',
},
address_token_balances: {
path: '/api/v2/addresses/:id/token-balances',
path: '/api/v2/addresses/:hash/counters',
pathParams: [ 'hash' as const ],
},
// this resource doesn't have pagination, so causing huge problems on some addresses page
// address_token_balances: {
// path: '/api/v2/addresses/:hash/token-balances',
// },
address_txs: {
path: '/api/v2/addresses/:id/transactions',
path: '/api/v2/addresses/:hash/transactions',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [ 'filter' as const ],
},
address_internal_txs: {
path: '/api/v2/addresses/:id/internal-transactions',
path: '/api/v2/addresses/:hash/internal-transactions',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const ],
},
address_token_transfers: {
path: '/api/v2/addresses/:id/token-transfers',
path: '/api/v2/addresses/:hash/token-transfers',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
},
address_blocks_validated: {
path: '/api/v2/addresses/:id/blocks-validated',
path: '/api/v2/addresses/:hash/blocks-validated',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ],
},
address_coin_balance: {
path: '/api/v2/addresses/:id/coin-balance-history',
path: '/api/v2/addresses/:hash/coin-balance-history',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ],
},
address_coin_balance_chart: {
path: '/api/v2/addresses/:id/coin-balance-history-by-day',
path: '/api/v2/addresses/:hash/coin-balance-history-by-day',
pathParams: [ 'hash' as const ],
},
address_logs: {
path: '/api/v2/addresses/:id/logs',
path: '/api/v2/addresses/:hash/logs',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'transaction_index' as const, 'index' as const, 'block_number' as const ],
filterFields: [ ],
},
address_tokens: {
path: '/api/v2/addresses/:id/tokens',
path: '/api/v2/addresses/:hash/tokens',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const ],
filterFields: [ 'type' as const ],
},
// CONTRACT
contract: {
path: '/api/v2/smart-contracts/:id',
path: '/api/v2/smart-contracts/:hash',
pathParams: [ 'hash' as const ],
},
contract_methods_read: {
path: '/api/v2/smart-contracts/:id/methods-read',
path: '/api/v2/smart-contracts/:hash/methods-read',
pathParams: [ 'hash' as const ],
},
contract_methods_read_proxy: {
path: '/api/v2/smart-contracts/:id/methods-read-proxy',
path: '/api/v2/smart-contracts/:hash/methods-read-proxy',
pathParams: [ 'hash' as const ],
},
contract_method_query: {
path: '/api/v2/smart-contracts/:id/query-read-method',
path: '/api/v2/smart-contracts/:hash/query-read-method',
pathParams: [ 'hash' as const ],
},
contract_methods_write: {
path: '/api/v2/smart-contracts/:id/methods-write',
path: '/api/v2/smart-contracts/:hash/methods-write',
pathParams: [ 'hash' as const ],
},
contract_methods_write_proxy: {
path: '/api/v2/smart-contracts/:id/methods-write-proxy',
path: '/api/v2/smart-contracts/:hash/methods-write-proxy',
pathParams: [ 'hash' as const ],
},
contract_verification_config: {
path: '/api/v2/smart-contracts/verification/config',
},
contract_verification_via: {
path: '/api/v2/smart-contracts/:id/verification/via/:method',
path: '/api/v2/smart-contracts/:hash/verification/via/:method',
pathParams: [ 'hash' as const, 'method' as const ],
},
// TOKEN
token: {
path: '/api/v2/tokens/:hash',
pathParams: [ 'hash' as const ],
},
token_counters: {
path: '/api/v2/tokens/:hash/counters',
pathParams: [ 'hash' as const ],
},
token_holders: {
path: '/api/v2/tokens/:hash/holders',
pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [],
},
token_transfers: {
path: '/api/v2/tokens/:hash/transfers',
pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
token_inventory: {
path: '/api/v2/tokens/:hash/instances',
pathParams: [ 'hash' as const ],
paginationFields: [ 'unique_token' as const ],
filterFields: [],
},
tokens: {
path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ],
filterFields: [ 'filter' as const, 'type' as const ],
},
// TOKEN INSTANCE
token_instance: {
path: '/api/v2/tokens/:hash/instances/:id',
pathParams: [ 'hash' as const, 'id' as const ],
},
token_instance_transfers_count: {
path: '/api/v2/tokens/:hash/instances/:id/transfers-count',
pathParams: [ 'hash' as const, 'id' as const ],
},
token_instance_transfers: {
path: '/api/v2/tokens/:hash/instances/:id/transfers',
pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
filterFields: [],
},
// HOMEPAGE
homepage_stats: {
path: '/api/v2/stats',
......@@ -300,6 +365,15 @@ export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R]
export const resourceKey = (x: keyof typeof RESOURCES) => x;
type ResourcePathParamName<Resource extends ResourceName> =
typeof RESOURCES[Resource] extends { pathParams: Array<string> } ?
ArrayElement<typeof RESOURCES[Resource]['pathParams']> :
string;
export type ResourcePathParams<Resource extends ResourceName> = typeof RESOURCES[Resource] extends { pathParams: Array<string> } ?
Record<ResourcePathParamName<Resource>, string | undefined> :
never;
export interface ResourceError<T = unknown> {
payload?: T;
status: Response['status'];
......@@ -315,7 +389,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' |
'address_logs' | 'address_tokens' |
'token_transfers' | 'token_holders' | 'tokens';
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
'token_instance_transfers';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -350,7 +425,6 @@ Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters :
Q extends 'address_token_balances' ? Array<AddressTokenBalance> :
Q extends 'address_txs' ? AddressTransactionsResponse :
Q extends 'address_internal_txs' ? AddressInternalTxsResponse :
Q extends 'address_token_transfers' ? AddressTokenTransferResponse :
......@@ -363,6 +437,10 @@ Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters :
Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders :
Q extends 'token_instance' ? TokenInstance :
Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount :
Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract :
......
......@@ -6,10 +6,10 @@ import useFetch from 'lib/hooks/useFetch';
import buildUrl from './buildUrl';
import { RESOURCES } from './resources';
import type { ApiResource } from './resources';
import type { ApiResource, ResourceName, ResourcePathParams } from './resources';
export interface Params {
pathParams?: Record<string, string | undefined>;
export interface Params<R extends ResourceName> {
pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | Array<string> | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
}
......@@ -17,12 +17,12 @@ export interface Params {
export default function useApiFetch() {
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,
{ pathParams, queryParams, fetchParams }: Params = {},
{ pathParams, queryParams, fetchParams }: Params<R> = {},
) => {
const resource: ApiResource = RESOURCES[resourceName];
const url = buildUrl(resource, pathParams, queryParams);
const url = buildUrl(resourceName, pathParams, queryParams);
return fetch<SuccessType, ErrorType>(url, {
credentials: 'include',
...(resource.endpoint && appConfig.host === 'localhost' ? {
......
......@@ -5,7 +5,7 @@ import type { ResourceError, ResourceName, ResourcePayload } from './resources';
import type { Params as ApiFetchParams } 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'>;
}
......
import type BigNumber from 'bignumber.js';
export default function sumBnReducer(result: BigNumber, item: BigNumber) {
return result.plus(item);
}
......@@ -141,6 +141,9 @@ function makePolicyMap() {
// walletconnect
'*.walletconnect.com',
// token's media
'ipfs.io',
],
'font-src': [
......
import BigNumber from 'bignumber.js';
import { ZERO } from 'lib/consts';
interface Params {
value: string;
exchangeRate?: string | null;
......@@ -13,10 +15,11 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
let usdResult: string | undefined;
let usdBn = ZERO;
if (exchangeRate) {
const exchangeRateBn = new BigNumber(exchangeRate);
const usdBn = valueCurr.times(exchangeRateBn);
usdBn = valueCurr.times(exchangeRateBn);
if (accuracyUsd && !usdBn.isEqualTo(0)) {
const usdBnDp = usdBn.dp(accuracyUsd);
usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat();
......@@ -25,5 +28,5 @@ export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimal
}
}
return { valueStr: valueResult, usd: usdResult };
return { valueStr: valueResult, usd: usdResult, usdBn };
}
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() {
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 appConfig from 'configs/app/config';
......@@ -14,40 +16,76 @@ import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg';
import watchlistIcon from 'icons/watchlist.svg';
import link from 'lib/link/link';
import useCurrentRoute from 'lib/link/useCurrentRoute';
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 currentRoute = useCurrentRoute()();
const router = useRouter();
const pathname = router.pathname;
return React.useMemo(() => {
const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: true },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: true },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute.startsWith('token'), isNewUi: true },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: true },
{ text: 'Blocks', nextRoute: { pathname: '/blocks' as const }, icon: blocksIcon, isActive: pathname.startsWith('/block'), isNewUi: true },
{ text: 'Transactions', nextRoute: { pathname: '/txs' as const }, icon: transactionsIcon, isActive: pathname.startsWith('/tx'), isNewUi: true },
{ text: 'Tokens', nextRoute: { pathname: '/tokens' as const }, icon: tokensIcon, isActive: pathname.startsWith('/token'), isNewUi: true },
{ text: 'Accounts', nextRoute: { pathname: '/accounts' as const }, icon: walletIcon, isActive: pathname === '/accounts', isNewUi: true },
isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute.startsWith('app'), isNewUi: true } : null,
{ text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: true },
{ text: 'Apps', nextRoute: { pathname: '/apps' as const }, icon: appsIcon, isActive: pathname.startsWith('/app'), isNewUi: true } : null,
{ 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'
// 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
// { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' },
// { text: 'Other', url: link('other'), icon: gearIcon, isActive: pathname === 'other' },
].filter(notEmpty);
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: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags', isNewUi: true },
{ text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys', isNewUi: true },
{ text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi', isNewUi: true },
{
text: 'Watchlist',
nextRoute: { pathname: '/account/watchlist' as const },
icon: watchlistIcon,
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 };
}, [ isMarketplaceFilled, currentRoute ]);
}, [ isMarketplaceFilled, pathname ]);
}
......@@ -3,14 +3,13 @@ import { useRouter } from 'next/router';
import React from 'react';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import { ROUTES } from 'lib/link/routes';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
function getSocketParams(router: NextRouter) {
if (
router.pathname === ROUTES.txs.pattern &&
router.pathname === '/txs' &&
(router.query.tab === 'validated' || router.query.tab === undefined) &&
!router.query.block_number &&
!router.query.page
......@@ -18,12 +17,12 @@ function getSocketParams(router: NextRouter) {
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 };
}
if (
router.pathname === ROUTES.txs.pattern &&
router.pathname === '/txs' &&
router.query.tab === 'pending' &&
!router.query.block_number &&
!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';
export default function getSeo(params: PageParams) {
export default function getSeo(params: RoutedQuery<'/address/[hash]'>) {
const networkTitle = getNetworkTitle();
return {
title: params ? `${ params.id } - ${ networkTitle }` : '',
title: params ? `${ params.hash } - ${ networkTitle }` : '',
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';
export default function getSeo(params?: PageParams) {
export default function getSeo(params?: RoutedQuery<'/block/[height]'>) {
const networkTitle = getNetworkTitle();
return {
title: params ? `Block ${ params.id } - ${ networkTitle }` : '',
description: params ? `View the transactions, token transfers, and uncles for block number ${ params.id }` : '',
title: params ? `Block ${ params.height } - ${ networkTitle }` : '',
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 = {
cookies: string;
referrer: string;
id?: string;
height?: string;
hash?: string;
}
export const getServerSideProps: GetServerSideProps<Props> = async({ req, query }) => {
......@@ -12,6 +14,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async({ req, query
cookies: req.headers.cookie || '',
referrer: req.headers.referer || '',
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';
export default function getSeo(params?: PageParams) {
export default function getSeo(params?: RoutedQuery<'/tx/[hash]'>) {
const networkTitle = getNetworkTitle();
return {
title: params ? `Transaction ${ params.id } - ${ networkTitle }` : '',
description: params ? `View transaction ${ params.id } on ${ networkTitle }` : '',
title: params ? `Transaction ${ params.hash } - ${ 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 || '';
}
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenType } from 'types/api/token';
const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
......
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { route } from 'nextjs-routes';
import appConfig from 'configs/app/config';
import { NAMES } from 'lib/cookies';
import getCspPolicy from 'lib/csp/getCspPolicy';
import link from 'lib/link/link';
const cspPolicy = getCspPolicy();
......@@ -22,7 +22,7 @@ export function middleware(req: NextRequest) {
const apiToken = req.cookies.get(NAMES.API_TOKEN);
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);
}
......
......@@ -80,20 +80,26 @@ export const erc1155LongId: AddressTokenBalance = {
value: '42',
};
export const baseList = [
erc20a,
erc20b,
erc20c,
erc721a,
erc721b,
erc721c,
erc1155withoutName,
erc1155a,
erc1155b,
];
export const longValuesList = [
erc20LongSymbol,
erc721LongSymbol,
erc1155LongId,
];
export const erc20List = {
items: [
erc20a,
erc20b,
erc20c,
],
};
export const erc721List = {
items: [
erc721a,
erc721b,
erc721c,
],
};
export const erc1155List = {
items: [
erc1155withoutName,
erc1155a,
erc1155b,
],
};
import type { TokenHolders } from 'types/api/tokenInfo';
import type { TokenHolders } from 'types/api/token';
import { withName, withoutName } from 'mocks/address/address';
......
import type { TokenCounters, TokenInfo } from 'types/api/tokenInfo';
import type { TokenCounters, TokenInfo } from 'types/api/token';
export const tokenInfo: TokenInfo = {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
......
import type { TokenInstance } from 'types/api/token';
import * as addressMock from '../address/address';
import { tokenInfoERC721a } from './tokenInfo';
export const base: TokenInstance = {
animation_url: null,
external_app_url: null,
id: '32925298983216553915666621415831103694597106215670571463977478984525997408266',
image_url: null,
is_unique: false,
holder_address_hash: null,
metadata: {
animation_url: null,
description: 'Sign for you!',
external_link: null,
image: 'https://i.seadn.io/gcs/files/1ee1c5e1ead058322615e3206abb8ba3.png?w=500&auto=format',
name: 'Sign4U',
},
owner: addressMock.withName,
token: tokenInfoERC721a,
};
......@@ -112,7 +112,7 @@ export const erc1155: TokenTransfer = {
exchange_rate: null,
holders: '1',
name: null,
symbol: null,
symbol: 'MY_SYMBOL_IS_VERY_LONG',
type: 'ERC-1155',
total_supply: '0',
},
......
const withRoutes = require('nextjs-routes/config')({
outDir: 'types',
});
const path = require('path');
const headers = require('./configs/nextjs/headers');
......@@ -38,4 +41,4 @@ const moduleExports = {
},
};
module.exports = moduleExports;
module.exports = withRoutes(moduleExports);
import type { NextPage } from 'next';
import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react';
import type { PageParams } from 'lib/next/address/types';
import getSeo from 'lib/next/address/getSeo';
import ContractVerification from 'ui/pages/ContractVerification';
const ContractVerificationPage: NextPage<PageParams> = ({ id }: PageParams) => {
const { title, description } = getSeo({ id });
const ContractVerificationPage: NextPage<RoutedQuery<'/address/[hash]/contract_verification'>> =
({ hash }: RoutedQuery<'/address/[hash]/contract_verification'>) => {
const { title, description } = getSeo({ hash });
return (
<>
......
import type { NextPage } from 'next';
import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react';
import type { PageParams } from 'lib/next/address/types';
import getSeo from 'lib/next/address/getSeo';
import Address from 'ui/pages/Address';
const AddressPage: NextPage<PageParams> = ({ id }: PageParams) => {
const { title, description } = getSeo({ id });
const AddressPage: NextPage<RoutedQuery<'/address/[hash]'>> = ({ hash }: RoutedQuery<'/address/[hash]'>) => {
const { title, description } = getSeo({ hash });
return (
<>
......
......@@ -16,7 +16,7 @@ const AppPage: NextPage = () => {
const [ isLoading, setIsLoading ] = useState(true);
const [ app, setApp ] = useState<AppItemOverview | undefined>(undefined);
const { id }: { id?: string } = router.query;
const id = router.query.id;
useEffect(() => {
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 Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react';
import type { PageParams } from 'lib/next/block/types';
import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block';
const BlockPage: NextPage<PageParams> = ({ id }: PageParams) => {
const { title, description } = getSeo({ id });
const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => {
const { title, description } = getSeo({ height });
return (
<>
<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 Head from 'next/head';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SearchRedirectResult } from 'types/api/search';
import buildUrlNode from 'lib/api/buildUrlNode';
import fetchFactory from 'lib/api/nodeFetch';
import link from 'lib/link/link';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props } 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
const redirectUrl = (() => {
switch (payload.type) {
case 'block': {
return link('block', { id: q });
return route({ pathname: '/block/[height]', query: { height: q } });
}
case 'address': {
return link('address_index', { id: payload.parameter || q });
return route({ pathname: '/address/[hash]', query: { hash: payload.parameter || q } });
}
case 'transaction': {
return link('tx', { id: q });
return route({ pathname: '/tx/[hash]', query: { hash: q } });
}
}
})();
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from 'lib/next/token/types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import TokenInstance from 'ui/pages/TokenInstance';
const TokenInstancePage: NextPage<PageParams> = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<TokenInstance/>
</>
);
};
export default TokenInstancePage;
export { getServerSideProps } from 'lib/next/token/getServerSideProps';
import type { NextPage } from 'next';
import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react';
import type { PageParams } from 'lib/next/tx/types';
import getSeo from 'lib/next/tx/getSeo';
import Transaction from 'ui/pages/Transaction';
const TransactionPage: NextPage<PageParams> = ({ id }: PageParams) => {
const { title, description } = getSeo({ id });
const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQuery<'/tx/[hash]'>) => {
const { title, description } = getSeo({ hash });
return (
<>
......
import './fonts.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import _defaultsDeep from 'lodash/defaultsDeep';
import MockDate from 'mockdate';
import * as router from 'next/router';
const NEXT_ROUTER_MOCK = {
query: {},
pathname: '',
};
beforeMount(async({ hooksConfig }) => {
// Before mount, redefine useRouter to return mock value from test.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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
MockDate.set('2022-11-11T12:00:00Z');
......
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';
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];
return compile('/node-api/proxy/poa/core' + resource.path)(pathParams);
}
......@@ -22,7 +22,7 @@ const baseStyleDialog = defineStyle((props) => {
const baseStyleDialogContainer = defineStyle({
'::-webkit-scrollbar': { display: '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) => ({
......
......@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction';
import type { AddressTag, WatchlistName } from './addressParams';
import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction';
import type { TokenInfo, TokenType } from './tokenInfo';
import type { TokenInfo, TokenType } from './token';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
export interface Address {
......
......@@ -34,3 +34,28 @@ export type TokenHoldersPagination = {
items_count: number;
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 { TokenInfoGeneric, TokenType } from './tokenInfo';
import type { TokenInfoGeneric, TokenType } from './token';
export type Erc20TotalPayload = {
decimals: string | null;
......@@ -46,7 +46,6 @@ export type TokenTransferPagination = {
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
}
export interface TokenTransferResponse {
......
export type Tokenlist = {
message: string;
result: Array<TokenlistItem> | string;
}
export type TokenlistItem = {
balance: number;
contractAddress: string;
decimals: number | null;
id: number;
name: string;
symbol: string;
type: string;
}
import type { TokenInfo, TokenType } from './tokenInfo';
import type { TokenInfo, TokenType } from './token';
import type { TokenTransfer } from './tokenTransfer';
export type TokensResponse = {
items: Array<TokenInfo>;
......@@ -10,3 +11,15 @@ export type TokensResponse = {
}
export type TokensFilters = { filter: string; type: Array<TokenType> | undefined };
export interface TokenInstanceTransferResponse {
items: Array<TokenTransfer>;
next_page_params: TokenInstanceTransferPagination | null;
}
export interface TokenInstanceTransferPagination {
block_number: number;
index: number;
items_count: number;
token_id: string;
}
// 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) => {
const queryClient = useQueryClient();
const router = useRouter();
const addressHash = String(router.query?.id);
const addressHash = String(router.query.hash);
const query = useQueryWithPages({
resourceName: 'address_blocks_validated',
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
scrollRef,
});
......@@ -46,7 +46,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
setSocketAlert(false);
queryClient.setQueryData(
getResourceKey('address_blocks_validated', { pathParams: { id: addressHash } }),
getResourceKey('address_blocks_validated', { pathParams: { hash: addressHash } }),
(prevData: AddressBlocksValidatedResponse | undefined) => {
if (!prevData) {
return;
......
......@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressCoinBalance from './AddressCoinBalance';
const addressHash = 'hash';
const BALANCE_HISTORY_API_URL = buildApiUrl('address_coin_balance', { id: addressHash });
const BALANCE_HISTORY_CHART_API_URL = buildApiUrl('address_coin_balance_chart', { id: addressHash });
const BALANCE_HISTORY_API_URL = buildApiUrl('address_coin_balance', { hash: addressHash });
const BALANCE_HISTORY_CHART_API_URL = buildApiUrl('address_coin_balance_chart', { hash: addressHash });
const hooksConfig = {
router: {
query: { id: addressHash },
query: { hash: addressHash },
},
};
......
......@@ -7,6 +7,7 @@ import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import SocketAlert from 'ui/shared/SocketAlert';
......@@ -20,10 +21,10 @@ const AddressCoinBalance = () => {
const router = useRouter();
const scrollRef = React.useRef<HTMLDivElement>(null);
const addressHash = String(router.query?.id);
const addressHash = getQueryParamString(router.query.hash);
const coinBalanceQuery = useQueryWithPages({
resourceName: 'address_coin_balance',
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
scrollRef,
});
......@@ -35,7 +36,7 @@ const AddressCoinBalance = () => {
setSocketAlert(false);
queryClient.setQueryData(
getResourceKey('address_coin_balance', { pathParams: { id: addressHash } }),
getResourceKey('address_coin_balance', { pathParams: { hash: addressHash } }),
(prevData: AddressCoinBalanceHistoryResponse | undefined) => {
if (!prevData) {
return;
......
import { chakra, Icon, Link, Tooltip, Hide } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import svgFileIcon from 'icons/files/csv.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
interface Props {
address: string;
......@@ -20,7 +20,9 @@ const AddressCsvExportLink = ({ className, address, type }: Props) => {
className={ className }
display="inline-flex"
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 }}/>
<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';
import type { ResourceError } from 'lib/api/resources';
import * as addressMock from 'mocks/address/address';
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 buildApiUrl from 'playwright/utils/buildApiUrl';
......@@ -16,12 +16,14 @@ import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage';
const ADDRESS_HASH = addressMock.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH });
const API_URL_COUNTERS = buildApiUrl('address_counters', { id: ADDRESS_HASH });
const API_URL_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH });
const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_COUNTERS = buildApiUrl('address_counters', { hash: 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 = {
router: {
query: { id: ADDRESS_HASH },
query: { hash: ADDRESS_HASH },
},
};
......@@ -54,10 +56,18 @@ test('token', async({ mount, page }) => {
status: 200,
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,
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(() => {
window.ethereum = { } as MetaMaskInpageProvider;
......
import { Box, Flex, Text, Icon, Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React from 'react';
import type { Address as TAddress } from 'types/api/address';
......@@ -9,25 +10,19 @@ import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import AddressAddToMetaMask from './details/AddressAddToMetaMask';
import AddressBalance from './details/AddressBalance';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressFavoriteButton from './details/AddressFavoriteButton';
import AddressNameInfo from './details/AddressNameInfo';
import AddressQrCode from './details/AddressQrCode';
import TokenSelect from './tokenSelect/TokenSelect';
interface Props {
......@@ -37,14 +32,13 @@ interface Props {
const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter();
const isMobile = useIsMobile();
const addressHash = router.query.id?.toString();
const addressHash = getQueryParamString(router.query.hash);
const countersQuery = useApiQuery('address_counters', {
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
enabled: Boolean(addressHash) && Boolean(addressQuery.data),
},
});
......@@ -92,23 +86,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
return (
<Box>
<Flex alignItems="center">
<AddressIcon address={ data }/>
<Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }>
{ isMobile ? <HashStringShorten hash={ data.hash }/> : data.hash }
</Text>
<CopyToClipboard text={ data.hash }/>
{ data.is_contract && data.token && <AddressAddToMetaMask ml={ 2 } token={ data.token }/> }
{ !data.is_contract && (
<AddressFavoriteButton hash={ data.hash } isAdded={ Boolean(data.watchlist_names?.length) } ml={ 3 }/>
) }
<AddressQrCode hash={ data.hash } ml={ 2 }/>
</Flex>
<AddressHeadingInfo address={ data } token={ data.token } isLinkDisabled/>
{ explorers.length > 0 && (
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
<Text fontSize="sm">Verify with other explorers</Text>
{ 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() }/>;
}) }
</Flex>
......@@ -123,7 +106,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
{ data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation."
hint="Transaction and address of creation"
>
<AddressLink type="address" hash={ data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at txn </Text>
......@@ -133,10 +116,10 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
{ data.is_contract && data.implementation_address && (
<DetailsInfoItem
title="Implementation"
hint="Implementation address of the proxy contract."
hint="Implementation address of the proxy contract"
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 }/> }
</LinkInternal>
{ data.implementation_name && (
......@@ -150,7 +133,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
{ data.has_tokens && (
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value."
hint="All tokens in the account and total value"
alignSelf="center"
py={ 0 }
>
......@@ -159,7 +142,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
) }
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address."
hint="Number of transactions related to this address"
>
{ addressQuery.data ?
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
......@@ -168,7 +151,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
{ data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
hint="Number of transfers to/from this address"
>
{ addressQuery.data ?
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
......@@ -177,7 +160,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
) }
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address."
hint="Gas used by the address"
>
{ addressQuery.data ?
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
......@@ -186,7 +169,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
{ data.has_validated_blocks && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator."
hint="Number of blocks validated by this validator"
>
{ addressQuery.data ?
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
......@@ -196,12 +179,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
{ data.block_number_balance_updated_at && (
<DetailsInfoItem
title="Last balance update"
hint="Block number in which the address was updated."
hint="Block number in which the address was updated"
alignSelf="center"
py={{ base: '2px', lg: 1 }}
>
<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"
alignItems="center"
>
......
......@@ -9,10 +9,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressInternalTxs from './AddressInternalTxs';
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 = {
router: {
query: { id: ADDRESS_HASH },
query: { hash: ADDRESS_HASH },
},
};
......
import { Text, Show, Hide } from '@chakra-ui/react';
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -9,6 +8,7 @@ import { AddressFromToFilterValues } from 'types/api/address';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressIntTxsSkeletonDesktop from 'ui/address/internals/AddressIntTxsSkeletonDesktop';
import AddressIntTxsSkeletonMobile from 'ui/address/internals/AddressIntTxsSkeletonMobile';
import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable';
......@@ -27,13 +27,11 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
const router = useRouter();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const queryId = router.query.id;
const queryIdArray = castArray(queryId);
const queryIdStr = queryIdArray[0];
const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_internal_txs',
pathParams: { id: queryIdStr },
pathParams: { hash },
filters: { filter: filterValue },
scrollRef,
});
......@@ -70,10 +68,10 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
content = (
<>
<Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ queryIdStr }/>
<AddressIntTxsList data={ data.items } currentAddress={ hash }/>
</Show>
<Hide below="lg" ssr={ false }>
<AddressIntTxsTable data={ data.items } currentAddress={ queryIdStr }/>
<AddressIntTxsTable data={ data.items } currentAddress={ hash }/>
</Hide>
</>
);
......@@ -87,7 +85,7 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
onFilterChange={ handleFilterChange }
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 }/> }
</ActionBar>
{ content }
......
......@@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import LogItem from 'ui/shared/logs/LogItem';
......@@ -12,10 +13,10 @@ import Pagination from 'ui/shared/Pagination';
const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const addressHash = String(router.query?.id);
const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_logs',
pathParams: { id: addressHash },
pathParams: { hash },
scrollRef,
});
......
......@@ -8,12 +8,12 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokenTransfers from './AddressTokenTransfers';
const API_URL = buildApiUrl('address_token_transfers', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) +
const API_URL = buildApiUrl('address_token_transfers', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = {
router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token_hash: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
},
};
......
......@@ -6,7 +6,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import { AddressFromToFilterValues } 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 crossIcon from 'icons/cross.svg';
......@@ -16,12 +16,14 @@ import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
......@@ -69,12 +71,12 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const currentAddress = router.query.id?.toString();
const currentAddress = getQueryParamString(router.query.hash);
const [ socketAlert, setSocketAlert ] = React.useState('');
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>(
{
......@@ -85,7 +87,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_token_transfers',
pathParams: { id: currentAddress },
pathParams: { hash: currentAddress },
filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef,
});
......@@ -117,7 +119,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
}
} else {
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) => {
if (!prevData) {
return;
......@@ -147,7 +149,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
}, []);
const channel = useSocketChannel({
topic: `addresses:${ (router.query.id as string).toLowerCase() }`,
topic: `addresses:${ currentAddress.toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: pagination.page !== 1 || Boolean(tokenFilter),
......@@ -160,7 +162,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
});
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 = (() => {
if (isLoading) {
......@@ -225,23 +227,25 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
})();
const tokenFilterComponent = tokenFilter && (
<Flex alignItems="center" py={ 1 } flexWrap="wrap" mb={{ base: isPaginationVisible ? 6 : 3, lg: 0 }}>
Filtered by token
<TokenLogo hash={ tokenFilter } boxSize={ 6 } mx={ 2 }/>
{ isMobile ? tokenFilter.slice(0, 4) + '...' + tokenFilter.slice(-4) : tokenFilter }
<Tooltip label="Reset filter">
<Flex>
<Icon
as={ crossIcon }
boxSize={ 6 }
ml={ 1 }
color={ resetTokenIconColor }
cursor="pointer"
_hover={{ color: resetTokenIconHoverColor }}
onClick={ resetTokenFilter }
/>
</Flex>
</Tooltip>
<Flex alignItems="center" flexWrap="wrap" mb={{ base: isActionBarHidden ? 3 : 6, lg: 0 }} mr={ 4 }>
<Text whiteSpace="nowrap" mr={ 2 } py={ 1 }>Filtered by token</Text>
<Flex alignItems="center" py={ 1 }>
<TokenLogo hash={ tokenFilter } boxSize={ 6 } mr={ 2 }/>
{ isMobile ? <HashStringShorten hash={ tokenFilter }/> : tokenFilter }
<Tooltip label="Reset filter">
<Flex>
<Icon
as={ crossIcon }
boxSize={ 6 }
ml={ 1 }
color={ resetTokenIconColor }
cursor="pointer"
_hover={{ color: resetTokenIconHoverColor }}
onClick={ resetTokenFilter }
/>
</Flex>
</Tooltip>
</Flex>
</Flex>
);
......
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 { withName } from 'mocks/address/address';
import * as tokenBalanceMock from 'mocks/address/tokenBalance';
import { baseList } from 'mocks/address/tokenBalance';
import * as tokensMock from 'mocks/address/tokens';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokens from './AddressTokens';
const ADDRESS_HASH = withName.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH });
const API_URL_ADDRESS_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH });
const API_URL_TOKENS = buildApiUrl('address_tokens', { id: ADDRESS_HASH });
const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_TOKENS = buildApiUrl('address_tokens', { hash: ADDRESS_HASH });
const nextPageParams = {
items_count: 50,
......@@ -22,32 +20,50 @@ const nextPageParams = {
value: 1,
};
test('erc20 +@mobile +@dark-mode', async({ mount, page }) => {
const test = base.extend({
page: async({ page }, use) => {
const response20 = {
items: [ tokensMock.erc20a, tokensMock.erc20b, tokensMock.erc20c, tokensMock.erc20d ],
next_page_params: nextPageParams,
};
const response721 = {
items: [ tokensMock.erc721a, tokensMock.erc721b, tokensMock.erc721c ],
next_page_params: nextPageParams,
};
const response1155 = {
items: [ tokensMock.erc1155a, tokensMock.erc1155b ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
await page.route(API_URL_TOKENS + '?type=ERC-721', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response721),
}));
await page.route(API_URL_TOKENS + '?type=ERC-1155', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response1155),
}));
use(page);
},
});
test('erc20 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc20' },
query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' },
isReady: true,
},
};
const response20 = {
items: [ tokenBalanceMock.erc20a, tokenBalanceMock.erc20b, tokenBalanceMock.erc20c, tokenBalanceMock.erc20d ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
......@@ -59,32 +75,14 @@ test('erc20 +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot();
});
test('erc721 +@mobile +@dark-mode', async({ mount, page }) => {
test('erc721 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc721' },
query: { hash: ADDRESS_HASH, tab: 'tokens_erc721' },
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(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
......@@ -96,32 +94,14 @@ test('erc721 +@mobile +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot();
});
test('erc1155 +@mobile +@dark-mode', async({ mount, page }) => {
test('erc1155 +@mobile +@dark-mode', async({ mount }) => {
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc1155' },
query: { hash: ADDRESS_HASH, tab: 'tokens_erc1155' },
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(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
......
......@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
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 useQueryWithPages from 'lib/hooks/useQueryWithPages';
......@@ -36,7 +36,7 @@ const AddressTokens = () => {
const tokensQuery = useQueryWithPages({
resourceName: 'address_tokens',
pathParams: { id: router.query.id?.toString() },
pathParams: { hash: router.query.hash?.toString() },
filters: { type: tokenType },
scrollRef,
});
......
......@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTxs from './AddressTxs';
const API_URL = buildApiUrl('address_txs', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' });
const API_URL = buildApiUrl('address_txs', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' });
const hooksConfig = {
router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' },
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' },
},
};
......
......@@ -10,6 +10,7 @@ import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar';
......@@ -31,13 +32,13 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
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 addressTxsQuery = useQueryWithPages({
resourceName: 'address_txs',
pathParams: { id: currentAddress },
pathParams: { hash: currentAddress },
filters: { filter: filterValue },
scrollRef,
});
......@@ -63,7 +64,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
}
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) => {
if (!prevData) {
return;
......
import { Text, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
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 getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
......@@ -17,7 +17,7 @@ type Props = Block & {
};
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 totalReward = getBlockTotalReward(props);
......
import { Td, Tr, Text, Box, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import type { Block } from 'types/api/block';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal';
import Utilization from 'ui/shared/Utilization/Utilization';
......@@ -15,7 +15,7 @@ type Props = Block & {
};
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 totalReward = getBlockTotalReward(props);
......
......@@ -11,7 +11,7 @@ interface Props {
const AddressCoinBalanceChart = ({ addressHash }: Props) => {
const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', {
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
});
const items = React.useMemo(() => data?.map(({ date, value }) => ({
......
import { Text, Stat, StatHelpText, StatArrow, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
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 { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -18,7 +18,7 @@ type Props = AddressCoinBalanceHistoryItem & {
};
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 isPositiveDelta = deltaBn.gte(ZERO);
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
......
import { Td, Tr, Text, Stat, StatHelpText, StatArrow } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -16,7 +16,7 @@ type Props = AddressCoinBalanceHistoryItem & {
};
const AddressCoinBalanceTableItem = (props: Props) => {
const blockUrl = link('block', { id: String(props.block_number) });
const blockUrl = route({ pathname: '/block/[height]', query: { height: String(props.block_number) } });
const deltaBn = BigNumber(props.delta).div(WEI);
const isPositiveDelta = deltaBn.gte(ZERO);
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
......
......@@ -8,10 +8,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import ContractCode from './ContractCode';
const addressHash = 'hash';
const CONTRACT_API_URL = buildApiUrl('contract', { id: addressHash });
const CONTRACT_API_URL = buildApiUrl('contract', { hash: addressHash });
const hooksConfig = {
router: {
query: { id: addressHash },
query: { hash: addressHash },
},
};
......
import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link';
import dayjs from 'lib/date/dayjs';
import getQueryParamString from 'lib/router/getQueryParamString';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -14,19 +16,19 @@ import RawDataSnippet from 'ui/shared/RawDataSnippet';
import ContractSourceCode from './ContractSourceCode';
const InfoItem = ({ label, value }: { label: string; value: string }) => (
<GridItem display="flex" columnGap={ 6 }>
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }>
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text>
<Text wordBreak="break-all">{ value }</Text>
<Text>{ value }</Text>
</GridItem>
);
));
const ContractCode = () => {
const router = useRouter();
const addressHash = router.query.id?.toString();
const addressHash = getQueryParamString(router.query.hash);
const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash),
refetchOnMount: false,
......@@ -60,7 +62,7 @@ const ContractCode = () => {
ml="auto"
mr={ 3 }
as="a"
href={ link('address_contract_verification', { id: addressHash }) }
href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash } }) }
>
Verify & publish
</Button>
......@@ -74,7 +76,7 @@ const ContractCode = () => {
const decoded = data.decoded_constructor_args
.map(([ value, { name, type } ], index) => {
const valueEl = type === 'address' ?
<LinkInternal href={ link('address_index', { id: value }) }>{ value }</LinkInternal> :
<LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: value } }) }>{ value }</LinkInternal> :
<span>{ value }</span>;
return (
<Box key={ index }>
......@@ -101,7 +103,7 @@ const ContractCode = () => {
return data.external_libraries.map((item) => (
<Box key={ item.address_hash }>
<chakra.span fontWeight={ 500 }>{ item.name }: </chakra.span>
<LinkInternal href={ link('address_index', { id: item.address_hash }, { tab: 'contract' }) }>{ item.address_hash }</LinkInternal>
<LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: item.address_hash, tab: 'contract' } }) }>{ item.address_hash }</LinkInternal>
</Box>
));
})();
......@@ -129,7 +131,7 @@ const ContractCode = () => {
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<LinkInternal href={ link('address_contract_verification', { id: addressHash }) }>Verify & Publish</LinkInternal>
<LinkInternal href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash } }) }>Verify & Publish</LinkInternal>
<span> page</span>
</Alert>
) }
......@@ -152,10 +154,10 @@ const ContractCode = () => {
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name }/> }
{ data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version }/> }
{ data.evm_version && <InfoItem label="EVM version" value={ data.evm_version }/> }
{ data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize"/> }
{ typeof data.optimization_enabled === 'boolean' && <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' }/> }
{ data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) }/> }
{ data.verified_at && <InfoItem label="Verified at" value={ data.verified_at }/> }
{ data.verified_at && <InfoItem label="Verified at" value={ dayjs(data.verified_at).format('LLLL') } wordBreak="break-word"/> }
</Grid>
) }
<Flex flexDir="column" rowGap={ 6 }>
......
......@@ -14,7 +14,7 @@ interface Props {
const ContractImplementationAddress = ({ hash }: Props) => {
const queryClient = useQueryClient();
const data = queryClient.getQueryData<TAddress>(getResourceKey('address', {
pathParams: { id: hash },
pathParams: { hash },
}));
if (!data?.implementation_address) {
......
import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Flex, Icon, Link, Tooltip } from '@chakra-ui/react';
import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Flex, Link } from '@chakra-ui/react';
import _range from 'lodash/range';
import React from 'react';
import type { SmartContractMethod } from 'types/api/contract';
import infoIcon from 'icons/info.svg';
import Hint from 'ui/shared/Hint';
interface Props<T extends SmartContractMethod> {
data: Array<T>;
......@@ -46,32 +46,22 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name }
</Box>
{ item.type === 'fallback' && (
<Tooltip
label={ `The fallback function is executed on a call to the contract if none of the other functions match
the given function signature, or if no data was supplied at all and there is no receive Ether function.
The fallback function always receives data, but in order to also receive Ether it must be marked payable.` }
placement="top"
maxW="320px"
>
<Box cursor="pointer" display="inherit">
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
<Hint
label={
`The fallback function is executed on a call to the contract if none of the other functions match
the given function signature, or if no data was supplied at all and there is no receive Ether function.
The fallback function always receives data, but in order to also receive Ether it must be marked payable.`
}/>
) }
{ item.type === 'receive' && (
<Tooltip
label={ `The receive function is executed on a call to the contract with empty calldata.
This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).
If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
If neither a receive Ether nor a payable fallback function is present,
the contract cannot receive Ether through regular transactions and throws an exception.` }
placement="top"
maxW="320px"
>
<Box cursor="pointer" display="inherit">
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
<Hint
label={
`The receive function is executed on a call to the contract with empty calldata.
This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).
If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
If neither a receive Ether nor a payable fallback function is present,
the contract cannot receive Ether through regular transactions and throws an exception.`
}/>
) }
<AccordionIcon/>
</AccordionButton>
......
......@@ -8,11 +8,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import ContractRead from './ContractRead';
const addressHash = 'hash';
const CONTRACT_READ_METHODS_API_URL = buildApiUrl('contract_methods_read', { id: addressHash }) + '?is_custom_abi=false';
const CONTRACT_QUERY_METHOD_API_URL = buildApiUrl('contract_method_query', { id: addressHash });
const CONTRACT_READ_METHODS_API_URL = buildApiUrl('contract_methods_read', { hash: addressHash }) + '?is_custom_abi=false';
const CONTRACT_QUERY_METHOD_API_URL = buildApiUrl('contract_method_query', { hash: addressHash }) + '?is_custom_abi=false';
const hooksConfig = {
router: {
query: { id: addressHash },
query: { hash: addressHash },
},
};
......
......@@ -7,6 +7,7 @@ import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'type
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -28,21 +29,25 @@ const ContractRead = ({ isProxy, isCustomAbi }: Props) => {
const apiFetch = useApiFetch();
const { address: userAddress } = useAccount();
const addressHash = router.query.id?.toString();
const addressHash = getQueryParamString(router.query.hash);
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false',
from: userAddress,
},
queryOptions: {
enabled: Boolean(router.query.id),
enabled: Boolean(addressHash),
},
});
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array<string | Array<string>>) => {
return apiFetch<'contract_method_query', SmartContractQueryMethodRead>('contract_method_query', {
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false',
},
fetchParams: {
method: 'POST',
body: {
......@@ -53,11 +58,11 @@ const ContractRead = ({ isProxy, isCustomAbi }: Props) => {
},
},
});
}, [ addressHash, apiFetch, isProxy, userAddress ]);
}, [ addressHash, apiFetch, isCustomAbi, isProxy, userAddress ]);
const renderContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => {
if (item.error) {
return <Alert status="error" fontSize="sm">{ item.error }</Alert>;
return <Alert status="error" fontSize="sm" wordBreak="break-word">{ item.error }</Alert>;
}
if (item.outputs.some(({ value }) => value)) {
......
......@@ -18,12 +18,12 @@ const ContractReadResult = ({ item, result, onSettle }: Props) => {
}, [ onSettle ]);
if ('status' in result) {
return <Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm">{ result.statusText }</Alert>;
return <Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-word">{ result.statusText }</Alert>;
}
if (result.is_error) {
const message = 'error' in result.result ? result.result.error : result.result.message;
return <Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm">{ message }</Alert>;
return <Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-word">{ message }</Alert>;
}
return (
......
import { Box, chakra, Flex, Text, Tooltip } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SmartContract } from 'types/api/contract';
import link from 'lib/link/link';
import CodeEditor from 'ui/shared/CodeEditor';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -28,7 +28,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
const diagramLink = hasSol2Yml && address ? (
<Tooltip label="Visualize contract code using Sol2Uml JS library">
<LinkInternal
href={ link('visualize_sol2uml', undefined, { address }) }
href={ route({ pathname: '/visualize/sol2uml', query: { address } }) }
ml="auto"
mr={ 3 }
>
......
......@@ -8,10 +8,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import ContractWrite from './ContractWrite';
const addressHash = 'hash';
const CONTRACT_WRITE_METHODS_API_URL = buildApiUrl('contract_methods_write', { id: addressHash }) + '?is_custom_abi=false';
const CONTRACT_WRITE_METHODS_API_URL = buildApiUrl('contract_methods_write', { hash: addressHash }) + '?is_custom_abi=false';
const hooksConfig = {
router: {
query: { id: addressHash },
query: { hash: addressHash },
},
};
......
......@@ -7,6 +7,7 @@ import type { SmartContractWriteMethod } from 'types/api/contract';
import config from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -27,12 +28,12 @@ interface Props {
const ContractWrite = ({ isProxy, isCustomAbi }: Props) => {
const router = useRouter();
const addressHash = router.query.id?.toString();
const addressHash = getQueryParamString(router.query.hash);
const { data: signer } = useSigner();
const { isConnected } = useAccount();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
queryParams: {
is_custom_abi: isCustomAbi ? 'true' : 'false',
},
......@@ -41,8 +42,18 @@ const ContractWrite = ({ isProxy, isCustomAbi }: Props) => {
},
});
const { contract, proxy } = useContractContext();
const _contract = isProxy ? proxy : contract;
const { contract, proxy, custom } = useContractContext();
const _contract = (() => {
if (isProxy) {
return proxy;
}
if (isCustomAbi) {
return custom;
}
return contract;
})();
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<string>>) => {
if (!isConnected) {
......@@ -51,7 +62,7 @@ const ContractWrite = ({ isProxy, isCustomAbi }: Props) => {
try {
if (!_contract) {
return;
throw new Error('Something went wrong. Try again later.');
}
if (item.type === 'receive') {
......
import { Box, chakra, Spinner } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { ContractMethodWriteResult } from './types';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
......@@ -30,9 +30,9 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => {
const isErrorResult = 'message' in result;
const txLink = (
<LinkInternal href={ link('tx', { id: txHash }) }>View transaction details</LinkInternal>
);
const txLink = txHash ? (
<LinkInternal href={ route({ pathname: '/tx/[hash]', query: { hash: txHash } }) }>View transaction details</LinkInternal>
) : null;
const content = (() => {
if (isErrorResult) {
......
......@@ -15,11 +15,13 @@ type ProviderProps = {
type TContractContext = {
contract: Contract | null;
proxy: Contract | null;
custom: Contract | null;
};
const ContractContext = React.createContext<TContractContext>({
contract: null,
proxy: null,
custom: null,
});
export function ContractContextProvider({ children }: ProviderProps) {
......@@ -28,9 +30,9 @@ export function ContractContextProvider({ children }: ProviderProps) {
const { data: signer } = useSigner();
const queryClient = useQueryClient();
const addressHash = router.query.id?.toString();
const addressHash = router.query.hash?.toString();
const { data: contractInfo } = useApiQuery('contract', {
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash),
refetchOnMount: false,
......@@ -38,32 +40,47 @@ export function ContractContextProvider({ children }: ProviderProps) {
});
const addressInfo = queryClient.getQueryData<Address>(getResourceKey('address', {
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
}));
const { data: proxyInfo } = useApiQuery('contract', {
pathParams: { id: addressInfo?.implementation_address || '' },
pathParams: { hash: addressInfo?.implementation_address || '' },
queryOptions: {
enabled: Boolean(addressInfo?.implementation_address),
refetchOnMount: false,
},
});
const { data: customInfo } = useApiQuery('contract_methods_write', {
pathParams: { hash: addressHash },
queryParams: { is_custom_abi: 'true' },
queryOptions: {
enabled: Boolean(addressInfo?.has_custom_methods_write),
refetchOnMount: false,
},
});
const contract = useContract({
address: addressHash,
abi: contractInfo?.abi || undefined,
signerOrProvider: signer || provider,
abi: contractInfo?.abi ?? undefined,
signerOrProvider: signer ?? provider,
});
const proxy = useContract({
address: addressInfo?.implementation_address ?? undefined,
abi: proxyInfo?.abi ?? undefined,
signerOrProvider: signer ?? provider,
});
const custom = useContract({
address: addressHash,
abi: customInfo ?? undefined,
signerOrProvider: signer ?? provider,
});
const value = React.useMemo(() => ({
contract,
proxy,
}), [ contract, proxy ]);
custom,
}), [ contract, proxy, custom ]);
return (
<ContractContext.Provider value={ value }>
......
import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenInfo } from 'types/api/token';
import metamaskIcon from 'icons/metamask.svg';
import useToast from 'lib/hooks/useToast';
......
......@@ -26,7 +26,7 @@ const AddressBalance = ({ data }: Props) => {
}
setLastBlockNumber(blockNumber);
const queryKey = getResourceKey('address', { pathParams: { id: data.hash } });
const queryKey = getResourceKey('address', { pathParams: { hash: data.hash } });
queryClient.setQueryData(queryKey, (prevData: Address | undefined) => {
if (!prevData) {
return;
......@@ -66,7 +66,7 @@ const AddressBalance = ({ data }: Props) => {
return (
<DetailsInfoItem
title="Balance"
hint={ `Address balance in ${ appConfig.network.currency.symbol }. Doesn't include ERC20, ERC721 and ERC1155 tokens.` }
hint={ `Address balance in ${ appConfig.network.currency.symbol }. Doesn't include ERC20, ERC721 and ERC1155 tokens` }
flexWrap="nowrap"
alignItems="flex-start"
>
......
import { Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import type { AddressCounters } from 'types/api/address';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
......@@ -42,7 +42,7 @@ const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
return <span>0</span>;
}
return (
<LinkInternal href={ link('address_index', { id: address }, { tab: PROP_TO_TAB[prop] }) } onClick={ onClick }>
<LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: address, tab: PROP_TO_TAB[prop] } }) } onClick={ onClick }>
{ Number(data).toLocaleString() }
</LinkInternal>
);
......
......@@ -41,10 +41,10 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
}, [ addModalProps, deleteModalProps, isAdded, isAuth, loginUrl ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => {
const queryKey = getResourceKey('address', { pathParams: { id: router.query.id?.toString() } });
const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } });
await queryClient.refetchQueries({ queryKey });
addModalProps.onClose();
}, [ addModalProps, queryClient, router.query.id ]);
}, [ addModalProps, queryClient, router.query.hash ]);
const handleAddModalClose = React.useCallback(() => {
addModalProps.onClose();
......
import { route } from 'nextjs-routes';
import React from 'react';
import type { Address } from 'types/api/address';
import link from 'lib/link/link';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -19,7 +19,7 @@ const AddressNameInfo = ({ data }: Props) => {
title="Token name"
hint="Token name and symbol"
>
<LinkInternal href={ link('token_index', { hash: data.token.address }) }>
<LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }>
{ data.token.name }{ symbol }
</LinkInternal>
</DetailsInfoItem>
......
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
......@@ -7,7 +8,6 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -50,7 +50,7 @@ const TxInternalsListItem = ({
</Flex>
<HStack spacing={ 1 }>
<Text fontSize="sm" fontWeight={ 500 }>Block</Text>
<LinkInternal href={ link('block', { id: block.toString() }) }>{ block }</LinkInternal>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: block.toString() } }) }>{ block }</LinkInternal>
</HStack>
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
......
import { Tr, Td, Tag, Icon, Box, Flex, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
......@@ -7,7 +8,6 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -58,7 +58,7 @@ const AddressIntTxsTableItem = ({
</Flex>
</Td>
<Td verticalAlign="middle">
<LinkInternal href={ link('block', { id: block.toString() }) }>{ block }</LinkInternal>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: block.toString() } }) }>{ block }</LinkInternal>
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
......
......@@ -7,8 +7,8 @@ const MockAddressPage = ({ children }: { children: JSX.Element }): JSX.Element =
const router = useRouter();
const { data } = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
pathParams: { hash: router.query.hash?.toString() },
queryOptions: { enabled: Boolean(router.query.hash) },
});
if (!data) {
......
......@@ -3,7 +3,7 @@ import { test as base, expect, devices } from '@playwright/experimental-ct-react
import React from 'react';
import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokenBalanceMock from 'mocks/address/tokenBalance';
import * as tokensMock from 'mocks/address/tokens';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
......@@ -12,11 +12,13 @@ import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
import TokenSelect from './TokenSelect';
const ASSET_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0xb2a90505dc6680a7a695f7975d0d32EeF610f456/logo.png';
const TOKENS_API_URL = buildApiUrl('address_token_balances', { id: '1' });
const ADDRESS_API_URL = buildApiUrl('address', { id: '1' });
const TOKENS_ERC20_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-20';
const TOKENS_ERC721_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-721';
const TOKENS_ER1155_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-1155';
const ADDRESS_API_URL = buildApiUrl('address', { hash: '1' });
const hooksConfig = {
router: {
query: { id: '1' },
query: { hash: '1' },
},
};
const CLIPPING_AREA = { x: 0, y: 0, width: 360, height: 500 };
......@@ -33,9 +35,17 @@ const test = base.extend({
status: 200,
body: JSON.stringify({ hash: '1' }),
}), { times: 1 });
await page.route(TOKENS_API_URL, async(route) => route.fulfill({
await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenBalanceMock.baseList),
body: JSON.stringify(tokensMock.erc20List),
}), { times: 1 });
await page.route(TOKENS_ERC721_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc721List),
}), { times: 1 });
await page.route(TOKENS_ER1155_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc1155List),
}), { times: 1 });
use(page);
......@@ -137,9 +147,17 @@ base('long values', async({ mount, page }) => {
status: 200,
body: JSON.stringify({ hash: '1' }),
}), { times: 1 });
await page.route(TOKENS_API_URL, async(route) => route.fulfill({
await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokensMock.erc20LongSymbol ] }),
}), { times: 1 });
await page.route(TOKENS_ERC721_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokensMock.erc721LongSymbol ] }),
}), { times: 1 });
await page.route(TOKENS_ER1155_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenBalanceMock.longValuesList),
body: JSON.stringify({ items: [ tokensMock.erc1155LongId ] }),
}), { times: 1 });
await mount(
......@@ -175,13 +193,15 @@ test.describe('socket', () => {
{ hooksConfig },
);
await page.route(TOKENS_API_URL, (route) => route.fulfill({
await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify([
...tokenBalanceMock.baseList,
tokenBalanceMock.erc20d,
]),
}));
body: JSON.stringify({
items: [
...tokensMock.erc20List.items,
tokensMock.erc20d,
],
}),
}), { times: 1 });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1');
......@@ -206,13 +226,15 @@ test.describe('socket', () => {
{ hooksConfig },
);
await page.route(TOKENS_API_URL, (route) => route.fulfill({
await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify([
...tokenBalanceMock.baseList,
tokenBalanceMock.erc20d,
]),
}));
body: JSON.stringify({
items: [
...tokensMock.erc20List.items,
tokensMock.erc20d,
],
}),
}), { times: 1 });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1');
......
import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQueryClient, useIsFetching } from '@tanstack/react-query';
import _sumBy from 'lodash/sumBy';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -8,12 +9,13 @@ import type { SocketMessage } from 'lib/socket/types';
import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import useFetchTokens from '../utils/useFetchTokens';
import TokenSelectDesktop from './TokenSelectDesktop';
import TokenSelectMobile from './TokenSelectMobile';
......@@ -27,17 +29,14 @@ const TokenSelect = ({ onClick }: Props) => {
const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>();
const addressHash = router.query.id?.toString();
const addressResourceKey = getResourceKey('address', { pathParams: { id: addressHash } });
const addressHash = getQueryParamString(router.query.hash);
const addressResourceKey = getResourceKey('address', { pathParams: { hash: addressHash } });
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
const { data, isError, isLoading, refetch } = useApiQuery('address_token_balances', {
pathParams: { id: addressQueryData?.hash },
queryOptions: { enabled: Boolean(addressQueryData) },
});
const balancesResourceKey = getResourceKey('address_token_balances', { pathParams: { id: addressQueryData?.hash } });
const balancesIsFetching = useIsFetching({ queryKey: balancesResourceKey });
const { data, isError, isLoading, refetch } = useFetchTokens({ hash: addressQueryData?.hash });
const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } });
const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey });
const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => {
if (payload.block_number !== blockNumber) {
......@@ -71,19 +70,20 @@ const TokenSelect = ({ onClick }: Props) => {
return <Skeleton h={ 8 } w="160px"/>;
}
if (isError || data.length === 0) {
const hasTokens = _sumBy(Object.values(data), ({ items }) => items.length) > 0;
if (isError || !hasTokens) {
return <Box py="6px">0</Box>;
}
return (
<Flex columnGap={ 3 } mt={{ base: '6px', lg: 0 }}>
{ isMobile ?
<TokenSelectMobile data={ data } isLoading={ balancesIsFetching === 1 }/> :
<TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/>
<TokenSelectMobile data={ data } isLoading={ tokensIsFetching === 1 }/> :
<TokenSelectDesktop data={ data } isLoading={ tokensIsFetching === 1 }/>
}
<Tooltip label="Show all tokens">
<Box>
<NextLink href={ link('address_index', { id: addressHash }, { tab: 'tokens' }) } passHref>
<NextLink href={{ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'tokens' } }} passHref>
<IconButton
aria-label="Show all tokens"
variant="outline"
......
import { Box, Button, Icon, Skeleton, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { FormattedData } from './types';
import arrowIcon from 'icons/arrows/east-mini.svg';
import tokensIcon from 'icons/tokens.svg';
import type { EnhancedData } from '../utils/tokenUtils';
import { getTokenBalanceTotal } from '../utils/tokenUtils';
import { getTokensTotalInfo } from '../utils/tokenUtils';
interface Props {
isOpen: boolean;
isLoading: boolean;
onClick: () => void;
data: Array<EnhancedData>;
data: FormattedData;
}
const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const totalBn = getTokenBalanceTotal(data);
const { usd, num, isOverflow } = getTokensTotalInfo(data);
const skeletonBgColor = useColorModeValue('white', 'black');
const prefix = isOverflow ? '>' : '';
const handleClick = React.useCallback(() => {
if (isLoading && !isOpen) {
return;
......@@ -36,8 +39,8 @@ const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: Rea
aria-label="Token select"
>
<Icon as={ tokensIcon } boxSize={ 4 } mr={ 2 }/>
<Text fontWeight={ 600 }>{ data.length }</Text>
<Text whiteSpace="pre" variant="secondary" fontWeight={ 400 }> (${ totalBn.toFormat(2) })</Text>
<Text fontWeight={ 600 }>{ prefix }{ num }</Text>
<Text whiteSpace="pre" variant="secondary" fontWeight={ 400 }> ({ prefix }${ usd.toFormat(2) })</Text>
<Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 3 }/>
</Button>
{ isLoading && !isOpen && <Skeleton h="100%" w="100%" position="absolute" top={ 0 } left={ 0 } bgColor={ skeletonBgColor }/> }
......
import { useColorModeValue, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import type { FormattedData } from './types';
import TokenSelectButton from './TokenSelectButton';
import TokenSelectMenu from './TokenSelectMenu';
import useTokenSelect from './useTokenSelect';
interface Props {
data: Array<AddressTokenBalance>;
data: FormattedData;
isLoading: boolean;
}
......@@ -22,7 +22,7 @@ const TokenSelectDesktop = ({ data, isLoading }: Props) => {
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.modifiedData } isLoading={ isLoading }/>
<TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.data } isLoading={ isLoading }/>
</PopoverTrigger>
<PopoverContent w="355px" maxH="450px" overflowY="scroll">
<PopoverBody px={ 4 } py={ 6 } bgColor={ bgColor } boxShadow="2xl" >
......
import { chakra, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import link from 'lib/link/link';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import HashStringShorten from 'ui/shared/HashStringShorten';
import TokenLogo from 'ui/shared/TokenLogo';
import type { EnhancedData } from '../utils/tokenUtils';
import type { TokenEnhancedData } from '../utils/tokenUtils';
interface Props {
data: EnhancedData;
data: TokenEnhancedData;
}
const TokenSelectItem = ({ data }: Props) => {
......@@ -45,7 +45,7 @@ const TokenSelectItem = ({ data }: Props) => {
})();
// TODO add filter param when token page is ready
const url = link('token_index', { hash: data.token.address });
const url = route({ pathname: '/token/[hash]', query: { hash: data.token.address } });
return (
<Flex
......
import { Icon, Text, Box, Input, InputGroup, InputLeftElement, useColorModeValue, Flex, Link } from '@chakra-ui/react';
import type { Dictionary } from 'lodash';
import _sumBy from 'lodash/sumBy';
import type { ChangeEvent } from 'react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import type { FormattedData } from './types';
import type { TokenType } from 'types/api/token';
import arrowIcon from 'icons/arrows/east.svg';
import searchIcon from 'icons/search.svg';
import type { Sort, EnhancedData } from '../utils/tokenUtils';
import type { Sort } from '../utils/tokenUtils';
import { sortTokenGroups, sortingFns } from '../utils/tokenUtils';
import TokenSelectItem from './TokenSelectItem';
......@@ -16,16 +17,17 @@ interface Props {
searchTerm: string;
erc20sort: Sort;
erc1155sort: Sort;
modifiedData: Array<EnhancedData>;
groupedData: Dictionary<Array<EnhancedData>>;
filteredData: FormattedData;
onInputChange: (event: ChangeEvent<HTMLInputElement>) => void;
onSortClick: (event: React.SyntheticEvent) => void;
}
const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, onInputChange, onSortClick, searchTerm }: Props) => {
const TokenSelectMenu = ({ erc20sort, erc1155sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
const hasFilteredResult = _sumBy(Object.values(filteredData), ({ items }) => items.length) > 0;
return (
<>
<InputGroup size="xs" mb={ 5 }>
......@@ -41,7 +43,12 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, on
/>
</InputGroup>
<Flex flexDir="column" rowGap={ 6 }>
{ Object.entries(groupedData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => {
{ Object.entries(filteredData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => {
if (tokenInfo.items.length === 0) {
return null;
}
const type = tokenType as TokenType;
const arrowTransform = (type === 'ERC-1155' && erc1155sort === 'desc') || (type === 'ERC-20' && erc20sort === 'desc') ?
'rotate(90deg)' :
......@@ -56,24 +63,26 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, on
return 'desc';
}
})();
const hasSort = type === 'ERC-1155' || (type === 'ERC-20' && tokenInfo.some(({ usd }) => usd));
const hasSort = type === 'ERC-1155' || (type === 'ERC-20' && tokenInfo.items.some(({ usd }) => usd));
const numPrefix = tokenInfo.isOverflow ? '>' : '';
return (
<Box key={ type }>
<Flex justifyContent="space-between">
<Text mb={ 3 } color="gray.500" fontWeight={ 600 } fontSize="sm">{ type } tokens ({ tokenInfo.length })</Text>
<Text mb={ 3 } color="gray.500" fontWeight={ 600 } fontSize="sm">{ type } tokens ({ numPrefix }{ tokenInfo.items.length })</Text>
{ hasSort && (
<Link data-type={ type } onClick={ onSortClick } aria-label={ `Sort ${ type } tokens` }>
<Icon as={ arrowIcon } boxSize={ 5 } transform={ arrowTransform } transitionDuration="faster"/>
</Link>
) }
</Flex>
{ tokenInfo.sort(sortingFns[type](sortDirection)).map((data) => <TokenSelectItem key={ data.token.address + data.token_id } data={ data }/>) }
{ tokenInfo.items.sort(sortingFns[type](sortDirection)).map((data) =>
<TokenSelectItem key={ data.token.address + data.token_id } data={ data }/>) }
</Box>
);
}) }
</Flex>
{ modifiedData.length === 0 && searchTerm && <Text fontSize="sm">Could not find any matches.</Text> }
{ Boolean(searchTerm) && !hasFilteredResult && <Text fontSize="sm">Could not find any matches.</Text> }
</>
);
};
......
import { useDisclosure, Modal, ModalContent, ModalCloseButton } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import type { FormattedData } from './types';
import TokenSelectButton from './TokenSelectButton';
import TokenSelectMenu from './TokenSelectMenu';
import useTokenSelect from './useTokenSelect';
interface Props {
data: Array<AddressTokenBalance>;
data: FormattedData;
isLoading: boolean;
}
......@@ -18,7 +18,7 @@ const TokenSelectMobile = ({ data, isLoading }: Props) => {
return (
<>
<TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.modifiedData } isLoading={ isLoading }/>
<TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.data } isLoading={ isLoading }/>
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent>
<ModalCloseButton/>
......
import type { TokenType } from 'types/api/token';
import type { TokenEnhancedData } from 'ui/address/utils/tokenUtils';
export type FormattedData = Record<TokenType, FormattedDataItem>;
export interface FormattedDataItem {
items: Array<TokenEnhancedData>;
isOverflow: boolean;
}
import _groupBy from 'lodash/groupBy';
import _mapValues from 'lodash/mapValues';
import type { ChangeEvent } from 'react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import type { FormattedData } from './types';
import type { Sort } from '../utils/tokenUtils';
import { calculateUsdValue, filterTokens } from '../utils/tokenUtils';
import { filterTokens } from '../utils/tokenUtils';
export default function useData(data: Array<AddressTokenBalance>) {
export default function useTokenSelect(data: FormattedData) {
const [ searchTerm, setSearchTerm ] = React.useState('');
const [ erc1155sort, setErc1155Sort ] = React.useState<Sort>('desc');
const [ erc20sort, setErc20Sort ] = React.useState<Sort>('desc');
......@@ -26,12 +26,12 @@ export default function useData(data: Array<AddressTokenBalance>) {
}
}, []);
const modifiedData = React.useMemo(() => {
return data.filter(filterTokens(searchTerm.toLowerCase())).map(calculateUsdValue);
const filteredData = React.useMemo(() => {
return _mapValues(data, ({ items, isOverflow }) => ({
isOverflow,
items: items.filter(filterTokens(searchTerm.toLowerCase())),
}));
}, [ data, searchTerm ]);
const groupedData = React.useMemo(() => {
return _groupBy(modifiedData, 'token.type');
}, [ modifiedData ]);
return {
searchTerm,
......@@ -39,7 +39,7 @@ export default function useData(data: Array<AddressTokenBalance>) {
erc1155sort,
onInputChange,
onSortClick,
modifiedData,
groupedData,
data,
filteredData,
};
}
import { Center, Flex, Icon, Link, Text, LinkBox, LinkOverlay, useColorModeValue } from '@chakra-ui/react';
import { Flex, Link, Text, LinkBox, LinkOverlay, useColorModeValue } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import NFTIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import NftImage from 'ui/shared/nft/NftImage';
import TokenLogo from 'ui/shared/TokenLogo';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = AddressTokenBalance;
const NFTItem = ({ token, token_id: tokenId }: Props) => {
const tokenLink = link('token_index', { hash: token.address });
const tokenLink = route({ pathname: '/token/[hash]', query: { hash: token.address } });
return (
<LinkBox
w={{ base: 'calc((100% - 12px)/2)', lg: '210px' }}
h={{ base: 'auto', lg: '272px' }}
w={{ base: '100%', lg: '210px' }}
border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px"
......@@ -26,16 +25,14 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => {
fontWeight={ 500 }
lineHeight="20px"
>
<LinkOverlay href={ tokenLink }/>
<Center
w={{ base: '100%', lg: '182px' }}
h={{ base: 'calc((100vw - 36px)/2 - 12px)', lg: '182px' }}
bg={ useColorModeValue('blackAlpha.50', 'whiteAlpha.50') }
mb="18px"
borderRadius="12px"
>
<Icon as={ NFTIcon } boxSize="112px" color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') }/>
</Center>
<LinkOverlay href={ tokenLink }>
<NftImage
mb="18px"
url={ null }
fallbackPadding="30px"
cursor="pointer"
/>
</LinkOverlay>
{ tokenId && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text>
......@@ -44,7 +41,7 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => {
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
href={ link('token_instance_item', { hash: token.address, id: tokenId }) }
href={ route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) }
>
{ tokenId }
</Link>
......
import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import { ZERO } from 'lib/consts';
import getCurrencyValue from 'lib/getCurrencyValue';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import { getTokenBalanceTotal, calculateUsdValue } from '../utils/tokenUtils';
import { getTokensTotalInfo } from '../utils/tokenUtils';
import useFetchTokens from '../utils/useFetchTokens';
import TokenBalancesItem from './TokenBalancesItem';
const TokenBalances = () => {
const router = useRouter();
const hash = router.query.hash?.toString();
const addressQuery = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
pathParams: { hash },
queryOptions: { enabled: Boolean(hash) },
});
const balancesQuery = useApiQuery('address_token_balances', {
pathParams: { id: addressQuery.data?.hash },
queryOptions: { enabled: Boolean(addressQuery.data) },
});
const tokenQuery = useFetchTokens({ hash });
if (addressQuery.isError || balancesQuery.isError) {
if (addressQuery.isError || tokenQuery.isError) {
return <DataFetchAlert/>;
}
if (addressQuery.isLoading || balancesQuery.isLoading) {
if (addressQuery.isLoading || tokenQuery.isLoading) {
const item = <Skeleton w={{ base: '100%', lg: '240px' }} h="82px" borderRadius="16px"/>;
return (
<Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}>
......@@ -40,7 +40,7 @@ const TokenBalances = () => {
}
const addressData = addressQuery.data;
const { valueStr: nativeValue, usd: nativeUsd } = getCurrencyValue({
const { valueStr: nativeValue, usdBn: nativeUsd } = getCurrencyValue({
value: addressData.coin_balance || '0',
accuracy: 8,
accuracyUsd: 2,
......@@ -48,22 +48,25 @@ const TokenBalances = () => {
decimals: String(appConfig.network.currency.decimals),
});
const tokenBalanceBn = getTokenBalanceTotal(balancesQuery.data.map(calculateUsdValue)).toFixed(2);
const totalUsd = nativeUsd ? BigNumber(nativeUsd).toNumber() + BigNumber(tokenBalanceBn).toNumber() : undefined;
const tokensInfo = getTokensTotalInfo(tokenQuery.data);
const prefix = tokensInfo.isOverflow ? '>' : '';
const totalUsd = nativeUsd.plus(tokensInfo.usd);
const tokensNumText = tokensInfo.num > 0 ?
` | ${ prefix }${ tokensInfo.num } ${ tokensInfo.num > 1 ? 'tokens' : 'token' }` :
'';
return (
<Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}>
<TokenBalancesItem name="Net Worth" value={ totalUsd ? `$${ totalUsd } USD` : 'N/A' }/>
<TokenBalancesItem name="Net Worth" value={ addressData.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) } USD` : 'N/A' }/>
<TokenBalancesItem
name={ `${ appConfig.network.currency.symbol } Balance` }
value={ (nativeUsd ? `$${ nativeUsd } USD | ` : '') + `${ nativeValue } ${ appConfig.network.currency.symbol }` }
value={ (!nativeUsd.eq(ZERO) ? `$${ nativeUsd.toFormat(2) } USD | ` : '') + `${ nativeValue } ${ appConfig.network.currency.symbol }` }
/>
<TokenBalancesItem
name="Tokens"
value={
`$${ tokenBalanceBn } USD ` +
(balancesQuery.data.length ? ` | ${ balancesQuery.data.length } ${ balancesQuery.data.length === 1 ? 'token' : 'tokens' }` : '')
`${ prefix }$${ tokensInfo.usd.toFormat(2) } USD ` +
tokensNumText
}
/>
</Flex>
......
import { Flex, Skeleton, Text } from '@chakra-ui/react';
import { Grid, Skeleton, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -38,11 +38,18 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
return (
<>
{ bar }
<Flex columnGap={ 6 } rowGap={ 6 } flexWrap="wrap">
<Skeleton w="210px" h="272px"/>
<Skeleton w="210px" h="272px"/>
<Skeleton w="210px" h="272px"/>
</Flex>
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
</>
);
}
......@@ -54,9 +61,14 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
return (
<>
{ bar }
<Flex columnGap={{ base: 3, lg: 6 }} rowGap={{ base: 3, lg: 6 }} flexWrap="wrap">
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) }
</Flex>
</Grid>
</>
);
};
......
import BigNumber from 'bignumber.js';
import fpAdd from 'lodash/fp/add';
import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenType } from 'types/api/token';
import sumBnReducer from 'lib/bigint/sumBnReducer';
import { ZERO } from 'lib/consts';
export type EnhancedData = AddressTokenBalance & {
export type TokenEnhancedData = AddressTokenBalance & {
usd?: BigNumber ;
}
export type Sort = 'desc' | 'asc';
export type TokenSelectData = Record<TokenType, TokenSelectDataItem>;
export interface TokenSelectDataItem {
items: Array<TokenEnhancedData>;
isOverflow: boolean;
}
type TokenGroup = [string, TokenSelectDataItem];
const TOKEN_GROUPS_ORDER: Array<TokenType> = [ 'ERC-20', 'ERC-721', 'ERC-1155' ];
type TokenGroup = [string, Array<AddressTokenBalance>];
export const sortTokenGroups = (groupA: TokenGroup, groupB: TokenGroup) => {
return TOKEN_GROUPS_ORDER.indexOf(groupA[0] as TokenType) > TOKEN_GROUPS_ORDER.indexOf(groupB[0] as TokenType) ? 1 : -1;
......@@ -27,7 +38,7 @@ const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: Ad
return Number(dataA.value) > Number(dataB.value) ? 1 : -1;
};
const sortErc20Tokens = (sort: Sort) => (dataA: EnhancedData, dataB: EnhancedData) => {
const sortErc20Tokens = (sort: Sort) => (dataA: TokenEnhancedData, dataB: TokenEnhancedData) => {
if (!dataA.usd && !dataB.usd) {
return 0;
}
......@@ -63,7 +74,7 @@ export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBala
return token.name?.toLowerCase().includes(searchTerm);
};
export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => {
export const calculateUsdValue = (data: AddressTokenBalance): TokenEnhancedData => {
if (data.token.type !== 'ERC-20') {
return data;
}
......@@ -80,6 +91,18 @@ export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => {
};
};
export const getTokenBalanceTotal = (data: Array<EnhancedData>) => {
return data.reduce((result, item) => !item.usd ? result : result.plus(BigNumber(item.usd)), ZERO);
export const getTokensTotalInfo = (data: TokenSelectData) => {
const usd = Object.values(data)
.map(({ items }) => items.reduce(usdValueReducer, ZERO))
.reduce(sumBnReducer, ZERO);
const num = Object.values(data)
.map(({ items }) => items.length)
.reduce(fpAdd, 0);
const isOverflow = Object.values(data).some(({ isOverflow }) => isOverflow);
return { usd, num, isOverflow };
};
const usdValueReducer = (result: BigNumber, item: TokenEnhancedData) => !item.usd ? result : result.plus(BigNumber(item.usd));
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { calculateUsdValue } from './tokenUtils';
interface Props {
hash?: string;
}
export default function useFetchTokens({ hash }: Props) {
const erc20query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-20' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const erc721query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-721' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const erc1155query = useApiQuery('address_tokens', {
pathParams: { hash },
queryParams: { type: 'ERC-1155' },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const refetch = React.useCallback(() => {
erc20query.refetch();
erc721query.refetch();
erc1155query.refetch();
}, [ erc1155query, erc20query, erc721query ]);
const data = React.useMemo(() => {
return {
'ERC-20': {
items: erc20query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc20query.data?.next_page_params),
},
'ERC-721': {
items: erc721query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc721query.data?.next_page_params),
},
'ERC-1155': {
items: erc1155query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc1155query.data?.next_page_params),
},
};
}, [ erc1155query.data, erc20query.data, erc721query.data ]);
return {
isLoading: erc20query.isLoading || erc721query.isLoading || erc1155query.isLoading,
isError: erc20query.isError || erc721query.isError || erc1155query.isError,
data,
refetch,
};
}
......@@ -2,8 +2,6 @@ import { LinkOverlay } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import link from 'lib/link/link';
type Props = {
id: string;
url: string;
......@@ -17,7 +15,7 @@ const AppLink = ({ url, external, id, title }: Props) => {
{ title }
</LinkOverlay>
) : (
<NextLink href={ link('app_index', { id: id }) } passHref>
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref>
<LinkOverlay>
{ title }
</LinkOverlay>
......
......@@ -2,8 +2,6 @@ import { Button } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import link from 'lib/link/link';
type Props = {
id: string;
url: string;
......@@ -29,7 +27,7 @@ const AppModalLink = ({ url, external, id }: Props) => {
{ ...buttonProps }
>Launch app</Button>
) : (
<NextLink href={ link('app_index', { id: id }) } passHref>
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref>
<Button
as="a"
{ ...buttonProps }
......
......@@ -7,10 +7,10 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import BlockDetails from './BlockDetails';
const API_URL = buildApiUrl('block', { id: '1' });
const API_URL = buildApiUrl('block', { height: '1' });
const hooksConfig = {
router: {
query: { id: '1' },
query: { height: '1' },
},
};
......
......@@ -2,6 +2,7 @@ import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React from 'react';
import { scroller, Element } from 'react-scroll';
......@@ -13,8 +14,8 @@ import getBlockReward from 'lib/block/getBlockReward';
import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString';
import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
......@@ -30,10 +31,11 @@ import Utilization from 'ui/shared/Utilization/Utilization';
const BlockDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter();
const height = getQueryParamString(router.query.height);
const { data, isLoading, isError, error } = useApiQuery('block', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
pathParams: { height },
queryOptions: { enabled: Boolean(height) },
});
const handleCutClick = React.useCallback(() => {
......@@ -46,11 +48,10 @@ const BlockDetails = () => {
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
const increment = direction === 'next' ? +1 : -1;
const nextId = String(Number(router.query.id) + increment);
const nextId = String(Number(height) + increment);
const url = link('block', { id: nextId });
router.push(url, undefined);
}, [ router ]);
router.push({ pathname: '/block/[height]', query: { height: nextId } }, undefined);
}, [ height, router ]);
if (isLoading) {
return <BlockDetailsSkeleton/>;
......@@ -85,7 +86,7 @@ const BlockDetails = () => {
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
<DetailsInfoItem
title="Block height"
hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain."
hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain"
>
{ data.height }
{ data.height === 0 && <Text whiteSpace="pre"> - Genesis Block</Text> }
......@@ -99,7 +100,7 @@ const BlockDetails = () => {
</DetailsInfoItem>
<DetailsInfoItem
title="Size"
hint="Size of the block in bytes."
hint="Size of the block in bytes"
>
{ data.size.toLocaleString('en') }
</DetailsInfoItem>
......@@ -114,15 +115,15 @@ const BlockDetails = () => {
</DetailsInfoItem>
<DetailsInfoItem
title="Transactions"
hint="The number of transactions in the block."
hint="The number of transactions in the block"
>
<LinkInternal href={ link('block', { id: router.query.id }, { tab: 'txs' }) }>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height, tab: 'txs' } }) }>
{ data.tx_count } transactions
</LinkInternal>
</DetailsInfoItem>
<DetailsInfoItem
title={ appConfig.network.verificationType === 'validation' ? 'Validated by' : 'Mined by' }
hint="A block producer who successfully included the block onto the blockchain."
hint="A block producer who successfully included the block onto the blockchain"
columnGap={ 1 }
>
<AddressLink type="address" hash={ data.miner.hash }/>
......@@ -135,7 +136,7 @@ const BlockDetails = () => {
title="Block reward"
hint={
`For each block, the ${ validatorTitle } is rewarded with a finite amount of ${ appConfig.network.currency.symbol || 'native token' }
on top of the fees paid for all transactions in the block.`
on top of the fees paid for all transactions in the block`
}
columnGap={ 1 }
>
......@@ -172,7 +173,7 @@ const BlockDetails = () => {
key={ type }
title={ type }
// is this text correct for validators?
hint={ `Amount of distributed reward. ${ capitalize(validatorTitle) }s receive a static block reward + Tx fees + uncle fees.` }
hint={ `Amount of distributed reward. ${ capitalize(validatorTitle) }s receive a static block reward + Tx fees + uncle fees` }
>
{ BigNumber(reward).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
</DetailsInfoItem>
......@@ -183,7 +184,7 @@ const BlockDetails = () => {
<DetailsInfoItem
title="Gas used"
hint="The total gas amount used in the block and its percentage of gas filled in the block."
hint="The total gas amount used in the block and its percentage of gas filled in the block"
>
<Text>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
<Utilization
......@@ -196,14 +197,14 @@ const BlockDetails = () => {
</DetailsInfoItem>
<DetailsInfoItem
title="Gas limit"
hint="Total gas limit provided by all transactions in the block."
hint="Total gas limit provided by all transactions in the block"
>
<Text>{ BigNumber(data.gas_limit).toFormat() }</Text>
</DetailsInfoItem>
{ data.base_fee_per_gas && (
<DetailsInfoItem
title="Base fee per gas"
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion."
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion"
>
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol } </Text>
<Text variant="secondary" whiteSpace="pre">
......@@ -216,7 +217,7 @@ const BlockDetails = () => {
hint={
`Amount of ${ appConfig.network.currency.symbol || 'native token' } burned from transactions included in the block.
Equals Block Base Fee per Gas * Gas Used.`
Equals Block Base Fee per Gas * Gas Used`
}
>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
......@@ -235,7 +236,7 @@ const BlockDetails = () => {
{ data.priority_fee !== null && BigNumber(data.priority_fee).gt(ZERO) && (
<DetailsInfoItem
title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion."
hint="User-defined tips sent to validator for transaction priority/inclusion"
>
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
</DetailsInfoItem>
......@@ -243,7 +244,7 @@ const BlockDetails = () => {
{ /* api doesn't support extra data yet */ }
{ /* <DetailsInfoItem
title="Extra data"
hint={ `Any data that can be included by the ${ validatorTitle } in the block.` }
hint={ `Any data that can be included by the ${ validatorTitle } in the block` }
>
<Text whiteSpace="pre">{ data.extra_data } </Text>
<Text variant="secondary">(Hex: { data.extra_data })</Text>
......@@ -272,7 +273,7 @@ const BlockDetails = () => {
<DetailsInfoItem
title="Difficulty"
hint={ `Block difficulty for ${ validatorTitle }, used to calibrate block generation time.` }
hint={ `Block difficulty for ${ validatorTitle }, used to calibrate block generation time` }
whiteSpace="normal"
wordBreak="break-all"
>
......@@ -280,7 +281,7 @@ const BlockDetails = () => {
</DetailsInfoItem>
<DetailsInfoItem
title="Total difficulty"
hint="Total difficulty of the chain until this block."
hint="Total difficulty of the chain until this block"
whiteSpace="normal"
wordBreak="break-all"
>
......@@ -291,7 +292,7 @@ const BlockDetails = () => {
<DetailsInfoItem
title="Hash"
hint="The SHA256 hash of the block."
hint="The SHA256 hash of the block"
flexWrap="nowrap"
>
<Box overflow="hidden">
......@@ -302,7 +303,7 @@ const BlockDetails = () => {
{ data.height > 0 && (
<DetailsInfoItem
title="Parent hash"
hint="The hash of the block from which this block was generated."
hint="The hash of the block from which this block was generated"
flexWrap="nowrap"
>
<AddressLink hash={ data.parent_hash } type="block" id={ String(data.height - 1) }/>
......@@ -312,13 +313,13 @@ const BlockDetails = () => {
{ /* api doesn't support state root yet */ }
{ /* <DetailsInfoItem
title="State root"
hint="The root of the state trie."
hint="The root of the state trie"
>
<Text wordBreak="break-all" whiteSpace="break-spaces">{ data.state_root }</Text>
</DetailsInfoItem> */ }
<DetailsInfoItem
title="Nonce"
hint="Block nonce is a value used during mining to demonstrate proof of work for a block."
hint="Block nonce is a value used during mining to demonstrate proof of work for a block"
>
{ data.nonce }
</DetailsInfoItem>
......
import { Flex, Spinner, Text, Box, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import { route } from 'nextjs-routes';
import React from 'react';
import type { Block } from 'types/api/block';
......@@ -9,7 +10,6 @@ import appConfig from 'configs/app/config';
import flameIcon from 'icons/flame.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts';
import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -36,7 +36,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
{ isPending && <Spinner size="sm"/> }
<LinkInternal
fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) }
href={ route({ pathname: '/block/[height]', query: { height: String(data.height) } }) }
>
{ data.height }
</LinkInternal>
......@@ -54,7 +54,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text>
{ data.tx_count > 0 ? (
<LinkInternal href={ link('block', { id: String(data.height) }, { tab: 'txs' }) }>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count }
</LinkInternal>
) :
......
import { Tr, Td, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { motion } from 'framer-motion';
import { route } from 'nextjs-routes';
import React from 'react';
import type { Block } from 'types/api/block';
......@@ -8,7 +9,6 @@ import type { Block } from 'types/api/block';
import flameIcon from 'icons/flame.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts';
import link from 'lib/link/link';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
......@@ -41,7 +41,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<LinkInternal
fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) }
href={ route({ pathname: '/block/[height]', query: { height: String(data.height) } }) }
>
{ data.height }
</LinkInternal>
......@@ -55,7 +55,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Td>
<Td isNumeric fontSize="sm">
{ data.tx_count > 0 ? (
<LinkInternal href={ link('block', { id: String(data.height) }, { tab: 'txs' }) }>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count }
</LinkInternal>
) : data.tx_count }
......
......@@ -10,7 +10,6 @@ import type { SmartContractVerificationMethod, SmartContractVerificationConfig }
import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast';
import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -55,7 +54,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
try {
await apiFetch('contract_verification_via', {
pathParams: { method: data.method.value, id: hash.toLowerCase() },
pathParams: { method: data.method.value, hash: hash.toLowerCase() },
fetchParams: {
method: 'POST',
body,
......@@ -86,7 +85,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
router.push(link('address_index', { id: hash }, { tab: 'contract' }), undefined, { shallow: true });
router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { shallow: true });
},
});
}, [ hash, router, setError, toast ]);
......
import { Box, Heading, Flex, Text, VStack, Skeleton } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { AnimatePresence } from 'framer-motion';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
......@@ -9,7 +10,6 @@ import type { Block } from 'types/api/block';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -98,7 +98,7 @@ const LatestBlocks = () => {
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ link('blocks') }>View all blocks</LinkInternal>
<LinkInternal fontSize="sm" href={ route({ pathname: '/blocks' }) }>View all blocks</LinkInternal>
</Flex>
</>
);
......
......@@ -8,13 +8,13 @@ import {
Text,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import { route } from 'nextjs-routes';
import React from 'react';
import type { Block } from 'types/api/block';
import blockIcon from 'icons/block.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import link from 'lib/link/link';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -44,7 +44,7 @@ const LatestBlocksItem = ({ block, h }: Props) => {
<HStack spacing={ 2 }>
<Icon as={ blockIcon } boxSize="30px" color="link"/>
<LinkInternal
href={ link('block', { id: String(block.height) }) }
href={ route({ pathname: '/block/[height]', query: { height: String(block.height) } }) }
fontSize="xl"
fontWeight="500"
>
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { ROUTES } from 'lib/link/routes';
import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer';
......@@ -42,7 +41,7 @@ test.describe('socket', () => {
const hooksConfig = {
router: {
pathname: ROUTES.network_index.pattern,
pathname: '/',
query: {},
},
};
......
import { Box, Heading, Flex, Text, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
......@@ -34,7 +34,7 @@ const LatestTransactions = () => {
}
if (data) {
const txsUrl = link('txs');
const txsUrl = route({ pathname: '/txs' });
content = (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/>
......
import { Grid } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import appConfig from 'configs/app/config';
......@@ -8,7 +9,6 @@ import gasIcon from 'icons/gas.svg';
import txIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg';
import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link';
import StatsGasPrices from './StatsGasPrices';
import StatsItem from './StatsItem';
......@@ -45,7 +45,7 @@ const Stats = () => {
icon={ blockIcon }
title="Total blocks"
value={ Number(data.total_blocks).toLocaleString() }
url={ link('blocks') }
url={ route({ pathname: '/blocks' }) }
/>
{ hasAvgBlockTime && (
<StatsItem
......@@ -58,7 +58,7 @@ const Stats = () => {
icon={ txIcon }
title="Total transactions"
value={ Number(data.total_transactions).toLocaleString() }
url={ link('txs') }
url={ route({ pathname: '/txs' }) }
/>
<StatsItem
icon={ walletIcon }
......
import { Box, Flex, Icon, Text, useColorModeValue, chakra, Tooltip, LightMode } from '@chakra-ui/react';
import type { TooltipProps } from '@chakra-ui/react';
import { Flex, Icon, Text, useColorModeValue, chakra, LightMode } from '@chakra-ui/react';
import React from 'react';
import infoIcon from 'icons/info.svg';
import breakpoints from 'theme/foundations/breakpoints';
import Hint from 'ui/shared/Hint';
type Props = {
icon: React.FC<React.SVGAttributes<SVGElement>>;
......@@ -15,6 +16,14 @@ type Props = {
const LARGEST_BREAKPOINT = '1240px';
const TOOLTIP_PROPS: Partial<TooltipProps> = {
hasArrow: false,
borderRadius: 'md',
placement: 'bottom-end',
offset: [ 0, 0 ],
bgColor: 'blackAlpha.900',
};
const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sxContainer = {} as any;
......@@ -55,20 +64,15 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props)
</Flex>
{ tooltipLabel && (
<LightMode>
<Tooltip label={ tooltipLabel } hasArrow={ false } borderRadius="12px" placement="bottom-end" offset={ [ 0, 0 ] } bgColor="blackAlpha.900">
<Box
position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
right="10px"
cursor="pointer"
>
<Icon
as={ infoIcon }
boxSize={ 6 }
color={ infoColor }
/>
</Box>
</Tooltip>
<Hint
label={ tooltipLabel }
tooltipProps={ TOOLTIP_PROPS }
boxSize={ 6 }
color={ infoColor }
position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
right="10px"
/>
</LightMode>
) }
</Flex>
......
import { Box, Flex, Icon, Skeleton, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { Flex, Skeleton, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import infoIcon from 'icons/info.svg';
import useApiQuery from 'lib/api/useApiQuery';
import Hint from 'ui/shared/Hint';
import ChainIndicatorChartContainer from './ChainIndicatorChartContainer';
import ChainIndicatorItem from './ChainIndicatorItem';
......@@ -72,13 +72,7 @@ const ChainIndicators = () => {
<Flex flexGrow={ 1 } flexDir="column" order={{ base: 2, lg: 1 }} p={{ base: 6, lg: 0 }}>
<Flex alignItems="center">
<Text fontWeight={ 500 } fontFamily="heading" fontSize="lg">{ indicator?.title }</Text>
{ indicator?.hint && (
<Tooltip label={ indicator.hint } maxW="300px">
<Box display="inline-flex" cursor="pointer" ml={ 1 }>
<Icon as={ infoIcon } boxSize={ 4 }/>
</Box>
</Tooltip>
) }
{ indicator?.hint && <Hint label={ indicator.hint } ml={ 1 }/> }
</Flex>
{ valueTitle }
<ChainIndicatorChartContainer { ...queryResult }/>
......
......@@ -2,13 +2,14 @@ import { Flex, Skeleton, Tag, Box, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenType } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import notEmpty from 'lib/notEmpty';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract';
......@@ -44,10 +45,11 @@ const AddressPageContent = () => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const hash = getQueryParamString(router.query.hash);
const addressQuery = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
pathParams: { hash },
queryOptions: { enabled: Boolean(hash) },
});
const tags = [
......
......@@ -6,6 +6,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import BlockDetails from 'ui/block/BlockDetails';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
......@@ -24,17 +25,19 @@ const BlockPageContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const appProps = useAppContext();
const height = getQueryParamString(router.query.height);
const tab = getQueryParamString(router.query.tab);
const blockTxsQuery = useQueryWithPages({
resourceName: 'block_txs',
pathParams: { id: router.query.id?.toString() },
pathParams: { height },
options: {
enabled: Boolean(router.query.id && router.query.tab === 'txs'),
enabled: Boolean(height && tab === 'txs'),
},
});
if (!router.query.id) {
return null;
if (!height) {
throw new Error('Block not found', { cause: { status: 404 } });
}
const tabs: Array<RoutedTab> = [
......@@ -42,7 +45,7 @@ const BlockPageContent = () => {
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
];
const hasPagination = !isMobile && router.query.tab === 'txs' && blockTxsQuery.isPaginationVisible;
const hasPagination = !isMobile && tab === 'txs' && blockTxsQuery.isPaginationVisible;
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/blocks');
......@@ -50,7 +53,7 @@ const BlockPageContent = () => {
<Page>
<TextAd mb={ 6 }/>
<PageTitle
text={ `Block #${ router.query.id }` }
text={ `Block #${ height }` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to blocks list"
/>
......
......@@ -7,7 +7,7 @@ import type { SmartContractVerificationConfigRaw, SmartContractVerificationMetho
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import isBrowser from 'lib/isBrowser';
import link from 'lib/link/link';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm';
import { isValidVerificationMethod, sortVerificationMethods } from 'ui/contractVerification/utils';
import Address from 'ui/shared/address/Address';
......@@ -26,11 +26,11 @@ const ContractVerification = () => {
const hasGoBackLink = referrer && referrer.includes('/address');
const router = useRouter();
const hash = router.query.id?.toString();
const method = router.query.id?.toString() as SmartContractVerificationMethod | undefined;
const hash = getQueryParamString(router.query.hash);
const method = getQueryParamString(router.query.method) as SmartContractVerificationMethod;
const contractQuery = useApiQuery('contract', {
pathParams: { id: hash },
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
},
......@@ -55,7 +55,7 @@ const ContractVerification = () => {
React.useEffect(() => {
if (method && hash) {
router.replace(link('address_contract_verification', { id: hash }), undefined, { scroll: false, shallow: true });
router.replace({ pathname: '/address/[hash]/contract_verification', query: { hash } }, undefined, { scroll: false, shallow: true });
}
// onMount only
// eslint-disable-next-line react-hooks/exhaustive-deps
......@@ -65,7 +65,7 @@ const ContractVerification = () => {
React.useEffect(() => {
if (isVerifiedContract) {
router.push(link('address_index', { id: hash }, { tab: 'contract' }), undefined, { scroll: false, shallow: true });
router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { scroll: false, shallow: true });
}
}, [ hash, isVerifiedContract, router ]);
......
import { Box, Center, useColorMode } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import appConfig from 'configs/app/config';
import link from 'lib/link/link';
import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page';
......@@ -26,9 +26,9 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
if (app && !isFrameLoading) {
const message = {
blockscoutColorMode: colorMode,
blockscoutRootUrl: link('network_index'),
blockscoutAddressExplorerUrl: link('address_index'),
blockscoutTransactionExplorerUrl: link('tx'),
blockscoutRootUrl: appConfig.baseUrl + route({ pathname: '/' }),
blockscoutAddressExplorerUrl: appConfig.baseUrl + route({ pathname: '/address/[hash]', query: { hash: '' } }),
blockscoutTransactionExplorerUrl: appConfig.baseUrl + route({ pathname: '/tx/[hash]', query: { hash: '' } }),
blockscoutNetworkName: appConfig.network.name,
blockscoutNetworkId: Number(appConfig.network.id),
blockscoutNetworkCurrency: appConfig.network.currency,
......
......@@ -12,7 +12,7 @@ import Token from './Token';
const TOKEN_API_URL = buildApiUrl('token', { hash: '1' });
const TOKEN_COUNTERS_API_URL = buildApiUrl('token_counters', { hash: '1' });
const TOKEN_TRANSFERS_API_URL = buildApiUrl('token_transfers', { hash: '1' });
const ADDRESS_API_URL = buildApiUrl('address', { id: '1' });
const ADDRESS_API_URL = buildApiUrl('address', { hash: '1' });
const hooksConfig = {
router: {
query: { hash: 1, tab: 'token_transfers' },
......
......@@ -16,13 +16,15 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import TokenLogo from 'ui/shared/TokenLogo';
import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenInventory from 'ui/token/TokenInventory';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
export type TokenTabs = 'token_transfers' | 'holders'
export type TokenTabs = 'token_transfers' | 'holders' | 'inventory';
const TokenPageContent = () => {
const router = useRouter();
......@@ -72,11 +74,24 @@ const TokenPageContent = () => {
},
});
const inventoryQuery = useQueryWithPages({
resourceName: 'token_inventory',
pathParams: { hash: router.query.hash?.toString() },
scrollRef,
options: {
enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && tokenQuery.data),
},
});
const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
];
if (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') {
tabs.push({ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> });
}
let hasPagination;
let pagination;
......@@ -90,6 +105,11 @@ const TokenPageContent = () => {
pagination = holdersQuery.pagination;
}
if (router.query.tab === 'inventory') {
hasPagination = inventoryQuery.isPaginationVisible;
pagination = inventoryQuery.pagination;
}
const tokenSymbolText = tokenQuery.data?.symbol ? ` (${ trimTokenSymbol(tokenQuery.data.symbol) })` : '';
return (
......@@ -118,12 +138,14 @@ const TokenPageContent = () => {
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
stickyEnabled={ !isMobile }
/>
{ tokenQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
stickyEnabled={ !isMobile }
/>
) }
</Page>
);
};
......
import React from 'react';
import Page from 'ui/shared/Page/Page';
import TokenInstanceContent from 'ui/tokenInstance/TokenInstanceContent';
export type TokenTabs = 'token_transfers' | 'holders'
const TokenInstance = () => {
return (
<Page>
<TokenInstanceContent/>
</Page>
);
};
export default TokenInstance;
......@@ -7,6 +7,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import networkExplorers from 'lib/networks/networkExplorers';
import getQueryParamString from 'lib/router/getQueryParamString';
import TextAd from 'ui/shared/ad/TextAd';
import LinkExternal from 'ui/shared/LinkExternal';
import Page from 'ui/shared/Page/Page';
......@@ -34,16 +35,17 @@ const TransactionPageContent = () => {
const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/txs');
const hash = getQueryParamString(router.query.hash);
const { data } = useApiQuery('tx', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
pathParams: { hash },
queryOptions: { enabled: Boolean(hash) },
});
const explorersLinks = networkExplorers
.filter((explorer) => explorer.paths.tx)
.map((explorer) => {
const url = new URL(explorer.paths.tx + '/' + router.query.id, explorer.baseUrl);
const url = new URL(explorer.paths.tx + '/' + hash, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } title={ `Open in ${ explorer.title }` } href={ url.toString() }/>;
});
......
import { Text, Flex, Icon, Box, chakra } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SearchResultItem } from 'types/api/search';
......@@ -6,7 +7,6 @@ import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -31,7 +31,7 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
return (
<Flex alignItems="flex-start">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<LinkInternal ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 } wordBreak="break-all">
<LinkInternal ml={ 2 } href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) } fontWeight={ 700 } wordBreak="break-all">
<chakra.span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</LinkInternal>
</Flex>
......@@ -56,7 +56,7 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
return (
<Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<LinkInternal fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }>
<LinkInternal fontWeight={ 700 } href={ route({ pathname: '/block/[height]', query: { height: String(data.block_number) } }) }>
<Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
</LinkInternal>
</Flex>
......
import { Tr, Td, Text, Flex, Icon, Box } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SearchResultItem } from 'types/api/search';
......@@ -6,7 +7,6 @@ import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -31,7 +31,7 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Td fontSize="sm">
<Flex alignItems="center">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<LinkInternal ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 } wordBreak="break-all">
<LinkInternal ml={ 2 } href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) } fontWeight={ 700 } wordBreak="break-all">
<span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</LinkInternal>
</Flex>
......@@ -54,7 +54,12 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Td fontSize="sm">
<Flex alignItems="center" overflow="hidden">
<AddressIcon address={{ hash: data.address, is_contract: data.type === 'contract', implementation_name: null }} mr={ 2 } flexShrink={ 0 }/>
<LinkInternal href={ link('address_index', { id: data.address }) } fontWeight={ 700 } overflow="hidden" whiteSpace="nowrap">
<LinkInternal
href={ route({ pathname: '/address/[hash]', query: { hash: data.address } }) }
fontWeight={ 700 }
overflow="hidden"
whiteSpace="nowrap"
>
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block">
<HashStringShortenDynamic hash={ data.address }/>
</Box>
......@@ -88,7 +93,7 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Td fontSize="sm">
<Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<LinkInternal fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }>
<LinkInternal fontWeight={ 700 } href={ route({ pathname: '/block/[height]', query: { height: String(data.block_number) } }) }>
<Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
</LinkInternal>
</Flex>
......
import { Flex } from '@chakra-ui/react';
import React from 'react';
import type { AddressParam } from 'types/api/addressParams';
import type { TokenInfo } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names'>;
token?: TokenInfo | null;
isLinkDisabled?: boolean;
}
const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => {
const isMobile = useIsMobile();
return (
<Flex alignItems="center">
<AddressIcon address={ address }/>
<AddressLink
type="address"
hash={ address.hash }
ml={ 2 }
fontFamily="heading"
fontWeight={ 500 }
truncation={ isMobile ? 'constant' : 'none' }
isDisabled={ isLinkDisabled }
/>
<CopyToClipboard text={ address.hash }/>
{ address.is_contract && token && <AddressAddToMetaMask ml={ 2 } token={ token }/> }
{ !address.is_contract && (
<AddressFavoriteButton hash={ address.hash } isAdded={ Boolean(address.watchlist_names?.length) } ml={ 3 }/>
) }
<AddressQrCode hash={ address.hash } ml={ 2 }/>
</Flex>
);
};
export default AddressHeadingInfo;
import { Box, Button, Heading, Icon, Text, chakra } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import icon404 from 'icons/error-pages/404.svg';
import icon422 from 'icons/error-pages/422.svg';
import icon500 from 'icons/error-pages/500.svg';
import link from 'lib/link/link';
interface Props {
statusCode: number;
......@@ -42,7 +42,7 @@ const AppError = ({ statusCode, className }: Props) => {
size="lg"
variant="outline"
as="a"
href={ link('network_index') }
href={ route({ pathname: '/' }) }
>
Back to home
</Button>
......
import { IconButton, Tooltip, useClipboard, chakra } from '@chakra-ui/react';
import { IconButton, Tooltip, useClipboard, chakra, useDisclosure } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';
import CopyIcon from 'icons/copy.svg';
......@@ -6,6 +6,8 @@ import CopyIcon from 'icons/copy.svg';
const CopyToClipboard = ({ text, className }: {text: string; className?: string}) => {
const { hasCopied, onCopy } = useClipboard(text, 3000);
const [ copied, setCopied ] = useState(false);
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
useEffect(() => {
if (hasCopied) {
......@@ -15,8 +17,13 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
}
}, [ hasCopied ]);
const handleClick = React.useCallback(() => {
onToggle();
onCopy();
}, [ onCopy, onToggle ]);
return (
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } closeOnClick={ false }>
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen }>
<IconButton
aria-label="copy"
icon={ <CopyIcon/> }
......@@ -25,11 +32,13 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
variant="simple"
display="inline-block"
flexShrink={ 0 }
onClick={ onCopy }
onClick={ handleClick }
className={ className }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
/>
</Tooltip>
);
};
export default chakra(CopyToClipboard);
export default React.memo(chakra(CopyToClipboard));
import { GridItem, Icon, Flex, Tooltip, Box, Text } from '@chakra-ui/react';
import { GridItem, Flex, Text } from '@chakra-ui/react';
import type { HTMLChakraProps } from '@chakra-ui/system';
import React from 'react';
import infoIcon from 'icons/info.svg';
import Hint from 'ui/shared/Hint';
interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
title: React.ReactNode;
......@@ -16,15 +16,7 @@ const DetailsInfoItem = ({ title, hint, note, children, id, ...styles }: Props)
<>
<GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}>
<Flex columnGap={ 2 } alignItems="flex-start">
<Tooltip
label={ hint }
placement="top"
maxW="320px"
>
<Box cursor="pointer" display="inherit">
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
<Hint label={ hint }/>
<Text fontWeight={{ base: 700, lg: 500 }}>
{ title }
{ note && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">{ note }</Text> }
......
import type { TooltipProps } from '@chakra-ui/react';
import { chakra, IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import InfoIcon from 'icons/info.svg';
interface Props {
label: string | React.ReactNode;
className?: string;
tooltipProps?: Partial<TooltipProps>;
}
const Hint = ({ label, className, tooltipProps }: Props) => {
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
const handleClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
onToggle();
}, [ onToggle ]);
return (
<Tooltip
label={ label }
placement="top"
maxW="320px"
isOpen={ isOpen }
{ ...tooltipProps }
>
<IconButton
colorScheme="none"
aria-label="hint"
icon={ <InfoIcon/> }
boxSize={ 5 }
variant="simple"
display="inline-block"
flexShrink={ 0 }
className={ className }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
onClick={ handleClick }
/>
</Tooltip>
);
};
export default React.memo(chakra(Hint));
import type { LinkProps } from '@chakra-ui/react';
import { Link } from '@chakra-ui/react';
import type { LinkProps as NextLinkProps } from 'next/link';
import NextLink from 'next/link';
import type { LegacyRef } from 'react';
import React from 'react';
......@@ -11,7 +12,7 @@ const LinkInternal = (props: LinkProps, ref: LegacyRef<HTMLAnchorElement>) => {
}
return (
<NextLink href={ props.href } passHref target={ props.target }>
<NextLink href={ props.href as NextLinkProps['href'] } passHref target={ props.target }>
<Link { ...props } ref={ ref }/>
</NextLink>
);
......
......@@ -21,6 +21,7 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi
as="h1"
size="lg"
flex="none"
wordBreak="break-word"
>
{ text }
</Heading>
......
......@@ -10,6 +10,7 @@ import {
chakra,
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import _pickBy from 'lodash/pickBy';
import { useRouter } from 'next/router';
import React, { useEffect, useRef, useState } from 'react';
......@@ -51,8 +52,9 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index];
const queryForPathname = _pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`));
router.push(
{ pathname: router.asPath.split('?')[0], query: { tab: nextTab.id } },
{ pathname: router.pathname, query: { ...queryForPathname, tab: nextTab.id } },
undefined,
{ shallow: true },
);
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { ROUTES } from 'lib/link/routes';
import TestApp from 'playwright/TestApp';
import SocketNewItemsNotice from './SocketNewItemsNotice';
const hooksConfig = {
router: {
pathname: ROUTES.txs.pattern,
pathname: '/tx/[hash]',
query: {},
},
};
......
......@@ -7,7 +7,7 @@ import {
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenType } from 'types/api/token';
import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
......
import { Box, Icon, Link, chakra } from '@chakra-ui/react';
import { Box, Icon, chakra } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
hash: string;
id: string;
className?: string;
isDisabled?: boolean;
}
const TokenTransferNft = ({ hash, id, className }: Props) => {
const TokenTransferNft = ({ hash, id, className, isDisabled }: Props) => {
const Component = isDisabled ? Box : LinkInternal;
return (
<Link
href={ link('token_instance_item', { hash, id }) }
<Component
href={ isDisabled ? undefined : route({ pathname: '/token/[hash]/instance/[id]', query: { hash, id } }) }
overflow="hidden"
whiteSpace="nowrap"
display="flex"
......@@ -26,7 +30,7 @@ const TokenTransferNft = ({ hash, id, className }: Props) => {
<Box maxW="calc(100% - 34px)">
<HashStringShortenDynamic hash={ id } fontWeight={ 500 }/>
</Box>
</Link>
</Component>
);
};
......
......@@ -30,7 +30,7 @@ const TokenTransferTable = ({
}: Props) => {
return (
<Table variant="simple" size="sm">
<Table variant="simple" size="sm" minW="950px">
<Thead top={ top }>
<Tr>
{ showTxInfo && <Th width="44px"></Th> }
......
import { chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import type { HTMLAttributeAnchorTarget } from 'react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -27,7 +27,7 @@ type AddressTokenTxProps = {
type BlockProps = {
type: 'block';
hash: string;
id: string;
height: string;
}
type AddressTokenProps = {
......@@ -44,15 +44,15 @@ const AddressLink = (props: Props) => {
let url;
if (type === 'transaction') {
url = link('tx', { id: hash });
url = route({ pathname: '/tx/[hash]', query: { hash } });
} else if (type === 'token') {
url = link('token_index', { hash: hash });
url = route({ pathname: '/token/[hash]', query: { hash } });
} else if (type === 'block') {
url = link('block', { id: props.id });
url = route({ pathname: '/block/[height]', query: { height: props.height } });
} else if (type === 'address_token') {
url = link('address_index', { id: hash }, { tab: 'token_transfers', token: props.tokenHash, scroll_to_tabs: 'true' });
url = route({ pathname: '/address/[hash]', query: { hash, tab: 'token_transfers', token_hash: props.tokenHash, scroll_to_tabs: 'true' } });
} else {
url = link('address_index', { id: hash });
url = route({ pathname: '/address/[hash]', query: { hash } });
}
const content = (() => {
......
import { CheckboxGroup, Checkbox, Text } from '@chakra-ui/react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenType } from 'types/api/token';
import TOKEN_TYPE from 'lib/token/tokenTypes';
......
import { Text, Grid, GridItem, Tooltip, Button, useColorModeValue, Alert, Link } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { Log } from 'types/api/log';
// import searchIcon from 'icons/search.svg';
import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -47,7 +47,7 @@ const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash
<GridItem colSpan={{ base: 1, lg: 2 }}>
<Alert status="warning" display="inline-table" whiteSpace="normal">
To see accurate decoded input data, the contract must be verified.{ space }
<Link href={ link('address_contract_verification', { id: address.hash }) }>Verify the contract here</Link>
<Link href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: address.hash } }) }>Verify the contract here</Link>
</Alert>
</GridItem>
) }
......
import type { ResponsiveValue } from '@chakra-ui/react';
import { AspectRatio, chakra, Icon, Image, shouldForwardProp, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { Property } from 'csstype';
import React from 'react';
import nftIcon from 'icons/nft_shield.svg';
interface Props {
url: string | null;
className?: string;
fallbackPadding?: string;
objectFit: ResponsiveValue<Property.ObjectFit>;
}
interface FallbackProps {
className?: string;
padding?: string;
}
const Fallback = ({ className, padding }: FallbackProps) => {
return (
<Icon
className={ className }
as={ nftIcon }
p={ padding ?? '50px' }
color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') }
/>
);
};
const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
const [ isLoading, setIsLoading ] = React.useState(true);
const handleLoad = React.useCallback(() => {
setIsLoading(false);
}, []);
const handleLoadError = React.useCallback(() => {
setIsLoading(false);
}, []);
const _objectFit = objectFit || 'contain';
return (
<AspectRatio
className={ className }
ratio={ 1 / 1 }
bgColor={ useColorModeValue('blackAlpha.50', 'whiteAlpha.50') }
overflow="hidden"
borderRadius="md"
sx={{
'&>img': {
objectFit: _objectFit,
},
}}
>
<Image
w="100%"
h="100%"
objectFit={ _objectFit }
src={ url || undefined }
alt="Token instance image"
fallback={ url && isLoading ? <Skeleton/> : <Fallback className={ className } padding={ fallbackPadding }/> }
onError={ handleLoadError }
onLoad={ handleLoad }
/>
</AspectRatio>
);
};
const NftImageChakra = chakra(NftImage, {
shouldForwardProp: (prop) => {
const isChakraProp = !shouldForwardProp(prop);
if (isChakraProp && prop !== 'objectFit') {
return false;
}
return true;
},
});
export default NftImageChakra;
import { AspectRatio, chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import NftImage from './NftImage';
import NftVideo from './NftVideo';
interface Props {
imageUrl: string | null;
animationUrl: string | null;
className?: string;
}
const NftMedia = ({ imageUrl, animationUrl, className }: Props) => {
const [ type, setType ] = React.useState<'image' | 'video' | undefined>(!animationUrl ? 'image' : undefined);
React.useEffect(() => {
if (!animationUrl) {
return;
}
// media could be either gif or video
// so we pre-fetch the resources in order to get its content type
fetch(animationUrl, { method: 'HEAD' })
.then((response) => {
const contentType = response.headers.get('content-type');
setType(contentType?.startsWith('video') ? 'video' : 'image');
});
}, [ animationUrl ]);
if (!type) {
return (
<AspectRatio
className={ className }
ratio={ 1 / 1 }
overflow="hidden"
borderRadius="md"
>
<Skeleton/>
</AspectRatio>
);
}
if (animationUrl && type === 'video') {
return <NftVideo className={ className } src={ animationUrl }/>;
}
return <NftImage className={ className } url={ animationUrl || imageUrl }/>;
};
export default chakra(NftMedia);
import { AspectRatio, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
className?: string;
src: string;
}
const NftVideo = ({ className, src }: Props) => {
const [ isLoading, setIsLoading ] = React.useState(true);
const handleCanPlay = React.useCallback(() => {
setIsLoading(false);
}, []);
return (
<AspectRatio
className={ className }
ratio={ 1 / 1 }
overflow="hidden"
borderRadius="md"
>
<>
<chakra.video
src={ src }
autoPlay
disablePictureInPicture
loop
muted
playsInline
onCanPlayThrough={ handleCanPlay }
borderRadius="md"
/>
{ isLoading && <Skeleton position="absolute" w="100%" h="100%" left={ 0 } top={ 0 }/> }
</>
</AspectRatio>
);
};
export default chakra(NftVideo);
import { GridItem, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
const DetailsSkeletonRow = ({ w = '100%' }: { w?: string }) => {
const DetailsSkeletonRow = ({ w = '100%', maxW }: { w?: string; maxW?: string }) => {
return (
<>
<GridItem display="flex" columnGap={ 2 } w={{ base: '50%', lg: 'auto' }} _notFirst={{ mt: { base: 3, lg: 0 } }}>
......@@ -9,7 +9,7 @@ const DetailsSkeletonRow = ({ w = '100%' }: { w?: string }) => {
<Skeleton flexGrow={ 1 } h={ 5 } borderRadius="full"/>
</GridItem>
<GridItem pl={{ base: 7, lg: 0 }}>
<Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }}/>
<Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }} maxW={ maxW }/>
</GridItem>
</>
);
......
import { Link, Icon, Text, HStack, Tooltip, Box } from '@chakra-ui/react';
import NextLink from 'next/link';
import { route } from 'nextjs-routes';
import React from 'react';
import type { NavItem } from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import useColors from './useColors';
interface Props {
type Props = NavItem & {
isCollapsed?: boolean;
isActive?: boolean;
url: string;
text: string;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
px?: string | number;
isNewUi: boolean;
}
const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props) => {
const NavLink = ({ text, nextRoute, icon, isCollapsed, isActive, px, isNewUi }: Props) => {
const colors = useColors();
const isExpanded = isCollapsed === false;
const content = (
<Link
{ ...(isNewUi ? {} : { href: url }) }
{ ...(isNewUi ? {} : { href: route(nextRoute) }) }
w={{ base: '100%', lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }}
px={ px || { base: 3, lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 } }
py={ 2.5 }
......@@ -68,7 +65,7 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props)
{ /* why not NextLink in all cases? since prev UI and new one are hosting in the same domain and global routing is managed by nginx */ }
{ /* we have to hard reload page on every transition between urls from different part of the app */ }
{ isNewUi ? (
<NextLink href={ url } passHref>
<NextLink href={ nextRoute } passHref>
{ content }
</NextLink>
) : content }
......
......@@ -12,6 +12,7 @@ const hooksConfig = {
router: {
route: '/blocks',
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' },
pathname: '/blocks',
},
};
......
import { Icon, Box, Image, useColorModeValue } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import appConfig from 'configs/app/config';
import smallLogoPlaceholder from 'icons/networks/icons/placeholder.svg';
import logoPlaceholder from 'icons/networks/logos/blockscout.svg';
import link from 'lib/link/link';
import ASSETS from 'lib/networks/networkAssets';
interface Props {
......@@ -14,7 +14,7 @@ interface Props {
const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
const logoColor = useColorModeValue('blue.600', 'white');
const href = link('network_index');
const href = route({ pathname: '/' });
const [ isLogoError, setLogoError ] = React.useState(false);
const [ isSmallLogoError, setSmallLogoError ] = React.useState(false);
......
......@@ -12,6 +12,7 @@ test('no auth', async({ mount, page }) => {
const hooksConfig = {
router: {
asPath: '/',
pathname: '/',
},
};
const component = await mount(
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import _debounce from 'lodash/debounce';
import { route } from 'nextjs-routes';
import type { FormEvent, FocusEvent } from 'react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import SearchBarInput from './SearchBarInput';
import SearchBarSuggest from './SearchBarSuggest';
......@@ -26,7 +26,7 @@ const SearchBar = ({ isHomepage }: Props) => {
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (searchTerm) {
const url = link('search_results', undefined, { q: searchTerm });
const url = route({ pathname: '/search-results', query: { q: searchTerm } });
window.location.assign(url);
}
}, [ searchTerm ]);
......
import { chakra, Text, Flex, useColorModeValue, Icon, Box } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SearchResultItem } from 'types/api/search';
......@@ -6,7 +7,6 @@ import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import AddressIcon from 'ui/shared/address/AddressIcon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import TokenLogo from 'ui/shared/TokenLogo';
......@@ -22,17 +22,17 @@ const SearchBarSuggestItem = ({ data, isMobile, searchTerm }: Props) => {
const url = (() => {
switch (data.type) {
case 'token': {
return link('token_index', { hash: data.address });
return route({ pathname: '/token/[hash]', query: { hash: data.address } });
}
case 'contract':
case 'address': {
return link('address_index', { id: data.address });
return route({ pathname: '/address/[hash]', query: { hash: data.address } });
}
case 'transaction': {
return link('tx', { id: data.tx_hash });
return route({ pathname: '/tx/[hash]', query: { hash: data.tx_hash } });
}
case 'block': {
return link('block', { id: String(data.block_number) });
return route({ pathname: '/block/[height]', query: { height: String(data.block_number) } });
}
}
})();
......
......@@ -28,7 +28,7 @@ function composeSources(contract: SmartContract | undefined) {
const Sol2UmlDiagram = ({ addressHash }: Props) => {
const contractQuery = useApiQuery<'contract', ResourceError>('contract', {
pathParams: { id: addressHash },
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash),
refetchOnMount: false,
......
......@@ -3,15 +3,10 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressContractIcon from 'ui/shared/address/AddressContractIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
interface Props {
tokenQuery: UseQueryResult<TokenInfo>;
......@@ -19,10 +14,9 @@ interface Props {
const TokenContractInfo = ({ tokenQuery }: Props) => {
const router = useRouter();
const isMobile = useIsMobile();
const contractQuery = useApiQuery('address', {
pathParams: { id: router.query.hash?.toString() },
pathParams: { hash: router.query.hash?.toString() },
queryOptions: { enabled: Boolean(router.query.hash) },
});
......@@ -43,17 +37,14 @@ const TokenContractInfo = ({ tokenQuery }: Props) => {
return null;
}
const hash = tokenQuery.data.address;
return (
<Flex alignItems="center">
<AddressContractIcon/>
<AddressLink type="address" hash={ hash } ml={ 2 } truncation={ isMobile ? 'constant' : 'none' }/>
<CopyToClipboard text={ hash } ml={ 1 }/>
{ contractQuery.data?.token && <AddressAddToMetaMask token={ contractQuery.data?.token } ml={ 2 }/> }
<AddressQrCode hash={ hash } ml={ 2 }/>
</Flex>
);
const address = {
hash: tokenQuery.data.address,
is_contract: true,
implementation_name: null,
watchlist_names: [],
};
return <AddressHeadingInfo address={ address } token={ contractQuery.data?.token }/>;
};
export default React.memo(TokenContractInfo);
......@@ -4,11 +4,10 @@ import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { scroller } from 'react-scroll';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue';
import link from 'lib/link/link';
import type { TokenTabs } from 'ui/pages/Token';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
......@@ -26,9 +25,8 @@ const TokenDetails = ({ tokenQuery }: Props) => {
});
const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => {
const newLink = link('token_index', { hash: router.query.hash }, { tab: tab });
router.push(
newLink,
{ pathname: '/token/[hash]', query: { hash: router.query.hash?.toString() || '', tab } },
undefined,
{ shallow: true },
);
......@@ -88,7 +86,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
{ exchangeRate && (
<DetailsInfoItem
title="Price"
hint="Price per token on the exchanges."
hint="Price per token on the exchanges"
alignSelf="center"
>
{ `$${ exchangeRate }` }
......@@ -97,7 +95,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
{ totalValue?.usd && (
<DetailsInfoItem
title="Fully diluted market cap"
hint="Total supply * Price."
hint="Total supply * Price"
alignSelf="center"
>
{ `$${ totalValue?.usd }` }
......@@ -105,7 +103,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
) }
<DetailsInfoItem
title="Max total supply"
hint="The total amount of tokens issued."
hint="The total amount of tokens issued"
alignSelf="center"
wordBreak="break-word"
whiteSpace="pre-wrap"
......@@ -114,7 +112,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
</DetailsInfoItem>
<DetailsInfoItem
title="Holders"
hint="Number of accounts holding the token."
hint="Number of accounts holding the token"
alignSelf="center"
>
{ tokenCountersQuery.isLoading && <Skeleton w={ 20 } h={ 4 }/> }
......@@ -122,7 +120,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
</DetailsInfoItem>
<DetailsInfoItem
title="Transfers"
hint="Number of transfer for the token."
hint="Number of transfer for the token"
alignSelf="center"
>
{ tokenCountersQuery.isLoading && <Skeleton w={ 20 } h={ 4 }/> }
......@@ -131,7 +129,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
{ decimals && (
<DetailsInfoItem
title="Decimals"
hint="Number of digits that come after the decimal place when displaying token value."
hint="Number of digits that come after the decimal place when displaying token value"
alignSelf="center"
>
{ decimals }
......
......@@ -2,7 +2,7 @@ import { Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { TokenHolders, TokenInfo } from 'types/api/tokenInfo';
import type { TokenHolders, TokenInfo } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import type { TokenHolder, TokenInfo } from 'types/api/token';
import TokenHoldersListItem from './TokenHoldersListItem';
......
......@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import type { TokenHolder, TokenInfo } from 'types/api/token';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import type { TokenHolder, TokenInfo } from 'types/api/token';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokenHoldersTableItem from 'ui/token/TokenHolders/TokenHoldersTableItem';
......
......@@ -2,7 +2,7 @@ import { Tr, Td } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo';
import type { TokenHolder, TokenInfo } from 'types/api/token';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { base as tokenInstanse } from 'mocks/tokens/tokenInstance';
import TestApp from 'playwright/TestApp';
import TokenInventory from './TokenInventory';
test('base view +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 0 }}/>
<TokenInventory
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
inventoryQuery={{
data: {
items: [ tokenInstanse, tokenInstanse, tokenInstanse ],
next_page_params: { unique_token: 1 },
},
isPaginationVisible: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
pagination: { page: 1 },
}}
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Grid, Text, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { TokenInventoryResponse } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import TokenInventoryItem from './TokenInventoryItem';
type Props = {
inventoryQuery: UseQueryResult<TokenInventoryResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokenInventory = ({ inventoryQuery }: Props) => {
const isMobile = useIsMobile();
if (inventoryQuery.isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && inventoryQuery.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...inventoryQuery.pagination }/>
</ActionBar>
);
if (inventoryQuery.isLoading) {
return (
<>
{ bar }
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
</>
);
}
const items = inventoryQuery.data.items;
if (!items?.length) {
return <Text as="span">There are no tokens.</Text>;
}
return (
<>
{ bar }
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ items.map((item) => <TokenInventoryItem key={ item.token.address } item={ item }/>) }
</Grid></>
);
};
export default TokenInventory;
import { Flex, Link, Text, LinkBox, LinkOverlay, useColorModeValue, Hide } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { TokenInstance } from 'types/api/token';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import NftMedia from 'ui/shared/nft/NftMedia';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = { item: TokenInstance };
const NFTItem = ({ item }: Props) => {
const tokenLink = route({ pathname: '/token/[hash]/instance/[id]', query: { hash: item.token.address, id: item.id } });
return (
<LinkBox
w={{ base: '100%', lg: '210px' }}
border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px"
p="10px"
_hover={{ boxShadow: 'md' }}
fontSize="sm"
fontWeight={ 500 }
lineHeight="20px"
>
<LinkOverlay href={ tokenLink }>
<NftMedia
mb="18px"
imageUrl={ item.image_url }
animationUrl={ item.animation_url }
/>
</LinkOverlay>
{ item.id && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text>
<TruncatedTextTooltip label={ item.id }>
<Link
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ item.id }
</Link>
</TruncatedTextTooltip>
</Flex>
) }
{ item.owner && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary" mr={ 2 } lineHeight="24px">Owner</Text>
<Address>
<Hide below="lg" ssr={ false }><AddressIcon address={ item.owner } mr={ 1 }/></Hide>
<AddressLink hash={ item.owner.hash } alias={ item.owner.name } type="address" truncation="constant"/>
</Address>
</Flex>
) }
</LinkBox>
);
};
export default NFTItem;
......@@ -2,7 +2,6 @@ import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
......@@ -13,7 +12,6 @@ test('erc20 +@mobile', async({ mount }) => {
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={ tokenInfo }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
......@@ -38,7 +36,6 @@ test('erc721 +@mobile', async({ mount }) => {
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={{ ...tokenInfo, type: 'ERC-721' }}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
......@@ -63,7 +60,6 @@ test('erc1155 +@mobile', async({ mount }) => {
<TestApp>
<Box h={{ base: '134px', lg: '100px' }}/>
<TokenTransfer
token={{ ...tokenInfo, type: 'ERC-1155', symbol: tokenTransferMock.erc1155multiple.token.symbol }}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
transfersQuery={{
......
......@@ -4,7 +4,6 @@ import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
......@@ -23,14 +22,14 @@ import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable';
type Props = {
token?: TokenInfo;
transfersQuery: UseQueryResult<TokenTransferResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
tokenId?: string;
}
const TokenTransfer = ({ transfersQuery, token }: Props) => {
const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
const isMobile = useIsMobile();
const router = useRouter();
const { isError, isLoading, data, pagination, isPaginationVisible } = transfersQuery;
......@@ -92,12 +91,10 @@ const TokenTransfer = ({ transfersQuery, token }: Props) => {
<TokenTransferTable
data={ items }
top={ isPaginationVisible ? 80 : 0 }
// token transfers query depends on token data
// so if we are here, we definitely have token data
token={ token as TokenInfo }
showSocketInfo={ pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
tokenId={ tokenId }
/>
</Hide>
<Show below="lg" ssr={ false }>
......@@ -110,7 +107,7 @@ const TokenTransfer = ({ transfersQuery, token }: Props) => {
borderBottomRadius={ 0 }
/>
) }
<TokenTransferList data={ items }/>
<TokenTransferList data={ items } tokenId={ tokenId }/>
</Show>
</>
);
......
......@@ -7,15 +7,17 @@ import TokenTransferListItem from 'ui/token/TokenTransfer/TokenTransferListItem'
interface Props {
data: Array<TokenTransfer>;
tokenId?: string;
}
const TokenTransferList = ({ data }: Props) => {
const TokenTransferList = ({ data, tokenId }: Props) => {
return (
<Box>
{ data.map((item, index) => (
<TokenTransferListItem
key={ index }
{ ...item }
tokenId={ tokenId }
/>
)) }
</Box>
......
......@@ -14,7 +14,7 @@ import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer;
type Props = TokenTransfer & {tokenId?: string};
const TokenTransferListItem = ({
token,
......@@ -24,6 +24,7 @@ const TokenTransferListItem = ({
to,
method,
timestamp,
tokenId,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
......@@ -78,7 +79,7 @@ const TokenTransferListItem = ({
</Flex>
) }
{ 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') &&
<TokenTransferNft hash={ token.address } id={ total.token_id }/> }
<TokenTransferNft hash={ token.address } id={ total.token_id } isDisabled={ Boolean(tokenId && tokenId === total.token_id) }/> }
</ListItemMobile>
);
};
......
import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
......@@ -12,24 +11,27 @@ import TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableIte
interface Props {
data: Array<TokenTransfer>;
top: number;
token: TokenInfo;
showSocketInfo: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
tokenId?: string;
}
const TokenTransferTable = ({ data, top, token, showSocketInfo, socketInfoAlert, socketInfoNum }: Props) => {
const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socketInfoNum, tokenId }: Props) => {
const tokenType = data[0].token.type;
const tokenSymbol = data[0].token.symbol;
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width={ token.type === 'ERC-1155' ? '60%' : '80%' }>Txn hash</Th>
<Th width="60%">Txn hash</Th>
<Th width="164px">Method</Th>
<Th width="148px">From</Th>
<Th width="36px" px={ 0 }/>
<Th width="218px" >To</Th>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && <Th width="20%" isNumeric={ token.type === 'ERC-721' }>Token ID</Th> }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && <Th width="20%" isNumeric>Value { trimTokenSymbol(token.symbol) }</Th> }
{ (tokenType === 'ERC-721' || tokenType === 'ERC-1155') && <Th width="20%" isNumeric={ tokenType === 'ERC-721' }>Token ID</Th> }
{ (tokenType === 'ERC-20' || tokenType === 'ERC-1155') && <Th width="20%" isNumeric whiteSpace="nowrap">Value { trimTokenSymbol(tokenSymbol) }</Th> }
</Tr>
</Thead>
<Tbody>
......@@ -48,7 +50,7 @@ const TokenTransferTable = ({ data, top, token, showSocketInfo, socketInfoAlert,
</Tr>
) }
{ data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item }/>
<TokenTransferTableItem key={ index } { ...item } tokenId={ tokenId }/>
)) }
</Tbody>
</Table>
......
......@@ -11,7 +11,7 @@ import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer
type Props = TokenTransfer & { tokenId?: string }
const TokenTransferTableItem = ({
token,
......@@ -21,6 +21,7 @@ const TokenTransferTableItem = ({
to,
method,
timestamp,
tokenId,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
......@@ -84,7 +85,9 @@ const TokenTransferTableItem = ({
<TokenTransferNft
hash={ token.address }
id={ total.token_id }
justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' }/>
justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' }
isDisabled={ Boolean(tokenId && tokenId === total.token_id) }
/>
) : '-'
}
</Td>
......
import { Box, Tag } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import AdBanner from 'ui/shared/ad/AdBanner';
import TextAd from 'ui/shared/ad/TextAd';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenLogo from 'ui/shared/TokenLogo';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
import TokenInstanceDetails from './TokenInstanceDetails';
import TokenInstanceSkeleton from './TokenInstanceSkeleton';
export type TokenTabs = 'token_transfers' | 'holders'
const TokenInstanceContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const appProps = useAppContext();
const hash = router.query.hash?.toString();
const id = router.query.id?.toString();
const tab = router.query.tab?.toString();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance');
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenInstanceQuery = useApiQuery('token_instance', {
pathParams: { hash, id },
queryOptions: { enabled: Boolean(hash && id) },
});
const transfersQuery = useQueryWithPages({
resourceName: 'token_instance_transfers',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && (!tab || tab === 'token_transfers') && tokenInstanceQuery.data),
},
});
const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> },
// there is no api for this tab yet
// { id: 'holders', title: 'Holders', component: <span>Holders</span> },
{ id: 'metadata', title: 'Metadata', component: <span>Metadata</span> },
];
if (tokenInstanceQuery.isError) {
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
}
if (tokenInstanceQuery.isLoading) {
return <TokenInstanceSkeleton/>;
}
const tokenLogo = <TokenLogo hash={ tokenInstanceQuery.data.token.address } name={ tokenInstanceQuery.data.token.name } boxSize={ 6 }/>;
const tokenTag = <Tag>{ tokenInstanceQuery.data.token.type }</Tag>;
const address = {
hash: hash || '',
is_contract: true,
implementation_name: null,
watchlist_names: [],
};
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
text={ `${ tokenInstanceQuery.data.token.name } #${ tokenInstanceQuery.data.id }` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to token page"
additionalsLeft={ tokenLogo }
additionalsRight={ tokenTag }
/>
<AddressHeadingInfo address={ address } token={ tokenInstanceQuery.data.token }/>
<TokenInstanceDetails data={ tokenInstanceQuery.data } scrollRef={ scrollRef }/>
<AdBanner mt={{ base: 6, lg: 8 }} justifyContent="center"/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && transfersQuery.isPaginationVisible ? <Pagination { ...transfersQuery.pagination }/> : null }
stickyEnabled={ !isMobile }
/>
</>
);
};
export default React.memo(TokenInstanceContent);
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as addressMock from 'mocks/address/address';
import * as tokenInstanceMock from 'mocks/tokens/tokenInstance';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TokenInstanceDetails from './TokenInstanceDetails';
const API_URL_ADDRESS = buildApiUrl('address', { hash: tokenInstanceMock.base.token.address });
const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', {
id: tokenInstanceMock.base.id,
hash: tokenInstanceMock.base.token.address,
});
test('base view +@dark-mode', async({ mount, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.contract),
}));
await page.route(API_URL_TOKEN_TRANSFERS_COUNT, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ transfers_count: 42 }),
}));
const component = await mount(
<TestApp>
<TokenInstanceDetails data={ tokenInstanceMock.base }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Flex, Grid } from '@chakra-ui/react';
import React from 'react';
import type { TokenInstance } from 'types/api/token';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import NftMedia from 'ui/shared/nft/NftMedia';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import TokenInstanceCreatorAddress from './details/TokenInstanceCreatorAddress';
import TokenInstanceTransfersCount from './details/TokenInstanceTransfersCount';
interface Props {
data: TokenInstance;
scrollRef?: React.RefObject<HTMLDivElement>;
}
const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
const handleCounterItemClick = React.useCallback(() => {
window.setTimeout(() => {
// cannot do scroll instantly, have to wait a little
scrollRef?.current?.scrollIntoView({ behavior: 'smooth' });
}, 500);
}, [ scrollRef ]);
return (
<Flex alignItems="flex-start" mt={ 8 } flexDir={{ base: 'column-reverse', lg: 'row' }} columnGap={ 6 } rowGap={ 6 }>
<Grid
flexGrow={ 1 }
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: '200px minmax(0, 1fr)' }} overflow="hidden"
>
<DetailsInfoItem
title="Token"
hint="Token name"
>
<TokenSnippet hash={ data.token.address } name={ data.token.name }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Owner"
hint="Current owner of this token instance"
>
<Address>
<AddressIcon address={ data.owner }/>
<AddressLink type="address" hash={ data.owner.hash } ml={ 2 }/>
<CopyToClipboard text={ data.owner.hash }/>
</Address>
</DetailsInfoItem>
<TokenInstanceCreatorAddress hash={ data.token.address }/>
<DetailsInfoItem
title="Token ID"
hint="This token instance unique token ID"
>
<Flex alignItems="center" overflow="hidden">
<Box overflow="hidden" display="inline-block" w="100%">
<HashStringShortenDynamic hash={ data.id }/>
</Box>
<CopyToClipboard text={ data.id } ml={ 1 }/>
</Flex>
</DetailsInfoItem>
<TokenInstanceTransfersCount hash={ data.token.address } id={ data.id } onClick={ handleCounterItemClick }/>
</Grid>
<NftMedia
imageUrl={ data.image_url }
animationUrl={ data.animation_url }
w="250px"
flexShrink={ 0 }
alignSelf={{ base: 'center', lg: 'flex-start' }}
/>
</Flex>
);
};
export default React.memo(TokenInstanceDetails);
import { Box, Flex, Grid, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
const TokenInstanceSkeleton = () => {
return (
<Box>
<Skeleton h={ 10 } maxW="400px" w="100%" mb={ 6 }/>
<Flex align="center">
<SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 } borderRadius="full"/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
</Flex>
<Flex columnGap={ 6 } rowGap={ 6 } alignItems="flex-start" flexDir={{ base: 'column-reverse', lg: 'row' }} mt={ 8 }>
<Grid
columnGap={ 8 }
rowGap={{ base: 5, lg: 7 }}
templateColumns={{ base: '1fr', lg: '200px 1fr' }}
flexGrow={ 1 }
w="100%"
>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="100%" maxW="450px"/>
<DetailsSkeletonRow w="100%" maxW="450px"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="10%"/>
</Grid>
<Skeleton h="250px" w="250px" flexShrink={ 0 } alignSelf="center"/>
</Flex>
<SkeletonTabs/>
</Box>
);
};
export default TokenInstanceSkeleton;
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
interface Props {
hash: string;
}
const TokenInstanceCreatorAddress = ({ hash }: Props) => {
const addressQuery = useApiQuery('address', {
pathParams: { hash },
});
if (addressQuery.isError) {
return null;
}
if (addressQuery.isLoading) {
return <DetailsSkeletonRow w="30%"/>;
}
if (!addressQuery.data.creator_address_hash) {
return null;
}
const creatorAddress = {
hash: addressQuery.data.creator_address_hash,
is_contract: false,
implementation_name: null,
};
return (
<DetailsInfoItem
title="Creator"
hint="Address that deployed this token contract"
>
<Address>
<AddressIcon address={ creatorAddress }/>
<AddressLink type="address" hash={ creatorAddress.hash } ml={ 2 }/>
<CopyToClipboard text={ creatorAddress.hash }/>
</Address>
</DetailsInfoItem>
);
};
export default TokenInstanceCreatorAddress;
import { route } from 'nextjs-routes';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
interface Props {
hash: string;
id: string;
onClick: () => void;
}
const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
const transfersCountQuery = useApiQuery('token_instance_transfers_count', {
pathParams: { hash, id },
});
if (transfersCountQuery.isError) {
return null;
}
if (transfersCountQuery.isLoading) {
return <DetailsSkeletonRow w="30%"/>;
}
if (!transfersCountQuery.data.transfers_count) {
return null;
}
const url = transfersCountQuery.data.transfers_count > 0 ?
route({ pathname: '/token/[hash]/instance/[id]', query: { hash, id, tab: 'token_transfers' } }) :
undefined;
return (
<DetailsInfoItem
title="Transfers"
hint="Number of transfer for the token instance"
>
<LinkInternal
href={ url }
onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined }
>
{ transfersCountQuery.data.transfers_count }
</LinkInternal>
</DetailsInfoItem>
);
};
export default TokenInstanceTransfersCount;
......@@ -2,7 +2,7 @@ import { Hide, HStack, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenType } from 'types/api/token';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useDebounce from 'lib/hooks/useDebounce';
......
import { Flex, Text, Tag, HStack, Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
......
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenInfo } from 'types/api/token';
import { default as Thead } from 'ui/shared/TheadSticky';
......
import { Box, Flex, Td, Tr, Text, Tag } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
......
import { Flex, Link, Text, Icon, Box } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import nftIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
......@@ -17,7 +17,7 @@ interface Props {
const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props) => {
const num = value === '1' ? '' : value;
const url = link('token_instance_item', { hash: hash, id: tokenId });
const url = route({ pathname: '/token/[hash]/instance/[id]', query: { hash: hash, id: tokenId } });
return (
<Flex alignItems="center" columnGap={ 3 } rowGap={ 2 } flexWrap="wrap">
......
......@@ -8,10 +8,10 @@ import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder';
import TxDetails from './TxDetails';
const API_URL = buildApiUrl('tx', { id: '1' });
const API_URL = buildApiUrl('tx', { hash: '1' });
const hooksConfig = {
router: {
query: { id: 1 },
query: { hash: 1 },
},
};
......
......@@ -126,7 +126,7 @@ const TxDetails = () => {
) }
<DetailsInfoItem
title="Transaction hash"
hint="Unique character string (TxID) assigned to every verified transaction."
hint="Unique character string (TxID) assigned to every verified transaction"
flexWrap="nowrap"
>
{ data.status === null && <Spinner mr={ 2 } size="sm" flexShrink={ 0 }/> }
......@@ -146,14 +146,14 @@ const TxDetails = () => {
{ data.revert_reason && (
<DetailsInfoItem
title="Revert reason"
hint="The revert reason of the transaction."
hint="The revert reason of the transaction"
>
<TxRevertReason { ...data.revert_reason }/>
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Block"
hint="Block number containing the transaction."
hint="Block number containing the transaction"
>
<Text>{ data.block === null ? 'Pending' : data.block }</Text>
{ Boolean(data.confirmations) && (
......@@ -168,7 +168,7 @@ const TxDetails = () => {
{ data.timestamp && (
<DetailsInfoItem
title="Timestamp"
hint="Date & time of transaction inclusion, including length of time for confirmation."
hint="Date & time of transaction inclusion, including length of time for confirmation"
>
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ dayjs(data.timestamp).fromNow() }</Text>
......@@ -205,7 +205,7 @@ const TxDetails = () => {
<DetailsInfoItem
title="From"
hint="Address (external or contract) sending the transaction."
hint="Address (external or contract) sending the transaction"
columnGap={ 3 }
>
<Address>
......@@ -222,7 +222,7 @@ const TxDetails = () => {
</DetailsInfoItem>
<DetailsInfoItem
title={ data.to?.is_contract ? 'Interacted with contract' : 'To' }
hint="Address (external or contract) receiving the transaction."
hint="Address (external or contract) receiving the transaction"
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
columnGap={ 3 }
>
......@@ -257,13 +257,13 @@ const TxDetails = () => {
<DetailsInfoItem
title="Value"
hint="Value sent in the native token (and USD) if applicable."
hint="Value sent in the native token (and USD) if applicable"
>
<CurrencyValue value={ data.value } currency={ appConfig.network.currency.symbol } exchangeRate={ data.exchange_rate }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Transaction fee"
hint="Total transaction fee."
hint="Total transaction fee"
>
<CurrencyValue
value={ data.fee.value }
......@@ -274,14 +274,14 @@ const TxDetails = () => {
</DetailsInfoItem>
<DetailsInfoItem
title="Gas price"
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage."
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage"
>
<Text mr={ 1 }>{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text>
</DetailsInfoItem>
<DetailsInfoItem
title="Gas limit & usage by txn"
hint="Actual gas amount used by the transaction."
hint="Actual gas amount used by the transaction"
>
<Text>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
<TextSeparator/>
......@@ -292,7 +292,7 @@ const TxDetails = () => {
<DetailsInfoItem
title="Gas fees (Gwei)"
// eslint-disable-next-line max-len
hint="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively."
hint="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively"
>
{ data.base_fee_per_gas && (
<Box>
......@@ -319,7 +319,7 @@ const TxDetails = () => {
{ data.tx_burnt_fee && (
<DetailsInfoItem
title="Burnt fees"
hint={ `Amount of ${ appConfig.network.currency.symbol } burned for this transaction. Equals Block Base Fee per Gas * Gas Used.` }
hint={ `Amount of ${ appConfig.network.currency.symbol } burned for this transaction. Equals Block Base Fee per Gas * Gas Used` }
>
<Icon as={ flameIcon } mr={ 1 } boxSize={ 5 } color="gray.500"/>
<CurrencyValue
......@@ -349,7 +349,7 @@ const TxDetails = () => {
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
<DetailsInfoItem
title="Other"
hint="Other data related to this transaction."
hint="Other data related to this transaction"
>
{
[
......@@ -382,7 +382,7 @@ const TxDetails = () => {
</DetailsInfoItem>
<DetailsInfoItem
title="Raw input"
hint="Binary data included with the transaction. See logs tab for additional info."
hint="Binary data included with the transaction. See logs tab for additional info"
>
<RawInputData hex={ data.raw_input }/>
</DetailsInfoItem>
......
......@@ -9,11 +9,11 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxInternals from './TxInternals';
const TX_HASH = txMock.base.hash;
const API_URL_TX = buildApiUrl('tx', { id: TX_HASH });
const API_URL_TX_INTERNALS = buildApiUrl('tx_internal_txs', { id: TX_HASH });
const API_URL_TX = buildApiUrl('tx', { hash: TX_HASH });
const API_URL_TX_INTERNALS = buildApiUrl('tx_internal_txs', { hash: TX_HASH });
const hooksConfig = {
router: {
query: { id: TX_HASH },
query: { hash: TX_HASH },
},
};
......
......@@ -76,7 +76,7 @@ const TxInternals = () => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'tx_internal_txs',
pathParams: { id: txInfo.data?.hash },
pathParams: { hash: txInfo.data?.hash },
options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
},
......
......@@ -16,7 +16,7 @@ const TxLogs = () => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'tx_logs',
pathParams: { id: txInfo.data?.hash },
pathParams: { hash: txInfo.data?.hash },
options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
},
......
......@@ -4,6 +4,7 @@ import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { SECOND } from 'lib/consts';
import getQueryParamString from 'lib/router/getQueryParamString';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
......@@ -12,12 +13,13 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxRawTrace = () => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useApiQuery('tx_raw_trace', {
pathParams: { id: router.query.id?.toString() },
pathParams: { hash },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status),
enabled: Boolean(hash) && Boolean(txInfo.data?.status),
},
});
......
......@@ -2,7 +2,7 @@ import { Hide, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenType } from 'types/api/token';
import { SECOND } from 'lib/consts';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
......@@ -36,7 +36,7 @@ const TxTokenTransfer = () => {
const tokenTransferQuery = useQueryWithPages({
resourceName: 'tx_token_transfers',
pathParams: { id: txsInfo.data?.hash.toString() },
pathParams: { hash: txsInfo.data?.hash.toString() },
options: { enabled: Boolean(txsInfo.data?.status && txsInfo.data?.hash) },
filters: { type: typeFilter },
});
......
import { Flex, Link, Icon, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import type { TxAction, TxActionGeneral } from 'types/api/txAction';
import appConfig from 'configs/app/config';
import uniswapIcon from 'icons/uniswap.svg';
import link from 'lib/link/link';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
......@@ -97,7 +97,7 @@ const TxDetailsAction = ({ action }: Props) => {
<Flex columnGap={ 1 } rowGap={ 2 } pl={ 3 } flexDirection="column" mt={ 2 }>
{
data.ids.map((id: string) => {
const url = link('token_instance_item', { hash: data.address, id });
const url = route({ pathname: '/token/[hash]/instance/[id]', query: { hash: data.address, id } });
return (
<Flex key={ data.address + id } whiteSpace="pre-wrap">
<span>1 of </span>
......
import { Icon, GridItem, Show, Flex } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import tokenIcon from 'icons/token.svg';
import link from 'lib/link/link';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
......@@ -17,15 +17,15 @@ interface Props {
}
const TOKEN_TRANSFERS_TYPES = [
{ title: 'Tokens transferred', hint: 'List of tokens transferred in the transaction.', type: 'token_transfer' },
{ title: 'Tokens minted', hint: 'List of tokens minted in the transaction.', type: 'token_minting' },
{ title: 'Tokens burnt', hint: 'List of tokens burnt in the transaction.', type: 'token_burning' },
{ title: 'Tokens created', hint: 'List of tokens created in the transaction.', type: 'token_spawning' },
{ title: 'Tokens transferred', hint: 'List of tokens transferred in the transaction', type: 'token_transfer' },
{ title: 'Tokens minted', hint: 'List of tokens minted in the transaction', type: 'token_minting' },
{ title: 'Tokens burnt', hint: 'List of tokens burnt in the transaction', type: 'token_burning' },
{ title: 'Tokens created', hint: 'List of tokens created in the transaction', type: 'token_spawning' },
];
const VISIBLE_ITEMS_NUM = 3;
const TxDetailsTokenTransfers = ({ data, txHash }: Props) => {
const viewAllUrl = link('tx', { id: txHash }, { tab: 'token_transfers' });
const viewAllUrl = route({ pathname: '/tx/[hash]', query: { hash: txHash, tab: 'token_transfers' } });
const formattedData = data.reduce(flattenTotal, []);
const transferGroups = TOKEN_TRANSFERS_TYPES.map((group) => ({
......
......@@ -9,6 +9,7 @@ import type { Transaction } from 'types/api/transaction';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import delay from 'lib/delay';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -25,11 +26,12 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
const router = useRouter();
const queryClient = useQueryClient();
const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>();
const hash = getQueryParamString(router.query.hash);
const queryResult = useApiQuery<'tx', { status: number }>('tx', {
pathParams: { id: router.query.id?.toString() },
pathParams: { hash },
queryOptions: {
enabled: Boolean(router.query.id),
enabled: Boolean(hash),
refetchOnMount: false,
},
});
......@@ -38,10 +40,10 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => {
updateDelay && await delay(updateDelay);
queryClient.invalidateQueries({
queryKey: getResourceKey('tx', { pathParams: { id: router.query.id?.toString() } }),
queryKey: getResourceKey('tx', { pathParams: { hash } }),
});
onTxStatusUpdate?.();
}, [ onTxStatusUpdate, queryClient, router.query.id, updateDelay ]);
}, [ onTxStatusUpdate, queryClient, hash, updateDelay ]);
const handleSocketClose = React.useCallback(() => {
setSocketStatus('close');
......@@ -52,7 +54,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
}, []);
const channel = useSocketChannel({
topic: `transactions:${ router.query.id }`,
topic: `transactions:${ hash }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: isLoading || isError || data.status !== null,
......
......@@ -12,7 +12,7 @@ interface Props {
const TxAdditionalInfoContainer = ({ hash }: Props) => {
const { data, isError, isLoading } = useApiQuery('tx', {
pathParams: { id: hash },
pathParams: { hash },
queryOptions: {
refetchOnMount: false,
},
......
import { Box, Heading, Text, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import appConfig from 'configs/app/config';
import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link';
import CurrencyValue from 'ui/shared/CurrencyValue';
import LinkInternal from 'ui/shared/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
......@@ -90,7 +90,7 @@ const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => {
<Text fontWeight="600" as="span">{ tx.position }</Text>
</Box>
</Box>
<LinkInternal fontSize="sm" href={ link('tx', { id: tx.hash }) }>More details</LinkInternal>
<LinkInternal fontSize="sm" href={ route({ pathname: '/tx/[hash]', query: { hash: tx.hash } }) }>More details</LinkInternal>
</>
);
};
......
......@@ -5,6 +5,7 @@ import {
Icon,
Text,
} from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
......@@ -14,7 +15,6 @@ import rightArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg';
import getValueWithUnit from 'lib/getValueWithUnit';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -88,7 +88,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
{ showBlockInfo && tx.block !== null && (
<Box mt={ 2 }>
<Text as="span">Block </Text>
<LinkInternal href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</LinkInternal>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: tx.block.toString() } }) }>{ tx.block }</LinkInternal>
</Box>
) }
<Flex alignItems="center" height={ 6 } mt={ 6 }>
......
......@@ -10,13 +10,13 @@ import {
Hide,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import { route } from 'nextjs-routes';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import rightArrowIcon from 'icons/arrows/east.svg';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -99,7 +99,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
</Td>
{ showBlockInfo && (
<Td>
{ tx.block && <LinkInternal href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</LinkInternal> }
{ tx.block && <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: tx.block.toString() } }) }>{ tx.block }</LinkInternal> }
</Td>
) }
<Show above="xl" ssr={ false }>
......
......@@ -4897,6 +4897,14 @@ anymatch@^3.0.3:
normalize-path "^3.0.0"
picomatch "^2.0.4"
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
aproba@^1.0.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
......@@ -5161,6 +5169,11 @@ bignumber.js@^9.1.0:
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.0.tgz#8d340146107fe3a6cb8d40699643c302e8773b62"
integrity sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bind-decorator@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/bind-decorator/-/bind-decorator-1.0.11.tgz#e41bc06a1f65dd9cec476c91c5daf3978488252f"
......@@ -5222,7 +5235,7 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
braces@^3.0.2:
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
......@@ -5415,6 +5428,21 @@ char-regex@^1.0.2:
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
ci-info@^3.2.0:
version "3.6.1"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.6.1.tgz#7594f1c95cb7fdfddee7af95a13af7dbc67afdcf"
......@@ -7421,7 +7449,7 @@ get-symbol-description@^1.0.0:
call-bind "^1.0.2"
get-intrinsic "^1.1.1"
glob-parent@^5.1.2:
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
......@@ -7778,6 +7806,13 @@ is-bigint@^1.0.1:
dependencies:
has-bigints "^1.0.1"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-boolean-object@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
......@@ -7844,7 +7879,7 @@ is-generator-function@^1.0.7:
dependencies:
has-tostringtag "^1.0.0"
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
......@@ -9039,6 +9074,13 @@ next@12.2.5:
"@next/swc-win32-ia32-msvc" "12.2.5"
"@next/swc-win32-x64-msvc" "12.2.5"
nextjs-routes@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/nextjs-routes/-/nextjs-routes-1.0.8.tgz#7604fe12dd0132c5a4c61e8d4a6c00c19cf615fb"
integrity sha512-EWBrzT0VS+SGcrhEbCUGnDlEAS6tjJSiecbSA7x75pBbcW9IYkYw7JxDMNqFhiSrvtYAL8yIWFwbQ8x0Jmz7wg==
dependencies:
chokidar "^3.5.3"
node-addon-api@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
......@@ -9080,7 +9122,7 @@ node-releases@^2.0.6:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
normalize-path@^3.0.0:
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
......@@ -9386,7 +9428,7 @@ picocolors@^1.0.0:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1:
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
......@@ -9931,6 +9973,13 @@ readable-stream@^4.0.0:
events "^3.3.0"
process "^0.11.10"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
real-require@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.1.0.tgz#736ac214caa20632847b7ca8c1056a0767df9381"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment