mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
Merge pull request #2263 from blockscout/fe-2255
Zora: implement custom tag
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import type { Feature } from './types';
|
||||
import type { AddressProfileAPIConfig } from 'types/client/addressProfileAPIConfig';
|
||||
|
||||
import { getEnvValue, parseEnvJson } from '../utils';
|
||||
|
||||
const value = parseEnvJson<AddressProfileAPIConfig>(getEnvValue('NEXT_PUBLIC_ADDRESS_USERNAME_TAG'));
|
||||
|
||||
function checkApiUrlTemplate(apiUrlTemplate: string): boolean {
|
||||
try {
|
||||
const testUrl = apiUrlTemplate.replace('{address}', '0x0000000000000000000000000000000000000000');
|
||||
new URL(testUrl).toString();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const title = 'User profile API';
|
||||
|
||||
const config: Feature<{
|
||||
apiUrlTemplate: string;
|
||||
tagLinkTemplate?: string;
|
||||
tagIcon?: string;
|
||||
tagBgColor?: string;
|
||||
tagTextColor?: string;
|
||||
}> = (() => {
|
||||
if (value && checkApiUrlTemplate(value.api_url_template)) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
apiUrlTemplate: value.api_url_template,
|
||||
tagLinkTemplate: value.tag_link_template,
|
||||
tagIcon: value.tag_icon,
|
||||
tagBgColor: value.tag_bg_color,
|
||||
tagTextColor: value.tag_text_color,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: false,
|
||||
});
|
||||
})();
|
||||
|
||||
export default config;
|
||||
@@ -32,6 +32,7 @@ export { default as stats } from './stats';
|
||||
export { default as suave } from './suave';
|
||||
export { default as txInterpretation } from './txInterpretation';
|
||||
export { default as userOps } from './userOps';
|
||||
export { default as addressProfileAPI } from './addressProfileAPI';
|
||||
export { default as validators } from './validators';
|
||||
export { default as verifiedTokens } from './verifiedTokens';
|
||||
export { default as web3Wallet } from './web3Wallet';
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Set of ENVs for Zora Mainnet network explorer
|
||||
# https://explorer.zora.energy
|
||||
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=zora"
|
||||
|
||||
# Local ENVs
|
||||
NEXT_PUBLIC_APP_PROTOCOL=http
|
||||
NEXT_PUBLIC_APP_HOST=localhost
|
||||
NEXT_PUBLIC_APP_PORT=3000
|
||||
NEXT_PUBLIC_APP_ENV=development
|
||||
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
|
||||
|
||||
# Instance ENVs
|
||||
NEXT_PUBLIC_AD_BANNER_PROVIDER=none
|
||||
NEXT_PUBLIC_AD_TEXT_PROVIDER=none
|
||||
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
|
||||
NEXT_PUBLIC_API_BASE_PATH=/
|
||||
NEXT_PUBLIC_API_HOST=explorer.zora.energy
|
||||
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
|
||||
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
|
||||
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'}]
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/zora.json
|
||||
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x6d54c0226a57f5bc854f8aa589bb15113388f984f318c9e1b2722115e4e35873
|
||||
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true
|
||||
NEXT_PUBLIC_HAS_USER_OPS=true
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
|
||||
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=linear-gradient(89deg, rgb(63, 36, 22) 0.56%, rgb(44, 56, 105) 98.31%)
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
|
||||
NEXT_PUBLIC_LOGOUT_URL=https://zora-blockscout.us.auth0.com/v2/logout
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=patbqG4V2CI998jAq.9810c58c9de973ba2650621c94559088cbdfa1a914498e385621ed035d33c0d0
|
||||
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
|
||||
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
|
||||
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/zora-network/pools'}}]
|
||||
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora.svg
|
||||
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora-dark.svg
|
||||
NEXT_PUBLIC_NETWORK_ID=7777777
|
||||
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zora.svg
|
||||
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/zora-dark.svg
|
||||
NEXT_PUBLIC_NETWORK_NAME=Zora Mainnet
|
||||
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.zora.energy
|
||||
NEXT_PUBLIC_NETWORK_SHORT_NAME=Zora Mainnet
|
||||
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
|
||||
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
|
||||
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/zora-mainnet.png
|
||||
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/
|
||||
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://bridge.zora.energy
|
||||
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
|
||||
NEXT_PUBLIC_STATS_API_HOST=https://stats-l2-zora-mainnet.k8s-prod-1.blockscout.com
|
||||
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
|
||||
NEXT_PUBLIC_ADDRESS_USERNAME_TAG={'api_url_template': 'https://api.zora.co/discover/user/{address}', 'tag_link_template': 'httpszora.co/{username}', 'tag_icon': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/zora.svg', 'tag_bg_color': 'rgba(0,0,0)', 'tag_text_color': 'rgba(255,255,255)'}
|
||||
@@ -10,6 +10,7 @@ declare module 'yup' {
|
||||
import * as yup from 'yup';
|
||||
|
||||
import type { AdButlerConfig } from '../../../types/client/adButlerConfig';
|
||||
import type { AddressProfileAPIConfig } from '../../../types/client/addressProfileAPIConfig';
|
||||
import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS } from '../../../types/client/adProviders';
|
||||
import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../types/client/adProviders';
|
||||
import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS, type ContractCodeIde, type SmartContractVerificationMethodExtra } from '../../../types/client/contract';
|
||||
@@ -803,6 +804,20 @@ const schema = yup
|
||||
),
|
||||
}),
|
||||
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_ADDRESS_USERNAME_TAG: yup
|
||||
.mixed()
|
||||
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_ADDRESS_USERNAME_TAG, it should have api_url_template', (data) => {
|
||||
const isUndefined = data === undefined;
|
||||
const valueSchema = yup.object<AddressProfileAPIConfig>().transform(replaceQuotes).json().shape({
|
||||
api_url_template: yup.string().required(),
|
||||
tag_link_template: yup.string(),
|
||||
tag_icon: yup.string(),
|
||||
tag_bg_color: yup.string(),
|
||||
tag_text_color: yup.string(),
|
||||
});
|
||||
|
||||
return isUndefined || valueSchema.isValidSync(data);
|
||||
}),
|
||||
|
||||
// 6. External services envs
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
|
||||
|
||||
@@ -54,6 +54,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
|
||||
- [Data availability](ENVS.md#data-availability)
|
||||
- [Bridged tokens](ENVS.md#bridged-tokens)
|
||||
- [Safe{Core} address tags](ENVS.md#safecore-address-tags)
|
||||
- [Address profile API](ENVS.md#address-profile-api)
|
||||
- [SUAVE chain](ENVS.md#suave-chain)
|
||||
- [MetaSuites extension](ENVS.md#metasuites-extension)
|
||||
- [Validators list](ENVS.md#validators-list)
|
||||
@@ -653,6 +654,28 @@ For the smart contract addresses which are [Safe{Core} accounts](https://safe.gl
|
||||
|
||||
|
||||
|
||||
### Address profile API
|
||||
|
||||
This feature allows the integration of an external API to fetch user info for addresses or contracts. When configured, if the API returns a username, a public tag with a custom link will be displayed in the address page header.
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_ADDRESS_USERNAME_TAG | `{api_url: string; tag_link_template: string; tag_icon: string; tag_bg_color: string; tag_text_color: string}` | Address profile API tag configuration properties. See [below](#user-profile-api-configuration-properties). | - | - | `uniswap` | v1.35.0+ |
|
||||
|
||||
|
||||
|
||||
#### Address profile API configuration properties
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| api_url_template | `string` | User profile API URL. Should be a template with `{address}` variable | Required | - | `https://example-api.com/{address}` |
|
||||
| tag_link_template | `string` | External link to the profile. Should be a template with `{username}` variable | - | - | `https://example.com/{address}` |
|
||||
| tag_icon | `string` | Public tag icon (.svg) url | - | - | `https://example.com/icon.svg` |
|
||||
| tag_bg_color | `string` | Public tag background color (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | - | `\#000000` |
|
||||
| tag_text_color | `string` | Public tag text color (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | - | `\#FFFFFF` |
|
||||
|
||||
|
||||
|
||||
### SUAVE chain
|
||||
|
||||
For blockchains that implement SUAVE architecture additional fields will be shown on the transaction page ("Allowed peekers", "Kettle"). Users also will be able to see the list of all transactions for a particular Kettle in the separate view.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import config from 'configs/app';
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
|
||||
const feature = config.features.addressProfileAPI;
|
||||
|
||||
type AddressInfoApiQueryResponse = v.InferOutput<typeof AddressInfoSchema>;
|
||||
|
||||
const AddressInfoSchema = v.object({
|
||||
user_profile: v.object({
|
||||
username: v.union([ v.string(), v.null() ]),
|
||||
}),
|
||||
});
|
||||
|
||||
const ERROR_NAME = 'Invalid response schema';
|
||||
|
||||
export default function useAddressProfileApiQuery(hash: string | undefined, isEnabled = true) {
|
||||
const fetch = useFetch();
|
||||
|
||||
return useQuery<unknown, ResourceError<unknown>, AddressInfoApiQueryResponse>({
|
||||
queryKey: [ 'username_api', hash ],
|
||||
queryFn: async() => {
|
||||
if (!feature.isEnabled || !hash) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return fetch(feature.apiUrlTemplate.replace('{address}', hash), undefined, { omitSentryErrorLog: true });
|
||||
},
|
||||
enabled: isEnabled && Boolean(hash),
|
||||
refetchOnMount: false,
|
||||
select: (response) => {
|
||||
const parsedResponse = v.safeParse(AddressInfoSchema, response);
|
||||
|
||||
if (!parsedResponse.success) {
|
||||
throw Error(ERROR_NAME);
|
||||
}
|
||||
|
||||
return parsedResponse.output;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -16,6 +16,7 @@ function generateCspPolicy() {
|
||||
descriptors.monaco(),
|
||||
descriptors.safe(),
|
||||
descriptors.sentry(),
|
||||
descriptors.usernameApi(),
|
||||
descriptors.walletConnect(),
|
||||
);
|
||||
|
||||
|
||||
@@ -11,4 +11,5 @@ export { mixpanel } from './mixpanel';
|
||||
export { monaco } from './monaco';
|
||||
export { safe } from './safe';
|
||||
export { sentry } from './sentry';
|
||||
export { usernameApi } from './usernameApi';
|
||||
export { walletConnect } from './walletConnect';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type CspDev from 'csp-dev';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
const feature = config.features.addressProfileAPI;
|
||||
|
||||
export function usernameApi(): CspDev.DirectiveDescriptor {
|
||||
if (!feature.isEnabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const apiOrigin = (() => {
|
||||
try {
|
||||
const url = new URL(feature.apiUrlTemplate);
|
||||
return url.origin;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
'connect-src': [
|
||||
apiOrigin,
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@ const PRESETS = {
|
||||
stability_testnet: 'https://stability-testnet.blockscout.com',
|
||||
zkevm: 'https://zkevm.blockscout.com',
|
||||
zksync: 'https://zksync.blockscout.com',
|
||||
zora: 'https://explorer.zora.energy',
|
||||
// main === staging
|
||||
main: 'https://eth-sepolia.k8s-dev.blockscout.com',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export type AddressProfileAPIConfig = {
|
||||
api_url_template: string;
|
||||
tag_link_template?: string;
|
||||
tag_icon?: string;
|
||||
tag_bg_color?: string;
|
||||
tag_text_color?: string;
|
||||
};
|
||||
+20
-2
@@ -10,6 +10,7 @@ import getCheckedSummedAddress from 'lib/address/getCheckedSummedAddress';
|
||||
import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import { useAppContext } from 'lib/contexts/app';
|
||||
import useAddressProfileApiQuery from 'lib/hooks/useAddressProfileApiQuery';
|
||||
import useContractTabs from 'lib/hooks/useContractTabs';
|
||||
import useIsSafeAddress from 'lib/hooks/useIsSafeAddress';
|
||||
import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText';
|
||||
@@ -54,6 +55,7 @@ import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ];
|
||||
|
||||
const txInterpretation = config.features.txInterpretation;
|
||||
const addressProfileAPIFeature = config.features.addressProfileAPI;
|
||||
|
||||
const AddressPageContent = () => {
|
||||
const router = useRouter();
|
||||
@@ -92,6 +94,7 @@ const AddressPageContent = () => {
|
||||
|
||||
const addressesForMetadataQuery = React.useMemo(() => ([ hash ].filter(Boolean)), [ hash ]);
|
||||
const addressMetadataQuery = useAddressMetadataInfoQuery(addressesForMetadataQuery, areQueriesEnabled);
|
||||
const userPropfileApiQuery = useAddressProfileApiQuery(hash, addressProfileAPIFeature.isEnabled && areQueriesEnabled);
|
||||
|
||||
const addressEnsDomainsQuery = useApiQuery('addresses_lookup', {
|
||||
pathParams: { chainId: config.chain.id },
|
||||
@@ -248,6 +251,8 @@ const AddressPageContent = () => {
|
||||
mudTablesCountQuery.data,
|
||||
]);
|
||||
|
||||
const usernameApiTag = userPropfileApiQuery.data?.user_profile?.username;
|
||||
|
||||
const tags: Array<EntityTag> = React.useMemo(() => {
|
||||
return [
|
||||
...(addressQuery.data?.public_tags?.map((tag) => ({ slug: tag.label, name: tag.display_name, tagType: 'custom' as const, ordinal: -1 })) || []),
|
||||
@@ -258,6 +263,18 @@ const AddressPageContent = () => {
|
||||
addressQuery.data?.implementations?.length ? { slug: 'proxy', name: 'Proxy', tagType: 'custom' as const, ordinal: -1 } : undefined,
|
||||
addressQuery.data?.token ? { slug: 'token', name: 'Token', tagType: 'custom' as const, ordinal: -1 } : undefined,
|
||||
isSafeAddress ? { slug: 'safe', name: 'Multisig: Safe', tagType: 'custom' as const, ordinal: -10 } : undefined,
|
||||
addressProfileAPIFeature.isEnabled && usernameApiTag ? {
|
||||
slug: 'username_api',
|
||||
name: usernameApiTag,
|
||||
tagType: 'custom' as const,
|
||||
ordinal: 11,
|
||||
meta: {
|
||||
tagIcon: addressProfileAPIFeature.tagIcon,
|
||||
bgColor: addressProfileAPIFeature.tagBgColor,
|
||||
textColor: addressProfileAPIFeature.tagTextColor,
|
||||
tagUrl: addressProfileAPIFeature.tagLinkTemplate ? addressProfileAPIFeature.tagLinkTemplate.replace('{username}', usernameApiTag) : undefined,
|
||||
},
|
||||
} : undefined,
|
||||
config.features.userOps.isEnabled && userOpsAccountQuery.data ?
|
||||
{ slug: 'user_ops_acc', name: 'Smart contract wallet', tagType: 'custom' as const, ordinal: -10 } :
|
||||
undefined,
|
||||
@@ -267,7 +284,7 @@ const AddressPageContent = () => {
|
||||
...formatUserTags(addressQuery.data),
|
||||
...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags || []),
|
||||
].filter(Boolean).sort(sortEntityTags);
|
||||
}, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data, mudTablesCountQuery.data ]);
|
||||
}, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data, mudTablesCountQuery.data, usernameApiTag ]);
|
||||
|
||||
const titleContentAfter = (
|
||||
<EntityTags
|
||||
@@ -275,7 +292,8 @@ const AddressPageContent = () => {
|
||||
isLoading={
|
||||
isLoading ||
|
||||
(config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData) ||
|
||||
(config.features.addressMetadata.isEnabled && addressMetadataQuery.isPending)
|
||||
(config.features.addressMetadata.isEnabled && addressMetadataQuery.isPending) ||
|
||||
(addressProfileAPIFeature.isEnabled && userPropfileApiQuery.isPending)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user