diff --git a/package-lock.json b/package-lock.json index 950e4151..1d92faf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2454,9 +2454,9 @@ } }, "@open-rpc/client-js": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@open-rpc/client-js/-/client-js-1.7.0.tgz", - "integrity": "sha512-cRGJbXTgdhJNU49vWzJIATRmKBLP2x6tuHJzX9Jg3N8f1VEkge0riUEek2LFIrZiM4TdUp8XV4Ns1W0SZzdfSw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@open-rpc/client-js/-/client-js-1.7.1.tgz", + "integrity": "sha512-DycSYZUGSUwFl+k9T8wLBSGA8f2hYkvS5A9AB94tBOuU8QlP468NS5ZtAxy72dF4g2WW0genwNJdfeFnHnaxXQ==", "requires": { "isomorphic-fetch": "^3.0.0", "isomorphic-ws": "^4.0.1", @@ -3915,6 +3915,11 @@ "acorn-walk": "^7.1.1" } }, + "acorn-import-assertions": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz", + "integrity": "sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA==" + }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -6105,9 +6110,9 @@ "dev": true }, "casper-js-sdk": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/casper-js-sdk/-/casper-js-sdk-2.2.3.tgz", - "integrity": "sha512-e8vyktJBsSBh8lJzwwbWAgP7mTHGC04iE4Fhd7sdYCYClWjKv+XQ22aXNwawWYNOxHqipF7n7Z319F9dEhxQhg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/casper-js-sdk/-/casper-js-sdk-2.5.0.tgz", + "integrity": "sha512-26IlA3dZRqiw4rMPJMMFS/fEUneJ4HmM2z7/tjoLqnVCr6YWayTLX+yqFqw+8Iz5ehx1SEvxhXt40OmUO+/z7A==", "requires": { "@ethersproject/bignumber": "^5.0.8", "@ethersproject/bytes": "^5.0.5", @@ -6265,9 +6270,9 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "jest-worker": { - "version": "27.0.6", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.6.tgz", - "integrity": "sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA==", + "version": "27.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.1.0.tgz", + "integrity": "sha512-mO4PHb2QWLn9yRXGp7rkvXLAYuxwhq1ZYUo0LoDhg8wqvv4QizP1ZWEJOeolgbEgAWZLIEU0wsku8J+lGWfBhg==", "requires": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -6324,9 +6329,9 @@ "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==" }, "terser": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.1.tgz", - "integrity": "sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.2.tgz", + "integrity": "sha512-0Omye+RD4X7X69O0eql3lC4Heh/5iLj3ggxR/B5ketZLOtLiOqukUgjw3q4PDnNQbsrkKr3UMypqStQG3XKRvw==", "requires": { "commander": "^2.20.0", "source-map": "~0.7.2", @@ -6363,9 +6368,9 @@ } }, "webpack": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.46.0.tgz", - "integrity": "sha512-qxD0t/KTedJbpcXUmvMxY5PUvXDbF8LsThCzqomeGaDlCA6k998D8yYVwZMvO8sSM3BTEOaD4uzFniwpHaTIJw==", + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.51.1.tgz", + "integrity": "sha512-xsn3lwqEKoFvqn4JQggPSRxE4dhsRcysWTqYABAZlmavcoTmwlOb9b1N36Inbt/eIispSkuHa80/FJkDTPos1A==", "requires": { "@types/eslint-scope": "^3.7.0", "@types/estree": "^0.0.50", @@ -6373,6 +6378,7 @@ "@webassemblyjs/wasm-edit": "1.11.1", "@webassemblyjs/wasm-parser": "1.11.1", "acorn": "^8.4.1", + "acorn-import-assertions": "^1.7.6", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.8.0", @@ -6389,17 +6395,13 @@ "tapable": "^2.1.1", "terser-webpack-plugin": "^5.1.3", "watchpack": "^2.2.0", - "webpack-sources": "^2.3.1" + "webpack-sources": "^3.2.0" } }, "webpack-sources": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", - "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", - "requires": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - } + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.0.tgz", + "integrity": "sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==" } } }, @@ -15368,12 +15370,25 @@ } }, "keccak": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", - "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.2.tgz", + "integrity": "sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==", "requires": { "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" + "node-gyp-build": "^4.2.0", + "readable-stream": "^3.6.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "key-encoder": { diff --git a/package.json b/package.json index 37614f7d..898e9619 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "casperlabs-signer", - "version": "1.3.0", + "version": "1.4.0", "private": true, "dependencies": { "@babel/core": "^7.14.6", @@ -21,7 +21,7 @@ "@types/react-dom": "^16.9.14", "axios": "^0.21.1", "browser-passworder": "^2.0.3", - "casper-js-sdk": "^2.2.3", + "casper-js-sdk": "^2.5.1", "copy-to-clipboard": "^3.3.1", "download": "^8.0.0", "eslint-config-react-app": "^6.0.0", @@ -50,6 +50,7 @@ "tweetnacl": "^1.0.3", "tweetnacl-ts": "latest", "tweetnacl-util": "^0.15.1", + "typedjson": "^1.7.0", "validator": "^12.2.0", "webpack": "4.44.2", "webpack-extension-reloader": "^1.1.4", diff --git a/public/manifest.json b/public/manifest.json index 1863e3f6..5b5a8ee3 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { "manifest_version": 2, - "version": "1.3.0", + "version": "1.4.0", "name": "CasperLabs Signer", "author": "https://casperlabs.io", "description": "CasperLabs Signer tool for signing transactions on the blockchain.", @@ -28,7 +28,9 @@ "*://casperholders.io/*", "*://*.casperholders.io/*", "*://casperholders.com/*", - "*://*.casperholders.com/*" + "*://*.casperholders.com/*", + "*://*.casperstats.io/*", + "*://casperstats.io/*" ], "js": ["./scripts/content/content.js"], "run_at": "document_start", diff --git a/src/background/AuthController.ts b/src/background/AuthController.ts index d1d9d051..dbef4818 100644 --- a/src/background/AuthController.ts +++ b/src/background/AuthController.ts @@ -616,6 +616,11 @@ class AuthController { async clearAccount() { this.appState.userAccounts.clear(); } + + @action.bound + configureTimeout(durationMins: number) { + this.appState.idleTimeoutMins = durationMins; + } } export default AuthController; diff --git a/src/background/PopupManager.ts b/src/background/PopupManager.ts index 52cbe18e..34e663cb 100644 --- a/src/background/PopupManager.ts +++ b/src/background/PopupManager.ts @@ -2,8 +2,21 @@ import { browser } from 'webextension-polyfill-ts'; -export type openPurpose = 'connect' | 'sign' | 'importAccount' | 'noAccount'; +export type openPurpose = + | 'connect' + | 'signDeploy' + | 'signMessage' + | 'importAccount' + | 'noAccount'; +const normalPopupWidth = 300; +const normalPopupHeight = 480; +const expandedPopupHeight = 820; +// Pads around popup window +const popupBuffer = { + right: 20, + top: 40 +}; /** * A Class to manager Popup * Provide inject and background a way to show popup. @@ -13,11 +26,8 @@ export default class PopupManager { browser.windows .getCurrent() .then(window => { - let popupWidth = 300; - let bufferRight = 20; - let bufferTop = 40; let windowWidth = - window.width === undefined || null ? 300 : window.width; + window.width === undefined || null ? normalPopupWidth : window.width; let xOffset = window.left === undefined || null ? 0 : window.left; let yOffset = window.top === undefined || null ? 0 : window.top; browser.windows.create({ @@ -26,10 +36,11 @@ export default class PopupManager { ? 'index.html?#/import' : 'index.html?#/', type: 'popup', - height: openFor === 'sign' ? 820 : 480, - width: 300, - left: windowWidth + xOffset - popupWidth - bufferRight, - top: yOffset + bufferTop + height: + openFor === 'signDeploy' ? expandedPopupHeight : normalPopupHeight, + width: normalPopupWidth, + left: windowWidth + xOffset - normalPopupWidth - popupBuffer.right, + top: yOffset + popupBuffer.top }); }) .catch(() => { @@ -37,7 +48,7 @@ export default class PopupManager { if (openFor === 'connect') { title = 'Connection Request'; message = 'Open Signer to Approve or Reject Connection'; - } else if (openFor === 'sign') { + } else if (openFor === 'signDeploy' || openFor === 'signMessage') { title = 'Signature Request'; message = 'Open Signer to Approve or Cancel Signing'; } else { diff --git a/src/background/SignMessageManager.ts b/src/background/SigningManager.ts similarity index 66% rename from src/background/SignMessageManager.ts rename to src/background/SigningManager.ts index 9b896fb8..ae441e1c 100644 --- a/src/background/SignMessageManager.ts +++ b/src/background/SigningManager.ts @@ -1,17 +1,31 @@ import * as events from 'events'; import { AppState } from '../lib/MemStore'; -import PopupManager from '../background/PopupManager'; +import PopupManager from './PopupManager'; import { DeployUtil, encodeBase16, CLPublicKey, CLPublicKeyType, CLByteArrayType, - CLAccountHashType + CLAccountHashType, + formatMessageWithHeaders, + signFormattedMessage } from 'casper-js-sdk'; import { JsonTypes } from 'typedjson'; export type deployStatus = 'unsigned' | 'signed' | 'failed'; type argDict = { [key: string]: string }; + +export interface messageWithID { + id: number; + messageBytes: Uint8Array; + messageString: string; + signingKey: string; + signature?: Uint8Array; + status: deployStatus; + error?: Error; + pushed?: boolean; +} + export interface deployWithID { id: number; status: deployStatus; @@ -35,33 +49,16 @@ export interface DeployData { deployArgs: Object; } -// Covers Delegating and Undelegating - -/** - * Sign Message Manager - * TODO: Update these docs - * Algorithm: - * 1. Injected script call `SignMessageManager.addUnsignedMessageAsync`, we return a Promise, inside the Promise, we will - * construct a message and assign it a unique id msgId and then we set up a event listen for `${msgId}:finished`. - * Resolve or reject when the event emits. - * 2. Popup call `SignMessageManager.{rejectMsg|approveMsg}` either to reject or commit the signature request, - * and both methods will fire a event `${msgId}:finished`, which is listened by step 1. - * - * Important to Note: - * Any mention of CLPublicKey below will refer to the hex-encoded bytes of the Public Key prefixed with 01 or 02 - * to denote the algorithm used to generate the key. - * 01 - ed25519 - * 02 - secp256k1 - * - */ -export default class SignMessageManager extends events.EventEmitter { +export default class SigningManager extends events.EventEmitter { private unsignedDeploys: deployWithID[]; + private unsignedMessages: messageWithID[]; private nextId: number; private popupManager: PopupManager; constructor(private appState: AppState) { super(); this.unsignedDeploys = []; + this.unsignedMessages = []; this.nextId = Math.round(Math.random() * Number.MAX_SAFE_INTEGER); this.popupManager = new PopupManager(); } @@ -95,6 +92,13 @@ export default class SignMessageManager extends events.EventEmitter { ...d, pushed: true })); + this.appState.unsignedMessages.replace( + this.unsignedMessages.filter(d => !d.pushed) + ); + this.unsignedMessages = this.unsignedMessages.map(d => ({ + ...d, + pushed: true + })); } /** @@ -190,7 +194,7 @@ export default class SignMessageManager extends events.EventEmitter { sourcePublicKeyHex, targetPublicKeyHex ); - this.popupManager.openPopup('sign'); + this.popupManager.openPopup('signDeploy'); // Await outcome of user interaction with popup. this.once(`${deployId}:finished`, (processedDeploy: deployWithID) => { if (!this.appState.isUnlocked) { @@ -286,6 +290,15 @@ export default class SignMessageManager extends events.EventEmitter { return deployWithId; } + private getMessageById(messageId: number): messageWithID { + const messageWithId = this.appState.unsignedMessages.find( + msgWithId => msgWithId.id === messageId + ); + if (!messageWithId) + throw new Error(`Could not find message with id: ${messageId}`); + return messageWithId; + } + public parseDeployData(deployId: number): DeployData { let deployWithID = this.getDeployById(deployId); if (deployWithID !== undefined && deployWithID.deploy !== undefined) { @@ -392,6 +405,146 @@ export default class SignMessageManager extends events.EventEmitter { } } + /** + * Sign a message. + * @param message The string message to be signed. + * @param signingPublicKey The key for signing (in hex format). + * @returns `Base16` encoded signature. + */ + public signMessage(message: string, signingPublicKey: string) { + return new Promise((resolve, reject) => { + // TODO: Need to abstract it to reusable method + const { currentTab, connectedSites } = this.appState; + const connected = + currentTab && + connectedSites.some( + site => site.url === currentTab.url && site.isConnected + ); + if (!connected) return reject('This site is not connected'); + + if (!message || !signingPublicKey) + throw new Error('Message or public key was null/undefined'); + if ( + this.appState.userAccounts.some( + account => account.KeyPair.publicKey.toHex() !== signingPublicKey + ) + ) + throw new Error('Provided key is not present in vault.'); + const activeKeyPair = this.appState.activeUserAccount?.KeyPair; + if (!activeKeyPair) throw new Error('No active account'); + if (activeKeyPair.publicKey.toHex() !== signingPublicKey) + throw new Error( + 'Provided key is not set as Active Key - please set it and try again.' + ); + + const messageId = this.createId(); + let messageBytes; + try { + messageBytes = formatMessageWithHeaders(message); + } catch (err) { + throw new Error('Could not format message: ' + err); + } + try { + this.unsignedMessages.push({ + id: messageId, + messageBytes: messageBytes, + messageString: message, + signingKey: signingPublicKey, + status: 'unsigned' + }); + } catch (err) { + throw new Error(err); + } + + this.updateAppState(); + this.popupManager.openPopup('signMessage'); + this.once(`${messageId}:finished`, (processedMessage: messageWithID) => { + if (!this.appState.isUnlocked) { + return reject( + new Error( + `Signer locked during signing process, please unlock and try again.` + ) + ); + } + switch (processedMessage.status) { + case 'signed': + if (processedMessage.messageBytes) { + this.appState.unsignedMessages.remove(processedMessage); + if (activeKeyPair !== this.appState.activeUserAccount?.KeyPair) + throw new Error('Active account changed during signing.'); + const signature = signFormattedMessage( + activeKeyPair, + processedMessage.messageBytes + ); + return resolve(encodeBase16(signature)); + } else { + this.appState.unsignedMessages.remove(processedMessage); + return reject(new Error(processedMessage.error?.message)); + } + case 'failed': + this.unsignedMessages = this.unsignedMessages.filter( + d => d.id !== processedMessage.id + ); + return reject( + new Error( + processedMessage.error?.message! ?? 'User Cancelled Signing' + ) + ); + default: + return reject(new Error(`Signer: Unknown error occurred`)); + } + }); + }); + } + + public async approveSigningMessage(messageId: number) { + const messageWithId = this.getMessageById(messageId); + if (!this.appState.activeUserAccount) { + throw new Error(`No Active Account!`); + } + let activeKeyPair = this.appState.activeUserAccount.KeyPair; + if (!messageWithId.messageBytes || !messageWithId.messageString) { + messageWithId.error = new Error( + `Cannot sign message: ${ + !messageWithId.messageBytes + ? 'message bytes were null' + : !messageWithId.messageString + ? 'message string was null' + : '' + }` + ); + this.saveAndEmitEventIfNeeded(messageWithId); + return; + } + + // Reject if user switches keys during signing process + if ( + messageWithId.signingKey && + activeKeyPair.publicKey.toHex() !== messageWithId.signingKey + ) { + messageWithId.status = 'failed'; + messageWithId.error = new Error('Active key changed during signing'); + this.saveAndEmitEventIfNeeded(messageWithId); + return; + } + + messageWithId.signature = signFormattedMessage( + activeKeyPair, + messageWithId.messageBytes + ); + + messageWithId.status = 'signed'; + this.saveAndEmitEventIfNeeded(messageWithId); + } + + public async cancelSigningMessage(messageId: number) { + const messageWithId = this.getMessageById(messageId); + messageWithId.status = 'failed'; + messageWithId.error = new Error('User Cancelled Signing'); + this.appState.unsignedMessages.remove(messageWithId); + this.saveAndEmitEventIfNeeded(messageWithId); + } + private verifyTargetAccountMatch( publicKeyHex: string, targetAccountHash: string @@ -436,12 +589,27 @@ export default class SignMessageManager extends events.EventEmitter { return transferArgs; } - private saveAndEmitEventIfNeeded(deployWithId: deployWithID) { - let status = deployWithId.status; - this.updateDeployWithId(deployWithId); + private saveAndEmitEventIfNeeded(itemWithId: deployWithID | messageWithID) { + let status = itemWithId.status; + const isDeployWithId = ( + object: deployWithID | messageWithID + ): object is deployWithID => { + return (object as deployWithID).deploy !== undefined; + }; + const isMessageWithId = ( + object: deployWithID | messageWithID + ): object is messageWithID => { + return (object as messageWithID).messageBytes !== undefined; + }; + + if (isDeployWithId(itemWithId)) { + this.updateDeployWithId(itemWithId); + } else if (isMessageWithId(itemWithId)) { + this.updateMessageWithId(itemWithId); + } if (status === 'failed' || status === 'signed') { // fire finished event, so that the Promise can resolve and return result to RPC caller - this.emit(`${deployWithId.id}:finished`, deployWithId); + this.emit(`${itemWithId.id}:finished`, itemWithId); } } @@ -450,9 +618,24 @@ export default class SignMessageManager extends events.EventEmitter { deployData => deployData.id === deployWithId.id ); if (index === -1) { - throw new Error(`Could not find message with id: ${deployWithId.id}`); + throw new Error( + `Could not find deploy in queue with id: ${deployWithId.id}` + ); } this.unsignedDeploys[index] = deployWithId; this.updateAppState(); } + + private updateMessageWithId(messageWithId: messageWithID) { + const index = this.unsignedMessages.findIndex( + messageData => messageData.id === messageWithId.id + ); + if (index === -1) { + throw new Error( + `Could not find message in queue with id: ${messageWithId.id}` + ); + } + this.unsignedMessages[index] = messageWithId; + this.updateAppState(); + } } diff --git a/src/background/background.ts b/src/background/background.ts index 56bc8153..7d3b42d4 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -3,14 +3,14 @@ import { browser } from 'webextension-polyfill-ts'; import { Rpc } from '../lib/rpc/rpc'; import { AppState } from '../lib/MemStore'; import { autorun } from 'mobx'; -import SignMessageManager from './SignMessageManager'; +import SigningManager from './SigningManager'; import ConnectionManager from './ConnectionManager'; import { updateBadge } from './utils'; import { setupInjectPageAPIServer } from '../lib/rpc/Provider'; const appState = new AppState(); const accountController = new AccountController(appState); -const signMessageManager = new SignMessageManager(appState); +const signingManager = new SigningManager(appState); const connectionManager = new ConnectionManager(appState); initialize().catch(console.log); @@ -18,7 +18,7 @@ initialize().catch(console.log); async function initialize() { await setupPopupAPIServer(); // Setup RPC server for inject page - setupInjectPageAPIServer(signMessageManager, connectionManager); + setupInjectPageAPIServer(signingManager, connectionManager); } // Setup RPC server for Popup @@ -110,15 +110,23 @@ async function setupPopupAPIServer() { }); rpc.register( 'sign.signDeploy', - signMessageManager.approveSignDeploy.bind(signMessageManager) + signingManager.approveSignDeploy.bind(signingManager) ); rpc.register( 'sign.rejectSignDeploy', - signMessageManager.rejectSignDeploy.bind(signMessageManager) + signingManager.rejectSignDeploy.bind(signingManager) ); rpc.register( 'sign.parseDeployData', - signMessageManager.parseDeployData.bind(signMessageManager) + signingManager.parseDeployData.bind(signingManager) + ); + rpc.register( + 'sign.approveSigningMessage', + signingManager.approveSigningMessage.bind(signingManager) + ); + rpc.register( + 'sign.cancelSigningMessage', + signingManager.cancelSigningMessage.bind(signingManager) ); rpc.register( 'connection.requestConnection', @@ -152,4 +160,8 @@ async function setupPopupAPIServer() { 'connection.isIntegratedSite', connectionManager.isIntegratedSite.bind(connectionManager) ); + rpc.register( + 'account.configureTimeout', + accountController.configureTimeout.bind(accountController) + ); } diff --git a/src/background/utils.ts b/src/background/utils.ts index 19d2a12a..314f56e4 100644 --- a/src/background/utils.ts +++ b/src/background/utils.ts @@ -3,8 +3,13 @@ import { browser } from 'webextension-polyfill-ts'; export function updateBadge(appState: AppState) { let label = ''; - let count = appState.unsignedDeploys.length; - if (appState.connectionRequested) { + let count = + appState.unsignedDeploys.length > 0 + ? appState.unsignedDeploys.length + : appState.unsignedMessages.length > 0 + ? appState.unsignedMessages.length + : undefined; + if (appState.connectionRequested && !appState.connectionStatus) { label = '1'; } else if (count) { label = String(count); diff --git a/src/content/inpage.ts b/src/content/inpage.ts index b061ae01..7289c12f 100644 --- a/src/content/inpage.ts +++ b/src/content/inpage.ts @@ -31,6 +31,10 @@ class CasperLabsPluginHelper { return this.call('sign', deploy, sourcePublicKey, targetPublicKey); } + async signMessage(message: string, signingPublicKey: string) { + return this.call('signMessage', message, signingPublicKey); + } + async getActivePublicKey() { return this.call('getActivePublicKey'); } diff --git a/src/index.tsx b/src/index.tsx index ecbff058..ddc6541d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,7 +13,7 @@ import { BackgroundManager } from './popup/BackgroundManager'; import ErrorContainer from './popup/container/ErrorContainer'; import './popup/styles/custom.scss'; import { AppState } from './lib/MemStore'; -import SignMessageContainer from './popup/container/SignMessageContainer'; +import SigningContainer from './popup/container/SigningContainer'; import ConnectSignerContainer from './popup/container/ConnectSignerContainer'; import { signerTheme } from './popup/components/Theme'; import { ThemeProvider } from '@material-ui/core'; @@ -26,10 +26,7 @@ const authContainer = new AccountManager( backgroundManager, appState ); -const signMessageContainer = new SignMessageContainer( - backgroundManager, - appState -); +const signingContainer = new SigningContainer(backgroundManager, appState); const connectSignerContainer = new ConnectSignerContainer( backgroundManager, appState @@ -44,7 +41,7 @@ ReactDOM.render( errors={errorsContainer} authContainer={authContainer} homeContainer={homeContainer} - signMessageContainer={signMessageContainer} + signingContainer={signingContainer} connectSignerContainer={connectSignerContainer} popupManager={popupManager} /> diff --git a/src/lib/MemStore.ts b/src/lib/MemStore.ts index 659d1ea6..01e7a5e4 100644 --- a/src/lib/MemStore.ts +++ b/src/lib/MemStore.ts @@ -1,7 +1,7 @@ import { IObservableArray, observable, computed } from 'mobx'; import { Tab, Site } from '../background/ConnectionManager'; import { KeyPairWithAlias } from '../@types/models'; -import { deployWithID } from '../background/SignMessageManager'; +import { deployWithID, messageWithID } from '../background/SigningManager'; export class AppState { @observable isIntegratedSite: boolean = false; @@ -13,7 +13,7 @@ export class AppState { @observable lockoutTimerStarted: boolean = false; timerDurationMins: number = 5; @observable remainingMins: number = this.timerDurationMins; - @observable idleTimeoutMins: number = 1; + @observable idleTimeoutMins: number = 2; @observable currentTab: Tab | null = null; @computed get connectionStatus(): boolean { const url = this.currentTab && this.currentTab.url; @@ -35,4 +35,6 @@ export class AppState { observable.array([], { deep: true }); @observable unsignedDeploys: IObservableArray = observable.array([], { deep: true }); + @observable unsignedMessages: IObservableArray = + observable.array([], { deep: true }); } diff --git a/src/lib/rpc/Provider.ts b/src/lib/rpc/Provider.ts index 7c5b5409..d67ed97a 100644 --- a/src/lib/rpc/Provider.ts +++ b/src/lib/rpc/Provider.ts @@ -1,6 +1,6 @@ import { browser } from 'webextension-polyfill-ts'; import { Rpc } from './rpc'; -import SignMessageManager from '../../background/SignMessageManager'; +import SigningManager from '../../background/SigningManager'; import ConnectionManager from '../../background/ConnectionManager'; /* @@ -34,7 +34,7 @@ let rpc: Rpc; // Setup RPC server for inject page // used in background.ts export function setupInjectPageAPIServer( - signMessageManager: SignMessageManager, + signingManager: SigningManager, connectionManager: ConnectionManager, logMessages: boolean = false ) { @@ -44,10 +44,11 @@ export function setupInjectPageAPIServer( destination: 'page', source: 'background' }); - rpc.register('sign', signMessageManager.signDeploy.bind(signMessageManager)); + rpc.register('sign', signingManager.signDeploy.bind(signingManager)); + rpc.register('signMessage', signingManager.signMessage.bind(signingManager)); rpc.register( 'getActivePublicKey', - signMessageManager.getActivePublicKey.bind(signMessageManager) + signingManager.getActivePublicKey.bind(signingManager) ); rpc.register( 'isConnected', diff --git a/src/popup/App.tsx b/src/popup/App.tsx index 15c4b204..90f5e166 100644 --- a/src/popup/App.tsx +++ b/src/popup/App.tsx @@ -10,8 +10,8 @@ import PopupManager from '../background/PopupManager'; import { HomeContainer } from './container/HomeContainer'; import { observer } from 'mobx-react'; import ErrorContainer from './container/ErrorContainer'; -import SignMessagePage from './components/SignMessagePage'; -import SignMessageContainer from './container/SignMessageContainer'; +import SignDeployPage from './components/SignDeployPage'; +import SigningContainer from './container/SigningContainer'; import ConnectSignerPage from './components/ConnectSignerPage'; import ConnectSignerContainer from './container/ConnectSignerContainer'; import AccountPage from './components/AccountPage'; @@ -22,13 +22,15 @@ import AnalyticsProvider from './components/AnalyticsProvider'; import AccountManagementPage from './components/AccountManagementPage'; import { ConnectedSitesPage } from './components/ConnectedSitesPage'; import IdleTimer from 'react-idle-timer'; +import { SignMessagePage } from './components/SignMessagePage'; +import { ConfigureTimeoutPage } from './components/ConfigureTimeout'; export interface AppProps { errors: ErrorContainer; authContainer: AccountManager; popupManager: PopupManager; homeContainer: HomeContainer; - signMessageContainer: SignMessageContainer; + signingContainer: SigningContainer; connectSignerContainer: ConnectSignerContainer; } @@ -39,10 +41,6 @@ const App = (props: AppProps) => { return (
- {/* TODO - Lockout time is hardcoded in the appState but this could - be made configurable to allow users to set convenient timeouts. - */} { authContainer={props.authContainer} homeContainer={props.homeContainer} connectionContainer={props.connectSignerContainer} + signingContainer={props.signingContainer} popupManager={props.popupManager} errors={props.errors} /> @@ -113,15 +112,20 @@ const App = (props: AppProps) => { )} /> ( - )} /> + SignMessagePage(props.signingContainer)} + /> { /> )} /> + ( + + )} + />
diff --git a/src/popup/BackgroundManager.ts b/src/popup/BackgroundManager.ts index 50ad73c4..4a6df884 100644 --- a/src/popup/BackgroundManager.ts +++ b/src/popup/BackgroundManager.ts @@ -4,7 +4,7 @@ import { AppState } from '../lib/MemStore'; import { action } from 'mobx'; import ErrorContainer from './container/ErrorContainer'; import { KeyPairWithAlias } from '../@types/models'; -import { DeployData } from '../background/SignMessageManager'; +import { DeployData } from '../background/SigningManager'; export class BackgroundManager { private rpc: Rpc; @@ -39,6 +39,8 @@ export class BackgroundManager { this.appState.activeUserAccount = appState.activeUserAccount; this.appState.userAccounts.replace(appState.userAccounts); this.appState.unsignedDeploys.replace(appState.unsignedDeploys); + this.appState.unsignedMessages.replace(appState.unsignedMessages); + this.appState.idleTimeoutMins = appState.idleTimeoutMins; } public unlock(password: string) { @@ -100,6 +102,18 @@ export class BackgroundManager { ); } + public approveSigningMessage(messageId: number) { + return this.errors.withCapture( + this.rpc.call('sign.approveSigningMessage', messageId) + ); + } + + public cancelSigningMessage(messageId: number) { + return this.errors.withCapture( + this.rpc.call('sign.cancelSigningMessage', messageId) + ); + } + public switchToAccount(accountName: string) { return this.errors.withCapture( this.rpc.call('account.switchToAccount', accountName) @@ -202,4 +216,10 @@ export class BackgroundManager { this.rpc.call('connection.isIntegratedSite', hostname) ); } + + public configureTimeout(durationMins: number) { + return this.errors.withCapture( + this.rpc.call('account.configureTimeout', durationMins) + ); + } } diff --git a/src/popup/components/ConfigureTimeout.tsx b/src/popup/components/ConfigureTimeout.tsx new file mode 100644 index 00000000..bc804597 --- /dev/null +++ b/src/popup/components/ConfigureTimeout.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import AccountManager from '../container/AccountManager'; +import Pages from '../components/Pages'; +import { + List, + ListItem, + ListItemIcon, + ListItemText, + Typography +} from '@material-ui/core'; +import { Brightness1 as ActiveIcon } from '@material-ui/icons'; +import { observer } from 'mobx-react'; +import { Redirect } from 'react-router-dom'; + +export const ConfigureTimeoutPage = observer( + (props: { accountManager: AccountManager }) => { + return props.accountManager.isUnLocked ? ( +
+

Set Idle Timeout

+ + Select how long you'd like before the Signer locks due to inactivity. + + + { + await props.accountManager.configureTimeout(1); + }} + > + + {1 === props.accountManager.idleTimeoutMins && ( + + + + )} + + { + await props.accountManager.configureTimeout(2); + }} + > + + {2 === props.accountManager.idleTimeoutMins && ( + + + + )} + + { + await props.accountManager.configureTimeout(5); + }} + > + + {5 === props.accountManager.idleTimeoutMins && ( + + + + )} + + { + await props.accountManager.configureTimeout(10); + }} + > + + {10 === props.accountManager.idleTimeoutMins && ( + + + + )} + + +
+ ) : ( + + ); + } +); diff --git a/src/popup/components/Confirmation.tsx b/src/popup/components/Confirmation.tsx index c879971c..4f6b5862 100644 --- a/src/popup/components/Confirmation.tsx +++ b/src/popup/components/Confirmation.tsx @@ -165,7 +165,6 @@ export function confirm( unmountAfter: 10000 } ) { - console.log(options.unmountAfter); return createConfirmation( confirmable(Confirmation), options.unmountAfter diff --git a/src/popup/components/Home.tsx b/src/popup/components/Home.tsx index c92e48c8..824102ff 100644 --- a/src/popup/components/Home.tsx +++ b/src/popup/components/Home.tsx @@ -26,6 +26,8 @@ import Pages from './Pages'; import { confirm } from './Confirmation'; import { RouteComponentProps, withRouter } from 'react-router'; import { TextFieldWithFormState } from './Forms'; +import SigningContainer from '../container/SigningContainer'; +import { SignMessagePage } from './SignMessagePage'; /* eslint-disable jsx-a11y/anchor-is-valid */ const styles = (theme: Theme) => @@ -73,6 +75,7 @@ interface Props extends RouteComponentProps, WithStyles { authContainer: AccountManager; homeContainer: HomeContainer; connectionContainer: ConnectSignerContainer; + signingContainer: SigningContainer; popupManager: PopupManager; errors: ErrorContainer; } @@ -154,8 +157,9 @@ class Home extends React.Component<
- + + ; } } else { - if (this.props.authContainer.unsignedDeploys.length > 0) { - return ; + if (this.props.signingContainer.deployToSign) { + return ; + } else if (this.props.signingContainer.messageToSign) { + return SignMessagePage(this.props.signingContainer); } else { return this.renderAccountLists(); } diff --git a/src/popup/components/Menu.tsx b/src/popup/components/Menu.tsx index 33afc3b2..729e90f7 100644 --- a/src/popup/components/Menu.tsx +++ b/src/popup/components/Menu.tsx @@ -1,19 +1,28 @@ import React from 'react'; -import AccountManager from '../container/AccountManager'; import { observer } from 'mobx-react'; -import SettingsIcon from '@material-ui/icons/Settings'; -import CheckIcon from '@material-ui/icons/Check'; -import Icon from '@material-ui/core/Icon'; -import Menu from '@material-ui/core/Menu'; -import LockIcon from '@material-ui/icons/Lock'; -import CloudDownloadIcon from '@material-ui/icons/CloudDownload'; -import WebIcon from '@material-ui/icons/Web'; import Pages from './Pages'; -import MenuIcon from '@material-ui/icons/Menu'; -import IconButton from '@material-ui/core/IconButton'; -import { List, ListItem, ListItemText, ListSubheader } from '@material-ui/core'; -import Divider from '@material-ui/core/Divider'; import { Link } from 'react-router-dom'; +import AccountManager from '../container/AccountManager'; +import { + Settings as SettingsIcon, + Check as CheckIcon, + Lock as LockIcon, + CloudDownload as CloudDownloadIcon, + Web as WebIcon, + Menu as MenuIcon, + Timer as TimerIcon +} from '@material-ui/icons'; +import { + Icon, + IconButton, + Menu, + List, + ListItem, + ListItemText, + ListSubheader, + Divider, + Typography +} from '@material-ui/core'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; interface Props { @@ -126,6 +135,20 @@ const MoreMenu = observer((props: Props) => { )} + + + + + {props.authContainer.idleTimeoutMins} min + {props.authContainer.idleTimeoutMins === 1 ? '' : 's'} + + ({ + tooltip: { + fontSize: '.8rem', + width: '260px', + margin: '10px 0 0 0' + } +}); + +const CsprTooltip = withStyles({ + tooltip: { + fontSize: '1rem', + width: 'fit-content', + margin: '10px 0 0 0', + textAlign: 'center' + } +})(Tooltip); + +interface Props extends RouteComponentProps { + signingContainer: SigningContainer; + authContainer: AccountManager; + classes: Record, string>; +} + +@observer +class SignDeployPage extends React.Component< + Props, + { + genericRows: { + key: string; + value: any; + title: any; + }[]; + deploySpecificRows: { + key: string; + value: any; + title: any; + }[]; + deployToSign: deployWithID | null; + argsExpanded: boolean; + } +> { + constructor(props: Props) { + super(props); + this.state = { + genericRows: [], + deploySpecificRows: [], + deployToSign: this.props.signingContainer.deployToSign, + argsExpanded: false + }; + } + + async componentDidMount() { + let w = await browser.windows.getCurrent(); + if (w.type === 'popup') { + window.addEventListener('beforeunload', e => { + this.props.signingContainer.cancel(this.state.deployToSign?.id!); + }); + } + if (this.state.deployToSign) { + this.generateDeployInfo(this.state.deployToSign); + } + } + + createRow(key: string, value: any, title?: any) { + return { key, value, title }; + } + + async generateDeployInfo(deployToSign: deployWithID) { + const deployData = await this.props.signingContainer.parseDeployData( + deployToSign.id + ); + const baseRows = [ + this.createRow( + 'Signing Key', + truncateString(deployData.signingKey, 6, 6), + deployData.signingKey + ), + this.createRow( + 'Account', + truncateString(deployData.account, 6, 6), + deployData.account + ), + this.createRow( + 'Deploy Hash', + truncateString(deployData.deployHash, 6, 6), + deployData.deployHash + ), + // this.createRow( + // 'Body Hash', + // truncateString(deployData.bodyHash, 6, 6), + // deployData.bodyHash + // ), + this.createRow('Timestamp', deployData.timestamp), + this.createRow('Chain Name', deployData.chainName), + /* + Gas Price refers to how much a caller is willing to pay per unit of gas. + + Currently there is no logic in place to prioritise those willing to pay more + meaning there is no reason to set it higher than 1. + + In cspr.live Gas Price is fixed at 1 and the user has no visibility of it. + + Until Gas Price impacts contract execution I will omit it from the deploy data + screen to reduce confusion for users. + + this.createRow('Gas Price', `${deployData.gasPrice} motes`), + */ + this.createRow( + 'Transaction Fee', + `${numberWithSpaces(deployData.payment)} motes`, + `${motesToCSPR(deployData.payment)} CSPR` + ), + this.createRow('Deploy Type', deployData.deployType) + ]; + let argRows = []; + for (let [key, value] of Object.entries(deployData.deployArgs)) { + argRows.push( + this.createRow( + key, + value.length > 15 ? truncateString(value, 6, 6) : value, + value.length > 12 ? value : undefined + ) + ); + } + this.setState({ + genericRows: baseRows, + deploySpecificRows: argRows, + argsExpanded: argRows.length > 3 ? false : true + }); + } + + render() { + if (this.state.deployToSign && this.props.authContainer.isUnLocked) { + const deployId = this.props.signingContainer.deployToSign?.id; + return ( +
+ + Signature Request + + + + + {this.state.genericRows.map((row: any) => + row.key === 'Amount' || row.key === 'Payment' ? ( + + + + {row.key} + + {row.value} + + + ) : ( + + + + {row.key} + + {row.value} + + + ) + )} + {this.state.genericRows.some( + row => row.key === 'Deploy Type' && row.value === 'Transfer' + ) ? ( + <> + + + Transfer Data + + + + +
+ + {this.state.deploySpecificRows.map((row, index) => { + return row.key === 'Amount' ? ( + + + + {row.key} + + + {`${numberWithSpaces(row.value)} motes`} + + + + ) : ( + + + + {row.key} + + + {isNaN(+row.value) + ? row.value + : numberWithSpaces(row.value)} + + + + ); + })} + +
+ + + + ) : ( + <> + + + Contract Arguments + + + + this.setState({ + argsExpanded: !this.state.argsExpanded + }) + } + > + {this.state.argsExpanded ? ( + + ) : ( + + )} + + + + + + + + + {this.state.deploySpecificRows.map( + (row, index) => { + return ( + + + + {row.key} + + + {isNaN(+row.value) + ? row.value + : numberWithSpaces(row.value)} + + + + ); + } + )} + +
+
+
+
+ + )} + + +
+ + + + + + + + + + +
+ ); + } else { + return ; + } + } +} + +export default withStyles(styles)(withRouter(SignDeployPage)); diff --git a/src/popup/components/SignMessagePage.tsx b/src/popup/components/SignMessagePage.tsx index b4d3baae..e07a6810 100644 --- a/src/popup/components/SignMessagePage.tsx +++ b/src/popup/components/SignMessagePage.tsx @@ -1,404 +1,102 @@ -import { observer } from 'mobx-react'; +import SigningContainer from '../container/SigningContainer'; import React from 'react'; -import { Redirect, RouteComponentProps, withRouter } from 'react-router'; -import SignMessageContainer from '../container/SignMessageContainer'; +import { Redirect } from 'react-router-dom'; import Pages from './Pages'; import { browser } from 'webextension-polyfill-ts'; -import AccountManager from '../container/AccountManager'; -import { withStyles } from '@material-ui/core/styles'; -import { - Box, - Button, - Collapse, - IconButton, - Grid, - Table, - TableBody, - TableCell, - TableContainer, - TableRow, - Tooltip, - Typography -} from '@material-ui/core'; -import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; -import { deployWithID } from '../../background/SignMessageManager'; -import { - truncateString, - numberWithSpaces, - motesToCSPR -} from '../../background/utils'; +import { Button, withStyles } from '@material-ui/core'; +import { truncateString } from '../../background/utils'; -const styles = () => ({ - tooltip: { - fontSize: '.8rem', - width: '260px', - margin: '10px 0 0 0' - } -}); - -const CsprTooltip = withStyles({ - tooltip: { - fontSize: '1rem', - width: 'fit-content', - margin: '10px 0 0 0', - textAlign: 'center' - } -})(Tooltip); - -interface Props extends RouteComponentProps { - signMessageContainer: SignMessageContainer; - authContainer: AccountManager; - classes: Record, string>; -} - -@observer -class SignMessagePage extends React.Component< - Props, - { - genericRows: { - key: string; - value: any; - title: any; - }[]; - deploySpecificRows: { - key: string; - value: any; - title: any; - }[]; - deployToSign: deployWithID | null; - argsExpanded: boolean; - } -> { - constructor(props: Props) { - super(props); - this.state = { - genericRows: [], - deploySpecificRows: [], - deployToSign: this.props.signMessageContainer.deployToSign, - argsExpanded: false - }; - } - - async componentDidMount() { - let w = await browser.windows.getCurrent(); - if (w.type === 'popup') { - window.addEventListener('beforeunload', e => { - this.props.signMessageContainer.cancel(this.state.deployToSign?.id!); - }); +const ApproveButton = withStyles(() => ({ + root: { + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'rgba(11, 156, 49, 0.6)' } - if (this.state.deployToSign) { - this.generateDeployInfo(this.state.deployToSign); - } - } - - createRow(key: string, value: any, title?: any) { - return { key, value, title }; } +}))(Button); - async generateDeployInfo(deployToSign: deployWithID) { - const deployData = await this.props.signMessageContainer.parseDeployData( - deployToSign.id - ); - const baseRows = [ - this.createRow( - 'Signing Key', - truncateString(deployData.signingKey, 6, 6), - deployData.signingKey - ), - this.createRow( - 'Account', - truncateString(deployData.account, 6, 6), - deployData.account - ), - this.createRow( - 'Deploy Hash', - truncateString(deployData.deployHash, 6, 6), - deployData.deployHash - ), - // this.createRow( - // 'Body Hash', - // truncateString(deployData.bodyHash, 6, 6), - // deployData.bodyHash - // ), - this.createRow('Timestamp', deployData.timestamp), - this.createRow('Chain Name', deployData.chainName), - /* - Gas Price refers to how much a caller is willing to pay per unit of gas. - - Currently there is no logic in place to prioritise those willing to pay more - meaning there is no reason to set it higher than 1. - - In cspr.live Gas Price is fixed at 1 and the user has no visibility of it. - - Until Gas Price impacts contract execution I will omit it from the deploy data - screen to reduce confusion for users. - - this.createRow('Gas Price', `${deployData.gasPrice} motes`), - */ - this.createRow( - 'Transaction Fee', - `${numberWithSpaces(deployData.payment)} motes`, - `${motesToCSPR(deployData.payment)} CSPR` - ), - this.createRow('Deploy Type', deployData.deployType) - ]; - let argRows = []; - for (let [key, value] of Object.entries(deployData.deployArgs)) { - argRows.push( - this.createRow( - key, - value.length > 15 ? truncateString(value, 6, 6) : value, - value.length > 12 ? value : undefined - ) - ); +const CancelButton = withStyles(() => ({ + root: { + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'rgba(255, 0, 0, 0.6)' } - this.setState({ - genericRows: baseRows, - deploySpecificRows: argRows, - argsExpanded: argRows.length > 3 ? false : true - }); } +}))(Button); - render() { - if (this.state.deployToSign && this.props.authContainer.isUnLocked) { - const deployId = this.props.signMessageContainer.deployToSign?.id; - return ( -
- - Signature Request - - - - - {this.state.genericRows.map((row: any) => - row.key === 'Amount' || row.key === 'Payment' ? ( - - - - {row.key} - - {row.value} - - - ) : ( - - - - {row.key} - - {row.value} - - - ) - )} - {this.state.genericRows.some( - row => row.key === 'Deploy Type' && row.value === 'Transfer' - ) ? ( - <> - - - Transfer Data - - - - -
- - {this.state.deploySpecificRows.map((row, index) => { - return row.key === 'Amount' ? ( - - - - {row.key} - - - {`${numberWithSpaces(row.value)} motes`} - - - - ) : ( - - - - {row.key} - - - {isNaN(+row.value) - ? row.value - : numberWithSpaces(row.value)} - - - - ); - })} - -
- - - - ) : ( - <> - - - Contract Arguments - - - - this.setState({ - argsExpanded: !this.state.argsExpanded - }) - } - > - {this.state.argsExpanded ? ( - - ) : ( - - )} - - - - - - - - - {this.state.deploySpecificRows.map( - (row, index) => { - return ( - - - - {row.key} - - - {isNaN(+row.value) - ? row.value - : numberWithSpaces(row.value)} - - - - ); - } - )} - -
-
-
-
- - )} - - -
- - - - - - - - - - -
- ); - } else { - return ; +export const SignMessagePage = (signingContainer: SigningContainer) => { + const messageWithID = signingContainer.messageToSign; // useState(signingContainer.messageToSign); + browser.windows.getCurrent().then(w => { + if (w.type === 'popup' && messageWithID?.id) { + window.addEventListener('beforeunload', e => { + signingContainer.messageToSign && + signingContainer.cancelSigningMessage(messageWithID.id); + }); } - } -} + }); -export default withStyles(styles)(withRouter(SignMessagePage)); + // console.log(messageWithID); + return messageWithID ? ( +
+

Do you want to sign the message?

+
+ + Casper Message: + +
+

{messageWithID.messageString}

+
+
+ + Signing Key: + +
+

{truncateString(messageWithID.signingKey, 15, 15)}

+
+
+ + signingContainer + .approveSigningMessage(messageWithID.id) + .then(() => window.close()) + } + > + Approve + + + signingContainer + .cancelSigningMessage(messageWithID.id) + .then(() => window.close()) + } + > + Cancel + +
+
+ ) : ( + + ); +}; diff --git a/src/popup/container/AccountManager.ts b/src/popup/container/AccountManager.ts index 8ba7b402..7b70c386 100644 --- a/src/popup/container/AccountManager.ts +++ b/src/popup/container/AccountManager.ts @@ -122,11 +122,6 @@ class AccountManager { return this.appState.activeUserAccount; } - @computed - get unsignedDeploys() { - return this.appState.unsignedDeploys; - } - @computed get remainingUnlockAttempts() { return this.appState.unlockAttempts; @@ -192,6 +187,11 @@ class AccountManager { get idleTimeoutMins(): number { return this.appState.idleTimeoutMins; } + + async configureTimeout(durationMins: number) { + if (durationMins === this.idleTimeoutMins) return; + await this.backgroundManager.configureTimeout(durationMins); + } } export default AccountManager; diff --git a/src/popup/container/SignMessageContainer.ts b/src/popup/container/SigningContainer.ts similarity index 67% rename from src/popup/container/SignMessageContainer.ts rename to src/popup/container/SigningContainer.ts index 3aa4406f..3dbb1ef0 100644 --- a/src/popup/container/SignMessageContainer.ts +++ b/src/popup/container/SigningContainer.ts @@ -3,7 +3,7 @@ import { AppState } from '../../lib/MemStore'; import { browser } from 'webextension-polyfill-ts'; import { computed } from 'mobx'; -class SignMessageContainer { +class SigningContainer { constructor( private backgroundManager: BackgroundManager, private appState: AppState @@ -17,6 +17,14 @@ class SignMessageContainer { return null; } + @computed + get messageToSign() { + if (this.appState.unsignedMessages.length > 0) { + return this.appState.unsignedMessages[0]; + } + return null; + } + async parseDeployData(deployId: number) { return await this.backgroundManager.parseDeployData(deployId); } @@ -31,6 +39,14 @@ class SignMessageContainer { // this.closeWindow(); } + async approveSigningMessage(messageId: number) { + await this.backgroundManager.approveSigningMessage(messageId); + } + + async cancelSigningMessage(messageId: number) { + await this.backgroundManager.cancelSigningMessage(messageId); + } + private async closeWindow() { let views = await browser.extension.getViews(); let popup = views[1].window; @@ -38,4 +54,4 @@ class SignMessageContainer { } } -export default SignMessageContainer; +export default SigningContainer;