-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UI for smart contract deployment #44
Changes from all commits
6b0d6c2
03379a0
f2d98b6
1f2ccce
8c63a0e
e82dee2
d347b58
b705359
6a3bf7b
04701b4
4626dd8
136bb17
de5efc2
dba2ccd
64c2e65
9e3a9bf
63b8117
944a74b
6c64e44
560069e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import * as crypto from 'crypto'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import * as vscode from 'vscode'; | ||
|
||
class Contract { | ||
public readonly hash?: string; | ||
public readonly name?: string; | ||
public readonly path: string; | ||
public readonly avmHex?: string; | ||
constructor(fullpath: string) { | ||
this.path = fullpath; | ||
try { | ||
this.name = path.basename(fullpath).replace(/\.avm$/, ''); | ||
const avmContents = fs.readFileSync(fullpath); | ||
this.avmHex = avmContents.toString('hex'); | ||
const sha256Bytes = crypto.createHash('sha256').update(avmContents).digest(); | ||
const ripemd160Bytes = crypto.createHash('ripemd160').update(sha256Bytes).digest(); | ||
this.hash = '0x' + Buffer.from(ripemd160Bytes.reverse()).toString('hex'); | ||
} catch (e) { | ||
console.error('Error parsing', path, e); | ||
} | ||
} | ||
} | ||
|
||
export class ContractDetector { | ||
|
||
private readonly fileSystemWatcher: vscode.FileSystemWatcher; | ||
private readonly searchPattern: vscode.GlobPattern = '**/*.avm'; | ||
|
||
public contracts: Contract[]; | ||
|
||
constructor() { | ||
this.contracts = []; | ||
this.refresh(); | ||
this.fileSystemWatcher = vscode.workspace.createFileSystemWatcher(this.searchPattern); | ||
this.fileSystemWatcher.onDidChange(this.refresh, this); | ||
this.fileSystemWatcher.onDidCreate(this.refresh, this); | ||
this.fileSystemWatcher.onDidDelete(this.refresh, this); | ||
} | ||
|
||
public dispose() { | ||
if (this.fileSystemWatcher) { | ||
this.fileSystemWatcher.dispose(); | ||
} | ||
} | ||
|
||
public async refresh() { | ||
const files = await vscode.workspace.findFiles(this.searchPattern); | ||
this.contracts = files | ||
.map(uri => new Contract(uri.fsPath)) | ||
.filter(contract => !!contract.hash); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
import * as fs from 'fs'; | ||
import * as neon from '@cityofzion/neon-js'; | ||
import * as path from 'path'; | ||
import * as vscode from 'vscode'; | ||
|
||
import { deployEvents } from './panels/deployEvents'; | ||
|
||
import { ContractDetector } from './contractDetector'; | ||
import { NeoExpressConfig } from './neoExpressConfig'; | ||
import { WalletExplorer } from './walletExplorer'; | ||
|
||
const JavascriptHrefPlaceholder : string = '[JAVASCRIPT_HREF]'; | ||
const CssHrefPlaceholder : string = '[CSS_HREF]'; | ||
|
||
class ViewState { | ||
contracts: any[] = []; | ||
contractPath?: string = undefined; | ||
contractName?: string = undefined; | ||
contractHash?: string = undefined; | ||
contractAvmHex?: string = undefined; | ||
isValid: boolean = false; | ||
result: string = ''; | ||
showError: boolean = false; | ||
showSuccess: boolean = false; | ||
walletAddress?: string = undefined; | ||
wallets: any[] = []; | ||
} | ||
|
||
export class DeployPanel { | ||
|
||
private readonly contractDetector: ContractDetector; | ||
private readonly neoExpressConfig?: NeoExpressConfig; | ||
private readonly panel: vscode.WebviewPanel; | ||
private readonly rpcUri: string; | ||
private readonly walletExplorer: WalletExplorer; | ||
|
||
private viewState: ViewState; | ||
private initialized: boolean = false; | ||
|
||
constructor( | ||
extensionPath: string, | ||
rpcUri: string, | ||
walletExplorer: WalletExplorer, | ||
contractDetector: ContractDetector, | ||
disposables: vscode.Disposable[], | ||
neoExpressConfig?: NeoExpressConfig) { | ||
|
||
this.contractDetector = contractDetector; | ||
this.rpcUri = rpcUri; | ||
this.walletExplorer = walletExplorer; | ||
this.neoExpressConfig = neoExpressConfig; | ||
this.viewState = new ViewState(); | ||
|
||
this.panel = vscode.window.createWebviewPanel( | ||
'deployPanel', | ||
(this.neoExpressConfig ? this.neoExpressConfig.basename : this.rpcUri) + ' - Deploy contract', | ||
vscode.ViewColumn.Active, | ||
{ enableScripts: true }); | ||
this.panel.iconPath = vscode.Uri.file(path.join(extensionPath, 'resources', 'neo.svg')); | ||
this.panel.webview.onDidReceiveMessage(this.onMessage, this, disposables); | ||
|
||
const htmlFileContents = fs.readFileSync( | ||
path.join(extensionPath, 'src', 'panels', 'deploy.html'), { encoding: 'utf8' }); | ||
const javascriptHref : string = this.panel.webview.asWebviewUri( | ||
vscode.Uri.file(path.join(extensionPath, 'out', 'panels', 'bundles', 'deploy.main.js'))) + ''; | ||
const cssHref : string = this.panel.webview.asWebviewUri( | ||
vscode.Uri.file(path.join(extensionPath, 'out', 'panels', 'deploy.css'))) + ''; | ||
this.panel.webview.html = htmlFileContents | ||
.replace(JavascriptHrefPlaceholder, javascriptHref) | ||
.replace(CssHrefPlaceholder, cssHref); | ||
} | ||
|
||
public dispose() { | ||
this.panel.dispose(); | ||
} | ||
|
||
private async doDeploy() { | ||
try { | ||
// | ||
// Construct a script that calls Neo.Contract.Create | ||
// -- see: https://docs.neo.org/docs/en-us/reference/scapi/fw/dotnet/neo/Contract/Create.html | ||
// | ||
// TODO: Find the contracts .abi.json and correctly determine parameters and return types. For now | ||
// we assume that all contracts take two parameters (a string and an array) and return a byte | ||
// array. | ||
// TODO: Allow storage usage to be specified in the UI. For now we assume storage is used. | ||
// TODO: Allow specification of description, email, etc. in the UI. For now we use blank values. | ||
// | ||
const sb = neon.default.create.scriptBuilder(); | ||
const script = sb | ||
.emitPush(neon.default.u.str2hexstring('')) // description | ||
.emitPush(neon.default.u.str2hexstring('')) // email | ||
.emitPush(neon.default.u.str2hexstring('')) // author | ||
.emitPush(neon.default.u.str2hexstring('')) // code_version | ||
.emitPush(neon.default.u.str2hexstring('')) // name | ||
.emitPush(0x01) // storage: {none: 0x00, storage: 0x01, dynamic: 0x02, storage+dynamic:0x03} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. storage and dynamic is part of the aformentioned contract metadata. If the metadata property is missing, prompt the user instead of assuming contracts use storage but arent dynamic |
||
.emitPush('05') // return type - see https://docs.neo.org/docs/en-us/sc/deploy/Parameter.html | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This information is in the abi.json file. If the abi.json file is missing, fail deployment rather than assuming return/parameter types |
||
.emitPush('0710') // parameter list - see https://docs.neo.org/docs/en-us/sc/deploy/Parameter.html | ||
.emitPush(this.viewState.contractAvmHex) | ||
.emitSysCall('Neo.Contract.Create') | ||
.str; | ||
|
||
// Determine required GAS: | ||
const rpcClient = new neon.rpc.RPCClient(this.rpcUri); | ||
const invokeResult = await rpcClient.invokeScript(script); | ||
const gas = parseFloat(invokeResult.gas_consumed); | ||
|
||
const walletConfig = this.viewState.wallets.filter(_ => _.address === this.viewState.walletAddress)[0]; | ||
if (await walletConfig.unlock()) { | ||
const api = new neon.api.neoCli.instance(this.rpcUri); | ||
const config = { | ||
api: api, | ||
script: script, | ||
account: walletConfig.account, | ||
signingFunction: walletConfig.signingFunction, | ||
gas: gas, | ||
}; | ||
const result = await neon.default.doInvoke(config); | ||
if (result.response && result.response.txid) { | ||
this.viewState.result = result.response.txid; | ||
this.viewState.showError = false; | ||
this.viewState.showSuccess = true; | ||
} else { | ||
this.viewState.result = 'No response from RPC server; contract may not have deployed'; | ||
this.viewState.showError = true; | ||
this.viewState.showSuccess = false; | ||
} | ||
} | ||
} catch (e) { | ||
this.viewState.result = 'Error deploying contract: ' + e; | ||
this.viewState.showError = true; | ||
this.viewState.showSuccess = false; | ||
} | ||
} | ||
|
||
private async onMessage(message: any) { | ||
if (message.e === deployEvents.Init) { | ||
await this.refresh(false); | ||
await this.panel.webview.postMessage({ viewState: this.viewState }); | ||
} else if (message.e === deployEvents.Refresh) { | ||
await this.refresh(true); | ||
await this.panel.webview.postMessage({ viewState: this.viewState }); | ||
} else if (message.e === deployEvents.Update) { | ||
this.viewState = message.c; | ||
await this.refresh(true); | ||
await this.panel.webview.postMessage({ viewState: this.viewState }); | ||
} else if (message.e === deployEvents.Deploy) { | ||
await this.refresh(true); | ||
if (this.viewState.isValid) { | ||
await this.doDeploy(); | ||
} | ||
await this.panel.webview.postMessage({ viewState: this.viewState }); | ||
} else if (message.e === deployEvents.Close) { | ||
this.dispose(); | ||
} else if (message.e === deployEvents.NewWallet) { | ||
this.initialized = false; // cause wallet list to be refreshed when this panel is next initialized | ||
vscode.commands.executeCommand('neo-visual-devtracker.createWalletFile'); | ||
} | ||
} | ||
|
||
private async refresh(force: boolean) { | ||
if (!force && this.initialized) { | ||
return; | ||
} | ||
|
||
this.viewState.showError = false; | ||
this.viewState.showSuccess = false; | ||
|
||
this.viewState.wallets = []; | ||
if (this.neoExpressConfig) { | ||
this.neoExpressConfig.refresh(); | ||
this.viewState.wallets = this.neoExpressConfig.wallets.slice(); | ||
} | ||
|
||
for (let i = 0; i < this.walletExplorer.allAccounts.length; i++) { | ||
this.viewState.wallets.push(this.walletExplorer.allAccounts[i]); | ||
} | ||
|
||
const walletConfig = this.viewState.wallets.filter(_ => _.address === this.viewState.walletAddress)[0]; | ||
if (!walletConfig) { | ||
this.viewState.walletAddress = undefined; | ||
} | ||
|
||
await this.contractDetector.refresh(); | ||
this.viewState.contracts = this.contractDetector.contracts; | ||
|
||
const contractConfig = this.viewState.contracts.filter(_ => _.path === this.viewState.contractPath)[0]; | ||
if (!contractConfig) { | ||
this.viewState.contractPath = undefined; | ||
this.viewState.contractName = undefined; | ||
this.viewState.contractHash = undefined; | ||
this.viewState.contractAvmHex = undefined; | ||
} else { | ||
this.viewState.contractName = contractConfig.name; | ||
this.viewState.contractHash = contractConfig.hash; | ||
this.viewState.contractAvmHex = contractConfig.avmHex; | ||
} | ||
|
||
this.viewState.isValid = | ||
!!this.viewState.walletAddress && | ||
!!this.viewState.contractPath; | ||
|
||
this.initialized = true; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Contract metadata should be in the abi.json file as of NEON v2.6. If the metadata property is missing, using empty strings is a fine default, but these should be using abi.json if possible