More features and fixes

Linting

Increased default window size.
Fixed transaction text overlapping on a small screen

More changes.

Replaced icons.
Added loki icon to the top menu bar.
Added small margin to copy button in show private key modal.

Hid back and next button in welcome screen. Users must now click the language to go to the next screen.

Randomise remote nodes when configuring from the welcome screen.

Added viewing QR code from receive
Added QR copying

Made mac icon a bit smaller

Updated links and added loki project to copyright
check data dir exists

Updated service node page.
Moved staking into its own component.

Check remote node before booting up daemon.
Update restoration start date.
Made network types in GUI match those returned from lokid (main -> mainnet, staging -> stagenet, test -> testnet).
Removed unaccessible remote nodes.

Separate data and wallet directories.

Updated created page layout

Don't ask user for a password if it's not set.
Show a dialog to the user if they don't set a password.

Show staking transaction.

Added option to import file from old gui.

Added saving transaction notes straight from send.
Fix updated tx notes not showing in transactions

Clean up transfer code

Minor fixes

Show user error if a wallet failed to import.
bug fixes
This commit is contained in:
Mikunj 2019-03-14 08:58:24 +11:00
parent bcf21c3804
commit e7727f628f
45 changed files with 1752 additions and 610 deletions

View File

@ -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

28
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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"

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -41,8 +41,8 @@ function createWindow () {
*/
let mainWindowState = windowStateKeeper({
defaultWidth: 800,
defaultHeight: 650
defaultWidth: 900,
defaultHeight: 700
})
mainWindow = new BrowserWindow({

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -107,6 +107,9 @@
</qrcode-vue>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyQR()">
<q-item-main label="Copy QR code" />
</q-item>
<q-item v-close-overlay @click.native="saveQR()">
<q-item-main label="Save QR code to file" />
</q-item>
@ -127,7 +130,7 @@
<script>
import { mapState } from "vuex"
const {clipboard} = require("electron")
const { clipboard, nativeImage } = require("electron")
import AddressHeader from "components/address_header"
import FormatLoki from "components/format_loki"
import QrcodeVue from "qrcode.vue";
@ -144,6 +147,16 @@ export default {
}
},
methods: {
copyQR () {
const data = this.$refs.qr.$el.childNodes[0].toDataURL()
const img = nativeImage.createFromDataURL(data)
clipboard.writeImage(img)
this.$q.notify({
type: "positive",
timeout: 1000,
message: "Copied QR code to clipboard"
})
},
saveQR() {
let img = this.$refs.qr.$el.childNodes[0].toDataURL()
this.$gateway.send("core", "save_png", {img, type: "QR Code"})

View File

@ -77,7 +77,7 @@ export default {
} else {
if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "scanning"
} else if(this.daemon.info.height_without_bootstrap < this.target_height) {
} else if(this.config_daemon.type === "local_remote" && this.daemon.info.height_without_bootstrap < this.target_height) {
return "syncing"
} else {
return "ready"

View File

@ -34,6 +34,7 @@
<img class="q-mb-md" src="statics/loki.svg" height="42" />
<p class="q-my-sm">Version: ATOM v{{version}}-v{{daemonVersion}}</p>
<p class="q-my-sm">Copyright (c) 2018-2019, Loki Project</p>
<p class="q-my-sm">Copyright (c) 2018, Ryo Currency Project</p>
<p class="q-my-sm">All rights reserved.</p>
@ -45,7 +46,7 @@
<a @click="openExternal('https://t.me/joinchat/DeNvR0JJ4JPn6TVSQjCsZQ')" href="#">Telegram</a> -
<a @click="openExternal('https://discordapp.com/invite/67GXfD6')" href="#">Discord</a> -
<a @click="openExternal('https://www.reddit.com/r/LokiProject/')" href="#">Reddit</a> -
<a @click="openExternal('https://github.com')" href="#">Github</a>
<a @click="openExternal('https://github.com/loki-project/loki-electron-wallet')" href="#">Github</a>
</p>
</div>
@ -103,7 +104,7 @@ export default {
title: "Switch wallet",
message: "Are you sure you want to close the current wallet?",
ok: {
label: "CLOSE"
label: "OK"
},
cancel: {
flat: true,

View File

@ -0,0 +1,120 @@
<template>
<div class="service-node-registration">
<div class="q-pa-md">
<div class="description q-mb-lg">
Enter the <b>register_service_node</b> command produced by the daemon that is registering to become a Service Node using the "<b>prepare_registration</b>" command
</div>
<LokiField label="Service Node Command" :error="$v.registration_string.$error" :disabled="registration_status.sending">
<q-input
v-model="registration_string"
type="textarea"
:dark="theme=='dark'"
@blur="$v.registration_string.$touch"
placeholder="register_service_node ..."
:disabled="registration_status.sending"
hide-underline
/>
</LokiField>
<q-field class="q-pt-sm">
<q-btn color="primary" @click="register()" label="Register service node" :disabled="registration_status.sending"/>
</q-field>
</div>
<q-inner-loading :visible="registration_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
</div>
</template>
<script>
const objectAssignDeep = require("object-assign-deep");
import { mapState } from "vuex"
import { required } from "vuelidate/lib/validators"
import LokiField from "components/loki_field"
import WalletPassword from "src/mixins/wallet_password"
export default {
name: "ServiceNodeRegistration",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
registration_status: state => state.gateway.service_node_status.registration,
}),
data () {
return {
registration_string: "",
}
},
validations: {
registration_string: { required }
},
watch: {
registration_status: {
handler(val, old){
if(val.code == old.code) return
switch(this.registration_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.registration_status.message
})
this.$v.$reset();
this.registration_string = ""
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message: this.registration_status.message
})
break;
}
},
deep: true
},
},
methods: {
register: function() {
this.$v.registration_string.$touch()
if (this.$v.registration_string.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Please enter the service node registration command"
})
return
}
this.showPasswordConfirmation({
title: "Register service node",
noPasswordMessage: "Do you want to register the service node?",
ok: {
label: "REGISTER"
},
}).then(password => {
this.$store.commit("gateway/set_snode_status", {
registration: {
code: 1,
message: "Registering...",
sending: true
}
})
this.$gateway.send("wallet", "register_service_node", {
password,
string: this.registration_string
})
}).catch(() => {
})
}
},
mixins: [WalletPassword],
components: {
LokiField
}
}
</script>
<style lang="scss">
</style>

View File

