Commit 2ac9ac87 authored by Max Alekseenko's avatar Max Alekseenko

Merge remote-tracking branch 'origin/tom2drum/issue-2029' into rewards

parents 379777bc 5552cd6d
......@@ -16,6 +16,7 @@ const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNE
const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL');
const ratingAirtableApiKey = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY');
const ratingAirtableBaseId = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID');
const graphLinksUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL');
const title = 'Marketplace';
......@@ -30,6 +31,7 @@ const config: Feature<(
featuredApp: string | undefined;
banner: { contentUrl: string; linkUrl: string } | undefined;
rating: { airtableApiKey: string; airtableBaseId: string } | undefined;
graphLinksUrl: string | undefined;
}> = (() => {
if (enabled === 'true' && chain.rpcUrl && submitFormUrl) {
const props = {
......@@ -46,6 +48,7 @@ const config: Feature<(
airtableApiKey: ratingAirtableApiKey,
airtableBaseId: ratingAirtableBaseId,
} : undefined,
graphLinksUrl,
};
if (configUrl) {
......
......@@ -16,7 +16,7 @@ const L2WithdrawalUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL');
const title = 'Rollup (L2) chain';
const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: string }> = (() => {
const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: string; homepage: { showLatestBlocks: boolean } }> = (() => {
if (type && L1BaseUrl) {
return Object.freeze({
......@@ -25,6 +25,9 @@ const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: s
type,
L1BaseUrl: stripTrailingSlash(L1BaseUrl),
L2WithdrawalUrl,
homepage: {
showLatestBlocks: getEnvValue('NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS') === 'true',
},
});
}
......
......@@ -42,6 +42,7 @@ NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/marketplace-graph-test/test-configs/marketplace-graph-links.json
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true
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'}
......
......@@ -18,6 +18,7 @@ ASSETS_ENVS=(
"NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL"
"NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL"
"NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL"
"NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL"
"NEXT_PUBLIC_FEATURED_NETWORKS"
"NEXT_PUBLIC_FOOTER_LINKS"
"NEXT_PUBLIC_NETWORK_LOGO"
......
......@@ -39,6 +39,7 @@ async function validateEnvs(appEnvs: Record<string, string>) {
'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL',
'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL',
'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL',
'NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL',
'NEXT_PUBLIC_FOOTER_LINKS',
];
......
......@@ -243,6 +243,14 @@ const marketplaceSchema = yup
// eslint-disable-next-line max-len
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
}),
NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL: yup
.string()
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
is: true,
then: (schema) => schema,
// eslint-disable-next-line max-len
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
}),
});
const beaconChainSchema = yup
......@@ -279,6 +287,17 @@ const rollupSchema = yup
then: (schema) => schema.test(urlTest).required(),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL can be used only if NEXT_PUBLIC_ROLLUP_TYPE is set to \'optimistic\' '),
}),
NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS: yup
.boolean()
.when('NEXT_PUBLIC_ROLLUP_TYPE', {
is: (value: string) => value,
then: (schema) => schema,
otherwise: (schema) => schema.test(
'not-exist',
'NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS cannot not be used if NEXT_PUBLIC_ROLLUP_TYPE is not defined',
value => value === undefined,
),
}),
});
const adButlerConfigSchema = yup
......
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://example.com
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://example.com
NEXT_PUBLIC_FAULT_PROOF_ENABLED=true
\ No newline at end of file
NEXT_PUBLIC_FAULT_PROOF_ENABLED=true
NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS=true
\ No newline at end of file
......@@ -435,6 +435,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals (Optimistic stack only) | Required for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ |
| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ |
| NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | v1.33.0+ |
| NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS | `boolean` | Set to `true` to display "Latest blocks" widget instead of "Latest batches" on the home page | - | - | `true` | v1.36.0+ |
&nbsp;
......@@ -507,6 +508,7 @@ This feature is **always enabled**, but you can disable it by passing `none` val
| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | v1.29.0+ |
| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY | `string` | Airtable API key | - | - | - | v1.33.0+ |
| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID | `string` | Airtable base ID with dapp ratings | - | - | - | v1.33.0+ |
| NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL | `string` | URL of the file (`.json` format only) which contains the list of The Graph links to be displayed on the Marketplace page | - | - | `https://example.com/graph_links.json` | v1.36.0+ |
#### Marketplace app configuration properties
......
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path fill="#6747ED" d="M10 20c5.523 0 10-4.477 10-10S15.523 0 10 0 0 4.477 0 10s4.477 10 10 10"/>
<path fill="#fff" fill-rule="evenodd" d="M9.854 11.292a2.66 2.66 0 0 1-2.666-2.667 2.66 2.66 0 0 1 2.666-2.667 2.66 2.66 0 0 1 2.667 2.667 2.66 2.66 0 0 1-2.667 2.667m0-6.667a4.001 4.001 0 0 1 0 8 4.001 4.001 0 0 1 0-8m3.813 8.208c.27.271.27.688 0 .938L11 16.437c-.27.271-.687.271-.937 0-.271-.27-.271-.687 0-.937l2.666-2.667c.25-.27.688-.27.938 0m1.541-7.541a.66.66 0 0 1-.666.666.66.66 0 0 1-.667-.666c0-.375.292-.667.667-.667.354 0 .666.292.666.667" clip-rule="evenodd"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30">
<path fill="currentColor" fill-rule="evenodd" d="m6.134 10.067 3.722-3.828a.97.97 0 0 1 1.337.06c.177.182.283.428.292.69a1.05 1.05 0 0 1-.234.704L9.243 9.765h14.371c.261 0 .513.107.7.299s.294.454.294.73c0 .274-.107.537-.294.729a.98.98 0 0 1-.7.299H6.831a.97.97 0 0 1-.55-.17 1 1 0 0 1-.368-.46 1.06 1.06 0 0 1 .22-1.126m18.723 6a.965.965 0 0 1 .55.17c.163.111.291.27.368.459a1.06 1.06 0 0 1-.22 1.125l-3.737 3.842-.006.008a1 1 0 0 1-.323.255.97.97 0 0 1-1.13-.197q-.146-.152-.224-.353a1.06 1.06 0 0 1 .281-1.16l.007-.007 2.022-2.086h-5.882c.104-.685.184-1.404 0-2.073z" clip-rule="evenodd"/>
<path fill="currentColor" d="M10 19.513c-1.272 0-2.48-.276-3.395-.778C5.57 18.169 5 17.374 5 16.497c0-.878.57-1.672 1.605-2.239.917-.502 2.123-.778 3.395-.778s2.48.276 3.395.778C14.43 14.825 15 15.622 15 16.497s-.57 1.671-1.605 2.238c-.917.502-2.123.778-3.395.778m0-4.793c-1.052 0-2.073.228-2.8.626-.61.334-.96.753-.96 1.15 0 .398.35.818.96 1.151.727.397 1.746.626 2.8.626s2.073-.228 2.8-.626c.61-.333.96-.753.96-1.15 0-.398-.35-.817-.96-1.151-.727-.398-1.746-.626-2.8-.626"/>
<path stroke="currentColor" stroke-width=".3" d="M10 19.513c-1.272 0-2.48-.276-3.395-.778C5.57 18.169 5 17.374 5 16.497c0-.878.57-1.672 1.605-2.239.917-.502 2.123-.778 3.395-.778s2.48.276 3.395.778C14.43 14.825 15 15.622 15 16.497s-.57 1.671-1.605 2.238c-.917.502-2.123.778-3.395.778Zm0-4.793c-1.052 0-2.073.228-2.8.626-.61.334-.96.753-.96 1.15 0 .398.35.818.96 1.151.727.397 1.746.626 2.8.626s2.073-.228 2.8-.626c.61-.333.96-.753.96-1.15 0-.398-.35-.817-.96-1.151-.727-.398-1.746-.626-2.8-.626Z"/>
<path fill="currentColor" d="M10 23.962c-1.272 0-2.48-.276-3.395-.778C5.57 22.618 5 21.823 5 20.946v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .397.35.817.96 1.151.727.397 1.748.626 2.8.626 1.053 0 2.073-.229 2.8-.626.61-.334.96-.754.96-1.151v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .877-.57 1.672-1.605 2.238-.917.502-2.123.778-3.395.778"/>
<path stroke="currentColor" stroke-width=".3" d="M10 23.962c-1.272 0-2.48-.276-3.395-.778C5.57 22.618 5 21.823 5 20.946v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .397.35.817.96 1.151.727.397 1.748.626 2.8.626 1.053 0 2.073-.229 2.8-.626.61-.334.96-.754.96-1.151v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .877-.57 1.672-1.605 2.238-.917.502-2.123.778-3.395.778Z"/>
<path fill="currentColor" d="M10 21.738c-1.272 0-2.48-.277-3.395-.778C5.57 20.393 5 19.598 5 18.72a.62.62 0 1 1 1.24 0c0 .397.35.817.96 1.151.727.398 1.748.626 2.8.626 1.053 0 2.073-.228 2.8-.626.61-.334.96-.754.96-1.15a.62.62 0 1 1 1.24 0c0 .876-.57 1.671-1.605 2.238-.917.501-2.123.778-3.395.778"/>
<path stroke="currentColor" stroke-width=".3" d="M10 21.738c-1.272 0-2.48-.277-3.395-.778C5.57 20.393 5 19.598 5 18.72a.62.62 0 1 1 1.24 0c0 .397.35.817.96 1.151.727.398 1.748.626 2.8.626 1.053 0 2.073-.228 2.8-.626.61-.334.96-.754.96-1.15a.62.62 0 1 1 1.24 0c0 .876-.57 1.671-1.605 2.238-.917.501-2.123.778-3.395.778Z"/>
</svg>
......@@ -37,6 +37,7 @@ import type {
AddressMudRecordsFilter,
AddressMudRecordsSorting,
AddressMudRecord,
AddressEpochRewardsResponse,
} from 'types/api/address';
import type { AddressesResponse, AddressesMetadataSearchResult, AddressesMetadataSearchFilters } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
......@@ -587,6 +588,11 @@ export const RESOURCES = {
pathParams: [ 'hash' as const ],
filterFields: [],
},
address_epoch_rewards: {
path: '/api/v2/addresses/:hash/election-rewards',
pathParams: [ 'hash' as const ],
filterFields: [],
},
// CONTRACT
contract: {
......@@ -681,6 +687,12 @@ export const RESOURCES = {
filterFields: [],
},
// TOKEN TRANSFERS
token_transfers_all: {
path: '/api/v2/token-transfers',
filterFields: [ 'type' as const ],
},
// APP STATS
stats: {
path: '/api/v2/stats',
......@@ -1084,7 +1096,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'addresses' | 'addresses_metadata_search' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' |
'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' |
'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' | 'address_epoch_rewards' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' |
......@@ -1097,7 +1109,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx' |
'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history';
'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history' |
'token_transfers_all';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -1264,6 +1277,7 @@ Q extends 'address_mud_tables' ? AddressMudTables :
Q extends 'address_mud_tables_count' ? number :
Q extends 'address_mud_records' ? AddressMudRecords :
Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'rewards_config' ? RewardsConfigResponse :
......@@ -1275,6 +1289,7 @@ Q extends 'rewards_user_balances' ? RewardsUserBalancesResponse :
Q extends 'rewards_user_daily_check' ? RewardsUserDailyCheckResponse :
Q extends 'rewards_user_daily_claim' ? RewardsUserDailyClaimResponse :
Q extends 'rewards_user_referrals' ? RewardsUserReferralsResponse :
Q extends 'token_transfers_all' ? TokenTransferResponse :
never;
/* eslint-enable @typescript-eslint/indent */
......@@ -1309,6 +1324,7 @@ Q extends 'user_ops' ? UserOpsFilters :
Q extends 'validators_stability' ? ValidatorsStabilityFilters :
Q extends 'address_mud_tables' ? AddressMudTablesFilter :
Q extends 'address_mud_records' ? AddressMudRecordsFilter :
Q extends 'token_transfers_all' ? TokenTransferFilters :
never;
/* eslint-enable @typescript-eslint/indent */
......
import { useQuery } from '@tanstack/react-query';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useFetch from 'lib/hooks/useFetch';
const feature = config.features.marketplace;
export default function useGraphLinks() {
const fetch = useFetch();
return useQuery<unknown, ResourceError<unknown>, Record<string, Array<{text: string; url: string}>>>({
queryKey: [ 'graph-links' ],
queryFn: async() => fetch((feature.isEnabled && feature.graphLinksUrl) ? feature.graphLinksUrl : '', undefined, { resource: 'graph-links' }),
enabled: feature.isEnabled && Boolean(feature.graphLinksUrl),
staleTime: Infinity,
placeholderData: {},
});
}
......@@ -187,6 +187,21 @@ export default function useNavItems(): ReturnType {
].filter(Boolean);
}
const tokensNavItems = [
{
text: 'Tokens',
nextRoute: { pathname: '/tokens' as const },
icon: 'token',
isActive: pathname.startsWith('/token'),
},
{
text: 'Token transfers',
nextRoute: { pathname: '/token-transfers' as const },
icon: 'token-transfers',
isActive: pathname === '/token-transfers',
},
];
const apiNavItems: Array<NavItem> = [
config.features.restApiDocs.isEnabled ? {
text: 'REST API',
......@@ -240,9 +255,9 @@ export default function useNavItems(): ReturnType {
},
{
text: 'Tokens',
nextRoute: { pathname: '/tokens' as const },
icon: 'token',
isActive: pathname.startsWith('/token'),
isActive: tokensNavItems.flat().some(item => isInternalItem(item) && item.isActive),
subItems: tokensNavItems,
},
config.features.marketplace.isEnabled ? {
text: 'DApps',
......
......@@ -52,6 +52,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/validators': 'Root page',
'/gas-tracker': 'Root page',
'/mud-worlds': 'Root page',
'/token-transfers': 'Root page',
// service routes, added only to make typescript happy
'/login': 'Regular page',
......
......@@ -56,6 +56,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/validators': DEFAULT_TEMPLATE,
'/gas-tracker': DEFAULT_TEMPLATE,
'/mud-worlds': DEFAULT_TEMPLATE,
'/token-transfers': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy
'/login': DEFAULT_TEMPLATE,
......
......@@ -41,8 +41,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/deposits': '%network_name% deposits (L1 > L2)',
'/output-roots': '%network_name% output roots',
'/dispute-games': '%network_name% dispute games',
'/batches': '%network_name% tx batches (L2 blocks)',
'/batches/[number]': '%network_name% L2 tx batch %number%',
'/batches': '%network_name% txn batches',
'/batches/[number]': '%network_name% L2 txn batch %number%',
'/blobs/[hash]': '%network_name% blob %hash% details',
'/ops': 'User operations on %network_name% - %network_name% explorer',
'/op/[hash]': '%network_name% user operation %hash%',
......@@ -52,6 +52,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/validators': '%network_name% validators list',
'/gas-tracker': '%network_name% gas tracker - Current gas fees',
'/mud-worlds': '%network_name% MUD worlds list',
'/token-transfers': '%network_name% token transfers',
// service routes, added only to make typescript happy
'/login': '%network_name% login',
......
......@@ -39,8 +39,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/deposits': 'Deposits (L1 > L2)',
'/output-roots': 'Output roots',
'/dispute-games': 'Dispute games',
'/batches': 'Tx batches (L2 blocks)',
'/batches/[number]': 'L2 tx batch details',
'/batches': 'Txn batches',
'/batches/[number]': 'L2 txn batch details',
'/blobs/[hash]': 'Blob details',
'/ops': 'User operations',
'/op/[hash]': 'User operation details',
......@@ -50,6 +50,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/validators': 'Validators list',
'/gas-tracker': 'Gas tracker',
'/mud-worlds': 'MUD worlds',
'/token-transfers': 'Token transfers',
// service routes, added only to make typescript happy
'/login': 'Login',
......
import _get from 'lodash/get';
import React from 'react';
import config from 'configs/app';
......@@ -24,9 +25,10 @@ export default function useAddOrSwitchChain() {
const errorObj = getErrorObj(error);
const code = errorObj && 'code' in errorObj ? errorObj.code : undefined;
const originalErrorCode = _get(errorObj, 'data.originalError.code');
// This error code indicates that the chain has not been added to Wallet.
if (code === 4902) {
if (code === 4902 || originalErrorCode === 4902) {
const params = [ {
chainId: hexadecimalChainId,
chainName: config.chain.name,
......
......@@ -48,12 +48,12 @@ export default function useWeb3Wallet({ source }: Params) {
const isConnected = isClientLoaded && !isDisconnected && address !== undefined;
return {
return React.useMemo(() => ({
connect: handleConnect,
disconnect: handleDisconnect,
isOpen: isOpening || isOpen,
isConnected,
address,
openModal,
};
}), [ handleConnect, handleDisconnect, isOpen, isOpening, isConnected, address, openModal ]);
}
import type { AddressEpochRewardsResponse } from 'types/api/address';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import { withEns, withName, withoutName } from './address';
export const epochRewards: AddressEpochRewardsResponse = {
items: [
{
type: 'delegated_payment',
amount: '136609473658452408568',
account: withName,
associated_account: withName,
block_hash: '0x',
block_number: 26369280,
epoch_number: 1526,
token: tokenInfo,
},
{
type: 'group',
amount: '117205842355246195095',
account: withoutName,
associated_account: withoutName,
block_hash: '0x',
block_number: 26352000,
epoch_number: 1525,
token: tokenInfo,
},
{
type: 'validator',
amount: '125659647325556554060',
account: withEns,
associated_account: withEns,
block_hash: '0x',
block_number: 26300160,
epoch_number: 1524,
token: tokenInfo,
},
],
next_page_params: null,
};
......@@ -102,3 +102,14 @@ export const noteTag: AddressMetadataTagApi = {
data: '<b>Warning!</b> This is scam! See the <a href="https://example.com" target="_blank">report</a>',
},
};
export const noteTag2: AddressMetadataTagApi = {
slug: 'note0',
name: 'note_0',
tagType: 'note',
ordinal: 0,
meta: {
alertStatus: 'info',
data: 'The token MILF was launched on May 13, 2021. The maximum total supply of the token is 100 billion.',
},
};
......@@ -42,6 +42,7 @@ export const erc20: TokenTransfer = {
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z',
block_number: '12345',
block_hash: '1',
log_index: '1',
method: 'updateSmartAsset',
......@@ -88,6 +89,7 @@ export const erc721: TokenTransfer = {
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z',
block_number: '12345',
block_hash: '1',
log_index: '1',
method: 'updateSmartAsset',
......@@ -136,6 +138,7 @@ export const erc1155A: TokenTransfer = {
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
type: 'token_minting',
timestamp: '2022-10-10T14:34:30.000000Z',
block_number: '12345',
block_hash: '1',
log_index: '1',
};
......@@ -214,6 +217,7 @@ export const erc404A: TokenTransfer = {
type: 'token_transfer',
method: 'swap',
timestamp: '2022-10-10T14:34:30.000000Z',
block_number: '12345',
block_hash: '1',
log_index: '1',
};
......
......@@ -58,6 +58,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }>
| DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }>
| StaticRoute<"/token-transfers">
| StaticRoute<"/tokens">
| DynamicRoute<"/tx/[hash]", { "hash": string }>
| StaticRoute<"/txs">
......
......@@ -29,6 +29,7 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => {
setCookie?.forEach((value) => {
nextRes.appendHeader('set-cookie', value);
});
nextRes.setHeader('content-type', apiRes.headers.get('content-type') || '');
nextRes.status(apiRes.status).send(apiRes.body);
};
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const TokenTransfers = dynamic(() => import('ui/pages/TokenTransfers'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/token-transfers">
<TokenTransfers/>
</PageNextJs>
);
};
export default Page;
export { base as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -26,6 +26,7 @@
| "block"
| "brands/blockscout"
| "brands/celenium"
| "brands/graph"
| "brands/safe"
| "brands/solidity_scan"
| "burger"
......@@ -154,6 +155,7 @@
| "swap"
| "testnet"
| "token-placeholder"
| "token-transfers"
| "token"
| "tokens"
| "tokens/xdai"
......
......@@ -3,6 +3,7 @@ import type {
AddressCoinBalanceHistoryItem,
AddressCollection,
AddressCounters,
AddressEpochRewardsItem,
AddressMudTableItem,
AddressNFT,
AddressTabsCounters,
......@@ -10,7 +11,7 @@ import type {
} from 'types/api/address';
import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams';
import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams';
import { MUD_SCHEMA, MUD_TABLE } from './mud';
import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token';
import { TX_HASH } from './tx';
......@@ -116,3 +117,14 @@ export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = {
schema: MUD_SCHEMA,
table: MUD_TABLE,
};
export const EPOCH_REWARD_ITEM: AddressEpochRewardsItem = {
amount: '136609473658452408568',
block_number: 10355938,
type: 'voter',
token: TOKEN_INFO_ERC_20,
block_hash: '0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6',
account: ADDRESS_PARAMS,
epoch_number: 1526,
associated_account: ADDRESS_PARAMS,
};
......@@ -91,6 +91,7 @@ export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenH
export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
block_hash: BLOCK_HASH,
block_number: '123456',
from: ADDRESS_PARAMS,
log_index: '4',
method: 'addLiquidity',
......
......@@ -154,33 +154,34 @@ const variantSubtle = defineStyle((props) => {
// for buttons in the hero banner
const variantHero = defineStyle((props) => {
const buttonConfig = config.UI.homepage.heroBanner?.button;
return {
bg: mode(
config.UI.homepage.heroBanner?.button?._default?.background?.[0] || 'blue.600',
config.UI.homepage.heroBanner?.button?._default?.background?.[1] || 'blue.600',
buttonConfig?._default?.background?.[0] || 'blue.600',
buttonConfig?._default?.background?.[1] || buttonConfig?._default?.background?.[0] || 'blue.600',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._default?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._default?.text_color?.[1] || 'white',
buttonConfig?._default?.text_color?.[0] || 'white',
buttonConfig?._default?.text_color?.[1] || buttonConfig?._default?.text_color?.[0] || 'white',
)(props),
_hover: {
bg: mode(
config.UI.homepage.heroBanner?.button?._hover?.background?.[0] || 'blue.400',
config.UI.homepage.heroBanner?.button?._hover?.background?.[1] || 'blue.400',
buttonConfig?._hover?.background?.[0] || 'blue.400',
buttonConfig?._hover?.background?.[1] || buttonConfig?._hover?.background?.[0] || 'blue.400',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[1] || 'white',
buttonConfig?._hover?.text_color?.[0] || 'white',
buttonConfig?._hover?.text_color?.[1] || buttonConfig?._hover?.text_color?.[0] || 'white',
)(props),
},
'&[data-selected=true]': {
bg: mode(
config.UI.homepage.heroBanner?.button?._selected?.background?.[0] || 'blue.50',
config.UI.homepage.heroBanner?.button?._selected?.background?.[1] || 'blue.50',
buttonConfig?._selected?.background?.[0] || 'blue.50',
buttonConfig?._selected?.background?.[1] || buttonConfig?._selected?.background?.[0] || 'blue.50',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[0] || 'blackAlpha.800',
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[1] || 'blackAlpha.800',
buttonConfig?._selected?.text_color?.[0] || 'blackAlpha.800',
buttonConfig?._selected?.text_color?.[1] || buttonConfig?._selected?.text_color?.[0] || 'blackAlpha.800',
)(props),
},
};
......
......@@ -30,39 +30,30 @@ const variantSimple = definePartsStyle((props) => {
});
const sizes = {
md: definePartsStyle({
th: {
px: 4,
fontSize: 'sm',
},
td: {
p: 4,
},
}),
sm: definePartsStyle({
th: {
px: '10px',
py: '10px',
fontSize: 'sm',
},
td: {
px: '10px',
py: 4,
fontSize: 'sm',
fontWeight: 500,
},
}),
xs: definePartsStyle({
th: {
px: '6px',
py: '10px',
fontSize: 'sm',
_first: {
pl: 3,
},
_last: {
pr: 3,
},
},
td: {
px: '6px',
py: 4,
fontSize: 'sm',
fontWeight: 500,
lineHeight: 5,
_first: {
pl: 3,
},
_last: {
pr: 3,
},
},
}),
};
......@@ -104,6 +95,10 @@ const Table = defineMultiStyleConfig({
baseStyle,
sizes,
variants,
defaultProps: {
size: 'sm',
variant: 'simple',
},
});
export default Table;
import type { Transaction } from 'types/api/transaction';
import type { UserTags, AddressImplementation } from './addressParams';
import type { Block } from './block';
import type { UserTags, AddressImplementation, AddressParam } from './addressParams';
import type { Block, EpochRewardsType } from './block';
import type { InternalTransaction } from './internalTransaction';
import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
......@@ -191,6 +191,7 @@ export type AddressTabsCounters = {
transactions_count: number | null;
validations_count: number | null;
withdrawals_count: number | null;
celo_election_rewards_count?: number | null;
}
// MUD framework
......@@ -245,3 +246,25 @@ export type AddressMudRecord = {
schema: MudWorldSchema;
table: MudWorldTable;
}
export type AddressEpochRewardsResponse = {
items: Array<AddressEpochRewardsItem>;
next_page_params: {
amount: string;
associated_account_address_hash: string;
block_number: number;
items_count: number;
type: EpochRewardsType;
} | null;
}
export type AddressEpochRewardsItem = {
type: EpochRewardsType;
token: TokenInfo;
amount: string;
block_number: number;
block_hash: string;
account: AddressParam;
epoch_number: number;
associated_account: AddressParam;
}
......@@ -144,6 +144,8 @@ export interface BlockEpochElectionReward {
total: string;
}
export type EpochRewardsType = 'group' | 'validator' | 'delegated_payment' | 'voter';
export interface BlockEpoch {
number: number;
distribution: {
......@@ -151,12 +153,7 @@ export interface BlockEpoch {
community_transfer: TokenTransfer | null;
reserve_bolster_transfer: TokenTransfer | null;
};
aggregated_election_rewards: {
delegated_payment: BlockEpochElectionReward | null;
group: BlockEpochElectionReward | null;
validator: BlockEpochElectionReward | null;
voter: BlockEpochElectionReward | null;
};
aggregated_election_rewards: Record<EpochRewardsType, BlockEpochElectionReward | null>;
}
export interface BlockEpochElectionRewardDetails {
......
......@@ -51,6 +51,7 @@ interface TokenTransferBase {
from: AddressParam;
to: AddressParam;
timestamp: string;
block_number: string;
block_hash: string;
log_index: string;
method?: string;
......
......@@ -87,7 +87,7 @@ const AddressAccountHistory = ({ scrollRef, shouldRender = true, isQueryEnabled
</Hide>
<Show above="lg" ssr={ false }>
<Table variant="simple" >
<Table>
<TheadSticky top={ 75 }>
<Tr>
<Th width="120px">
......
......@@ -105,7 +105,7 @@ const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled
const content = query.data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}>
<Table style={{ tableLayout: 'auto' }}>
<Thead top={ query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }>
<Tr>
<Th>Block</Th>
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { epochRewards } from 'mocks/address/epochRewards';
import { test, expect } from 'playwright/lib';
import AddressEpochRewards from './AddressEpochRewards';
const ADDRESS_HASH = '0x1234';
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH },
},
};
test('base view +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('address_epoch_rewards', epochRewards, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressEpochRewards/>
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
import { Hide, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString';
import { EPOCH_REWARD_ITEM } from 'stubs/address';
import { generateListStub } from 'stubs/utils';
import AddressEpochRewardsTable from 'ui/address/epochRewards/AddressEpochRewardsTable';
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressEpochRewardsListItem from './epochRewards/AddressEpochRewardsListItem';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
}
const AddressEpochRewards = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
const hash = getQueryParamString(router.query.hash);
const rewardsQuery = useQueryWithPages({
resourceName: 'address_epoch_rewards',
pathParams: {
hash,
},
scrollRef,
options: {
enabled: isQueryEnabled && Boolean(hash),
placeholderData: generateListStub<'address_epoch_rewards'>(EPOCH_REWARD_ITEM, 50, { next_page_params: {
amount: '1',
items_count: 50,
type: 'voter',
associated_account_address_hash: '1',
block_number: 10355938,
} }),
},
});
if (!isMounted || !shouldRender) {
return null;
}
const content = rewardsQuery.data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<AddressEpochRewardsTable
items={ rewardsQuery.data.items }
top={ rewardsQuery.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
isLoading={ rewardsQuery.isPlaceholderData }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ rewardsQuery.data.items.map((item, index) => (
<AddressEpochRewardsListItem
key={ item.block_hash + item.type + item.account.hash + item.associated_account.hash + (rewardsQuery.isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ rewardsQuery.isPlaceholderData }
/>
)) }
</Show>
</>
) : null;
const actionBar = rewardsQuery.pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...rewardsQuery.pagination }/>
</ActionBar>
) : null;
return (
<DataListDisplay
isError={ rewardsQuery.isError }
items={ rewardsQuery.data?.items }
emptyText="There are no epoch rewards for this address."
content={ content }
actionBar={ actionBar }
/>
);
};
export default AddressEpochRewards;
import { Flex, Skeleton } from '@chakra-ui/react';
import { Text, Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -45,7 +45,9 @@ const AddressBlocksValidatedListItem = (props: Props) => {
</Flex>
<Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Skeleton>
<Skeleton isLoaded={ !props.isLoading }>
<Text color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Text>
</Skeleton>
<BlockGasUsed
gasUsed={ props.gas_used }
gasLimit={ props.gas_limit }
......@@ -55,7 +57,9 @@ const AddressBlocksValidatedListItem = (props: Props) => {
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && (
<Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { currencyUnits.ether }</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ totalReward.toFixed() }</Skeleton>
<Skeleton isLoaded={ !props.isLoading }>
<Text color="text_secondary">{ totalReward.toFixed() }</Text>
</Skeleton>
</Flex>
) }
</ListItemMobile>
......
......@@ -57,7 +57,7 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
</Flex>
</Td>
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && (
<Td isNumeric display="flex" justifyContent="end">
<Td isNumeric>
<Skeleton isLoaded={ !props.isLoading } display="inline-block">
<span>{ totalReward.toFixed() }</span>
</Skeleton>
......
......@@ -26,7 +26,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
const content = query.data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm">
<Table>
<Thead top={ query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }>
<Tr>
<Th width="20%">Block</Th>
......
......@@ -69,6 +69,17 @@ const abiItem: AbiFunction = {
name: 'internalProposals',
type: 'tuple[]',
},
// ARRAY OF TUPLES WITHOUT NAMES
{
components: [
{ type: 'address' },
{ type: 'uint256' },
],
internalType: 'struct SharingPercentage[]',
name: '_sharingPercentages',
type: 'tuple[]',
},
],
};
......@@ -115,6 +126,10 @@ const result = [
},
},
],
[
[ '0xfD36176C63dA52E783a347DE3544B0b44C7054a6', 0 ],
[ '0xC9534cB913150aD3e98D792857689B55e2404212', 3500 ],
],
];
const onSettle = () => {};
......
......@@ -23,8 +23,20 @@ const ItemTuple = ({ abiParameter, data, mode, level }: Props) => {
<span> { '{' }</span>
</p>
{ 'components' in abiParameter && abiParameter.components.map((component, index) => {
const dataObj = typeof data === 'object' && data !== null ? data : undefined;
const itemData = dataObj && component.name && component.name in dataObj ? dataObj[component.name as keyof typeof dataObj] : undefined;
const itemData = (() => {
if (typeof data !== 'object' || data === null) {
return;
}
if (Array.isArray(data)) {
return data[index];
}
if (component.name && component.name in data) {
return data[component.name as keyof typeof data];
}
})();
return (
<Item
key={ index }
......
......@@ -6,7 +6,7 @@ import { test, expect } from 'playwright/lib';
import AddressMetadataAlert from './AddressMetadataAlert';
test('base view', async({ render }) => {
const component = await render(<AddressMetadataAlert tags={ [ metadataMock.noteTag ] }/>);
const component = await render(<AddressMetadataAlert tags={ [ metadataMock.noteTag, metadataMock.noteTag2 ] }/>);
await expect(component).toHaveScreenshot();
});
import { Alert, chakra } from '@chakra-ui/react';
import { Alert, Flex, chakra } from '@chakra-ui/react';
import React from 'react';
import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
......@@ -9,35 +9,34 @@ interface Props {
}
const AddressMetadataAlert = ({ tags, className }: Props) => {
const noteTag = tags?.find(({ tagType }) => tagType === 'note');
if (!noteTag) {
return null;
}
const content = noteTag.meta?.data;
const noteTags = tags?.filter(({ tagType }) => tagType === 'note').filter(({ meta }) => meta?.data);
if (!content) {
if (!noteTags?.length) {
return null;
}
return (
<Alert
className={ className }
status={ noteTag.meta?.alertStatus ?? 'error' }
bgColor={ noteTag.meta?.alertBgColor }
color={ noteTag.meta?.alertTextColor }
whiteSpace="pre-wrap"
display="inline-block"
sx={{
'& a': {
color: 'link',
_hover: {
color: 'link_hovered',
},
},
}}
dangerouslySetInnerHTML={{ __html: content }}
/>
<Flex flexDir="column" gap={ 3 } className={ className }>
{ noteTags.map((noteTag) => (
<Alert
key={ noteTag.name }
status={ noteTag.meta?.alertStatus ?? 'error' }
bgColor={ noteTag.meta?.alertBgColor }
color={ noteTag.meta?.alertTextColor }
whiteSpace="pre-wrap"
display="inline-block"
sx={{
'& a': {
color: 'link',
_hover: {
color: 'link_hovered',
},
},
}}
dangerouslySetInnerHTML={{ __html: noteTag.meta?.data ?? '' }}
/>
)) }
</Flex>
);
};
......
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = {
item: AddressEpochRewardsItem;
isLoading?: boolean;
};
const AddressEpochRewardsListItem = ({ item, isLoading }: Props) => {
const { valueStr } = getCurrencyValue({ value: item.amount, accuracy: 2, decimals: item.token.decimals });
return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>Block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntity
number={ Number(item.block_number) }
isLoading={ isLoading }
noIcon
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Epoch #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.epoch_number }
</ListItemMobileGrid.Value>
{ /* <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.timestamp }
isLoading={ isLoading }
color="text_secondary"
display="inline-block"
/>
</ListItemMobileGrid.Value> */ }
<ListItemMobileGrid.Label isLoading={ isLoading }>Reward type</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<EpochRewardTypeTag type={ item.type } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Associated address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity
address={ item.associated_account }
isLoading={ isLoading }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="flex" alignItems="center" gap={ 2 }>
{ valueStr }
<TokenEntity token={ item.token } isLoading={ isLoading } onlySymbol width="auto" noCopy/>
</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default AddressEpochRewardsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressEpochRewardsTableItem from './AddressEpochRewardsTableItem';
type Props = {
items: Array<AddressEpochRewardsItem>;
isLoading?: boolean;
top: number;
};
const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => {
return (
<Table minW="1000px" style={{ tableLayout: 'auto' }}>
<Thead top={ top }>
<Tr>
<Th>Block</Th>
<Th>Reward type</Th>
<Th>Associated address</Th>
<Th isNumeric>Value</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => {
return (
<AddressEpochRewardsTableItem
key={ item.block_hash + item.type + item.account.hash + item.associated_account.hash + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
);
}) }
</Tbody>
</Table>
);
};
export default AddressEpochRewardsTable;
import { Flex, Td, Tr, Text, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
type Props = {
item: AddressEpochRewardsItem;
isLoading?: boolean;
};
const AddressEpochRewardsTableItem = ({ item, isLoading }: Props) => {
const { valueStr } = getCurrencyValue({ value: item.amount, decimals: item.token.decimals });
return (
<Tr>
<Td verticalAlign="middle">
<Flex alignItems="center" gap={ 3 }>
<BlockEntity number={ item.block_number } isLoading={ isLoading } noIcon/>
<Text color="text_secondary" fontWeight={ 600 }>{ `Epoch # ${ item.epoch_number }` }</Text>
{ /* no timestamp from API, will be added later */ }
{ /* <TimeAgoWithTooltip timestamp={ item } isLoading={ isLoading }/> */ }
</Flex>
</Td>
<Td verticalAlign="middle">
<EpochRewardTypeTag type={ item.type } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
<AddressEntity address={ item.associated_account } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="flex" alignItems="center" gap={ 2 } justifyContent="flex-end">
{ valueStr }
<TokenEntity token={ item.token } isLoading={ isLoading } onlySymbol width="auto" noCopy/>
</Skeleton>
</Td>
</Tr>
);
};
export default AddressEpochRewardsTableItem;
......@@ -18,7 +18,7 @@ interface Props {
const AddressIntTxsTable = ({ data, currentAddress, isLoading }: Props) => {
return (
<AddressHighlightProvider>
<Table variant="simple" size="sm">
<Table>
<Thead top={ 68 }>
<Tr>
<Th width="15%">Parent txn hash</Th>
......
......@@ -26,9 +26,9 @@ const AddressMudRecordValues = ({ data }: Props) => {
{
data?.schema.value_names.map((valName, index) => (
<Tr key={ valName } backgroundColor={ valuesBgColor } borderBottomStyle="hidden">
<Td fontSize="sm" w="100px" py={ 0 } pb={ 4 } pr={ 0 }wordBreak="break-all">{ valName }</Td>
<Td fontSize="sm" w="90px" py={ 0 } pb={ 4 } wordBreak="break-all">{ data.schema.value_types[index] }</Td>
<Td fontSize="sm" wordBreak="break-word" py={ 0 } pb={ 4 }>
<Td fontWeight={ 400 } w="100px" py={ 0 } pb={ 4 } pr={ 0 }wordBreak="break-all">{ valName }</Td>
<Td fontWeight={ 400 } w="90px" py={ 0 } pb={ 4 } wordBreak="break-all">{ data.schema.value_types[index] }</Td>
<Td fontWeight={ 400 } wordBreak="break-word" py={ 0 } pb={ 4 }>
<Box>
{ getValueString(data.record.decoded[valName]) }
</Box>
......
......@@ -140,7 +140,7 @@ const AddressMudRecordsTable = ({
return (
// can't implement both horizontal table scroll and sticky header
<Box maxW="100%" overflowX={ hasHorizontalScroll ? 'scroll' : 'unset' } whiteSpace="nowrap" ref={ tableRef }>
<Table variant="simple" size="sm" style={{ tableLayout: 'fixed' }}>
<Table style={{ tableLayout: 'fixed' }}>
<Thead top={ hasHorizontalScroll ? 0 : top } display={ hasHorizontalScroll ? 'table' : 'table-header-group' } w="100%">
<Tr >
{ keys.map((keyName, index) => {
......
......@@ -18,7 +18,7 @@ type Props = {
//sorry for the naming
const AddressMudTablesTable = ({ items, isLoading, top, scrollRef, hash }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}>
<Table style={{ tableLayout: 'auto' }}>
<Thead top={ top }>
<Tr>
<Th width="24px"></Th>
......
......@@ -15,7 +15,7 @@ interface Props {
const ERC20TokensTable = ({ data, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm">
<Table>
<Thead top={ top }>
<Tr>
<Th width="30%">Asset</Th>
......
......@@ -21,7 +21,7 @@ interface Props {
const AddressesTable = ({ items, totalSupply, pageStartIndex, top, isLoading }: Props) => {
const hasPercentage = !totalSupply.eq(ZERO);
return (
<Table variant="simple" size="sm">
<Table>
<Thead top={ top }>
<Tr>
<Th width="64px">Rank</Th>
......
......@@ -16,7 +16,7 @@ interface Props {
const AddressesLabelSearchTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm">
<Table>
<Thead top={ top }>
<Tr>
<Th width="70%">Address</Th>
......
......@@ -21,7 +21,7 @@ interface Props {
const ApiKeyTable = ({ data, isLoading, onDeleteClick, onEditClick, limit }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Table minWidth="600px">
<Thead>
<Tr>
<Th>{ `API key token (limit ${ limit } keys)` }</Th>
......
import React from 'react';
import type { BlockEpoch } from 'types/api/block';
import Tag from 'ui/shared/chakra/Tag';
interface Props {
type: keyof BlockEpoch['aggregated_election_rewards'];
isLoading?: boolean;
}
const BlockEpochElectionRewardType = ({ type, isLoading }: Props) => {
switch (type) {
case 'delegated_payment':
return <Tag colorScheme="blue" isLoading={ isLoading }>Delegated payments</Tag>;
case 'group':
return <Tag colorScheme="teal" isLoading={ isLoading }>Validator group rewards</Tag>;
case 'validator':
return <Tag colorScheme="purple" isLoading={ isLoading }>Validator rewards</Tag>;
case 'voter':
return <Tag colorScheme="yellow" isLoading={ isLoading }>Voting rewards</Tag>;
}
};
export default React.memo(BlockEpochElectionRewardType);
......@@ -16,7 +16,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
<Box mt={ 8 }>
<Heading as="h4" size="sm" mb={ 3 }>Election rewards</Heading>
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}>
<Table style={{ tableLayout: 'auto' }}>
<Thead>
<Tr>
<Th width="24px"/>
......
......@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import IconSvg from 'ui/shared/IconSvg';
import BlockEpochElectionRewardDetailsMobile from './BlockEpochElectionRewardDetailsMobile';
import BlockEpochElectionRewardType from './BlockEpochElectionRewardType';
interface Props {
data: BlockEpochElectionReward;
......@@ -53,7 +53,7 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) =>
/>
</Skeleton>
) : <Box boxSize={ 6 }/> }
<BlockEpochElectionRewardType type={ type } isLoading={ isLoading }/>
<EpochRewardTypeTag type={ type } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading }>{ data.count }</Skeleton>
<Flex columnGap={ 2 } alignItems="center" ml="auto" fontWeight={ 500 }>
<Skeleton isLoaded={ !isLoading }>{ valueStr }</Skeleton>
......
......@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import IconSvg from 'ui/shared/IconSvg';
import BlockEpochElectionRewardDetailsDesktop from './BlockEpochElectionRewardDetailsDesktop';
import BlockEpochElectionRewardType from './BlockEpochElectionRewardType';
import { getRewardNumText } from './utils';
interface Props {
......@@ -54,7 +54,7 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) =>
) }
</Td>
<Td borderColor={ mainRowBorderColor }>
<BlockEpochElectionRewardType type={ type } isLoading={ isLoading }/>
<EpochRewardTypeTag type={ type } isLoading={ isLoading }/>
</Td>
<Td borderColor={ mainRowBorderColor }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 400 } my={ 1 }>
......
......@@ -40,7 +40,7 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum
return (
<AddressHighlightProvider>
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
<Table minWidth="1040px" fontWeight={ 500 }>
<Thead top={ top }>
<Tr>
<Th width="150px">Block</Th>
......
......@@ -20,7 +20,7 @@ interface Props {
const CustomAbiTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Table minWidth="600px">
<Thead>
<Tr>
<Th>ABI for Smart contract address (0x...)</Th>
......
......@@ -15,7 +15,7 @@ import OptimisticDepositsTableItem from './OptimisticDepositsTableItem';
const OptimisticDepositsTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
<Th>L1 block No</Th>
......
......@@ -15,7 +15,7 @@ import DepositsTableItem from './DepositsTableItem';
const DepositsTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
<Th>L1 block No</Th>
......
......@@ -15,7 +15,7 @@ import ZkEvmL2DepositsTableItem from './ZkEvmL2DepositsTableItem';
const ZkEvmL2DepositsTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
<Th>L1 block</Th>
......
......@@ -15,7 +15,7 @@ type Props = {
const OptimisticL2DisputeGamesTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
<Th>Index</Th>
......
......@@ -14,18 +14,32 @@ const BORDER_DEFAULT = 'none';
const HeroBanner = () => {
const background = useColorModeValue(
config.UI.homepage.heroBanner?.background?.[0] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT,
config.UI.homepage.heroBanner?.background?.[1] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT,
// light mode
config.UI.homepage.heroBanner?.background?.[0] ||
config.UI.homepage.plate.background ||
BACKGROUND_DEFAULT,
// dark mode
config.UI.homepage.heroBanner?.background?.[1] ||
config.UI.homepage.heroBanner?.background?.[0] ||
config.UI.homepage.plate.background ||
BACKGROUND_DEFAULT,
);
const textColor = useColorModeValue(
config.UI.homepage.heroBanner?.text_color?.[0] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT,
config.UI.homepage.heroBanner?.text_color?.[1] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT,
// light mode
config.UI.homepage.heroBanner?.text_color?.[0] ||
config.UI.homepage.plate.textColor ||
TEXT_COLOR_DEFAULT,
// dark mode
config.UI.homepage.heroBanner?.text_color?.[1] ||
config.UI.homepage.heroBanner?.text_color?.[0] ||
config.UI.homepage.plate.textColor ||
TEXT_COLOR_DEFAULT,
);
const border = useColorModeValue(
config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT,
config.UI.homepage.heroBanner?.border?.[1] || BORDER_DEFAULT,
config.UI.homepage.heroBanner?.border?.[1] || config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT,
);
return (
......
......@@ -11,6 +11,7 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AppSecurityReport from './AppSecurityReport';
import FavoriteIcon from './FavoriteIcon';
import MarketplaceAppCardLink from './MarketplaceAppCardLink';
import MarketplaceAppGraphLinks from './MarketplaceAppGraphLinks';
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
import Rating from './Rating/Rating';
import type { RateFunction } from './Rating/useRatings';
......@@ -28,6 +29,7 @@ interface Props extends MarketplaceAppWithSecurityReport {
isRatingSending: boolean;
isRatingLoading: boolean;
canRate: boolean | undefined;
graphLinks: Array<{text: string; url: string}>;
}
const MarketplaceAppCard = ({
......@@ -54,6 +56,7 @@ const MarketplaceAppCard = ({
isRatingSending,
isRatingLoading,
canRate,
graphLinks,
}: Props) => {
const isMobile = useIsMobile();
const categoriesLabel = categories.join(', ');
......@@ -118,11 +121,7 @@ const MarketplaceAppCard = ({
>
<Skeleton
isLoaded={ !isLoading }
fontSize={{ base: 'sm', md: 'lg' }}
lineHeight={{ base: '20px', md: '28px' }}
paddingRight={{ base: '40px', md: 0 }}
fontWeight="semibold"
fontFamily="heading"
display="inline-block"
>
<MarketplaceAppCardLink
......@@ -131,8 +130,18 @@ const MarketplaceAppCard = ({
external={ external }
title={ title }
onClick={ onAppClick }
fontWeight="semibold"
fontFamily="heading"
fontSize={{ base: 'sm', md: 'lg' }}
lineHeight={{ base: '20px', md: '28px' }}
/>
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
<MarketplaceAppGraphLinks
links={ graphLinks }
ml={ 2 }
verticalAlign="middle"
mb={{ base: 0, md: 1 }}
/>
</Skeleton>
<Skeleton
......
import { LinkOverlay } from '@chakra-ui/react';
import { LinkOverlay, chakra } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import type { MouseEvent } from 'react';
......@@ -9,24 +9,25 @@ type Props = {
external?: boolean;
title: string;
onClick?: (event: MouseEvent, id: string) => void;
className?: string;
}
const MarketplaceAppCardLink = ({ url, external, id, title, onClick }: Props) => {
const MarketplaceAppCardLink = ({ url, external, id, title, onClick, className }: Props) => {
const handleClick = React.useCallback((event: MouseEvent) => {
onClick?.(event, id);
}, [ onClick, id ]);
return external ? (
<LinkOverlay href={ url } isExternal={ true } marginRight={ 2 }>
<LinkOverlay href={ url } isExternal={ true } marginRight={ 2 } className={ className }>
{ title }
</LinkOverlay>
) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<LinkOverlay onClick={ handleClick } marginRight={ 2 }>
<LinkOverlay onClick={ handleClick } marginRight={ 2 } className={ className }>
{ title }
</LinkOverlay>
</NextLink>
);
};
export default MarketplaceAppCardLink;
export default chakra(MarketplaceAppCardLink);
import {
Text,
PopoverTrigger,
PopoverBody,
PopoverContent,
chakra,
Box,
VStack,
} from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props {
className?: string;
links?: Array<{ title: string; url: string }>;
}
const MarketplaceAppGraphLinks = ({ className, links }: Props) => {
const isMobile = useIsMobile();
const handleButtonClick = React.useCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
}, []);
if (!links || links.length === 0) {
return null;
}
return (
<Box position="relative" className={ className } display="inline-flex" alignItems="center" height={ 7 } onClick={ handleButtonClick }>
<Popover
placement={ isMobile ? 'bottom-end' : 'bottom' }
isLazy
trigger="hover"
>
<PopoverTrigger>
<IconSvg name="brands/graph" boxSize={ 5 } onClick={ handleButtonClick }/>
</PopoverTrigger>
<PopoverContent w="260px">
<PopoverBody fontSize="sm">
<VStack gap={ 4 } align="start">
<Text>{ `This dapp uses ${ links.length > 1 ? 'several subgraphs' : 'a subgraph' } powered by The Graph` }</Text>
{ links.map(link => (
<LinkExternal key={ link.url } href={ link.url }>{ link.title }</LinkExternal>
)) }
</VStack>
</PopoverBody>
</PopoverContent>
</Popover>
</Box>
);
};
export default React.memo(chakra(MarketplaceAppGraphLinks));
......@@ -18,6 +18,8 @@ import IconSvg from 'ui/shared/IconSvg';
import AppSecurityReport from './AppSecurityReport';
import FavoriteIcon from './FavoriteIcon';
import MarketplaceAppGraphLinks from './MarketplaceAppGraphLinks';
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
import MarketplaceAppModalLink from './MarketplaceAppModalLink';
import Rating from './Rating/Rating';
import type { RateFunction } from './Rating/useRatings';
......@@ -36,6 +38,7 @@ type Props = {
isRatingSending: boolean;
isRatingLoading: boolean;
canRate: boolean | undefined;
graphLinks?: Array<{text: string; url: string}>;
}
const MarketplaceAppModal = ({
......@@ -49,6 +52,7 @@ const MarketplaceAppModal = ({
isRatingSending,
isRatingLoading,
canRate,
graphLinks,
}: Props) => {
const {
id,
......@@ -67,6 +71,7 @@ const MarketplaceAppModal = ({
categories,
securityReport,
rating,
internalWallet,
} = data;
const socialLinks = [
......@@ -148,16 +153,19 @@ const MarketplaceAppModal = ({
/>
</Flex>
<Heading
as="h2"
gridColumn={ 2 }
fontSize={{ base: '2xl', md: '32px' }}
fontWeight="medium"
lineHeight={{ md: 10 }}
mb={{ md: 2 }}
>
{ title }
</Heading>
<Flex alignItems="center" mb={{ md: 2 }} gridColumn={ 2 }>
<Heading
as="h2"
fontSize={{ base: '2xl', md: '32px' }}
fontWeight="medium"
lineHeight={{ md: 10 }}
mr={ 2 }
>
{ title }
</Heading>
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
<MarketplaceAppGraphLinks links={ graphLinks } ml={ 2 }/>
</Flex>
<Text
variant="secondary"
......
import { Grid, Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { MouseEvent } from 'react';
......@@ -25,11 +26,13 @@ type Props = {
isRatingSending: boolean;
isRatingLoading: boolean;
canRate: boolean | undefined;
graphLinksQuery: UseQueryResult<Record<string, Array<{text: string; url: string}>>, unknown>;
}
const MarketplaceList = ({
apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId,
onAppClick, showContractList, userRatings, rateApp, isRatingSending, isRatingLoading, canRate,
graphLinksQuery,
}: Props) => {
const { cutRef, renderedItemsNum } = useLazyRenderedList(apps, !isLoading, 16);
......@@ -75,6 +78,7 @@ const MarketplaceList = ({
isRatingSending={ isRatingSending }
isRatingLoading={ isRatingLoading }
canRate={ canRate }
graphLinks={ graphLinksQuery.data?.[app.id] }
/>
)) }
</Grid>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment