Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Neon: Solana transactions #2483

Merged
merged 5 commits into from
Feb 13, 2025
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/deploy-review-l2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ on:
- eth_sepolia
- eth_goerli
- filecoin
- neon_devnet
- optimism
- optimism_celestia
- optimism_sepolia
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/deploy-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ on:
- eth_goerli
- filecoin
- mekong
- neon_devnet
- optimism
- optimism_celestia
- optimism_sepolia
Expand Down
27 changes: 27 additions & 0 deletions configs/app/features/externalTxs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Feature } from './types';
import type { TxExternalTxsConfig } from 'types/client/externalTxsConfig';

import { getEnvValue, parseEnvJson } from '../utils';

const externalTransactionsConfig = parseEnvJson<TxExternalTxsConfig>(getEnvValue('NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG'));

const title = 'External transactions';

const config: Feature<{ chainName: string; chainLogoUrl: string; explorerUrlTemplate: string }> = (() => {
if (externalTransactionsConfig) {
return Object.freeze({
title,
isEnabled: true,
chainName: externalTransactionsConfig.chain_name,
chainLogoUrl: externalTransactionsConfig.chain_logo_url,
explorerUrlTemplate: externalTransactionsConfig.explorer_url_template,
});
}

return Object.freeze({
title,
isEnabled: false,
});
})();

export default config;
1 change: 1 addition & 0 deletions configs/app/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { default as csvExport } from './csvExport';
export { default as dataAvailability } from './dataAvailability';
export { default as deFiDropdown } from './deFiDropdown';
export { default as easterEggBadge } from './easterEggBadge';
export { default as externalTxs } from './externalTxs';
export { default as faultProofSystem } from './faultProofSystem';
export { default as gasTracker } from './gasTracker';
export { default as getGasButton } from './getGasButton';
Expand Down
44 changes: 44 additions & 0 deletions configs/envs/.env.neon_devnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Set of ENVs for Neon Devnet network explorer
# https://neon-devnet.blockscout.com
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=neon_devnet"

# 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_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=neon-devnet.blockscout.com
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_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/neon-devnet.json
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x0716b7a70a1c3b83f731084d7c1449148392512318c2ce0fd812d029204707b5
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(0, rgb(223, 66, 171), rgb(176, 40, 209))'],'text_color':['rgba(255, 255, 255, 1)']}
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_LOGOUT_URL=https://blockscout-neon.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=NEON
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=NEON
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/neon-short.svg
NEXT_PUBLIC_NETWORK_ID=245022926
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/neon.svg
NEXT_PUBLIC_NETWORK_NAME=Neon Devnet
NEXT_PUBLIC_NETWORK_RPC_URL=https://devnet.neonevm.org
NEXT_PUBLIC_NETWORK_SHORT_NAME=Neon
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/neon-devnet.png
NEXT_PUBLIC_STATS_API_HOST=https://stats-neon-devnet.k8s.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG={'chain_name':'Solana','chain_logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/main/configs/network-icons/solana.svg','explorer_url_template':'https://solscan.io/tx/{hash}'}
20 changes: 20 additions & 0 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type { NftMarketplaceItem } from '../../../types/views/nft';
import type { TxAdditionalFieldsId, TxFieldsId } from '../../../types/views/tx';
import { TX_ADDITIONAL_FIELDS_IDS, TX_FIELDS_IDS } from '../../../types/views/tx';
import type { VerifiedContractsFilter } from '../../../types/api/contracts';
import type { TxExternalTxsConfig } from '../../../types/client/externalTxsConfig';

import { replaceQuotes } from '../../../configs/app/utils';
import * as regexp from '../../../lib/regexp';
Expand Down Expand Up @@ -563,6 +564,12 @@ const multichainProviderConfigSchema: yup.ObjectSchema<MultichainProviderConfig>
dapp_id: yup.string(),
});

const externalTxsConfigSchema: yup.ObjectSchema<TxExternalTxsConfig> = yup.object({
chain_name: yup.string().required(),
chain_logo_url: yup.string().required(),
explorer_url_template: yup.string().required(),
});

