Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,17 @@ import IconGitHub from '@/components/__common/icon/GitHub.vue';
--font-size-small: 14px;
--font-size-medium: 16px;
--font-size-big: 20px;
--font-sans: 'Inter Variable', -apple-system, 'BlinkMacSystemFont',
avenir next, avenir, segoe ui, helvetica neue, helvetica, 'Ubuntu', roboto,
noto, arial, sans-serif;
--font-serif: 'Iowan Old Style', 'Apple Garamond', 'Baskerville',
'Times New Roman', 'Droid Serif', 'Times', 'Source Serif Pro', serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--font-mono: 'Inconsolata Variable', 'Menlo', 'Consolas', 'Monaco',
'Liberation Mono', 'Lucida Console', monospace;
--font-sans:
'Inter Variable', -apple-system, 'BlinkMacSystemFont', avenir next, avenir,
segoe ui, helvetica neue, helvetica, 'Ubuntu', roboto, noto, arial,
sans-serif;
--font-serif:
'Iowan Old Style', 'Apple Garamond', 'Baskerville', 'Times New Roman',
'Droid Serif', 'Times', 'Source Serif Pro', serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol';
--font-mono:
'Inconsolata Variable', 'Menlo', 'Consolas', 'Monaco', 'Liberation Mono',
'Lucida Console', monospace;
}

