diff --git a/README.md b/README.md index 969c5db..47ef433 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ quasar build -m electron -t mat ### LICENSE +Copyright (c) 2018-2019, Loki Project Copyright (c) 2018, Ryo Currency Project Portions of this software are available under BSD-3 license. Please see ORIGINAL-LICENSE for details diff --git a/package-lock.json b/package-lock.json index 9f44f1d..2e44862 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7003,7 +7003,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, "requires": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -12610,12 +12609,28 @@ "dev": true }, "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "ripemd160": { @@ -14432,8 +14447,7 @@ "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, "unpipe": { "version": "1.0.0", diff --git a/package.json b/package.json index ce45762..d4c594a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "axios": "^0.18.0", "electron-window-state": "^5.0.2", + "fs-extra": "^7.0.1", "object-assign-deep": "^0.4.0", "portscanner": "^2.2.0", "promise-queue": "^2.2.5", diff --git a/quasar.conf.js b/quasar.conf.js index 1a33ffc..48b2851 100644 --- a/quasar.conf.js +++ b/quasar.conf.js @@ -176,7 +176,7 @@ module.exports = function (ctx) { appId: "com.lokinetwork.wallet", productName: "Loki Wallet Atom", - copyright: "Copyright © 2018 Ryo Currency Project", + copyright: "Copyright © 2018-2019 Loki Project, 2018 Ryo Currency Project", // directories: { // buildResources: "src-electron/build" diff --git a/src-electron/icons/icon.icns b/src-electron/icons/icon.icns index 66cfd15..e2504af 100644 Binary files a/src-electron/icons/icon.icns and b/src-electron/icons/icon.icns differ diff --git a/src-electron/icons/icon.ico b/src-electron/icons/icon.ico index d5897af..43900e8 100644 Binary files a/src-electron/icons/icon.ico and b/src-electron/icons/icon.ico differ diff --git a/src-electron/icons/icon_512x512.png b/src-electron/icons/icon_512x512.png index 2972a49..d339174 100644 Binary files a/src-electron/icons/icon_512x512.png and b/src-electron/icons/icon_512x512.png differ diff --git a/src-electron/icons/linux-512x512.png b/src-electron/icons/linux-512x512.png index 2972a49..d339174 100644 Binary files a/src-electron/icons/linux-512x512.png and b/src-electron/icons/linux-512x512.png differ diff --git a/src-electron/icons/macos-512x512.png b/src-electron/icons/macos-512x512.png index 3585167..d339174 100644 Binary files a/src-electron/icons/macos-512x512.png and b/src-electron/icons/macos-512x512.png differ diff --git a/src-electron/main-process/electron-main.js b/src-electron/main-process/electron-main.js index 8421fe4..0bdbf4a 100644 --- a/src-electron/main-process/electron-main.js +++ b/src-electron/main-process/electron-main.js @@ -41,8 +41,8 @@ function createWindow () { */ let mainWindowState = windowStateKeeper({ - defaultWidth: 800, - defaultHeight: 650 + defaultWidth: 900, + defaultHeight: 700 }) mainWindow = new BrowserWindow({ diff --git a/src-electron/main-process/modules/backend.js b/src-electron/main-process/modules/backend.js index f3b2282..fa2f08c 100644 --- a/src-electron/main-process/modules/backend.js +++ b/src-electron/main-process/modules/backend.js @@ -5,7 +5,7 @@ import { dialog } from "electron" const WebSocket = require("ws") const os = require("os") -const fs = require("fs") +const fs = require("fs-extra") const path = require("path") const objectAssignDeep = require("object-assign-deep") @@ -17,6 +17,7 @@ export class Backend { this.wss = null this.token = null this.config_dir = null + this.wallet_dir = null this.config_file = null this.config_data = {} this.scee = new SCEE() @@ -24,16 +25,19 @@ export class Backend { init (config) { if (os.platform() === "win32") { - this.config_dir = "C:\\ProgramData\\loki-wallet" + this.config_dir = "C:\\ProgramData\\loki" + this.wallet_dir = `${os.homedir()}\\Documents\\Loki` } else { - this.config_dir = path.join(os.homedir(), ".loki-wallet") + this.config_dir = path.join(os.homedir(), ".loki") + this.wallet_dir = path.join(os.homedir(), "Loki") } + if (!fs.existsSync(this.config_dir)) { - fs.mkdirSync(this.config_dir) + fs.mkdirpSync(this.config_dir) } if (!fs.existsSync(path.join(this.config_dir, "gui"))) { - fs.mkdirSync(path.join(this.config_dir, "gui")) + fs.mkdirpSync(path.join(this.config_dir, "gui")) } this.config_file = path.join(this.config_dir, "gui", "config.json") @@ -54,19 +58,19 @@ export class Backend { } const daemons = { - main: { + mainnet: { ...daemon, remote_host: "doopool.xyz", remote_port: 22020 }, - staging: { + stagenet: { ...daemon, type: "local", p2p_bind_port: 38153, rpc_bind_port: 38154, zmq_rpc_bind_port: 38155 }, - test: { + testnet: { ...daemon, type: "local", p2p_bind_port: 38156, @@ -80,8 +84,9 @@ export class Backend { daemons: objectAssignDeep({}, daemons), app: { data_dir: this.config_dir, + wallet_data_dir: this.wallet_dir, ws_bind_port: 12213, - net_type: "main" + net_type: "mainnet" }, wallet: { rpc_bind_port: 18082, @@ -102,10 +107,6 @@ export class Backend { host: "doopool.xyz", port: "22020" }, - { - host: "rpc.nodes.rentals", - port: "22023" - }, { host: "daemons.cryptopool.space", port: "22023" @@ -114,10 +115,6 @@ export class Backend { host: "node.loki-pool.com", port: "18081" }, - { - host: "uk.loki.cash", - port: "22020" - }, { host: "imaginary.stream", port: "22023" @@ -214,7 +211,7 @@ export class Backend { // Validate deamon data this.config_data = { ...this.config_data, - ...validated, + ...validated } fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => { @@ -307,27 +304,60 @@ export class Backend { } // save config file back to file, so updated options are stored on disk - fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8") + fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => {}) this.send("set_app_data", { config: this.config_data, pending_config: this.config_data }) + // Make the wallet dir + const { wallet_data_dir, data_dir } = this.config_data.app + if (!fs.existsSync(wallet_data_dir)) { fs.mkdirpSync(wallet_data_dir) } + + // Check to see if data and wallet directories exist + const dirs_to_check = [{ + path: data_dir, + error: "Data storge path not found" + }, + { + path: wallet_data_dir, + error: "Wallet data storge path not found" + }] + + for (const dir of dirs_to_check) { + // Check to see if dir exists + if (!fs.existsSync(dir.path)) { + this.send("show_notification", { + type: "negative", + message: `Error: ${dir.error}`, + timeout: 2000 + }) + + // Go back to config + this.send("set_app_data", { + status: { + code: -1 // Return to config screen + } + }) + return + } + } + const { net_type } = this.config_data.app const dirs = { - "main": this.config_data.app.data_dir, - "staging": path.join(this.config_data.app.data_dir, "staging"), - "test": path.join(this.config_data.app.data_dir, "testnet") + "mainnet": this.config_data.app.data_dir, + "stagenet": path.join(this.config_data.app.data_dir, "stagenet"), + "testnet": path.join(this.config_data.app.data_dir, "testnet") } // Make sure we have the directories we need const net_dir = dirs[net_type] - if (!fs.existsSync(net_dir)) { fs.mkdirSync(net_dir) } + if (!fs.existsSync(net_dir)) { fs.mkdirpSync(net_dir) } const log_dir = path.join(net_dir, "logs") - if (!fs.existsSync(log_dir)) { fs.mkdirSync(log_dir) } + if (!fs.existsSync(log_dir)) { fs.mkdirpSync(log_dir) } this.daemon = new Daemon(this) this.walletd = new WalletRPC(this) @@ -338,72 +368,127 @@ export class Backend { } }) - this.daemon.checkVersion().then((version) => { - if (version) { - this.send("set_app_data", { - status: { - code: 4, - message: version - } - }) - } else { - // daemon not found, probably removed by AV, set to remote node - this.config_data.daemons[net_type].type = "remote" - this.send("set_app_data", { - status: { - code: 5 - }, - config: this.config_data, - pending_config: this.config_data - }) + // Make sure the remote node provided is accessible + const config_daemon = this.config_data.daemons[net_type] + this.daemon.checkRemote(config_daemon).then(data => { + if (data.error) { + // If we can default to local then we do so, otherwise we tell the user to re-set the node + if (config_daemon.type === "local_remote") { + this.config_data.daemons[net_type].type = "local" + this.send("set_app_data", { + config: this.config_data, + pending_config: this.config_data + }) + this.send("show_notification", { + type: "warning", + textColor: "black", + message: "Warning: Could not access remote node, switching to local only", + timeout: 2000 + }) + } else { + this.send("show_notification", { + type: "negative", + message: "Error: Could not access remote node, please try another remote node", + timeout: 2000 + }) + + // Go back to config + this.send("set_app_data", { + status: { + code: -1 // Return to config screen + } + }) + return + } } - this.daemon.start(this.config_data).then(() => { - this.send("set_app_data", { - status: { - code: 6 // Starting wallet - } + // If we got a net type back then check if ours match + if (data.net_type && data.net_type !== net_type) { + this.send("show_notification", { + type: "negative", + message: "Error: Remote node is using a different nettype", + timeout: 2000 }) - this.walletd.start(this.config_data).then(() => { + // Go back to config + this.send("set_app_data", { + status: { + code: -1 // Return to config screen + } + }) + return + } + + this.daemon.checkVersion().then((version) => { + if (version) { this.send("set_app_data", { status: { - code: 7 // Reading wallet list + code: 4, + message: version + } + }) + } else { + // daemon not found, probably removed by AV, set to remote node + this.config_data.daemons[net_type].type = "remote" + this.send("set_app_data", { + status: { + code: 5 + }, + config: this.config_data, + pending_config: this.config_data + }) + } + + this.daemon.start(this.config_data).then(() => { + this.send("set_app_data", { + status: { + code: 6 // Starting wallet } }) - this.walletd.listWallets(true) + this.walletd.start(this.config_data).then(() => { + this.send("set_app_data", { + status: { + code: 7 // Reading wallet list + } + }) - this.send("set_app_data", { - status: { - code: 0 // Ready - } + this.walletd.listWallets(true) + + this.send("set_app_data", { + status: { + code: 0 // Ready + } + }) + // eslint-disable-next-line + }).catch(error => { + this.send("set_app_data", { + status: { + code: -1 // Return to config screen + } + }) }) + // eslint-disable-next-line }).catch(error => { + if (this.config_data.daemons[net_type].type == "remote") { + this.send("show_notification", {type: "negative", message: "Remote daemon cannot be reached", timeout: 2000}) + } else { + this.send("show_notification", {type: "negative", message: "Local daemon internal error", timeout: 2000}) + } this.send("set_app_data", { status: { code: -1 // Return to config screen } }) }) + // eslint-disable-next-line }).catch(error => { - if (this.config_data.daemons[net_type].type == "remote") { - this.send("show_notification", {type: "negative", message: "Remote daemon cannot be reached", timeout: 2000}) - } else { - this.send("show_notification", {type: "negative", message: "Local daemon internal error", timeout: 2000}) - } this.send("set_app_data", { status: { code: -1 // Return to config screen } }) }) - }).catch(error => { - this.send("set_app_data", { - status: { - code: -1 // Return to config screen - } - }) }) }) } @@ -423,7 +508,7 @@ export class Backend { // Replace any invalid value with default values validate_values (values, defaults) { - const isDictionary = (v) => typeof v === "object" && v !== null && !(v instanceof Array) && !(v instanceof Date); + const isDictionary = (v) => typeof v === "object" && v !== null && !(v instanceof Array) && !(v instanceof Date) const modified = { ...values } // Make sure we have valid defaults diff --git a/src-electron/main-process/modules/daemon.js b/src-electron/main-process/modules/daemon.js index 285987f..648f371 100644 --- a/src-electron/main-process/modules/daemon.js +++ b/src-electron/main-process/modules/daemon.js @@ -11,7 +11,7 @@ export class Daemon { this.heartbeat = null this.heartbeat_slow = null this.id = 0 - this.net_type = "main" + this.net_type = "mainnet" this.local = false // do we have a local daemon ? this.agent = new http.Agent({keepAlive: true, maxSockets: 1}) @@ -40,6 +40,23 @@ export class Daemon { }) } + checkRemote (daemon) { + if (daemon.type === "local") { + return Promise.resolve({}) + } + + return this.sendRPC("get_info", {}, { + protocol: "http://", + hostname: daemon.remote_host, + port: daemon.remote_port + }).then(data => { + if (data.error) return { error: data.error } + return { + net_type: data.result.nettype + } + }) + } + start (options) { const { net_type } = options.app const daemon = options.daemons[net_type] @@ -81,17 +98,17 @@ export class Daemon { ] const dirs = { - "main": options.app.data_dir, - "staging": path.join(options.app.data_dir, "staging"), - "test": path.join(options.app.data_dir, "testnet") + "mainnet": options.app.data_dir, + "stagenet": path.join(options.app.data_dir, "stagenet"), + "testnet": path.join(options.app.data_dir, "testnet") } const { net_type } = options.app this.net_type = net_type - if (net_type === "test") { + if (net_type === "testnet") { args.push("--testnet") - } else if (net_type === "staging") { + } else if (net_type === "stagenet") { args.push("--stagenet") } @@ -100,7 +117,7 @@ export class Daemon { if (daemon.rpc_bind_ip !== "127.0.0.1") { args.push("--confirm-external-bind") } // TODO: Check if we need to push this command for staging too - if (daemon.type === "local_remote" && net_type === "main") { + if (daemon.type === "local_remote" && net_type === "mainnet") { args.push( "--bootstrap-daemon-address", `${daemon.remote_host}:${daemon.remote_port}` @@ -328,10 +345,15 @@ export class Daemon { this.backend.send(method, data) } - sendRPC (method, params = {}) { + sendRPC (method, params = {}, options = {}) { let id = this.id++ - let options = { - uri: `${this.protocol}${this.hostname}:${this.port}/json_rpc`, + + const protocol = options.protocol || this.protocol + const hostname = options.hostname || this.hostname + const port = options.port || this.port + + let requestOptions = { + uri: `${protocol}${hostname}:${port}/json_rpc`, method: "POST", json: { jsonrpc: "2.0", @@ -341,11 +363,11 @@ export class Daemon { agent: this.agent } if (Object.keys(params).length !== 0) { - options.json.params = params + requestOptions.json.params = params } return this.queue.add(() => { - return request(options) + return request(requestOptions) .then((response) => { if (response.hasOwnProperty("error")) { return { diff --git a/src-electron/main-process/modules/wallet-rpc.js b/src-electron/main-process/modules/wallet-rpc.js index 426b810..a53fbab 100644 --- a/src-electron/main-process/modules/wallet-rpc.js +++ b/src-electron/main-process/modules/wallet-rpc.js @@ -3,7 +3,7 @@ const request = require("request-promise") const queue = require("promise-queue") const http = require("http") const os = require("os") -const fs = require("fs") +const fs = require("fs-extra") const path = require("path") const crypto = require("crypto") @@ -14,7 +14,7 @@ export class WalletRPC { this.wallet_dir = null this.auth = [] this.id = 0 - this.net_type = "main" + this.net_type = "mainnet" this.heartbeat = null this.wallet_state = { open: false, @@ -23,7 +23,7 @@ export class WalletRPC { balance: null, unlocked_balance: null } - + this.dirs = null this.last_height_send_time = Date.now() this.height_regex1 = /Processed block: <([a-f0-9]+)>, height (\d+)/ @@ -63,31 +63,32 @@ export class WalletRPC { "--log-level", "*:WARNING,net*:FATAL,net.http:DEBUG,global:INFO,verify:FATAL,stacktrace:INFO" ] - const { net_type } = options.app + const { net_type, wallet_data_dir, data_dir } = options.app this.net_type = net_type - this.data_dir = options.app.data_dir + this.data_dir = data_dir + this.wallet_data_dir = wallet_data_dir - const dirs = { - "main": this.data_dir, - "staging": path.join(this.data_dir, "staging"), - "test": path.join(this.data_dir, "testnet") + this.dirs = { + "mainnet": this.wallet_data_dir, + "stagenet": path.join(this.wallet_data_dir, "stagenet"), + "testnet": path.join(this.wallet_data_dir, "testnet") } - this.wallet_dir = path.join(dirs[net_type], "wallets") + this.wallet_dir = path.join(this.dirs[net_type], "wallets") args.push("--wallet-dir", this.wallet_dir) - const log_file = path.join(dirs[net_type], "logs", "wallet-rpc.log") + const log_file = path.join(this.dirs[net_type], "logs", "wallet-rpc.log") args.push("--log-file", log_file) - if (net_type === "test") { + if (net_type === "testnet") { args.push("--testnet") - } else if (net_type === "staging") { + } else if (net_type === "stagenet") { args.push("--stagenet") } if (fs.existsSync(log_file)) { fs.truncateSync(log_file, 0) } - if (!fs.existsSync(this.wallet_dir)) { fs.mkdirSync(this.wallet_dir) } + if (!fs.existsSync(this.wallet_dir)) { fs.mkdirpSync(this.wallet_dir) } if (process.platform === "win32") { this.walletRPCProcess = child_process.spawn(path.join(__ryo_bin, "loki-wallet-rpc.exe"), args) @@ -160,10 +161,18 @@ export class WalletRPC { let params = data.data switch (data.method) { + case "has_password": + this.hasPassword() + break + case "validate_address": this.validateAddress(params.address) break + case "copy_old_gui_wallets": + this.copyOldGuiWallets(params.wallets || []) + break + case "list_wallets": this.listWallets() break @@ -199,8 +208,16 @@ export class WalletRPC { this.stake(params.password, params.amount, params.key, params.destination) break + case "register_service_node": + this.registerSnode(params.password, params.string) + break + + case "unlock_stake": + this.unlockStake(params.password, params.service_node_key, params.confirmed || false) + break + case "transfer": - this.transfer(params.password, params.amount, params.address, params.payment_id, params.priority, params.address_book) + this.transfer(params.password, params.amount, params.address, params.payment_id, params.priority, params.note || "", params.address_book) break case "add_address_book": @@ -246,6 +263,18 @@ export class WalletRPC { } } + hasPassword () { + crypto.pbkdf2("", this.auth[2], 1000, 64, "sha512", (err, password_hash) => { + if (err) { + this.sendGateway("set_has_password", false) + return + } + + // If the pass hash doesn't match empty string then we don't have a password + this.sendGateway("set_has_password", this.wallet_state.password_hash !== password_hash.toString("hex")) + }) + } + validateAddress (address) { this.sendRPC("validate_address", { address @@ -260,12 +289,7 @@ export class WalletRPC { const { valid, nettype } = data.result - // Check if the net types match - let ourNetType = "mainnet" - if (this.net_type === "test") ourNetType = "testnet" - if (this.net_type === "staging") ourNetType = "stagenet" - - const netMatches = ourNetType === nettype + const netMatches = this.net_type === nettype const isValid = valid && netMatches this.sendGateway("set_valid_address", { @@ -620,18 +644,22 @@ export class WalletRPC { stake (password, amount, service_node_key, destination) { crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { if (err) { - this.sendGateway("set_stake_status", { - code: -1, - message: "Internal error", - sending: false + this.sendGateway("set_snode_status", { + stake: { + code: -1, + message: "Internal error", + sending: false + } }) return } if (this.wallet_state.password_hash !== password_hash.toString("hex")) { - this.sendGateway("set_stake_status", { - code: -1, - message: "Invalid password", - sending: false + this.sendGateway("set_snode_status", { + stake: { + code: -1, + message: "Invalid password", + sending: false + } }) return } @@ -645,24 +673,150 @@ export class WalletRPC { }).then((data) => { if (data.hasOwnProperty("error")) { let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1) - this.sendGateway("set_stake_status", { - code: -1, - message: error, - sending: false + this.sendGateway("set_snode_status", { + stake: { + code: -1, + message: error, + sending: false + } }) return } - this.sendGateway("set_stake_status", { - code: 0, - message: "Successfully staked", - sending: false + this.sendGateway("set_snode_status", { + stake: { + code: 0, + message: "Successfully staked", + sending: false + } }) }) }) } - transfer (password, amount, address, payment_id, priority, address_book = {}) { + registerSnode (password, register_service_node_str) { + crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { + if (err) { + this.sendGateway("set_snode_status", { + registration: { + code: -1, + message: "Internal error", + sending: false + } + }) + return + } + + if (this.wallet_state.password_hash !== password_hash.toString("hex")) { + this.sendGateway("set_snode_status", { + registration: { + code: -1, + message: "Invalid password", + sending: false + } + }) + return + } + + this.sendRPC("register_service_node", { + register_service_node_str + }).then((data) => { + if (data.hasOwnProperty("error")) { + const error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1) + this.sendGateway("set_snode_status", { + registration: { + code: -1, + message: error, + sending: false + } + }) + return + } + + this.sendGateway("set_snode_status", { + registration: { + code: 0, + message: "Successfully registered service node", + sending: false + } + }) + }) + }) + } + + unlockStake (password, service_node_key, confirmed = false) { + // Unlock code 0 means success, 1 means can unlock, -1 means error + crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { + if (err) { + this.sendGateway("set_snode_status", { + unlock: { + code: -1, + message: "Internal error", + sending: false + } + }) + return + } + + if (this.wallet_state.password_hash !== password_hash.toString("hex")) { + this.sendGateway("set_snode_status", { + unlock: { + code: -1, + message: "Invalid password", + sending: false + } + }) + return + } + + const sendRPC = (path) => { + return this.sendRPC(path, { + service_node_key + }).then(data => { + if (data.hasOwnProperty("error")) { + const error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1) + this.sendGateway("set_snode_status", { + unlock: { + code: -1, + message: error, + sending: false + } + }) + return null + } + return data + }) + } + + if (confirmed) { + sendRPC("request_stake_unlock").then((data) => { + if (!data) return + + const unlock = { + code: data.unlocked ? 0 : -1, + message: data.msg, + sending: false + } + + this.sendGateway("set_snode_status", { unlock }) + }) + } else { + sendRPC("can_request_stake_unlock").then((data) => { + if (!data) return + + const unlock = { + code: data.can_unlock ? 1 : -1, + message: data.msg, + sending: false + } + + this.sendGateway("set_snode_status", { unlock }) + }) + } + }) + } + + transfer (password, amount, address, payment_id, priority, note, address_book = {}) { crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => { if (err) { this.sendGateway("set_tx_status", { @@ -685,65 +839,48 @@ export class WalletRPC { let sweep_all = amount == this.wallet_state.unlocked_balance - if (sweep_all) { - let params = { - "address": address, - "account_index": 0, - "priority": priority, - "mixin": 9 // Always force a ring size of 10 (ringsize = mixin + 1) - } - - if (payment_id) { - params.payment_id = payment_id - } - - this.sendRPC("sweep_all", params).then((data) => { - if (data.hasOwnProperty("error")) { - let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1) - this.sendGateway("set_tx_status", { - code: -1, - message: error, - sending: false - }) - return - } - - this.sendGateway("set_tx_status", { - code: 0, - message: "Transaction successfully sent", - sending: false - }) - }) - } else { - let params = { - "destinations": [{"amount": amount, "address": address}], - "priority": priority, - "mixin": 9 - } - - if (payment_id) { - params.payment_id = payment_id - } - - this.sendRPC("transfer_split", params).then((data) => { - if (data.hasOwnProperty("error")) { - let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1) - this.sendGateway("set_tx_status", { - code: -1, - message: error, - sending: false - }) - return - } - - this.sendGateway("set_tx_status", { - code: 0, - message: "Transaction successfully sent", - sending: false - }) - }) + const rpc_endpoint = sweep_all ? "sweep_all" : "transfer_split" + const params = sweep_all ? { + "address": address, + "account_index": 0, + "priority": priority, + "mixin": 9 // Always force a ring size of 10 (ringsize = mixin + 1) + } : { + "destinations": [{"amount": amount, "address": address}], + "priority": priority, + "mixin": 9 } + if (payment_id) { + params.payment_id = payment_id + } + + this.sendRPC(rpc_endpoint, params).then((data) => { + if (data.hasOwnProperty("error")) { + let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1) + this.sendGateway("set_tx_status", { + code: -1, + message: error, + sending: false + }) + return + } + + this.sendGateway("set_tx_status", { + code: 0, + message: "Transaction successfully sent", + sending: false + }) + + if (data.result) { + const hash_list = data.result.tx_hash_list || [] + // Save notes + if (note && note !== "") { + hash_list.forEach(txid => this.saveTxNotes(txid, note)) + } + } + }) + if (address_book.hasOwnProperty("save") && address_book.save) { this.addAddressBook(address, payment_id, address_book.description, address_book.name) } }) } @@ -891,10 +1028,10 @@ export class WalletRPC { } } - const types = ["in", "out", "pending", "failed", "pool", "miner", "snode", "gov"] + const types = ["in", "out", "pending", "failed", "pool", "miner", "snode", "gov", "stake"] types.forEach(type => { if (data.result.hasOwnProperty(type)) { - wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result[type]); + wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result[type]) } }) @@ -1063,9 +1200,71 @@ export class WalletRPC { }) } + copyOldGuiWallets (wallets) { + this.sendGateway("set_old_gui_import_status", { code: 1, failed_wallets: [] }) + + const failed_wallets = [] + + for (const wallet of wallets) { + const { type, directory } = wallet + + const old_gui_path = path.join(this.wallet_dir, "old-gui") + const dir_path = path.join(this.wallet_dir, directory) + const stat = fs.statSync(dir_path) + if (!stat.isDirectory()) continue + + // Make sure the directory has the regular and keys file + const wallet_file = path.join(dir_path, directory) + const key_file = wallet_file + ".keys" + + // If we don't have them then don't bother copying + if (!(fs.existsSync(wallet_file) && fs.existsSync(key_file))) { + failed_wallets.push(directory) + continue + } + + // Copy out the file into the relevant directory + const destination = path.join(this.dirs[type], "wallets") + if (!fs.existsSync(destination)) fs.mkdirpSync(destination) + + const new_path = path.join(destination, directory) + + try { + // Copy into temp file + if (fs.existsSync(new_path + ".atom") || fs.existsSync(new_path + ".atom.keys")) { + failed_wallets.push(directory) + continue + } + + fs.copyFileSync(wallet_file, new_path + ".atom", fs.constants.COPYFILE_EXCL) + fs.copyFileSync(key_file, new_path + ".atom.keys", fs.constants.COPYFILE_EXCL) + + // Move the folder into a subfolder + if (!fs.existsSync(old_gui_path)) fs.mkdirpSync(old_gui_path) + fs.moveSync(dir_path, path.join(old_gui_path, directory), { overwrite: true }) + } catch (e) { + // Cleanup the copied files if an error + if (fs.existsSync(new_path + ".atom")) fs.unlinkSync(new_path + ".atom") + if (fs.existsSync(new_path + ".atom.keys")) fs.unlinkSync(new_path + ".atom.keys") + failed_wallets.push(directory) + continue + } + + // Rename the imported wallets if we can + if (!fs.existsSync(new_path) && !fs.existsSync(new_path + ".keys")) { + fs.renameSync(new_path + ".atom", new_path) + fs.renameSync(new_path + ".atom.keys", new_path + ".keys") + } + } + + this.sendGateway("set_old_gui_import_status", { code: 0, failed_wallets }) + this.listWallets() + } + listWallets (legacy = false) { let wallets = { - list: [] + list: [], + directories: [] } fs.readdirSync(this.wallet_dir).forEach(filename => { @@ -1083,6 +1282,22 @@ export class WalletRPC { case ".Trashes": case "ehthumbs.db": case "Thumbs.db": + case "old-gui": + return + } + + // If it's a directory then check if it's an old gui wallet + const name = path.join(this.wallet_dir, filename) + const stat = fs.statSync(name) + if (stat.isDirectory()) { + // Make sure the directory has the regular and keys file + const wallet_file = path.join(name, filename) + const key_file = wallet_file + ".keys" + + // If we have them then it is an old gui wallet + if (fs.existsSync(wallet_file) && fs.existsSync(key_file)) { + wallets.directories.push(filename) + } return } diff --git a/src/components/address_details.vue b/src/components/address_details.vue index 5292fcd..a9f6bf4 100644 --- a/src/components/address_details.vue +++ b/src/components/address_details.vue @@ -107,6 +107,9 @@ + + + @@ -127,7 +130,7 @@ + + diff --git a/src/components/service_node_staking.vue b/src/components/service_node_staking.vue new file mode 100644 index 0000000..2203fd3 --- /dev/null +++ b/src/components/service_node_staking.vue @@ -0,0 +1,276 @@ + + + + + diff --git a/src/components/service_node_unlock.vue b/src/components/service_node_unlock.vue new file mode 100644 index 0000000..d1e59aa --- /dev/null +++ b/src/components/service_node_unlock.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/src/components/settings_general.vue b/src/components/settings_general.vue index ea446fd..aaea821 100644 --- a/src/components/settings_general.vue +++ b/src/components/settings_general.vue @@ -53,7 +53,7 @@ hide-underline /> - + @@ -80,12 +80,17 @@ -
+
Select Location + + + + Select Location +
@@ -142,9 +147,9 @@ type="radio" v-model="config.app.net_type" :options="[ - { label: 'Main Net', value: 'main' }, - { label: 'Stage Net', value: 'staging' }, - { label: 'Test Net', value: 'test' } + { label: 'Main Net', value: 'mainnet' }, + { label: 'Stage Net', value: 'stagenet' }, + { label: 'Test Net', value: 'testnet' } ]" /> @@ -158,6 +163,13 @@ import { mapState } from "vuex" import LokiField from "components/loki_field" export default { name: "SettingsGeneral", + props: { + randomise_remote: { + type: Boolean, + required: false, + default: false, + }, + }, computed: mapState({ theme: state => state.gateway.app.config.appearance.theme, remotes: state => state.gateway.app.remotes, @@ -173,6 +185,12 @@ export default { return this.defaults.daemons[this.config.app.net_type] } }), + mounted () { + if(this.randomise_remote && this.remotes.length > 0 && this.config.app.net_type === "mainnet") { + const index = Math.floor(Math.random() * Math.floor(this.remotes.length)); + this.setPreset(this.remotes[index]); + } + }, methods: { selectPath () { this.$refs.fileInput.click() @@ -180,6 +198,9 @@ export default { setDataPath (file) { this.config.app.data_dir = file.target.files[0].path }, + setWalletDataPath (file) { + this.config.app.wallet_data_dir = file.target.files[0].path + }, setPreset (option) { if (!option) return; @@ -227,6 +248,12 @@ export default { } } + .col.pt-sm { + > * + * { + padding-top: 16px; + } + } + .remote-dropdown { padding: 0 !important; } diff --git a/src/components/tx_details.vue b/src/components/tx_details.vue index c4a4768..b87802f 100644 --- a/src/components/tx_details.vue +++ b/src/components/tx_details.vue @@ -198,7 +198,11 @@ export default { for(j=0; j < address_book.length; j++) { console.log(destination.address, address_book[j].address) if(destination.address == address_book[j].address) { - destination.name = address_book[j].description + let name = address_book[j].description + if (name === "") { + name = address_book[j].name + } + destination.name = name break; } } diff --git a/src/components/tx_list.vue b/src/components/tx_list.vue index 1c31518..ade88ad 100644 --- a/src/components/tx_list.vue +++ b/src/components/tx_list.vue @@ -64,6 +64,7 @@ import Identicon from "components/identicon" import TxTypeIcon from "components/tx_type_icon" import TxDetails from "components/tx_details" import FormatLoki from "components/format_loki" + export default { name: "TxList", props: { @@ -119,8 +120,12 @@ export default { } }, tx_list: { - handler(val, old){ - if(val.length == old.length) return + handler(val, old ) { + // Check if anything changed in the tx list + if(val.length == old.length) { + const changed = val.filter((v, i) => v.note !== old[i].note) + if (changed.length === 0) return + } this.filterTxList() this.pageTxList() } @@ -170,6 +175,8 @@ export default { return "Service Node" case "gov": return "Governance" + case "stake": + return "Stake" default: return "-" } @@ -177,9 +184,9 @@ export default { }, methods: { filterTxList () { - const all_in = ['in', 'pool', "miner", "snode", "gov"] - const all_out = ['out', 'pending'] - const all_pending = ['pending', 'pool'] + const all_in = ["in", "pool", "miner", "snode", "gov"] + const all_out = ["out", "pending", "stake"] + const all_pending = ["pending", "pool"] this.tx_list_filtered = this.tx_list.filter((tx) => { let valid = true @@ -306,6 +313,10 @@ export default { .main { margin: 0; padding: 8px 10px; + div { + overflow: hidden; + text-overflow: ellipsis; + } } .type { diff --git a/src/components/wallet_settings.vue b/src/components/wallet_settings.vue index 90f82e0..925016d 100644 --- a/src/components/wallet_settings.vue +++ b/src/components/wallet_settings.vue @@ -38,7 +38,7 @@ - +
@@ -50,6 +50,7 @@
@@ -69,6 +70,7 @@
@@ -88,6 +90,7 @@
@@ -226,6 +229,7 @@ diff --git a/src/css/app.styl b/src/css/app.styl index 1aad4be..d663b13 100644 --- a/src/css/app.styl +++ b/src/css/app.styl @@ -77,7 +77,7 @@ footer, } .q-item-sublabel { - colr: #cecece; + color: #cecece; } .advanced-options-label .q-item-side-right { @@ -307,6 +307,12 @@ footer, } } + .q-item.tx-stake { + .amount span { + color: goldenrod; + } + } + .q-item.tx-out, .q-item.tx-pending { .amount span { @@ -431,7 +437,7 @@ footer, } } - .q-item:hover { + .q-item:hover, .q-item.selected { background: $primary !important; .wallet-icon { @@ -441,6 +447,10 @@ footer, } } + .q-icon { + color: white; + } + .q-item-sublabel { color: white } @@ -550,7 +560,7 @@ footer, } } -.service-node-page { +.service-node-staking { .address-type { color: $loki-green-solid; &.not-ours { @@ -558,3 +568,20 @@ footer, } } } + +.service-node-registration { + .description{ + color: #b7b7b7; + font-style: normal; + b { + color: white; + font-style: italic; + } + } +} + +.welcome { + .q-layout-footer { + background: $secondary + } +} diff --git a/src/gateway/gateway.js b/src/gateway/gateway.js index 0c725bf..082bf0d 100644 --- a/src/gateway/gateway.js +++ b/src/gateway/gateway.js @@ -100,6 +100,9 @@ export class Gateway extends EventEmitter { !decrypted_data.hasOwnProperty("data")) { return } switch (decrypted_data.event) { + case "set_has_password": + this.emit("has_password", decrypted_data.data) + break case "set_valid_address": this.emit("validate_address", decrypted_data.data) break @@ -124,8 +127,12 @@ export class Gateway extends EventEmitter { this.app.store.commit("gateway/set_tx_status", decrypted_data.data) break - case "set_stake_status": - this.app.store.commit("gateway/set_stake_status", decrypted_data.data) + case "set_snode_status": + this.app.store.commit("gateway/set_snode_status", decrypted_data.data) + break + + case "set_old_gui_import_status": + this.app.store.commit("gateway/set_old_gui_import_status", decrypted_data.data) break case "wallet_list": diff --git a/src/layouts/wallet-select/main.vue b/src/layouts/wallet-select/main.vue index 49f23a4..aa6ea5f 100644 --- a/src/layouts/wallet-select/main.vue +++ b/src/layouts/wallet-select/main.vue @@ -12,7 +12,7 @@ -
+
@@ -59,6 +59,8 @@ export default { return "Restore view-only wallet" case "wallet-import-legacy": return "Import wallet from legacy gui" + case "wallet-import-old-gui": + return "Import wallets from old GUI" case "wallet-created": return "Wallet created/restored" diff --git a/src/layouts/wallet/main.vue b/src/layouts/wallet/main.vue index de19adf..c26de0f 100644 --- a/src/layouts/wallet/main.vue +++ b/src/layouts/wallet/main.vue @@ -2,7 +2,11 @@ - + +
+ +
+
@@ -96,9 +100,10 @@ export default { diff --git a/src/pages/wallet-select/import-old-gui.vue b/src/pages/wallet-select/import-old-gui.vue new file mode 100644 index 0000000..253e297 --- /dev/null +++ b/src/pages/wallet-select/import-old-gui.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/src/pages/wallet-select/index.vue b/src/pages/wallet-select/index.vue index 743cae8..916130a 100644 --- a/src/pages/wallet-select/index.vue +++ b/src/pages/wallet-select/index.vue @@ -16,7 +16,7 @@
- +
969 @@ -58,6 +58,7 @@ const { clipboard } = require("electron") import { mapState } from "vuex" import Identicon from "components/identicon" + export default { computed: mapState({ theme: state => state.gateway.app.config.appearance.theme, @@ -68,7 +69,7 @@ export default { // // // - return [ + const actions = [ { name: "Create new wallet", handler: this.createNewWallet, @@ -82,8 +83,20 @@ export default { handler: this.importWallet, } ]; + + if (this.wallets.directories.length > 0) { + actions.push( { + name: "Import wallets from old GUI", + handler: this.importOldGuiWallets, + }) + } + + return actions } }), + created () { + this.$gateway.send("wallet", "list_wallets") + }, methods: { openWallet(wallet) { if(wallet.password_protected !== false) { @@ -129,6 +142,9 @@ export default { importWallet() { this.$router.replace({ path: "wallet-select/import" }); }, + importOldGuiWallets() { + this.$router.replace({ path: "wallet-select/import-old-gui" }); + }, importLegacyWallet() { this.$router.replace({ path: "wallet-select/import-legacy" }); }, @@ -183,6 +199,10 @@ export default { diff --git a/src/pages/wallet/txhistory.vue b/src/pages/wallet/txhistory.vue index b828048..d1cfe62 100644 --- a/src/pages/wallet/txhistory.vue +++ b/src/pages/wallet/txhistory.vue @@ -45,6 +45,7 @@ export default { {label: "Miner", value: "miner"}, {label: "Service Node", value: "snode"}, {label: "Governance", value: "gov"}, + {label: "Stake", value: "stake"}, {label: "Failed", value: "failed"}, ] diff --git a/src/pages/wallet/wallet.vue b/src/pages/wallet/wallet.vue index 2072d9b..3e29930 100644 --- a/src/pages/wallet/wallet.vue +++ b/src/pages/wallet/wallet.vue @@ -1,3 +1,7 @@ +/** + This is an unused class in LOKI + */ +