const schema = yup
.object()
.noUnknown(true, (params) => {
Expand Down Expand Up @@ -912,6 +919,19 @@ const schema = yup
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_XSTAR_SCORE_URL: yup.string().test(urlTest),
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK: yup.string().test(urlTest),
NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG: yup.mixed().test(
'shape',
'Invalid schema were provided for NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG, it should have chain_name, chain_logo_url, and explorer_url_template',
(data) => {
const isUndefined = data === undefined;
const valueSchema = yup.object<TxExternalTxsConfig>().transform(replaceQuotes).json().shape({
chain_name: yup.string().required(),
chain_logo_url: yup.string().required(),
explorer_url_template: yup.string().required(),
});

return isUndefined || valueSchema.isValidSync(data);
}),

// 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
Expand Down
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/test/.env.external_txs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG={'chain_name':'Solana','chain_logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/main/configs/network-icons/solana.svg','explorer_url_template':'https://solscan.io/tx/{hash}'}
8 changes: 8 additions & 0 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,14 @@ This feature is **enabled by default** with the `['metamask']` value. To switch

&nbsp;

### External transactions

| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG | `{ chain_name: string; chain_logo_url: string; explorer_url_template: string; }` | Configuration of the external transactions links that should be added to the transaction details. | - | - | `{ chain_name: 'ethereum', chain_logo_url: 'https://example.com/logo.png', explorer_url_template: 'https://explorer.com/tx/{hash}' }` | v1.38.0+ |

&nbsp;

### Verified tokens info

| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
Expand Down
5 changes: 5 additions & 0 deletions lib/api/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,10 @@ export const RESOURCES = {
path: '/api/v2/transactions/:hash/summary',
pathParams: [ 'hash' as const ],
},
tx_external_transactions: {
path: '/api/v2/transactions/:hash/external-transactions',
pathParams: [ 'hash' as const ],
},
withdrawals: {
path: '/api/v2/withdrawals',
filterFields: [],
Expand Down Expand Up @@ -1296,6 +1300,7 @@ Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'tx_state_changes' ? TxStateChanges :
Q extends 'tx_blobs' ? TxBlobs :
Q extends 'tx_interpretation' ? TxInterpretationResponse :
Q extends 'tx_external_transactions' ? Array<string> :
Q extends 'addresses' ? AddressesResponse :
Q extends 'addresses_metadata_search' ? AddressesMetadataSearchResult :
Q extends 'address' ? Address :
Expand Down
3 changes: 3 additions & 0 deletions playwright/fixtures/mockEnvs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
[ 'NEXT_PUBLIC_ADDRESS_FORMAT', '["bech32","base16"]' ],
[ 'NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX', 'tom' ],
],
externalTxs: [
[ 'NEXT_PUBLIC_TX_EXTERNAL_TRANSACTIONS_CONFIG', '{"chain_name": "Solana", "chain_logo_url": "http://example.url", "explorer_url_template": "https://scan.io/tx/{hash}"}' ],
],
};
1 change: 1 addition & 0 deletions tools/preset-sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const PRESETS = {
filecoin: 'https://filecoin.blockscout.com',
gnosis: 'https://gnosis.blockscout.com',
mekong: 'https://mekong.blockscout.com',
neon_devnet: 'https://neon-devnet.blockscout.com',
optimism: 'https://optimism.blockscout.com',
optimism_celestia: 'https://opcelestia-raspberry.gelatoscout.com',
optimism_sepolia: 'https://optimism-sepolia.blockscout.com',
Expand Down
5 changes: 5 additions & 0 deletions types/client/externalTxsConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type TxExternalTxsConfig = {
chain_name: string;
chain_logo_url: string;
explorer_url_template: string;
};
18 changes: 18 additions & 0 deletions ui/tx/TxExternalTxs.pw.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';

import TxExternalTxs from './TxExternalTxs';

const EXT_TX_HASH = '2uwpB95K9ae8yrpxxVXJ27ivvHXqrmy82jsamgNtdWJrYDGkCHsRwd2LKXubrQUzXMaojGxZmHZ85XVJN8EJ3LW8';

test('base view', async({ page, render, mockEnvs, mockAssetResponse }) => {
await mockEnvs(ENVS_MAP.externalTxs);
await mockAssetResponse('http://example.url', './playwright/mocks/image_s.jpg');
await render(<TxExternalTxs data={ Array(13).fill(EXT_TX_HASH) }/>);
await page.getByText('13 Solana txns').hover();
const popover = page.getByText('Solana transactions');
await expect(popover).toBeVisible();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 500, height: 500 } });
});
61 changes: 61 additions & 0 deletions ui/tx/TxExternalTxs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
PopoverTrigger,
PopoverBody,
PopoverContent,
Flex,
Link,
Image,
} from '@chakra-ui/react';
import React from 'react';

import config from 'configs/app';
import Popover from 'ui/shared/chakra/Popover';
import TxEntity from 'ui/shared/entities/tx/TxEntity';

const externalTxFeature = config.features.externalTxs;

interface Props {
data: Array<string>;
}

