Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how

## Unreleased

### Added

- A panel that shows a list of all known smart contracts, allowing quick access to contract metadata

### Changed

- Creating a Java smart contract automatically targets the latest version of neow3j (per Maven Central)
Expand Down
20 changes: 15 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"onCommand:neo3-visual-devtracker.neo.invokeContract",
"onCommand:neo3-visual-devtracker.neo.newContract",
"onCommand:neo3-visual-devtracker.neo.walletCreate",
"onCommand:neo3-visual-devtracker.tracker.openContract",
"onCommand:neo3-visual-devtracker.tracker.openTracker",
"onCustomEditor:neo3-visual-devtracker.express.neo-invoke-json",
"onView:neo3-visual-devtracker.views.blockchains",
Expand Down Expand Up @@ -153,6 +154,11 @@
"title": "Create wallet",
"category": "Neo N3"
},
{
"command": "neo3-visual-devtracker.tracker.openContract",
"title": "Show smart contract information",
"category": "Neo N3 Visual DevTracker"
},
{
"command": "neo3-visual-devtracker.tracker.openTracker",
"title": "Open Neo N3 Visual DevTracker",
Expand Down Expand Up @@ -259,19 +265,19 @@
],
"view/title": [
{
"command": "neo3-visual-devtracker.express.create",
"command": "neo3-visual-devtracker.customizeServerList",
"when": "view == neo3-visual-devtracker.views.blockchains"
},
{
"command": "neo3-visual-devtracker.neo.newContract",
"command": "neo3-visual-devtracker.express.create",
"when": "view == neo3-visual-devtracker.views.blockchains"
},
{
"command": "neo3-visual-devtracker.neo.walletCreate",
"when": "view == neo3-visual-devtracker.views.blockchains"
"command": "neo3-visual-devtracker.neo.newContract",
"when": "view == neo3-visual-devtracker.views.contracts"
},
{
"command": "neo3-visual-devtracker.customizeServerList",
"command": "neo3-visual-devtracker.neo.walletCreate",
"when": "view == neo3-visual-devtracker.views.blockchains"
}
]
Expand All @@ -282,6 +288,10 @@
"id": "neo3-visual-devtracker.views.blockchains",
"name": "Blockchains"
},
{
"id": "neo3-visual-devtracker.views.contracts",
"name": "Smart contracts"
},
{
"id": "neo3-visual-devtracker.views.quickStart",
"name": "Quick Start",
Expand Down
2 changes: 2 additions & 0 deletions src/extension/commandArguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type CommandArguments = {
amount?: number;
asset?: string;
blockchainIdentifier?: BlockchainIdentifier;
hash?: string;
path?: string;
receiver?: string;
secondsPerBlock?: number;
Expand All @@ -32,6 +33,7 @@ async function sanitizeCommandArguments(input: any): Promise<CommandArguments> {
? `${input.asset}`.replace(/[^a-z0-9]/gi, "")
: undefined,
blockchainIdentifier: undefined, // TODO: Allow blockchain to be specified in command URIs
hash: input.hash ? `${input.hash}`.replace(/[^xa-f0-9]/gi, "") : undefined,
path: undefined, // TODO: Allow specification of path in command URIs (ensure supplied path is within the workspace though)
receiver: input.receiver
? `${input.receiver}`.replace(/[^a-z0-9]/gi, "")
Expand Down
31 changes: 31 additions & 0 deletions src/extension/commands/trackerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,40 @@ import AutoComplete from "../autoComplete";
import BlockchainMonitorPool from "../blockchainMonitor/blockchainMonitorPool";
import BlockchainsTreeDataProvider from "../vscodeProviders/blockchainsTreeDataProvider";
import { CommandArguments } from "../commandArguments";
import ContractPanelController from "../panelControllers/contractPanelController";
import IoHelpers from "../util/ioHelpers";
import TrackerPanelController from "../panelControllers/trackerPanelController";

export default class TrackerCommands {
static async openContract(
context: vscode.ExtensionContext,
autoComplete: AutoComplete,
commandArguments: CommandArguments
) {
const autoCompleteData = autoComplete.data;
let hash = commandArguments.hash;
if (!hash) {
if (!!Object.keys(autoCompleteData.contractNames).length) {
const selection = await IoHelpers.multipleChoice(
"Select a contract",
...Object.keys(autoCompleteData.contractNames).map(
(_) => `${_} - ${autoCompleteData.contractNames[_]}`
)
);
if (selection) {
hash = selection.split(" ")[0];
}
} else {
vscode.window.showInformationMessage(
"No N3 contracts are available to display"
);
}
}
if (hash) {
new ContractPanelController(context, hash, autoComplete);
}
}

static async openTracker(
context: vscode.ExtensionContext,
autoComplete: AutoComplete,
Expand Down
19 changes: 19 additions & 0 deletions src/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import BlockchainsTreeDataProvider from "./vscodeProviders/blockchainsTreeDataPr
import CheckpointDetector from "./fileDetectors/checkpointDetector";
import { CommandArguments, sanitizeCommandArguments } from "./commandArguments";
import ContractDetector from "./fileDetectors/contractDetector";
import ContractsTreeDataProvider from "./vscodeProviders/contractsTreeDataProvider";
import Log from "../shared/log";
import NeoCommands from "./commands/neoCommands";
import NeoExpress from "./neoExpress/neoExpress";
Expand Down Expand Up @@ -77,6 +78,10 @@ export async function activate(context: vscode.ExtensionContext) {
walletDetector,
neoExpressDetector
);
const contractsTreeDataProvider = new ContractsTreeDataProvider(
context.extensionPath,
autoComplete
);
const neoInvokeFileEditorProvider = new NeoInvokeFileEditorProvider(
context,
activeConnection,
Expand All @@ -101,6 +106,13 @@ export async function activate(context: vscode.ExtensionContext) {
)
);

context.subscriptions.push(
vscode.window.registerTreeDataProvider(
"neo3-visual-devtracker.views.contracts",
contractsTreeDataProvider
)
);

context.subscriptions.push(
vscode.window.registerCustomEditorProvider(
"neo3-visual-devtracker.neo.neo-invoke-json",
Expand Down Expand Up @@ -312,6 +324,13 @@ export async function activate(context: vscode.ExtensionContext) {
commandArguments
)
);

registerCommand(
context,
"neo3-visual-devtracker.tracker.openContract",
(commandArguments) =>
TrackerCommands.openContract(context, autoComplete, commandArguments)
);
}

export function deactivate() {
Expand Down
47 changes: 47 additions & 0 deletions src/extension/panelControllers/contractPanelController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as vscode from "vscode";

import AutoComplete from "../autoComplete";
import ContractViewRequest from "../../shared/messages/contractViewRequest";
import ContractViewState from "../../shared/viewState/contractViewState";
import Log from "../../shared/log";
import PanelControllerBase from "./panelControllerBase";

const LOG_PREFIX = "ContractPanelController";

export default class ContractPanelController extends PanelControllerBase<
ContractViewState,
ContractViewRequest
> {
constructor(
context: vscode.ExtensionContext,
private readonly contractHash: string,
autoComplete: AutoComplete
) {
super(
{
view: "contract",
panelTitle:
autoComplete.data.contractNames[contractHash] || contractHash,
autoCompleteData: autoComplete.data,
contractHash,
},
context
);
autoComplete.onChange((autoCompleteData) => {
const name = autoCompleteData.contractNames[contractHash] || contractHash;
this.updateViewState({ panelTitle: name, ...autoCompleteData });
});
}

onClose() {}

protected async onRequest(request: ContractViewRequest) {
Log.log(LOG_PREFIX, `Request: ${JSON.stringify(request)}`);
if (request.copyHash) {
await vscode.env.clipboard.writeText(this.contractHash);
vscode.window.showInformationMessage(
`Contract hash copied to clipboard: ${this.contractHash}`
);
}
}
}
70 changes: 70 additions & 0 deletions src/extension/vscodeProviders/contractsTreeDataProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as vscode from "vscode";

import AutoComplete from "../autoComplete";
import Log from "../../shared/log";
import posixPath from "../util/posixPath";

const LOG_PREFIX = "ContractsTreeDataProvider";

type ContractData = {
description: string;
hash: string;
name: string;
nefInWorkspace: boolean;
};

export default class ContractsTreeDataProvider
implements vscode.TreeDataProvider<ContractData> {
onDidChangeTreeData: vscode.Event<void>;

private readonly onDidChangeTreeDataEmitter: vscode.EventEmitter<void>;

private contracts: ContractData[] = [];

constructor(
private readonly extensionPath: string,
private readonly autoComplete: AutoComplete
) {
this.onDidChangeTreeDataEmitter = new vscode.EventEmitter<void>();
this.onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event;
autoComplete.onChange(() => this.refresh());
}

getTreeItem(contract: ContractData): vscode.TreeItem {
return {
command: {
command: "neo3-visual-devtracker.tracker.openContract",
arguments: [{ hash: contract.hash }],
title: contract.hash,
},
label: contract.name,
tooltip: `${contract.hash}\n${contract.description || ""}`.trim(),
description: contract.description,
iconPath: contract.nefInWorkspace
? posixPath(this.extensionPath, "resources", "blockchain-express.svg")
: posixPath(this.extensionPath, "resources", "blockchain-private.svg"),
};
}

getChildren(contractHash?: ContractData): ContractData[] {
return contractHash ? [] : this.contracts;
}

refresh() {
Log.log(LOG_PREFIX, "Refreshing contract list");
const newData: ContractData[] = [];
for (const hash of Object.keys(this.autoComplete.data.contractNames)) {
const name = this.autoComplete.data.contractNames[hash] || hash;
const manifest = this.autoComplete.data.contractManifests[hash] || {};
const description =
((manifest.extra || {}) as any)["Description"] || undefined;
const nefInWorkspace =
!!this.autoComplete.data.contractPaths[hash] ||
!!this.autoComplete.data.contractPaths[name];
newData.push({ hash, name, description, nefInWorkspace });
}
newData.sort((a, b) => a.name.localeCompare(b.name));
this.contracts = newData;
this.onDidChangeTreeDataEmitter.fire();
}
}
86 changes: 86 additions & 0 deletions src/panel/components/views/Contract.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from "react";

import ContractViewState from "../../../shared/viewState/contractViewState";
import ContractViewRequest from "../../../shared/messages/contractViewRequest";
import Hash from "../Hash";

type Props = {
viewState: ContractViewState;
postMessage: (message: ContractViewRequest) => void;
};

export default function Contract({ viewState, postMessage }: Props) {
const hash = viewState.contractHash;
const name =
viewState.autoCompleteData.contractNames[hash] || "Unknown contract";
const manifest = viewState.autoCompleteData.contractManifests[hash] || {};
const extra = (manifest.extra || {}) as any;
const description = extra["Description"] || undefined;
const author = extra["Author"] || undefined;
const email = extra["Email"] || undefined;
const supportedStandards = manifest.supportedstandards || [];
const contractPaths =
viewState.autoCompleteData.contractPaths[hash] ||
viewState.autoCompleteData.contractPaths[name] ||
[];
return (
<div style={{ padding: 10 }}>
<h1>{name}</h1>
{!!description && (
<p style={{ paddingLeft: 20 }}>
<div style={{ fontWeight: "bold", marginBottom: 10, marginTop: 15 }}>
Description:
</div>
<div style={{ paddingLeft: 20 }}>{description}</div>
</p>
)}
{(!!author || !!email) && (
<p style={{ paddingLeft: 20 }}>
<div style={{ fontWeight: "bold", marginBottom: 10, marginTop: 15 }}>
Author:
</div>
{!!author && <div style={{ paddingLeft: 20 }}>{author}</div>}
{!!email && <div style={{ paddingLeft: 20 }}>&lt;{email}&gt;</div>}
</p>
)}
<p style={{ paddingLeft: 20 }}>
<div style={{ fontWeight: "bold", marginBottom: 10, marginTop: 15 }}>
Hash:
</div>
<div
style={{ cursor: "pointer", paddingLeft: 20 }}
onClick={() => postMessage({ copyHash: true })}
>
<strong>
<Hash hash={hash} />
</strong>{" "}
<em> &mdash; click to copy contract hash to clipboard</em>
</div>
</p>
{!!supportedStandards.length && (
<p style={{ paddingLeft: 20 }}>
<div style={{ fontWeight: "bold", marginBottom: 10, marginTop: 15 }}>
Supported standards:
</div>
<ul>
{supportedStandards.map((_, i) => (
<li key={i}>{_}</li>
))}
</ul>
</p>
)}
{!!contractPaths.length && (
<p style={{ paddingLeft: 20 }}>
<div style={{ fontWeight: "bold", marginBottom: 10, marginTop: 15 }}>
Byte code location:
</div>
<ul>
{contractPaths.map((_, i) => (
<li key={i}>{_}</li>
))}
</ul>
</p>
)}
</div>
);
}
10 changes: 10 additions & 0 deletions src/panel/viewRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useEffect, useState } from "react";

import Contract from "./components/views/Contract";
import ContractViewState from "../shared/viewState/contractViewState";
import ControllerRequest from "../shared/messages/controllerRequest";
import InvokeFile from "./components/views/InvokeFile";
import InvokeFileViewState from "../shared/viewState/invokeFileViewState";
Expand Down Expand Up @@ -52,6 +54,14 @@ export default function ViewRouter() {
let panelContent = <div></div>;
if (!!view && !!viewState) {
switch (view) {
case "contract":
panelContent = (
<Contract
viewState={viewState as ContractViewState}
postMessage={(typedRequest) => postMessage({ typedRequest })}
/>
);
break;
case "invokeFile":
panelContent = (
<InvokeFile
Expand Down
Loading