Skip to content
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

Storage explorer for Neo Express #122

Merged
merged 2 commits into from
Mar 5, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
"dark": "resources/dark/open.svg"
}
},
{
"command": "neo-visual-devtracker.storageExplorer",
"title": "Open Storage Explorer",
"category": "Neo Visual DevTracker"
},
{
"command": "neo-visual-devtracker.refreshObjectExplorerNode",
"title": "Refresh Neo RPC Server list",
Expand Down Expand Up @@ -275,6 +280,14 @@
"command": "neo-visual-devtracker.openTracker",
"when": "view == neo-visual-devtracker.rpcServerExplorer && viewItem == url"
},
{
"command": "neo-visual-devtracker.storageExplorer",
"when": "view == neo-visual-devtracker.rpcServerExplorer && viewItem == expressNode"
},
{
"command": "neo-visual-devtracker.storageExplorer",
"when": "view == neo-visual-devtracker.rpcServerExplorer && viewItem == expressNodeMulti"
},
{
"command": "neo-visual-devtracker.transferAssets",
"when": "view == neo-visual-devtracker.rpcServerExplorer && viewItem == expressNode"
Expand Down
7 changes: 7 additions & 0 deletions src/cachedRpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ export class CachedRpcClient {
return this.rpcClient.query({ method: 'express-get-populated-blocks' });
}

public getContractStorage(contractHash: string): Promise<any> {
// Note that this method is not cached. Contract storage could potentially change at any
// new block.

return this.rpcClient.query({ method: 'express-get-contract-storage', params: [ contractHash ] });
}

public getUnclaimed(address: string): Promise<any> {
// Note that this method is not cached. The state of an address can change in-between calls.
return this.rpcClient.getUnclaimed(address);
Expand Down
23 changes: 23 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { NeoTrackerPanel } from './neoTrackerPanel';
import { RpcServerExplorer, RpcServerTreeItemIdentifier } from './rpcServerExplorer';
import { RpcConnectionPool } from './rpcConnectionPool';
import { StartPanel } from './startPanel';
import { StoragePanel } from './storagePanel';
import { TransferPanel } from './transferPanel';
import { WalletExplorer } from './walletExplorer';

Expand Down Expand Up @@ -188,6 +189,27 @@ export function activate(context: vscode.ExtensionContext) {
}
});

const openStorageExplorerCommand = vscode.commands.registerCommand('neo-visual-devtracker.storageExplorer', async (server) => {
await requireNeoExpress(async () => {
server = server || await selectBlockchain('Explore Storage', context.globalState, [ 'expressNode', 'expressNodeMulti' ]);
if (!server) {
return;
}
const rpcUri = await selectUri('Explore Storage', server, context.globalState);
try {
const panel = new StoragePanel(
context.extensionPath,
rpcUri,
rpcConnectionPool.getConnection(rpcUri),
contractDetector,
context.subscriptions,
server.jsonFile ? new NeoExpressConfig(server.jsonFile) : undefined);
} catch (e) {
console.error('Error opening new storage explorer panel ', e);
}
});
});