.app {
Expand Down
55 changes: 52 additions & 3 deletions app/components/contract/ChainItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,20 @@
</a>
</div>
</BlockStatus>
<ScanButton
v-if="verification !== null && verification === 'unverified'"
:disabled="verifying"
class="verify-button"
@click="verify"
>
{{ verifying ? 'Verifying...' : 'Verify' }}
</ScanButton>
</div>
</template>

<script setup lang="ts">
import type { Address } from 'viem';
import { computed } from 'vue';
import { computed, ref } from 'vue';

import BlockStatus, { type Status } from './BlockStatus.vue';

Expand All @@ -46,16 +54,53 @@ import {
getChainName,
getAddressExplorerUrl,
} from '@/utils/chains';
import type { VerificationStatus } from '@/utils/verification';
import { verifyContract, type VerificationStatus } from '@/utils/verification';

const emit = defineEmits<{
(e: 'verificationUpdate', status: VerificationStatus): void;
(e: 'log', message: string): void;
}>();

const { address, chain, verification } = defineProps<{
const { address, chain, verification, verifiedChains } = defineProps<{
address: Address;
chain: Chain;
status: Status;
verification: VerificationStatus | null;
verifiedChains: Chain[];
}>();

const explorerUrl = computed(() => getAddressExplorerUrl(chain, address));
const verifying = ref(false);

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function verify(): Promise<void> {
verifying.value = true;
let attempts = 0;
for (const source of verifiedChains) {
emit('log', `Trying ${getChainName(source)} -> ${getChainName(chain)}`);
const { success, error } = await verifyContract(address, chain, source);
if (success) {
emit('verificationUpdate', 'verified');
emit(
'log',
`Verified on ${getChainName(chain)} using ${getChainName(source)}`,
);
verifying.value = false;
return;
}
if (attempts < 3 && error) {
emit('log', `Error: ${error}`);
}
attempts += 1;
await sleep(300);
}
emit('verificationUpdate', 'unverified');
emit('log', `Failed to verify on ${getChainName(chain)}`);
verifying.value = false;
}
</script>

<style scoped>
Expand Down Expand Up @@ -89,4 +134,8 @@ a {
position: absolute;
left: -20px;
}

.verify-button {
margin-left: 8px;
}
</style>
19 changes: 18 additions & 1 deletion app/components/contract/ChainList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
:status="chain.status"
:address="address"
:verification="chain.verification"
:verifiedChains="verifiedChains"
@verificationUpdate="update(chain.id, $event)"
@log="log"
/>
</div>
</template>
Expand All @@ -21,13 +24,19 @@ import ChainItem from './ChainItem.vue';
import type { Chain } from '@/utils/chains';
import type { VerificationStatus } from '@/utils/verification';

const { chains } = defineProps<{
const emit = defineEmits<{
(e: 'updateVerification', chain: Chain, status: VerificationStatus): void;
(e: 'log', message: string): void;
}>();

const { chains, verifiedChains } = defineProps<{
address: Address;
chains: {
id: Chain;
status: Status;
verification: VerificationStatus | null;
}[];
verifiedChains: Chain[];
}>();

const sortedChains = computed(() => {
Expand All @@ -51,6 +60,14 @@ const sortedChains = computed(() => {
return statusToPriority(a.status) - statusToPriority(b.status);
});
});

function update(chain: Chain, status: VerificationStatus): void {
emit('updateVerification', chain, status);
}

function log(message: string): void {
emit('log', message);
}
</script>

<style scoped>
Expand Down
1 change: 0 additions & 1 deletion app/data/cache.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

{
"0x1f98431c8ad98523631ae4a59f267346ea31f984": {
"1": "0x4d7b8525cd5d14343fa67a732fba5b24cddba11620ca88392f4ec6c52f91fd69",
Expand Down
35 changes: 35 additions & 0 deletions app/pages/contract/[address].vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,22 @@
<ChainList
:address
:chains
:verifiedChains="verifiedChains"
@updateVerification="updateVerification"
@log="addLog"
/>
<div
v-if="logs.length"
class="logs"
>
<div
v-for="(log, i) in logs"
:key="i"
class="log"
>
{{ log }}
</div>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -84,6 +99,10 @@ const verificationStatus = ref<Record<number, VerificationStatus | null>>({});
const checkedVerifications = computed(
() => Object.keys(verificationStatus.value).length,
);
const verifiedChains = computed(() =>
CHAINS.filter((chain) => verificationStatus.value[chain] === 'verified'),
);
const logs = ref<string[]>([]);
async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Expand All @@ -103,6 +122,14 @@ async function checkVerification(): Promise<void> {
checkingVerification.value = false;
}

function updateVerification(chain: Chain, status: VerificationStatus): void {
verificationStatus.value[chain] = status;
}

function addLog(message: string): void {
logs.value.push(message);
}

async function getCodeHash(chain: Chain): Promise<Hex | null | undefined> {
const cachedCodeHash =
(cache as Record<Address, Partial<Record<Chain, Hex>>>)[address.value]?.[
Expand Down Expand Up @@ -278,4 +305,12 @@ h1 {
color: var(--color-text-secondary);
font-size: var(--font-size-small);
}

.logs {
display: flex;
flex-direction: column;
gap: 4px;
font-size: var(--font-size-small);
margin-top: 16px;
}
</style>
32 changes: 31 additions & 1 deletion app/utils/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ interface CheckVerificationResponse {

type VerificationStatus = 'verified' | 'unverified' | 'unknown';

interface VerifyContractResponse {
status: 'ok' | 'error';
error?: string;
}

async function checkContractVerification(
address: Address,
chain: Chain,
Expand All @@ -28,5 +33,30 @@ async function checkContractVerification(
return checkResponse.status;
}

export { checkContractVerification };
async function verifyContract(
address: Address,
chain: Chain,
sourceChain: Chain,
): Promise<{ success: boolean; error?: string }> {
try {
const verifyResponse = await ky
.post('/api/verify', {
json: {
chain: chain.toString(),
address,
sourceChain: sourceChain.toString(),
},
})
.json<VerifyContractResponse>();

return {
success: verifyResponse.status === 'ok',
error: verifyResponse.error,
};
} catch (err: unknown) {
return { success: false, error: (err as Error).message };
}
}

export { checkContractVerification, verifyContract };
export type { VerificationStatus };
111 changes: 111 additions & 0 deletions server/api/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { defineEventHandler, readBody } from 'h3';
import ky from 'ky';
import type { Address } from 'viem';

interface VerifyRequestBody {
chain: string;
address: Address;
sourceChain: string;
}

interface GetSourceCodeResponse {
status: '0' | '1';
message: 'OK' | 'NOTOK';
result: {
SourceCode: string;
ABI: string;
ContractName: string;
CompilerVersion: string;
OptimizationUsed: string;
Runs: string;
ConstructorArguments: string;
EVMVersion: string;
Library: string;
LicenseType: string;
Proxy: string;
Implementation: string;
SwarmSource: string;
}[];
}

interface VerifySourceCodeResponse {
status: '0' | '1';
message: string;
result: string;
}

interface VerifyResponse {
status: 'ok' | 'error';
error?: string;
}

export default defineEventHandler(async (event): Promise<VerifyResponse> => {
const etherscanApiKey = process.env.ETHERSCAN_API_KEY;
if (!etherscanApiKey) {
return { status: 'error', error: 'Missing ETHERSCAN_API_KEY' };
}

const { chain, address, sourceChain } =
(await readBody<VerifyRequestBody>(event)) || {};

if (!chain || !address || !sourceChain) {
return { status: 'error', error: 'Missing parameters' };
}

try {
const source = await ky
.get('https://api.etherscan.io/v2/api', {
searchParams: {
chainid: sourceChain,
module: 'contract',
action: 'getsourcecode',
address,
apikey: etherscanApiKey,
},
timeout: 5_000,
})
.json<GetSourceCodeResponse>();

if (source.status !== '1' || source.message !== 'OK') {
return {
status: 'error',
error: Array.isArray(source.result)
? JSON.stringify(source.result)
: String(source.result || source.message),
};
}

const data = source.result[0];
if (!data || !data.SourceCode) {
return { status: 'error', error: 'Empty source code' };
}

const verify = await ky
.post('https://api.etherscan.io/v2/api', {
json: {
chainid: chain,
module: 'contract',
action: 'verifysourcecode',
apikey: etherscanApiKey,
contractaddress: address,
sourceCode: data.SourceCode,
contractname: data.ContractName,
compilerversion: data.CompilerVersion,
optimizationUsed: data.OptimizationUsed,
runs: data.Runs,
constructorArguements: data.ConstructorArguments,
evmVersion: data.EVMVersion,
licenseType: data.LicenseType,
},
timeout: 20_000,
})
.json<VerifySourceCodeResponse>();

if (verify.status === '1') {
return { status: 'ok' };
}
return { status: 'error', error: verify.result || verify.message };
} catch (err: unknown) {
return { status: 'error', error: (err as Error).message };
}
});
Loading