@ -0,0 +1,276 @@
<template>
<div class="service-node-staking">
<div class="q-pa-md">
<LokiField label="Service Node Key" :error="$v.service_node.key.$error">
<q-input v-model="service_node.key"
:dark="theme=='dark'"
@blur="$v.service_node.key.$touch"
placeholder="64 hexadecimal characters"
hide-underline
/>
</LokiField>
<div class="q-mt-md col">
<LokiField label="Award Recepient's Address" :error="$v.service_node.award_address.$error">
<q-input v-model="service_node.award_address"
:dark="theme=='dark'"
@blur="$v.service_node.award_address.$touch"
placeholder="64 hexadecimal characters"
hide-underline
/>
</LokiField>
<div class="address-type" :class="[addressType]">( {{ addressType | addressTypeString }} )</div>
</div>
<LokiField label="Amount" class="q-mt-md" :error="$v.service_node.amount.$error">
<q-input v-model="service_node.amount"
:dark="theme=='dark'"
type="number"
min="0"
:max="unlocked_balance / 1e9"
placeholder="0"
@blur="$v.service_node.amount.$touch"
hide-underline
/>
<q-btn color="secondary" @click="service_node.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">All</q-btn>
</LokiField>
<q-field class="q-pt-sm">
<q-btn
:disable="!is_able_to_send"
color="primary" @click="stake()" label="Stake" />
</q-field>
</div>
<q-inner-loading :visible="stake_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
</div>
</template>
<script>
const { clipboard } = require("electron")
const objectAssignDeep = require("object-assign-deep");
import { mapState } from "vuex"
import { required, decimal } from "vuelidate/lib/validators"
import { payment_id, service_node_key, greater_than_zero, address } from "src/validators/common"
import LokiField from "components/loki_field"
import WalletPassword from "src/mixins/wallet_password"
export default {
name: "ServiceNodeStaking",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
unlocked_balance: state => state.gateway.wallet.info.unlocked_balance,
info: state => state.gateway.wallet.info,
address_list: state => state.gateway.wallet.address_list,
stake_status: state => state.gateway.service_node_status.stake,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
is_able_to_send (state) {
return this.$store.getters["gateway/isAbleToSend"]
},
addressType (state) {
const address = this.service_node.award_address;
const inArray = (array) => array.map(o => o.address).includes(address);
const { primary, used, unused } = this.address_list
if (inArray(primary)) {
return "primary"
} else if (inArray(used)) {
return "used"
} else if (inArray(unused)) {
return "unsued"
} else {
return "not-ours"
}
}
}),
data () {
return {
service_node: {
key: "",
amount: 0,
award_address: "",
},
}
},
filters: {
addressTypeString: function (value) {
switch (value) {
case "primary":
return "Your primary address"
case "used":
return "Your used address"
case "ununsed":
return "Your unused address"
default:
return "Not your address!"
}
}
},
validations: {
service_node: {
key: { required, service_node_key },
amount: {
required,
decimal,
greater_than_zero,
},
award_address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
}
}
},
watch: {
stake_status: {
handler(val, old){
if(val.code == old.code) return
switch(this.stake_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.stake_status.message
})
this.$v.$reset();
this.service_node = {
key: "",
amount: 0,
award_address: "",
}
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.stake_status.message
})
break;
}
},
deep: true
},
},
created () {
const { address } = this.info;
if (!this.service_node.award_address || this.service_node.award_address === "") {
this.service_node.award_address = address || ""
}
},
methods: {
isOurAddress (address) {
const { primary, used, unused } = this.address_list
const addresses = [...primary, ...used, ...unused].map(o => o.address);
console.log(addresses);
return addresses.includes(address);
},
stake: function () {
this.$v.service_node.$touch()
if (this.$v.service_node.key.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Service node key not valid"
})
return
}
if (this.$v.service_node.award_address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Address not valid"
})
return
}
if(this.service_node.amount < 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount cannot be negative"
})
return
} else if(this.service_node.amount == 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount must be greater than zero"
})
return
} else if(this.service_node.amount > this.unlocked_balance / 1e9) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Not enough unlocked balance"
})
return
} else if (this.$v.service_node.amount.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount not valid"
})
return
}
this.showPasswordConfirmation({
title: "Stake",
noPasswordMessage: "Do you want to stake?",
ok: {
label: "STAKE"
},
}).then(password => {
this.$store.commit("gateway/set_snode_status", {
stake: {
code: 1,
message: "Staking...",
sending: true
}
})
const service_node = objectAssignDeep.noMutate(this.service_node, {password})
this.$gateway.send("wallet", "stake", {
...service_node,
destination: service_node.award_address,
})
}).catch(() => {
})
}
},
mixins: [WalletPassword],
components: {
LokiField
}
}
</script>
<style lang="scss">
.service-node-staking {
.address-type {
margin-top: 4px;
font-size: 13px;
font-weight: 400;
text-align: right;
&.not-ours {
font-weight: bold;
}
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<div class="service-node-unlock">
<div class="q-pa-md">
<LokiField label="Service Node Key" :error="$v.node_key.$error" :disabled="unlock_status.sending">
<q-input
v-model="node_key"
:dark="theme=='dark'"
@blur="$v.node_key.$touch"
placeholder="64 hexadecimal characters"
:disabled="unlock_status.sending"
hide-underline
/>
</LokiField>
<q-field class="q-pt-sm">
<q-btn color="primary" @click="unlock()" label="Unlock service node" :disabled="unlock_status.sending"/>
</q-field>
</div>
<q-inner-loading :visible="unlock_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
</div>
</template>
<script>
const objectAssignDeep = require("object-assign-deep");
import { mapState } from "vuex"
import { required } from "vuelidate/lib/validators"
import { service_node_key } from "src/validators/common"
import LokiField from "components/loki_field"
import WalletPassword from "src/mixins/wallet_password"
export default {
name: "ServiceNodeUnlock",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
unlock_status: state => state.gateway.service_node_status.unlock,
}),
data () {
return {
node_key: "",
}
},
validations: {
node_key: { required, service_node_key }
},
watch: {
unlock_status: {
handler(val, old){
if(val.code == old.code) return
switch(this.unlock_status.code) {
case 0:
this.key = null
this.password = null
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.unlock_status.message
})
this.$v.$reset();
this.node_key = ""
break;
case 1:
// Tell the user to confirm
this.$q.dialog({
title: "Confirm",
message: this.unlock_status.message,
ok: {
label: "UNLOCK"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(() => {
this.gatewayUnlock(this.password, this.key, true);
}).catch(() => {})
break;
case -1:
this.key = null
this.password = null
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.unlock_status.message
})
break;
}
},
deep: true
},
},
methods: {
unlock: function () {
this.$v.node_key.$touch()
if (this.$v.node_key.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Service node key not valid"
})
return
}
// We store this as it could change between the 2 step process
this.key = this.node_key
this.showPasswordConfirmation({
title: "Unlock service node",
noPasswordMessage: "Do you want to unlock the service node?",
ok: {
label: "UNLOCK"
},
}).then(password => {
this.password = password
this.gatewayUnlock(this.password, this.key, false);
}).catch(() => {
})
},
gatewayUnlock: function (password, key, confirmed = false) {
this.$store.commit("gateway/set_snode_status", {
unlock: {
code: 2, // Code 1 is reserved for can_unlock
message: "Unlocking...",
sending: true
}
})
this.$gateway.send("wallet", "unlock_stake", {
password,
service_node_key: key,
confirmed
})
},
},
mixins: [WalletPassword],
components: {
LokiField
}
}
</script>
<style lang="scss">
</style>

View File

@ -53,7 +53,7 @@
hide-underline
/>
<!-- Remote node presets -->
<q-btn-dropdown class="remote-dropdown" v-if="config.app.net_type === 'main'" flat>
<q-btn-dropdown class="remote-dropdown" v-if="config.app.net_type === 'mainnet'" flat>
<q-list link dark no-border>
<q-item v-for="option in remotes" :key="option.host" @click.native="setPreset(option)" v-close-overlay>
<q-item-main>
@ -80,12 +80,17 @@
</template>
<div class="col q-mt-md">
<div class="col q-mt-md pt-sm">
<LokiField label="Data Storage Path" disable-hover>
<q-input v-model="config.app.data_dir" disable :dark="theme=='dark'" hide-underline/>
<input type="file" webkitdirectory directory id="dataPath" v-on:change="setDataPath" ref="fileInput" hidden />
<q-btn color="secondary" v-on:click="selectPath" :text-color="theme=='dark'?'white':'dark'">Select Location</q-btn>
</LokiField>
<LokiField label="Wallet Storage Path" disable-hover>
<q-input v-model="config.app.wallet_data_dir" disable :dark="theme=='dark'" hide-underline/>
<input type="file" webkitdirectory directory id="dataPath" v-on:change="setWalletDataPath" ref="fileInput" hidden />
<q-btn color="secondary" v-on:click="selectPath" :text-color="theme=='dark'?'white':'dark'">Select Location</q-btn>
</LokiField>
</div>
<q-collapsible label="Advanced Options" header-class="q-mt-sm non-selectable row reverse advanced-options-label">
@ -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' }
]"
/>
</q-field>
@ -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;
}

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -38,7 +38,7 @@
</q-btn>
<!-- Modals -->
<q-modal minimized v-model="modals.private_keys.visible" @hide="closePrivateKeys()">
<q-modal minimized class="private-key-modal" v-model="modals.private_keys.visible" @hide="closePrivateKeys()">
<div class="modal-header">Show private keys</div>
<div class="q-ma-lg">
@ -50,6 +50,7 @@
</div>
<div class="col-auto">
<q-btn
class="copy-btn"
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('mnemonic', $event)">
@ -69,6 +70,7 @@
</div>
<div class="col-auto">
<q-btn
class="copy-btn"
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('view_key', $event)">
@ -88,6 +90,7 @@
</div>
<div class="col-auto">
<q-btn
class="copy-btn"
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('spend_key', $event)">
@ -226,6 +229,7 @@
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import WalletPassword from "src/mixins/wallet_password"
export default {
name: "WalletSettings",
@ -348,21 +352,12 @@ export default {
},
getPrivateKeys () {
if(!this.is_ready) return
this.$q.dialog({
this.showPasswordConfirmation({
title: "Show private keys",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
noPasswordMessage: "Do you want to view your private keys?",
ok: {
label: "SHOW"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$gateway.send("wallet", "get_private_keys", {password})
}).catch(() => {
@ -417,21 +412,12 @@ export default {
doKeyImages () {
this.hideModal("key_image")
this.$q.dialog({
this.showPasswordConfirmation({
title: this.modals.key_image.type + " key images",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
noPasswordMessage: `Do you want to ${this.modals.key_image.type.toLowerCase()} key images?`,
ok: {
label: this.modals.key_image.type
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
if(this.modals.key_image.type == "Export")
this.$gateway.send("wallet", "export_key_images", {password: password, path: this.modals.key_image.export_path})
@ -484,7 +470,10 @@ export default {
color: this.theme=="dark"?"white":"dark"
}
}).then(() => {
this.$q.dialog({
return this.hasPassword()
}).then(hasPassword => {
if (!hasPassword) return ""
return this.$q.dialog({
title: "Delete wallet",
message: "Enter wallet password to continue.",
prompt: {
@ -500,17 +489,22 @@ export default {
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$gateway.send("wallet", "delete_wallet", {password})
}).catch(() => {
})
}).then(password => {
this.$gateway.send("wallet", "delete_wallet", {password})
}).catch(() => {
})
}
},
mixins: [WalletPassword],
}
</script>
<style lang="scss">
.private-key-modal {
.copy-btn {
margin-left: 8px;
}
}
</style>

View File

@ -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
}
}

View File

@ -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":

View File

@ -12,7 +12,7 @@
</template>
<q-toolbar-title v-if="page_title=='Loki'">
<div style="margin-top:7px">
<div class="flex items-center justify-center" style="margin-top:7px">
<img src="statics/loki.svg" height="32">
</div>
</q-toolbar-title>
@ -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"

View File

@ -2,7 +2,11 @@
<q-layout view="hHh Lpr lFf">
<q-layout-header class="shift-title">
<main-menu />
<q-toolbar-title>
<div class="flex items-center justify-center" style="margin-top:7px">
<img src="statics/loki.svg" height="32">
</div>
</q-toolbar-title>
</q-layout-header>
<q-page-container>
@ -96,9 +100,10 @@ export default {
<style lang="scss">
.navigation {
padding: 12px;
padding: 8px 12px;
> * {
margin: 2px 0;
margin-right: 12px;
}

View File

@ -0,0 +1,38 @@
import { mapState } from "vuex"
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme
}),
methods: {
hasPassword () {
// Validate the address
return new Promise((resolve) => {
this.$gateway.once("has_password", (data) => {
resolve(!!data)
})
this.$gateway.send("wallet", "has_password")
})
},
showPasswordConfirmation (options) {
const { noPasswordMessage, ...other } = options
return this.hasPassword().then(hasPassword => {
return this.$q.dialog({
cancel: {
flat: true,
label: "CANCEL",
color: this.theme === "dark" ? "white" : "dark"
},
...other,
message: hasPassword ? "Enter wallet password to continue." : noPasswordMessage,
prompt: hasPassword ? {
model: "",
type: "password"
} : null
})
}).then(password => password || "")
}
}
}

View File

@ -1,7 +1,7 @@
<template>
<q-page class="welcome">
<q-stepper class="no-shadow" ref="stepper" :color="theme == 'dark' ? 'light' : 'dark'" dark>
<q-stepper class="no-shadow" ref="stepper" :color="theme == 'dark' ? 'light' : 'dark'" dark @step="onStep">
<q-step default title="Welcome" class="first-step">
@ -11,14 +11,13 @@
<h6 class="q-mb-md" style="font-weight: 300">Select language:</h6>
<q-btn-toggle
v-model="choose_lang"
toggle-color="primary"
<q-btn
color="primary"
size="md"
:options="[
{label: 'English', value: 'EN', icon: 'language'},
]"
/>
icon="language"
label="English"
@click="clickNext()"
/>
<p class="q-mt-md">More languages coming soon</p>
</div>
@ -26,31 +25,28 @@
</q-step>
<q-step title="Configure">
<SettingsGeneral ref="settingsGeneral"></SettingsGeneral>
<SettingsGeneral randomise_remote ref="settingsGeneral" />
</q-step>
</q-stepper>
<q-layout-footer class="no-shadow q-pa-sm">
<q-layout-footer v-if="!is_first_page" class="no-shadow q-pa-sm">
<div class="row justify-end">
<div>
<q-btn
flat
@click="clickPrev()"
label="Back"
/>
<q-btn
flat
@click="clickPrev()"
label="Back"
/>
</div>
<div>
<q-btn
<q-btn
class="q-ml-sm"
color="primary"
@click="clickNext()"
label="Next"
/>
@click="clickNext()"
label="Next"
/>
</div>
</div>
</q-layout-footer>
</q-page>
@ -70,6 +66,7 @@ export default {
}),
data() {
return {
is_first_page: true,
choose_lang: "EN",
version: "",
daemonVersion: ""
@ -88,6 +85,9 @@ export default {
});
},
methods: {
onStep () {
this.is_first_page = this.$refs.stepper.steps[0].active
},
clickNext () {
if(this.$refs.stepper.steps[this.$refs.stepper.length-1].active) {
this.$gateway.send("core", "save_config_init", this.pending_config);

View File

@ -133,11 +133,31 @@ export default {
return
}
this.$q.loading.show({
delay: 0
})
// Warn user if no password is set
let passwordPromise = Promise.resolve();
if (!this.wallet.password) {
passwordPromise = this.$q.dialog({
title: "No password set",
message: "Are you sure you want to create a wallet with no password?",
ok: {
label: "YES",
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme === "dark" ? "white" : "dark"
},
})
}
this.$gateway.send("wallet", "create_wallet", this.wallet);
passwordPromise
.then(() => {
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "create_wallet", this.wallet)
})
.catch(() => {})
},
cancel() {
this.$router.replace({ path: "/wallet-select" });

View File

@ -1,32 +1,10 @@
<template>
<q-page padding>
<AddressHeader :address="info.address" :title="walletName"/>
<template v-if="secret.mnemonic">
<h6 class="q-mb-xs q-mt-lg">Seed words</h6>
<div class="row">
<div class="col">
{{ secret.mnemonic }}
</div>
<div class="q-item-side">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('mnemonic', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy seed words
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<template v-if="secret.view_key != secret.spend_key">
<h6 class="q-mb-xs">View key</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.view_key }}
<q-page padding class="created">
<div class="col wallet q-mb-lg">
<h6>{{walletName}}</h6>
<div class="row items-center">
<div class="col address">
{{ info.address }}
</div>
<div class="q-item-side">
<q-btn
@ -39,30 +17,70 @@
</q-btn>
</div>
</div>
</template>
</div>
<template v-if="!/^0*$/.test(secret.spend_key)">
<h6 class="q-mb-xs">Spend key</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.spend_key }}
<template v-if="secret.mnemonic">
<div class="seed-box col">
<h6 class="q-mb-xs q-mt-lg">Seed words</h6>
<div class="seed q-my-lg">
{{ secret.mnemonic }}
</div>
<div class="q-item-side">
<div class="q-my-md warning">
Please copy and save these in a secure location!
</div>
<div>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('spend_key', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy spend key
</q-tooltip>
</q-btn>
color="primary"
size="md"
icon="file_copy"
label="Copy seed words"
@click="copyPrivateKey('mnemonic', $event)"
/>
</div>
</div>
</template>
<q-field>
<q-btn class="q-mt-lg" color="primary" @click="open" label="Open wallet" />
</q-field>
<q-collapsible label="Advanced" header-class="q-mt-sm non-selectable row reverse advanced-options-label">
<template v-if="secret.view_key != secret.spend_key">
<h6 class="q-mb-xs title">View key</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.view_key }}
</div>
<div class="q-item-side">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('view_key', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy view key
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<template v-if="!/^0*$/.test(secret.spend_key)">
<h6 class="q-mb-xs title">Spend key</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.spend_key }}
</div>
<div class="q-item-side">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('spend_key', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy spend key
</q-tooltip>
</q-btn>
</div>
</div>
</template>
</q-collapsible>
<q-btn class="q-mt-lg" color="primary" @click="open" label="Open wallet" />
</q-page>
</template>
@ -76,7 +94,7 @@ export default {
info: state => state.gateway.wallet.info,
secret: state => state.gateway.wallet.secret,
walletName (state) {
return `Your Wallet (${this.info.name})`
return `Wallet: ${this.info.name}`
}
}),
methods: {
@ -141,5 +159,45 @@ export default {
}
</script>
<style>
<style lang="scss">
.created {
.wallet h6 {
text-align: center;
}
.address {
text-align: center;
word-break: break-all;
}
.seed-box {
border: 1px solid white;
border-radius: 3px;
margin: 16px;
padding: 16px;
div, h6 {
text-align: center;
}
.seed {
font-size: 24px;
text-transform: uppercase;
font-weight: 600;
}
.warning {
color: goldenrod;
}
}
h6 {
font-size: 18px;
margin: 8px 0;
font-weight: 450;
}
.advanced-options-label {
padding-left: 0;
padding-right: 0;
}
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<q-page>
<div class="q-mx-md import-old-gui">
<q-list link dark no-border class="wallet-list">
<q-item v-for="state in directory_state" :key="state.directory" :class="{selected : state.selected}">
<q-item-side>
<q-checkbox v-model="state.selected" />
</q-item-side>
<q-item-main @click.native="state.selected = !state.selected">
<q-item-tile label>{{ state.directory }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-select hide-underline dark class="q-ma-none full-width" v-model="state.type" :options="selectOptions" />
</q-item-side>
</q-item>
</q-list>
<q-field>
<q-btn color="primary" @click="import_wallets" label="Import wallets" :disable="selectedWallets.length === 0"/>
</q-field>
</div>
</q-page>
</template>
<script>
import { mapState } from "vuex"
export default {
data () {
return {
directory_state: [],
}
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
directories: state => state.gateway.wallets.directories,
old_gui_import_status: state => state.gateway.old_gui_import_status,
selectOptions: state => [
{
label: 'Main',
value: 'mainnet'
},
{
label: 'Staging',
value: 'stagenet'
},
{
label: 'Test',
value: 'testnet'
},
],
selectedWallets () {
return this.directory_state.filter(s => s.selected)
}
}),
watch: {
directories: {
handler(val, old) {
this.populate_state()
},
deep: true
},
old_gui_import_status: {
handler(val, old) {
if(val.code == old.code) return
const { code, failed_wallets } = this.old_gui_import_status
// Imported
if (code === 0) {
this.$q.loading.hide()
if (failed_wallets.length === 0) {
this.$router.replace({ path: "/wallet-select" });
} else {
failed_wallets.forEach(wallet => {
this.$q.notify({
type: "negative",
timeout: 3000,
message: `Failed to import wallet: ${wallet}`
})
})
}
}
},
deep: true
}
},
created () {
this.$gateway.send("wallet", "list_wallets")
this.populate_state()
},
methods: {
populate_state () {
// Keep any directories that intersect
const new_state = this.directory_state.filter(state => this.directories.includes(state.directory))
// Add in new directories
this.directories
.filter(dir => !new_state.find(state => state.directory === dir))
.forEach(directory => {
new_state.push({
directory,
selected: false,
type: "mainnet"
})
});
// Sort them
this.directory_state = new_state.sort(function(a, b){
return a.directory.localeCompare(b.directory);
})
},
import_wallets() {
this.$q.loading.show({
delay: 0
})
this.$gateway.send("wallet", "copy_old_gui_wallets", {
wallets: this.selectedWallets
})
},
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
},
}
</script>
<style lang="scss">
.import-old-gui {
.wallet-list {
.q-item {
margin: 10px 0px;
margin-bottom: 0px;
padding: 14px;
border-radius: 3px;
}
}
}
</style>

View File

@ -16,7 +16,7 @@
</q-btn>
</div>
<div class="hr-separator" />
<q-item v-for="wallet in wallets.list" @click.native="openWallet(wallet)" :key="wallet.address">
<q-item v-for="wallet in wallets.list" @click.native="openWallet(wallet)" :key="`${wallet.address}-${wallet.name}`">
<q-item-side>
<div class="wallet-icon">
<svg width="48" viewBox="0 0 17 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="si-glyph si-glyph-wallet"><title>969</title><defs class="si-glyph-fill"></defs><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(1.000000, 0.000000)" fill="#434343"><path d="M7.988,10.635 L7.988,8.327 C7.988,7.578 8.561,6.969 9.267,6.969 L13.964,6.969 L13.964,5.531 C13.964,4.849 13.56,4.279 13.007,4.093 L13.007,4.094 L11.356,4.08 L11.336,4.022 L3.925,4.022 L3.784,4.07 L1.17,4.068 L1.165,4.047 C0.529,4.167 0.017,4.743 0.017,5.484 L0.017,13.437 C0.017,14.269 0.665,14.992 1.408,14.992 L12.622,14.992 C13.365,14.992 13.965,14.316 13.965,13.484 L13.965,12.031 L9.268,12.031 C8.562,12.031 7.988,11.384 7.988,10.635 L7.988,10.635 Z" class="si-glyph-fill"></path><path d="M14.996,8.061 L14.947,8.061 L9.989,8.061 C9.46,8.061 9.031,8.529 9.031,9.106 L9.031,9.922 C9.031,10.498 9.46,10.966 9.989,10.966 L14.947,10.966 L14.996,10.966 C15.525,10.966 15.955,10.498 15.955,9.922 L15.955,9.106 C15.955,8.528 15.525,8.061 14.996,8.061 L14.996,8.061 Z M12.031,10.016 L9.969,10.016 L9.969,9 L12.031,9 L12.031,10.016 L12.031,10.016 Z" class="si-glyph-fill"></path><path d="M3.926,4.022 L10.557,1.753 L11.337,4.022 L12.622,4.022 C12.757,4.022 12.885,4.051 13.008,4.092 L11.619,0.051 L1.049,3.572 L1.166,4.048 C1.245,4.033 1.326,4.023 1.408,4.023 L3.926,4.023 L3.926,4.022 Z" class="si-glyph-fill"></path></g></g></svg>
@ -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 {
// <q-item @click.native="restoreViewWallet()">
// <q-item-main label="Restore view-only wallet" />
// </q-item>
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 {
</script>
<style lang="scss">
.header-popover.q-popover {
max-width: 250px !important;
}
.wallet-list {
.header {
margin: 0 16px;

View File

@ -99,7 +99,7 @@ export default {
seed: "",
refresh_type: "date",
refresh_start_height: 0,
refresh_start_date: 1492486495000, // timestamp of block 1
refresh_start_date: 1525305600000, // timestamp of block 1
password: "",
password_confirm: ""
},
@ -161,7 +161,11 @@ export default {
return
}
let seed = this.wallet.seed.trim().replace(/\s{2,}/g, " ").split(" ")
let seed = this.wallet.seed.trim()
.replace(/\n/g, " ")
.replace(/\t/g, " ")
.replace(/\s{2,}/g, " ")
.split(" ")
if(seed.length !== 14 && seed.length !== 24 && seed.length !== 25 && seed.length !== 26) {
this.$q.notify({
type: "negative",

View File

@ -10,12 +10,24 @@
<q-item-tile sublabel>Primary address</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
flat
style="width:25px;"
size="md"
@click="showQR(address.address, $event)"
>
<img src="statics/qr-code.svg" height="20" />
<q-tooltip anchor="bottom right" self="top right" :offset="[0, 5]">
Show QR code
</q-tooltip>
</q-btn>
<q-btn
flat
style="width:25px;"
size="md" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
@click="copyAddress(address.address, $event)"
>
<q-tooltip anchor="bottom right" self="top right" :offset="[0, 5]">
Copy address
</q-tooltip>
</q-btn>
@ -62,12 +74,23 @@
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
flat
style="width:25px;"
size="md"
@click="showQR(address.address, $event)"
>
<img src="statics/qr-code-grey.svg" height="20" />
<q-tooltip anchor="bottom right" self="top right" :offset="[0, 5]">
Show QR code
</q-tooltip>
</q-btn>
<q-btn
flat
style="width:25px;"
size="md" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
<q-tooltip anchor="bottom right" self="top right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
@ -117,12 +140,23 @@
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
flat
style="width:25px;"
size="md"
@click="showQR(address.address, $event)"
>
<img src="statics/qr-code-grey.svg" height="20" />
<q-tooltip anchor="bottom right" self="top right" :offset="[0, 5]">
Show QR code
</q-tooltip>
</q-btn>
<q-btn
flat
style="width:25px;"
size="md" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
<q-tooltip anchor="bottom right" self="top right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
@ -148,15 +182,44 @@
</q-list>
<AddressDetails ref="addressDetails" />
<!-- QR Code -->
<template v-if="QR.address != null">
<q-modal v-model="QR.visible" minimized :content-css="{padding: '25px'}">
<div class="text-center q-mb-sm q-pa-md" style="background: white;">
<qrcode-vue :value="QR.address" size="240" ref="qr">
</qrcode-vue>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay @click.native="copyQR()">
<q-item-main label="Copy QR code" />
</q-item>
<q-item v-close-overlay @click.native="saveQR()">
<q-item-main label="Save QR code to file" />
</q-item>
</q-list>
</q-context-menu>
</div>
<q-btn
color="primary"
@click="QR.visible = false"
label="Close"
/>
</q-modal>
</template>
</q-page>
</template>
<script>
const { clipboard } = require("electron")
const { clipboard, nativeImage } = require("electron")
import { mapState } from "vuex"
import QrcodeVue from "qrcode.vue";
import Identicon from "components/identicon"
import AddressDetails from "components/address_details"
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
@ -174,11 +237,38 @@ export default {
return amount.toLocaleString()
}
},
data () {
return {
QR: {
visible: false,
address: null,
}
}
},
methods: {
details (address) {
this.$refs.addressDetails.address = address;
this.$refs.addressDetails.isVisible = true;
},
showQR (address, event) {
event.stopPropagation()
this.QR.visible = true
this.QR.address = address
},
copyQR () {
const data = this.$refs.qr.$el.childNodes[0].toDataURL()
const img = nativeImage.createFromDataURL(data)
clipboard.writeImage(img)
this.$q.notify({
type: "positive",
timeout: 1000,
message: "Copied QR code to clipboard"
})
},
saveQR () {
let img = this.$refs.qr.$el.childNodes[0].toDataURL()
this.$gateway.send("core", "save_png", {img, type: "QR Code"})
},
copyAddress (address, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
@ -198,6 +288,7 @@ export default {
components: {
Identicon,
AddressDetails,
QrcodeVue
}
}
</script>

View File

@ -65,6 +65,18 @@
</LokiField>
</div>
<!-- Notes -->
<div class="col q-mt-sm">
<LokiField label="Notes" optional>
<q-input v-model="newTx.note"
type="textarea"
:dark="theme=='dark'"
placeholder="Additional notes to attach to the transaction"
hide-underline
/>
</LokiField>
</div>
<!-- Save to address book -->
<q-field>
<q-checkbox v-model="newTx.address_book.save" label="Save to address book" :dark="theme=='dark'" />
@ -113,7 +125,9 @@ import { required, decimal } from "vuelidate/lib/validators"
import { payment_id, address, greater_than_zero } from "src/validators/common"
import Identicon from "components/identicon"
import LokiField from "components/loki_field"
import WalletPassword from "src/mixins/wallet_password"
const objectAssignDeep = require("object-assign-deep");
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
@ -147,10 +161,11 @@ export default {
}
},
priorityOptions: [
{label: "Normal (x1 fee)", value: 0},
{label: "Slow (x0.25 fee)", value: 1},
{label: "Fast (x5 fee)", value: 2},
{label: "Fastest (x41.5 fee)", value: 3},
{label: "Automatic", value: 0},
{label: "Slow (x0.2 fee)", value: 1},
{label: "Normal (x1 fee)", value: 2},
{label: "Fast (x5 fee)", value: 3},
{label: "Fastest (x200 fee)", value: 4},
],
}
},
@ -197,7 +212,8 @@ export default {
save: false,
name: "",
description: ""
}
},
note: ""
}
break;
case -1:
@ -230,7 +246,6 @@ export default {
},
send: function () {
this.$v.newTx.$touch()
if(this.newTx.amount < 0) {
@ -282,21 +297,12 @@ export default {
return
}
this.$q.dialog({
this.showPasswordConfirmation({
title: "Transfer",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
noPasswordMessage: "Do you want to send the transaction?",
ok: {
label: "SEND"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$store.commit("gateway/set_tx_status", {
code: 1,
@ -309,6 +315,7 @@ export default {
})
}
},
mixins: [WalletPassword],
components: {
Identicon,
LokiField

View File

@ -1,286 +1,40 @@
<template>
<q-page class="service-node-page">
<template>
<div class="q-pa-md">
<LokiField label="Service Node Key" :error="$v.service_node.key.$error">
<q-input v-model="service_node.key"
:dark="theme=='dark'"
@blur="$v.service_node.key.$touch"
placeholder="64 hexadecimal characters"
hide-underline
/>
</LokiField>
<div class="q-mt-md col">
<LokiField label="Award Recepient's Address" :error="$v.service_node.award_address.$error">
<q-input v-model="service_node.award_address"
:dark="theme=='dark'"
@blur="$v.service_node.award_address.$touch"
placeholder="64 hexadecimal characters"
hide-underline
/>
</LokiField>
<div class="address-type" :class="[addressType]">( {{ addressType | addressTypeString }} )</div>
</div>
<LokiField label="Amount" class="q-mt-md" :error="$v.service_node.amount.$error">
<q-input v-model="service_node.amount"
:dark="theme=='dark'"
type="number"
min="0"
:max="unlocked_balance / 1e9"
placeholder="0"
@blur="$v.service_node.amount.$touch"
hide-underline
/>
<q-btn color="secondary" @click="service_node.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">All</q-btn>
</LokiField>
<q-field class="q-pt-sm">
<q-btn
:disable="!is_able_to_send"
color="primary" @click="stake()" label="Stake" />
</q-field>
</div>
<q-inner-loading :visible="stake_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading>
</template>
<div class="header row items-center justify-center q-mt-md">
<q-btn-toggle
v-model="screen"
toggle-color="primary"
color="secondary"
:options="[
{label: 'Staking', value: 'staking'},
{label: 'Registration', value: 'registration'},
{label: 'Unlock', value: 'unlock'}
]"
/>
</div>
<ServiceNodeStaking v-if="screen === 'staking'"/>
<ServiceNodeRegistration v-if="screen === 'registration'" />
<ServiceNodeUnlock v-if="screen === 'unlock'" />
</q-page>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import { required, decimal } from "vuelidate/lib/validators"
import { payment_id, service_node_key, greater_than_zero, address } from "src/validators/common"
import Identicon from "components/identicon"
import LokiField from "components/loki_field"
const objectAssignDeep = require("object-assign-deep");
import ServiceNodeStaking from "components/service_node_staking"
import ServiceNodeRegistration from "components/service_node_registration"
import ServiceNodeUnlock from "components/service_node_unlock"
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
unlocked_balance: state => state.gateway.wallet.info.unlocked_balance,
info: state => state.gateway.wallet.info,
address_list: state => state.gateway.wallet.address_list,
stake_status: state => state.gateway.stake_status,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
is_able_to_send (state) {
return this.$store.getters["gateway/isAbleToSend"]
},
addressType (state) {
const address = this.service_node.award_address;
const inArray = (array) => array.map(o => o.address).includes(address);
const { primary, used, unused } = this.address_list
if (inArray(primary)) {
return "primary"
} else if (inArray(used)) {
return "used"
} else if (inArray(unused)) {
return "unsued"
} else {
return "not-ours"
}
}
}),
data () {
return {
service_node: {
key: "",
amount: 0,
award_address: "",
},
}
},
filters: {
addressTypeString: function (value) {
switch (value) {
case "primary":
return "Your primary address"
case "used":
return "Your used address"
case "ununsed":
return "Your unused address"
default:
return "Not your address!"
}
}
},
validations: {
service_node: {
key: { required, service_node_key },
amount: {
required,
decimal,
greater_than_zero,
},
award_address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
}
}
},
watch: {
stake_status: {
handler(val, old){
if(val.code == old.code) return
switch(this.stake_status.code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.stake_status.message
})
this.$v.$reset();
this.service_node = {
key: "",
amount: 0,
award_address: "",
}
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.stake_status.message
})
break;
}
},
deep: true
},
},
created () {
const { address } = this.info;
if (!this.service_node.award_address || this.service_node.award_address === "") {
this.service_node.award_address = address || ""
}
},
methods: {
isOurAddress (address) {
const { primary, used, unused } = this.address_list
const addresses = [...primary, ...used, ...unused].map(o => o.address);
console.log(addresses);
return addresses.includes(address);
},
stake: function () {
this.$v.service_node.$touch()
if (this.$v.service_node.key.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Service node key not valid"
})
return
}
if (this.$v.service_node.award_address.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Address not valid"
})
return
}
if(this.service_node.amount < 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount cannot be negative"
})
return
} else if(this.service_node.amount == 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount must be greater than zero"
})
return
} else if(this.service_node.amount > this.unlocked_balance / 1e9) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Not enough unlocked balance"
})
return
} else if (this.$v.service_node.amount.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount not valid"
})
return
}
this.$q.dialog({
title: "Stake",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: "STAKE"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$store.commit("gateway/set_stake_status", {
code: 1,
message: "Staking...",
sending: true
})
const service_node = objectAssignDeep.noMutate(this.service_node, {password})
this.$gateway.send("wallet", "stake", {
...service_node,
destination: service_node.award_address,
})
}).catch(() => {
})
screen: "staking",
}
},
components: {
Identicon,
LokiField
ServiceNodeStaking,
ServiceNodeRegistration,
ServiceNodeUnlock
}
}
</script>
<style lang="scss">
.service-node-page {
.address-type {
margin-top: 4px;
font-size: 13px;
font-weight: 400;
text-align: right;
&.not-ours {
font-weight: bold;
}
}
}
</style>

View File

@ -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"},
]

View File

@ -1,3 +1,7 @@
/**
This is an unused class in LOKI
*/
<template>
<q-page padding>

View File

@ -72,6 +72,12 @@ export default [
name: "wallet-created",
component: () =>
import("pages/wallet-select/created")
},
{
path: "import-old-gui",
name: "wallet-import-old-gui",
component: () =>
import("pages/wallet-select/import-old-gui")
}
]
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 401 401" width="401" height="401"><defs><path d="M0 401L0 218.72L182.27 218.72L182.27 401L0 401ZM36.45 364.26L145.82 364.26L145.82 255.18L36.45 255.18L36.45 364.26Z" id="cP4vhvhrf"></path><path d="M109.36 291.63C109.36 291.63 109.36 291.63 109.36 291.63C109.36 313.51 109.36 325.66 109.36 328.09C109.36 328.09 109.36 328.09 109.36 328.09C87.49 328.09 75.34 328.09 72.91 328.09C72.91 328.09 72.91 328.09 72.91 328.09C72.91 306.22 72.91 294.06 72.91 291.63C72.91 291.63 72.91 291.63 72.91 291.63C94.78 291.63 106.93 291.63 109.36 291.63Z" id="b3K4Wy5wBr"></path><path d="M328.09 364.55C328.09 364.55 328.09 364.55 328.09 364.55C328.09 386.42 328.09 398.57 328.09 401C328.09 401 328.09 401 328.09 401C306.21 401 294.06 401 291.63 401C291.63 401 291.63 401 291.63 401C291.63 379.13 291.63 366.98 291.63 364.55C291.63 364.55 291.63 364.55 291.63 364.55C313.51 364.55 325.66 364.55 328.09 364.55Z" id="ajtxNY2Qi"></path><path d="M401 364.55C401 364.55 401 364.55 401 364.55C401 386.42 401 398.57 401 401C401 401 401 401 401 401C379.12 401 366.97 401 364.54 401C364.54 401 364.54 401 364.54 401C364.54 379.13 364.54 366.98 364.54 364.55C364.54 364.55 364.54 364.55 364.54 364.55C386.42 364.55 398.57 364.55 401 364.55Z" id="b12z2Qj5i"></path><path d="M328.09 255.18L328.09 218.72L218.72 218.72L218.72 401L255.18 401L255.18 291.63L291.63 291.63L291.63 328.09L401 328.09L401 218.72L401 218.72L364.54 218.72L364.54 255.18L328.09 255.18Z" id="aDSxNIZuL"></path><path d="M0 182.28L0 0L182.27 0L182.27 182.28L0 182.28ZM36.45 145.82L145.82 145.82L145.82 36.45L36.45 36.45L36.45 145.82Z" id="bpcQxL4BR"></path><path d="M109.36 72.91C109.36 72.91 109.36 72.91 109.36 72.91C109.36 94.78 109.36 106.93 109.36 109.36C109.36 109.36 109.36 109.36 109.36 109.36C87.49 109.36 75.34 109.36 72.91 109.36C72.91 109.36 72.91 109.36 72.91 109.36C72.91 87.49 72.91 75.34 72.91 72.91C72.91 72.91 72.91 72.91 72.91 72.91C94.78 72.91 106.93 72.91 109.36 72.91Z" id="b2tkw7De1"></path><path d="M218.72 182.28L218.72 0L401 0L401 182.28L218.72 182.28ZM255.18 145.82L364.54 145.82L364.54 36.45L255.18 36.45L255.18 145.82Z" id="b3NlRRtUXl"></path><path d="M328.09 72.91C328.09 72.91 328.09 72.91 328.09 72.91C328.09 94.78 328.09 106.93 328.09 109.36C328.09 109.36 328.09 109.36 328.09 109.36C306.21 109.36 294.06 109.36 291.63 109.36C291.63 109.36 291.63 109.36 291.63 109.36C291.63 87.49 291.63 75.34 291.63 72.91C291.63 72.91 291.63 72.91 291.63 72.91C313.51 72.91 325.66 72.91 328.09 72.91Z" id="aiJPdIloT"></path></defs><g><g><g><use xlink:href="#cP4vhvhrf" opacity="1" fill="#bbbbbb" fill-opacity="1"></use><g><use xlink:href="#cP4vhvhrf" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#b3K4Wy5wBr" opacity="1" fill="#bbbbbb" fill-opacity="1"></use><g><use xlink:href="#b3K4Wy5wBr" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#ajtxNY2Qi" opacity="1" fill="#bbbbbb" fill-opacity="1"></use><g><use xlink:href="#ajtxNY2Qi" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#b12z2Qj5i" opacity="1" fill="#bbbbbb" fill-opacity="1"></use><g><use xlink:href="#b12z2Qj5i" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#aDSxNIZuL" opacity="1" fill="#bbbbbb" fill-opacity="1"></use><g><use xlink:href="#aDSxNIZuL" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#bpcQxL4BR" opacity="1" fill="#bbbbbb" fill-opacity="1"></use><g><use xlink:href="#bpcQxL4BR" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#b2tkw7De1" opacity="1" fill="#bbbbbb" fill-opacity="1"></use><g><use xlink:href="#b2tkw7De1" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#b3NlRRtUXl" opacity="1" fill="#bbbbbb" fill-opacity="1"></use><g><use xlink:href="#b3NlRRtUXl" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#aiJPdIloT" opacity="1" fill="#bbbbbb" fill-opacity="1"></use><g><use xlink:href="#aiJPdIloT" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

3
src/statics/qr-code.svg Normal file
View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 401 401" width="401" height="401"><defs><path d="M0 401L0 218.72L182.27 218.72L182.27 401L0 401ZM36.45 364.26L145.82 364.26L145.82 255.18L36.45 255.18L36.45 364.26Z" id="c4EjTiqRPu"></path><path d="M109.36 291.63C109.36 291.63 109.36 291.63 109.36 291.63C109.36 313.51 109.36 325.66 109.36 328.09C109.36 328.09 109.36 328.09 109.36 328.09C87.49 328.09 75.34 328.09 72.91 328.09C72.91 328.09 72.91 328.09 72.91 328.09C72.91 306.22 72.91 294.06 72.91 291.63C72.91 291.63 72.91 291.63 72.91 291.63C94.78 291.63 106.93 291.63 109.36 291.63Z" id="e1x2wlSIyH"></path><path d="M328.09 364.55C328.09 364.55 328.09 364.55 328.09 364.55C328.09 386.42 328.09 398.57 328.09 401C328.09 401 328.09 401 328.09 401C306.21 401 294.06 401 291.63 401C291.63 401 291.63 401 291.63 401C291.63 379.13 291.63 366.98 291.63 364.55C291.63 364.55 291.63 364.55 291.63 364.55C313.51 364.55 325.66 364.55 328.09 364.55Z" id="b2SEDxmpGS"></path><path d="M401 364.55C401 364.55 401 364.55 401 364.55C401 386.42 401 398.57 401 401C401 401 401 401 401 401C379.12 401 366.97 401 364.54 401C364.54 401 364.54 401 364.54 401C364.54 379.13 364.54 366.98 364.54 364.55C364.54 364.55 364.54 364.55 364.54 364.55C386.42 364.55 398.57 364.55 401 364.55Z" id="b1t3ctO9Ib"></path><path d="M328.09 255.18L328.09 218.72L218.72 218.72L218.72 401L255.18 401L255.18 291.63L291.63 291.63L291.63 328.09L401 328.09L401 218.72L401 218.72L364.54 218.72L364.54 255.18L328.09 255.18Z" id="f2X4EE7RDM"></path><path d="M0 182.28L0 0L182.27 0L182.27 182.28L0 182.28ZM36.45 145.82L145.82 145.82L145.82 36.45L36.45 36.45L36.45 145.82Z" id="c222nLedUh"></path><path d="M109.36 72.91C109.36 72.91 109.36 72.91 109.36 72.91C109.36 94.78 109.36 106.93 109.36 109.36C109.36 109.36 109.36 109.36 109.36 109.36C87.49 109.36 75.34 109.36 72.91 109.36C72.91 109.36 72.91 109.36 72.91 109.36C72.91 87.49 72.91 75.34 72.91 72.91C72.91 72.91 72.91 72.91 72.91 72.91C94.78 72.91 106.93 72.91 109.36 72.91Z" id="bfDGWJSeW"></path><path d="M218.72 182.28L218.72 0L401 0L401 182.28L218.72 182.28ZM255.18 145.82L364.54 145.82L364.54 36.45L255.18 36.45L255.18 145.82Z" id="a8V10SdpO"></path><path d="M328.09 72.91C328.09 72.91 328.09 72.91 328.09 72.91C328.09 94.78 328.09 106.93 328.09 109.36C328.09 109.36 328.09 109.36 328.09 109.36C306.21 109.36 294.06 109.36 291.63 109.36C291.63 109.36 291.63 109.36 291.63 109.36C291.63 87.49 291.63 75.34 291.63 72.91C291.63 72.91 291.63 72.91 291.63 72.91C313.51 72.91 325.66 72.91 328.09 72.91Z" id="eWcmpmYP8"></path></defs><g><g><g><use xlink:href="#c4EjTiqRPu" opacity="1" fill="#ffffff" fill-opacity="1"></use><g><use xlink:href="#c4EjTiqRPu" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#e1x2wlSIyH" opacity="1" fill="#ffffff" fill-opacity="1"></use><g><use xlink:href="#e1x2wlSIyH" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#b2SEDxmpGS" opacity="1" fill="#ffffff" fill-opacity="1"></use><g><use xlink:href="#b2SEDxmpGS" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#b1t3ctO9Ib" opacity="1" fill="#ffffff" fill-opacity="1"></use><g><use xlink:href="#b1t3ctO9Ib" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#f2X4EE7RDM" opacity="1" fill="#ffffff" fill-opacity="1"></use><g><use xlink:href="#f2X4EE7RDM" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#c222nLedUh" opacity="1" fill="#ffffff" fill-opacity="1"></use><g><use xlink:href="#c222nLedUh" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#bfDGWJSeW" opacity="1" fill="#ffffff" fill-opacity="1"></use><g><use xlink:href="#bfDGWJSeW" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#a8V10SdpO" opacity="1" fill="#ffffff" fill-opacity="1"></use><g><use xlink:href="#a8V10SdpO" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#eWcmpmYP8" opacity="1" fill="#ffffff" fill-opacity="1"></use><g><use xlink:href="#eWcmpmYP8" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -12,9 +12,12 @@ export const set_wallet_data = (state, data) => {
export const set_wallet_list = (state, data) => {
state.wallets = objectAssignDeep.noMutate(state.wallets, data)
}
export const set_old_gui_import_status = (state, data) => {
state.old_gui_import_status = data
}
export const set_tx_status = (state, data) => {
state.tx_status = data
}
export const set_stake_status = (state, data) => {
state.stake_status = data
export const set_snode_status = (state, data) => {
state.service_node_status = objectAssignDeep.noMutate(state.service_node_status, data)
}

View File

@ -15,7 +15,14 @@ export default {
},
wallets: {
list: [],
legacy: []
legacy: [],
// List of wallets that are in a sub folder (format of the old GUI)
directories: []
},
old_gui_import_status: {
code: 0, // Success
failed_wallets: []
},
wallet: {
status: {
@ -48,10 +55,22 @@ export default {
code: 0,
message: ""
},
stake_status: {
code: 0,
message: "",
sending: false
service_node_status: {
stake: {
code: 0,
message: "",
sending: false
},
registration: {
code: 0,
message: "",
sending: false
},
unlock: {
code: 0,
message: "",
sending: false
}
},
daemon: {
info: {