Skip to content

Commit 1f538b9

Browse files
authored
Merge pull request #65 from ngdenterprise/issue37
Add a panel that shows a list of all known smart contracts, allowing quick access to contract metadata
2 parents 6ca9176 + a31aa8a commit 1f538b9

File tree

12 files changed

+303
-6
lines changed

12 files changed

+303
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
66

77
## Unreleased
88

9+
### Added
10+
11+
- A panel that shows a list of all known smart contracts, allowing quick access to contract metadata
12+
913
### Changed
1014

1115
- Creating a Java smart contract automatically targets the latest version of neow3j (per Maven Central)

package.json

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"onCommand:neo3-visual-devtracker.neo.invokeContract",
4141
"onCommand:neo3-visual-devtracker.neo.newContract",
4242
"onCommand:neo3-visual-devtracker.neo.walletCreate",
43+
"onCommand:neo3-visual-devtracker.tracker.openContract",
4344
"onCommand:neo3-visual-devtracker.tracker.openTracker",
4445
"onCustomEditor:neo3-visual-devtracker.express.neo-invoke-json",
4546
"onView:neo3-visual-devtracker.views.blockchains",
@@ -153,6 +154,11 @@
153154
"title": "Create wallet",
154155
"category": "Neo N3"
155156
},
157+
{
158+
"command": "neo3-visual-devtracker.tracker.openContract",
159+
"title": "Show smart contract information",
160+
"category": "Neo N3 Visual DevTracker"
161+
},
156162
{
157163
"command": "neo3-visual-devtracker.tracker.openTracker",
158164
"title": "Open Neo N3 Visual DevTracker",
@@ -259,19 +265,19 @@
259265
],
260266
"view/title": [
261267
{
262-
"command": "neo3-visual-devtracker.express.create",
268+
"command": "neo3-visual-devtracker.customizeServerList",
263269
"when": "view == neo3-visual-devtracker.views.blockchains"
264270
},
265271
{
266-
"command": "neo3-visual-devtracker.neo.newContract",
272+
"command": "neo3-visual-devtracker.express.create",
267273
"when": "view == neo3-visual-devtracker.views.blockchains"
268274
},
269275
{
270-
"command": "neo3-visual-devtracker.neo.walletCreate",
271-
"when": "view == neo3-visual-devtracker.views.blockchains"
276+
"command": "neo3-visual-devtracker.neo.newContract",
277+
"when": "view == neo3-visual-devtracker.views.contracts"
272278
},
273279
{
274-
"command": "neo3-visual-devtracker.customizeServerList",
280+
"command": "neo3-visual-devtracker.neo.walletCreate",
275281
"when": "view == neo3-visual-devtracker.views.blockchains"
276282
}
277283
]
@@ -282,6 +288,10 @@
282288
"id": "neo3-visual-devtracker.views.blockchains",
283289
"name": "Blockchains"
284290
},
291+
{
292+
"id": "neo3-visual-devtracker.views.contracts",
293+
"name": "Smart contracts"
294+
},
285295
{
286296
"id": "neo3-visual-devtracker.views.quickStart",
287297
"name": "Quick Start",

src/extension/commandArguments.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type CommandArguments = {
1919
amount?: number;
2020
asset?: string;
2121
blockchainIdentifier?: BlockchainIdentifier;
22+
hash?: string;
2223
path?: string;
2324
receiver?: string;
2425
secondsPerBlock?: number;
@@ -32,6 +33,7 @@ async function sanitizeCommandArguments(input: any): Promise<CommandArguments> {
3233
? `${input.asset}`.replace(/[^a-z0-9]/gi, "")
3334
: undefined,
3435
blockchainIdentifier: undefined, // TODO: Allow blockchain to be specified in command URIs
36+
hash: input.hash ? `${input.hash}`.replace(/[^xa-f0-9]/gi, "") : undefined,
3537
path: undefined, // TODO: Allow specification of path in command URIs (ensure supplied path is within the workspace though)
3638
receiver: input.receiver
3739
? `${input.receiver}`.replace(/[^a-z0-9]/gi, "")

src/extension/commands/trackerCommands.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,40 @@ import AutoComplete from "../autoComplete";
44
import BlockchainMonitorPool from "../blockchainMonitor/blockchainMonitorPool";
55
import BlockchainsTreeDataProvider from "../vscodeProviders/blockchainsTreeDataProvider";
66
import { CommandArguments } from "../commandArguments";
7+
import ContractPanelController from "../panelControllers/contractPanelController";
8+
import IoHelpers from "../util/ioHelpers";
79
import TrackerPanelController from "../panelControllers/trackerPanelController";
810

911
export default class TrackerCommands {
12+
static async openContract(
13+
context: vscode.ExtensionContext,
14+
autoComplete: AutoComplete,
15+
commandArguments: CommandArguments
16+
) {
17+
const autoCompleteData = autoComplete.data;
18+
let hash = commandArguments.hash;
19+
if (!hash) {
20+
if (!!Object.keys(autoCompleteData.contractNames).length) {
21+
const selection = await IoHelpers.multipleChoice(
22+
"Select a contract",
23+
...Object.keys(autoCompleteData.contractNames).map(
24+
(_) => `${_} - ${autoCompleteData.contractNames[_]}`
25+
)
26+
);
27+
if (selection) {
28+
hash = selection.split(" ")[0];
29+
}
30+
} else {
31+
vscode.window.showInformationMessage(
32+
"No N3 contracts are available to display"
33+
);
34+
}
35+
}
36+
if (hash) {
37+
new ContractPanelController(context, hash, autoComplete);
38+
}
39+
}
40+
1041
static async openTracker(
1142
context: vscode.ExtensionContext,
1243
autoComplete: AutoComplete,

src/extension/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import BlockchainsTreeDataProvider from "./vscodeProviders/blockchainsTreeDataPr
88
import CheckpointDetector from "./fileDetectors/checkpointDetector";
99
import { CommandArguments, sanitizeCommandArguments } from "./commandArguments";
1010
import ContractDetector from "./fileDetectors/contractDetector";
11+
import ContractsTreeDataProvider from "./vscodeProviders/contractsTreeDataProvider";
1112
import Log from "../shared/log";
1213
import NeoCommands from "./commands/neoCommands";
1314
import NeoExpress from "./neoExpress/neoExpress";
@@ -77,6 +78,10 @@ export async function activate(context: vscode.ExtensionContext) {
7778
walletDetector,
7879
neoExpressDetector
7980
);
81+
const contractsTreeDataProvider = new ContractsTreeDataProvider(
82+
context.extensionPath,
83+
autoComplete
84+
);
8085
const neoInvokeFileEditorProvider = new NeoInvokeFileEditorProvider(
8186
context,
8287
activeConnection,
@@ -101,6 +106,13 @@ export async function activate(context: vscode.ExtensionContext) {
101106
)
102107
);
103108

109+
context.subscriptions.push(
110+
vscode.window.registerTreeDataProvider(
111+
"neo3-visual-devtracker.views.contracts",
112+
contractsTreeDataProvider
113+
)
114+
);
115+
104116
context.subscriptions.push(
105117
vscode.window.registerCustomEditorProvider(
106118
"neo3-visual-devtracker.neo.neo-invoke-json",
@@ -312,6 +324,13 @@ export async function activate(context: vscode.ExtensionContext) {
312324
commandArguments
313325
)
314326
);
327+
328+
registerCommand(
329+
context,
330+
"neo3-visual-devtracker.tracker.openContract",
331+
(commandArguments) =>
332+
TrackerCommands.openContract(context, autoComplete, commandArguments)
333+
);
315334
}
316335

317336
export function deactivate() {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as vscode from "vscode";
2+
3+
import AutoComplete from "../autoComplete";
4+
import ContractViewRequest from "../../shared/messages/contractViewRequest";
5+
import ContractViewState from "../../shared/viewState/contractViewState";
6+
import Log from "../../shared/log";
7+
import PanelControllerBase from "./panelControllerBase";
8+
9+
const LOG_PREFIX = "ContractPanelController";
10+
11+
export default class ContractPanelController extends PanelControllerBase<
12+
ContractViewState,
13+
ContractViewRequest
14+
> {
15+
constructor(
16+
context: vscode.ExtensionContext,
17+
private readonly contractHash: string,
18+
autoComplete: AutoComplete
19+
) {
20+
super(
21+
{
22+
view: "contract",
23+
panelTitle:
24+
autoComplete.data.contractNames[contractHash] || contractHash,
25+
autoCompleteData: autoComplete.data,
26+
contractHash,
27+
},
28+
context
29+
);
30+
autoComplete.onChange((autoCompleteData) => {
31+
const name = autoCompleteData.contractNames[contractHash] || contractHash;
32+
this.updateViewState({ panelTitle: name, ...autoCompleteData });
33+
});
34+
}
35+
36+
onClose() {}
37+
38+
protected async onRequest(request: ContractViewRequest) {
39+
Log.log(LOG_PREFIX, `Request: ${JSON.stringify(request)}`);
40+
if (request.copyHash) {
41+
await vscode.env.clipboard.writeText(this.contractHash);
42+
vscode.window.showInformationMessage(
43+
`Contract hash copied to clipboard: ${this.contractHash}`
44+
);
45+
}
46+
}
47+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as vscode from "vscode";
2+
3+
import AutoComplete from "../autoComplete";
4+
import Log from "../../shared/log";
5+
import posixPath from "../util/posixPath";
6+
7+
const LOG_PREFIX = "ContractsTreeDataProvider";
8+
9+
type ContractData = {
10+
description: string;
11+
hash: string;
12+
name: string;
13+
nefInWorkspace: boolean;
14+
};
15+
16+
export default class ContractsTreeDataProvider
17+
implements vscode.TreeDataProvider<ContractData> {
18+
onDidChangeTreeData: vscode.Event<void>;
19+
20+
private readonly onDidChangeTreeDataEmitter: vscode.EventEmitter<void>;
21+
22+
private contracts: ContractData[] = [];
23+
24+
constructor(
25+
private readonly extensionPath: string,
26+
private readonly autoComplete: AutoComplete
27+
) {
28+
this.onDidChangeTreeDataEmitter = new vscode.EventEmitter<void>();
29+
this.onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event;
30+
autoComplete.onChange(() => this.refresh());
31+
}
32+
33+
getTreeItem(contract: ContractData): vscode.TreeItem {
34+
return {
35+
command: {
36+
command: "neo3-visual-devtracker.tracker.openContract",
37+
arguments: [{ hash: contract.hash }],
38+
title: contract.hash,
39+
},
40+
label: contract.name,
41+
tooltip: `${contract.hash}\n${contract.description || ""}`.trim(),
42+
description: contract.description,
43+
iconPath: contract.nefInWorkspace
44+
? posixPath(this.extensionPath, "resources", "blockchain-express.svg")
45+
: posixPath(this.extensionPath, "resources", "blockchain-private.svg"),
46+
};
47+
}
48+
49+
getChildren(contractHash?: ContractData): ContractData[] {
50+
return contractHash ? [] : this.contracts;
51+
}
52+
53+
refresh() {
54+
Log.log(LOG_PREFIX, "Refreshing contract list");
55+
const newData: ContractData[] = [];
56+
for (const hash of Object.keys(this.autoComplete.data.contractNames)) {
57+
const name = this.autoComplete.data.contractNames[hash] || hash;
58+
const manifest = this.autoComplete.data.contractManifests[hash] || {};
59+
const description =
60+
((manifest.extra || {}) as any)["Description"] || undefined;
61+
const nefInWorkspace =
62+
!!this.autoComplete.data.contractPaths[hash] ||
63+
!!this.autoComplete.data.contractPaths[name];
64+
newData.push({ hash, name, description, nefInWorkspace });
65+
}
66+
newData.sort((a, b) => a.name.localeCompare(b.name));
67+
this.contracts = newData;
68+
this.onDidChangeTreeDataEmitter.fire();
69+
}
70+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React from "react";
2+
3+
import ContractViewState from "../../../shared/viewState/contractViewState";
4+
import ContractViewRequest from "../../../shared/messages/contractViewRequest";
5+
import Hash from "../Hash";
6+
7+
type Props = {
8+
viewState: ContractViewState;
9+
postMessage: (message: ContractViewRequest) => void;
10+
};
11+
12+
export default function Contract({ viewState, postMessage }: Props) {
13+
const hash = viewState.contractHash;
14+
const name =
15+
viewState.autoCompleteData.contractNames[hash] || "Unknown contract";
16+
const manifest = viewState.autoCompleteData.contractManifests[hash] || {};
17+
const extra = (manifest.extra || {}) as any;
18+
const description = extra["Description"] || undefined;
19+
const author = extra["Author"] || undefined;
20+
const email = extra["Email"] || undefined;
21+
const supportedStandards = manifest.supportedstandards || [];
22+
const contractPaths =
23+
viewState.autoCompleteData.contractPaths[hash] ||
24+
viewState.autoCompleteData.contractPaths[name] ||
25+
[];
26+
return (
27+
<div style={{ padding: 10 }}>
28+
<h1>{name}</h1>
29+
{!!description && (
30+
<p style={{ paddingLeft: 20 }}>
31+
<div style={{ fontWeight: "bold", marginBottom: 10, marginTop: 15 }}>
32+
Description:
33+
</div>
34+
<div style={{ paddingLeft: 20 }}>{description}</div>
35+
</p>
36+
)}
37+
{(!!author || !!email) && (
38+
<p style={{ paddingLeft: 20 }}>
39+
<div style={{ fontWeight: "bold", marginBottom: 10, marginTop: 15 }}>
40+
Author:
41+
</div>
42+
{!!author && <div style={{ paddingLeft: 20 }}>{author}</div>}
43+
{!!email && <div style={{ paddingLeft: 20 }}>&lt;{email}&gt;</div>}
44+
</p>
45+
)}
46+
<p style={{ paddingLeft: 20 }}>
47+
<div style={{ fontWeight: "bold", marginBottom: 10, marginTop: 15 }}>
48+
Hash:
49+
</div>
50+
<div
51+
style={{ cursor: "pointer", paddingLeft: 20 }}
52+
onClick={() => postMessage({ copyHash: true })}
53+
>
54+
<strong>
55+
<Hash hash={hash} />
56+
</strong>{" "}
57+
<em> &mdash; click to copy contract hash to clipboard</em>
58+
</div>
59+
</p>
60+
{!!supportedStandards.length && (
61+
<p style={{ paddingLeft: 20 }}>
62+
<div style={{ fontWeight: "bold", marginBottom: 10, marginTop: 15 }}>
63+
Supported standards:
64+
</div>
65+
<ul>
66+
{supportedStandards.map((_, i) => (
67+
<li key={i}>{_}</li>
68+
))}
69+
</ul>
70+
</p>
71+
)}
72+
{!!contractPaths.length && (
73+
<p style={{ paddingLeft: 20 }}>
74+
<div style={{ fontWeight: "bold", marginBottom: 10, marginTop: 15 }}>
75+
Byte code location:
76+
</div>
77+
<ul>
78+
{contractPaths.map((_, i) => (
79+
<li key={i}>{_}</li>
80+
))}
81+
</ul>
82+
</p>
83+
)}
84+
</div>
85+
);
86+
}

src/panel/viewRouter.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React, { useEffect, useState } from "react";
22

3+
import Contract from "./components/views/Contract";
4+
import ContractViewState from "../shared/viewState/contractViewState";
35
import ControllerRequest from "../shared/messages/controllerRequest";
46
import InvokeFile from "./components/views/InvokeFile";
57
import InvokeFileViewState from "../shared/viewState/invokeFileViewState";
@@ -52,6 +54,14 @@ export default function ViewRouter() {
5254
let panelContent = <div></div>;
5355
if (!!view && !!viewState) {
5456
switch (view) {
57+
case "contract":
58+
panelContent = (
59+
<Contract
60+
viewState={viewState as ContractViewState}
61+
postMessage={(typedRequest) => postMessage({ typedRequest })}
62+
/>
63+
);
64+
break;
5565
case "invokeFile":
5666
panelContent = (
5767
<InvokeFile

0 commit comments

Comments
 (0)