Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions configs/app/features/multichainButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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((a, b) => (b.promo ? 1 : -1)),
});
}

Expand Down
2 changes: 1 addition & 1 deletion configs/envs/.env.eth
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
16 changes: 12 additions & 4 deletions deploy/scripts/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 "$@"
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 32 additions & 16 deletions deploy/tools/essential-dapps-chains-config-generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
21 changes: 16 additions & 5 deletions deploy/tools/essential-dapps-chains-config-generator/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
29 changes: 20 additions & 9 deletions deploy/tools/multichain-config-generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
21 changes: 16 additions & 5 deletions deploy/tools/multichain-config-generator/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
3 changes: 3 additions & 0 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+ |
Expand Down Expand Up @@ -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` |

&nbsp;

Expand Down
2 changes: 2 additions & 0 deletions types/client/multichainProviderConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ export type MultichainProviderConfig = {
dapp_id?: string;
url_template: string;
logo: string;
promo?: boolean;
};

export type MultichainProviderConfigParsed = {
name: string;
logoUrl: string;
urlTemplate: string;
dappId?: string;
promo?: boolean;
};
57 changes: 57 additions & 0 deletions ui/address/AddressMultichainInfoButton.tsx
Original file line number Diff line number Diff line change
@@ -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);
Loading
Loading