Skip to content

Commit

Permalink
Various UI improvements based on walk-through with Peng/John (#117)
Browse files Browse the repository at this point in the history
* improve layout of invocation panel

* change a word

* close dialogs after navigating to tracker

* when doing a devtracker search, reuse an old panel if possible (preference to oldest panel)

* fixes #105

* Make "Claim GAS" dialog capable of transferring NEO before claiming GAS. fixes #114

* claim will never work if RPC server does not support getting claimables
  • Loading branch information
djnicholson authored Feb 25, 2020
1 parent 2c5ea3b commit 8d7ebf9
Show file tree
Hide file tree
Showing 11 changed files with 328 additions and 135 deletions.
102 changes: 98 additions & 4 deletions src/claimPanel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as bignumber from 'bignumber.js';
import * as fs from 'fs';
import * as neon from '@cityofzion/neon-js';
import * as path from 'path';
Expand All @@ -6,6 +7,7 @@ import * as vscode from 'vscode';
import { claimEvents } from './panels/claimEvents';

import { INeoRpcConnection } from './neoRpcConnection';
import { IWallet } from './iWallet';
import { NeoExpressConfig } from './neoExpressConfig';
import { NeoTrackerPanel } from './neoTrackerPanel';
import { WalletExplorer } from './walletExplorer';
Expand All @@ -15,6 +17,9 @@ const CssHrefPlaceholder : string = '[CSS_HREF]';

class ViewState {
claimable: number = 0;
unavailable: number = 0;
doSelfTransfer: boolean = false;
doSelfTransferEnabled: boolean = true;
getClaimableError: boolean = false;
isValid: boolean = false;
result: string = '';
Expand Down Expand Up @@ -63,6 +68,7 @@ export class ClaimPanel {
walletExplorer,
disposables,
neoExpressConfig);
this.dispose(); // close the dialog after navigating to the tracker
};

this.panel = vscode.window.createWebviewPanel(
Expand Down Expand Up @@ -100,6 +106,9 @@ export class ClaimPanel {
try {
const walletConfig = this.viewState.wallets.filter(_ => _.address === this.viewState.walletAddress)[0];
if (await walletConfig.unlock()) {
if (this.viewState.doSelfTransfer) {
await this.doSelfTransfer(walletConfig);
}
const api = new neon.api.neoCli.instance(this.rpcUri);
const config: any = {
api: api,
Expand Down Expand Up @@ -129,6 +138,74 @@ export class ClaimPanel {
}
}

private async doSelfTransfer(walletConfig: IWallet) {
if (this.viewState.walletAddress) {
const walletAddress = this.viewState.walletAddress;
const getUnspentsResult = await this.rpcConnection.getUnspents(walletAddress, this);
if (getUnspentsResult.assets && getUnspentsResult.assets['NEO']) {
const neoUnspents = getUnspentsResult.assets['NEO'].unspent;
if (neoUnspents && neoUnspents.length) {
let sum = new bignumber.BigNumber(0);
for (let i = 0; i < neoUnspents.length; i++) {
sum = sum.plus(neoUnspents[i].value as bignumber.BigNumber);
}
const totalNeo = sum.toNumber();
const api = new neon.api.neoCli.instance(this.rpcUri);
const config: any = {
api: api,
account: walletConfig.account,
signingFunction: walletConfig.signingFunction,
intents: neon.api.makeIntent({ 'NEO': totalNeo }, walletAddress),
};
if (walletConfig.isMultiSig) {
// The neon.default.sendAsset function expects the config.account property to be present and
// a regular (non-multisig) account object (so we arbitrarily provide the fist account in
// the multisig group); however it also uses config.account.address when looking up the available
// balance. So we manually lookup the available balance (using the multisig address) and then
// pass it in (thus avoiding the balance lookup within sendAsset).
config.balance = await api.getBalance(walletAddress);
}
const result = await neon.default.sendAsset(config);
if (result.response && result.response.txid) {
return new Promise((resolve, reject) => {
const initialClaimable = this.viewState.claimable;
let attempts = 0;
const resolveWhenClaimableIncreases = async () => {
attempts++;
if (attempts > 15) {
console.error('ClaimPanel timed out waiting for self-transfer to confirm; continuing with claim');
resolve();
} else {
try {
const unclaimed = await this.rpcConnection.getUnclaimed(walletAddress, this);
if (!unclaimed.getUnclaimedSupport) {
reject('No longer able to determine unclaimed GAS');
} else if (unclaimed.available > initialClaimable) {
resolve();
} else {
setTimeout(resolveWhenClaimableIncreases, 2000);
}
} catch (e) {
reject('Error determining unclaimed GAS');
}
}
};
resolveWhenClaimableIncreases();
});
} else {
console.error('ClaimPanel self-transfer failed', result, getUnspentsResult, this.viewState);
}
} else {
console.error('ClaimPanel skipping self-transfer (no unspent NEO)', getUnspentsResult, this.viewState);
}
} else {
console.error('ClaimPanel skipping self-transfer (no unspents)', getUnspentsResult, this.viewState);
}
} else {
console.error('ClaimPanel skipping self-transfer (no wallet sleected)', this.viewState);
}
}

private onClose() {
this.dispose();
}
Expand Down Expand Up @@ -178,14 +255,30 @@ export class ClaimPanel {
this.viewState.claimable = 0;
const walletConfig = this.viewState.wallets.filter(_ => _.address === this.viewState.walletAddress)[0];
const walletAddress = walletConfig ? walletConfig.address : undefined;
this.viewState.doSelfTransferEnabled = false;
if (walletAddress) {
this.viewState.doSelfTransferEnabled = true;
try {
const claimable = await this.rpcConnection.getClaimable(walletAddress, this);
this.viewState.claimable = claimable.unclaimed || 0;
this.viewState.getClaimableError = !claimable.getClaimableSupport;
const unclaimed = await this.rpcConnection.getUnclaimed(walletAddress, this);
this.viewState.claimable = unclaimed.available || 0;
this.viewState.unavailable = unclaimed.unavailable || 0;
this.viewState.getClaimableError = !unclaimed.getUnclaimedSupport;
if (this.viewState.getClaimableError) {
this.viewState.doSelfTransfer = false;
this.viewState.doSelfTransferEnabled = false;
} else {
if ((this.viewState.claimable === 0) && (this.viewState.unavailable > 0)) {
this.viewState.doSelfTransfer = true;
this.viewState.doSelfTransferEnabled = false;
} else if (this.viewState.unavailable === 0) {
this.viewState.doSelfTransfer = false;
this.viewState.doSelfTransferEnabled = false;
}
}
} catch (e) {
console.error('ClaimPanel could not query claimable GAS', walletAddress, this.rpcUri, e);
this.viewState.claimable = 0;
this.viewState.unavailable = 0;
this.viewState.getClaimableError = true;
}
}
Expand All @@ -198,7 +291,8 @@ export class ClaimPanel {

this.viewState.isValid =
!!this.viewState.walletAddress &&
((this.viewState.claimable > 0) || this.viewState.getClaimableError);
!this.viewState.getClaimableError &&
((this.viewState.claimable > 0) || (this.viewState.unavailable > 0));

this.initialized = true;
}
Expand Down
1 change: 1 addition & 0 deletions src/deployPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class DeployPanel {
walletExplorer,
disposables,
neoExpressConfig);
this.dispose(); // close the dialog after navigating to the tracker
};

this.panel = vscode.window.createWebviewPanel(
Expand Down
65 changes: 57 additions & 8 deletions src/neoTrackerPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,41 @@ class ViewState {
public searchCompletions: any[] = [];
}

class PanelCache {

private readonly cache: Map<string, NeoTrackerPanel[]> = new Map<string, NeoTrackerPanel[]>();

public add(panel: NeoTrackerPanel, rpcConnection: INeoRpcConnection, neoExpressConfig?: NeoExpressConfig) {
const key = PanelCache.makeKey(rpcConnection, neoExpressConfig);
let panels = this.cache.get(key);
if (!panels) {
panels = [];
this.cache.set(key, panels);
}
panels.push(panel);
}

public remove(panel: NeoTrackerPanel, rpcConnection: INeoRpcConnection, neoExpressConfig?: NeoExpressConfig) {
const key = PanelCache.makeKey(rpcConnection, neoExpressConfig);
const panels = this.cache.get(key);
if (panels) {
this.cache.set(key, panels.filter(p => p !== panel));
}
}

public get(rpcConnection: INeoRpcConnection, neoExpressConfig?: NeoExpressConfig): NeoTrackerPanel | undefined {
const key = PanelCache.makeKey(rpcConnection, neoExpressConfig);
const panels = this.cache.get(key);
if (panels && panels.length) {
return panels[0];
}
}

private static makeKey(rpcConnection: INeoRpcConnection, neoExpressConfig?: NeoExpressConfig): string {
return rpcConnection.rpcUrl + ':' + (neoExpressConfig ? neoExpressConfig.neoExpressJsonFullPath : 'non-neo-express');
}
}

export class NeoTrackerPanel implements INeoSubscription, INeoStatusReceiver {
public readonly panel: vscode.WebviewPanel;
public readonly ready: Promise<void>;
Expand All @@ -52,6 +87,8 @@ export class NeoTrackerPanel implements INeoSubscription, INeoStatusReceiver {
private rpcConnection: INeoRpcConnection;
private isPageLoading: boolean;

private static panelCache = new PanelCache();

public static async newSearch(
query: string,
extensionPath: string,
Expand All @@ -62,14 +99,18 @@ export class NeoTrackerPanel implements INeoSubscription, INeoStatusReceiver {
disposables: vscode.Disposable[],
neoExpressConfig?: NeoExpressConfig) {

const panel = new NeoTrackerPanel(
extensionPath,
rpcConnection,
historyId,
state,
walletExplorer,
disposables,
neoExpressConfig);
let panel = this.panelCache.get(rpcConnection, neoExpressConfig);
if (!panel) {
panel = new NeoTrackerPanel(
extensionPath,
rpcConnection,
historyId,
state,
walletExplorer,
disposables,
neoExpressConfig);
}
panel.focus();
await panel.search(query);
}

Expand Down Expand Up @@ -118,6 +159,8 @@ export class NeoTrackerPanel implements INeoSubscription, INeoStatusReceiver {
this.panel.webview.html = htmlFileContents
.replace(JavascriptHrefPlaceholder, javascriptHref)
.replace(CssHrefPlaceholder, cssHref);

NeoTrackerPanel.panelCache.add(this, this.rpcConnection, this.neoExpressConfig);
}

public async onNewBlock(blockchainInfo: BlockchainInfo) {
Expand All @@ -134,6 +177,10 @@ export class NeoTrackerPanel implements INeoSubscription, INeoStatusReceiver {
this.panel.webview.postMessage({ status: { message: status, isLoading: this.isPageLoading } });
}

public focus() {
this.panel.reveal();
}

private async augmentSearchHistory(query: string) {
query = (query + '').replace(/^0x/, '');
if (query.length) {
Expand Down Expand Up @@ -193,7 +240,9 @@ export class NeoTrackerPanel implements INeoSubscription, INeoStatusReceiver {
}

private onClose() {
NeoTrackerPanel.panelCache.remove(this, this.rpcConnection, this.neoExpressConfig);
this.rpcConnection.unsubscribe(this);
this.dispose();
}

private async search(query: string) {
Expand Down
13 changes: 9 additions & 4 deletions src/panels/claim.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ <h1>Claim GAS</h1>
<div class="select"><select id="walletDropdown"></select></div>
</div>
<div class="error" id="errorClaimableRetrievalFailure">
There was an error when checking claimable GAS for
<span class="display-wallet emphasis"></span>.
Is Neo Express running?
There was an error when checking claimable GAS for this account.
</div>
<div class="error" id="errorWalletHasNoClaimableGas">
The
Expand All @@ -55,7 +53,14 @@ <h1>Claim GAS</h1>
</div>
<div id="claimableInfo">
The <span class="display-wallet emphasis"></span> account
can claim <span class="display-claimable"></span> GAS.
can immediately claim <span class="display-claimable"></span> GAS.
</div>
<div id="unavailableInfo">
<label>
<input type="checkbox" id="doSelfTransfer" />
Claim <span class="display-unavailable"></span> additional GAS by first self-transferring all
NEO (slower).
</label>
</div>

<div id="error-message" class="result-text error m-t">
Expand Down
16 changes: 14 additions & 2 deletions src/panels/claim.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function populateWalletDropdown() {
function render() {
const claimButton = document.querySelector(claimSelectors.ClaimButton) as HTMLButtonElement;
const walletDropdown = document.querySelector(claimSelectors.WalletDropdown) as HTMLSelectElement;
const doTransferCheckbox = document.querySelector(claimSelectors.DoSelfTransferCheckbox) as HTMLInputElement;
populateWalletDropdown();
if (viewState.showSuccess) {
const resultPlaceholder = document.querySelector(claimSelectors.SearchLinkPlaceholder) as HTMLElement;
Expand All @@ -66,14 +67,18 @@ function render() {
}
htmlHelpers.setPlaceholder(claimSelectors.DisplayWallet, htmlHelpers.text(viewState.walletDescription || '(unknown)'));
htmlHelpers.setPlaceholder(claimSelectors.DisplayClaimable, htmlHelpers.text(htmlHelpers.number(viewState.claimable || 0)));
htmlHelpers.setPlaceholder(claimSelectors.DisplayUnavailable, htmlHelpers.text(viewState.getClaimableError ? '' : htmlHelpers.number(viewState.unavailable || 0)));
htmlHelpers.setPlaceholder(claimSelectors.ErrorMessage, htmlHelpers.text(viewState.result));
htmlHelpers.showHide(claimSelectors.ErrorClaimableRetrievalFailure, viewState.getClaimableError);
htmlHelpers.showHide(claimSelectors.ErrorWalletHasNoClaimableGas, viewState.walletAddress && !viewState.showError && !(viewState.getClaimableError || (viewState.claimable > 0)));
htmlHelpers.showHide(claimSelectors.ClaimableInfo, viewState.claimable > 0);
htmlHelpers.showHide(claimSelectors.ErrorWalletHasNoClaimableGas, viewState.walletAddress && !viewState.showError && !(viewState.getClaimableError || (viewState.claimable > 0) || (viewState.unavailable > 0)));
htmlHelpers.showHide(claimSelectors.ClaimableInfo, !!viewState.walletAddress && !viewState.getClaimableError);
htmlHelpers.showHide(claimSelectors.UnavailableInfo, !!viewState.walletAddress && !viewState.getClaimableError);
htmlHelpers.showHide(claimSelectors.ErrorMessage, viewState.showError);
htmlHelpers.showHide(claimSelectors.ViewResults, viewState.showSuccess);
htmlHelpers.showHide(claimSelectors.ViewDataEntry, !viewState.showSuccess);
htmlHelpers.showHide(claimSelectors.ErrorMessage, !!viewState.showError);
doTransferCheckbox.checked = viewState.doSelfTransfer;
doTransferCheckbox.disabled = !viewState.doSelfTransferEnabled;
claimButton.disabled = !viewState.isValid;
walletDropdown.disabled = false;
}
Expand All @@ -94,6 +99,7 @@ function initializePanel() {
const closeButton = document.querySelector(claimSelectors.CloseButton) as HTMLButtonElement;
const refreshLink = document.querySelector(claimSelectors.RefreshClaimableLink) as HTMLAnchorElement;
const walletDropdown = document.querySelector(claimSelectors.WalletDropdown) as HTMLSelectElement;
const doTransferCheckbox = document.querySelector(claimSelectors.DoSelfTransferCheckbox) as HTMLInputElement;
claimButton.addEventListener('click', _ => {
enterLoadingState();
vscode.postMessage({ e: claimEvents.Claim });
Expand All @@ -118,6 +124,12 @@ function initializePanel() {
}
console.log('->', viewState);
});
doTransferCheckbox.addEventListener('change', _ => {
viewState.doSelfTransfer = doTransferCheckbox.checked;
enterLoadingState();
vsCodePostMessage({ e: claimEvents.Update, c: viewState });
console.log('->', viewState);
});
window.addEventListener('message', msg => handleMessage(msg.data));
enterLoadingState();
vscode.postMessage({ e: claimEvents.Init });
Expand Down
3 changes: 3 additions & 0 deletions src/panels/claimSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ const claimSelectors = {
ClaimableInfo: '#claimableInfo',
ClaimButton: '#claim',
CloseButton: '#close',
DisplayUnavailable: '.display-unavailable',
DisplayClaimable: '.display-claimable',
DisplayWallet: '.display-wallet',
DoSelfTransferCheckbox: '#doSelfTransfer',
ErrorClaimableRetrievalFailure: '#errorClaimableRetrievalFailure',
ErrorMessage: '#error-message',
ErrorWalletHasNoClaimableGas: '#errorWalletHasNoClaimableGas',
LoadingIndicator: '.loading',
RefreshClaimableLink: '#refreshClaimableLink',
ResultText: '.result-text',
SearchLinkPlaceholder: '#searchLink',
UnavailableInfo: '#unavailableInfo',
ViewDataEntry: '#data-entry-view',
ViewResults: '#results-view',
WalletDropdown: '#walletDropdown',
Expand Down
Loading

0 comments on commit 8d7ebf9

Please sign in to comment.