diff --git a/src/cli/verify.ts b/src/cli/verify.ts index 8a1b74c..ed78266 100644 --- a/src/cli/verify.ts +++ b/src/cli/verify.ts @@ -5,6 +5,7 @@ import { Args, Runner, RunnerContext } from './Runner'; import path from 'path'; import { argSpec, createNetworkProvider } from '../network/createNetworkProvider'; import { selectCompile } from './build'; +import { sleep } from '../utils'; import arg from 'arg'; type FuncCompilerSettings = { @@ -97,6 +98,66 @@ class VerifierRegistry implements Contract { } } +async function lookupCodeHash(hash: Buffer, ui: UIProvider, retryCount: number = 5): Promise { + type QueryResponse = { + data: { + account_states: Array<{ + address: string; + workchain: number; + }>; + }; + }; + + let queryResponse: QueryResponse; + let foundAddr: string | undefined; + let done = false; + const graphqlUrl = 'https://dton.io/graphql/'; + const query = `{ + account_states(page:0, page_size:1, account_state_state_init_code_hash: "${hash.toString('hex').toUpperCase()}") + { + address + workchain + } + }`; + + do { + try { + ui.write('Checking if such a contract is already deployed...'); + const resp = await fetch(graphqlUrl, { + method: 'POST', + body: JSON.stringify({ query }), + headers: { 'Content-Type': 'application/json' }, + }); + if (resp.ok) { + queryResponse = await resp.json(); + const states = queryResponse.data.account_states; + if (states.length > 0) { + const state = states[0]; + foundAddr = Address.parseRaw(`${state.workchain}:${state.address}`).toString(); + } else { + ui.write('No such contract was found!'); + } + done = true; + } else { + retryCount--; + } + // Meh + } catch (e: any) { + retryCount--; + if (e.cause) { + if (e.cause.code == 'ETIMEDOUT') { + ui.write('API timed out, waiting...'); + await sleep(5000); + } + } else { + ui.write(e); + } + } + } while (!done && retryCount > 0); + + return foundAddr; +} + export const verify: Runner = async (args: Args, ui: UIProvider, context: RunnerContext) => { const localArgs = arg(argSpec); @@ -116,9 +177,28 @@ export const verify: Runner = async (args: Args, ui: UIProvider, context: Runner throw new Error('Cannot use custom network'); } - const addr = await ui.input('Deployed contract address'); - const result = await doCompile(sel.name); + const resHash = result.code.hash(); + + ui.write(`Compiled code hash hex:${resHash.toString('hex')}`); + ui.write('We can look up the address with such code hash in the blockchain automatically'); + + const passManually = await ui.prompt('Do you want to specify the address manually?'); + let addr: string; + + if (passManually) { + addr = (await ui.inputAddress('Deployed contract address')).toString(); + } else { + const alreadyDeployed = await lookupCodeHash(resHash, ui); + if (alreadyDeployed) { + ui.write(`Contract is already deployed at: ${alreadyDeployed}\nUsing that address.`); + ui.write(`https://tonscan.org/address/${alreadyDeployed}`); + addr = alreadyDeployed; + } else { + ui.write("Please enter the contract's address manually"); + addr = (await ui.inputAddress('Deployed contract address')).toString(); + } + } let src: SourcesObject; const fd = new FormData(); @@ -211,28 +291,31 @@ export const verify: Runner = async (args: Args, ui: UIProvider, context: Runner }); if (sourceResponse.status !== 200) { - throw new Error('Could not compile on backend:\n' + (await sourceResponse.text())); + throw new Error('Could not compile on backend:\n' + (await sourceResponse.json())); } const sourceResult = await sourceResponse.json(); if (sourceResult.compileResult.result !== 'similar') { - throw new Error('The code is not similar'); + throw new Error(sourceResult.compileResult.error); } let msgCell = sourceResult.msgCell; let acquiredSigs = 1; while (acquiredSigs < verifier.quorum) { - const signResponse = await fetch(removeRandom(remainingBackends) + '/sign', { + const curBackend = removeRandom(remainingBackends); + ui.write(`Using backend: ${curBackend}`); + const signResponse = await fetch(curBackend + '/sign', { method: 'POST', body: JSON.stringify({ messageCell: msgCell, }), + headers: { 'Content-Type': 'application/json' }, }); if (signResponse.status !== 200) { - throw new Error('Could not compile on backend:\n' + (await signResponse.text())); + throw new Error('Could not sign on backend:\n' + (await signResponse.text())); } const signResult = await signResponse.json(); @@ -248,4 +331,6 @@ export const verify: Runner = async (args: Args, ui: UIProvider, context: Runner value: toNano('0.5'), body: c, }); + + ui.write(`Contract successfully verified at https://verifier.ton.org/${addr}`); }; diff --git a/src/ui/InquirerUIProvider.ts b/src/ui/InquirerUIProvider.ts index bc0f085..3b47163 100644 --- a/src/ui/InquirerUIProvider.ts +++ b/src/ui/InquirerUIProvider.ts @@ -1,5 +1,6 @@ import inquirer from 'inquirer'; import { UIProvider } from '../ui/UIProvider'; +import { Address } from '@ton/core'; class DestroyableBottomBar extends inquirer.ui.BottomBar { destroy() { @@ -27,6 +28,18 @@ export class InquirerUIProvider implements UIProvider { return prompt; } + async inputAddress(message: string, fallback?: Address) { + const prompt = message + (fallback === undefined ? '' : ` (default: ${fallback})`); + while (true) { + const addr = (await this.input(prompt)).trim(); + try { + return addr === '' && fallback !== undefined ? fallback : Address.parse(addr); + } catch (e) { + this.write(addr + ' is not valid!\n'); + } + } + } + async input(message: string): Promise { const { val } = await inquirer.prompt({ name: 'val', diff --git a/src/ui/UIProvider.ts b/src/ui/UIProvider.ts index 98a7dc4..894cde5 100644 --- a/src/ui/UIProvider.ts +++ b/src/ui/UIProvider.ts @@ -1,6 +1,9 @@ +import { Address } from '@ton/core'; + export interface UIProvider { write(message: string): void; prompt(message: string): Promise; + inputAddress(message: string, fallback?: Address): Promise
; input(message: string): Promise; choose(message: string, choices: T[], display: (v: T) => string): Promise; setActionPrompt(message: string): void;