Skip to content

Commit f672d23

Browse files
authored
Support verification of Stylus contracts (blockscout#2450)
* add Arbitrum Sepolia dev preset * base form layout * add validation for repo url and commit hash * improve global error text * show stylus contract info * add test for contract verification form * hide "open in IDE" for stylus contracts * [skip ci] adjust validation rules for GitHub URL * change contract info items order * fix error text styles
1 parent 1685610 commit f672d23

32 files changed

+586
-52
lines changed

.github/workflows/deploy-review-l2.yml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212
- none
1313
- arbitrum
1414
- arbitrum_nova
15+
- arbitrum_sepolia
1516
- base
1617
- celo_alfajores
1718
- garnet

.github/workflows/deploy-review.yml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212
- none
1313
- arbitrum
1414
- arbitrum_nova
15+
- arbitrum_sepolia
1516
- base
1617
- celo_alfajores
1718
- garnet

.vscode/tasks.json

+1
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@
360360
"main",
361361
"localhost",
362362
"arbitrum",
363+
"arbitrum_sepolia",
363364
"base",
364365
"celo_alfajores",
365366
"garnet",

configs/envs/.env.arbitrum_sepolia

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Set of ENVs for Arbitrum Sepolia network explorer
2+
# https://arbitrum-sepolia.blockscout.com
3+
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=arbitrum_sepolia"
4+
5+
# Local ENVs
6+
NEXT_PUBLIC_APP_PROTOCOL=http
7+
NEXT_PUBLIC_APP_HOST=localhost
8+
NEXT_PUBLIC_APP_PORT=3000
9+
NEXT_PUBLIC_APP_ENV=development
10+
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
11+
12+
# Instance ENVs
13+
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
14+
NEXT_PUBLIC_API_BASE_PATH=/
15+
NEXT_PUBLIC_API_HOST=arbitrum-sepolia.blockscout.com
16+
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
17+
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'}]
18+
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
19+
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/arbitrum-sepolia.json
20+
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xb730960249381c72588024f5e213abd8e032d968aeb9629103e70677b0850bfa
21+
NEXT_PUBLIC_HAS_USER_OPS=true
22+
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
23+
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(27, 74, 221, 1)']}
24+
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
25+
NEXT_PUBLIC_IS_TESTNET=true
26+
NEXT_PUBLIC_LOGOUT_URL=https://blockscout-arbitrum.us.auth0.com/v2/logout
27+
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
28+
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
29+
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
30+
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
31+
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
32+
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-sepolia.svg
33+
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/arbitrum-sepolia-dark.svg
34+
NEXT_PUBLIC_NETWORK_ID=421614
35+
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/arbitrum-sepolia.svg
36+
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/arbitrum-sepolia-dark.svg
37+
NEXT_PUBLIC_NETWORK_NAME=Arbitrum Sepolia
38+
NEXT_PUBLIC_NETWORK_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
39+
NEXT_PUBLIC_NETWORK_SHORT_NAME=Arbitrum Sepolia
40+
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
41+
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/arbitrum-sepolia.png
42+
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com
43+
NEXT_PUBLIC_ROLLUP_TYPE=arbitrum
44+
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/4503902500421632
45+
NEXT_PUBLIC_STATS_API_HOST=https://stats-arbitrum-sepolia.k8s-prod-2.blockscout.com
46+
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
47+
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com

lib/contracts/formatLanguageName.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function formatLanguageName(language: string) {
2+
return language.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
3+
}

mocks/contract/info.ts

+13
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ export const zkSync: SmartContract = {
106106
optimization_runs: 's',
107107
};
108108

109+
export const stylusRust: SmartContract = {
110+
...verified,
111+
language: 'stylus_rust',
112+
github_repository_metadata: {
113+
commit: 'af5029f822815e32def0015bf8e591e769c62f34',
114+
path_prefix: 'examples/erc20',
115+
repository_url: 'https://github.com/blockscout/cargo-stylus-test-examples',
116+
},
117+
compiler_version: 'v0.5.6',
118+
package_name: 'erc20',
119+
evm_version: null,
120+
};
121+
109122
export const nonVerified: SmartContract = {
110123
is_verified: false,
111124
is_blueprint: false,

mocks/contracts/index.ts

+26
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,36 @@ export const contract2: VerifiedContract = {
4747
license_type: 'bsd_3_clause',
4848
};
4949

50+
export const contract3: VerifiedContract = {
51+
address: {
52+
ens_domain_name: null,
53+
hash: '0xf145e3A26c6706F64d95Dc8d9d45022D8b3D676B',
54+
implementations: [],
55+
is_contract: true,
56+
is_verified: true,
57+
metadata: null,
58+
name: 'StylusTestToken',
59+
private_tags: [],
60+
public_tags: [],
61+
watchlist_names: [],
62+
},
63+
certified: false,
64+
coin_balance: '0',
65+
compiler_version: 'v0.5.6',
66+
has_constructor_args: false,
67+
language: 'stylus_rust',
68+
license_type: 'none',
69+
market_cap: null,
70+
optimization_enabled: false,
71+
transaction_count: 0,
72+
verified_at: '2024-12-03T14:05:42.796224Z',
73+
};
74+
5075
export const baseResponse: VerifiedContractsResponse = {
5176
items: [
5277
contract1,
5378
contract2,
79+
contract3,
5480
],
5581
next_page_params: {
5682
items_count: '50',

nextjs/csp/policies/app.ts

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ export function app(): CspDev.DirectiveDescriptor {
5656

5757
// github (spec for api-docs page)
5858
'raw.githubusercontent.com',
59+
60+
// github api (used for Stylus contract verification)
61+
'api.github.com',
5962
].filter(Boolean),
6063

6164
'script-src': [

nextjs/csp/policies/monaco.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export function monaco(): CspDev.DirectiveDescriptor {
1313
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/basic-languages/elixir/elixir.js',
1414
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/basic-languages/javascript/javascript.js',
1515
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/basic-languages/typescript/typescript.js',
16+
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/basic-languages/rust/rust.js',
1617
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/language/json/jsonMode.js',
1718
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/language/json/jsonWorker.js',
1819
'https://cdn.jsdelivr.net/npm/[email protected]/min/vs/language/typescript/tsMode.js',

tools/preset-sync/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'path';
44
/* eslint-disable no-console */
55
const PRESETS = {
66
arbitrum: 'https://arbitrum.blockscout.com',
7+
arbitrum_sepolia: 'https://arbitrum-sepolia.blockscout.com',
78
base: 'https://base.blockscout.com',
89
blackfort_testnet: 'https://blackfort-testnet.blockscout.com',
910
celo_alfajores: 'https://celo-alfajores.blockscout.com',

types/api/contract.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ export interface SmartContract {
7373
license_type: SmartContractLicenseType | null;
7474
certified?: boolean;
7575
zk_compiler_version?: string;
76+
github_repository_metadata?: {
77+
commit?: string;
78+
path_prefix?: string;
79+
repository_url?: string;
80+
};
81+
package_name?: string;
7682
}
7783

7884
export type SmartContractDecodedConstructorArg = [
@@ -92,13 +98,14 @@ export interface SmartContractExternalLibrary {
9298
// VERIFICATION
9399

94100
export type SmartContractVerificationMethodApi = 'flattened-code' | 'standard-input' | 'sourcify' | 'multi-part'
95-
| 'vyper-code' | 'vyper-multi-part' | 'vyper-standard-input';
101+
| 'vyper-code' | 'vyper-multi-part' | 'vyper-standard-input' | 'stylus-github-repository';
96102

97103
export interface SmartContractVerificationConfigRaw {
98104
solidity_compiler_versions: Array<string>;
99105
solidity_evm_versions: Array<string>;
100106
verification_options: Array<string>;
101107
vyper_compiler_versions: Array<string>;
108+
stylus_compiler_versions?: Array<string>;
102109
vyper_evm_versions: Array<string>;
103110
is_rust_verifier_microservice_enabled: boolean;
104111
license_types: Record<SmartContractLicenseType, number>;

types/api/contracts.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface VerifiedContract {
66
certified?: boolean;
77
coin_balance: string;
88
compiler_version: string | null;
9-
language: 'vyper' | 'yul' | 'solidity';
9+
language: 'vyper' | 'yul' | 'solidity' | 'stylus_rust';
1010
has_constructor_args: boolean;
1111
optimization_enabled: boolean;
1212
transaction_count: number | null;

ui/address/contract/ContractSourceCode.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { SmartContract } from 'types/api/contract';
55

66
import { route } from 'nextjs-routes';
77

8+
import formatLanguageName from 'lib/contracts/formatLanguageName';
89
import CopyToClipboard from 'ui/shared/CopyToClipboard';
910
import LinkInternal from 'ui/shared/links/LinkInternal';
1011
import CodeEditor from 'ui/shared/monaco/CodeEditor';
@@ -24,6 +25,10 @@ function getEditorData(contractInfo: SmartContract | undefined) {
2425
return 'vy';
2526
case 'yul':
2627
return 'yul';
28+
case 'scilla':
29+
return 'scilla';
30+
case 'stylus_rust':
31+
return 'rs';
2732
default:
2833
return 'sol';
2934
}
@@ -51,7 +56,7 @@ export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) =>
5156
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
5257
<span>Contract source code</span>
5358
{ data?.language &&
54-
<Text whiteSpace="pre" as="span" variant="secondary" textTransform="capitalize"> ({ data.language })</Text> }
59+
<Text whiteSpace="pre" as="span" variant="secondary"> ({ formatLanguageName(data.language) })</Text> }
5560
</Skeleton>
5661
);
5762

@@ -73,7 +78,9 @@ export const ContractSourceCode = ({ data, isLoading, sourceAddress }: Props) =>
7378
</Tooltip>
7479
) : null;
7580

76-
const ides = <ContractCodeIdes hash={ sourceAddress } isLoading={ isLoading }/>;
81+
const ides = data?.language && [ 'solidity', 'vyper', 'yul' ].includes(data.language) ?
82+
<ContractCodeIdes hash={ sourceAddress } isLoading={ isLoading }/> :
83+
null;
7784

7885
const copyToClipboard = data && editorData?.length === 1 ? (
7986
<CopyToClipboard

ui/address/contract/info/ContractDetailsInfo.pw.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ test('zkSync contract', async({ render, mockEnvs }) => {
3131
await expect(component).toHaveScreenshot();
3232
});
3333

34+
test('stylus rust contract', async({ render, mockEnvs }) => {
35+
await mockEnvs(ENVS_MAP.zkSyncRollup);
36+
const props = {
37+
data: contractMock.stylusRust,
38+
isLoading: false,
39+
addressHash: addressMock.contract.hash,
40+
};
41+
const component = await render(<ContractDetailsInfo { ...props }/>);
42+
43+
await expect(component).toHaveScreenshot();
44+
});
45+
3446
test.describe('with audits feature', () => {
3547

3648
test.beforeEach(async({ mockEnvs }) => {

ui/address/contract/info/ContractDetailsInfo.tsx

+37-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { SmartContract } from 'types/api/contract';
66
import config from 'configs/app';
77
import { CONTRACT_LICENSES } from 'lib/contracts/licenses';
88
import dayjs from 'lib/date/dayjs';
9+
import { getGitHubOwnerAndRepo } from 'ui/contractVerification/utils';
910
import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
1011
import LinkExternal from 'ui/shared/links/LinkExternal';
1112

@@ -45,6 +46,25 @@ const ContractDetailsInfo = ({ data, isLoading, addressHash }: Props) => {
4546
);
4647
})();
4748

49+
const sourceCodeLink = (() => {
50+
if (!data.github_repository_metadata?.repository_url || !data.github_repository_metadata?.commit) {
51+
return null;
52+
}
53+
54+
const { owner, repo } = getGitHubOwnerAndRepo(data.github_repository_metadata.repository_url) || {};
55+
56+
const repoUrl = data.github_repository_metadata.repository_url;
57+
const commit = data.github_repository_metadata.commit;
58+
const pathPrefix = data.github_repository_metadata.path_prefix;
59+
return (
60+
<LinkExternal href={ `${ repoUrl }/tree/${ commit }${ pathPrefix ? `/${ pathPrefix }` : '' }` }>
61+
{ owner && repo ? `${ owner }/${ repo }` : data.github_repository_metadata.repository_url }
62+
</LinkExternal>
63+
);
64+
})();
65+
66+
const isStylusContract = data.language === 'stylus_rust';
67+
4868
return (
4969
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
5070
{ data.name && (
@@ -84,20 +104,27 @@ const ContractDetailsInfo = ({ data, isLoading, addressHash }: Props) => {
84104
isLoading={ isLoading }
85105
/>
86106
) }
87-
{ typeof data.optimization_enabled === 'boolean' && (
107+
{ typeof data.optimization_enabled === 'boolean' && !isStylusContract && (
88108
<ContractDetailsInfoItem
89109
label="Optimization enabled"
90110
content={ data.optimization_enabled ? 'true' : 'false' }
91111
isLoading={ isLoading }
92112
/>
93113
) }
94-
{ data.optimization_runs !== null && (
114+
{ data.optimization_runs !== null && !isStylusContract && (
95115
<ContractDetailsInfoItem
96116
label={ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' ? 'Optimization mode' : 'Optimization runs' }
97117
content={ String(data.optimization_runs) }
98118
isLoading={ isLoading }
99119
/>
100120
) }
121+
{ data.package_name && (
122+
<ContractDetailsInfoItem
123+
label="Package name"
124+
content={ data.package_name }
125+
isLoading={ isLoading }
126+
/>
127+
) }
101128
{ data.verified_at && (
102129
<ContractDetailsInfoItem
103130
label="Verified at"
@@ -106,14 +133,21 @@ const ContractDetailsInfo = ({ data, isLoading, addressHash }: Props) => {
106133
isLoading={ isLoading }
107134
/>
108135
) }
109-
{ data.file_path && (
136+
{ data.file_path && !isStylusContract && (
110137
<ContractDetailsInfoItem
111138
label="Contract file path"
112139
content={ data.file_path }
113140
wordBreak="break-word"
114141
isLoading={ isLoading }
115142
/>
116143
) }
144+
{ sourceCodeLink && (
145+
<ContractDetailsInfoItem
146+
label="Source code"
147+
content={ sourceCodeLink }
148+
isLoading={ isLoading }
149+
/>
150+
) }
117151
{ config.UI.hasContractAuditReports && (
118152
<ContractDetailsInfoItem
119153
label="Security audit"

0 commit comments

Comments
 (0)