const refreshServersCommand = vscode.commands.registerCommand('neo-visual-devtracker.refreshObjectExplorerNode', () => {
rpcServerExplorer.refresh();
walletExplorer.refresh();
Expand Down Expand Up @@ -495,6 +517,7 @@ export function activate(context: vscode.ExtensionContext) {
const waletExplorerProvider = vscode.languages.registerCodeLensProvider({ language: 'json' }, walletExplorer);

context.subscriptions.push(openTrackerCommand);
context.subscriptions.push(openStorageExplorerCommand);
context.subscriptions.push(refreshServersCommand);
context.subscriptions.push(startServerCommand);
context.subscriptions.push(startServerAdvancedCommand);
Expand Down
30 changes: 1 addition & 29 deletions src/invocationPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { INeoRpcConnection } from './neoRpcConnection';
import { invokeEvents } from './panels/invokeEvents';
import { NeoExpressConfig } from './neoExpressConfig';
import { NeoTrackerPanel } from './neoTrackerPanel';
import { ResultValue } from './resultValue';
import { WalletExplorer } from './walletExplorer';

import { api } from '@cityofzion/neon-js';
Expand All @@ -20,35 +21,6 @@ const bs58check = require('bs58check');
const JavascriptHrefPlaceholder : string = '[JAVASCRIPT_HREF]';
const CssHrefPlaceholder : string = '[CSS_HREF]';

class ResultValue {
public readonly asInteger: string;
public readonly asByteArray: string;
public readonly asString: string;
public readonly asAddress: string;
constructor(result: any) {
let value = result.value;
if ((value !== null) && (value !== undefined)) {
if (value.length === 0) {
value = '00';
}
this.asByteArray = '0x' + value;
const buffer = Buffer.from(value, 'hex');
this.asString = buffer.toString();
this.asAddress = '';
if (value.length === 42) {
this.asAddress = bs58check.encode(buffer);
}
buffer.reverse();
this.asInteger = BigInt('0x' + buffer.toString('hex')).toString();
} else {
this.asInteger = '0';
this.asByteArray = '(empty)';
this.asString = '(empty)';
this.asAddress = '(empty)';
}
}
}

class ViewState {
rpcUrl: string = '';
rpcDescription: string = '';
Expand Down
18 changes: 17 additions & 1 deletion src/neoRpcConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface INeoRpcConnection {
getBlock(index: string | number, statusReceiver?: INeoStatusReceiver): Promise<any>;
getBlocks(index: number | undefined, hideEmptyBlocks: boolean, forwards: boolean, statusReceiver: INeoStatusReceiver): Promise<Blocks>;
getClaimable(address: string, statusReceiver: INeoStatusReceiver): Promise<any>;
getContractStorage(contractHash: string, statusReceiver: INeoStatusReceiver): Promise<any>;
getTransaction(txid: string, statusReceiver: INeoStatusReceiver): Promise<any>;
getUnspents(address: string, statusReceiver: INeoStatusReceiver): Promise<any>;
getUnclaimed(address: string, statusReceiver: INeoStatusReceiver): Promise<any>;
Expand Down Expand Up @@ -220,6 +221,21 @@ export class NeoRpcConnection implements INeoRpcConnection {
}
}

public async getContractStorage(contractHash: string, statusReceiver: INeoStatusReceiver) {
try {
statusReceiver.updateStatus('Getting storage for contract ' + contractHash);
return (await this.rpcClient.getContractStorage(contractHash)).result;
} catch(e) {
if (e.message.toLowerCase().indexOf('method not found') !== -1) {
console.warn('NeoRpcConnection: get-contract-storage unsupported by ' + this.rpcUrl + ' (contractHash=' + contractHash + '): ' + e);
return undefined;
} else {
console.error('NeoRpcConnection could not retrieve contract storage (contractHash=' + contractHash + '): ' + e);
return undefined;
}
}
}

public async getTransaction(txid: string, statusReceiver: INeoStatusReceiver) {
try {
statusReceiver.updateStatus('Retrieving transaction ' + txid);
Expand Down Expand Up @@ -362,7 +378,7 @@ export class NeoRpcConnection implements INeoRpcConnection {
try {
await this.subscriptions[i].onNewBlock(blockchainInfo);
} catch (e) {
console.error('Error within INeoSubscription #' + i + ' (' + this.subscriptions[i] + ')');
console.error('Error within INeoSubscription #' + i, e, this.subscriptions[i]);
}
}
}
Expand Down
63 changes: 63 additions & 0 deletions src/panels/storage.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src vscode-resource:; style-src vscode-resource:;">
<link rel="stylesheet" href="[CSS_HREF]">
</head>

<body>

<div class="header">
<div class="col col-1">
Contract:
</div>
<div class="col col-2">
<select id="contractDropdown"></select>
</div>
<div class="col col-3">
&nbsp;
</div>
</div>

<div class="error" >
Could not retrieve storage for the selected contract.
Ensure that Neo Express is running.
</div>

<table id="storageTable">
<thead>
<tr>
<th colspan="2">Key</th>
<th colspan="4">Value</th>
<th colspan="1" rowspan="2">Constant</th>
</tr>
<tr>
<th>(as string)</th>
<th>(as bytes)</th>
<th>(as bytes)</th>
<th>(as string)</th>
<th>(as number)</th>
<th>(as address)</th>
</tr>
</thead>
<tbody>
</tbody>
</table>

<div class="no-storage" >
(No stored data found for this contract)
</div>

<div class="status-bar">
<span id="rpcUrl" class="left"></span>
<span id="rpcDescription" class="right"></span>
</div>

<script src="[JAVASCRIPT_HREF]"></script>

</body>

</html>
95 changes: 95 additions & 0 deletions src/panels/storage.main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { htmlHelpers } from "./htmlHelpers";
import { storageEvents } from "./storageEvents";
import { storageSelectors } from "./storageSelectors";

/*
* This code runs in the context of the WebView panel. It exchanges JSON messages with the main
* extension code. The browser instance running this code is disposed whenever the panel loses
* focus, and reloaded when it later gains focus (so navigation state should not be stored here).
*/

declare var acquireVsCodeApi: any;

let viewState: any = {};

let vsCodePostMessage: Function;

const contractDropdown = document.querySelector(storageSelectors.ContractDropdown) as HTMLSelectElement;
const storageTableBody = document.querySelector(storageSelectors.StorageTableBody) as HTMLElement;

function hexToAscii(hex?: string) {
var result = '';
if (hex) {
for (var i = 0; i < hex.length; i += 2) {
result += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
}
return result;
}

function storageRow(key?: string, value?: string, parsed?: any, constant?: boolean) {
const keyAsString = hexToAscii(key);
const keyAsBytes = key || ' ';
const valueAsBytes = value || ' ';
const valueAsString = hexToAscii(value);
const valueAsNumber = parsed?.asInteger || ' ';
const valueAsAddress = parsed?.asAddress || ' ';
return htmlHelpers.newTableRow(
htmlHelpers.text(keyAsString),
htmlHelpers.text(keyAsBytes),
htmlHelpers.text(valueAsBytes),
htmlHelpers.text(valueAsString),
htmlHelpers.text(valueAsNumber),
htmlHelpers.text(valueAsAddress),
htmlHelpers.text(constant === undefined ? '' : (constant ? 'true' : 'false')));
}

function render() {
htmlHelpers.setPlaceholder(storageSelectors.RpcDescription, htmlHelpers.text(viewState.rpcDescription));
htmlHelpers.setPlaceholder(storageSelectors.RpcUrl, htmlHelpers.text(viewState.rpcUrl));
htmlHelpers.showHide(storageSelectors.Error, viewState.selectedContractHash && !viewState.selectedContractStorage);
htmlHelpers.showHide(storageSelectors.NoStorage, viewState.selectedContractHash && viewState.selectedContractStorage && !viewState.selectedContractStorage.length);
htmlHelpers.showHide(storageSelectors.StorageTable, viewState.selectedContractStorage, 'table');
htmlHelpers.clearChildren(contractDropdown);
for (let i = 0; i < viewState.contracts.length; i++) {
const contract = viewState.contracts[i];
const contractHash = contract.hash;
const contractName = contract.name;
const option = document.createElement('option') as HTMLOptionElement;
option.value = contractHash;
option.appendChild(htmlHelpers.text(contractName + ' - ' + contractHash));
contractDropdown.appendChild(option);
if (viewState.selectedContractHash === contractHash) {
option.selected = true;
}
}
htmlHelpers.clearChildren(storageTableBody);
if (viewState.selectedContractStorage) {
for (let i = 0; i < viewState.selectedContractStorage.length; i++) {
const row = viewState.selectedContractStorage[i];
storageTableBody.appendChild(storageRow(row.key, row.value, row.parsed, row.constant));
}
}
contractDropdown.focus();
}

function handleMessage(message: any) {
if (message.viewState) {
viewState = message.viewState;
console.log('<-', viewState);
render();
}
}

function initializePanel() {
const vscode = acquireVsCodeApi();
vsCodePostMessage = vscode.postMessage;
contractDropdown.addEventListener('change', _ => {
viewState.selectedContractHash = contractDropdown.options[contractDropdown.selectedIndex].value;
vsCodePostMessage({ e: storageEvents.Update, c: viewState });
});
window.addEventListener('message', msg => handleMessage(msg.data));
vscode.postMessage({ e: storageEvents.Init });
}

window.onload = initializePanel;
65 changes: 65 additions & 0 deletions src/panels/storage.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
@import 'common';

body {
padding: 0;
background-color: var(--vscode-editor-background);
}

.header {
padding: 10px 0px 10px 0px;
margin: 0px;
height: 45px;
line-height: 45px;
background-color: var(--vscode-sideBarSectionHeader-background);
color: var(--vscode-sideBarSectionHeader-foreground);
border-bottom: 1px solid var(--vscode-sideBarSectionHeader-border);
.col {
float: left;
padding: 0px 10px 0px 10px;
box-sizing: border-box;
font-size: 1.0em;
font-weight: bold;
&.col-1 {
width: 15%;
text-align: right;
}
&.col-2 {
width: 80%;
select {
width: 100%;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
}
}
&.col-3 {
width: 5%;
}
}
}

.error {
padding: 10px;
}

.no-storage {
padding: 10px;
text-align: center;
font-style: italic;
}

#storageTable {
width: 100%;
border-spacing: 0px;
th, td {
padding: 10px;
border: 1px solid var(--vscode-sideBarSectionHeader-background);
}
th {
white-space: nowrap;
background-color: var(--vscode-sideBar-background);
}
td {
word-wrap: break-word;
word-break: break-all;
}
}
Loading