Fixes for release v2.5.0 (#3142)
* fail the app start if multichain config or essential dapps config were not generated properly * add timeouts for essential dapps and multichain config generators * fix "add to wallet" button in address tokens table * [skip ci] update docs * fix tx tables when there are no fee and value columns * add support for promoted multichain provider * fixes * change action button logic * fix tests * fix * hide "add network" button for multichain * shorten the amount of txs on multichain homepage * change name formatting for multichain button * add bright color variants and lg size for badges * fix y-axis margin calculation for chart with custom tick formatter * superchain bug fixes * bump versions of react and nextjs * update screenshot * bump dappscout-iframe --------- Co-authored-by: Max Alekseenko <[email protected]>
@@ -13,12 +13,15 @@ const config: Feature<{ providers: Array<MultichainProviderConfigParsed> }> = ((
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
providers: value.map((provider) => ({
|
||||
name: provider.name,
|
||||
logoUrl: provider.logo,
|
||||
urlTemplate: provider.url_template,
|
||||
dappId: marketplace.isEnabled ? provider.dapp_id : undefined,
|
||||
})),
|
||||
providers: value
|
||||
.map((provider) => ({
|
||||
name: provider.name,
|
||||
logoUrl: provider.logo,
|
||||
urlTemplate: provider.url_template,
|
||||
dappId: marketplace.isEnabled ? provider.dapp_id : undefined,
|
||||
promo: provider.promo,
|
||||
}))
|
||||
.sort((_, b) => (b.promo ? 1 : -1)),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKj
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
NEXT_PUBLIC_METASUITES_ENABLED=true
|
||||
NEXT_PUBLIC_MIXPANEL_CONFIG_OVERRIDES={"record_sessions_percent": 0.5,"record_heatmap_data": true}
|
||||
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'},{'name': 'zapper', 'url_template': 'https://zapper.xyz/account/{address}', 'logo': 'https://blockscout-content.s3.amazonaws.com/zapper-icon.png'}]
|
||||
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'},{'name': 'zapper', 'url_template': 'https://zapper.xyz/account/{address}', 'logo': 'https://blockscout-content.s3.amazonaws.com/zapper-icon.png'},{'name': 'blockscout', 'url_template': 'https://superchain.blockscout.com/address/{address}', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/multichain-balance/blockscout.svg', 'promo': true}]
|
||||
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
|
||||
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
|
||||
NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG={"img_url": {"small": "https://blockscout-merits-images.s3.us-east-1.amazonaws.com/banners/sidemenu-banner-small.png", "large": "https://blockscout-merits-images.s3.us-east-1.amazonaws.com/banners/sidemenu-banner-big.png"}, "link_url": "https://www.blockscout.com/?utm_source=blockscout&utm_medium=side-menu-banner"}
|
||||
|
||||
@@ -69,19 +69,27 @@ node --no-warnings ./og_image_generator.js
|
||||
./make_envs_script.sh
|
||||
|
||||
# Generate multichain config
|
||||
node ./deploy/tools/multichain-config-generator/dist/index.js
|
||||
node --no-warnings ./deploy/tools/multichain-config-generator/dist/index.js
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "👎 Unable to generate multichain config."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate essential dapps chains config
|
||||
node ./deploy/tools/essential-dapps-chains-config-generator/dist/index.js
|
||||
node --no-warnings ./deploy/tools/essential-dapps-chains-config-generator/dist/index.js
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "👎 Unable to generate essential dapps chains config."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate sitemap.xml and robots.txt files
|
||||
./sitemap_generator.sh
|
||||
|
||||
# Generate llms.txt file
|
||||
node ./deploy/tools/llms-txt-generator/dist/index.js
|
||||
node --no-warnings ./deploy/tools/llms-txt-generator/dist/index.js
|
||||
|
||||
# Print list of enabled features
|
||||
node ./feature-reporter.js
|
||||
node --no-warnings ./feature-reporter.js
|
||||
|
||||
echo "Starting Next.js application"
|
||||
exec "$@"
|
||||
|
||||
@@ -35,6 +35,7 @@ const multichainProviderConfigSchema: yup.ObjectSchema<MultichainProviderConfig>
|
||||
url_template: yup.string().required(),
|
||||
logo: yup.string().required(),
|
||||
dapp_id: yup.string(),
|
||||
promo: yup.boolean(),
|
||||
});
|
||||
|
||||
const schema = yup
|
||||
|
||||
@@ -17,23 +17,39 @@ const currentFilePath = fileURLToPath(import.meta.url);
|
||||
const currentDir = dirname(currentFilePath);
|
||||
|
||||
async function getChainscoutInfo(externalChainIds: Array<string>, currentChainId: string | undefined) {
|
||||
const response = await fetch('https://chains.blockscout.com/api/chains');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch chains info from Chainscout API`);
|
||||
}
|
||||
const chainsInfo = await response.json() as Record<string, { explorers: [ { url: string } ], logo: string }>;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort(`Request to Chainscout API timed out`);
|
||||
}, 10_000);
|
||||
|
||||
return {
|
||||
externals: externalChainIds.map((chainId) => ({
|
||||
id: chainId,
|
||||
explorerUrl: chainsInfo[chainId]?.explorers[0]?.url,
|
||||
logoUrl: chainsInfo[chainId]?.logo,
|
||||
})),
|
||||
current: currentChainId ? {
|
||||
id: currentChainId,
|
||||
explorerUrl: chainsInfo[currentChainId]?.explorers[0]?.url,
|
||||
logoUrl: chainsInfo[currentChainId]?.logo,
|
||||
} : undefined,
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://chains.blockscout.com/api/chains?chain_ids=${ [currentChainId, ...externalChainIds].filter(Boolean).join(',') }`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch chains info from Chainscout API`);
|
||||
}
|
||||
const chainsInfo = await response.json() as Record<string, { explorers: [ { url: string } ], logo: string }>;
|
||||
|
||||
return {
|
||||
externals: externalChainIds.map((chainId) => ({
|
||||
id: chainId,
|
||||
explorerUrl: chainsInfo[chainId]?.explorers[0]?.url,
|
||||
logoUrl: chainsInfo[chainId]?.logo,
|
||||
})),
|
||||
current: currentChainId ? {
|
||||
id: currentChainId,
|
||||
explorerUrl: chainsInfo[currentChainId]?.explorers[0]?.url,
|
||||
logoUrl: chainsInfo[currentChainId]?.logo,
|
||||
} : undefined,
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,23 @@ interface ChainConfig {
|
||||
}
|
||||
|
||||
async function fetchChainConfig(url: string): Promise<ChainConfig> {
|
||||
const response = await fetch(`${ url }/node-api/config`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch config from ${ url }: ${ response.statusText }`);
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort(`Request to ${ url } timed out`);
|
||||
}, 5_000);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ url }/node-api/config`, { signal: controller.signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch config from ${ url }: ${ response.statusText }`);
|
||||
}
|
||||
const config = await response.json();
|
||||
return config as ChainConfig;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
const config = await response.json();
|
||||
return config as ChainConfig;
|
||||
}
|
||||
|
||||
async function computeConfig() {
|
||||
|
||||
@@ -24,16 +24,27 @@ function getSlug(chainName: string) {
|
||||
}
|
||||
|
||||
async function getChainscoutInfo(chainIds: Array<string>) {
|
||||
const response = await fetch('https://chains.blockscout.com/api/chains');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch chains info from Chainscout API`);
|
||||
}
|
||||
const chainsInfo = await response.json() as Record<string, { explorers: [ { url: string } ], logo: string }>;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort(`Request to Chainscout API timed out`);
|
||||
}, 10_000);
|
||||
|
||||
return chainIds.map((chainId) => ({
|
||||
id: chainId,
|
||||
logoUrl: chainsInfo[chainId]?.logo,
|
||||
}))
|
||||
try {
|
||||
const response = await fetch(`https://chains.blockscout.com/api/chains?chain_ids=${ chainIds.join(',') }`, { signal: controller.signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch chains info from Chainscout API`);
|
||||
}
|
||||
const chainsInfo = await response.json() as Record<string, { explorers: [ { url: string } ], logo: string }>;
|
||||
|
||||
return chainIds.map((chainId) => ({
|
||||
id: chainId,
|
||||
logoUrl: chainsInfo[chainId]?.logo,
|
||||
}))
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function computeChainConfig(url: string): Promise<unknown> {
|
||||
|
||||
@@ -10,12 +10,23 @@ interface ChainConfig {
|
||||
}
|
||||
|
||||
async function fetchChainConfig(url: string): Promise<ChainConfig> {
|
||||
const response = await fetch(`${ url }/node-api/config`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch config from ${ url }: ${ response.statusText }`);
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort(`Request to ${ url } timed out`);
|
||||
}, 5_000);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ url }/node-api/config`, { signal: controller.signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch config from ${ url }: ${ response.statusText }`);
|
||||
}
|
||||
const config = await response.json();
|
||||
return config as ChainConfig;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
const config = await response.json();
|
||||
return config as ChainConfig;
|
||||
}
|
||||
|
||||
async function computeConfig() {
|
||||
|
||||
@@ -102,6 +102,8 @@ All json-like values should be single-quoted. If it contains a hash (`#`) or a d
|
||||
|
||||
*Note!* The `NEXT_PUBLIC_NETWORK_CURRENCY` variables represent the blockchain's native token used for paying transaction fees. `NEXT_PUBLIC_NETWORK_SECONDARY_COIN` variables refer to tokens like protocol-specific tokens (e.g., OP token on Optimism chain) or governance tokens (e.g., GNO on Gnosis chain).
|
||||
|
||||
Also, be aware that if you customize the name of the currency or any of its denominations (wei or gwei) while running Stats microservices, you may want to change those names in the indicators and charts returned by the microservice. To do this, pass the appropriate values to the Stats microservice environment variables, such as `STATS_CHARTS__LINE_CHARTS__<LINE_CHART_NAME>__UNITS` and `STATS_CHARTS__LINE_CHARTS__<LINE_CHART_NAME>__DESCRIPTION`. For the Average Gas Price chart, the `<LINE_CHART_NAME>` will be `average_gas_price`. Please refer to the [microservice documentation](https://github.com/blockscout/blockscout-rs/tree/main/stats#charts) for the complete list of these variables.
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | v1.0.x+ |
|
||||
@@ -913,6 +915,7 @@ If the feature is enabled, a Multichain balance button will be displayed on the
|
||||
| url_template | `string` | Url template to the portfolio. Should be a template with `{address}` variable | Required | - | `https://app.zerion.io/{address}/overview` |
|
||||
| dapp_id | `string` | Set for open a Blockscout dapp page with the portfolio instead of opening external app page | - | - | `zerion` |
|
||||
| logo | `string` | Multichain portfolio application logo (.svg) url | - | - | `https://example.com/icon.svg` |
|
||||
| promo | `boolean` | Make the provider stand out by placing their logo prominently at the first place in the section and in the page subheader. | - | - | `true` |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ const bgColor = '#FF007A';
|
||||
|
||||
export const buttonWithoutStyles: AddressMetadataTagApi['meta'] = {
|
||||
appID,
|
||||
appMarketplaceURL,
|
||||
appLogoURL,
|
||||
appActionButtonText,
|
||||
};
|
||||
|
||||
export const linkWithoutStyles: AddressMetadataTagApi['meta'] = {
|
||||
appID,
|
||||
appMarketplaceURL,
|
||||
appLogoURL,
|
||||
appActionButtonText,
|
||||
@@ -22,7 +22,6 @@ export const linkWithoutStyles: AddressMetadataTagApi['meta'] = {
|
||||
|
||||
export const buttonWithStyles: AddressMetadataTagApi['meta'] = {
|
||||
appID,
|
||||
appMarketplaceURL,
|
||||
appLogoURL,
|
||||
appActionButtonText,
|
||||
textColor,
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"chakra-react-select": "^4.4.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"d3": "^7.6.1",
|
||||
"dappscout-iframe": "0.3.0",
|
||||
"dappscout-iframe": "0.3.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"es-toolkit": "1.39.10",
|
||||
@@ -107,7 +107,7 @@
|
||||
"magic-bytes.js": "1.8.0",
|
||||
"mixpanel-browser": "2.67.0",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"next": "15.5.2",
|
||||
"next": "15.5.7",
|
||||
"next-themes": "0.4.4",
|
||||
"nextjs-routes": "^1.0.8",
|
||||
"node-fetch": "^3.2.9",
|
||||
@@ -118,9 +118,9 @@
|
||||
"pino-pretty": "^9.1.1",
|
||||
"prom-client": "15.1.1",
|
||||
"qrcode": "^1.5.1",
|
||||
"react": "19.1.0",
|
||||
"react": "19.1.2",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "19.1.0",
|
||||
"react-dom": "19.1.2",
|
||||
"react-google-recaptcha": "3.1.0",
|
||||
"react-hook-form": "7.52.1",
|
||||
"react-icons": "5.4.0",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 42 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="42" height="14" rx="4" fill="#F6E05E"/>
|
||||
<path d="M37.398 3.718v6.545h-.991V4.708h-.038l-1.566 1.023v-.946l1.633-1.067h.962Zm-9.096 6.545V3.718h4.104v.85H29.29v1.994h2.902v.847H29.29v2.004h3.154v.85h-4.142Zm-2.406-4.478a1.995 1.995 0 0 0-.249-.52 1.593 1.593 0 0 0-.857-.65 1.966 1.966 0 0 0-.597-.086 1.81 1.81 0 0 0-.997.285c-.297.19-.53.468-.704.834-.17.364-.255.81-.255 1.34s.086.978.259 1.345c.172.366.409.644.71.834.3.19.642.284 1.025.284.356 0 .666-.072.93-.217a1.52 1.52 0 0 0 .617-.614c.147-.266.22-.58.22-.94l.256.049h-1.873v-.815h2.573v.744c0 .55-.117 1.027-.351 1.432a2.394 2.394 0 0 1-.966.933c-.409.22-.878.33-1.406.33-.592 0-1.112-.137-1.56-.41a2.772 2.772 0 0 1-1.041-1.16c-.25-.502-.374-1.1-.374-1.79 0-.521.072-.99.217-1.406.145-.415.348-.768.61-1.058a2.7 2.7 0 0 1 .93-.668 2.912 2.912 0 0 1 1.173-.233c.354 0 .683.052.988.157.307.104.58.252.818.444a2.515 2.515 0 0 1 .917 1.556h-1.013Zm-9.781 4.478h-1.048l2.355-6.545h1.141l2.356 6.545H19.87l-1.85-5.356h-.051l-1.854 5.356Zm.176-2.563h3.4v.83h-3.4V7.7Zm-6.088-3.132v-.85h5.065v.85h-2.042v5.695h-.984V4.568h-2.04Zm-1.772.869a.934.934 0 0 0-.421-.703c-.248-.168-.559-.252-.934-.252-.268 0-.5.042-.696.127a1.073 1.073 0 0 0-.457.346.815.815 0 0 0-.16.495c0 .155.036.29.108.403a.975.975 0 0 0 .291.284c.122.075.252.137.39.189.139.049.272.09.4.121l.639.166c.209.051.423.12.642.208.22.087.423.202.61.345.188.143.34.32.454.53.118.212.176.464.176.758 0 .37-.096.7-.287.988-.19.287-.466.514-.828.68-.36.167-.796.25-1.307.25-.49 0-.914-.078-1.272-.233a1.959 1.959 0 0 1-.84-.662 1.918 1.918 0 0 1-.336-1.023h.99c.02.237.096.434.23.591.137.156.31.272.521.349.213.074.447.112.7.112.28 0 .528-.044.745-.131.22-.09.392-.213.518-.371a.878.878 0 0 0 .188-.56.686.686 0 0 0-.166-.476 1.228 1.228 0 0 0-.444-.306 4.342 4.342 0 0 0-.63-.215l-.773-.21c-.524-.143-.94-.353-1.247-.63-.304-.277-.457-.644-.457-1.1 0-.377.103-.706.307-.987.205-.281.482-.5.831-.655.35-.158.744-.237 1.183-.237.443 0 .834.078 1.173.233.34.156.609.37.805.643.196.27.298.582.307.933H8.43Z" fill="#101112" fill-opacity=".8"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 42 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="42" height="14" rx="4" fill="#9AE6B4"/>
|
||||
<path d="M33.949 10.263v-.716l2.215-2.295c.236-.249.431-.467.585-.655.155-.19.271-.37.348-.54a1.3 1.3 0 0 0 .115-.543 1.01 1.01 0 0 0-.153-.563 1.002 1.002 0 0 0-.419-.367 1.332 1.332 0 0 0-.598-.131c-.234 0-.439.048-.613.144a.985.985 0 0 0-.403.405 1.28 1.28 0 0 0-.14.614h-.943c0-.398.091-.747.275-1.045.183-.298.434-.53.754-.694.32-.166.683-.249 1.09-.249.41 0 .773.082 1.086.246.316.162.562.384.738.665.177.28.266.595.266.946 0 .243-.046.48-.138.713-.09.232-.246.49-.47.776-.223.284-.534.628-.933 1.033l-1.3 1.361v.048h2.946v.847H33.95Zm-6.506 0V3.718h4.103v.85H28.43v1.994h2.902v.847H28.43v2.004h3.155v.85h-4.142Zm-2.407-4.478a1.998 1.998 0 0 0-.249-.52 1.593 1.593 0 0 0-.856-.65 1.967 1.967 0 0 0-.598-.086 1.81 1.81 0 0 0-.997.285c-.296.19-.53.468-.703.834-.17.364-.256.81-.256 1.34s.086.978.259 1.345c.172.366.409.644.71.834.3.19.642.284 1.025.284.356 0 .666-.072.93-.217.267-.145.472-.35.617-.614.147-.266.22-.58.22-.94l.256.049h-1.872v-.815h2.572v.744c0 .55-.117 1.027-.351 1.432a2.393 2.393 0 0 1-.965.933c-.41.22-.878.33-1.407.33-.592 0-1.112-.137-1.56-.41a2.773 2.773 0 0 1-1.041-1.16c-.25-.502-.374-1.1-.374-1.79 0-.521.072-.99.217-1.406.145-.415.348-.768.61-1.058a2.7 2.7 0 0 1 .93-.668 2.911 2.911 0 0 1 1.174-.233c.353 0 .682.052.987.157.307.104.58.252.818.444a2.516 2.516 0 0 1 .917 1.556h-1.013Zm-9.781 4.478h-1.048l2.355-6.545h1.141l2.356 6.545H19.01l-1.85-5.356h-.052l-1.854 5.356Zm.176-2.563h3.4v.83h-3.4V7.7ZM9.38 4.568v-.85h5.067v.85h-2.043v5.695h-.984V4.568H9.38Zm-1.808.869a.934.934 0 0 0-.422-.703c-.248-.168-.559-.252-.934-.252-.268 0-.5.042-.696.127a1.073 1.073 0 0 0-.457.346.815.815 0 0 0-.16.495c0 .155.036.29.109.403a.975.975 0 0 0 .29.284c.122.075.252.137.39.189a4.4 4.4 0 0 0 .4.121l.639.166c.209.051.423.12.642.208.22.087.423.202.61.345a1.537 1.537 0 0 1 .63 1.288c0 .37-.095.7-.287.988-.19.287-.466.514-.828.68-.36.167-.796.25-1.307.25-.49 0-.914-.078-1.272-.233a1.959 1.959 0 0 1-.84-.662 1.918 1.918 0 0 1-.336-1.023h.99c.02.237.096.434.23.591.137.156.31.272.522.349.213.074.446.112.7.112.279 0 .527-.044.744-.131.22-.09.392-.213.518-.371a.878.878 0 0 0 .188-.56.686.686 0 0 0-.166-.476 1.228 1.228 0 0 0-.444-.306 4.342 4.342 0 0 0-.63-.215l-.773-.21c-.524-.143-.94-.353-1.247-.63-.304-.277-.457-.644-.457-1.1 0-.377.103-.706.307-.987.205-.281.482-.5.831-.655.35-.158.744-.237 1.183-.237.443 0 .834.078 1.173.233.34.156.61.37.805.643.196.27.298.582.307.933h-.952Z" fill="#101112" fill-opacity=".8"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -17,7 +17,7 @@ export interface LabelFormatParams extends Intl.NumberFormatOptions {
|
||||
type Data = TimeChartData;
|
||||
|
||||
export function getAxesParams(data: Data, axesConfig?: AxesConfig) {
|
||||
const { labelFormatParams: labelFormatParamsY, scale: yScale } = getAxisParamsY(data, axesConfig?.y);
|
||||
const { labelFormatParams: labelFormatParamsY, scale: yScale } = getAxisParamsY(data, axesConfig?.y, axesConfig?.y?.tickFormatter);
|
||||
|
||||
return {
|
||||
x: {
|
||||
@@ -60,7 +60,7 @@ const tickFormatterX = (axis: d3.Axis<d3.NumberValue>) => (d: d3.AxisDomain) =>
|
||||
return format(d as Date);
|
||||
};
|
||||
|
||||
function getAxisParamsY(data: Data, config?: AxisConfig) {
|
||||
function getAxisParamsY(data: Data, config?: AxisConfig, tickFormatter?: () => (d: d3.AxisDomain) => string) {
|
||||
const DEFAULT_TICKS_NUM = 3;
|
||||
const min = d3.min(data, ({ items }) => d3.min(items, ({ value }) => value)) ?? 0;
|
||||
const max = d3.max(data, ({ items }) => d3.max(items, ({ value }) => value)) ?? 0;
|
||||
@@ -72,7 +72,7 @@ function getAxisParamsY(data: Data, config?: AxisConfig) {
|
||||
.domain([ config?.scale?.min ?? min, max ]);
|
||||
|
||||
const ticks = scale.ticks(config?.ticks ?? DEFAULT_TICKS_NUM);
|
||||
const labelFormatParams = getYLabelFormatParams(ticks);
|
||||
const labelFormatParams = getYLabelFormatParams(ticks, tickFormatter);
|
||||
|
||||
return { min, max, scale, labelFormatParams };
|
||||
}
|
||||
@@ -82,19 +82,23 @@ const getTickFormatterY = (params: Intl.NumberFormatOptions) => () => (d: d3.Axi
|
||||
return num.toLocaleString(undefined, params);
|
||||
};
|
||||
|
||||
function getYLabelFormatParams(ticks: Array<number>, maximumSignificantDigits = DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS): LabelFormatParams {
|
||||
function getYLabelFormatParams(
|
||||
ticks: Array<number>,
|
||||
tickFormatter?: () => (d: d3.AxisDomain) => string,
|
||||
maximumSignificantDigits = DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS,
|
||||
): LabelFormatParams {
|
||||
const params = {
|
||||
maximumFractionDigits: DEFAULT_MAXIMUM_FRACTION_DIGITS,
|
||||
maximumSignificantDigits,
|
||||
notation: 'compact' as const,
|
||||
};
|
||||
|
||||
const uniqTicksStr = uniq(ticks.map((tick) => tick.toLocaleString(undefined, params)));
|
||||
const uniqTicksStr = uniq(ticks.map((tick) => tickFormatter ? tickFormatter()(tick) : tick.toLocaleString(undefined, params)));
|
||||
const maxLabelLength = maxBy(uniqTicksStr, (items) => items.length)?.length ?? DEFAULT_LABEL_LENGTH;
|
||||
|
||||
if (uniqTicksStr.length === ticks.length || maximumSignificantDigits === MAXIMUM_SIGNIFICANT_DIGITS_LIMIT) {
|
||||
return { ...params, maxLabelLength };
|
||||
}
|
||||
|
||||
return getYLabelFormatParams(ticks, maximumSignificantDigits + 1);
|
||||
return getYLabelFormatParams(ticks, tickFormatter, maximumSignificantDigits + 1);
|
||||
}
|
||||
|
||||
@@ -396,6 +396,53 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
|
||||
bg: { value: { _light: '{colors.cyan.50}', _dark: '{colors.cyan.800}' } },
|
||||
fg: { value: { _light: '{colors.cyan.500}', _dark: '{colors.cyan.100}' } },
|
||||
},
|
||||
pink: {
|
||||
bg: { value: { _light: '{colors.pink.50}', _dark: '{colors.pink.800}' } },
|
||||
fg: { value: { _light: '{colors.pink.500}', _dark: '{colors.pink.100}' } },
|
||||
},
|
||||
// bright badges mainly used in other projects (e.g. autoscout, dev portal, etc.)
|
||||
bright: {
|
||||
gray: {
|
||||
bg: { value: { _light: '{colors.gray.100}', _dark: '{colors.gray.800}' } },
|
||||
fg: { value: { _light: '{colors.gray.600}', _dark: '{colors.gray.200}' } },
|
||||
},
|
||||
green: {
|
||||
bg: { value: { _light: '{colors.green.100}', _dark: '{colors.green.800}' } },
|
||||
fg: { value: { _light: '{colors.green.600}', _dark: '{colors.green.200}' } },
|
||||
},
|
||||
red: {
|
||||
bg: { value: { _light: '{colors.red.100}', _dark: '{colors.red.800}' } },
|
||||
fg: { value: { _light: '{colors.red.600}', _dark: '{colors.red.200}' } },
|
||||
},
|
||||
blue: {
|
||||
bg: { value: { _light: '{colors.blue.100}', _dark: '{colors.blue.800}' } },
|
||||
fg: { value: { _light: '{colors.blue.600}', _dark: '{colors.blue.200}' } },
|
||||
},
|
||||
yellow: {
|
||||
bg: { value: { _light: '{colors.yellow.100}', _dark: '{colors.yellow.800}' } },
|
||||
fg: { value: { _light: '{colors.yellow.600}', _dark: '{colors.yellow.200}' } },
|
||||
},
|
||||
teal: {
|
||||
bg: { value: { _light: '{colors.teal.100}', _dark: '{colors.teal.800}' } },
|
||||
fg: { value: { _light: '{colors.teal.600}', _dark: '{colors.teal.200}' } },
|
||||
},
|
||||
cyan: {
|
||||
bg: { value: { _light: '{colors.cyan.100}', _dark: '{colors.cyan.800}' } },
|
||||
fg: { value: { _light: '{colors.cyan.600}', _dark: '{colors.cyan.200}' } },
|
||||
},
|
||||
orange: {
|
||||
bg: { value: { _light: '{colors.orange.100}', _dark: '{colors.orange.600}' } },
|
||||
fg: { value: { _light: '{colors.orange.600}', _dark: '{colors.orange.100}' } },
|
||||
},
|
||||
purple: {
|
||||
bg: { value: { _light: '{colors.purple.50}', _dark: '{colors.purple.600}' } },
|
||||
fg: { value: { _light: '{colors.purple.600}', _dark: '{colors.purple.50}' } },
|
||||
},
|
||||
pink: {
|
||||
bg: { value: { _light: '{colors.pink.50}', _dark: '{colors.pink.600}' } },
|
||||
fg: { value: { _light: '{colors.pink.600}', _dark: '{colors.pink.50}' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
tag: {
|
||||
root: {
|
||||
|
||||
@@ -57,6 +57,10 @@ export const recipe = defineRecipe({
|
||||
bg: 'badge.cyan.bg',
|
||||
color: 'badge.cyan.fg',
|
||||
},
|
||||
pink: {
|
||||
bg: 'badge.pink.bg',
|
||||
color: 'badge.pink.fg',
|
||||
},
|
||||
purple_alt: {
|
||||
bg: 'badge.purple_alt.bg',
|
||||
color: 'badge.purple_alt.fg',
|
||||
@@ -65,6 +69,47 @@ export const recipe = defineRecipe({
|
||||
bg: 'badge.blue_alt.bg',
|
||||
color: 'badge.blue_alt.fg',
|
||||
},
|
||||
// bright badges mainly used in other projects (e.g. autoscout, dev portal, etc.)
|
||||
bright_gray: {
|
||||
bg: 'badge.bright.gray.bg',
|
||||
color: 'badge.bright.gray.fg',
|
||||
},
|
||||
bright_green: {
|
||||
bg: 'badge.bright.green.bg',
|
||||
color: 'badge.bright.green.fg',
|
||||
},
|
||||
bright_red: {
|
||||
bg: 'badge.bright.red.bg',
|
||||
color: 'badge.bright.red.fg',
|
||||
},
|
||||
bright_blue: {
|
||||
bg: 'badge.bright.blue.bg',
|
||||
color: 'badge.bright.blue.fg',
|
||||
},
|
||||
bright_yellow: {
|
||||
bg: 'badge.bright.yellow.bg',
|
||||
color: 'badge.bright.yellow.fg',
|
||||
},
|
||||
bright_teal: {
|
||||
bg: 'badge.bright.teal.bg',
|
||||
color: 'badge.bright.teal.fg',
|
||||
},
|
||||
bright_cyan: {
|
||||
bg: 'badge.bright.cyan.bg',
|
||||
color: 'badge.bright.cyan.fg',
|
||||
},
|
||||
bright_orange: {
|
||||
bg: 'badge.bright.orange.bg',
|
||||
color: 'badge.bright.orange.fg',
|
||||
},
|
||||
bright_purple: {
|
||||
bg: 'badge.bright.purple.bg',
|
||||
color: 'badge.bright.purple.fg',
|
||||
},
|
||||
bright_pink: {
|
||||
bg: 'badge.bright.pink.bg',
|
||||
color: 'badge.bright.pink.fg',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
sm: {
|
||||
@@ -79,6 +124,13 @@ export const recipe = defineRecipe({
|
||||
py: '0.5',
|
||||
minH: '6',
|
||||
},
|
||||
lg: {
|
||||
textStyle: 'sm',
|
||||
px: '2',
|
||||
py: '1',
|
||||
minH: '7',
|
||||
fontWeight: '600',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -3,6 +3,7 @@ export type MultichainProviderConfig = {
|
||||
dapp_id?: string;
|
||||
url_template: string;
|
||||
logo: string;
|
||||
promo?: boolean;
|
||||
};
|
||||
|
||||
export type MultichainProviderConfigParsed = {
|
||||
@@ -10,4 +11,5 @@ export type MultichainProviderConfigParsed = {
|
||||
logoUrl: string;
|
||||
urlTemplate: string;
|
||||
dappId?: string;
|
||||
promo?: boolean;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { Address } from 'types/api/address';
|
||||
|
||||
import config from 'configs/app';
|
||||
import type { LinkProps } from 'toolkit/chakra/link';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
|
||||
const feature = config.features.multichainButton;
|
||||
|
||||
interface Props extends LinkProps {
|
||||
addressData?: Address;
|
||||
}
|
||||
|
||||
const AddressMultichainInfoButton = ({ addressData, ...rest }: Props) => {
|
||||
if (!feature.isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const promotedProvider = feature.providers.find((provider) => provider.promo);
|
||||
|
||||
if (!promotedProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!addressData || (addressData.is_contract && addressData.proxy_type !== 'eip7702')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = (() => {
|
||||
try {
|
||||
const url = new URL(promotedProvider.urlTemplate.replace('{address}', addressData.hash));
|
||||
url.searchParams.append('utm_source', 'blockscout');
|
||||
url.searchParams.append('utm_medium', 'address');
|
||||
return url.toString();
|
||||
} catch (error) {}
|
||||
return null;
|
||||
})();
|
||||
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={ url }
|
||||
variant="underlaid"
|
||||
external={ promotedProvider.dappId ? false : true }
|
||||
flexShrink={ 0 }
|
||||
{ ...rest }
|
||||
>
|
||||
Multichain info
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddressMultichainInfoButton);
|
||||
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,119 @@
|
||||
/* Animation source - https://codepen.io/ARS/pen/vEwEPP */
|
||||
.promo {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
margin: auto;
|
||||
z-index: -1;
|
||||
border-radius: 6px;
|
||||
|
||||
content: "";
|
||||
background: linear-gradient(120deg, #e15eff, #fffb22, #e15eff);
|
||||
background-size: 400% 400%;
|
||||
clip-path: polygon(
|
||||
0% 100%,
|
||||
4px 100%,
|
||||
4px 4px,
|
||||
20px 4px,
|
||||
20px 20px,
|
||||
4px 20px,
|
||||
4px 100%,
|
||||
100% 100%,
|
||||
100% 0%,
|
||||
0% 0%
|
||||
);
|
||||
animation: gradient 3s ease-in-out 0.5s infinite, border 5s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 14% 0%;
|
||||
}
|
||||
50% {
|
||||
background-position: 87% 100%;
|
||||
}
|
||||
100% {
|
||||
background-position: 14% 0%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes border {
|
||||
0% {
|
||||
clip-path: polygon(
|
||||
0% 100%,
|
||||
4px 100%,
|
||||
4px 100%,
|
||||
4px 100%,
|
||||
4px 100%,
|
||||
4px 100%,
|
||||
4px 100%,
|
||||
4px 100%,
|
||||
4px 100%,
|
||||
0% 100%
|
||||
);
|
||||
}
|
||||
5% {
|
||||
clip-path: polygon(
|
||||
0% 100%,
|
||||
4px 100%,
|
||||
4px 4px,
|
||||
4px 4px,
|
||||
4px 4px,
|
||||
4px 4px,
|
||||
4px 4px,
|
||||
4px 4px,
|
||||
4px 0%,
|
||||
0% 0%
|
||||
);
|
||||
}
|
||||
10% {
|
||||
clip-path: polygon(
|
||||
0% 100%,
|
||||
4px 100%,
|
||||
4px 4px,
|
||||
20px 4px,
|
||||
20px 4px,
|
||||
20px 4px,
|
||||
20px 4px,
|
||||
20px 4px,
|
||||
100% 0%,
|
||||
0% 0%
|
||||
);
|
||||
}
|
||||
15% {
|
||||
clip-path: polygon(
|
||||
0% 100%,
|
||||
4px 100%,
|
||||
4px 4px,
|
||||
20px 4px,
|
||||
20px 20px,
|
||||
20px 20px,
|
||||
20px 100%,
|
||||
100% 100%,
|
||||
100% 0%,
|
||||
0% 0%
|
||||
);
|
||||
}
|
||||
20%,
|
||||
100% {
|
||||
clip-path: polygon(
|
||||
0% 100%,
|
||||
4px 100%,
|
||||
4px 4px,
|
||||
20px 4px,
|
||||
20px 20px,
|
||||
4px 20px,
|
||||
4px 100%,
|
||||
100% 100%,
|
||||
100% 0%,
|
||||
0% 0%
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { capitalize } from 'es-toolkit';
|
||||
import { upperFirst } from 'es-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
import type { MultichainProviderConfigParsed } from 'types/client/multichainProviderConfig';
|
||||
@@ -8,36 +8,44 @@ import { route } from 'nextjs-routes';
|
||||
import { Image } from 'toolkit/chakra/image';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { Tooltip } from 'toolkit/chakra/tooltip';
|
||||
import TextSeparator from 'ui/shared/TextSeparator';
|
||||
|
||||
import styles from './AddressMultichainButton.module.css';
|
||||
|
||||
const TEMPLATE_ADDRESS = '{address}';
|
||||
|
||||
type Props = {
|
||||
hasSingleProvider: boolean;
|
||||
item: MultichainProviderConfigParsed;
|
||||
addressHash: string;
|
||||
onClick?: () => void;
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
};
|
||||
|
||||
const AddressMultichainButton = ({ item, addressHash, onClick, hasSingleProvider }: Props) => {
|
||||
const AddressMultichainButton = ({ item, addressHash, onClick, isFirst, isLast }: Props) => {
|
||||
|
||||
const isOnlyOne = isFirst && isLast;
|
||||
|
||||
const buttonIcon = (
|
||||
<Image
|
||||
src={ item.logoUrl }
|
||||
alt={ item.name }
|
||||
boxSize={ 5 }
|
||||
mr={ hasSingleProvider ? { base: 1, lg: 2 } : 0 }
|
||||
mr={ isOnlyOne ? { base: 1, lg: 2 } : 0 }
|
||||
borderRadius="4px"
|
||||
overflow="hidden"
|
||||
/>
|
||||
);
|
||||
|
||||
const buttonContent = hasSingleProvider ? (
|
||||
const name = upperFirst(item.name.replaceAll('_', ' '));
|
||||
|
||||
const buttonContent = isOnlyOne ? (
|
||||
<>
|
||||
{ buttonIcon }
|
||||
{ capitalize(item.name) }
|
||||
{ name }
|
||||
</>
|
||||
) : (
|
||||
<Tooltip content={ capitalize(item.name) }>{ buttonIcon }</Tooltip>
|
||||
<Tooltip content={ name }>{ buttonIcon }</Tooltip>
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -49,17 +57,21 @@ const AddressMultichainButton = ({ item, addressHash, onClick, hasSingleProvider
|
||||
const isExternal = typeof dappId !== 'string';
|
||||
|
||||
return (
|
||||
<Link
|
||||
external={ isExternal }
|
||||
href={ isExternal ? portfolioUrl.toString() : route({ pathname: '/apps/[id]', query: { id: dappId, url: portfolioUrl.toString() } }) }
|
||||
variant={ hasSingleProvider ? 'underlaid' : undefined }
|
||||
textStyle="sm"
|
||||
fontWeight="medium"
|
||||
onClick={ onClick }
|
||||
noIcon={ !hasSingleProvider }
|
||||
>
|
||||
{ buttonContent }
|
||||
</Link>
|
||||
<>
|
||||
<Link
|
||||
className={ item.promo ? styles.promo : undefined }
|
||||
external={ isExternal }
|
||||
href={ isExternal ? portfolioUrl.toString() : route({ pathname: '/apps/[id]', query: { id: dappId, url: portfolioUrl.toString() } }) }
|
||||
variant={ isOnlyOne ? 'underlaid' : undefined }
|
||||
textStyle="sm"
|
||||
fontWeight="medium"
|
||||
onClick={ onClick }
|
||||
noIcon={ !isOnlyOne }
|
||||
>
|
||||
{ buttonContent }
|
||||
</Link>
|
||||
{ item.promo && isFirst && !isLast && <TextSeparator mx={ 0 }/> }
|
||||
</>
|
||||
);
|
||||
} catch (error) {}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import AddressNetWorth from './AddressNetWorth';
|
||||
|
||||
const ADDRESS_HASH = addressMock.hash;
|
||||
const ICON_URL = 'https://localhost:3000/my-icon.png';
|
||||
const ICON_URL_PROMO = 'https://localhost:3000/my-icon-promo.png';
|
||||
|
||||
test.beforeEach(async({ mockApiResponse }) => {
|
||||
await mockApiResponse('general:address_tokens', tokensMock.erc20List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } });
|
||||
@@ -47,14 +48,16 @@ test('with single multichain button external', async({ render, mockEnvs, mockAss
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('with two multichain button external', async({ render, mockEnvs, mockAssetResponse }) => {
|
||||
test('with two multichain button and promo', async({ render, mockEnvs, mockAssetResponse }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG', `[
|
||||
{"name": "duck", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"},
|
||||
{"name": "duck2", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}
|
||||
{"name": "duck2", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"},
|
||||
{"name": "duck3", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL_PROMO }", "promo": true}
|
||||
]` ],
|
||||
]);
|
||||
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
|
||||
await mockAssetResponse(ICON_URL_PROMO, './playwright/mocks/image_s.jpg');
|
||||
|
||||
const component = await render(<AddressNetWorth addressData={ addressMock.eoa } addressHash={ ADDRESS_HASH }/>);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Text, Flex } from '@chakra-ui/react';
|
||||
import { Text, HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Address } from 'types/api/address';
|
||||
@@ -47,24 +47,26 @@ const AddressNetWorth = ({ addressData, isLoading, addressHash }: Props) => {
|
||||
|
||||
if (multichainFeature.isEnabled && (!addressData?.is_contract || isEip7702)) {
|
||||
const { providers } = multichainFeature;
|
||||
const hasSingleProvider = providers.length === 1;
|
||||
|
||||
multichainItems = (
|
||||
<>
|
||||
<TextSeparator/>
|
||||
<Flex alignItems="center" columnGap={ 2 }>
|
||||
<HStack columnGap={ 2 }>
|
||||
<Text>Multichain</Text>
|
||||
{ providers.map((item) => (
|
||||
<AddressMultichainButton
|
||||
key={ item.name }
|
||||
item={ item }
|
||||
addressHash={ addressHash }
|
||||
onClick={ onMultichainClick }
|
||||
hasSingleProvider={ hasSingleProvider }
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Flex>
|
||||
<HStack gap={{ base: 2, lg: 3 }}>
|
||||
{ providers.map((item, index) => (
|
||||
<AddressMultichainButton
|
||||
key={ item.name }
|
||||
item={ item }
|
||||
addressHash={ addressHash }
|
||||
onClick={ onMultichainClick }
|
||||
isFirst={ index === 0 }
|
||||
isLast={ index === providers.length - 1 }
|
||||
/>
|
||||
))
|
||||
}
|
||||
</HStack>
|
||||
</HStack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -5,7 +5,6 @@ import type { FormattedData } from './types';
|
||||
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import { Button } from 'toolkit/chakra/button';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import { space } from 'toolkit/utils/htmlEntities';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
@@ -39,6 +38,7 @@ const TokenSelectButton = ({ isOpen, isLoading, data, ...rest }: Props, ref: Rea
|
||||
onClick={ handleClick }
|
||||
gap={ 0 }
|
||||
aria-label="Token select"
|
||||
loadingSkeleton={ isLoading && !isOpen }
|
||||
{ ...rest }
|
||||
>
|
||||
<IconSvg name="tokens" boxSize={ 4 } mr={ 2 }/>
|
||||
@@ -56,18 +56,6 @@ const TokenSelectButton = ({ isOpen, isLoading, data, ...rest }: Props, ref: Rea
|
||||
</chakra.span>
|
||||
<IconSvg name="arrows/east-mini" transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 3 }/>
|
||||
</Button>
|
||||
{ isLoading && !isOpen && (
|
||||
<Skeleton
|
||||
loading
|
||||
h="100%"
|
||||
w="100%"
|
||||
position="absolute"
|
||||
top={ 0 }
|
||||
left={ 0 }
|
||||
bgColor="bg.primary"
|
||||
borderRadius="base"
|
||||
/>
|
||||
) }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ const ERC20TokensTableItem = ({
|
||||
}, [ chainValues ]);
|
||||
|
||||
return (
|
||||
<TableRow role="group" >
|
||||
<TableRow className="group" >
|
||||
<TableCell verticalAlign="middle">
|
||||
<HStack gap={ 2 }>
|
||||
<TokenEntity
|
||||
|
||||
@@ -81,20 +81,22 @@ const LatestTxsItem = ({ tx, isLoading }: Props) => {
|
||||
isLoading={ isLoading }
|
||||
mode="compact"
|
||||
/>
|
||||
<Flex flexDir="column" rowGap={ 3 }>
|
||||
{ !config.UI.views.tx.hiddenFields?.value && (
|
||||
<Skeleton loading={ isLoading }>
|
||||
<Text as="span" whiteSpace="pre">Value </Text>
|
||||
<Text as="span" color="text.secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() } { currencyUnits.ether }</Text>
|
||||
</Skeleton>
|
||||
) }
|
||||
{ !config.UI.views.tx.hiddenFields?.tx_fee && (
|
||||
<Skeleton loading={ isLoading } display="flex" whiteSpace="pre">
|
||||
<Text as="span">Fee </Text>
|
||||
<TxFee tx={ tx } accuracy={ 5 } color="text.secondary"/>
|
||||
</Skeleton>
|
||||
) }
|
||||
</Flex>
|
||||
{ !(config.UI.views.tx.hiddenFields?.value && config.UI.views.tx.hiddenFields?.tx_fee) ? (
|
||||
<Flex flexDir="column" rowGap={ 3 }>
|
||||
{ !config.UI.views.tx.hiddenFields?.value && (
|
||||
<Skeleton loading={ isLoading }>
|
||||
<Text as="span" whiteSpace="pre">Value </Text>
|
||||
<Text as="span" color="text.secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() } { currencyUnits.ether }</Text>
|
||||
</Skeleton>
|
||||
) }
|
||||
{ !config.UI.views.tx.hiddenFields?.tx_fee && (
|
||||
<Skeleton loading={ isLoading } display="flex" whiteSpace="pre">
|
||||
<Text as="span">Fee </Text>
|
||||
<TxFee tx={ tx } accuracy={ 5 } color="text.secondary"/>
|
||||
</Skeleton>
|
||||
) }
|
||||
</Flex>
|
||||
) : null }
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Flex,
|
||||
HStack,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
@@ -73,20 +74,23 @@ const LatestTxsItem = ({ tx, isLoading }: Props) => {
|
||||
to={ dataTo }
|
||||
isLoading={ isLoading }
|
||||
fontWeight="500"
|
||||
mb={ 3 }
|
||||
/>
|
||||
{ !config.UI.views.tx.hiddenFields?.value && (
|
||||
<Skeleton loading={ isLoading } mb={ 2 } w="fit-content">
|
||||
<Text as="span">Value </Text>
|
||||
<Text as="span" color="text.secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() } { currencyUnits.ether }</Text>
|
||||
</Skeleton>
|
||||
) }
|
||||
{ !config.UI.views.tx.hiddenFields?.tx_fee && (
|
||||
<Skeleton loading={ isLoading } w="fit-content" display="flex" whiteSpace="pre">
|
||||
<Text as="span">Fee </Text>
|
||||
<TxFee tx={ tx } accuracy={ 5 } color="text.secondary"/>
|
||||
</Skeleton>
|
||||
) }
|
||||
{ !(config.UI.views.tx.hiddenFields?.value && config.UI.views.tx.hiddenFields?.tx_fee) ? (
|
||||
<VStack rowGap={ 2 } mt={ 3 } alignItems="flex-start">
|
||||
{ !config.UI.views.tx.hiddenFields?.value && (
|
||||
<Skeleton loading={ isLoading } w="fit-content">
|
||||
<Text as="span">Value </Text>
|
||||
<Text as="span" color="text.secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() } { currencyUnits.ether }</Text>
|
||||
</Skeleton>
|
||||
) }
|
||||
{ !config.UI.views.tx.hiddenFields?.tx_fee && (
|
||||
<Skeleton loading={ isLoading } w="fit-content" display="flex" whiteSpace="pre">
|
||||
<Text as="span">Fee </Text>
|
||||
<TxFee tx={ tx } accuracy={ 5 } color="text.secondary"/>
|
||||
</Skeleton>
|
||||
) }
|
||||
</VStack>
|
||||
) : null }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -137,7 +137,7 @@ const OpSuperchainAddressDetails = ({ data, addressHash, isLoading }: Props) =>
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue whiteSpace="pre-wrap">
|
||||
<Skeleton loading={ isLoading }>
|
||||
Local <Link href={ route({ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'txs_local' } }) }>view by chain</Link>
|
||||
<Link href={ route({ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'txs_local' } }) }>View by chain</Link>
|
||||
</Skeleton>
|
||||
</DetailedInfo.ItemValue>
|
||||
|
||||
@@ -149,7 +149,7 @@ const OpSuperchainAddressDetails = ({ data, addressHash, isLoading }: Props) =>
|
||||
</DetailedInfo.ItemLabel>
|
||||
<DetailedInfo.ItemValue whiteSpace="pre-wrap">
|
||||
<Skeleton loading={ isLoading }>
|
||||
Local <Link href={ route({ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'token_transfers_local' } }) }>view by chain</Link>
|
||||
<Link href={ route({ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'token_transfers_local' } }) }>View by chain</Link>
|
||||
</Skeleton>
|
||||
</DetailedInfo.ItemValue>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
@@ -50,13 +50,13 @@ const OpSuperchainTokenSelect = ({ isLoading, isError, data }: Props) => {
|
||||
href={ route({ pathname: '/address/[hash]', query: { hash, tab: 'tokens' } }) }
|
||||
asChild
|
||||
scroll={ false }
|
||||
loading={ isLoading }
|
||||
>
|
||||
<IconButton
|
||||
aria-label="Show all tokens"
|
||||
variant="icon_secondary"
|
||||
size="md"
|
||||
onClick={ handleIconButtonClick }
|
||||
loadingSkeleton={ isLoading }
|
||||
>
|
||||
<IconSvg name="wallet"/>
|
||||
</IconButton>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Box, HStack } from '@chakra-ui/react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { SocketMessage } from 'lib/socket/types';
|
||||
import type { ClusterChainConfig } from 'types/multichain';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import useSocketChannel from 'lib/socket/useSocketChannel';
|
||||
import useSocketMessage from 'lib/socket/useSocketMessage';
|
||||
import { BLOCK } from 'stubs/block';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
|
||||
import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip';
|
||||
|
||||
interface Props {
|
||||
@@ -50,20 +48,13 @@ const ChainLatestBlockInfo = ({ chainData }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack gap={ 2 }>
|
||||
<Box color="text.secondary">Latest block</Box>
|
||||
<Link
|
||||
loading={ blocksQuery.isPlaceholderData }
|
||||
href={ route({
|
||||
pathname: '/chain/[chain_slug]/block/[height_or_hash]',
|
||||
query: {
|
||||
chain_slug: chainData.slug,
|
||||
height_or_hash: blocksQuery.data[0].height.toString(),
|
||||
},
|
||||
}) }
|
||||
>
|
||||
{ blocksQuery.data[0].height }
|
||||
</Link>
|
||||
<Box display="grid" gridTemplateColumns="repeat(3, auto)" gap={ 2 }>
|
||||
<Box color="text.secondary" whiteSpace="nowrap">Latest block</Box>
|
||||
<BlockEntity
|
||||
number={ blocksQuery.data[0].height }
|
||||
isLoading={ blocksQuery.isPlaceholderData }
|
||||
noIcon
|
||||
/>
|
||||
<TimeWithTooltip
|
||||
timestamp={ blocksQuery.data[0].timestamp }
|
||||
enableIncrement={ !blocksQuery.isPlaceholderData }
|
||||
@@ -72,7 +63,7 @@ const ChainLatestBlockInfo = ({ chainData }: Props) => {
|
||||
flexShrink={ 0 }
|
||||
timeFormat="relative"
|
||||
/>
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ const ChainWidget = ({ data }: Props) => {
|
||||
</HStack>
|
||||
</HStack>
|
||||
<Heading my={ 3 } textStyle="heading.md ">{ data.name }</Heading>
|
||||
<RollupStageBadge chainConfig={ data.app_config } variant="filled" mb={ 2.5 }/>
|
||||
<RollupStageBadge chainConfig={ data.app_config } mb={ 2.5 }/>
|
||||
<VStack gap={ 2 } alignItems="flex-start" fontWeight={ 500 }>
|
||||
<HStack gap={ 2 }>
|
||||
<Box color="text.secondary">Chain ID</Box>
|
||||
|
||||
@@ -30,8 +30,8 @@ const LatestTxsLocal = () => {
|
||||
const query = useApiQuery('general:homepage_txs', {
|
||||
chain,
|
||||
queryOptions: {
|
||||
placeholderData: Array(5).fill(TX),
|
||||
select: (data) => data.slice(0, 5),
|
||||
placeholderData: Array(3).fill(TX),
|
||||
select: (data) => data.slice(0, 3),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const OpSuperchainTokenTransfersLocal = ({ query, typeFilter, onTokenTypesChange
|
||||
<Box hideFrom="lg">
|
||||
{ query.data?.items.map((item, index) => (
|
||||
<TokenTransfersListItem
|
||||
key={ item.transaction_hash + item.log_index + (query.isPlaceholderData ? index : '') }
|
||||
key={ item.transaction_hash + item.log_index + (query.isPlaceholderData ? index : '') + (chainData ? chainData.id : '') }
|
||||
isLoading={ query.isPlaceholderData }
|
||||
item={ item }
|
||||
chainData={ chainData }
|
||||
|
||||
@@ -34,6 +34,7 @@ import AddressEpochRewards from 'ui/address/AddressEpochRewards';
|
||||
import AddressInternalTxs from 'ui/address/AddressInternalTxs';
|
||||
import AddressLogs from 'ui/address/AddressLogs';
|
||||
import AddressMud from 'ui/address/AddressMud';
|
||||
import AddressMultichainInfoButton from 'ui/address/AddressMultichainInfoButton';
|
||||
import AddressTokens from 'ui/address/AddressTokens';
|
||||
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
|
||||
import AddressTxs from 'ui/address/AddressTxs';
|
||||
@@ -449,6 +450,7 @@ const AddressPageContent = () => {
|
||||
<AddressQrCode hash={ addressQuery.data?.filecoin?.robust ?? checkSummedHash } isLoading={ isLoading }/>
|
||||
<AccountActionsMenu isLoading={ isLoading }/>
|
||||
<HStack ml="auto" gap={ 2 }/>
|
||||
<AddressMultichainInfoButton loading={ isLoading } addressData={ addressQuery.data }/>
|
||||
{ !isLoading && addressQuery.data?.is_contract && addressQuery.data?.is_verified && config.UI.views.address.solidityscanEnabled &&
|
||||
<SolidityscanReport hash={ hash }/> }
|
||||
{ !isLoading && nameServicesFeature.isEnabled && nameServicesFeature.ens.isEnabled &&
|
||||
|
||||
@@ -29,7 +29,7 @@ const AppActionButton = ({ data, className, txHash, source }: Props) => {
|
||||
}
|
||||
}, [ source, appID, actionURL ]);
|
||||
|
||||
if ((!appID && !appMarketplaceURL) || (!appActionButtonText && !appLogoURL)) {
|
||||
if ((!appID && !actionURL) || (!appActionButtonText && !appLogoURL)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -50,13 +50,11 @@ const AppActionButton = ({ data, className, txHash, source }: Props) => {
|
||||
</>
|
||||
);
|
||||
|
||||
const isExternal = !appID;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={ className }
|
||||
href={ isExternal ? actionURL : route({ pathname: '/apps/[id]', query: { id: appID, action: 'connect', ...(actionURL ? { url: actionURL } : {}) } }) }
|
||||
external={ isExternal }
|
||||
href={ actionURL ?? (appID ? route({ pathname: '/apps/[id]', query: { id: appID, action: 'connect' } }) : undefined) }
|
||||
external={ Boolean(actionURL) }
|
||||
onClick={ handleClick }
|
||||
variant="underlaid"
|
||||
iconColor={ textColor }
|
||||
|
||||
@@ -40,12 +40,45 @@ const BadgeShowcase = () => {
|
||||
<Sample label="colorPalette: cyan">
|
||||
<Badge colorPalette="cyan">Internal txn</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: pink">
|
||||
<Badge colorPalette="pink">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: purple_alt">
|
||||
<Badge colorPalette="purple_alt">read</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: blue_alt">
|
||||
<Badge colorPalette="blue_alt">write</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: bright_gray">
|
||||
<Badge colorPalette="bright_gray">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: bright_green">
|
||||
<Badge colorPalette="bright_green">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: bright_red">
|
||||
<Badge colorPalette="bright_red">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: bright_blue">
|
||||
<Badge colorPalette="bright_blue">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: bright_yellow">
|
||||
<Badge colorPalette="bright_yellow">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: bright_teal">
|
||||
<Badge colorPalette="bright_teal">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: bright_cyan">
|
||||
<Badge colorPalette="bright_cyan">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: bright_orange">
|
||||
<Badge colorPalette="bright_orange">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: bright_purple">
|
||||
<Badge colorPalette="bright_purple">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="colorPalette: bright_pink">
|
||||
<Badge colorPalette="bright_pink">Content</Badge>
|
||||
</Sample>
|
||||
</SamplesStack>
|
||||
</Section>
|
||||
<Section>
|
||||
@@ -59,11 +92,14 @@ const BadgeShowcase = () => {
|
||||
<Section>
|
||||
<SectionHeader>Size</SectionHeader>
|
||||
<SamplesStack>
|
||||
<Sample label="size: sm">
|
||||
<Badge size="sm">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="size: md">
|
||||
<Badge size="md">Content</Badge>
|
||||
</Sample>
|
||||
<Sample label="size: sm">
|
||||
<Badge size="sm">Content</Badge>
|
||||
<Sample label="size: lg">
|
||||
<Badge size="lg">Content</Badge>
|
||||
</Sample>
|
||||
</SamplesStack>
|
||||
</Section>
|
||||
|
||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 116 KiB |
@@ -113,7 +113,7 @@ const Footer = () => {
|
||||
_empty={{ display: 'none' }}
|
||||
>
|
||||
{ !config.UI.indexingAlert.intTxs.isHidden && <IntTxsIndexingStatus/> }
|
||||
<NetworkAddToWallet source="Footer"/>
|
||||
{ !config.features.opSuperchain.isEnabled && <NetworkAddToWallet source="Footer"/> }
|
||||
</Flex>
|
||||
);
|
||||
}, []);
|
||||
|
||||
@@ -8,10 +8,9 @@ import { Tooltip } from 'toolkit/chakra/tooltip';
|
||||
|
||||
interface Props extends ImageProps {
|
||||
chainConfig?: typeof config;
|
||||
variant?: 'outline' | 'filled';
|
||||
}
|
||||
|
||||
const RollupStageBadge = ({ chainConfig = config, variant = 'outline', ...props }: Props) => {
|
||||
const RollupStageBadge = ({ chainConfig = config, ...props }: Props) => {
|
||||
|
||||
const feature = chainConfig.features.rollup;
|
||||
|
||||
@@ -31,18 +30,10 @@ const RollupStageBadge = ({ chainConfig = config, variant = 'outline', ...props
|
||||
</>
|
||||
);
|
||||
|
||||
const src = (() => {
|
||||
if (variant === 'filled') {
|
||||
return feature.stageIndex === '1' ? '/static/labels/stage-1-filled.svg' : '/static/labels/stage-2-filled.svg';
|
||||
}
|
||||
|
||||
return feature.stageIndex === '1' ? '/static/labels/stage-1.svg' : '/static/labels/stage-2.svg';
|
||||
})();
|
||||
|
||||
return (
|
||||
<Tooltip content={ tooltipContent } interactive>
|
||||
<Image
|
||||
src={ src }
|
||||
src={ feature.stageIndex === '1' ? '/static/labels/stage-1.svg' : '/static/labels/stage-2.svg' }
|
||||
h="14px"
|
||||
w="42px"
|
||||
{ ...props }
|
||||
|
||||
@@ -27,6 +27,7 @@ const TopBar = () => {
|
||||
web3.data?.wallet &&
|
||||
config.chain.rpcUrls.length &&
|
||||
config.features.web3Wallet.isEnabled &&
|
||||
!config.features.opSuperchain.isEnabled &&
|
||||
!isMobile,
|
||||
);
|
||||
const hasDeFiDropdown = Boolean(config.features.deFiDropdown.isEnabled);
|
||||
|
||||
@@ -56,7 +56,8 @@ test('base view +@dark-mode', async({ render, page }) => {
|
||||
|
||||
test.describe('action button', () => {
|
||||
test.beforeEach(async({ mockApiResponse, mockAssetResponse }) => {
|
||||
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
|
||||
const meta = { ...protocolTagWithMeta.meta, appMarketplaceURL: undefined };
|
||||
const metadataResponse = generateAddressMetadataResponse({ ...protocolTagWithMeta, meta });
|
||||
await mockApiResponse('metadata:info', metadataResponse, { queryParams: addressMetadataQueryParams });
|
||||
await mockAssetResponse(protocolTagWithMeta?.meta?.appLogoURL as string, './playwright/mocks/image_s.jpg');
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ const TokenTransferTable = ({ items, top, isLoading, chainData }: Props) => {
|
||||
<TableBody>
|
||||
{ items?.map((item, index) => (
|
||||
<TokenTransferTableItem
|
||||
key={ item.transaction_hash + item.log_index + (isLoading ? index : '') }
|
||||
key={ item.transaction_hash + item.log_index + (isLoading ? index : '') + (chainData ? chainData.id : '') }
|
||||
item={ item }
|
||||
isLoading={ isLoading }
|
||||
chainData={ chainData }
|
||||
|
||||
@@ -33,15 +33,19 @@ const Tokens = ({ query, onSortChange, sort, actionBar, description, hasActiveFi
|
||||
<>
|
||||
<Box hideFrom="lg">
|
||||
{ description }
|
||||
{ data.items.map((item, index) => (
|
||||
<TokensListItem
|
||||
key={ item.address_hash + (isPlaceholderData ? index : '') }
|
||||
token={ item }
|
||||
index={ index }
|
||||
page={ pagination.page }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
)) }
|
||||
{ data.items.map((item, index) => {
|
||||
const chainIds = 'chain_infos' in item ? Object.keys(item.chain_infos).join(',') : undefined;
|
||||
|
||||
return (
|
||||
<TokensListItem
|
||||
key={ item.address_hash + (isPlaceholderData ? index : '') + (chainIds ? chainIds : '') }
|
||||
token={ item }
|
||||
index={ index }
|
||||
page={ pagination.page }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
</Box>
|
||||
<Box hideBelow="lg">
|
||||
{ description }
|
||||
|
||||
@@ -92,9 +92,19 @@ const TokensTable = ({ items, page, isLoading, sorting, setSorting, top }: Props
|
||||
</TableRow>
|
||||
</TableHeaderSticky>
|
||||
<TableBody>
|
||||
{ items.map((item, index) => (
|
||||
<TokensTableItem key={ item.address_hash + (isLoading ? index : '') } token={ item } index={ index } page={ page } isLoading={ isLoading }/>
|
||||
)) }
|
||||
{ items.map((item, index) => {
|
||||
const chainIds = 'chain_infos' in item ? Object.keys(item.chain_infos).join(',') : undefined;
|
||||
|
||||
return (
|
||||
<TokensTableItem
|
||||
key={ item.address_hash + (isLoading ? index : '') + (chainIds ? chainIds : '') }
|
||||
token={ item }
|
||||
index={ index }
|
||||
page={ page }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
</TableBody>
|
||||
</TableRoot>
|
||||
);
|
||||
|
||||
@@ -58,7 +58,8 @@ test.describe('blockscout provider', () => {
|
||||
});
|
||||
|
||||
test('with interpretation and action button +@mobile +@dark-mode', async({ render, mockApiResponse, mockAssetResponse }) => {
|
||||
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
|
||||
const meta = { ...protocolTagWithMeta.meta, appMarketplaceURL: undefined };
|
||||
const metadataResponse = generateAddressMetadataResponse({ ...protocolTagWithMeta, meta });
|
||||
await mockApiResponse('metadata:info', metadataResponse, { queryParams: addressMetadataQueryParams });
|
||||
await mockAssetResponse(protocolTagWithMeta?.meta?.appLogoURL as string, './playwright/mocks/image_s.jpg');
|
||||
await mockApiResponse('general:tx_interpretation', txInterpretation, { pathParams: { hash } });
|
||||
@@ -100,7 +101,6 @@ test.describe('blockscout provider', () => {
|
||||
test('with interpretation and view all link, and action button (external link) +@mobile', async({
|
||||
render, mockApiResponse, mockAssetResponse,
|
||||
}) => {
|
||||
delete protocolTagWithMeta?.meta?.appID;
|
||||
const metadataResponse = generateAddressMetadataResponse(protocolTagWithMeta);
|
||||
await mockApiResponse('metadata:info', metadataResponse, { queryParams: addressMetadataQueryParams });
|
||||
await mockAssetResponse(protocolTagWithMeta?.meta?.appLogoURL as string, './playwright/mocks/image_s.jpg');
|
||||
|
||||
@@ -58,6 +58,14 @@ const TxsTable = ({
|
||||
|
||||
const TableHeaderComponent = stickyHeader ? TableHeaderSticky : TableHeader;
|
||||
|
||||
const columnNum = [
|
||||
showBlockInfo,
|
||||
true,
|
||||
!config.UI.views.tx.hiddenFields?.value,
|
||||
!config.UI.views.tx.hiddenFields?.tx_fee,
|
||||
].filter(Boolean).length;
|
||||
const baseWidth = `${ 100 / columnNum }%`;
|
||||
|
||||
return (
|
||||
<AddressHighlightProvider>
|
||||
<TableRoot minWidth="1000px">
|
||||
@@ -70,11 +78,11 @@ const TxsTable = ({
|
||||
<TimeFormatToggle/>
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader width="160px">Type</TableColumnHeader>
|
||||
<TableColumnHeader width="20%">Method</TableColumnHeader>
|
||||
<TableColumnHeader width={ baseWidth }>Method</TableColumnHeader>
|
||||
{ showBlockInfo && (
|
||||
onSortToggle ? (
|
||||
<TableColumnHeaderSortable
|
||||
width="18%"
|
||||
width={ baseWidth }
|
||||
sortField="block_number"
|
||||
sortValue={ sort }
|
||||
onSortToggle={ onSortToggle }
|
||||
@@ -82,14 +90,14 @@ const TxsTable = ({
|
||||
Block
|
||||
</TableColumnHeaderSortable>
|
||||
) : (
|
||||
<TableColumnHeader width="18%">Block</TableColumnHeader>
|
||||
<TableColumnHeader width={ baseWidth }>Block</TableColumnHeader>
|
||||
)
|
||||
) }
|
||||
<TableColumnHeader width="224px">From/To</TableColumnHeader>
|
||||
<TableColumnHeader width={ columnNum <= 2 ? baseWidth : '224px' }>From/To</TableColumnHeader>
|
||||
{ !config.UI.views.tx.hiddenFields?.value && (
|
||||
onSortToggle ? (
|
||||
<TableColumnHeaderSortable
|
||||
width="20%"
|
||||
width={ baseWidth }
|
||||
isNumeric
|
||||
sortField="value"
|
||||
sortValue={ sort }
|
||||
@@ -98,13 +106,13 @@ const TxsTable = ({
|
||||
{ `Value ${ currencyUnits.ether }` }
|
||||
</TableColumnHeaderSortable>
|
||||
) : (
|
||||
<TableColumnHeader width="20%" isNumeric>Value</TableColumnHeader>
|
||||
<TableColumnHeader width={ baseWidth } isNumeric>Value</TableColumnHeader>
|
||||
)
|
||||
) }
|
||||
{ !config.UI.views.tx.hiddenFields?.tx_fee && (
|
||||
onSortToggle ? (
|
||||
<TableColumnHeaderSortable
|
||||
width="20%"
|
||||
width={ baseWidth }
|
||||
isNumeric
|
||||
pr={ 5 }
|
||||
sortField="fee"
|
||||
@@ -114,7 +122,7 @@ const TxsTable = ({
|
||||
{ `Fee${ feeCurrency }` }
|
||||
</TableColumnHeaderSortable>
|
||||
) : (
|
||||
<TableColumnHeader width="20%" isNumeric pr={ 5 }>Fee</TableColumnHeader>
|
||||
<TableColumnHeader width={ baseWidth } isNumeric pr={ 5 }>Fee</TableColumnHeader>
|
||||
)
|
||||
) }
|
||||
</TableRow>
|
||||
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
@@ -5103,10 +5103,10 @@
|
||||
dependencies:
|
||||
webpack-bundle-analyzer "4.10.1"
|
||||
|
||||
"@next/[email protected].2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.2.tgz#0c6b959313cd6e71afb69bf0deb417237f1d2f8a"
|
||||
integrity sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==
|
||||
"@next/[email protected].7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.5.7.tgz#4168db34ae3bc9fd9ad3b951d327f4cfc38d4362"
|
||||
integrity sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==
|
||||
|
||||
"@next/[email protected]":
|
||||
version "15.0.3"
|
||||
@@ -5115,45 +5115,45 @@
|
||||
dependencies:
|
||||
fast-glob "3.3.1"
|
||||
|
||||
"@next/[email protected].2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz#f69713326fc08f2eff3726fe19165cdb429d67c7"
|
||||
integrity sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==
|
||||
"@next/[email protected].7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz#f0c9ccfec2cd87cbd4b241ce4c779a7017aed958"
|
||||
integrity sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==
|
||||
|
||||
"@next/[email protected].2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz#560a9da4126bae75cbbd6899646ad7a2e4fdcc9b"
|
||||
integrity sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==
|
||||
"@next/[email protected].7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz#18009e9fcffc5c0687cc9db24182ddeac56280d9"
|
||||
integrity sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==
|
||||
|
||||
"@next/[email protected].2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz#80b2be276e775e5a9286369ae54e536b0cdf8c3a"
|
||||
integrity sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==
|
||||
"@next/[email protected].7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz#fe7c7e08264cf522d4e524299f6d3e63d68d579a"
|
||||
integrity sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==
|
||||
|
||||
"@next/[email protected].2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz#68cf676301755fd99aca11a7ebdb5eae88d7c2e4"
|
||||
integrity sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==
|
||||
"@next/[email protected].7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz#94228fe293475ec34a5a54284e1056876f43a3cf"
|
||||
integrity sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==
|
||||
|
||||
"@next/[email protected].2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz#209d9a79d0f2333544f863b0daca3f7e29f2eaff"
|
||||
integrity sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==
|
||||
"@next/[email protected].7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz#078c71201dfe7fcfb8fa6dc92aae6c94bc011cdc"
|
||||
integrity sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==
|
||||
|
||||
"@next/[email protected].2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz#d4ad1cfb5e99e51db669fe2145710c1abeadbd7f"
|
||||
integrity sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==
|
||||
"@next/[email protected].7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz#72947f5357f9226292353e0bb775643da3c7a182"
|
||||
integrity sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==
|
||||
|
||||
"@next/[email protected].2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz#070e10e370a5447a198c2db100389646aca2c496"
|
||||
integrity sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==
|
||||
"@next/[email protected].7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz#397b912cd51c6a80e32b9c0507ecd82514353941"
|
||||
integrity sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==
|
||||
|
||||
"@next/[email protected].2":
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz#9237d40b82eaf2efc88baeba15b784d4126caf4a"
|
||||
integrity sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==
|
||||
"@next/[email protected].7":
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz#e02b543d9dc6c1631d4ac239cb1177245dfedfe4"
|
||||
integrity sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==
|
||||
|
||||
"@noble/[email protected]":
|
||||
version "1.2.1"
|
||||
@@ -12682,13 +12682,13 @@ damerau-levenshtein@^1.0.8:
|
||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
|
||||
|
||||
[email protected].0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/dappscout-iframe/-/dappscout-iframe-0.3.0.tgz#785d46774552dbc2a29f4196b6f39daa53689cd6"
|
||||
integrity sha512-vHiDO9jXG4Q80ZZbtqiHrDXEYbO1q3nNGYnF5nbUpe7lrYiCHvXK/NUKXC6sHt1zp8tSfD95fmgY/6ZDCrgfpA==
|
||||
[email protected].2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/dappscout-iframe/-/dappscout-iframe-0.3.2.tgz#a338a69a23ca1f4abe45566659c733afa1db0f13"
|
||||
integrity sha512-YZN/GTCUKm+gTi9FkFeRxmNqvh3FY3pQS+JFUuCcK7aTFmfPzohJFG1ILZafZbI44V2mJAc63PEBfdiNbwJYiQ==
|
||||
dependencies:
|
||||
react "19.1.0"
|
||||
react-dom "19.1.0"
|
||||
react "19.1.2"
|
||||
react-dom "19.1.2"
|
||||
viem "^1.20.3"
|
||||
|
||||
data-uri-to-buffer@^4.0.0:
|
||||
@@ -17602,25 +17602,25 @@ [email protected]:
|
||||
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.4.tgz#ce6f68a4af543821bbc4755b59c0d3ced55c2d13"
|
||||
integrity sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==
|
||||
|
||||
[email protected].2:
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.5.2.tgz#5e50102443fb0328a9dfcac2d82465c7bac93693"
|
||||
integrity sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==
|
||||
[email protected].7:
|
||||
version "15.5.7"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.5.7.tgz#4507700b2bbcaf2c9fb7a9ad25c0dac2ba4a9a75"
|
||||
integrity sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==
|
||||
dependencies:
|
||||
"@next/env" "15.5.2"
|
||||
"@next/env" "15.5.7"
|
||||
"@swc/helpers" "0.5.15"
|
||||
caniuse-lite "^1.0.30001579"
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.6"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "15.5.2"
|
||||
"@next/swc-darwin-x64" "15.5.2"
|
||||
"@next/swc-linux-arm64-gnu" "15.5.2"
|
||||
"@next/swc-linux-arm64-musl" "15.5.2"
|
||||
"@next/swc-linux-x64-gnu" "15.5.2"
|
||||
"@next/swc-linux-x64-musl" "15.5.2"
|
||||
"@next/swc-win32-arm64-msvc" "15.5.2"
|
||||
"@next/swc-win32-x64-msvc" "15.5.2"
|
||||
"@next/swc-darwin-arm64" "15.5.7"
|
||||
"@next/swc-darwin-x64" "15.5.7"
|
||||
"@next/swc-linux-arm64-gnu" "15.5.7"
|
||||
"@next/swc-linux-arm64-musl" "15.5.7"
|
||||
"@next/swc-linux-x64-gnu" "15.5.7"
|
||||
"@next/swc-linux-x64-musl" "15.5.7"
|
||||
"@next/swc-win32-arm64-msvc" "15.5.7"
|
||||
"@next/swc-win32-x64-msvc" "15.5.7"
|
||||
sharp "^0.34.3"
|
||||
|
||||
nextjs-routes@^1.0.8:
|
||||
@@ -19079,10 +19079,10 @@ react-device-detect@^2.2.3:
|
||||
dependencies:
|
||||
ua-parser-js "^1.0.33"
|
||||
|
||||
[email protected].0:
|
||||
version "19.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.0.tgz#133558deca37fa1d682708df8904b25186793623"
|
||||
integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==
|
||||
[email protected].2:
|
||||
version "19.1.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.2.tgz#18ce6b14f682f4871a69cc6b2568d42af2cd5a33"
|
||||
integrity sha512-dEoydsCp50i7kS1xHOmPXq4zQYoGWedUsvqv9H6zdif2r7yLHygyfP9qou71TulRN0d6ng9EbRVsQhSqfUc19g==
|
||||
dependencies:
|
||||
scheduler "^0.26.0"
|
||||
|
||||
@@ -19319,10 +19319,10 @@ react-transition-group@^4.3.0, react-transition-group@^4.4.5:
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
[email protected].0:
|
||||
version "19.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75"
|
||||
integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
|
||||
[email protected].2:
|
||||
version "19.1.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.1.2.tgz#fd75c4f684b52fe45101381535c58486f4407545"
|
||||
integrity sha512-MdWVitvLbQULD+4DP8GYjZUrepGW7d+GQkNVqJEzNxE+e9WIa4egVFE/RDfVb1u9u/Jw7dNMmPB4IqxzbFYJ0w==
|
||||
|
||||
[email protected], readable-stream@^3.4.0, readable-stream@^3.6.2:
|
||||
version "3.6.2"
|
||||
|
||||