const TxExternalTxs: React.FC<Props> = ({ data }) => {
if (!externalTxFeature.isEnabled) {
return null;
}

return (
<Popover placement="bottom-end" openDelay={ 300 } isLazy trigger="hover">
<PopoverTrigger>
<Link
_hover={{ textDecoration: 'none', color: 'link_hovered' }}
display="inline-flex"
alignItems="center"
gap={ 2 }
>
<Image src={ externalTxFeature.chainLogoUrl } alt={ externalTxFeature.chainName } width={ 5 } height={ 5 }/>
{ data.length } { externalTxFeature.chainName } txn{ data.length > 1 ? 's' : '' }
</Link>
</PopoverTrigger>
<PopoverContent border="1px solid" borderColor="divider" w={{ base: '300px', lg: '460px' }}>
<PopoverBody fontWeight={ 400 } fontSize="sm">
<Flex alignItems="center" gap={ 2 } fontSize="md" mb={ 3 }>
<Image src={ externalTxFeature.chainLogoUrl } alt={ externalTxFeature.chainName } width={ 5 } height={ 5 }/>
{ externalTxFeature.chainName } transaction{ data.length > 1 ? 's' : '' }
</Flex>
<Flex flexDirection="column" gap={ 2 } w="100%" maxHeight="460px" overflowY="auto">
{ data.map((txHash) => (
<TxEntity
key={ txHash }
hash={ txHash }
href={ externalTxFeature.explorerUrlTemplate.replace('{hash}', txHash) }
isExternal
/>
)) }
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};

export default TxExternalTxs;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions ui/tx/details/TxInfo.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,12 @@ test('arbitrum L1 status', async({ render, mockEnvs }) => {

await expect(statusElement).toHaveScreenshot();
});

test('with external txs +@mobile', async({ render, mockEnvs, mockApiResponse, mockAssetResponse }) => {
await mockEnvs(ENVS_MAP.externalTxs);
await mockApiResponse('tx_external_transactions', [ 'tx1', 'tx2', 'tx3' ], { pathParams: { hash: txMock.base.hash } });
await mockAssetResponse('http://example.url', './playwright/mocks/image_s.jpg');
const component = await render(<TxInfo data={ txMock.base } isLoading={ false }/>);

await expect(component).toHaveScreenshot();
});
49 changes: 37 additions & 12 deletions ui/tx/details/TxInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2';
import { route } from 'nextjs-routes';

import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { WEI, WEI_IN_GWEI } from 'lib/consts';
import useIsMobile from 'lib/hooks/useIsMobile';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import * as arbitrum from 'lib/rollups/arbitrum';
import { MESSAGE_DESCRIPTIONS } from 'lib/tx/arbitrumMessageStatusDescription';
Expand Down Expand Up @@ -61,6 +63,7 @@ import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxDetailsWithdrawalStatus from 'ui/tx/details/TxDetailsWithdrawalStatus';
import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers';
import TxExternalTxs from 'ui/tx/TxExternalTxs';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo';

Expand All @@ -74,9 +77,23 @@ interface Props {
socketStatus?: 'close' | 'error';
}

const externalTxFeature = config.features.externalTxs;

const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
const [ isExpanded, setIsExpanded ] = React.useState(false);

const isMobile = useIsMobile();

const externalTxsQuery = useApiQuery('tx_external_transactions', {
pathParams: {
hash: data?.hash,
},
queryOptions: {
enabled: externalTxFeature.isEnabled,
placeholderData: [ '1', '2', '3' ],
},
});

const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag);
scroller.scrollTo('TxInfo__cutLink', {
Expand Down Expand Up @@ -149,18 +166,26 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
>
Transaction hash
</DetailsInfoItem.Label>
<DetailsInfoItem.Value flexWrap="nowrap">
{ data.status === null && <Spinner mr={ 2 } size="sm" flexShrink={ 0 }/> }
<Skeleton isLoaded={ !isLoading } overflow="hidden">
<HashStringShortenDynamic hash={ data.hash }/>
</Skeleton>
<CopyToClipboard text={ data.hash } isLoading={ isLoading }/>

{ config.features.metasuites.isEnabled && (
<>
<TextSeparator color="gray.500" flexShrink={ 0 } display="none" id="meta-suites__tx-explorer-separator"/>
<Box display="none" flexShrink={ 0 } id="meta-suites__tx-explorer-link"/>
</>
<DetailsInfoItem.Value>
<Flex flexWrap="nowrap" alignItems="center" overflow="hidden">
{ data.status === null && <Spinner mr={ 2 } size="sm" flexShrink={ 0 }/> }
<Skeleton isLoaded={ !isLoading } overflow="hidden">
<HashStringShortenDynamic hash={ data.hash }/>
</Skeleton>
<CopyToClipboard text={ data.hash } isLoading={ isLoading }/>

{ config.features.metasuites.isEnabled && (
<>
<TextSeparator color="gray.500" flexShrink={ 0 } display="none" id="meta-suites__tx-explorer-separator"/>
<Box display="none" flexShrink={ 0 } id="meta-suites__tx-explorer-link"/>
</>
) }
</Flex>
{ config.features.externalTxs.isEnabled && externalTxsQuery.data && externalTxsQuery.data.length > 0 && (
<Skeleton isLoaded={ !isLoading && !externalTxsQuery.isPlaceholderData } display={{ base: 'block', lg: 'inline-flex' }} alignItems="center">
{ !isMobile && <TextSeparator color="gray.500" flexShrink={ 0 }/> }
<TxExternalTxs data={ externalTxsQuery.data }/>
</Skeleton>
) }
</DetailsInfoItem.Value>

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading