Commit 0063a3fa authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #600 from blockscout/verification-redesign

contract verification updates
parents 2822ced9 e444deec
NEXT_PUBLIC_SENTRY_DSN=xxx NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_CSP_REPORT_URI=xxx SENTRY_CSP_REPORT_URI=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
\ No newline at end of file
...@@ -32,6 +32,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true ...@@ -32,6 +32,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-tracker','title':'Token Approval Tracker','logo':'https://approval-tracker.vercel.app/icon-192.png','categories':['security','tools'],'shortDescription':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','site':'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker','description':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','url':'https://approval-tracker.vercel.app/'},{'author': 'Revoke','id':'revoke.cash','title':'Revoke.cash','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FVBMGyUFnd6CScjfK7CYQ%252Frevoke_sing.png%3Falt%3Dmedia%26token%3D9ab94986-7ab1-41c8-bf7e-d9ce11d23182','categories':['security','tools'],'shortDescription': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','site': 'https://revoke.cash/about','description': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','url':'https://revoke.cash/'},{'author':'Aave','id': 'aave','title': 'Aave','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrZkUTIUCG7Zx8BW6Em34%252FAave.png%3Falt%3Dmedia%26token%3D249797a4-4c1e-4372-9cd2-3e48e05e5f30','categories':['tools'],'shortDescription':'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','site': 'https://docs.aave.com/faq/','description': 'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','url': 'https://staging.aave.com/'},{'author':'LooksRare','id':'looksrare','external':true,'title':'LooksRare','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FeAI4gy3qPMt68mZOZHAx%252FLooksRare.png%3Falt%3Dmedia%26token%3D44c01439-ae09-40aa-b904-3a9ce5b2e002','categories':['tools'],'shortDescription': 'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','site':'https://docs.looksrare.org/about/welcome-to-looksrare','description':'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','url': 'https://goerli.looksrare.org/'},{'author':'zkSync Bridge','id':'zksync-bridge','external':true,'title':'zkSync Bridge','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrtQsaAz9BjGBc35tVAnq%252FzkSync.png%3Falt%3Dmedia%26token%3D5c18171c-8ccf-4a88-8f44-680cbf238115','categories':['security','tools'],'shortDescription':'zkSync 2.0 Goerli Bridge','site':'https://v2-docs.zksync.io/dev/','description':'zkSync 2.0 Goerli Bridge','url':'https://portal.zksync.io/bridge'},{'author':'dYdX','id':'dydx','external':true,'title':'dYdX','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FCrOglR72wpi0UhEscwe4%252Fdxdy.png%3Falt%3Dmedia%26token%3D8811909e-93e3-487c-9614-dffce37223e9','categories': ['security','tools'],'shortDescription':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','site':'https://help.dydx.exchange/en/articles/3047379-introduction-and-overview','description':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','url':'https://trade.stage.dydx.exchange/portfolio/overview'},{'author':'MetalSwap','id':'metalswap','title':'MetalSwap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252F8xqldTvxb6avrwVVc3rS%252FMetalSwap.png%3Falt%3Dmedia%26token%3D92d2db99-853a-487d-8d8c-8cdeaeaaf014','categories':['security','tools'],'shortDescription':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','site':'https://docs.metalswap.finance/','description':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','url':'https://demo.metalswap.finance/'},{'author':'FaucetDao','id':'faucetdao','title':'FaucetDao','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252Ffnnt3ZNZhzRwqMM5YYjD%252FPlaceholder.png%3Falt%3Dmedia%26token%3D507571bb-d76f-4d96-a35e-2b278608f7ca','categories':['tools'],'shortDescription':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','site':'https://linktr.ee/faucet_dao','description':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','url':'https://www.faucetdao.shop/swap?chain=goerli'},{'author':'Uniswap','id':'uniswap','title':'Uniswap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FJc0QAyeaBmFIL97tSmGv%252FUniswap.png%3Falt%3Dmedia%26token%3D5d25d796-c273-4e22-92fa-ff85206bec76','categories':['tools'],'shortDescription':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','site':'https://docs.uniswap.org/','description':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','url':'https://app.uniswap.org/swap'}] NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-tracker','title':'Token Approval Tracker','logo':'https://approval-tracker.vercel.app/icon-192.png','categories':['security','tools'],'shortDescription':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','site':'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker','description':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','url':'https://approval-tracker.vercel.app/'},{'author': 'Revoke','id':'revoke.cash','title':'Revoke.cash','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FVBMGyUFnd6CScjfK7CYQ%252Frevoke_sing.png%3Falt%3Dmedia%26token%3D9ab94986-7ab1-41c8-bf7e-d9ce11d23182','categories':['security','tools'],'shortDescription': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','site': 'https://revoke.cash/about','description': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','url':'https://revoke.cash/'},{'author':'Aave','id': 'aave','title': 'Aave','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrZkUTIUCG7Zx8BW6Em34%252FAave.png%3Falt%3Dmedia%26token%3D249797a4-4c1e-4372-9cd2-3e48e05e5f30','categories':['tools'],'shortDescription':'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','site': 'https://docs.aave.com/faq/','description': 'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','url': 'https://staging.aave.com/'},{'author':'LooksRare','id':'looksrare','external':true,'title':'LooksRare','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FeAI4gy3qPMt68mZOZHAx%252FLooksRare.png%3Falt%3Dmedia%26token%3D44c01439-ae09-40aa-b904-3a9ce5b2e002','categories':['tools'],'shortDescription': 'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','site':'https://docs.looksrare.org/about/welcome-to-looksrare','description':'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','url': 'https://goerli.looksrare.org/'},{'author':'zkSync Bridge','id':'zksync-bridge','external':true,'title':'zkSync Bridge','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrtQsaAz9BjGBc35tVAnq%252FzkSync.png%3Falt%3Dmedia%26token%3D5c18171c-8ccf-4a88-8f44-680cbf238115','categories':['security','tools'],'shortDescription':'zkSync 2.0 Goerli Bridge','site':'https://v2-docs.zksync.io/dev/','description':'zkSync 2.0 Goerli Bridge','url':'https://portal.zksync.io/bridge'},{'author':'dYdX','id':'dydx','external':true,'title':'dYdX','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FCrOglR72wpi0UhEscwe4%252Fdxdy.png%3Falt%3Dmedia%26token%3D8811909e-93e3-487c-9614-dffce37223e9','categories': ['security','tools'],'shortDescription':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','site':'https://help.dydx.exchange/en/articles/3047379-introduction-and-overview','description':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','url':'https://trade.stage.dydx.exchange/portfolio/overview'},{'author':'MetalSwap','id':'metalswap','title':'MetalSwap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252F8xqldTvxb6avrwVVc3rS%252FMetalSwap.png%3Falt%3Dmedia%26token%3D92d2db99-853a-487d-8d8c-8cdeaeaaf014','categories':['security','tools'],'shortDescription':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','site':'https://docs.metalswap.finance/','description':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','url':'https://demo.metalswap.finance/'},{'author':'FaucetDao','id':'faucetdao','title':'FaucetDao','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252Ffnnt3ZNZhzRwqMM5YYjD%252FPlaceholder.png%3Falt%3Dmedia%26token%3D507571bb-d76f-4d96-a35e-2b278608f7ca','categories':['tools'],'shortDescription':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','site':'https://linktr.ee/faucet_dao','description':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','url':'https://www.faucetdao.shop/swap?chain=goerli'},{'author':'Uniswap','id':'uniswap','title':'Uniswap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FJc0QAyeaBmFIL97tSmGv%252FUniswap.png%3Falt%3Dmedia%26token%3D5d25d796-c273-4e22-92fa-ff85206bec76','categories':['tools'],'shortDescription':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','site':'https://docs.uniswap.org/','description':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','url':'https://app.uniswap.org/swap'}]
NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
# api config # api config
NEXT_PUBLIC_API_BASE_PATH=/poa/core NEXT_PUBLIC_API_BASE_PATH=/poa/core
......
<svg viewBox="0 0 51 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.693 5.058c.498-.57 1.172-.891 1.875-.891h15.91c.351 0 .688.16.937.446l9.28 10.657c.249.285.388.672.388 1.076v24.36c0 .807-.279 1.581-.776 2.152-.498.571-1.172.892-1.875.892H13.568c-.703 0-1.377-.32-1.875-.892-.497-.57-.776-1.345-.776-2.153V7.212c0-.808.28-1.582.776-2.154Zm17.235 2.154h-15.36v33.493h23.864V16.977l-8.504-9.765Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.556 5c.767 0 1.389.622 1.389 1.389v8.333h8.333a1.389 1.389 0 0 1 0 2.778h-9.722a1.389 1.389 0 0 1-1.39-1.389V6.39c0-.767.623-1.389 1.39-1.389ZM22.46 25.151a1.326 1.326 0 0 0-1.875 1.875l3.04 3.04-3.04 3.04a1.326 1.326 0 0 0 1.875 1.875l3.04-3.04 3.04 3.04a1.326 1.326 0 0 0 1.875-1.875l-3.04-3.04 3.04-3.04a1.326 1.326 0 0 0-1.875-1.875l-3.04 3.04-3.04-3.04Z" fill="currentColor"/>
</svg>
...@@ -36,8 +36,10 @@ export function middleware(req: NextRequest) { ...@@ -36,8 +36,10 @@ export function middleware(req: NextRequest) {
/** /**
* Configure which routes should pass through the Middleware. * Configure which routes should pass through the Middleware.
* Exclude all `_next` urls.
*/ */
export const config = { export const config = {
matcher: [ '/', '/:notunderscore((?!_next).+)' ], matcher: [ '/', '/:notunderscore((?!_next).+)' ],
// matcher: [
// '/((?!.*\\.|api\\/|node-api\\/).*)', // exclude all static + api + node-api routes
// ],
}; };
...@@ -34,6 +34,11 @@ const moduleExports = { ...@@ -34,6 +34,11 @@ const moduleExports = {
redirects, redirects,
headers, headers,
output: 'standalone', output: 'standalone',
api: {
// disable body parser since we use next.js api only for local development and as a proxy
// otherwise it is impossible to upload large files (over 1Mb)
bodyParser: false,
},
}; };
module.exports = withRoutes(moduleExports); module.exports = withRoutes(moduleExports);
...@@ -4,6 +4,7 @@ import { WebSocketServer } from 'ws'; ...@@ -4,6 +4,7 @@ import { WebSocketServer } from 'ws';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
type ReturnType = () => Promise<WebSocket>; type ReturnType = () => Promise<WebSocket>;
...@@ -59,6 +60,7 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_bal ...@@ -59,6 +60,7 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_bal
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void { export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([ socket.send(JSON.stringify([
...channel, ...channel,
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract'; import type { SmartContractVerificationConfig } from 'types/api/contract';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import ContractVerificationForm from './ContractVerificationForm'; import ContractVerificationForm from './ContractVerificationForm';
...@@ -34,6 +35,7 @@ const formConfig: SmartContractVerificationConfig = { ...@@ -34,6 +35,7 @@ const formConfig: SmartContractVerificationConfig = {
'sourcify', 'sourcify',
'multi-part', 'multi-part',
'vyper-code', 'vyper-code',
'vyper-multi-part',
], ],
vyper_compiler_versions: [ vyper_compiler_versions: [
'v0.3.7+commit.6020b8bb', 'v0.3.7+commit.6020b8bb',
...@@ -60,8 +62,9 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) = ...@@ -60,8 +62,9 @@ test('flatten source code method +@dark-mode +@mobile', async({ mount, page }) =
{ hooksConfig }, { hooksConfig },
); );
await page.getByText(/flattened source code/i).click(); await component.getByLabel(/verification method/i).focus();
await page.getByText(/optimization enabled/i).click(); await component.getByLabel(/verification method/i).type('solidity');
await page.getByRole('button', { name: /flattened source code/i }).click();
await page.getByText(/add contract libraries/i).click(); await page.getByText(/add contract libraries/i).click();
await page.locator('button[aria-label="add"]').click(); await page.locator('button[aria-label="add"]').click();
...@@ -76,21 +79,32 @@ test('standard input json method', async({ mount, page }) => { ...@@ -76,21 +79,32 @@ test('standard input json method', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await page.getByText(/via standard/i).click(); await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('solidity');
await page.getByRole('button', { name: /standard json input/i }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { test.describe('sourcify', () => {
const testWithSocket = test.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
testWithSocket.describe.configure({ mode: 'serial', timeout: 20_000 });
testWithSocket('with multiple contracts +@mobile', async({ mount, page, createSocket }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp withSocket>
<ContractVerificationForm config={ formConfig } hash={ hash }/> <ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
await page.getByText(/via sourcify/i).click(); await component.getByLabel(/verification method/i).focus();
await page.getByText(/upload files/i).click(); await component.getByLabel(/verification method/i).type('solidity');
await page.getByRole('button', { name: /sourcify/i }).click();
await page.getByText(/drop files/i).click();
await page.locator('input[name="sources"]').setInputFiles([ await page.locator('input[name="sources"]').setInputFiles([
'./playwright/mocks/file_mock_1.json', './playwright/mocks/file_mock_1.json',
'./playwright/mocks/file_mock_2.json', './playwright/mocks/file_mock_2.json',
...@@ -98,6 +112,28 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -98,6 +112,28 @@ test('sourcify method +@dark-mode +@mobile', async({ mount, page }) => {
]); ]);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ hash.toLowerCase() }`);
await page.getByRole('button', { name: /verify/i }).click();
socketServer.sendMessage(socket, channel, 'verification_result', {
status: 'error',
errors: {
// eslint-disable-next-line max-len
files: [ 'Detected 5 contracts (ERC20, IERC20, IERC20Metadata, Context, MockERC20), but can only verify 1 at a time. Please choose a main contract and click Verify again.' ],
},
});
await component.getByLabel(/contract name/i).focus();
await component.getByLabel(/contract name/i).type('e');
const contractNameOption = page.getByRole('button', { name: /MockERC20/i });
await expect(contractNameOption).toBeVisible();
await expect(component).toHaveScreenshot();
});
}); });
test('multi-part files method', async({ mount, page }) => { test('multi-part files method', async({ mount, page }) => {
...@@ -108,7 +144,9 @@ test('multi-part files method', async({ mount, page }) => { ...@@ -108,7 +144,9 @@ test('multi-part files method', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await page.getByText(/via multi-part files/i).click(); await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('solidity');
await page.getByRole('button', { name: /multi-part files/i }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -121,7 +159,24 @@ test('vyper contract method', async({ mount, page }) => { ...@@ -121,7 +159,24 @@ test('vyper contract method', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await page.getByText(/vyper contract/i).click(); await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('vyper');
await page.getByRole('button', { name: /contract/i }).click();
await expect(component).toHaveScreenshot();
});
test('vyper multi-part method', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
</TestApp>,
{ hooksConfig },
);
await component.getByLabel(/verification method/i).focus();
await component.getByLabel(/verification method/i).type('vyper');
await page.getByRole('button', { name: /multi-part files/i }).click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { Button, chakra } from '@chakra-ui/react'; import { Button, chakra, useUpdateEffect } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
...@@ -20,7 +20,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify ...@@ -20,7 +20,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput'; import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract'; import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile'; import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors } from './utils'; import { prepareRequestBody, formatSocketErrors, DEFAULT_VALUES } from './utils';
const METHOD_COMPONENTS = { const METHOD_COMPONENTS = {
'flattened-code': <ContractVerificationFlattenSourceCode/>, 'flattened-code': <ContractVerificationFlattenSourceCode/>,
...@@ -40,11 +40,9 @@ interface Props { ...@@ -40,11 +40,9 @@ interface Props {
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => { const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: { defaultValues: methodFromQuery ? DEFAULT_VALUES[methodFromQuery] : undefined,
method: methodFromQuery,
},
}); });
const { control, handleSubmit, watch, formState, setError } = formApi; const { control, handleSubmit, watch, formState, setError, reset } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>(); const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
...@@ -56,7 +54,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -56,7 +54,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
try { try {
await apiFetch('contract_verification_via', { await apiFetch('contract_verification_via', {
pathParams: { method: data.method, hash }, pathParams: { method: data.method.value, hash: hash.toLowerCase() },
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body, body,
...@@ -109,7 +107,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -109,7 +107,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
variant: 'subtle', variant: 'subtle',
isClosable: true, isClosable: true,
}); });
}, [ formState.isSubmitting, toast ]); // callback should not change when form is submitted
// otherwise it will resubscribe to channel, but we don't want that since in that case we might miss verification result message
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ toast ]);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`, topic: `addresses:${ hash.toLowerCase() }`,
...@@ -124,7 +125,15 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -124,7 +125,15 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
}); });
const method = watch('method'); const method = watch('method');
const content = METHOD_COMPONENTS[method] || null; const content = METHOD_COMPONENTS[method?.value] || null;
const methodValue = method?.value;
useUpdateEffect(() => {
if (methodValue) {
reset(DEFAULT_VALUES[methodValue]);
}
// !!! should run only when method is changed
}, [ methodValue ]);
return ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
...@@ -134,8 +143,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -134,8 +143,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
> >
<ContractVerificationFieldMethod <ContractVerificationFieldMethod
control={ control } control={ control }
isDisabled={ Boolean(method) }
methods={ config.verification_options } methods={ config.verification_options }
isDisabled={ formState.isSubmitting }
/> />
{ content } { content }
{ Boolean(method) && ( { Boolean(method) && (
......
...@@ -15,8 +15,8 @@ const ContractVerificationMethod = ({ title, children }: Props) => { ...@@ -15,8 +15,8 @@ const ContractVerificationMethod = ({ title, children }: Props) => {
return ( return (
<section ref={ ref }> <section ref={ ref }>
<Text variant="secondary" mt={ 12 } mb={ 5 } fontSize="sm">{ title }</Text> <Text fontWeight={ 500 } fontSize="lg" fontFamily="heading" mt={ 12 } mb={ 5 }>{ title }</Text>
<Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}> <Grid columnGap="30px" rowGap={{ base: 2, lg: 5 }} templateColumns={{ base: '1fr', lg: 'minmax(0, 680px) minmax(0, 340px)' }}>
{ children } { children }
</Grid> </Grid>
</section> </section>
......
...@@ -34,7 +34,6 @@ const ContractVerificationFieldAutodetectArgs = () => { ...@@ -34,7 +34,6 @@ const ContractVerificationFieldAutodetectArgs = () => {
name="autodetect_constructor_args" name="autodetect_constructor_args"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
defaultValue={ true }
/> />
</ContractVerificationFormRow> </ContractVerificationFormRow>
{ !isOn && <ContractVerificationFieldConstructorArgs/> } { !isOn && <ContractVerificationFieldConstructorArgs/> }
......
...@@ -41,7 +41,6 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => { ...@@ -41,7 +41,6 @@ const ContractVerificationFieldCode = ({ isVyper }: Props) => {
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }} rules={{ required: true }}
defaultValue=""
/> />
{ isVyper ? null : ( { isVyper ? null : (
<> <>
......
import { Code } from '@chakra-ui/react'; import { chakra, Checkbox, Code } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps } from 'react-hook-form';
...@@ -20,26 +20,30 @@ interface Props { ...@@ -20,26 +20,30 @@ interface Props {
} }
const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
const { formState, control } = useFormContext<FormFields>(); const [ isNightly, setIsNightly ] = React.useState(false);
const { formState, control, getValues, resetField } = useFormContext<FormFields>();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config')); const config = queryClient.getQueryData<SmartContractVerificationConfig>(getResourceKey('contract_verification_config'));
const handleCheckboxChange = React.useCallback(() => {
if (isNightly) {
const field = getValues('compiler');
field?.value.includes('nightly') && resetField('compiler', { defaultValue: null });
}
setIsNightly(prev => !prev);
}, [ getValues, isNightly, resetField ]);
const options = React.useMemo(() => ( const options = React.useMemo(() => (
(isVyper ? config?.vyper_compiler_versions : config?.solidity_compiler_versions)?.map((option) => ({ label: option, value: option })) || [] (isVyper ? config?.vyper_compiler_versions : config?.solidity_compiler_versions)?.map((option) => ({ label: option, value: option })) || []
), [ config?.solidity_compiler_versions, config?.vyper_compiler_versions, isVyper ]); ), [ config?.solidity_compiler_versions, config?.vyper_compiler_versions, isVyper ]);
const loadOptions = React.useCallback(async(inputValue: string) => { const loadOptions = React.useCallback(async(inputValue: string) => {
return options return options
.filter(({ label }) => { .filter(({ label }) => !inputValue || label.toLowerCase().includes(inputValue.toLowerCase()))
if (!inputValue) { .filter(({ label }) => isNightly ? true : !label.includes('nightly'))
return !label.toLowerCase().includes('nightly');
}
return label.toLowerCase().includes(inputValue.toLowerCase());
})
.slice(0, OPTIONS_LIMIT); .slice(0, OPTIONS_LIMIT);
}, [ options ]); }, [ isNightly, options ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'compiler'>}) => {
const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined; const error = 'compiler' in formState.errors ? formState.errors.compiler : undefined;
...@@ -61,20 +65,32 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => { ...@@ -61,20 +65,32 @@ const ContractVerificationFieldCompiler = ({ isVyper }: Props) => {
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
<>
{ !isVyper && (
<Checkbox
size="lg"
mb={ 2 }
onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting }
>
Include nightly builds
</Checkbox>
) }
<Controller <Controller
name="compiler" name="compiler"
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }} rules={{ required: true }}
/> />
</>
{ isVyper ? null : ( { isVyper ? null : (
<> <chakra.div mt={{ base: 0, lg: 8 }}>
<span>The compiler version is specified in </span> <span >The compiler version is specified in </span>
<Code color="text_secondary">pragma solidity X.X.X</Code> <Code color="text_secondary">pragma solidity X.X.X</Code>
<span>. Use the compiler version rather than the nightly build. If using the Solidity compiler, run </span> <span>. Use the compiler version rather than the nightly build. If using the Solidity compiler, run </span>
<Code color="text_secondary">solc —version</Code> <Code color="text_secondary">solc —version</Code>
<span> to check.</span> <span> to check.</span>
</> </chakra.div>
) } ) }
</ContractVerificationFormRow> </ContractVerificationFormRow>
); );
......
import { useUpdateEffect } from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import { useFormContext, Controller } from 'react-hook-form';
import type { FormFields } from '../types';
import type { Option } from 'ui/shared/FancySelect/types';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import ContractVerificationFormRow from '../ContractVerificationFormRow';
const SOURCIFY_ERROR_REGEXP = /\(([^()]*)\)/;
const ContractVerificationFieldContractIndex = () => {
const [ options, setOptions ] = React.useState<Array<Option>>([]);
const { formState, control, watch } = useFormContext<FormFields>();
const isMobile = useIsMobile();
const sources = watch('sources');
const sourcesError = 'sources' in formState.errors ? formState.errors.sources?.message : undefined;
useUpdateEffect(() => {
if (!sourcesError) {
return;
}
const matchResult = sourcesError.match(SOURCIFY_ERROR_REGEXP);
const parsedMethods = matchResult?.[1].split(',');
if (!Array.isArray(parsedMethods) || parsedMethods.length === 0) {
return;
}
const newOptions = parsedMethods.map((option, index) => ({ label: option, value: String(index + 1) }));
setOptions(newOptions);
}, [ sourcesError ]);
useUpdateEffect(() => {
setOptions([]);
}, [ sources ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'contract_index'>}) => {
const error = 'contract_index' in formState.errors ? formState.errors.contract_index : undefined;
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Contract name"
isDisabled={ formState.isSubmitting }
error={ error }
isRequired
isAsync={ false }
/>
);
}, [ formState.errors, formState.isSubmitting, isMobile, options ]);
if (options.length === 0) {
return null;
}
return (
<ContractVerificationFormRow>
<Controller
name="contract_index"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
</ContractVerificationFormRow>
);
};
export default React.memo(ContractVerificationFieldContractIndex);
import { Checkbox } from '@chakra-ui/react'; import { Checkbox, useUpdateEffect } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form'; import { useFieldArray, useFormContext } from 'react-hook-form';
...@@ -8,13 +8,21 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow'; ...@@ -8,13 +8,21 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
import ContractVerificationFieldLibraryItem from './ContractVerificationFieldLibraryItem'; import ContractVerificationFieldLibraryItem from './ContractVerificationFieldLibraryItem';
const ContractVerificationFieldLibraries = () => { const ContractVerificationFieldLibraries = () => {
const { formState, control } = useFormContext<FormFields>(); const { formState, control, getValues } = useFormContext<FormFields>();
const { fields, append, remove, insert } = useFieldArray({ const { fields, append, remove, insert } = useFieldArray({
name: 'libraries', name: 'libraries',
control, control,
}); });
const [ isEnabled, setIsEnabled ] = React.useState(fields.length > 0); const [ isEnabled, setIsEnabled ] = React.useState(fields.length > 0);
const value = getValues('libraries');
useUpdateEffect(() => {
if (!value || value.length === 0) {
setIsEnabled(false);
}
}, [ value ]);
const handleCheckboxChange = React.useCallback(() => { const handleCheckboxChange = React.useCallback(() => {
if (!isEnabled) { if (!isEnabled) {
append({ name: '', address: '' }); append({ name: '', address: '' });
......
import { import {
RadioGroup,
Radio,
Stack,
Text,
Link, Link,
Icon, Icon,
chakra, chakra,
...@@ -14,16 +10,23 @@ import { ...@@ -14,16 +10,23 @@ import {
PopoverBody, PopoverBody,
useColorModeValue, useColorModeValue,
DarkMode, DarkMode,
useBoolean, ListItem,
OrderedList,
Grid,
Box,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form'; import type { ControllerRenderProps, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import type { SmartContractVerificationMethod, SmartContractVerificationConfig } from 'types/api/contract'; import type { SmartContractVerificationConfig, SmartContractVerificationMethod } from 'types/api/contract';
import infoIcon from 'icons/info.svg'; import infoIcon from 'icons/info.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import FancySelect from 'ui/shared/FancySelect/FancySelect';
import { METHOD_LABELS } from '../utils';
interface Props { interface Props {
control: Control<FormFields>; control: Control<FormFields>;
...@@ -32,91 +35,93 @@ interface Props { ...@@ -32,91 +35,93 @@ interface Props {
} }
const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => { const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props) => {
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean();
const tooltipBg = useColorModeValue('gray.700', 'gray.900'); const tooltipBg = useColorModeValue('gray.700', 'gray.900');
const isMobile = useIsMobile();
const options = React.useMemo(() => methods.map((method) => ({
value: method,
label: METHOD_LABELS[method],
})), [ methods ]);
const renderItem = React.useCallback((method: SmartContractVerificationMethod) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
return (
<FancySelect
{ ...field }
options={ options }
size={ isMobile ? 'md' : 'lg' }
placeholder="Verification method (compiler type)"
isDisabled={ isDisabled }
isRequired
isAsync={ false }
/>
);
}, [ isDisabled, isMobile, options ]);
const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => {
switch (method) { switch (method) {
case 'flattened-code': case 'flattened-code':
return 'Via flattened source code'; return <ListItem>Verification through flattened source code.</ListItem>;
case 'multi-part':
return <ListItem>Verification of multi-part Solidity files.</ListItem>;
case 'sourcify':
return <ListItem>Verification through <Link href="https://sourcify.dev/" target="_blank">Sourcify</Link>.</ListItem>;
case 'standard-input': case 'standard-input':
return ( return (
<> <ListItem>
<span>Via standard </span> <span>Verification using </span>
<Link <Link
href={ isDisabled ? undefined : 'https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description' } href="https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description"
target="_blank" target="_blank"
cursor={ isDisabled ? 'not-allowed' : 'pointer' }
> >
Input JSON Standard input JSON
</Link> </Link>
</> <span> file.</span>
</ListItem>
); );
case 'sourcify': case 'vyper-code':
return <ListItem>Verification of Vyper contract.</ListItem>;
case 'vyper-multi-part':
return <ListItem>Verification of multi-part Vyper files.</ListItem>;
}
}, []);
return ( return (
<> <section>
<span>Via sourcify: sources and metadata JSON file</span> <Grid columnGap="30px" rowGap={{ base: 2, lg: 4 }} templateColumns={{ base: '1fr', lg: 'minmax(auto, 680px) minmax(0, 340px)' }}>
<Popover trigger="hover" isLazy isOpen={ isDisabled ? false : isPopoverOpen } onOpen={ setIsPopoverOpen.on } onClose={ setIsPopoverOpen.off }> <div>
<Box mb={ 5 }>
<chakra.span fontWeight={ 500 } fontSize="lg" fontFamily="heading">
Currently, Blockscout supports { methods.length } contract verification methods
</chakra.span>
<Popover trigger="hover" isLazy placement={ isMobile ? 'bottom-end' : 'right-start' } offset={ [ -8, 8 ] }>
<PopoverTrigger> <PopoverTrigger>
<chakra.span cursor={ isDisabled ? 'not-allowed' : 'pointer' } display="inline-block" verticalAlign="middle" h="24px" ml={ 1 }> <chakra.span display="inline-block" ml={ 1 } cursor="pointer" verticalAlign="middle" h="22px">
<Icon as={ infoIcon } boxSize={ 5 } color="link" _hover={{ color: 'link_hovered' }}/> <Icon as={ infoIcon } boxSize={ 5 } color="link" _hover={{ color: 'link_hovered' }}/>
</chakra.span> </chakra.span>
</PopoverTrigger> </PopoverTrigger>
<Portal> <Portal>
<PopoverContent bgColor={ tooltipBg }> <PopoverContent bgColor={ tooltipBg } w={{ base: '300px', lg: '380px' }}>
<PopoverArrow bgColor={ tooltipBg }/> <PopoverArrow bgColor={ tooltipBg }/>
<PopoverBody color="white"> <PopoverBody color="white">
<DarkMode> <DarkMode>
<div> <span>Currently, Blockscout supports { methods.length } methods:</span>
<span>Verification through </span> <OrderedList>
<Link href="https://sourcify.dev/" target="_blank">Sourcify</Link> { methods.map(renderPopoverListItem) }
</div> </OrderedList>
<div>
<span>a) if smart-contract already verified on Sourcify, it will automatically fetch the data from the </span>
<Link href="https://repo.sourcify.dev/" target="_blank">repo</Link>
</div>
<div>
b) otherwise you will be asked to upload source files and JSON metadata file(s).
</div>
</DarkMode> </DarkMode>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Portal> </Portal>
</Popover> </Popover>
</> </Box>
);
case 'multi-part':
return 'Via multi-part files';
case 'vyper-code':
return 'Vyper contract';
case 'vyper-multi-part':
return 'Via multi-part Vyper files';
default:
break;
}
}, [ isDisabled, isPopoverOpen, setIsPopoverOpen.off, setIsPopoverOpen.on, tooltipBg ]);
const renderRadioGroup = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'method'>}) => {
return (
<RadioGroup defaultValue="add" colorScheme="blue" isDisabled={ isDisabled } isFocusable={ !isDisabled } { ...field } >
<Stack spacing={ 4 }>
{ methods.map((method) => {
return <Radio key={ method } value={ method } size="lg">{ renderItem(method) }</Radio>;
}) }
</Stack>
</RadioGroup>
);
}, [ isDisabled, methods, renderItem ]);
return (
<section>
<Text variant="secondary" fontSize="sm" mb={ 5 }>Smart-contract verification method</Text>
<Controller <Controller
name="method" name="method"
control={ control } control={ control }
render={ renderRadioGroup } render={ renderControl }
rules={{ required: true }}
/> />
</div>
</Grid>
</section> </section>
); );
}; };
......
...@@ -41,7 +41,6 @@ const ContractVerificationFieldName = ({ hint }: Props) => { ...@@ -41,7 +41,6 @@ const ContractVerificationFieldName = ({ hint }: Props) => {
control={ control } control={ control }
render={ renderControl } render={ renderControl }
rules={{ required: true }} rules={{ required: true }}
defaultValue=""
/> />
{ hint ? <span>{ hint }</span> : ( { hint ? <span>{ hint }</span> : (
<> <>
......
import { FormControl, Input } from '@chakra-ui/react'; import { Flex, Input } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps } from 'react-hook-form'; import type { ControllerRenderProps } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
...@@ -6,7 +6,6 @@ import { Controller, useFormContext } from 'react-hook-form'; ...@@ -6,7 +6,6 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import CheckboxInput from 'ui/shared/CheckboxInput'; import CheckboxInput from 'ui/shared/CheckboxInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import ContractVerificationFormRow from '../ContractVerificationFormRow'; import ContractVerificationFormRow from '../ContractVerificationFormRow';
...@@ -14,56 +13,59 @@ const ContractVerificationFieldOptimization = () => { ...@@ -14,56 +13,59 @@ const ContractVerificationFieldOptimization = () => {
const [ isEnabled, setIsEnabled ] = React.useState(true); const [ isEnabled, setIsEnabled ] = React.useState(true);
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const error = 'optimization_runs' in formState.errors ? formState.errors.optimization_runs : undefined;
const handleCheckboxChange = React.useCallback(() => { const handleCheckboxChange = React.useCallback(() => {
setIsEnabled(prev => !prev); setIsEnabled(prev => !prev);
}, []); }, []);
const renderCheckboxControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_optimization_enabled'>}) => ( const renderCheckboxControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'is_optimization_enabled'>}) => (
<Flex flexShrink={ 0 }>
<CheckboxInput<FormFields, 'is_optimization_enabled'> <CheckboxInput<FormFields, 'is_optimization_enabled'>
text="Optimization enabled" text="Optimization enabled"
field={ field } field={ field }
onChange={ handleCheckboxChange } onChange={ handleCheckboxChange }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
/> />
</Flex>
), [ formState.isSubmitting, handleCheckboxChange ]); ), [ formState.isSubmitting, handleCheckboxChange ]);
const renderInputControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'optimization_runs'>}) => { const renderInputControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'optimization_runs'>}) => {
return ( return (
<FormControl variant="floating" id={ field.name } size={{ base: 'md', lg: 'lg' }} isRequired>
<Input <Input
{ ...field } { ...field }
required required
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
autoComplete="off" autoComplete="off"
type="number" type="number"
placeholder="Optimization runs"
size="xs"
minW="100px"
maxW="200px"
flexShrink={ 1 }
isInvalid={ Boolean(error) }
/> />
<InputPlaceholder text="Optimization runs"/>
</FormControl>
); );
}, [ formState.isSubmitting ]); }, [ error, formState.isSubmitting ]);
return ( return (
<>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Flex columnGap={ 5 } h={{ base: 'auto', lg: '32px' }}>
<Controller <Controller
name="is_optimization_enabled" name="is_optimization_enabled"
control={ control } control={ control }
render={ renderCheckboxControl } render={ renderCheckboxControl }
defaultValue={ true }
/> />
</ContractVerificationFormRow>
{ isEnabled && ( { isEnabled && (
<ContractVerificationFormRow>
<Controller <Controller
name="optimization_runs" name="optimization_runs"
control={ control } control={ control }
render={ renderInputControl } render={ renderInputControl }
rules={{ required: true }} rules={{ required: true }}
defaultValue="200"
/> />
</ContractVerificationFormRow>
) } ) }
</> </Flex>
</ContractVerificationFormRow>
); );
}; };
......
import { Text, Button, Box, chakra, Flex } from '@chakra-ui/react'; import { Text, Button, Box, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form'; import type { ControllerRenderProps, FieldPathValue, ValidateResult } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
...@@ -6,7 +6,7 @@ import { Controller, useFormContext } from 'react-hook-form'; ...@@ -6,7 +6,7 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types'; import type { FormFields } from '../types';
import { Mb } from 'lib/consts'; import { Mb } from 'lib/consts';
// import DragAndDropArea from 'ui/shared/forms/DragAndDropArea'; import DragAndDropArea from 'ui/shared/forms/DragAndDropArea';
import FieldError from 'ui/shared/forms/FieldError'; import FieldError from 'ui/shared/forms/FieldError';
import FileInput from 'ui/shared/forms/FileInput'; import FileInput from 'ui/shared/forms/FileInput';
import FileSnippet from 'ui/shared/forms/FileSnippet'; import FileSnippet from 'ui/shared/forms/FileSnippet';
...@@ -19,11 +19,10 @@ interface Props { ...@@ -19,11 +19,10 @@ interface Props {
fileTypes: Array<FileTypes>; fileTypes: Array<FileTypes>;
multiple?: boolean; multiple?: boolean;
title: string; title: string;
className?: string;
hint: string; hint: string;
} }
const ContractVerificationFieldSources = ({ fileTypes, multiple, title, className, hint }: Props) => { const ContractVerificationFieldSources = ({ fileTypes, multiple, title, hint }: Props) => {
const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>(); const { setValue, getValues, control, formState, clearErrors } = useFormContext<FormFields>();
const error = 'sources' in formState.errors ? formState.errors.sources : undefined; const error = 'sources' in formState.errors ? formState.errors.sources : undefined;
...@@ -42,11 +41,28 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -42,11 +41,28 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
}, [ getValues, clearErrors, setValue ]); }, [ getValues, clearErrors, setValue ]);
const renderUploadButton = React.useCallback(() => {
return (
<div>
<Text fontWeight={ 500 } color="text_secondary" mb={ 3 }>{ title }</Text>
<Button size="sm" variant="outline">
Drop file{ multiple ? 's' : '' } or click here
</Button>
</div>
);
}, [ multiple, title ]);
const renderFiles = React.useCallback((files: Array<File>) => { const renderFiles = React.useCallback((files: Array<File>) => {
const errorList = fileError?.message?.split(';'); const errorList = fileError?.message?.split(';');
return ( return (
<Box display="grid" gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }} columnGap={ 3 } rowGap={ 3 }> <Box
display="grid"
gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(0, 1fr) minmax(0, 1fr)' }}
columnGap={ 3 }
rowGap={ 3 }
w="100%"
>
{ files.map((file, index) => ( { files.map((file, index) => (
<Box key={ file.name + file.lastModified + index }> <Box key={ file.name + file.lastModified + index }>
<FileSnippet <FileSnippet
...@@ -55,42 +71,36 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -55,42 +71,36 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
onRemove={ handleFileRemove } onRemove={ handleFileRemove }
index={ index } index={ index }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting }
error={ errorList?.[index] }
/> />
{ errorList?.[index] && <FieldError message={ errorList?.[index] } mt={ 1 } px={ 3 }/> }
</Box> </Box>
)) } )) }
</Box> </Box>
); );
}, [ formState.isSubmitting, handleFileRemove, fileError ]); }, [ formState.isSubmitting, handleFileRemove, fileError ]);
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => ( const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'sources'>}) => {
const hasValue = field.value && field.value.length > 0;
return (
<> <>
<FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }> <FileInput<FormFields, 'sources'> accept={ fileTypes.join(',') } multiple={ multiple } field={ field }>
{ () => ( { ({ onChange }) => (
<Flex <Flex
flexDir="column" flexDir="column"
alignItems="flex-start" alignItems="flex-start"
rowGap={ 2 } rowGap={ 2 }
w="100%" w="100%"
display={ field.value && field.value.length > 0 && !multiple ? 'none' : 'block' }
mb={ field.value && field.value.length > 0 ? 2 : 0 }
> >
<Button <DragAndDropArea onDrop={ onChange } p={{ base: 3, lg: 6 }} isDisabled={ formState.isSubmitting }>
variant="outline" { hasValue ? renderFiles(field.value) : renderUploadButton() }
size="sm" </DragAndDropArea>
// mb={ 2 }
>
Upload file{ multiple ? 's' : '' }
</Button>
{ /* design is not ready */ }
{ /* <DragAndDropArea onDrop={ onChange }/> */ }
</Flex> </Flex>
) } ) }
</FileInput> </FileInput>
{ field.value && field.value.length > 0 && renderFiles(field.value) }
{ commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> } { commonError?.message && <FieldError message={ commonError.type === 'required' ? 'Field is required' : commonError.message }/> }
</> </>
), [ fileTypes, commonError, multiple, renderFiles ]); );
}, [ fileTypes, multiple, commonError, formState.isSubmitting, renderFiles, renderUploadButton ]);
const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => { const validateFileType = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
...@@ -114,19 +124,24 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -114,19 +124,24 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
return true; return true;
}, []); }, []);
const validateQuantity = React.useCallback(async(value: FieldPathValue<FormFields, 'sources'>): Promise<ValidateResult> => {
if (!multiple && Array.isArray(value) && value.length > 1) {
return 'You can upload only one file';
}
return true;
}, [ multiple ]);
const rules = React.useMemo(() => ({ const rules = React.useMemo(() => ({
required: true, required: true,
validate: { validate: {
file_type: validateFileType, file_type: validateFileType,
file_size: validateFileSize, file_size: validateFileSize,
quantity: validateQuantity,
}, },
}), [ validateFileSize, validateFileType ]); }), [ validateFileSize, validateFileType, validateQuantity ]);
return ( return (
<>
<ContractVerificationFormRow >
<Text fontWeight={ 500 } className={ className } mt={ 4 }>{ title }</Text>
</ContractVerificationFormRow>
<ContractVerificationFormRow> <ContractVerificationFormRow>
<Controller <Controller
name="sources" name="sources"
...@@ -136,8 +151,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam ...@@ -136,8 +151,7 @@ const ContractVerificationFieldSources = ({ fileTypes, multiple, title, classNam
/> />
{ hint ? <span>{ hint }</span> : null } { hint ? <span>{ hint }</span> : null }
</ContractVerificationFormRow> </ContractVerificationFormRow>
</>
); );
}; };
export default React.memo(chakra(ContractVerificationFieldSources)); export default React.memo(ContractVerificationFieldSources);
...@@ -12,9 +12,9 @@ import ContractVerificationFieldOptimization from '../fields/ContractVerificatio ...@@ -12,9 +12,9 @@ import ContractVerificationFieldOptimization from '../fields/ContractVerificatio
const ContractVerificationFlattenSourceCode = () => { const ContractVerificationFlattenSourceCode = () => {
return ( return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (fattened source code)">
<ContractVerificationFieldIsYul/>
<ContractVerificationFieldName/> <ContractVerificationFieldName/>
<ContractVerificationFieldIsYul/>
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
......
...@@ -11,7 +11,7 @@ const FILE_TYPES = [ '.sol' as const, '.yul' as const ]; ...@@ -11,7 +11,7 @@ const FILE_TYPES = [ '.sol' as const, '.yul' as const ];
const ContractVerificationMultiPartFile = () => { const ContractVerificationMultiPartFile = () => {
return ( return (
<ContractVerificationMethod title="New Solidity/Yul Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (multi-part files)">
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
......
import React from 'react'; import React from 'react';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldContractIndex from '../fields/ContractVerificationFieldContractIndex';
import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources'; import ContractVerificationFieldSources from '../fields/ContractVerificationFieldSources';
const FILE_TYPES = [ '.json' as const, '.sol' as const ]; const FILE_TYPES = [ '.json' as const, '.sol' as const ];
const ContractVerificationSourcify = () => { const ContractVerificationSourcify = () => {
return ( return (
<ContractVerificationMethod title="New Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (Sourcify)">
<ContractVerificationFieldSources <ContractVerificationFieldSources
fileTypes={ FILE_TYPES } fileTypes={ FILE_TYPES }
multiple multiple
title="Sources and Metadata JSON" mt={ 0 } title="Sources and Metadata JSON"
hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation." hint="Upload all Solidity contract source files and JSON metadata file(s) created during contract compilation."
/> />
<ContractVerificationFieldContractIndex/>
</ContractVerificationMethod> </ContractVerificationMethod>
); );
}; };
......
...@@ -10,7 +10,7 @@ const FILE_TYPES = [ '.json' as const ]; ...@@ -10,7 +10,7 @@ const FILE_TYPES = [ '.json' as const ];
const ContractVerificationStandardInput = () => { const ContractVerificationStandardInput = () => {
return ( return (
<ContractVerificationMethod title="New Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Solidity (standard JSON input) ">
<ContractVerificationFieldName/> <ContractVerificationFieldName/>
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldSources <ContractVerificationFieldSources
......
...@@ -8,7 +8,7 @@ import ContractVerificationFieldName from '../fields/ContractVerificationFieldNa ...@@ -8,7 +8,7 @@ import ContractVerificationFieldName from '../fields/ContractVerificationFieldNa
const ContractVerificationVyperContract = () => { const ContractVerificationVyperContract = () => {
return ( return (
<ContractVerificationMethod title="New Vyper Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Vyper (contract)">
<ContractVerificationFieldName hint="Must match the name specified in the code."/> <ContractVerificationFieldName hint="Must match the name specified in the code."/>
<ContractVerificationFieldCompiler isVyper/> <ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldCode isVyper/> <ContractVerificationFieldCode isVyper/>
......
...@@ -9,7 +9,7 @@ const FILE_TYPES = [ '.vy' as const ]; ...@@ -9,7 +9,7 @@ const FILE_TYPES = [ '.vy' as const ];
const ContractVerificationVyperMultiPartFile = () => { const ContractVerificationVyperMultiPartFile = () => {
return ( return (
<ContractVerificationMethod title="New Vyper Smart Contract Verification"> <ContractVerificationMethod title="Contract verification via Vyper (multi-part files)">
<ContractVerificationFieldCompiler isVyper/> <ContractVerificationFieldCompiler isVyper/>
<ContractVerificationFieldEvmVersion isVyper/> <ContractVerificationFieldEvmVersion isVyper/>
<ContractVerificationFieldSources <ContractVerificationFieldSources
......
import type { SmartContractVerificationMethod } from 'types/api/contract';
import type { Option } from 'ui/shared/FancySelect/types'; import type { Option } from 'ui/shared/FancySelect/types';
export interface ContractLibrary { export interface ContractLibrary {
name: string; name: string;
address: string; address: string;
} }
interface MethodOption {
label: string;
value: SmartContractVerificationMethod;
}
export interface FormFieldsFlattenSourceCode { export interface FormFieldsFlattenSourceCode {
method: 'flattened-code'; method: MethodOption;
is_yul: boolean; is_yul: boolean;
name: string; name: string;
compiler: Option; compiler: Option;
...@@ -19,7 +26,7 @@ export interface FormFieldsFlattenSourceCode { ...@@ -19,7 +26,7 @@ export interface FormFieldsFlattenSourceCode {
} }
export interface FormFieldsStandardInput { export interface FormFieldsStandardInput {
method: 'standard-input'; method: MethodOption;
name: string; name: string;
compiler: Option; compiler: Option;
sources: Array<File>; sources: Array<File>;
...@@ -28,12 +35,13 @@ export interface FormFieldsStandardInput { ...@@ -28,12 +35,13 @@ export interface FormFieldsStandardInput {
} }
export interface FormFieldsSourcify { export interface FormFieldsSourcify {
method: 'sourcify'; method: MethodOption;
sources: Array<File>; sources: Array<File>;
contract_index?: Option;
} }
export interface FormFieldsMultiPartFile { export interface FormFieldsMultiPartFile {
method: 'multi-part'; method: MethodOption;
compiler: Option; compiler: Option;
evm_version: Option; evm_version: Option;
is_optimization_enabled: boolean; is_optimization_enabled: boolean;
...@@ -43,7 +51,7 @@ export interface FormFieldsMultiPartFile { ...@@ -43,7 +51,7 @@ export interface FormFieldsMultiPartFile {
} }
export interface FormFieldsVyperContract { export interface FormFieldsVyperContract {
method: 'vyper-code'; method: MethodOption;
name: string; name: string;
compiler: Option; compiler: Option;
code: string; code: string;
...@@ -51,7 +59,7 @@ export interface FormFieldsVyperContract { ...@@ -51,7 +59,7 @@ export interface FormFieldsVyperContract {
} }
export interface FormFieldsVyperMultiPartFile { export interface FormFieldsVyperMultiPartFile {
method: 'vyper-multi-part'; method: MethodOption;
compiler: Option; compiler: Option;
evm_version: Option; evm_version: Option;
sources: Array<File>; sources: Array<File>;
......
import type { FieldPath, ErrorOption } from 'react-hook-form'; import type { FieldPath, ErrorOption } from 'react-hook-form';
import type { ContractLibrary, FormFields } from './types'; import type {
ContractLibrary,
FormFields,
FormFieldsFlattenSourceCode,
FormFieldsMultiPartFile,
FormFieldsSourcify,
FormFieldsStandardInput,
FormFieldsVyperContract,
FormFieldsVyperMultiPartFile,
} from './types';
import type { SmartContractVerificationMethod, SmartContractVerificationError } from 'types/api/contract'; import type { SmartContractVerificationMethod, SmartContractVerificationError } from 'types/api/contract';
import type { Params as FetchParams } from 'lib/hooks/useFetch'; import type { Params as FetchParams } from 'lib/hooks/useFetch';
...@@ -14,6 +23,83 @@ export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMeth ...@@ -14,6 +23,83 @@ export const SUPPORTED_VERIFICATION_METHODS: Array<SmartContractVerificationMeth
'vyper-multi-part', 'vyper-multi-part',
]; ];
export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = {
'flattened-code': 'Solidity (Flattened source code)',
'standard-input': 'Solidity (Standard JSON input)',
sourcify: 'Solidity (Sourcify)',
'multi-part': 'Solidity (Multi-part files)',
'vyper-code': 'Vyper (Contract)',
'vyper-multi-part': 'Vyper (Multi-part files)',
};
export const DEFAULT_VALUES = {
'flattened-code': {
method: {
value: 'flattened-code' as const,
label: METHOD_LABELS['flattened-code'],
},
is_yul: false,
name: '',
compiler: null,
evm_version: null,
is_optimization_enabled: true,
optimization_runs: '200',
code: '',
autodetect_constructor_args: true,
constructor_args: '',
libraries: [],
},
'standard-input': {
method: {
value: 'standard-input' as const,
label: METHOD_LABELS['standard-input'],
},
name: '',
compiler: null,
sources: [],
autodetect_constructor_args: true,
constructor_args: '',
},
sourcify: {
method: {
value: 'sourcify' as const,
label: METHOD_LABELS.sourcify,
},
sources: [],
},
'multi-part': {
method: {
value: 'multi-part' as const,
label: METHOD_LABELS['multi-part'],
},
compiler: null,
evm_version: null,
is_optimization_enabled: true,
optimization_runs: 200,
sources: [],
libraries: [],
},
'vyper-code': {
method: {
value: 'vyper-code' as const,
label: METHOD_LABELS['vyper-code'],
},
name: '',
compiler: null,
code: '',
constructor_args: '',
},
'vyper-multi-part': {
method: {
value: 'vyper-multi-part' as const,
label: METHOD_LABELS['vyper-multi-part'],
},
compiler: null,
evm_version: null,
sources: [],
},
};
export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod { export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod {
return method && SUPPORTED_VERIFICATION_METHODS.includes(method as SmartContractVerificationMethod) ? true : false; return method && SUPPORTED_VERIFICATION_METHODS.includes(method as SmartContractVerificationMethod) ? true : false;
} }
...@@ -34,68 +120,79 @@ export function sortVerificationMethods(methodA: SmartContractVerificationMethod ...@@ -34,68 +120,79 @@ export function sortVerificationMethods(methodA: SmartContractVerificationMethod
} }
export function prepareRequestBody(data: FormFields): FetchParams['body'] { export function prepareRequestBody(data: FormFields): FetchParams['body'] {
switch (data.method) { switch (data.method.value) {
case 'flattened-code': { case 'flattened-code': {
const _data = data as FormFieldsFlattenSourceCode;
return { return {
compiler_version: data.compiler?.value, compiler_version: _data.compiler?.value,
source_code: data.code, source_code: _data.code,
is_optimization_enabled: data.is_optimization_enabled, is_optimization_enabled: _data.is_optimization_enabled,
is_yul_contract: data.is_yul, is_yul_contract: _data.is_yul,
optimization_runs: data.optimization_runs, optimization_runs: _data.optimization_runs,
contract_name: data.name, contract_name: _data.name,
libraries: reduceLibrariesArray(data.libraries), libraries: reduceLibrariesArray(_data.libraries),
evm_version: data.evm_version?.value, evm_version: _data.evm_version?.value,
autodetect_constructor_args: data.autodetect_constructor_args, autodetect_constructor_args: _data.autodetect_constructor_args,
constructor_args: data.constructor_args, constructor_args: _data.constructor_args,
}; };
} }
case 'standard-input': { case 'standard-input': {
const _data = data as FormFieldsStandardInput;
const body = new FormData(); const body = new FormData();
body.set('compiler_version', data.compiler?.value); body.set('compiler_version', _data.compiler?.value);
body.set('contract_name', data.name); body.set('contract_name', _data.name);
body.set('autodetect_constructor_args', String(Boolean(data.autodetect_constructor_args))); body.set('autodetect_constructor_args', String(Boolean(_data.autodetect_constructor_args)));
body.set('constructor_args', data.constructor_args); body.set('constructor_args', _data.constructor_args);
addFilesToFormData(body, data.sources); addFilesToFormData(body, _data.sources);
return body; return body;
} }
case 'sourcify': { case 'sourcify': {
const _data = data as FormFieldsSourcify;
const body = new FormData(); const body = new FormData();
addFilesToFormData(body, data.sources); addFilesToFormData(body, _data.sources);
_data.contract_index && body.set('chosen_contract_index', _data.contract_index.value);
return body; return body;
} }
case 'multi-part': { case 'multi-part': {
const _data = data as FormFieldsMultiPartFile;
const body = new FormData(); const body = new FormData();
body.set('compiler_version', data.compiler?.value); body.set('compiler_version', _data.compiler?.value);
body.set('evm_version', data.evm_version?.value); body.set('evm_version', _data.evm_version?.value);
body.set('is_optimization_enabled', String(Boolean(data.is_optimization_enabled))); body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled)));
data.is_optimization_enabled && body.set('optimization_runs', data.optimization_runs); _data.is_optimization_enabled && body.set('optimization_runs', _data.optimization_runs);
const libraries = reduceLibrariesArray(data.libraries); const libraries = reduceLibrariesArray(_data.libraries);
libraries && body.set('libraries', JSON.stringify(libraries)); libraries && body.set('libraries', JSON.stringify(libraries));
addFilesToFormData(body, data.sources); addFilesToFormData(body, _data.sources);
return body; return body;
} }
case 'vyper-code': { case 'vyper-code': {
const _data = data as FormFieldsVyperContract;
return { return {
compiler_version: data.compiler?.value, compiler_version: _data.compiler?.value,
source_code: data.code, source_code: _data.code,
contract_name: data.name, contract_name: _data.name,
constructor_args: data.constructor_args, constructor_args: _data.constructor_args,
}; };
} }
case 'vyper-multi-part': { case 'vyper-multi-part': {
const _data = data as FormFieldsVyperMultiPartFile;
const body = new FormData(); const body = new FormData();
body.set('compiler_version', data.compiler?.value); body.set('compiler_version', _data.compiler?.value);
body.set('evm_version', data.evm_version?.value); body.set('evm_version', _data.evm_version?.value);
addFilesToFormData(body, data.sources); addFilesToFormData(body, _data.sources);
return body; return body;
} }
......
import { Box } from '@chakra-ui/react'; import { chakra, Center, useColorModeValue } from '@chakra-ui/react';
import type { DragEvent } from 'react'; import type { DragEvent } from 'react';
import React from 'react'; import React from 'react';
import { getAllFileEntries, convertFileEntryToFile } from './utils/files'; import { getAllFileEntries, convertFileEntryToFile } from './utils/files';
interface Props { interface Props {
children: React.ReactNode;
onDrop: (files: Array<File>) => void; onDrop: (files: Array<File>) => void;
className?: string;
isDisabled?: boolean;
} }
const DragAndDropArea = ({ onDrop }: Props) => { const DragAndDropArea = ({ onDrop, children, className, isDisabled }: Props) => {
const [ isDragOver, setIsDragOver ] = React.useState(false); const [ isDragOver, setIsDragOver ] = React.useState(false);
const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => { const handleDrop = React.useCallback(async(event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
if (isDisabled) {
return;
}
const fileEntries = await getAllFileEntries(event.dataTransfer.items); const fileEntries = await getAllFileEntries(event.dataTransfer.items);
const files = await Promise.all(fileEntries.map(convertFileEntryToFile)); const files = await Promise.all(fileEntries.map(convertFileEntryToFile));
onDrop(files); onDrop(files);
setIsDragOver(false); setIsDragOver(false);
}, [ onDrop ]); }, [ isDisabled, onDrop ]);
const handleDragOver = React.useCallback((event: DragEvent<HTMLDivElement>) => { const handleDragOver = React.useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
...@@ -35,18 +42,39 @@ const DragAndDropArea = ({ onDrop }: Props) => { ...@@ -35,18 +42,39 @@ const DragAndDropArea = ({ onDrop }: Props) => {
setIsDragOver(false); setIsDragOver(false);
}, []); }, []);
const handleClick = React.useCallback((event: React.MouseEvent) => {
if (isDisabled) {
event.preventDefault();
event.stopPropagation();
}
}, [ isDisabled ]);
const disabledBorderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const borderColor = isDragOver ? 'link_hovered' : 'link';
return ( return (
<Box <Center
className={ className }
w="100%" w="100%"
h="200px" minH="120px"
bgColor="lightpink" borderWidth="2px"
opacity={ isDragOver ? 0.8 : 1 } borderColor={ isDisabled ? disabledBorderColor : borderColor }
_hover={{
borderColor: isDisabled ? disabledBorderColor : 'link_hovered',
}}
borderRadius="base"
borderStyle="dashed"
cursor="pointer"
textAlign="center"
onClick={ handleClick }
onDrop={ handleDrop } onDrop={ handleDrop }
onDragOver={ handleDragOver } onDragOver={ handleDragOver }
onDragEnter={ handleDragEnter } onDragEnter={ handleDragEnter }
onDragLeave={ handleDragLeave } onDragLeave={ handleDragLeave }
/> >
{ children }
</Center>
); );
}; };
export default React.memo(DragAndDropArea); export default React.memo(chakra(DragAndDropArea));
...@@ -7,7 +7,7 @@ interface Props { ...@@ -7,7 +7,7 @@ interface Props {
} }
const FieldError = ({ message, className }: Props) => { const FieldError = ({ message, className }: Props) => {
return <Box className={ className } color="error" fontSize="sm" mt={ 2 } wordBreak="break-all">{ message }</Box>; return <Box className={ className } color="error" fontSize="sm" mt={ 2 } wordBreak="break-word">{ message }</Box>;
}; };
export default chakra(FieldError); export default chakra(FieldError);
import { Box, Flex, Icon, Text, useColorModeValue, IconButton, chakra } from '@chakra-ui/react'; import { Box, Flex, Icon, Text, useColorModeValue, IconButton, chakra, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import CrossIcon from 'icons/cross.svg'; import CrossIcon from 'icons/cross.svg';
import imageIcon from 'icons/files/image.svg';
import jsonIcon from 'icons/files/json.svg'; import jsonIcon from 'icons/files/json.svg';
import placeholderIcon from 'icons/files/placeholder.svg';
import solIcon from 'icons/files/sol.svg'; import solIcon from 'icons/files/sol.svg';
import yulIcon from 'icons/files/yul.svg'; import yulIcon from 'icons/files/yul.svg';
import infoIcon from 'icons/info.svg';
import { shortenNumberWithLetter } from 'lib/formatters'; import { shortenNumberWithLetter } from 'lib/formatters';
const FILE_ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = { const FILE_ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
...@@ -29,31 +30,64 @@ interface Props { ...@@ -29,31 +30,64 @@ interface Props {
index?: number; index?: number;
onRemove?: (index?: number) => void; onRemove?: (index?: number) => void;
isDisabled?: boolean; isDisabled?: boolean;
error?: string;
} }
const FileSnippet = ({ file, className, index, onRemove, isDisabled }: Props) => { const FileSnippet = ({ file, className, index, onRemove, isDisabled, error }: Props) => {
const handleRemove = React.useCallback(() => { const handleRemove = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
onRemove?.(index); onRemove?.(index);
}, [ index, onRemove ]); }, [ index, onRemove ]);
const handleErrorHintIconClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
}, []);
const fileExtension = getFileExtension(file.name); const fileExtension = getFileExtension(file.name);
const fileIcon = FILE_ICONS[fileExtension] || imageIcon; const fileIcon = FILE_ICONS[fileExtension] || placeholderIcon;
const iconColor = useColorModeValue('gray.600', 'gray.400');
return ( return (
<Flex <Flex
p={ 3 } maxW="300px"
overflow="hidden"
className={ className }
alignItems="center"
textAlign="left"
>
<Icon
as={ fileIcon }
boxSize="74px"
color={ error ? 'error' : iconColor }
mr={ 2 }
borderWidth="2px" borderWidth="2px"
borderRadius="md" borderRadius="md"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
maxW="300px" p={ 3 }
/>
<Box maxW="calc(100% - 58px - 24px)">
<Flex alignItems="center">
<Text
fontWeight={ 600 }
overflow="hidden" overflow="hidden"
className={ className } textOverflow="ellipsis"
whiteSpace="nowrap"
color={ error ? 'error' : 'initial' }
> >
<Icon as={ fileIcon } boxSize="50px" color={ useColorModeValue('gray.600', 'gray.400') } mr={ 2 }/> { file.name }
<Box width="calc(100% - 58px - 24px)" > </Text>
<Text fontWeight={ 600 } overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ file.name }</Text> { Boolean(error) && (
<Text variant="secondary" mt={ 1 }>{ shortenNumberWithLetter(file.size) }B</Text> <Tooltip
label={ error }
placement="top"
maxW="320px"
>
<Box cursor="pointer" display="inherit" onClick={ handleErrorHintIconClick } ml={ 1 }>
<Icon as={ infoIcon } boxSize={ 5 } color="error"/>
</Box> </Box>
</Tooltip>
) }
<IconButton <IconButton
aria-label="remove" aria-label="remove"
icon={ <CrossIcon/> } icon={ <CrossIcon/> }
...@@ -64,8 +98,12 @@ const FileSnippet = ({ file, className, index, onRemove, isDisabled }: Props) => ...@@ -64,8 +98,12 @@ const FileSnippet = ({ file, className, index, onRemove, isDisabled }: Props) =>
ml="auto" ml="auto"
onClick={ handleRemove } onClick={ handleRemove }
isDisabled={ isDisabled } isDisabled={ isDisabled }
alignSelf="flex-start"
/> />
</Flex> </Flex>
<Text variant="secondary" mt={ 1 }>{ shortenNumberWithLetter(file.size) }B</Text>
</Box>
</Flex>
); );
}; };
......
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