Commit e24ac42e authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into skeletons/rest-pages

parents 05bc3818 0526882e
...@@ -6,6 +6,10 @@ NEXT_PUBLIC_APP_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_HOST__ ...@@ -6,6 +6,10 @@ NEXT_PUBLIC_APP_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_HOST__
NEXT_PUBLIC_APP_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PORT__ NEXT_PUBLIC_APP_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PORT__
NEXT_PUBLIC_AUTH_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH_URL__ NEXT_PUBLIC_AUTH_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH_URL__
# version config
NEXT_PUBLIC_GIT_COMMIT_SHA=__PLACEHOLDER_FOR_NEXT_PUBLIC_GIT_COMMIT_SHA__
NEXT_PUBLIC_GIT_TAG = __PLACEHOLDER_FOR_NEXT_PUBLIC_GIT_TAG__
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_NAME__ NEXT_PUBLIC_NETWORK_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_NAME__
NEXT_PUBLIC_NETWORK_SHORT_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_SHORT_NAME__ NEXT_PUBLIC_NETWORK_SHORT_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_SHORT_NAME__
...@@ -26,11 +30,8 @@ NEXT_PUBLIC_IS_TESTNET=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_TESTNET__ ...@@ -26,11 +30,8 @@ NEXT_PUBLIC_IS_TESTNET=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_TESTNET__
# ui config # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=__PLACEHOLDER_FOR_NEXT_PUBLIC_BLOCKSCOUT_VERSION__ NEXT_PUBLIC_BLOCKSCOUT_VERSION=__PLACEHOLDER_FOR_NEXT_PUBLIC_BLOCKSCOUT_VERSION__
NEXT_PUBLIC_FOOTER_GITHUB_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_GITHUB_LINK__
NEXT_PUBLIC_FOOTER_TWITTER_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_TWITTER_LINK__
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK__
NEXT_PUBLIC_FOOTER_STAKING_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_STAKING_LINK__
NEXT_PUBLIC_FEATURED_NETWORKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_FEATURED_NETWORKS__ NEXT_PUBLIC_FEATURED_NETWORKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_FEATURED_NETWORKS__
NEXT_PUBLIC_FOOTER_LINKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_LINKS__
NEXT_PUBLIC_NETWORK_EXPLORERS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_EXPLORERS__ NEXT_PUBLIC_NETWORK_EXPLORERS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_EXPLORERS__
NEXT_PUBLIC_OTHER_LINKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_OTHER_LINKS__ NEXT_PUBLIC_OTHER_LINKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_OTHER_LINKS__
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_CONFIG_URL__ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_CONFIG_URL__
......
...@@ -35,6 +35,8 @@ WORKDIR /app ...@@ -35,6 +35,8 @@ WORKDIR /app
# pass commit sha to the app (uses by sentry.io as release version) # pass commit sha to the app (uses by sentry.io as release version)
ARG GIT_COMMIT_SHA ARG GIT_COMMIT_SHA
ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA
# pass git tag to the app (for the footer link)
ARG GIT_TAG ARG GIT_TAG
ENV NEXT_PUBLIC_GIT_TAG=$GIT_TAG ENV NEXT_PUBLIC_GIT_TAG=$GIT_TAG
......
...@@ -96,15 +96,12 @@ const config = Object.freeze({ ...@@ -96,15 +96,12 @@ const config = Object.freeze({
rpcUrl: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_RPC_URL), rpcUrl: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_RPC_URL),
isTestnet: getEnvValue(process.env.NEXT_PUBLIC_IS_TESTNET) === 'true', isTestnet: getEnvValue(process.env.NEXT_PUBLIC_IS_TESTNET) === 'true',
}, },
footerLinks: {
github: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK),
twitter: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK),
telegram: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK),
staking: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK),
},
otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue(process.env.NEXT_PUBLIC_OTHER_LINKS)) || [], otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue(process.env.NEXT_PUBLIC_OTHER_LINKS)) || [],
featuredNetworks: getEnvValue(process.env.NEXT_PUBLIC_FEATURED_NETWORKS), featuredNetworks: getEnvValue(process.env.NEXT_PUBLIC_FEATURED_NETWORKS),
footerLinks: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_LINKS),
blockScoutVersion: getEnvValue(process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION), blockScoutVersion: getEnvValue(process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION),
frontendVersion: getEnvValue(process.env.NEXT_PUBLIC_GIT_TAG),
frontendCommit: getEnvValue(process.env.NEXT_PUBLIC_GIT_COMMIT_SHA),
isAccountSupported: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true', isAccountSupported: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true',
marketplaceConfigUrl: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL), marketplaceConfigUrl: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL),
marketplaceSubmitForm: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM), marketplaceSubmitForm: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM),
......
...@@ -7,8 +7,6 @@ NEXT_PUBLIC_APP_ENV=development ...@@ -7,8 +7,6 @@ NEXT_PUBLIC_APP_ENV=development
# ui config # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
# api config # api config
......
...@@ -5,8 +5,6 @@ NEXT_PUBLIC_APP_ENV=testing ...@@ -5,8 +5,6 @@ NEXT_PUBLIC_APP_ENV=testing
# ui config # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
# api config # api config
......
...@@ -7,10 +7,6 @@ NEXT_PUBLIC_APP_ENV=development ...@@ -7,10 +7,6 @@ NEXT_PUBLIC_APP_ENV=development
# ui config # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
......
# ui config # ui config
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address','block':'/ethereum/poa/core/block'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address','block':'/ethereum/poa/core/block'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
......
...@@ -7,16 +7,14 @@ NEXT_PUBLIC_APP_ENV=testing ...@@ -7,16 +7,14 @@ NEXT_PUBLIC_APP_ENV=testing
# ui config # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
NEXT_PUBLIC_GIT_TAG=v1.0.11
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND= NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=
NEXT_PUBLIC_FEATURED_NETWORKS= NEXT_PUBLIC_FEATURED_NETWORKS=
NEXT_PUBLIC_FOOTER_LINKS=
NEXT_PUBLIC_NETWORK_LOGO= NEXT_PUBLIC_NETWORK_LOGO=
NEXT_PUBLIC_NETWORK_LOGO_DARK= NEXT_PUBLIC_NETWORK_LOGO_DARK=
NEXT_PUBLIC_NETWORK_ICON= NEXT_PUBLIC_NETWORK_ICON=
......
...@@ -352,10 +352,6 @@ frontend: ...@@ -352,10 +352,6 @@ frontend:
environment: environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v5.1.5-beta _default: v5.1.5-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV: NEXT_PUBLIC_APP_ENV:
_default: stable _default: stable
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
......
...@@ -275,10 +275,6 @@ frontend: ...@@ -275,10 +275,6 @@ frontend:
_default: / _default: /
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v5.1.2-beta _default: v5.1.2-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV: NEXT_PUBLIC_APP_ENV:
_default: stable _default: stable
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
......
...@@ -61,10 +61,6 @@ frontend: ...@@ -61,10 +61,6 @@ frontend:
environment: environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v5.1.0-beta _default: v5.1.0-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV: NEXT_PUBLIC_APP_ENV:
_default: preview _default: preview
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
......
...@@ -57,18 +57,10 @@ frontend: ...@@ -57,18 +57,10 @@ frontend:
environment: environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v5.1.2-beta _default: v5.1.2-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV: NEXT_PUBLIC_APP_ENV:
_default: preview _default: preview
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
_default: eth_goerli _default: eth_goerli
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME: NEXT_PUBLIC_NETWORK_NAME:
_default: Göerli _default: Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
......
...@@ -30,11 +30,8 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -30,11 +30,8 @@ The app instance could be customized by passing following variables to NodeJS en
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) which contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` | | NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) which contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` |
| NEXT_PUBLIC_OTHER_LINKS | `Array<{url: string; text: string}>` | List of links for the "Other" navigation menu | - | - | `[{'url':'https://blockscout.com','text':'Blockscout'}]` | | NEXT_PUBLIC_OTHER_LINKS | `Array<{url: string; text: string}>` | List of links for the "Other" navigation menu | - | - | `[{'url':'https://blockscout.com','text':'Blockscout'}]` |
| NEXT_PUBLIC_FOOTER_LINKS | `string` | URL of configuration file (`.json` format only) which contains list of link groups to be displayed in the footer. See [below](#footer-links-configuration-properties) list of available properties for particular group | - | - | `https://example.com/footer_links_config.json` |
| NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` | Current running version of Blockscout (used to display link to release in the footer) | - | - | `v.5.1.0-beta` | NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` | Current running version of Blockscout (used to display link to release in the footer) | - | - | `v.5.1.0-beta`
| NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` | Link to Github in the footer | - | - | `https://github.com/blockscout/blockscout` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` | Link to Twitter in the footer | - | - | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` | Link to Telegram in the footer | - | - | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` | Link to staking dashboard in the footer | - | - | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains list of apps that will be shown on the marketplace page. See [below](#marketplace-app-configuration-properties) list of available properties for an app | - | - | `https://example.com/marketplace_config.json` | | NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains list of apps that will be shown on the marketplace page. See [below](#marketplace-app-configuration-properties) list of available properties for an app | - | - | `https://example.com/marketplace_config.json` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | - | - | `https://airtable.com/shrqUAcjgGJ4jU88C` | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | - | - | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` | | NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
...@@ -108,6 +105,13 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i ...@@ -108,6 +105,13 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
*Note* The url of an entity will be constructed as `<baseUrl><paths[<entity-type>]><entity-id>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/<tx-id>` *Note* The url of an entity will be constructed as `<baseUrl><paths[<entity-type>]><entity-id>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/<tx-id>`
## Footer links configuration properties
| Variable | Type| Description | Is required | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| title | `string` | Title of link group | yes | - | `Company` |
| links | `Array<{'text':string;'url':string;}>` | list of links | yes | - | `[{'text':'Homepage','url':'https://www.blockscout.com'}]` |
## App configuration ## App configuration
......
import type { ExternalProvider } from 'types/client/wallets'; import type { WindowProvider } from 'wagmi';
type CPreferences = { type CPreferences = {
zone: string; zone: string;
...@@ -8,9 +8,7 @@ type CPreferences = { ...@@ -8,9 +8,7 @@ type CPreferences = {
declare global { declare global {
export interface Window { export interface Window {
ethereum?: { ethereum?: WindowProvider;
providers?: Array<ExternalProvider>;
};
coinzilla_display: Array<CPreferences>; coinzilla_display: Array<CPreferences>;
ga?: { ga?: {
getAll: () => Array<{ get: (prop: string) => string }>; getAll: () => Array<{ get: (prop: string) => string }>;
......
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M13.76 7.444h2.88a5.76 5.76 0 0 1 0 11.52v2.52c-3.6-1.44-8.64-3.6-8.64-8.28a5.76 5.76 0 0 1 5.76-5.76Zm1.44 10.08h1.44a4.32 4.32 0 1 0 0-8.64h-2.88a4.32 4.32 0 0 0-4.32 4.32c0 2.6 1.772 4.296 5.76 6.106v-1.786Z"/>
<path fill="currentColor" d="M10.4 3.444H7.2a6.32 6.32 0 0 0-4.526 1.923A6.65 6.65 0 0 0 .8 10.008c0 1.741.674 3.41 1.874 4.642A6.32 6.32 0 0 0 7.2 16.572v2.872c4-1.64 9.6-4.102 9.6-9.436a6.65 6.65 0 0 0-1.875-4.641A6.32 6.32 0 0 0 10.4 3.444ZM8.8 14.931H7.2c-.63 0-1.255-.127-1.837-.374a4.793 4.793 0 0 1-1.557-1.068c-.446-.457-.8-1-1.04-1.597a5.033 5.033 0 0 1 0-3.768 4.933 4.933 0 0 1 1.04-1.597c.445-.457.975-.82 1.557-1.067A4.696 4.696 0 0 1 7.2 5.085h3.2a4.74 4.74 0 0 1 3.394 1.442 4.988 4.988 0 0 1 1.406 3.481c0 2.962-1.97 4.895-6.4 6.958v-2.035Z"/>
</svg>
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path d="M15 25c5.523 0 10-4.477 10-10S20.523 5 15 5 5 9.477 5 15s4.477 10 10 10Z" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M21.667 22.333a6.666 6.666 0 1 0-13.334 0" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M15 15.667a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
</svg>
This diff is collapsed.
...@@ -18,7 +18,8 @@ export default function buildUrl<R extends ResourceName>( ...@@ -18,7 +18,8 @@ export default function buildUrl<R extends ResourceName>(
const url = new URL(compile(path)(pathParams), baseUrl); const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
value && url.searchParams.append(key, String(value)); // there are some pagination params that can be null for the next page
(value || value === null) && url.searchParams.append(key, String(value));
}); });
return url.toString(); return url.toString();
......
...@@ -160,7 +160,6 @@ export const RESOURCES = { ...@@ -160,7 +160,6 @@ export const RESOURCES = {
// BLOCKS, TXS // BLOCKS, TXS
blocks: { blocks: {
path: '/api/v2/blocks', path: '/api/v2/blocks',
paginationFields: [ 'block_number' as const, 'items_count' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
block: { block: {
...@@ -170,28 +169,23 @@ export const RESOURCES = { ...@@ -170,28 +169,23 @@ export const RESOURCES = {
block_txs: { block_txs: {
path: '/api/v2/blocks/:height_or_hash/transactions', path: '/api/v2/blocks/:height_or_hash/transactions',
pathParams: [ 'height_or_hash' as const ], pathParams: [ 'height_or_hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
block_withdrawals: { block_withdrawals: {
path: '/api/v2/blocks/:height_or_hash/withdrawals', path: '/api/v2/blocks/:height_or_hash/withdrawals',
pathParams: [ 'height_or_hash' as const ], pathParams: [ 'height_or_hash' as const ],
paginationFields: [ 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
txs_validated: { txs_validated: {
path: '/api/v2/transactions', path: '/api/v2/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'filter' as const, 'index' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
}, },
txs_pending: { txs_pending: {
path: '/api/v2/transactions', path: '/api/v2/transactions',
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
}, },
txs_watchlist: { txs_watchlist: {
path: '/api/v2/transactions/watchlist', path: '/api/v2/transactions/watchlist',
paginationFields: [ 'block_number' as const, 'index' as const, 'items_count' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx: { tx: {
...@@ -201,19 +195,16 @@ export const RESOURCES = { ...@@ -201,19 +195,16 @@ export const RESOURCES = {
tx_internal_txs: { tx_internal_txs: {
path: '/api/v2/transactions/:hash/internal-transactions', path: '/api/v2/transactions/:hash/internal-transactions',
pathParams: [ 'hash' as const ], 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: [ ], filterFields: [ ],
}, },
tx_logs: { tx_logs: {
path: '/api/v2/transactions/:hash/logs', path: '/api/v2/transactions/:hash/logs',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx_token_transfers: { tx_token_transfers: {
path: '/api/v2/transactions/:hash/token-transfers', path: '/api/v2/transactions/:hash/token-transfers',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
tx_raw_trace: { tx_raw_trace: {
...@@ -226,7 +217,6 @@ export const RESOURCES = { ...@@ -226,7 +217,6 @@ export const RESOURCES = {
}, },
withdrawals: { withdrawals: {
path: '/api/v2/withdrawals', path: '/api/v2/withdrawals',
paginationFields: [ 'index' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
withdrawals_counters: { withdrawals_counters: {
...@@ -236,7 +226,6 @@ export const RESOURCES = { ...@@ -236,7 +226,6 @@ export const RESOURCES = {
// ADDRESSES // ADDRESSES
addresses: { addresses: {
path: '/api/v2/addresses/', path: '/api/v2/addresses/',
paginationFields: [ 'fetched_coin_balance' as const, 'hash' as const, 'items_count' as const ],
filterFields: [ ], filterFields: [ ],
}, },
...@@ -256,31 +245,26 @@ export const RESOURCES = { ...@@ -256,31 +245,26 @@ export const RESOURCES = {
address_txs: { address_txs: {
path: '/api/v2/addresses/:hash/transactions', path: '/api/v2/addresses/:hash/transactions',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [ 'filter' as const ], filterFields: [ 'filter' as const ],
}, },
address_internal_txs: { address_internal_txs: {
path: '/api/v2/addresses/:hash/internal-transactions', path: '/api/v2/addresses/:hash/internal-transactions',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const ], filterFields: [ 'filter' as const ],
}, },
address_token_transfers: { address_token_transfers: {
path: '/api/v2/addresses/:hash/token-transfers', path: '/api/v2/addresses/:hash/token-transfers',
pathParams: [ 'hash' as const ], 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 ], filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
}, },
address_blocks_validated: { address_blocks_validated: {
path: '/api/v2/addresses/:hash/blocks-validated', path: '/api/v2/addresses/:hash/blocks-validated',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_coin_balance: { address_coin_balance: {
path: '/api/v2/addresses/:hash/coin-balance-history', path: '/api/v2/addresses/:hash/coin-balance-history',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_coin_balance_chart: { address_coin_balance_chart: {
...@@ -290,19 +274,16 @@ export const RESOURCES = { ...@@ -290,19 +274,16 @@ export const RESOURCES = {
address_logs: { address_logs: {
path: '/api/v2/addresses/:hash/logs', path: '/api/v2/addresses/:hash/logs',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'transaction_index' as const, 'index' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_tokens: { address_tokens: {
path: '/api/v2/addresses/:hash/tokens', path: '/api/v2/addresses/:hash/tokens',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const, 'fiat_value' as const, 'id' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
address_withdrawals: { address_withdrawals: {
path: '/api/v2/addresses/:hash/withdrawals', path: '/api/v2/addresses/:hash/withdrawals',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -341,7 +322,6 @@ export const RESOURCES = { ...@@ -341,7 +322,6 @@ export const RESOURCES = {
verified_contracts: { verified_contracts: {
path: '/api/v2/smart-contracts', path: '/api/v2/smart-contracts',
paginationFields: [ 'items_count' as const, 'smart_contract_id' as const ],
filterFields: [ 'q' as const, 'filter' as const ], filterFields: [ 'q' as const, 'filter' as const ],
}, },
verified_contracts_counters: { verified_contracts_counters: {
...@@ -366,24 +346,20 @@ export const RESOURCES = { ...@@ -366,24 +346,20 @@ export const RESOURCES = {
token_holders: { token_holders: {
path: '/api/v2/tokens/:hash/holders', path: '/api/v2/tokens/:hash/holders',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [], filterFields: [],
}, },
token_transfers: { token_transfers: {
path: '/api/v2/tokens/:hash/transfers', path: '/api/v2/tokens/:hash/transfers',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
token_inventory: { token_inventory: {
path: '/api/v2/tokens/:hash/instances', path: '/api/v2/tokens/:hash/instances',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'unique_token' as const ],
filterFields: [], filterFields: [],
}, },
tokens: { tokens: {
path: '/api/v2/tokens', path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const, 'market_cap' as const ],
filterFields: [ 'q' as const, 'type' as const ], filterFields: [ 'q' as const, 'type' as const ],
}, },
...@@ -399,13 +375,11 @@ export const RESOURCES = { ...@@ -399,13 +375,11 @@ export const RESOURCES = {
token_instance_transfers: { token_instance_transfers: {
path: '/api/v2/tokens/:hash/instances/:id/transfers', path: '/api/v2/tokens/:hash/instances/:id/transfers',
pathParams: [ 'hash' as const, 'id' as const ], pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
filterFields: [], filterFields: [],
}, },
token_instance_holders: { token_instance_holders: {
path: '/api/v2/tokens/:hash/instances/:id/holders', path: '/api/v2/tokens/:hash/instances/:id/holders',
pathParams: [ 'hash' as const, 'id' as const ], pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'items_count' as const, 'token_id' as const, 'value' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -438,17 +412,6 @@ export const RESOURCES = { ...@@ -438,17 +412,6 @@ export const RESOURCES = {
// SEARCH // SEARCH
search: { search: {
path: '/api/v2/search', path: '/api/v2/search',
paginationFields: [
'address_hash' as const,
'block_hash' as const,
'holder_count' as const,
'inserted_at' as const,
'item_type' as const,
'items_count' as const,
'name' as const,
'q' as const,
'tx_hash' as const,
],
filterFields: [ 'q' ], filterFields: [ 'q' ],
}, },
search_check_redirect: { search_check_redirect: {
...@@ -463,7 +426,6 @@ export const RESOURCES = { ...@@ -463,7 +426,6 @@ export const RESOURCES = {
// L2 // L2
l2_deposits: { l2_deposits: {
path: '/api/v2/optimism/deposits', path: '/api/v2/optimism/deposits',
paginationFields: [ 'nonce' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -473,7 +435,6 @@ export const RESOURCES = { ...@@ -473,7 +435,6 @@ export const RESOURCES = {
l2_withdrawals: { l2_withdrawals: {
path: '/api/v2/optimism/withdrawals', path: '/api/v2/optimism/withdrawals',
paginationFields: [ 'nonce' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -483,7 +444,6 @@ export const RESOURCES = { ...@@ -483,7 +444,6 @@ export const RESOURCES = {
l2_output_roots: { l2_output_roots: {
path: '/api/v2/optimism/output-roots', path: '/api/v2/optimism/output-roots',
paginationFields: [ 'index' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -493,7 +453,6 @@ export const RESOURCES = { ...@@ -493,7 +453,6 @@ export const RESOURCES = {
l2_txn_batches: { l2_txn_batches: {
path: '/api/v2/optimism/txn-batches', path: '/api/v2/optimism/txn-batches',
paginationFields: [ 'block_number' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -527,10 +486,6 @@ export type ResourceFiltersKey<R extends ResourceName> = typeof RESOURCES[R] ext ...@@ -527,10 +486,6 @@ export type ResourceFiltersKey<R extends ResourceName> = typeof RESOURCES[R] ext
ArrayElement<typeof RESOURCES[R]['filterFields']> : ArrayElement<typeof RESOURCES[R]['filterFields']> :
never; never;
export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R] extends {paginationFields: Array<unknown>} ?
ArrayElement<typeof RESOURCES[R]['paginationFields']> :
never;
export const resourceKey = (x: keyof typeof RESOURCES) => x; export const resourceKey = (x: keyof typeof RESOURCES) => x;
type ResourcePathParamName<Resource extends ResourceName> = type ResourcePathParamName<Resource extends ResourceName> =
......
...@@ -10,8 +10,7 @@ export function walletConnect(): CspDev.DirectiveDescriptor { ...@@ -10,8 +10,7 @@ export function walletConnect(): CspDev.DirectiveDescriptor {
return { return {
'connect-src': [ 'connect-src': [
'*.walletconnect.com', '*.walletconnect.com',
'wss://*.bridge.walletconnect.org', 'wss://relay.walletconnect.com',
'wss://www.walletlink.org',
], ],
'img-src': [ 'img-src': [
'*.walletconnect.com', '*.walletconnect.com',
......
import React from 'react';
import appConfig from 'configs/app/config';
import isBrowser from 'lib/isBrowser';
const base = 'https://github.com/blockscout/blockscout/issues/new/';
const bodyTemplate = `*Describe your issue here.*
### Environment
* Backend Version/branch/commit: ${ appConfig.blockScoutVersion }
* Frontend Version+commit: ${ [ appConfig.frontendVersion, appConfig.frontendCommit ].filter(Boolean).join('+') }
* User Agent: __userAgent__
### Steps to reproduce
*Tell us how to reproduce this issue. ❤️ if you can push up a branch to your fork with a regression test we can run to reproduce locally.*
### Expected behaviour
*Tell us what should happen.*
### Actual behaviour
*Tell us what happens instead.*`;
const labels = 'new UI';
const title = `${ appConfig.network.name }: <Issue Title>`;
export default function useIssueUrl() {
const [ userAgent, setUserAgent ] = React.useState('');
const isInBrowser = isBrowser();
React.useEffect(() => {
if (isInBrowser) {
setUserAgent(window.navigator.userAgent);
}
}, [ isInBrowser ]);
const params = new URLSearchParams({ labels, title, body: bodyTemplate.replace('__userAgent__', userAgent) });
return base + '?' + params.toString();
}
...@@ -15,7 +15,6 @@ import globeIcon from 'icons/globe-b.svg'; ...@@ -15,7 +15,6 @@ import globeIcon from 'icons/globe-b.svg';
import graphQLIcon from 'icons/graphQL.svg'; import graphQLIcon from 'icons/graphQL.svg';
import outputRootsIcon from 'icons/output_roots.svg'; import outputRootsIcon from 'icons/output_roots.svg';
import privateTagIcon from 'icons/privattags.svg'; import privateTagIcon from 'icons/privattags.svg';
import profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg'; import publicTagIcon from 'icons/publictags.svg';
import apiDocsIcon from 'icons/restAPI.svg'; import apiDocsIcon from 'icons/restAPI.svg';
import rpcIcon from 'icons/RPC.svg'; import rpcIcon from 'icons/RPC.svg';
...@@ -27,6 +26,7 @@ import txnBatchIcon from 'icons/txn_batches.svg'; ...@@ -27,6 +26,7 @@ import txnBatchIcon from 'icons/txn_batches.svg';
import verifiedIcon from 'icons/verified.svg'; import verifiedIcon from 'icons/verified.svg';
import watchlistIcon from 'icons/watchlist.svg'; import watchlistIcon from 'icons/watchlist.svg';
import { rightLineArrow } from 'lib/html-entities'; import { rightLineArrow } from 'lib/html-entities';
import UserAvatar from 'ui/shared/UserAvatar';
interface ReturnType { interface ReturnType {
mainNavItems: Array<NavItem | NavGroupItem>; mainNavItems: Array<NavItem | NavGroupItem>;
...@@ -213,7 +213,7 @@ export default function useNavItems(): ReturnType { ...@@ -213,7 +213,7 @@ export default function useNavItems(): ReturnType {
const profileItem = { const profileItem = {
text: 'My profile', text: 'My profile',
nextRoute: { pathname: '/auth/profile' as const }, nextRoute: { pathname: '/auth/profile' as const },
icon: profileIcon, iconComponent: UserAvatar,
isActive: pathname === '/auth/profile', isActive: pathname === '/auth/profile',
}; };
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import difference from 'lodash/difference';
import mapValues from 'lodash/mapValues';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { PaginatedResources, PaginatedResponse, PaginationFilters } from 'lib/api/resources'; import type { PaginatedResources, PaginationFilters } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources'; import { RESOURCES } from 'lib/api/resources';
import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery'; import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
interface Params<Resource extends PaginatedResources> { interface Params<Resource extends PaginatedResources> {
resourceName: Resource; resourceName: Resource;
...@@ -20,6 +18,18 @@ interface Params<Resource extends PaginatedResources> { ...@@ -20,6 +18,18 @@ interface Params<Resource extends PaginatedResources> {
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
} }
type NextPageParams = Record<string, unknown>;
function getPaginationParamsFromQuery(queryString: string | Array<string> | undefined) {
if (queryString) {
try {
return JSON.parse(decodeURIComponent(getQueryParamString(queryString))) as NextPageParams;
} catch (error) {}
}
return {};
}
export default function useQueryWithPages<Resource extends PaginatedResources>({ export default function useQueryWithPages<Resource extends PaginatedResources>({
resourceName, resourceName,
filters, filters,
...@@ -31,14 +41,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -31,14 +41,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
type NextPageParams = {
[K in keyof PaginatedResponse<Resource>['next_page_params']]: string;
}
const currPageParams = mapValues(pick(router.query, resource.paginationFields), (value) => value?.toString()) as NextPageParams;
const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1); const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1);
const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({ const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({
[page]: currPageParams, [page]: getPaginationParamsFromQuery(router.query.next_page_params),
}); });
const [ hasPagination, setHasPagination ] = React.useState(page > 1); const [ hasPagination, setHasPagination ] = React.useState(page > 1);
...@@ -65,21 +70,21 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -65,21 +70,21 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
// we hide next page button if no next_page_params // we hide next page button if no next_page_params
return; return;
} }
const nextPageParams = data.next_page_params;
setPageParams((prev) => ({ setPageParams((prev) => ({
...prev, ...prev,
[page + 1]: mapValues(nextPageParams, (value) => String(value)) as NextPageParams, [page + 1]: data.next_page_params as NextPageParams,
})); }));
setPage(prev => prev + 1); setPage(prev => prev + 1);
const nextPageQuery = { ...router.query }; const nextPageQuery = {
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = String(val)); ...router.query,
nextPageQuery.page = String(page + 1); page: String(page + 1),
setHasPagination(true); next_page_params: encodeURIComponent(JSON.stringify(data.next_page_params)),
};
setHasPagination(true);
scrollToTop(); scrollToTop();
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }); router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true });
}, [ data?.next_page_params, page, router, scrollToTop ]); }, [ data?.next_page_params, page, router, scrollToTop ]);
...@@ -88,7 +93,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -88,7 +93,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
// we dont have pagination params for the first page // we dont have pagination params for the first page
let nextPageQuery: typeof router.query = { ...router.query }; let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) { if (page === 2) {
nextPageQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page'); nextPageQuery = omit(router.query, [ 'next_page_params', 'page' ]);
canGoBackwards.current = true; canGoBackwards.current = true;
} else { } else {
const nextPageParams = pageParams[page - 1]; const nextPageParams = pageParams[page - 1];
...@@ -103,13 +108,13 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -103,13 +108,13 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] }); page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
}); });
setHasPagination(true); setHasPagination(true);
}, [ router, page, resource.paginationFields, resource.filterFields, pageParams, scrollToTop, queryClient, resourceName ]); }, [ router, page, pageParams, scrollToTop, queryClient, resourceName ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop(); scrollToTop();
const nextRouterQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page'); const nextRouterQuery = omit(router.query, [ 'next_page_params', 'page' ]);
router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => { router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
setPage(1); setPage(1);
...@@ -123,10 +128,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -123,10 +128,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}); });
setHasPagination(true); setHasPagination(true);
}, [ queryClient, resourceName, router, resource.paginationFields, resource.filterFields, scrollToTop ]); }, [ queryClient, resourceName, router, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => { const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => {
const newQuery = omit<typeof router.query>(router.query, resource.paginationFields, 'page', resource.filterFields); const newQuery = omit<typeof router.query>(router.query, 'next_page_params', 'page', resource.filterFields);
if (newFilters) { if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => { Object.entries(newFilters).forEach(([ key, value ]) => {
if (value && value.length) { if (value && value.length) {
...@@ -147,7 +152,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -147,7 +152,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
setPage(1); setPage(1);
setPageParams({}); setPageParams({});
}); });
}, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]); }, [ router, resource.filterFields, scrollToTop ]);
const nextPageParams = data?.next_page_params; const nextPageParams = data?.next_page_params;
......
...@@ -49,9 +49,9 @@ export namespace SocketMessage { ...@@ -49,9 +49,9 @@ export namespace SocketMessage {
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>; SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>; export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>;
export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>; export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>;
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transactions: Array<Transaction> }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transactions: Array<Transaction> }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfers: Array<TokenTransfer> }>;
export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record<string, never>>; export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record<string, never>>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>; export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>; export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>;
......
import React from 'react'; import React from 'react';
import type { WindowProvider } from 'wagmi';
import type { ExternalProvider } from 'types/client/wallets'; import 'wagmi/window';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
export default function useProvider() { export default function useProvider() {
const [ provider, setProvider ] = React.useState<ExternalProvider>(); const [ provider, setProvider ] = React.useState<WindowProvider>();
React.useEffect(() => { React.useEffect(() => {
if (!('ethereum' in window)) { if (!('ethereum' in window)) {
......
...@@ -5,13 +5,11 @@ import metamaskIcon from 'icons/wallets/metamask.svg'; ...@@ -5,13 +5,11 @@ import metamaskIcon from 'icons/wallets/metamask.svg';
export const WALLETS_INFO: Record<WalletType, WalletInfo> = { export const WALLETS_INFO: Record<WalletType, WalletInfo> = {
metamask: { metamask: {
add_token_text: 'Add token to MetaMask', name: 'MetaMask',
add_network_text: 'Add network to MetaMask',
icon: metamaskIcon, icon: metamaskIcon,
}, },
coinbase: { coinbase: {
add_token_text: 'Add token to Coinbase Wallet', name: 'Coinbase Wallet',
add_network_text: 'Add network to Coinbase Wallet',
icon: coinbaseIcon, icon: coinbaseIcon,
}, },
}; };
import type { CustomLinksGroup } from 'types/footerLinks';
export const FOOTER_LINKS: Array<CustomLinksGroup> = [
{
title: 'Company',
links: [
{
text: 'Advertise',
url: 'https://coinzilla.com/',
},
{
text: 'Staking',
url: '',
},
{
text: 'Contact us',
url: '',
},
{
text: 'Brand assets',
url: '',
},
{
text: 'Term of service',
url: '',
},
],
},
{
title: 'Community',
links: [
{
text: 'API docs',
url: '',
},
{
text: 'Knowledge base',
url: '',
},
{
text: 'Network status',
url: '',
},
{
text: 'Learn Alphabet',
url: '',
},
],
},
{
title: 'Product',
links: [
{
text: 'Stake Alphabet',
url: '',
},
{
text: 'Build token',
url: '',
},
{
text: 'Build DAPPS',
url: '',
},
{
text: 'NFT marketplace',
url: '',
},
{
text: 'Become validator',
url: '',
},
],
},
];
...@@ -251,3 +251,26 @@ export const l2tx: Transaction = { ...@@ -251,3 +251,26 @@ export const l2tx: Transaction = {
l1_gas_used: '17060', l1_gas_used: '17060',
l1_fee: '1584574188135760', l1_fee: '1584574188135760',
}; };
export const base2 = {
...base,
hash: '0x02d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
from: {
...base.from,
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
},
};
export const base3 = {
...base,
hash: '0x12d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
from: {
...base.from,
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
},
};
export const base4 = {
...base,
hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
};
...@@ -28,6 +28,7 @@ const moduleExports = withTM({ ...@@ -28,6 +28,7 @@ const moduleExports = withTM({
use: [ '@svgr/webpack' ], use: [ '@svgr/webpack' ],
}, },
); );
config.resolve.fallback = { fs: false, net: false, tls: false };
return config; return config;
}, },
......
...@@ -43,14 +43,13 @@ ...@@ -43,14 +43,13 @@
"@tanstack/react-query-devtools": "^4.0.10", "@tanstack/react-query-devtools": "^4.0.10",
"@types/papaparse": "^5.3.5", "@types/papaparse": "^5.3.5",
"@types/react-scroll": "^1.8.4", "@types/react-scroll": "^1.8.4",
"@web3modal/ethereum": "^2.0.0-rc.2", "@web3modal/ethereum": "^2.4.1",
"@web3modal/react": "^2.0.0-rc.2", "@web3modal/react": "^2.4.1",
"bignumber.js": "^9.1.0", "bignumber.js": "^9.1.0",
"chakra-react-select": "^4.4.3", "chakra-react-select": "^4.4.3",
"d3": "^7.6.1", "d3": "^7.6.1",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"ethers": "^5.7.2",
"framer-motion": "^6.5.1", "framer-motion": "^6.5.1",
"graphiql": "^2.2.0", "graphiql": "^2.2.0",
"graphql": "^16.6.0", "graphql": "^16.6.0",
...@@ -78,7 +77,8 @@ ...@@ -78,7 +77,8 @@
"react-scroll": "^1.8.7", "react-scroll": "^1.8.7",
"swagger-ui-react": "^4.15.5", "swagger-ui-react": "^4.15.5",
"use-font-face-observer": "^1.2.1", "use-font-face-observer": "^1.2.1",
"wagmi": "^0.10.6" "viem": "^0.3.39",
"wagmi": "^1.0.9"
}, },
"devDependencies": { "devDependencies": {
"@playwright/experimental-ct-react": "1.32.3", "@playwright/experimental-ct-react": "1.32.3",
...@@ -100,6 +100,7 @@ ...@@ -100,6 +100,7 @@
"@types/swagger-ui-react": "^4.11.0", "@types/swagger-ui-react": "^4.11.0",
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.53.0", "@typescript-eslint/eslint-plugin": "^5.53.0",
"@vitejs/plugin-react": "^4.0.0",
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"dotenv-cli": "^6.0.0", "dotenv-cli": "^6.0.0",
"eslint": "^8.32.0", "eslint": "^8.32.0",
......
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { providers } from 'ethers'; import { w3mProvider } from '@web3modal/ethereum';
import React from 'react'; import React from 'react';
import { createClient, WagmiConfig } from 'wagmi'; import { configureChains, createConfig, WagmiConfig } from 'wagmi';
import { mainnet } from 'wagmi/chains'; import { mainnet } from 'wagmi/chains';
import { MockConnector } from 'wagmi/connectors/mock';
import { AppContextProvider } from 'lib/appContext'; import { AppContextProvider } from 'lib/appContext';
import type { Props as PageProps } from 'lib/next/getServerSideProps'; import type { Props as PageProps } from 'lib/next/getServerSideProps';
...@@ -28,25 +27,17 @@ const defaultAppContext = { ...@@ -28,25 +27,17 @@ const defaultAppContext = {
}; };
// >>> Web3 stuff // >>> Web3 stuff
const provider = new providers.JsonRpcProvider( const { publicClient } = configureChains(
'http://localhost:8545', [ mainnet ],
{ [
name: 'POA', w3mProvider({ projectId: '' }),
chainId: 99, ],
},
); );
const connector = new MockConnector({ const wagmiConfig = createConfig({
chains: [ mainnet ], autoConnect: false,
options: { connectors: [ ],
signer: provider.getSigner(), publicClient,
},
});
const wagmiClient = createClient({
autoConnect: true,
connectors: [ connector ],
provider,
}); });
// <<<< // <<<<
...@@ -65,7 +56,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props ...@@ -65,7 +56,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }> <SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
<AppContextProvider { ...appContext }> <AppContextProvider { ...appContext }>
<WagmiConfig client={ wagmiClient }> <WagmiConfig config={ wagmiConfig }>
{ children } { children }
</WagmiConfig> </WagmiConfig>
</AppContextProvider> </AppContextProvider>
......
...@@ -5,6 +5,8 @@ import { WebSocketServer } from 'ws'; ...@@ -5,6 +5,8 @@ import { WebSocketServer } from 'ws';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract'; import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction';
type ReturnType = () => Promise<WebSocket>; type ReturnType = () => Promise<WebSocket>;
...@@ -58,11 +60,14 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => { ...@@ -58,11 +60,14 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => {
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'coin_balance', payload: { coin_balance: AddressCoinBalanceHistoryItem }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'coin_balance', payload: { coin_balance: AddressCoinBalanceHistoryItem }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transactions: Array<Transaction> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transactions: Array<Transaction> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'total_supply', payload: { total_supply: number}): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'total_supply', payload: { total_supply: number}): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'changed_bytecode', payload: Record<string, never>): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'changed_bytecode', payload: Record<string, never>): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_transfer', payload: { token_transfers: Array<TokenTransfer> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void { export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([ socket.send(JSON.stringify([
...channel, ...channel,
......
...@@ -18,7 +18,7 @@ const variantPrimary = defineStyle((props) => { ...@@ -18,7 +18,7 @@ const variantPrimary = defineStyle((props) => {
const variantSecondary = defineStyle((props) => { const variantSecondary = defineStyle((props) => {
return { return {
color: mode('gray.500', 'gray.500')(props), color: mode('gray.600', 'gray.500')(props),
_hover: { _hover: {
color: mode('gray.600', 'gray.400')(props), color: mode('gray.600', 'gray.400')(props),
}, },
......
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
import type React from 'react';
type NavIconOrComponent = {
icon?: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
} | {
iconComponent?: React.FC<{size?: number}>;
};
type NavItemCommon = { type NavItemCommon = {
text: string; text: string;
icon?: React.FunctionComponent<React.SVGAttributes<SVGElement>>; } & NavIconOrComponent;
}
export type NavItemInternal = NavItemCommon & { export type NavItemInternal = NavItemCommon & {
nextRoute: Route; nextRoute: Route;
......
import type { providers } from 'ethers';
export type WalletType = 'metamask' | 'coinbase'; export type WalletType = 'metamask' | 'coinbase';
export interface WalletInfo { export interface WalletInfo {
add_token_text: string; name: string;
add_network_text: string;
icon: React.ElementType; icon: React.ElementType;
} }
export interface ExternalProvider extends providers.ExternalProvider {
isCoinbaseWallet?: boolean;
// have to patch ethers here, since params could be not only an array
// eslint-disable-next-line @typescript-eslint/no-explicit-any
request?: (request: { method: string; params?: any }) => Promise<any>;
}
type CustomLink = {
text: string;
url: string;
}
export type CustomLinksGroup = {
title: string;
links: Array<CustomLink>;
}
export type ArrayElement<ArrayType extends Array<unknown>> = export type ArrayElement<ArrType> = ArrType extends ReadonlyArray<infer ElementType>
ArrayType extends Array<(infer ElementType)> ? ElementType : never; ? ElementType
: never;
export type ExcludeNull<T> = T extends null ? never : T; export type ExcludeNull<T> = T extends null ? never : T;
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { WindowProvider } from 'wagmi';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
...@@ -74,7 +75,7 @@ test('token', async({ mount, page }) => { ...@@ -74,7 +75,7 @@ test('token', async({ mount, page }) => {
await page.evaluate(() => { await page.evaluate(() => {
window.ethereum = { window.ethereum = {
providers: [ { isMetaMask: true } ], providers: [ { isMetaMask: true } ],
}; }as WindowProvider;
}); });
const component = await mount( const component = await mount(
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect, devices } from '@playwright/experimental-ct-react'; import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { erc1155A } from 'mocks/tokens/tokenTransfer'; import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokenTransfers from './AddressTokenTransfers'; import AddressTokenTransfers from './AddressTokenTransfers';
const API_URL = buildApiUrl('address_token_transfers', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) + const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const API_URL = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859'; '?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: CURRENT_ADDRESS, token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
}, },
}; };
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('with token filter and pagination', async({ mount, page }) => { test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
})); }));
const component = await mount( const component = await mount(
...@@ -37,7 +48,7 @@ test('with token filter and pagination', async({ mount, page }) => { ...@@ -37,7 +48,7 @@ test('with token filter and pagination', async({ mount, page }) => {
test('with token filter and no pagination', async({ mount, page }) => { test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ] }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ] }),
})); }));
const component = await mount( const component = await mount(
...@@ -57,7 +68,7 @@ test.describe('mobile', () => { ...@@ -57,7 +68,7 @@ test.describe('mobile', () => {
test('with token filter and pagination', async({ mount, page }) => { test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
})); }));
const component = await mount( const component = await mount(
...@@ -74,7 +85,7 @@ test.describe('mobile', () => { ...@@ -74,7 +85,7 @@ test.describe('mobile', () => {
test('with token filter and no pagination', async({ mount, page }) => { test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ] }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ] }),
})); }));
const component = await mount( const component = await mount(
...@@ -88,3 +99,160 @@ test.describe('mobile', () => { ...@@ -88,3 +99,160 @@ test.describe('mobile', () => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
}); });
test.describe('socket', () => {
test('without overload', async({ mount, page, createSocket }) => {
const hooksConfigNoToken = {
router: {
query: { hash: CURRENT_ADDRESS },
},
};
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=';
await page.route(API_URL_NO_TOKEN, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig: hooksConfigNoToken },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc1155C ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(4);
});
test('with overload', async({ mount, page, createSocket }) => {
const hooksConfigNoToken = {
router: {
query: { hash: CURRENT_ADDRESS },
},
};
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=';
await page.route(API_URL_NO_TOKEN, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig: hooksConfigNoToken },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc1155C ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('1 ')).toBe(true);
});
test('without overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=ERC-1155';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc20 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
});
test('with overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=ERC-1155';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(
socket,
channel,
'token_transfer',
{ token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc20, tokenTransferMock.erc1155C, tokenTransferMock.erc721 ] },
);
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('1 ')).toBe(true);
});
});
...@@ -63,7 +63,13 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: ...@@ -63,7 +63,13 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?:
return true; return true;
}; };
const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
// for tests only
overloadCount?: number;
}
const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -117,11 +123,26 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -117,11 +123,26 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => { const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
setSocketAlert(''); setSocketAlert('');
if (data?.items && data.items.length >= OVERLOAD_COUNT) { const newItems: Array<TokenTransfer> = [];
if (matchFilters(filters, payload.token_transfer, currentAddress)) { let newCount = 0;
setNewItemsCount(prev => prev + 1);
payload.token_transfers.forEach(transfer => {
if (data?.items && data.items.length + newItems.length >= overloadCount) {
if (matchFilters(filters, transfer, currentAddress)) {
newCount++;
}
} else {
if (matchFilters(filters, transfer, currentAddress)) {
newItems.push(transfer);
}
} }
} else { });
if (newCount > 0) {
setNewItemsCount(prev => prev + newCount);
}
if (newItems.length > 0) {
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }), getResourceKey('address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }),
(prevData: AddressTokenTransferResponse | undefined) => { (prevData: AddressTokenTransferResponse | undefined) => {
...@@ -129,19 +150,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -129,19 +150,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
return; return;
} }
if (!matchFilters(filters, payload.token_transfer, currentAddress)) {
return prevData;
}
return { return {
...prevData, ...prevData,
items: [ items: [
payload.token_transfer, ...newItems,
...prevData.items, ...prevData.items,
], ],
}; };
}); },
);
} }
}; };
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { base as txMock } from 'mocks/txs/tx'; import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTxs from './AddressTxs'; import AddressTxs from './AddressTxs';
const API_URL = buildApiUrl('address_txs', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }); const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const API_URL = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: CURRENT_ADDRESS },
}, },
}; };
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('address txs +@mobile +@desktop-xl', async({ mount, page }) => { test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ txMock, txMock ], next_page_params: { block: 1 } }), body: JSON.stringify({ items: [ txMock.base, txMock.base ], next_page_params: { block: 1 } }),
})); }));
const component = await mount( const component = await mount(
...@@ -32,3 +43,167 @@ test('address txs +@mobile +@desktop-xl', async({ mount, page }) => { ...@@ -32,3 +43,167 @@ test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test.describe('socket', () => {
test('without overload', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(4);
});
test('with update', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.pending ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base, txMock.base2 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
});
test('with overload', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base3, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('2 ')).toBe(true);
});
test('without overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, filter: 'from' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS }) + '?filter=from';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
});
test('with overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, filter: 'from' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS }) + '?filter=from';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base3, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('1 ')).toBe(true);
});
});
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address'; import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import type { Transaction } from 'types/api/transaction';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
...@@ -26,7 +27,27 @@ const OVERLOAD_COUNT = 75; ...@@ -26,7 +27,27 @@ const OVERLOAD_COUNT = 75;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues); const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, address?: string) => {
if (!filterValue) {
return true;
}
if (filterValue === 'from') {
return transaction.from.hash === address;
}
if (filterValue === 'to') {
return transaction.to?.hash === address;
}
};
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
// for tests only
overloadCount?: number;
}
const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -62,16 +83,6 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -62,16 +83,6 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => { const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => {
setSocketAlert(''); setSocketAlert('');
if (addressTxsQuery.data?.items && addressTxsQuery.data.items.length >= OVERLOAD_COUNT) {
if (
!filterValue ||
(filterValue === 'from' && payload.transaction.from.hash === currentAddress) ||
(filterValue === 'to' && payload.transaction.to?.hash === currentAddress)
) {
setNewItemsCount(prev => prev + 1);
}
}
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }), getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }),
(prevData: AddressTransactionsResponse | undefined) => { (prevData: AddressTransactionsResponse | undefined) => {
...@@ -79,30 +90,33 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -79,30 +90,33 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
return; return;
} }
const currIndex = prevData.items.findIndex((tx) => tx.hash === payload.transaction.hash); const newItems: Array<Transaction> = [];
let newCount = 0;
if (currIndex > -1) {
prevData.items[currIndex] = payload.transaction; payload.transactions.forEach(tx => {
return prevData; const currIndex = prevData.items.findIndex((item) => item.hash === tx.hash);
}
if (currIndex > -1) {
if (prevData.items.length >= OVERLOAD_COUNT) { prevData.items[currIndex] = tx;
return prevData; } else {
} if (matchFilter(filterValue, tx, currentAddress)) {
if (newItems.length + prevData.items.length >= overloadCount) {
if (filterValue) { newCount++;
if ( } else {
(filterValue === 'from' && payload.transaction.from.hash !== currentAddress) || newItems.push(tx);
(filterValue === 'to' && payload.transaction.to?.hash !== currentAddress) }
) { }
return prevData;
} }
});
if (newCount > 0) {
setNewItemsCount(prev => prev + newCount);
} }
return { return {
...prevData, ...prevData,
items: [ items: [
payload.transaction, ...newItems,
...prevData.items, ...prevData.items,
], ],
}; };
......
...@@ -7,7 +7,6 @@ import { useForm } from 'react-hook-form'; ...@@ -7,7 +7,6 @@ import { useForm } from 'react-hook-form';
import type { MethodFormFields, ContractMethodCallResult } from './types'; import type { MethodFormFields, ContractMethodCallResult } from './types';
import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract'; import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract';
import appConfig from 'configs/app/config';
import arrowIcon from 'icons/arrows/down-right.svg'; import arrowIcon from 'icons/arrows/down-right.svg';
import ContractMethodField from './ContractMethodField'; import ContractMethodField from './ContractMethodField';
...@@ -25,7 +24,7 @@ interface Props<T extends SmartContractMethod> { ...@@ -25,7 +24,7 @@ interface Props<T extends SmartContractMethod> {
isWrite?: boolean; isWrite?: boolean;
} }
const getFieldName = (name: string, index: number): string => name || String(index); const getFieldName = (name: string | undefined, index: number): string => name || String(index);
const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, string], [ b ]: [string, string]): 1 | -1 | 0 => { const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, string], [ b ]: [string, string]): 1 | -1 | 0 => {
const fieldNames = data.map(({ name }, index) => getFieldName(name, index)); const fieldNames = data.map(({ name }, index) => getFieldName(name, index));
...@@ -67,14 +66,14 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit, ...@@ -67,14 +66,14 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
const [ result, setResult ] = React.useState<ContractMethodCallResult<T>>(); const [ result, setResult ] = React.useState<ContractMethodCallResult<T>>();
const [ isLoading, setLoading ] = React.useState(false); const [ isLoading, setLoading ] = React.useState(false);
const inputs = React.useMemo(() => { const inputs: Array<SmartContractMethodInput> = React.useMemo(() => {
return [ return [
...('inputs' in data ? data.inputs : []), ...('inputs' in data ? data.inputs : []),
...(data.stateMutability === 'payable' ? [ { ...('stateMutability' in data && data.stateMutability === 'payable' ? [ {
name: 'value', name: 'value',
type: appConfig.network.currency.symbol, type: 'uint256' as const,
internalType: appConfig.network.currency.symbol, internalType: 'uint256' as const,
} as SmartContractMethodInput ] : []), } ] : []),
]; ];
}, [ data ]); }, [ data ]);
......
import React from 'react'; import React from 'react';
import { useAccount, useSigner, useNetwork, useSwitchNetwork } from 'wagmi'; import { useAccount, useWalletClient, useNetwork, useSwitchNetwork } from 'wagmi';
import type { SmartContractWriteMethod } from 'types/api/contract'; import type { SmartContractWriteMethod } from 'types/api/contract';
...@@ -24,7 +24,7 @@ interface Props { ...@@ -24,7 +24,7 @@ interface Props {
} }
const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const { data: signer } = useSigner(); const { data: walletClient } = useWalletClient();
const { isConnected } = useAccount(); const { isConnected } = useAccount();
const { chain } = useNetwork(); const { chain } = useNetwork();
const { switchNetworkAsync } = useSwitchNetwork(); const { switchNetworkAsync } = useSwitchNetwork();
...@@ -39,17 +39,17 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -39,17 +39,17 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
}, },
}); });
const { contract, proxy, custom } = useContractContext(); const { contractInfo, customInfo, proxyInfo } = useContractContext();
const _contract = (() => { const abi = (() => {
if (isProxy) { if (isProxy) {
return proxy; return proxyInfo?.abi;
} }
if (isCustomAbi) { if (isCustomAbi) {
return custom; return customInfo?.abi;
} }
return contract; return contractInfo?.abi;
})(); })();
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<unknown>>) => { const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<unknown>>) => {
...@@ -61,30 +61,37 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -61,30 +61,37 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
await switchNetworkAsync?.(Number(config.network.id)); await switchNetworkAsync?.(Number(config.network.id));
} }
if (!_contract) { if (!abi) {
throw new Error('Something went wrong. Try again later.'); throw new Error('Something went wrong. Try again later.');
} }
if (item.type === 'receive') { if (item.type === 'receive' || item.type === 'fallback') {
const value = args[0] ? getNativeCoinValue(args[0]) : '0'; const value = getNativeCoinValue(args[0]);
const result = await signer?.sendTransaction({ const hash = await walletClient?.sendTransaction({
to: addressHash, to: addressHash as `0x${ string }` | undefined,
value, value,
}); });
return { hash: result?.hash as string }; return { hash };
} }
const _args = item.stateMutability === 'payable' ? args.slice(0, -1) : args; const _args = 'stateMutability' in item && item.stateMutability === 'payable' ? args.slice(0, -1) : args;
const value = item.stateMutability === 'payable' ? getNativeCoinValue(args[args.length - 1]) : undefined; const value = 'stateMutability' in item && item.stateMutability === 'payable' ? getNativeCoinValue(args[args.length - 1]) : undefined;
const methodName = item.type === 'fallback' ? 'fallback' : item.name; const methodName = item.name;
const result = await _contract[methodName](..._args, { if (!methodName) {
gasLimit: 100_000, throw new Error('Method name is not defined');
value, }
const hash = await walletClient?.writeContract({
args: _args,
abi: abi,
functionName: methodName,
address: addressHash as `0x${ string }`,
value: value as undefined,
}); });
return { hash: result.hash as string }; return { hash };
}, [ _contract, addressHash, chain, isConnected, signer, switchNetworkAsync ]); }, [ isConnected, chain, abi, walletClient, addressHash, switchNetworkAsync ]);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => { const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
return ( return (
......
...@@ -12,7 +12,7 @@ test('loading', async({ mount }) => { ...@@ -12,7 +12,7 @@ test('loading', async({ mount }) => {
error: null, error: null,
}, },
result: { result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29', hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
}, },
onSettle: () => {}, onSettle: () => {},
}; };
...@@ -33,7 +33,7 @@ test('success', async({ mount }) => { ...@@ -33,7 +33,7 @@ test('success', async({ mount }) => {
error: null, error: null,
}, },
result: { result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29', hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
}, },
onSettle: () => {}, onSettle: () => {},
}; };
...@@ -57,7 +57,7 @@ test('error +@mobile', async({ mount }) => { ...@@ -57,7 +57,7 @@ test('error +@mobile', async({ mount }) => {
} as Error, } as Error,
}, },
result: { result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29', hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
}, },
onSettle: () => {}, onSettle: () => {},
}; };
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import type { Contract } from 'ethers';
import React from 'react'; import React from 'react';
import { useContract, useProvider, useSigner } from 'wagmi';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import type { SmartContract } from 'types/api/contract';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
...@@ -13,20 +12,18 @@ type ProviderProps = { ...@@ -13,20 +12,18 @@ type ProviderProps = {
} }
type TContractContext = { type TContractContext = {
contract: Contract | null; contractInfo: SmartContract | undefined;
proxy: Contract | null; proxyInfo: SmartContract | undefined;
custom: Contract | null; customInfo: SmartContract | undefined;
}; };
const ContractContext = React.createContext<TContractContext>({ const ContractContext = React.createContext<TContractContext>({
contract: null, proxyInfo: undefined,
proxy: null, contractInfo: undefined,
custom: null, customInfo: undefined,
}); });
export function ContractContextProvider({ addressHash, children }: ProviderProps) { export function ContractContextProvider({ addressHash, children }: ProviderProps) {
const provider = useProvider();
const { data: signer } = useSigner();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: contractInfo } = useApiQuery('contract', { const { data: contractInfo } = useApiQuery('contract', {
...@@ -58,27 +55,11 @@ export function ContractContextProvider({ addressHash, children }: ProviderProps ...@@ -58,27 +55,11 @@ export function ContractContextProvider({ addressHash, children }: ProviderProps
}, },
}); });
const contract = useContract({
address: addressHash,
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(() => ({ const value = React.useMemo(() => ({
contract, proxyInfo,
proxy, contractInfo,
custom, customInfo,
}), [ contract, proxy, custom ]); } as TContractContext), [ proxyInfo, contractInfo, customInfo ]);
return ( return (
<ContractContext.Provider value={ value }> <ContractContext.Provider value={ value }>
......
...@@ -6,7 +6,7 @@ export type MethodFormFields = Record<string, string>; ...@@ -6,7 +6,7 @@ export type MethodFormFields = Record<string, string>;
export type ContractMethodReadResult = SmartContractQueryMethodRead | ResourceError; export type ContractMethodReadResult = SmartContractQueryMethodRead | ResourceError;
export type ContractMethodWriteResult = Error | { hash: string } | undefined; export type ContractMethodWriteResult = Error | { hash: `0x${ string }` | undefined } | undefined;
export type ContractMethodCallResult<T extends SmartContractMethod> = export type ContractMethodCallResult<T extends SmartContractMethod> =
T extends { method_id: string } ? ContractMethodReadResult : ContractMethodWriteResult; T extends { method_id: string } ? ContractMethodReadResult : ContractMethodWriteResult;
import BigNumber from 'bignumber.js';
import config from 'configs/app/config'; import config from 'configs/app/config';
export const getNativeCoinValue = (value: string | Array<unknown>) => { export const getNativeCoinValue = (value: string | Array<unknown>) => {
const _value = Array.isArray(value) ? value[0] : value; const _value = Array.isArray(value) ? value[0] : value;
if (typeof _value !== 'string') { if (typeof _value !== 'string') {
return '0'; return BigInt(0);
} }
return BigNumber(_value).times(10 ** config.network.currency.decimals).toString(); return BigInt(Number(_value) * 10 ** config.network.currency.decimals);
}; };
export const addZeroesAllowed = (valueType: string) => { export const addZeroesAllowed = (valueType: string) => {
......
...@@ -187,7 +187,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -187,7 +187,7 @@ const BlockDetails = ({ query }: Props) => {
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
> >
<Skeleton isLoaded={ !isPlaceholderData }> <Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: heightOrHash, tab: 'withdrawals' } }) }> <LinkInternal href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'withdrawals' } }) }>
{ data.withdrawals_count } withdrawal{ data.withdrawals_count === 1 ? '' : 's' } { data.withdrawals_count } withdrawal{ data.withdrawals_count === 1 ? '' : 's' }
</LinkInternal> </LinkInternal>
</Skeleton> </Skeleton>
......
import { Skeleton, Box, Icon } from '@chakra-ui/react'; import { Box, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -124,7 +124,7 @@ const AddressPageContent = () => { ...@@ -124,7 +124,7 @@ const AddressPageContent = () => {
return ( return (
<> <>
{ addressQuery.isPlaceholderData ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> } <TextAd mb={ 6 }/>
<PageTitle <PageTitle
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` } title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
backLink={ backLink } backLink={ backLink }
...@@ -135,7 +135,6 @@ const AddressPageContent = () => { ...@@ -135,7 +135,6 @@ const AddressPageContent = () => {
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box> <Box ref={ tabsScrollRef }></Box>
{ addressQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : content } { addressQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : content }
{ !addressQuery.isPlaceholderData && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</> </>
); );
}; };
......
...@@ -8,7 +8,6 @@ import Stats from 'ui/home/Stats'; ...@@ -8,7 +8,6 @@ import Stats from 'ui/home/Stats';
import Transactions from 'ui/home/Transactions'; import Transactions from 'ui/home/Transactions';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
...@@ -33,15 +32,9 @@ const Home = () => { ...@@ -33,15 +32,9 @@ const Home = () => {
> >
Welcome to { appConfig.network.name } explorer Welcome to { appConfig.network.name } explorer
</Heading> </Heading>
<Flex <Box display={{ base: 'none', lg: 'block' }}>
alignItems="center"
display={{ base: 'none', lg: 'flex' }}
columnGap={ 12 }
pl={ 4 }
>
<ColorModeToggler trackBg="blackAlpha.900"/>
{ appConfig.isAccountSupported && <ProfileMenuDesktop/> } { appConfig.isAccountSupported && <ProfileMenuDesktop/> }
</Flex> </Box>
</Flex> </Flex>
<LightMode> <LightMode>
<SearchBar isHomepage/> <SearchBar isHomepage/>
......
...@@ -9,7 +9,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -9,7 +9,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => { const MyProfile = () => {
const { data, isLoading, isError, error, isFetched } = useFetchProfileInfo(); const { data, isLoading, isError, error } = useFetchProfileInfo();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const content = (() => { const content = (() => {
...@@ -26,7 +26,7 @@ const MyProfile = () => { ...@@ -26,7 +26,7 @@ const MyProfile = () => {
return ( return (
<VStack maxW="412px" mt={ 8 } gap={ 5 } alignItems="stretch"> <VStack maxW="412px" mt={ 8 } gap={ 5 } alignItems="stretch">
<UserAvatar size={ 64 } data={ data } isFetched={ isFetched }/> <UserAvatar size={ 64 }/>
<FormControl variant="floating" id="name" isRequired size="lg"> <FormControl variant="floating" id="name" isRequired size="lg">
<Input <Input
required required
......
...@@ -276,8 +276,6 @@ const TokenPageContent = () => { ...@@ -276,8 +276,6 @@ const TokenPageContent = () => {
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
) } ) }
{ !tokenQuery.isLoading && !tokenQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</> </>
); );
}; };
......
...@@ -183,8 +183,6 @@ const TokenInstanceContent = () => { ...@@ -183,8 +183,6 @@ const TokenInstanceContent = () => {
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
) } ) }
{ !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</> </>
); );
}; };
......
import { Box, Icon, Tooltip, chakra } from '@chakra-ui/react'; import { Button, Icon, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -30,7 +30,9 @@ const NetworkAddToWallet = ({ className }: Props) => { ...@@ -30,7 +30,9 @@ const NetworkAddToWallet = ({ className }: Props) => {
rpcUrls: [ appConfig.network.rpcUrl ], rpcUrls: [ appConfig.network.rpcUrl ],
blockExplorerUrls: [ appConfig.baseUrl ], blockExplorerUrls: [ appConfig.baseUrl ],
} ], } ],
}; // in wagmi types for wallet_addEthereumChain method is not provided
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
await provider?.request?.(config); await provider?.request?.(config);
toast({ toast({
position: 'top-right', position: 'top-right',
...@@ -59,11 +61,10 @@ const NetworkAddToWallet = ({ className }: Props) => { ...@@ -59,11 +61,10 @@ const NetworkAddToWallet = ({ className }: Props) => {
const defaultWallet = appConfig.web3.defaultWallet; const defaultWallet = appConfig.web3.defaultWallet;
return ( return (
<Tooltip label={ WALLETS_INFO[defaultWallet].add_network_text }> <Button variant="outline" size="sm" onClick={ handleClick } className={ className }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }> <Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 5 } mr={ 2 }/>
<Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 5 }/> Add { appConfig.network.name }
</Box> </Button>
</Tooltip>
); );
}; };
......
import { Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode'; import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
...@@ -12,6 +12,7 @@ import AppErrorInvalidTxHash from 'ui/shared/AppError/AppErrorInvalidTxHash'; ...@@ -12,6 +12,7 @@ import AppErrorInvalidTxHash from 'ui/shared/AppError/AppErrorInvalidTxHash';
import AppErrorUnverifiedEmail from 'ui/shared/AppError/AppErrorUnverifiedEmail'; import AppErrorUnverifiedEmail from 'ui/shared/AppError/AppErrorUnverifiedEmail';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
import Footer from 'ui/snippets/footer/Footer';
import Header from 'ui/snippets/header/Header'; import Header from 'ui/snippets/header/Header';
import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop'; import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
...@@ -73,18 +74,21 @@ const Page = ({ ...@@ -73,18 +74,21 @@ const Page = ({
) : children; ) : children;
return ( return (
<Flex w="100%" minH="100vh" alignItems="stretch"> <Box minWidth={{ base: '100vw', lg: 'fit-content' }}>
<NavigationDesktop/> <Flex w="100%" minH="100vh" alignItems="stretch">
<Flex flexDir="column" flexGrow={ 1 } w={{ base: '100%', lg: 'auto' }}> <NavigationDesktop/>
{ renderHeader ? <Flex flexDir="column" flexGrow={ 1 } w={{ base: '100%', lg: 'auto' }}>
renderHeader() : { renderHeader ?
<Header isHomePage={ isHomePage }/> renderHeader() :
} <Header isHomePage={ isHomePage }/>
<ErrorBoundary renderErrorScreen={ renderErrorScreen }> }
{ renderedChildren } <ErrorBoundary renderErrorScreen={ renderErrorScreen }>
</ErrorBoundary> { renderedChildren }
</ErrorBoundary>
</Flex>
</Flex> </Flex>
</Flex> <Footer/>
</Box>
); );
}; };
......
...@@ -2,10 +2,9 @@ import { useColorModeValue, useToken, SkeletonCircle, Image, Box } from '@chakra ...@@ -2,10 +2,9 @@ import { useColorModeValue, useToken, SkeletonCircle, Image, Box } from '@chakra
import React from 'react'; import React from 'react';
import Identicon from 'react-identicons'; import Identicon from 'react-identicons';
import type { UserInfo } from 'types/api/account';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
const IdenticonComponent = typeof Identicon === 'object' && 'default' in Identicon ? Identicon.default : Identicon; const IdenticonComponent = typeof Identicon === 'object' && 'default' in Identicon ? Identicon.default : Identicon;
...@@ -34,14 +33,13 @@ const FallbackImage = ({ size, id }: { size: number; id: string }) => { ...@@ -34,14 +33,13 @@ const FallbackImage = ({ size, id }: { size: number; id: string }) => {
interface Props { interface Props {
size: number; size: number;
data?: UserInfo;
isFetched: boolean;
} }
const UserAvatar = ({ size, data, isFetched }: Props) => { const UserAvatar = ({ size }: Props) => {
const appProps = useAppContext(); const appProps = useAppContext();
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies)); const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies));
const [ isImageLoadError, setImageLoadError ] = React.useState(false); const [ isImageLoadError, setImageLoadError ] = React.useState(false);
const { data, isFetched } = useFetchProfileInfo();
const sizeString = `${ size }px`; const sizeString = `${ size }px`;
......
import { useColorModeValue, useToken } from '@chakra-ui/react'; import { useColorModeValue, useToken } from '@chakra-ui/react';
import { import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum';
EthereumClient,
modalConnectors,
walletConnectProvider,
} from '@web3modal/ethereum';
import { Web3Modal } from '@web3modal/react'; import { Web3Modal } from '@web3modal/react';
import React from 'react'; import React from 'react';
import type { Chain } from 'wagmi'; import type { Chain } from 'wagmi';
import { configureChains, createClient, WagmiConfig } from 'wagmi'; import { configureChains, createConfig, WagmiConfig } from 'wagmi';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
const { wagmiClient, ethereumClient } = (() => { const getConfig = () => {
try { try {
if (!appConfig.walletConnect.projectId) {
throw new Error('WalletConnect Project ID is not set');
}
const currentChain: Chain = { const currentChain: Chain = {
id: Number(appConfig.network.id), id: Number(appConfig.network.id),
name: appConfig.network.name || '', name: appConfig.network.name || '',
...@@ -23,6 +23,9 @@ const { wagmiClient, ethereumClient } = (() => { ...@@ -23,6 +23,9 @@ const { wagmiClient, ethereumClient } = (() => {
symbol: appConfig.network.currency.symbol || '', symbol: appConfig.network.currency.symbol || '',
}, },
rpcUrls: { rpcUrls: {
'public': {
http: [ appConfig.network.rpcUrl || '' ],
},
'default': { 'default': {
http: [ appConfig.network.rpcUrl || '' ], http: [ appConfig.network.rpcUrl || '' ],
}, },
...@@ -37,21 +40,23 @@ const { wagmiClient, ethereumClient } = (() => { ...@@ -37,21 +40,23 @@ const { wagmiClient, ethereumClient } = (() => {
const chains = [ currentChain ]; const chains = [ currentChain ];
const { provider } = configureChains(chains, [ const { publicClient } = configureChains(chains, [
walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }), w3mProvider({ projectId: appConfig.walletConnect.projectId || '' }),
]); ]);
const wagmiClient = createClient({ const wagmiConfig = createConfig({
autoConnect: true, autoConnect: true,
connectors: modalConnectors({ appName: 'web3Modal', chains }), connectors: w3mConnectors({ projectId: appConfig.walletConnect.projectId, chains, version: 2 }),
provider, publicClient,
}); });
const ethereumClient = new EthereumClient(wagmiClient, chains); const ethereumClient = new EthereumClient(wagmiConfig, chains);
return { wagmiClient, ethereumClient }; return { wagmiConfig, ethereumClient };
} catch (error) { } catch (error) {
return { wagmiClient: undefined, ethereumClient: undefined }; return { wagmiConfig: undefined, ethereumClient: undefined };
} }
})(); };
const { wagmiConfig, ethereumClient } = getConfig();
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
...@@ -62,21 +67,24 @@ const Web3ModalProvider = ({ children, fallback }: Props) => { ...@@ -62,21 +67,24 @@ const Web3ModalProvider = ({ children, fallback }: Props) => {
const modalZIndex = useToken<string>('zIndices', 'modal'); const modalZIndex = useToken<string>('zIndices', 'modal');
const web3ModalTheme = useColorModeValue('light', 'dark'); const web3ModalTheme = useColorModeValue('light', 'dark');
if (!wagmiClient || !ethereumClient) { if (!wagmiConfig || !ethereumClient || !appConfig.walletConnect.projectId) {
return typeof fallback === 'function' ? fallback() : (fallback || null); return typeof fallback === 'function' ? fallback() : (fallback || null);
} }
return ( return (
<WagmiConfig client={ wagmiClient }> <>
{ children } <WagmiConfig config={ wagmiConfig }>
{ children }
</WagmiConfig>
<Web3Modal <Web3Modal
projectId={ appConfig.walletConnect.projectId } projectId={ appConfig.walletConnect.projectId }
ethereumClient={ ethereumClient } ethereumClient={ ethereumClient }
themeZIndex={ Number(modalZIndex) }
themeMode={ web3ModalTheme } themeMode={ web3ModalTheme }
themeBackground="themeColor" themeVariables={{
'--w3m-z-index': modalZIndex,
}}
/> />
</WagmiConfig> </>
); );
}; };
......
...@@ -26,7 +26,7 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => { ...@@ -26,7 +26,7 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
type: 'ERC20', // Initially only supports ERC20, but eventually more! type: 'ERC20', // Initially only supports ERC20, but eventually more!
options: { options: {
address: token.address, address: token.address,
symbol: token.symbol, symbol: token.symbol || '',
decimals: Number(token.decimals) || 18, decimals: Number(token.decimals) || 18,
// TODO: add token image when we have it in API // TODO: add token image when we have it in API
// image: '' // image: ''
...@@ -67,7 +67,7 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => { ...@@ -67,7 +67,7 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
const defaultWallet = appConfig.web3.defaultWallet; const defaultWallet = appConfig.web3.defaultWallet;
return ( return (
<Tooltip label={ WALLETS_INFO[defaultWallet].add_token_text }> <Tooltip label={ `Add token to ${ WALLETS_INFO[defaultWallet].name }` }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }> <Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 6 }/> <Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 6 }/>
</Box> </Box>
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import type { WindowProvider } from 'wagmi';
import { FOOTER_LINKS } from 'mocks/config/footerLinks';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import Footer from './Footer';
const FOOTER_LINKS_URL = 'https://localhost:3000/footer-links.json';
base.describe('with custom links, 4 cols', () => {
const test = base.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
test('base view +@dark-mode +@mobile +@desktop-xl', async({ mount, page }) => {
await page.route(FOOTER_LINKS_URL, (route) => {
return route.fulfill({
body: JSON.stringify(FOOTER_LINKS),
});
});
await mount(
<TestApp>
<Footer/>
</TestApp>,
);
await expect(page).toHaveScreenshot();
});
});
base.describe('with custom links, 2 cols', () => {
const test = base.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
test('base view +@dark-mode +@mobile', async({ mount, page }) => {
await page.route(FOOTER_LINKS_URL, (route) => {
return route.fulfill({
body: JSON.stringify([ FOOTER_LINKS[0] ]),
});
});
await mount(
<TestApp>
<Footer/>
</TestApp>,
);
await expect(page).toHaveScreenshot();
});
});
base.describe('without custom links', () => {
base('base view +@dark-mode +@mobile', async({ mount, page }) => {
await page.evaluate(() => {
window.ethereum = {
providers: [ { isMetaMask: true } ],
} as WindowProvider;
});
await mount(
<TestApp>
<Footer/>
</TestApp>,
);
await expect(page).toHaveScreenshot();
});
});
import { Box, Grid, Flex, Text, Link, VStack, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { CustomLinksGroup } from 'types/footerLinks';
import appConfig from 'configs/app/config';
import discussionsIcon from 'icons/discussions.svg';
import editIcon from 'icons/edit.svg';
import discordIcon from 'icons/social/discord.svg';
import gitIcon from 'icons/social/git.svg';
import twitterIcon from 'icons/social/tweet.svg';
import type { ResourceError } from 'lib/api/resources';
import useFetch from 'lib/hooks/useFetch';
import useIssueUrl from 'lib/hooks/useIssueUrl';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
import ColorModeToggler from '../header/ColorModeToggler';
import FooterLinkItem from './FooterLinkItem';
const MAX_LINKS_COLUMNS = 3;
const API_VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ appConfig.blockScoutVersion }`;
// const FRONT_VERSION_URL = `https://github.com/blockscout/frontend/tree/${ appConfig.frontendVersion }`;
const Footer = () => {
const issueUrl = useIssueUrl();
const BLOCSKOUT_LINKS = [
{
icon: editIcon,
iconSize: '16px',
text: 'Submit an issue',
url: issueUrl,
},
{
icon: gitIcon,
iconSize: '18px',
text: 'Contribute',
url: 'https://github.com/blockscout/blockscout',
},
{
icon: twitterIcon,
iconSize: '18px',
text: 'Twitter',
url: 'https://www.twitter.com/blockscoutcom',
},
{
icon: discordIcon,
iconSize: '18px',
text: 'Discord',
url: 'https://discord.gg/blockscout',
},
{
icon: discussionsIcon,
iconSize: '20px',
text: 'Discussions',
url: 'https://github.com/orgs/blockscout/discussions',
},
];
const fetch = useFetch();
const { isLoading, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>(
[ 'footer-links' ],
async() => fetch(appConfig.footerLinks || ''),
{
enabled: Boolean(appConfig.footerLinks),
staleTime: Infinity,
});
return (
<Flex direction={{ base: 'column', lg: 'row' }} p={{ base: 4, lg: 9 }} borderTop="1px solid" borderColor="divider">
<Box flexGrow="1" mb={{ base: 8, lg: 0 }}>
<Flex>
<ColorModeToggler/>
<NetworkAddToWallet ml={ 8 }/>
</Flex>
<Box mt={{ base: 5, lg: '44px' }}>
<Link fontSize="xs" href="https://www.blockscout.com">blockscout.com</Link>
</Box>
<Text mt={ 3 } mr={{ base: 0, lg: '114px' }} maxW={{ base: 'unset', lg: '470px' }} fontSize="xs">
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
<VStack spacing={ 1 } mt={ 6 } alignItems="start">
{ appConfig.blockScoutVersion && (
<Text fontSize="xs">
Backend: <Link href={ API_VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link>
</Text>
) }
{ (appConfig.frontendVersion || appConfig.frontendCommit) && (
<Text fontSize="xs">
{ /* Frontend: <Link href={ FRONT_VERSION_URL } target="_blank">{ appConfig.frontendVersion }</Link> */ }
Frontend: { [ appConfig.frontendVersion, appConfig.frontendCommit ].filter(Boolean).join('+') }
</Text>
) }
</VStack>
</Box>
<Grid
gap={{ base: 6, lg: 12 }}
gridTemplateColumns={ appConfig.footerLinks ?
{ base: 'repeat(auto-fill, 160px)', lg: `repeat(${ (linksData?.length || MAX_LINKS_COLUMNS) + 1 }, 160px)` } :
'auto'
}
>
<Box minW="160px" w={ appConfig.footerLinks ? '160px' : '100%' }>
{ appConfig.footerLinks && <Text fontWeight={ 500 } mb={ 3 }>Blockscout</Text> }
<Grid
gap={ 1 }
gridTemplateColumns={ appConfig.footerLinks ? '160px' : { base: 'repeat(auto-fill, 160px)', lg: 'repeat(3, 160px)' } }
gridTemplateRows={{ base: 'auto', lg: appConfig.footerLinks ? 'auto' : 'repeat(2, auto)' }}
gridAutoFlow={{ base: 'row', lg: appConfig.footerLinks ? 'row' : 'column' }}
mt={{ base: 0, lg: appConfig.footerLinks ? 0 : '100px' }}
>
{ BLOCSKOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</Grid>
</Box>
{ appConfig.footerLinks && isLoading && (
Array.from(Array(3)).map((i, index) => (
<Box minW="160px" key={ index }>
<Skeleton w="120px" h="20px" mb={ 6 }/>
<VStack spacing={ 5 } alignItems="start" mb={ 2 }>
{ Array.from(Array(5)).map((i, index) => <Skeleton w="160px" h="14px" key={ index }/>) }
</VStack>
</Box>
))
) }
{ appConfig.footerLinks && linksData && (
linksData.slice(0, MAX_LINKS_COLUMNS).map(linkGroup => (
<Box minW="160px" key={ linkGroup.title }>
<Text fontWeight={ 500 } mb={ 3 }>{ linkGroup.title }</Text>
<VStack spacing={ 1 } alignItems="start">
{ linkGroup.links.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</VStack>
</Box>
))
) }
</Grid>
</Flex>
);
};
export default Footer;
import { Center, Icon, Link } from '@chakra-ui/react';
import React from 'react';
type Props = {
icon?: React.FC<React.SVGAttributes<SVGElement>>;
iconSize?: string;
text: string;
url: string;
}
const FooterLinkItem = ({ icon, iconSize, text, url }: Props) => {
return (
<Link href={ url } display="flex" alignItems="center" h="30px" variant="secondary" target="_blank" fontSize="xs">
{ icon && (
<Center minW={ 6 } mr="6px">
<Icon boxSize={ iconSize || 5 } as={ icon }/>
</Center>
) }
{ text }
</Link>
);
};
export default FooterLinkItem;
...@@ -110,7 +110,7 @@ test.describe('auth', () => { ...@@ -110,7 +110,7 @@ test.describe('auth', () => {
}, },
}); });
extendedTest.use({ viewport: { width: devices['iPhone 13 Pro'].viewport.width, height: 1000 } }); extendedTest.use({ viewport: { width: devices['iPhone 13 Pro'].viewport.width, height: 800 } });
extendedTest('base view', async({ mount, page }) => { extendedTest('base view', async({ mount, page }) => {
const component = await mount( const component = await mount(
......
...@@ -10,7 +10,6 @@ import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile'; ...@@ -10,7 +10,6 @@ import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
import Burger from './Burger'; import Burger from './Burger';
import ColorModeToggler from './ColorModeToggler';
type Props = { type Props = {
isHomePage?: boolean; isHomePage?: boolean;
...@@ -66,7 +65,6 @@ const Header = ({ isHomePage, renderSearchBar }: Props) => { ...@@ -66,7 +65,6 @@ const Header = ({ isHomePage, renderSearchBar }: Props) => {
<Box width="100%"> <Box width="100%">
{ searchBar } { searchBar }
</Box> </Box>
<ColorModeToggler/>
{ appConfig.isAccountSupported && <ProfileMenuDesktop/> } { appConfig.isAccountSupported && <ProfileMenuDesktop/> }
</HStack> </HStack>
) } ) }
......
import { Box, Text, Stack, Icon, Link, VStack } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import ghIcon from 'icons/social/git.svg';
import statsIcon from 'icons/social/stats.svg';
import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
const SOCIAL_LINKS = [
{ link: appConfig.footerLinks.github, icon: ghIcon, label: 'Github link' },
{ link: appConfig.footerLinks.twitter, icon: twIcon, label: 'Twitter link' },
{ link: appConfig.footerLinks.telegram, icon: tgIcon, label: 'Telegram link' },
{ link: appConfig.footerLinks.staking, icon: statsIcon, label: 'Staking analytic link' },
].filter(({ link }) => link);
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ appConfig.blockScoutVersion }`;
interface Props {
isCollapsed?: boolean;
hasAccount?: boolean;
}
const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
const isExpanded = isCollapsed === false;
const marginTop = (() => {
if (!hasAccount) {
return 'auto';
}
return { base: 6, lg: 20 };
})();
return (
<VStack
as="footer"
borderTop="1px solid"
borderColor="divider"
width={{ base: '100%', lg: isExpanded ? '180px' : '20px', xl: isCollapsed ? '20px' : '180px' }}
marginTop={ marginTop }
alignItems="flex-start"
alignSelf="center"
color="gray.500"
fontSize="xs"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
>
<Stack
direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}
mt={{ base: 6, lg: 8 }}
_empty={{
display: 'none',
}}
>
<NetworkAddToWallet/>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank">
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
}) }
</Stack>
<Box display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}>
<Text variant="secondary" mt={ 8 }>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
{ appConfig.blockScoutVersion &&
<Text variant="secondary" mt={ 8 }>Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text> }
</Box>
</VStack>
);
};
export default NavFooter;
import { Link, Icon, Text, HStack, Tooltip, Box, useBreakpointValue, chakra, shouldForwardProp } from '@chakra-ui/react'; import { Link, Text, HStack, Tooltip, Box, useBreakpointValue, chakra, shouldForwardProp } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -8,6 +8,7 @@ import type { NavItem } from 'types/client/navigation-items'; ...@@ -8,6 +8,7 @@ import type { NavItem } from 'types/client/navigation-items';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { isInternalItem } from 'lib/hooks/useNavItems'; import { isInternalItem } from 'lib/hooks/useNavItems';
import NavLinkIcon from './NavLinkIcon';
import useColors from './useColors'; import useColors from './useColors';
import useNavLinkStyleProps from './useNavLinkStyleProps'; import useNavLinkStyleProps from './useNavLinkStyleProps';
...@@ -50,7 +51,7 @@ const NavLink = ({ item, isCollapsed, px, className }: Props) => { ...@@ -50,7 +51,7 @@ const NavLink = ({ item, isCollapsed, px, className }: Props) => {
color={ isInternalLink && item.isActive ? colors.text.active : colors.text.hover } color={ isInternalLink && item.isActive ? colors.text.active : colors.text.hover }
> >
<HStack spacing={ 3 } overflow="hidden"> <HStack spacing={ 3 } overflow="hidden">
{ item.icon && <Icon as={ item.icon } boxSize="30px"/> } <NavLinkIcon item={ item }/>
<Text { ...styleProps.textProps }> <Text { ...styleProps.textProps }>
{ item.text } { item.text }
</Text> </Text>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment