diff --git a/.gitignore b/.gitignore index 66fd13c..48d1600 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,14 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +# Electron. +electron/release +electron/.cache +electron/.electron_cache +electron/node_modules +electron/.gox_output +electron/server + +# dependencies +**/node_modules diff --git a/.travis.yml b/.travis.yml index 45f52df..7a29ce6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,9 +13,14 @@ matrix: if: type != pull_request osx_image: xcode8 +before_install: + - nvm install 10.16 + install: - go get -u github.com/FiloSottile/vendorcheck - make install-linters + - make install-deps-ui script: - - make check \ No newline at end of file + - make lint-ui + - make build-ui diff --git a/Makefile b/Makefile index b9eff06..16e6a54 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,9 @@ DEFAULT_GOAL := help OPTS?=GO111MODULE=on TEST_OPTS?=-race -tags no_ci -cover -timeout=10m +# Static files directory +GUI_STATIC_DIR = src/gui/static + check: lint test ## Run linters and tests lint: ## Run linters. Use make install-linters first @@ -32,5 +35,14 @@ dep: ## Sorts dependencies ${OPTS} go mod download ${OPTS} go mod tidy -v +install-deps-ui: ## Install the UI dependencies + cd $(GUI_STATIC_DIR) && npm ci + +lint-ui: ## Lint the UI code + cd $(GUI_STATIC_DIR) && npm run lint + +build-ui: ## Builds the UI + cd $(GUI_STATIC_DIR) && npm run build + help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/electron/README.md b/electron/README.md new file mode 100644 index 0000000..086934e --- /dev/null +++ b/electron/README.md @@ -0,0 +1,48 @@ +# Build system + +The GUI client is an Electron (http://electron.atom.io/) app. + +It cross compiles for osx, linux and windows 64 bit systems. + +## Requirements + +The Skycoin repository must be cloned in the parent directory. + +gox (go cross compiler), node and npm. + +### gox + +To install gox: + +```sh +go get github.com/gz-c/gox +``` + +### NPM + +Node and npm installation is system dependent. + +## Make sure that the wallet dist is up to date + +Recompile the wallet frontend. See [Wallet GUI Development README](../src/gui/static/README.md) for instructions. + +## Use electron-builder to pack and create app installer + +Use this command for preparing the build process. + +```sh +./perpare-build.sh +``` + +Then you can compile the version for the OS you need with any of these commands: + +```sh +npm run dist-win32 +npm run dist-win64 +npm run dist-win +npm run dist-linux +npm run dist-mac +npm run dist-mac +``` + +Final results are placed in the `release` folder. diff --git a/electron/app/.snyk b/electron/app/.snyk new file mode 100644 index 0000000..4747845 --- /dev/null +++ b/electron/app/.snyk @@ -0,0 +1,9 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.13.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + 'npm:chownr:20180731': + - '*': + reason: No fix available + expires: '2019-06-25T15:35:33.473Z' +patch: {} diff --git a/electron/app/electron-api.js b/electron/app/electron-api.js new file mode 100644 index 0000000..7aa0990 --- /dev/null +++ b/electron/app/electron-api.js @@ -0,0 +1,10 @@ +const { contextBridge, ipcRenderer } = require('electron') + +// Allows to check the URL of the local node while using Electron, as the port number +// is selected randomly. + +contextBridge.exposeInMainWorld('electron', { + getLocalServerUrl: () => { + return ipcRenderer.sendSync('localNodeUrl'); + } +}); diff --git a/electron/app/electron-main.js b/electron/app/electron-main.js new file mode 100644 index 0000000..72d340a --- /dev/null +++ b/electron/app/electron-main.js @@ -0,0 +1,557 @@ +'use strict' + +const { app, Menu, BrowserWindow, shell, session, ipcMain } = require('electron'); +const path = require('path'); +const childProcess = require('child_process'); +const url = require('url'); +const axios = require('axios'); + +// This adds refresh and devtools console keybindings +// Page can refresh with cmd+r, ctrl+r, F5 +// Devtools can be toggled with cmd+alt+i, ctrl+shift+i, F12 +require('electron-debug')({enabled: true, showDevTools: false}); +require('electron-context-menu')({}); + +global.eval = function() { throw new Error('bad!!'); } + +let splashLoaded = false + +// Session of the current window. +let currentSession; +// Folder in which the local node saves the wallet files. +let walletsFolder = null; + +// URLs for accessing the local node and the app contents. +let currentLocalNodeURL; +let currentLocalNodeHost; +let guiURL; +ipcMain.on('localNodeUrl', (event) => { + event.returnValue = currentLocalNodeURL; +}) + +// Detect if the code is running with the "dev" arg. The "dev" arg is added when running npm +// start. If this is true, a local node will not be started, but one is expected to be running +// in 127.0.0.1:6420; also, the local web server will not be started, the contents served in +// http://localhost:4200 will be displayed and it will be allowed to reload the URLs using the +// Electron window, so that it is easier to test the changes made to the UI using npm start. +let dev = process.argv.find(arg => arg === 'dev') ? true : false; + +// Basic settings. +app.commandLine.appendSwitch('ssl-version-fallback-min', 'tls1.2'); +app.commandLine.appendSwitch('--no-proxy-server'); +app.setAsDefaultProtocolClient('skycoin'); +app.allowRendererProcessReuse = true; + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let win; + +// Local node and web server. +var skycoin = null; +let server = null; +const serverPort= 8412; + +// It is only possible to make connections to hosts that are in this list. +var allowedHosts = new Map(); +// Local server. +allowedHosts.set('127.0.0.1:' + serverPort, true); +// Skywallet daemon. +allowedHosts.set('127.0.0.1:9510', true); +// Price service. +allowedHosts.set('api.coinpaprika.com', true); +// For multiple operations. +allowedHosts.set('version.skycoin.com', true); +allowedHosts.set('downloads.skycoin.com', true); +if (dev) { + // Local server while testing. + allowedHosts.set('localhost:4200', true); +} + +// Starts the local node. +function startSkycoin() { + if (!dev) { + console.log('Starting local node from Electron'); + + if (skycoin) { + console.log('Local node already running'); + app.emit('skycoin-ready'); + return; + } + + // Resolve the local node binary location. + var appPath = app.getPath('exe'); + var exe = (() => { + switch (process.platform) { + case 'darwin': + return path.join(appPath, '../../Resources/app/skycoin'); + case 'win32': + // Use only the relative path on windows due to short path length + // limits + return './resources/app/skycoin.exe'; + case 'linux': + return path.join(path.dirname(appPath), './resources/app/skycoin'); + default: + return './resources/app/skycoin'; + } + })() + + // Start the local node. + var args = [ + '-launch-browser=false', + '-color-log=false', // must be disabled for web interface detection + '-logtofile=true', + '-download-peerlist=true', + '-enable-all-api-sets=true', + '-enable-api-sets=INSECURE_WALLET_SEED', + '-disable-csrf=false', + '-reset-corrupt-db=true', + '-enable-gui=false', + '-web-interface-port=0' // random port assignment + // will break + // broken (automatically generated certs do not work): + // '-web-interface-https=true', + ] + skycoin = childProcess.spawn(exe, args); + + createWindow(); + + // Print the local node messages and check for the local node URL. + skycoin.stdout.on('data', (data) => { + console.log(data.toString()); + if (currentLocalNodeURL) { + return; + } + + // String which is expected to precede the local node URL. + const marker = 'Full address: '; + // Get the local node URL. + data.toString().split('\n').forEach(line => { + if (line.indexOf(marker) !== -1) { + setLocalNodeUrl(line.split(marker)[1].trim()); + + var id = setInterval(function() { + // Wait till the splash page loading is finished. + if (splashLoaded) { + app.emit('skycoin-ready', { url: currentLocalNodeURL }); + clearInterval(id); + } + }, 500); + } + }); + }); + skycoin.stderr.on('data', (data) => { + console.log(data.toString()); + }); + + // Close the app if there is a problem. + skycoin.on('error', (e) => { + console.log('Error starting the local node: ' + e); + app.quit(); + }); + skycoin.on('close', (code) => { + console.log('Local node closed'); + app.quit(); + }); + skycoin.on('exit', (code) => { + console.log('Local node exited'); + app.quit(); + }); + } else { + // If in dev mode, use 127.0.0.1:6420 as the local node. It must have been started before. + setLocalNodeUrl('http://127.0.0.1:6420'); + app.emit('skycoin-ready', { url: currentLocalNodeURL }); + } +} + +// Starts the local web server. +function startLocalServer() { + if (!dev) { + console.log('Starting the local server'); + + if (server) { + console.log('Server already running'); + return + } + + // Resolve the server binary location. + var appPath = app.getPath('exe'); + var exe = (() => { + switch (process.platform) { + case 'darwin': + return path.join(appPath, '../../Resources/app/server') + case 'win32': + // User only the relative path on windows due to short path length + // limits + return './resources/app/server.exe'; + case 'linux': + return path.join(path.dirname(appPath), './resources/app/server'); + default: + return './resources/app/server'; + } + })() + + // Get the path to the app files. + var contentsPath = (() => { + switch (process.platform) { + case 'darwin': + return path.join(appPath, '../../Resources/app/dist/') + case 'win32': + return path.join(path.dirname(appPath), './resources/app/dist/'); + case 'linux': + return path.join(path.dirname(appPath), './resources/app/dist/'); + default: + return './resources/app/dist/'; + } + })() + + // Start the server + server = childProcess.spawn(exe, ['-port=' + serverPort, '-path=' + contentsPath]); + + // Close the app if there is a problem. + server.on('error', (e) => { + console.log('Failed to start the local server: ' + e); + app.quit(); + }); + server.on('close', (code) => { + console.log('Local server closed'); + app.quit(); + }); + server.on('exit', (code) => { + console.log('Local server exited'); + app.quit(); + }); + + // Load the contents. + guiURL = 'http://127.0.0.1:' + serverPort; + win.loadURL(guiURL); + } else { + // If in dev mode, simply open the dev server URL. It must have been started before. + guiURL = 'http://localhost:4200/'; + createWindow(guiURL); + } +} + +// Creates and configures the main app window. +function createWindow(urltoOpen) { + // To fix appImage doesn't show icon in dock issue. + var appPath = app.getPath('exe'); + var iconPath = (() => { + switch (process.platform) { + case 'linux': + return path.join(path.dirname(appPath), './resources/icon512x512.png'); + } + })() + + // Create the browser window. + win = new BrowserWindow({ + width: 1200, + height: 900, + backgroundColor: '#000000', + title: 'Skycoin Multicoin Wallet', + icon: iconPath, + nodeIntegration: false, + webPreferences: { + webgl: false, + webaudio: false, + contextIsolation: true, + webviewTag: false, + nodeIntegration: false, + nodeIntegrationInWorker: false, + allowRunningInsecureContent: false, + webSecurity: true, + plugins: false, + enableRemoteModule: false, + preload: __dirname + '/electron-api.js', + }, + }); + + win.webContents.on('did-finish-load', function() { + if (!splashLoaded) { + splashLoaded = true; + } + }); + + // patch out eval + win.eval = global.eval; + win.webContents.executeJavaScript('window.eval = 0;'); + + currentSession = win.webContents.session +/* + currentSession.clearCache().then(response => { + console.log('Cleared the caching of the skycoin wallet.'); + }); + */ + + // When an options request to a swaplab https endpoint is detected, asume that it is a cors + // request and redirect it to an invalid endpoint on the node API. + currentSession.protocol.registerHttpProtocol('https', (request, callback) => { + if (request.method.toLowerCase().includes('options') && request.url.toLowerCase().includes('swaplab.cc')) { + callback({ url: currentLocalNodeURL + '/api/v1/unused', method: 'get' }); + } else { + callback({ url:request.url }); + } + }); + + // Block the connection if the URL is not in allowedHosts. + currentSession.webRequest.onBeforeRequest((details, callback) => { + // This if is needed for allowing the devtools to work. + if (!details.url.startsWith('devtools://devtools')) { + let requestUrl = details.url; + if (details.url.startsWith('blob:')) { + requestUrl = requestUrl.substr('blob:'.length, requestUrl.length - 'blob:'.length); + } + + let requestHost = url.parse(requestUrl).host; + if (!allowedHosts.has(requestHost)) { + callback({cancel: true}) + return; + } + } + callback({cancel: false}) + }); + + // Configure some filters for special cases. + configureFilters(); + + // Open the url if it is already known. If not, open the loading page. + if (urltoOpen) { + win.loadURL(urltoOpen); + } else { + win.loadURL('file://' + __dirname + '/splash/index.html'); + } + + // Emitted when the window is closed. + win.on('closed', () => { + win = null; + }); + + // If in dev mode, allow to open URLs. + if (!dev) { + win.webContents.on('will-navigate', function(e, destinationUrl) { + const requestHost = url.parse(destinationUrl).host; + if (requestHost !== '127.0.0.1:' + serverPort) { + e.preventDefault(); + require('electron').shell.openExternal(destinationUrl); + } + }); + } + + // Open links with target='_blank' in the default browser. + win.webContents.on('new-window', function(e, url) { + e.preventDefault(); + require('electron').shell.openExternal(url); + }); + + // Create the main menu. + var template = [{ + label: 'Skycoin', + submenu: [ + { label: 'Quit', accelerator: 'Command+Q', click: function() { app.quit(); } } + ] + }, { + label: 'Edit', + submenu: [ + { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, + { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, + { type: 'separator' }, + { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, + { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, + { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' }, + { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectall' } + ] + }, { + label: 'Show', + submenu: [ + { + label: 'Wallets folder', + click: () => { + if (walletsFolder) { + shell.showItemInFolder(walletsFolder) + } else { + shell.showItemInFolder(path.join(app.getPath("home"), '.skycoin', 'wallets')); + } + }, + }, + { + label: 'Logs folder', + click: () => { + if (walletsFolder) { + shell.showItemInFolder(walletsFolder.replace('wallets', 'logs')) + } else { + shell.showItemInFolder(path.join(app.getPath("home"), '.skycoin', 'logs')); + } + }, + }, + { + label: 'DevTools', + accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', + click: (item, focusedWindow) => { + if (focusedWindow) { + focusedWindow.toggleDevTools(); + } + } + }, + ] + }]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + + session + .fromPartition('') + .setPermissionRequestHandler((webContents, permission, callback) => { + return callback(false); + }); +} + +// Makes the window correctly manage some special cases. +function configureFilters() { + if (!currentSession) { + return; + } + + // URLs to check. + const urls = ['https://swaplab.cc/*']; + if (currentLocalNodeURL) { + urls.push(currentLocalNodeURL + '/*'); + } + + // Use the origin headers expected by Swaplab and the local node. + currentSession.webRequest.onBeforeSendHeaders({ + urls: urls + }, (details, callback) => { + if (details.url.indexOf('swaplab.cc') !== -1) { + details.requestHeaders['origin'] = null; + details.requestHeaders['referer'] = null; + details.requestHeaders['host'] = null; + details.requestHeaders['Origin'] = null; + details.requestHeaders['Referer'] = null; + details.requestHeaders['Host'] = null; + } else { + details.requestHeaders['origin'] = currentLocalNodeURL; + details.requestHeaders['referer'] = currentLocalNodeURL; + details.requestHeaders['host'] = currentLocalNodeHost; + } + + callback({ requestHeaders: details.requestHeaders }); + }) + + // Add the CORS headers needed for accessing Swaplab and the local node. + currentSession.webRequest.onHeadersReceived({ + urls: urls + }, (details, callback) => { + const headers = details.responseHeaders; + if (headers) { + headers['Access-Control-Allow-Origin'] = '*'; + headers['Access-Control-Allow-Headers'] = '*'; + } + const response = { responseHeaders: headers }; + + // Options request are redirected in other part of this code to an invalid url, so the + // status must be changed to 200 to simulate a good response. + if (details.method.toLowerCase().includes('options')) { + response['statusLine'] = '200'; + } + + callback(response); + }); +} + +// Allow only one window. +const singleInstanceLockObtained = app.requestSingleInstanceLock() +if (!singleInstanceLockObtained) { + app.quit() + return; +} else { + app.on('second-instance', (event, commandLine, workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (win) { + if (win.isMinimized()) { + win.restore(); + } + win.focus(); + } else { + createWindow(guiURL); + } + }); +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', startSkycoin); + +// Called when the local node is running and ready. +app.on('skycoin-ready', (e) => { + // Start the local web server. + startLocalServer(); + + // Get the folder in which the local node saves the wallet files. + axios + .get(e.url + '/api/v1/wallets/folderName') + .then(response => { + walletsFolder = response.data.address; + }) + .catch(() => {}); +}); + +// Quit when all windows are closed. +app.on('window-all-closed', () => { + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', () => { + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (win === null) { + createWindow(guiURL); + } +}); + +app.on('will-quit', () => { + if (skycoin) { + skycoin.kill('SIGINT'); + } +}); + +app.on('web-contents-created', (event, contents) => { + contents.on('will-attach-webview', (event, webPreferences, params) => { + // Strip away preload scripts if unused or verify their location is legitimate + delete webPreferences.preload + delete webPreferences.preloadURL + + // Disable Node.js integration + webPreferences.nodeIntegration = false + + // Verify URL being loaded + if (!params.src.startsWith(url)) { + event.preventDefault(); + } + }); +}); + +// Populates currentLocalNodeURL and currentLocalNodeHost. It cleans the URL if needed and adds +// it to the list of allowed URLs (it also removes the previous value from the list, if needed). +// After finishing, configureFilters() is called, to make sure the new URL is processed correctly. +function setLocalNodeUrl(url) { + if (currentLocalNodeHost) { + allowedHosts.delete(currentLocalNodeHost); + } + + currentLocalNodeURL = url; + if (currentLocalNodeURL.endsWith('/')) { + currentLocalNodeURL = currentLocalNodeURL.substr(0, currentLocalNodeURL.length - 1); + } + + if (currentLocalNodeURL.startsWith('https://')) { + currentLocalNodeHost = currentLocalNodeURL.substr(8); + } else { + currentLocalNodeHost = currentLocalNodeURL.substr(7); + } + + allowedHosts.set(currentLocalNodeHost, true); + configureFilters(); +} diff --git a/electron/app/package-lock.json b/electron/app/package-lock.json new file mode 100644 index 0000000..c516d4f --- /dev/null +++ b/electron/app/package-lock.json @@ -0,0 +1,186 @@ +{ + "name": "skycoin-multicoin-electron", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "axios": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "electron-context-menu": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-0.9.1.tgz", + "integrity": "sha1-7U3yDAgEkcPJlqv8s2MVmUajgFg=", + "requires": { + "electron-dl": "^1.2.0", + "electron-is-dev": "^0.1.1" + } + }, + "electron-debug": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/electron-debug/-/electron-debug-1.5.0.tgz", + "integrity": "sha512-23CLHQXW+gMgdlJbeW1EinPX7DpwuLtfdzSuFL0OnsqEhKGJVJufAZTyq2hc3sr+R53rr3P+mJiYoR5VzAHKJQ==", + "requires": { + "electron-is-dev": "^0.3.0", + "electron-localshortcut": "^3.0.0" + }, + "dependencies": { + "electron-is-dev": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.3.0.tgz", + "integrity": "sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4=" + } + } + }, + "electron-dl": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-1.12.0.tgz", + "integrity": "sha512-UMc2CL45Ybpvu66LDPYzwmDRmYK4Ivz+wdnTM0eXcNMztvQwhixAk2UPme1c7McqG8bAlKEkQpZn3epmQy4EWg==", + "requires": { + "ext-name": "^5.0.0", + "pupa": "^1.0.0", + "unused-filename": "^1.0.0" + } + }, + "electron-is-accelerator": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz", + "integrity": "sha1-UJ5RDCala1Xhf4Y6SwThEYRqsns=" + }, + "electron-is-dev": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.1.2.tgz", + "integrity": "sha1-ihBD4ys6HaHD9VPc4oznZCRhZ+M=" + }, + "electron-localshortcut": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/electron-localshortcut/-/electron-localshortcut-3.1.0.tgz", + "integrity": "sha512-MgL/j5jdjW7iA0R6cI7S045B0GlKXWM1FjjujVPjlrmyXRa6yH0bGSaIAfxXAF9tpJm3pLEiQzerYHkRh9JG/A==", + "requires": { + "debug": "^2.6.8", + "electron-is-accelerator": "^0.1.0", + "keyboardevent-from-electron-accelerator": "^1.1.0", + "keyboardevents-areequal": "^0.2.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "requires": { + "mime-db": "^1.28.0" + } + }, + "ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "requires": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "keyboardevent-from-electron-accelerator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-1.1.0.tgz", + "integrity": "sha512-VDC4vKWGrR3VgIKCE4CsXnvObGgP8C2idnTKEMUkuEuvDGE1GEBX9FtNdJzrD00iQlhI3xFxRaeItsUmlERVng==" + }, + "keyboardevents-areequal": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/keyboardevents-areequal/-/keyboardevents-areequal-0.2.2.tgz", + "integrity": "sha512-Nv+Kr33T0mEjxR500q+I6IWisOQ0lK1GGOncV0kWE6n4KFmpcu7RUX5/2B0EUtX51Cb0HjZ9VJsSY3u4cBa0kw==" + }, + "mime-db": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" + }, + "modify-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz", + "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "pupa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz", + "integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y=" + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "requires": { + "sort-keys": "^1.0.0" + } + }, + "unused-filename": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-1.0.0.tgz", + "integrity": "sha1-00CID3GuIRXrqhMlvvBcxmhEacY=", + "requires": { + "modify-filename": "^1.1.0", + "path-exists": "^3.0.0" + } + } + } +} diff --git a/electron/app/package.json b/electron/app/package.json new file mode 100644 index 0000000..9ba20b3 --- /dev/null +++ b/electron/app/package.json @@ -0,0 +1,14 @@ +{ + "name": "skycoin-multicoin-electron", + "author": "skycoin", + "main": "electron-main.js", + "version": "0.1.0", + "license": "MIT", + "description": "Specific Electron code for the Skycoin multicoin wallet", + "private": true, + "dependencies": { + "axios": "^0.18.1", + "electron-context-menu": "^0.9.1", + "electron-debug": "^1.5.0" + } +} diff --git a/electron/app/splash/index.html b/electron/app/splash/index.html new file mode 100644 index 0000000..546f99e --- /dev/null +++ b/electron/app/splash/index.html @@ -0,0 +1,44 @@ + + +
+ + + + + +');g.push(" |
=0;){for(e=0,f=M[r]%v,g=M[r]/v|0,i=r+(o=a);i>r;)e=((u=f*(u=x[--o]%v)+(s=g*u+(c=x[o]/v|0)*f)%v*v+y[i]+e)/b|0)+(s/v|0)+g*c,y[i--]=u%b;y[i]=e}return e?++l:y.splice(0,1),U(n,y,l)},L.negated=function(){var n=new V(this);return n.s=-n.s||null,n},L.plus=function(n,t){var e,l=this,r=l.s;if(t=(n=new V(n,t)).s,!r||!t)return new V(NaN);if(r!=t)return n.s=-t,l.minus(n);var i=l.e/h,o=n.e/h,s=l.c,a=n.c;if(!i||!o){if(!s||!a)return new V(r/0);if(!s[0]||!a[0])return a[0]?n:new V(s[0]?l:0*r)}if(i=m(i),o=m(o),s=s.slice(),r=i-o){for(r>0?(o=i,e=a):(r=-r,e=s),e.reverse();r--;e.push(0));e.reverse()}for((r=s.length)-(t=a.length)<0&&(e=a,a=s,s=e,t=r),r=0;t;)r=(s[--t]=s[t]+a[t]+r)/d|0,s[t]=d===s[t]?0:s[t]%d;return r&&(s=[r].concat(s),++o),U(n,s,o)},L.precision=L.sd=function(n,t){var e,l,r,i=this;if(null!=n&&n!==!!n)return b(n,1,1e9),null==t?t=A:b(t,0,8),G(new V(i),n,t);if(!(e=i.c))return null;if(l=(r=e.length-1)*h+1,r=e[r]){for(;r%10==0;r/=10,l--);for(r=e[0];r>=10;r/=10,l++);}return n&&i.e+1>l&&(l=i.e+1),l},L.shiftedBy=function(n){return b(n,-p,p),this.times("1e"+n)},L.squareRoot=L.sqrt=function(){var n,t,l,r,i,o=this,s=o.c,a=o.s,u=o.e,c=D+4,d=new V("0.5");if(1!==a||!s||!s[0])return new V(!a||a<0&&(!s||s[0])?NaN:s?o:1/0);if(0==(a=Math.sqrt(+o))||a==1/0?(((t=g(s)).length+u)%2==0&&(t+="0"),a=Math.sqrt(t),u=m((u+1)/2)-(u<0||u%2),l=new V(t=a==1/0?"1e"+u:(t=a.toExponential()).slice(0,t.indexOf("e")+1)+u)):l=new V(a+""),l.c[0])for((a=(u=l.e)+c)<3&&(a=0);;)if(l=d.times((i=l).plus(e(o,i,c,1))),g(i.c).slice(0,a)===(t=g(l.c)).slice(0,a)){if(l.e0&&h>0){for(a=d.substr(0,l=h%i||i);l