Merge pull request #1 from Mikunj/redesign

Loki Redesign
This commit is contained in:
Mikunj Varsani 2019-03-21 16:02:09 +11:00 committed by GitHub
commit 686b820d59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 11866 additions and 8229 deletions

View file

@ -1 +1,2 @@
/dist
/src/validators/address_tools.js

View file

@ -25,6 +25,9 @@ module.exports = {
},
// add your custom rules here
"rules": {
"indent": ["error", 4],
"quotes": ["error", "double"],
// allow async-await
"generator-star-spacing": "off",
@ -32,6 +35,8 @@ module.exports = {
"arrow-parens": 0,
"one-var": 0,
"camelcase": 0,
"import/first": 0,
"import/named": 2,
"import/namespace": 2,

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
11.9.0

View file

@ -4,12 +4,16 @@ Loki GUI wallet
### Building from source
#### Pre-requisite
- Download latest [Lokid](https://github.com/loki-project/loki/releases)
#### Commands
```
npm install -g quasar-cli
git clone https://github.com/ryo-currency/ryo-wallet
cd ryo-wallet
cp /path/to/ryo/binaries/ryod bin/
cp /path/to/ryo/binaries/ryo-wallet-rpc bin/
git clone https://github.com/loki-project/loki-electron-wallet
cd loki-electron-wallet
cp path_to_loki_binaries/lokid bin/
cp path_to_loki_binaries/loki-wallet-rpc bin/
npm install
quasar build -m electron -t mat
```
@ -18,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

12958
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,23 @@
{
"name": "ryo-wallet-atom",
"name": "loki-wallet-atom",
"version": "1.1.0",
"daemonVersion": "0.3.2.0",
"description": "Modern GUI interface for Ryo Currency",
"productName": "Ryo Wallet Atom",
"cordovaId": "com.ryo-currency.ryo-gui-wallet",
"author": "Ryo-currency <contact@ryo-currency.com>",
"description": "Modern GUI interface for Loki Currency",
"productName": "Loki Wallet Atom",
"cordovaId": "com.lokinetwork.wallet",
"author": "Loki",
"private": true,
"scripts": {
"dev": "quasar dev -m electron -t mat",
"build": "quasar build -m electron -t mat",
"lint": "eslint --ext .js,.vue src",
"lint-fix": "eslint --fix .",
"test": "echo \"No test specified\" && exit 0"
},
"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",
@ -41,9 +45,10 @@
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.3.0",
"node-sass": "^4.9.3",
"quasar-cli": "^0.16.0",
"sass-loader": "^7.1.0"
"node-sass": "^4.11.0",
"quasar-cli": "^0.17.24",
"sass-loader": "^7.1.0",
"strip-ansi": "^3.0.1"
},
"engines": {
"node": ">= 8.9.0",

View file

@ -6,7 +6,7 @@ module.exports = function (ctx) {
plugins: [
"i18n",
"axios",
"vuelidate",
"vuelidate",
"gateway",
"timeago"
],
@ -28,7 +28,7 @@ module.exports = function (ctx) {
// gzip: true,
// analyze: true,
// extractCSS: false,
extendWebpack(cfg) {
extendWebpack (cfg) {
/*
cfg.module.rules.push({
enforce: "pre",
@ -59,6 +59,7 @@ module.exports = function (ctx) {
"QField",
"QInput",
"QRadio",
"QOptionGroup",
"QBtn",
"QBtnToggle",
"QIcon",
@ -88,7 +89,8 @@ module.exports = function (ctx) {
"QInnerLoading",
"QInfiniteScroll",
"QDatetime",
"QContextMenu"
"QContextMenu",
"QScrollArea"
],
directives: [
"Ripple",
@ -116,32 +118,32 @@ module.exports = function (ctx) {
display: "standalone",
orientation: "portrait",
background_color: "#ffffff",
theme_color: "#027be3",
theme_color: "#43BD43",
icons: [{
"src": "statics/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "statics/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "statics/icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "statics/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "statics/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
"src": "statics/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "statics/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "statics/icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "statics/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "statics/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
},
@ -150,7 +152,7 @@ module.exports = function (ctx) {
},
electron: {
bundler: "builder", // or "packager"
extendWebpack(cfg) {
extendWebpack (cfg) {
// do something with Electron process Webpack cfg
},
packager: {
@ -166,15 +168,15 @@ module.exports = function (ctx) {
// win32metadata: { ... }
extraResource: [
"bin",
"bin"
]
},
builder: {
// https://www.electron.build/configuration/configuration
appId: "com.ryo-currency.wallet",
productName: "Ryo Wallet Atom",
copyright: "Copyright © 2018 Ryo Currency Project",
appId: "com.lokinetwork.wallet",
productName: "Loki Wallet Atom",
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

@ -19,30 +19,30 @@ let mainWindow, backend
let showConfirmClose = true
let forceQuit = false
const portInUse = function(port, callback) {
var server = net.createServer(function(socket) {
socket.write("Echo server\r\n");
socket.pipe(socket);
});
const portInUse = function (port, callback) {
var server = net.createServer(function (socket) {
socket.write("Echo server\r\n")
socket.pipe(socket)
})
server.listen(port, "127.0.0.1");
server.listen(port, "127.0.0.1")
server.on("error", function (e) {
callback(true);
});
callback(true)
})
server.on("listening", function (e) {
server.close();
callback(false);
});
};
server.close()
callback(false)
})
}
function createWindow() {
function createWindow () {
/**
* Initial window options
*/
let mainWindowState = windowStateKeeper({
defaultWidth: 800,
defaultHeight: 650
defaultWidth: 900,
defaultHeight: 700
})
mainWindow = new BrowserWindow({
@ -93,11 +93,9 @@ function createWindow() {
})
mainWindow.webContents.on("did-finish-load", () => {
require("crypto").randomBytes(64, (err, buffer) => {
// if err, then we may have to use insecure token generation perhaps
if (err) throw err;
if (err) throw err
let config = {
port: 12213,
@ -112,20 +110,16 @@ function createWindow() {
} else {
dialog.showMessageBox(mainWindow, {
title: "Startup error",
message: `Ryo Wallet is already open, or port ${config.port} is in use`,
message: `Loki Wallet is already open, or port ${config.port} is in use`,
type: "error",
buttons: ["ok"]
}, () => {
showConfirmClose = false
app.quit()
})
}
})
})
})
mainWindow.loadURL(process.env.APP_URL)

View file

@ -35,7 +35,7 @@ let template = [
submenu: [
{
label: "Learn More",
click () { require("electron").shell.openExternal("https://ryo-currency.com/") }
click () { require("electron").shell.openExternal("https://loki.network/") }
}
]
}
@ -43,7 +43,7 @@ let template = [
if (process.platform === "darwin") {
template.unshift({
label: "Ryo Wallet Atom",
label: "Loki Wallet Atom",
submenu: [
{role: "about"},
{type: "separator"},

View file

@ -22,70 +22,70 @@
SOFTWARE.
*/
const crypto = require("crypto");
const crypto = require("crypto")
const ALGORITHM_NAME = "aes-128-gcm";
const ALGORITHM_NONCE_SIZE = 12;
const ALGORITHM_TAG_SIZE = 16;
const ALGORITHM_KEY_SIZE = 16;
const PBKDF2_NAME = "sha256";
const PBKDF2_SALT_SIZE = 16;
const PBKDF2_ITERATIONS = 32767;
const ALGORITHM_NAME = "aes-128-gcm"
const ALGORITHM_NONCE_SIZE = 12
const ALGORITHM_TAG_SIZE = 16
const ALGORITHM_KEY_SIZE = 16
const PBKDF2_NAME = "sha256"
const PBKDF2_SALT_SIZE = 16
const PBKDF2_ITERATIONS = 32767
export class SCEE {
encryptString(plaintext, password) {
encryptString (plaintext, password) {
// Generate a 128-bit salt using a CSPRNG.
let salt = crypto.randomBytes(PBKDF2_SALT_SIZE);
let salt = crypto.randomBytes(PBKDF2_SALT_SIZE)
// Derive a key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME)
// Encrypt and prepend salt.
let ciphertextAndNonceAndSalt = Buffer.concat([ salt, this.encrypt(new Buffer(plaintext, "utf8"), key) ]);
let ciphertextAndNonceAndSalt = Buffer.concat([ salt, this.encrypt(new Buffer(plaintext, "utf8"), key) ])
// Return as base64 string.
return ciphertextAndNonceAndSalt.toString("base64");
return ciphertextAndNonceAndSalt.toString("base64")
}
decryptString(base64CiphertextAndNonceAndSalt, password) {
decryptString (base64CiphertextAndNonceAndSalt, password) {
// Decode the base64.
let ciphertextAndNonceAndSalt = new Buffer(base64CiphertextAndNonceAndSalt, "base64");
let ciphertextAndNonceAndSalt = new Buffer(base64CiphertextAndNonceAndSalt, "base64")
// Create buffers of salt and ciphertextAndNonce.
let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE);
let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE);
let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE)
let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE)
// Derive the key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME)
// Decrypt and return result.
return this.decrypt(ciphertextAndNonce, key).toString("utf8");
return this.decrypt(ciphertextAndNonce, key).toString("utf8")
}
encrypt(plaintext, key) {
encrypt (plaintext, key) {
// Generate a 96-bit nonce using a CSPRNG.
let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE);
let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE)
// Create the cipher instance.
let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce);
let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce)
// Encrypt and prepend nonce.
let ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final() ]);
let ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final() ])
return Buffer.concat([ nonce, ciphertext, cipher.getAuthTag() ]);
return Buffer.concat([ nonce, ciphertext, cipher.getAuthTag() ])
}
decrypt(ciphertextAndNonce, key) {
decrypt (ciphertextAndNonce, key) {
// Create buffers of nonce, ciphertext and tag.
let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE);
let ciphertext = ciphertextAndNonce.slice(ALGORITHM_NONCE_SIZE, ciphertextAndNonce.length - ALGORITHM_TAG_SIZE);
let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE);
let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE)
let ciphertext = ciphertextAndNonce.slice(ALGORITHM_NONCE_SIZE, ciphertextAndNonce.length - ALGORITHM_TAG_SIZE)
let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE)
// Create the cipher instance.
let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce);
let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce)
// Decrypt and return result.
cipher.setAuthTag(tag);
return Buffer.concat([ cipher.update(ciphertext), cipher.final() ]);
cipher.setAuthTag(tag)
return Buffer.concat([ cipher.update(ciphertext), cipher.final() ])
}
}

View file

@ -1,79 +1,126 @@
import { Daemon } from "./daemon";
import { WalletRPC } from "./wallet-rpc";
import { SCEE } from "./SCEE-Node";
import { dialog } from "electron";
import { Daemon } from "./daemon"
import { WalletRPC } from "./wallet-rpc"
import { SCEE } from "./SCEE-Node"
import { dialog } from "electron"
const WebSocket = require("ws");
const os = require("os");
const fs = require("fs");
const path = require("path");
const WebSocket = require("ws")
const os = require("os")
const fs = require("fs-extra")
const path = require("path")
const objectAssignDeep = require("object-assign-deep")
export class Backend {
constructor(mainWindow) {
constructor (mainWindow) {
this.mainWindow = mainWindow
this.daemon = null
this.walletd = null
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()
}
init(config) {
if(os.platform() == "win32") {
this.config_dir = "C:\\ProgramData\\ryo";
//this.config_dir = path.join(os.homedir(), "ryo");
init (config) {
if (os.platform() === "win32") {
this.config_dir = "C:\\ProgramData\\loki"
this.wallet_dir = `${os.homedir()}\\Documents\\Loki`
} else {
this.config_dir = path.join(os.homedir(), ".ryo");
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"));
if (!fs.existsSync(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");
this.config_file = path.join(this.config_dir, "gui", "config.json")
this.config_data = {
const daemon = {
type: "remote",
p2p_bind_ip: "0.0.0.0",
p2p_bind_port: 22022,
rpc_bind_ip: "127.0.0.1",
rpc_bind_port: 22023,
zmq_rpc_bind_ip: "127.0.0.1",
zmq_rpc_bind_port: 22024,
out_peers: -1,
in_peers: -1,
limit_rate_up: -1,
limit_rate_down: -1,
log_level: 0
}
const daemons = {
mainnet: {
...daemon,
remote_host: "doopool.xyz",
remote_port: 22020
},
stagenet: {
...daemon,
type: "local",
p2p_bind_port: 38153,
rpc_bind_port: 38154,
zmq_rpc_bind_port: 38155
},
testnet: {
...daemon,
type: "local",
p2p_bind_port: 38156,
rpc_bind_port: 38157,
zmq_rpc_bind_port: 38158
}
}
// Default values
this.defaults = {
daemons: objectAssignDeep({}, daemons),
app: {
data_dir: this.config_dir,
wallet_data_dir: this.wallet_dir,
ws_bind_port: 12213,
testnet: false
net_type: "mainnet"
},
appearance: {
theme: "light"
},
daemon: {
type: "local_remote",
remote_host: "geo.ryoblocks.com",
remote_port: 12211,
p2p_bind_ip: "0.0.0.0",
p2p_bind_port: 12210,
rpc_bind_ip: "127.0.0.1",
rpc_bind_port: 12211,
zmq_rpc_bind_ip: "127.0.0.1",
zmq_rpc_bind_port: 12212,
out_peers: -1,
in_peers: -1,
limit_rate_up: -1,
limit_rate_down: -1,
log_level: 0
},
wallet: {
rpc_bind_port: 12214,
rpc_bind_port: 18082,
log_level: 0
}
}
this.config_data = {
// Copy all the properties of defaults
...objectAssignDeep({}, this.defaults),
appearance: {
theme: "dark"
}
}
this.remotes = [
{
host: "doopool.xyz",
port: "22020"
},
{
host: "daemons.cryptopool.space",
port: "22023"
},
{
host: "node.loki-pool.com",
port: "18081"
},
{
host: "imaginary.stream",
port: "22023"
}
]
this.token = config.token
this.wss = new WebSocket.Server({
@ -82,284 +129,413 @@ export class Backend {
})
this.wss.on("connection", ws => {
ws.on("message", data => this.receive(data));
});
ws.on("message", data => this.receive(data))
})
}
send(event, data={}) {
send (event, data = {}) {
let message = {
event,
data
}
let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token);
let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token)
this.wss.clients.forEach(function each(client) {
this.wss.clients.forEach(function each (client) {
if (client.readyState === WebSocket.OPEN) {
client.send(encrypted_data)
}
});
})
}
receive(data) {
let decrypted_data = JSON.parse(this.scee.decryptString(data, this.token));
receive (data) {
let decrypted_data = JSON.parse(this.scee.decryptString(data, this.token))
// route incoming request to either the daemon, wallet, or here
switch (decrypted_data.module) {
case "core":
this.handle(decrypted_data);
break;
case "daemon":
if (this.daemon) {
this.daemon.handle(decrypted_data);
}
break;
case "wallet":
if (this.walletd) {
this.walletd.handle(decrypted_data);
}
break;
case "core":
this.handle(decrypted_data)
break
case "daemon":
if (this.daemon) {
this.daemon.handle(decrypted_data)
}
break
case "wallet":
if (this.walletd) {
this.walletd.handle(decrypted_data)
}
break
}
}
handle(data) {
handle (data) {
let params = data.data
switch (data.method) {
case "quick_save_config":
// save only partial config settings
Object.keys(params).map(key => {
this.config_data[key] = Object.assign(this.config_data[key], params[key])
case "quick_save_config":
// save only partial config settings
Object.keys(params).map(key => {
this.config_data[key] = Object.assign(this.config_data[key], params[key])
})
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => {
this.send("set_app_data", {
config: params,
pending_config: params
})
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), 'utf8', () => {
})
break
case "save_config":
// check if config has changed
let config_changed = false
Object.keys(this.config_data).map(i => {
if (i == "appearance") return
Object.keys(this.config_data[i]).map(j => {
if (this.config_data[i][j] !== params[i][j]) { config_changed = true }
})
})
case "save_config_init":
Object.keys(params).map(key => {
this.config_data[key] = Object.assign(this.config_data[key], params[key])
})
const validated = Object.keys(this.defaults)
.filter(k => k in this.config_data)
.map(k => [k, this.validate_values(this.config_data[k], this.defaults[k])])
.reduce((map, obj) => {
map[obj[0]] = obj[1]
return map
}, {})
// Validate deamon data
this.config_data = {
...this.config_data,
...validated
}
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => {
if (data.method == "save_config_init") {
this.startup()
} else {
this.send("set_app_data", {
config: params,
pending_config: params
config: this.config_data,
pending_config: this.config_data
})
})
break
case "save_config":
// check if config has changed
let config_changed = false
Object.keys(this.config_data).map(i => {
if(i == "appearance") return
Object.keys(this.config_data[i]).map(j => {
if(this.config_data[i][j] !== params[i][j])
config_changed = true
})
})
case "save_config_init":
Object.keys(params).map(key => {
this.config_data[key] = Object.assign(this.config_data[key], params[key])
});
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), 'utf8', () => {
if(data.method == "save_config_init") {
this.startup();
} else {
this.send("set_app_data", {
config: this.config_data,
pending_config: this.config_data,
})
if(config_changed) {
this.send("settings_changed_reboot")
}
if (config_changed) {
this.send("settings_changed_reboot")
}
});
break;
case "init":
this.startup();
break;
case "open_explorer":
if(params.type == "tx") {
require("electron").shell.openExternal("https://explorer.ryo-currency.com/tx/"+params.id)
}
break;
})
break
case "init":
this.startup()
break
case "open_url":
require("electron").shell.openExternal(params.url)
break;
case "open_explorer":
if (params.type == "tx") {
require("electron").shell.openExternal("https://lokiblocks.com/tx/" + params.id)
}
break
case "save_png":
let filename = dialog.showSaveDialog(this.mainWindow, {
title: "Save "+params.type,
filters: [{name: "PNG", extensions:["png"]}],
defaultPath: os.homedir()
case "open_url":
require("electron").shell.openExternal(params.url)
break
case "save_png":
let filename = dialog.showSaveDialog(this.mainWindow, {
title: "Save " + params.type,
filters: [{name: "PNG", extensions: ["png"]}],
defaultPath: os.homedir()
})
if (filename) {
let base64Data = params.img.replace(/^data:image\/png;base64,/, "")
let binaryData = new Buffer(base64Data, "base64").toString("binary")
fs.writeFile(filename, binaryData, "binary", (err) => {
if (err) { this.send("show_notification", {type: "negative", message: "Error saving " + params.type, timeout: 2000}) } else { this.send("show_notification", {message: params.type + " saved to " + filename, timeout: 2000}) }
})
if(filename) {
let base64Data = params.img.replace(/^data:image\/png;base64,/,"")
let binaryData = new Buffer(base64Data, 'base64').toString("binary")
fs.writeFile(filename, binaryData, "binary", (err) => {
if(err)
this.send("show_notification", {type: "negative", message: "Error saving "+params.type, timeout: 2000})
else
this.send("show_notification", {message: params.type+" saved to "+filename, timeout: 2000})
})
}
break;
}
break
default:
default:
}
}
startup() {
fs.readFile(this.config_file, "utf8", (err,data) => {
startup () {
this.send("set_app_data", {
remotes: this.remotes,
defaults: this.defaults
})
fs.readFile(this.config_file, "utf8", (err, data) => {
if (err) {
this.send("set_app_data", {
status: {
code: -1 // Config not found
},
config: this.config_data,
pending_config: this.config_data,
});
return;
pending_config: this.config_data
})
return
}
let disk_config_data = JSON.parse(data);
let disk_config_data = JSON.parse(data)
// semi-shallow object merge
Object.keys(disk_config_data).map(key => {
if(!this.config_data.hasOwnProperty(key))
this.config_data[key] = {}
if (!this.config_data.hasOwnProperty(key)) { this.config_data[key] = {} }
this.config_data[key] = Object.assign(this.config_data[key], disk_config_data[key])
});
})
// here we may want to check if config data is valid, if not also send code -1
// i.e. check ports are integers and > 1024, check that data dir path exists, etc
const validated = Object.keys(this.defaults)
.filter(k => k in this.config_data)
.map(k => [k, this.validate_values(this.config_data[k], this.defaults[k])])
.reduce((map, obj) => {
map[obj[0]] = obj[1]
return map
}, {})
// Make sure the daemon data is valid
this.config_data = {
...this.config_data,
...validated
}
// 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,
});
pending_config: this.config_data
})
if(this.config_data.app.testnet) {
// 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) }
let testnet_dir = path.join(this.config_data.app.data_dir, "testnet")
if (!fs.existsSync(testnet_dir))
fs.mkdirSync(testnet_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"
}]
let log_dir = path.join(this.config_data.app.data_dir, "testnet", "logs")
if (!fs.existsSync(log_dir))
fs.mkdirSync(log_dir);
} else {
let log_dir = path.join(this.config_data.app.data_dir, "logs")
if (!fs.existsSync(log_dir))
fs.mkdirSync(log_dir);
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
}
}
this.daemon = new Daemon(this);
this.walletd = new WalletRPC(this);
const { net_type } = this.config_data.app
const dirs = {
"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.mkdirpSync(net_dir) }
const log_dir = path.join(net_dir, "logs")
if (!fs.existsSync(log_dir)) { fs.mkdirpSync(log_dir) }
this.daemon = new Daemon(this)
this.walletd = new WalletRPC(this)
this.send("set_app_data", {
status: {
code: 3 // Starting daemon
}
});
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.daemon.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.start(this.config_data).then(() => {
})
// 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", {
status: {
code: 7 // Reading wallet list
}
});
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
})
this.walletd.listWallets(true)
this.send("set_app_data", {
status: {
code: 0 // Ready
}
});
}).catch(error => {
// Go back to config
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
});
return;
});
}).catch(error => {
if(this.config_data.daemon.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})
})
return
}
}
// 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
})
// Go back to config
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
});
return;
});
})
return
}
}).catch(error => {
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
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
})
}
});
return;
});
});
this.daemon.start(this.config_data).then(() => {
this.send("set_app_data", {
status: {
code: 6 // Starting wallet
}
})
this.walletd.start(this.config_data).then(() => {
this.send("set_app_data", {
status: {
code: 7 // Reading wallet list
}
})
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 => {
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
})
})
})
})
}
quit() {
quit () {
return new Promise((resolve, reject) => {
let process = []
if(this.daemon)
process.push(this.daemon.quit())
if(this.walletd)
process.push(this.walletd.quit())
if(this.wss)
this.wss.close();
if (this.daemon) { process.push(this.daemon.quit()) }
if (this.walletd) { process.push(this.walletd.quit()) }
if (this.wss) { this.wss.close() }
Promise.all(process).then(() => {
resolve()
})
})
}
// 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 modified = { ...values }
// Make sure we have valid defaults
if (!isDictionary(defaults)) return modified
for (const key in modified) {
// Only modify if we have a default
if (!(key in defaults)) continue
const defaultValue = defaults[key]
const invalidDefault = defaultValue === null || defaultValue === undefined || Number.isNaN(defaultValue)
if (invalidDefault) continue
const value = modified[key]
// If we have a object then recurse through it
if (isDictionary(value)) {
modified[key] = this.validate_values(value, defaultValue)
} else {
// Check if we need to replace the value
const isValidValue = !(value === undefined || value === null || value === "" || Number.isNaN(value))
if (isValidValue) continue
// Otherwise set the default value
modified[key] = defaultValue
}
}
return modified
}
}

View file

@ -1,65 +1,76 @@
import child_process from "child_process";
const request = require("request-promise");
const queue = require("promise-queue");
const http = require("http");
const fs = require("fs");
const path = require("path");
import child_process from "child_process"
const request = require("request-promise")
const queue = require("promise-queue")
const http = require("http")
const fs = require("fs")
const path = require("path")
export class Daemon {
constructor(backend) {
constructor (backend) {
this.backend = backend
this.heartbeat = null
this.heartbeat_slow = null
this.id = 0
this.testnet = false
this.net_type = "mainnet"
this.local = false // do we have a local daemon ?
this.agent = new http.Agent({keepAlive: true, maxSockets: 1})
this.queue = new queue(1, Infinity)
}
checkVersion() {
checkVersion () {
return new Promise((resolve, reject) => {
if (process.platform === "win32") {
let ryod_path = path.join(__ryo_bin, "ryod.exe")
let ryod_version_cmd = `"${ryod_path}" --version`
if (!fs.existsSync(ryod_path))
resolve(false)
child_process.exec(ryod_version_cmd, (error, stdout, stderr) => {
if(error)
resolve(false)
let lokid_path = path.join(__ryo_bin, "lokid.exe")
let lokid_version_cmd = `"${lokid_path}" --version`
if (!fs.existsSync(lokid_path)) { resolve(false) }
child_process.exec(lokid_version_cmd, (error, stdout, stderr) => {
if (error) { resolve(false) }
resolve(stdout)
})
} else {
let ryod_path = path.join(__ryo_bin, "ryod")
let ryod_version_cmd = `"${ryod_path}" --version`
if (!fs.existsSync(ryod_path))
resolve(false)
child_process.exec(ryod_version_cmd, {detached: true}, (error, stdout, stderr) => {
if(error)
resolve(false)
let lokid_path = path.join(__ryo_bin, "lokid")
let lokid_version_cmd = `"${lokid_path}" --version`
if (!fs.existsSync(lokid_path)) { resolve(false) }
child_process.exec(lokid_version_cmd, {detached: true}, (error, stdout, stderr) => {
if (error) { resolve(false) }
resolve(stdout)
})
}
})
}
start(options) {
checkRemote (daemon) {
if (daemon.type === "local") {
return Promise.resolve({})
}
if(options.daemon.type === "remote") {
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]
if (daemon.type === "remote") {
this.local = false
// save this info for later RPC calls
this.protocol = "http://"
this.hostname = options.daemon.remote_host
this.port = options.daemon.remote_port
this.hostname = daemon.remote_host
this.port = daemon.remote_port
return new Promise((resolve, reject) => {
this.sendRPC("get_info").then((data) => {
if(!data.hasOwnProperty("error")) {
if (!data.hasOwnProperty("error")) {
this.startHeartbeat()
resolve()
} else {
@ -69,54 +80,62 @@ export class Daemon {
})
}
return new Promise((resolve, reject) => {
this.local = true
const args = [
"--data-dir", options.app.data_dir,
"--p2p-bind-ip", options.daemon.p2p_bind_ip,
"--p2p-bind-port", options.daemon.p2p_bind_port,
"--rpc-bind-ip", options.daemon.rpc_bind_ip,
"--rpc-bind-port", options.daemon.rpc_bind_port,
"--zmq-rpc-bind-ip", options.daemon.zmq_rpc_bind_ip,
"--zmq-rpc-bind-port", options.daemon.zmq_rpc_bind_port,
"--out-peers", options.daemon.out_peers,
"--in-peers", options.daemon.in_peers,
"--limit-rate-up", options.daemon.limit_rate_up,
"--limit-rate-down", options.daemon.limit_rate_down,
"--log-level", options.daemon.log_level,
];
"--p2p-bind-ip", daemon.p2p_bind_ip,
"--p2p-bind-port", daemon.p2p_bind_port,
"--rpc-bind-ip", daemon.rpc_bind_ip,
"--rpc-bind-port", daemon.rpc_bind_port,
"--zmq-rpc-bind-ip", daemon.zmq_rpc_bind_ip,
"--zmq-rpc-bind-port", daemon.zmq_rpc_bind_port,
"--out-peers", daemon.out_peers,
"--in-peers", daemon.in_peers,
"--limit-rate-up", daemon.limit_rate_up,
"--limit-rate-down", daemon.limit_rate_down,
"--log-level", daemon.log_level
]
if(options.app.testnet) {
this.testnet = true
args.push("--testnet")
args.push("--log-file", path.join(options.app.data_dir, "testnet", "logs", "ryod.log"))
} else {
args.push("--log-file", path.join(options.app.data_dir, "logs", "ryod.log"))
const dirs = {
"mainnet": options.app.data_dir,
"stagenet": path.join(options.app.data_dir, "stagenet"),
"testnet": path.join(options.app.data_dir, "testnet")
}
if(options.daemon.rpc_bind_ip !== "127.0.0.1")
args.push("--confirm-external-bind")
const { net_type } = options.app
this.net_type = net_type
if(options.daemon.type === "local_remote" && !options.app.testnet) {
if (net_type === "testnet") {
args.push("--testnet")
} else if (net_type === "stagenet") {
args.push("--stagenet")
}
args.push("--log-file", path.join(dirs[net_type], "logs", "lokid.log"))
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 === "mainnet") {
args.push(
"--bootstrap-daemon-address",
`${options.daemon.remote_host}:${options.daemon.remote_port}`
`${daemon.remote_host}:${daemon.remote_port}`
)
}
if (process.platform === "win32") {
this.daemonProcess = child_process.spawn(path.join(__ryo_bin, "ryod.exe"), args)
this.daemonProcess = child_process.spawn(path.join(__ryo_bin, "lokid.exe"), args)
} else {
this.daemonProcess = child_process.spawn(path.join(__ryo_bin, "ryod"), args, {
this.daemonProcess = child_process.spawn(path.join(__ryo_bin, "lokid"), args, {
detached: true
})
}
// save this info for later RPC calls
this.protocol = "http://"
this.hostname = options.daemon.rpc_bind_ip
this.port = options.daemon.rpc_bind_port
this.hostname = daemon.rpc_bind_ip
this.port = daemon.rpc_bind_port
this.daemonProcess.stdout.on("data", data => process.stdout.write(`Daemon: ${data}`))
this.daemonProcess.on("error", err => process.stderr.write(`Daemon: ${err}`))
@ -125,17 +144,17 @@ export class Daemon {
// To let caller know when the daemon is ready
let intrvl = setInterval(() => {
this.sendRPC("get_info").then((data) => {
if(!data.hasOwnProperty("error")) {
if (!data.hasOwnProperty("error")) {
this.startHeartbeat()
clearInterval(intrvl);
resolve();
clearInterval(intrvl)
resolve()
} else {
if(data.error.cause &&
if (data.error.cause &&
data.error.cause.code === "ECONNREFUSED") {
// Ignore
} else {
clearInterval(intrvl);
reject(error);
clearInterval(intrvl)
reject(error)
}
}
})
@ -143,26 +162,20 @@ export class Daemon {
})
}
handle(data) {
handle (data) {
let params = data.data
switch (data.method) {
case "ban_peer":
this.banPeer(params.host, params.seconds)
break
case "ban_peer":
this.banPeer(params.host, params.seconds)
break
default:
default:
}
}
banPeer(host, seconds=3600) {
if(!seconds)
seconds=3600
banPeer (host, seconds = 3600) {
if (!seconds) { seconds = 3600 }
let params = {
bans: [{
@ -173,51 +186,45 @@ export class Daemon {
}
this.sendRPC("set_bans", params).then((data) => {
if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
this.sendGateway("show_notification", {type: "negative", message: "Error banning peer", timeout: 2000})
return
}
let end_time = new Date(Date.now() + seconds * 1000).toLocaleString()
this.sendGateway("show_notification", {message: "Banned "+host+" until "+end_time, timeout: 2000})
this.sendGateway("show_notification", {message: "Banned " + host + " until " + end_time, timeout: 2000})
// Send updated peer and ban list
this.heartbeatSlowAction()
})
}
timestampToHeight(timestamp, pivot=null, recursion_limit=null) {
timestampToHeight (timestamp, pivot = null, recursion_limit = null) {
return new Promise((resolve, reject) => {
if(timestamp > 999999999999) {
if (timestamp > 999999999999) {
// We have got a JS ms timestamp, convert
timestamp = Math.floor(timestamp / 1000)
}
pivot = pivot || [137500, 1528073506]
recursion_limit = recursion_limit || 0;
recursion_limit = recursion_limit || 0
let diff = Math.floor((timestamp - pivot[1]) / 240)
let estimated_height = pivot[0] + diff
if(estimated_height <= 0) {
if (estimated_height <= 0) {
return resolve(0)
}
if(recursion_limit > 10) {
if (recursion_limit > 10) {
return resolve(pivot[0])
}
this.getRPC("block_header_by_height", {height: estimated_height}).then((data) => {
if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
if(data.error.code == -2) { // Too big height
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
if (data.error.code == -2) { // Too big height
this.getRPC("last_block_header").then((data) => {
if(data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
return reject()
}
@ -226,7 +233,7 @@ export class Daemon {
// If we are within an hour that is good enough
// If for some reason there is a > 1h gap between blocks
// the recursion limit will take care of infinite loop
if(Math.abs(timestamp - new_pivot[1]) < 3600) {
if (Math.abs(timestamp - new_pivot[1]) < 3600) {
return resolve(new_pivot[0])
}
@ -244,26 +251,23 @@ export class Daemon {
// If we are within an hour that is good enough
// If for some reason there is a > 1h gap between blocks
// the recursion limit will take care of infinite loop
if(Math.abs(timestamp - new_pivot[1]) < 3600) {
if (Math.abs(timestamp - new_pivot[1]) < 3600) {
return resolve(new_pivot[0])
}
// Continue recursion with new pivot
resolve(new_pivot)
})
}).then((pivot_or_height) => {
return Array.isArray(pivot_or_height)
? this.timestampToHeight(timestamp, pivot_or_height, recursion_limit + 1)
: pivot_or_height
}).catch(error => {
return false
})
}
startHeartbeat() {
startHeartbeat () {
clearInterval(this.heartbeat)
this.heartbeat = setInterval(() => {
this.heartbeatAction()
@ -275,14 +279,13 @@ export class Daemon {
this.heartbeatSlowAction()
}, 30 * 1000) // 30 seconds
this.heartbeatSlowAction()
}
heartbeatAction() {
heartbeatAction () {
let actions = []
// No difference between local and remote heartbeat action for now
if(this.local) {
if (this.local) {
actions = [
this.getRPC("info")
]
@ -296,9 +299,8 @@ export class Daemon {
let daemon_info = {
}
for (let n of data) {
if(n == undefined || !n.hasOwnProperty("result") || n.result == undefined)
continue
if(n.method == "get_info") {
if (n == undefined || !n.hasOwnProperty("result") || n.result == undefined) { continue }
if (n.method == "get_info") {
daemon_info.info = n.result
}
}
@ -306,28 +308,27 @@ export class Daemon {
})
}
heartbeatSlowAction() {
heartbeatSlowAction () {
let actions = []
if(this.local) {
if (this.local) {
actions = [
this.getRPC("connections"),
this.getRPC("bans"),
//this.getRPC("txpool_backlog"),
this.getRPC("bans")
// this.getRPC("txpool_backlog"),
]
} else {
actions = [
//this.getRPC("txpool_backlog"),
// this.getRPC("txpool_backlog"),
]
}
if(actions.length === 0) return
if (actions.length === 0) return
Promise.all(actions).then((data) => {
let daemon_info = {
}
for (let n of data) {
if(n == undefined || !n.hasOwnProperty("result") || n.result == undefined)
continue
if (n == undefined || !n.hasOwnProperty("result") || n.result == undefined) { continue }
if (n.method == "get_connections" && n.result.hasOwnProperty("connections")) {
daemon_info.connections = n.result.connections
} else if (n.method == "get_bans" && n.result.hasOwnProperty("bans")) {
@ -340,14 +341,19 @@ export class Daemon {
})
}
sendGateway(method, data) {
sendGateway (method, data) {
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",
@ -355,15 +361,15 @@ export class Daemon {
method: method
},
agent: this.agent
};
if(Object.keys(params).length !== 0) {
options.json.params = params;
}
if (Object.keys(params).length !== 0) {
requestOptions.json.params = params
}
return this.queue.add(() => {
return request(options)
return request(requestOptions)
.then((response) => {
if(response.hasOwnProperty("error")) {
if (response.hasOwnProperty("error")) {
return {
method: method,
params: params,
@ -392,12 +398,12 @@ export class Daemon {
/**
* Call one of the get_* RPC calls
*/
getRPC(parameter, args) {
return this.sendRPC(`get_${parameter}`, args);
getRPC (parameter, args) {
return this.sendRPC(`get_${parameter}`, args)
}
quit() {
clearInterval(this.heartbeat);
quit () {
clearInterval(this.heartbeat)
return new Promise((resolve, reject) => {
if (this.daemonProcess) {
this.daemonProcess.on("close", code => {

File diff suppressed because it is too large Load diff

View file

@ -15,72 +15,52 @@
<q-btn class="q-ml-sm" color="primary" @click="save()" label="Save" />
</q-toolbar>
<div>
<div class="address-book-modal q-mx-md">
<LokiField label="Address" :error="$v.newEntry.address.$error">
<q-input
v-model="newEntry.address"
:placeholder="address_placeholder"
@blur="$v.newEntry.address.$touch"
:dark="theme=='dark'"
hide-underline
/>
<q-checkbox
v-model="newEntry.starred"
checked-icon="star"
unchecked-icon="star_border"
class="star-entry"
dark
/>
</LokiField>
<LokiField label="Name">
<q-input
v-model="newEntry.name"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField label="Payment ID" :error="$v.newEntry.payment_id.$error" optional>
<q-input
v-model="newEntry.payment_id"
placeholder="16 or 64 hexadecimal characters"
@blur="$v.newEntry.payment_id.$touch"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField label="Notes" optional>
<q-input
v-model="newEntry.description"
placeholder="Additional notes"
type="textarea"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-list no-border :dark="theme=='dark'">
<q-item>
<q-item-side class="self-start">
<Identicon :address="newEntry.address" menu />
</q-item-side>
<q-item-main>
<q-field>
<q-input v-model="newEntry.address" float-label="Address"
@blur="$v.newEntry.address.$touch"
:error="$v.newEntry.address.$error"
:dark="theme=='dark'"
/>
</q-field>
</q-item-main>
</q-item>
<q-item>
<q-item-main>
<q-field>
<q-input v-model="newEntry.name" float-label="Name" :dark="theme=='dark'" />
</q-field>
</q-item-main>
<q-item-side class="self-start q-pa-sm">
<q-checkbox
v-model="newEntry.starred"
checked-icon="star"
unchecked-icon="star_border"
class="star-entry"
/>
</q-item-side>
</q-item>
<q-item>
<q-item-main>
<q-field>
<q-input v-model="newEntry.payment_id" float-label="Payment ID (optional)"
@blur="$v.newEntry.payment_id.$touch"
:error="$v.newEntry.payment_id.$error"
:dark="theme=='dark'"
/>
</q-field>
</q-item-main>
</q-item>
<q-item>
<q-item-main>
<q-field>
<q-input v-model="newEntry.description" type="textarea" float-label="Notes (optional)" :dark="theme=='dark'" />
</q-field>
</q-item-main>
</q-item>
<q-item v-if="mode=='edit'">
<q-item-main>
<q-field>
<q-btn class="float-right" color="red" @click="deleteEntry()" label="Delete" />
</q-field>
</q-item-main>
</q-item>
</q-list>
<q-field v-if="mode=='edit'">
<q-btn class="float-right" color="red" @click="deleteEntry()" label="Delete" />
</q-field>
</div>
</q-modal-layout>
@ -119,7 +99,7 @@
<span class="vertical-middle q-ml-xs">Recent transactions with this address</span>
</div>
<TxList type="in" :limit="5" :to-outgoing-address="entry.address" />
<TxList type="all_in" :limit="5" :to-outgoing-address="entry.address" :key="entry.address"/>
</div>
@ -137,6 +117,7 @@ import { mapState } from "vuex"
import Identicon from "components/identicon"
import AddressHeader from "components/address_header"
import TxList from "components/tx_list"
import LokiField from "components/loki_field"
import { payment_id, address } from "src/validators/common"
import { required } from "vuelidate/lib/validators"
export default {
@ -161,11 +142,27 @@ export default {
view_only: state => state.gateway.wallet.info.view_only,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
address_placeholder (state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
}
}),
validations: {
newEntry: {
address: { required, address },
address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
},
payment_id: { payment_id }
}
},
@ -235,7 +232,8 @@ export default {
components: {
AddressHeader,
Identicon,
TxList
TxList,
LokiField
}
}
</script>
@ -243,12 +241,14 @@ export default {
<style lang="scss">
.address-book-details {
.q-field {
margin: 0 10px 20px;
}
.q-checkbox.star-entry .q-checkbox-icon {
font-size:40px;
margin-left: 10px;
.address-book-modal {
> .loki-field {
margin-top: 16px;
}
.star-entry {
padding: 4px;
}
}
}
</style>

View file

@ -1,5 +1,5 @@
<template>
<q-modal v-model="isVisible" maximized :content-css="{padding: '50px'}">
<q-modal v-model="isVisible" maximized>
<q-modal-layout>
<q-toolbar slot="header" color="dark" inverted>
<q-btn
@ -23,6 +23,7 @@
<AddressHeader :address="address.address"
:title="address.address_index == 0 ? 'Primary address' : 'Sub-address (Index '+address.address_index+')'"
:extra="'You have '+(address.used?'used':'not used')+' this address'"
:showCopy="false"
/>
@ -32,14 +33,14 @@
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Balance</span></div>
<div class="value"><span><FormatRyo :amount="address.balance" /></span></div>
<div class="value"><span><FormatLoki :amount="address.balance" /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Unlocked balance</span></div>
<div class="value"><span><FormatRyo :amount="address.unlocked_balance" /></span></div>
<div class="value"><span><FormatLoki :amount="address.unlocked_balance" /></span></div>
</div>
</div>
@ -87,7 +88,7 @@
</div>
<div style="margin: 0 -16px;">
<TxList type="in" :limit="5" :to-incoming-address-index="address.address_index" />
<TxList type="all_in" :limit="5" :to-incoming-address-index="address.address_index" :key="address.address"/>
</div>
</div>
@ -106,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>
@ -126,9 +130,9 @@
<script>
import { mapState } from "vuex"
const {clipboard} = require("electron")
const { clipboard, nativeImage } = require("electron")
import AddressHeader from "components/address_header"
import FormatRyo from "components/format_ryo"
import FormatLoki from "components/format_loki"
import QrcodeVue from "qrcode.vue";
import TxList from "components/tx_list"
export default {
@ -143,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"})
@ -159,7 +173,7 @@ export default {
components: {
AddressHeader,
TxList,
FormatRyo,
FormatLoki,
QrcodeVue
}
}

View file

@ -1,17 +1,15 @@
<template>
<q-item class="address-header">
<q-item-side>
<Identicon :address="address" :size="12" ref="identicon" />
</q-item-side>
<q-item-main class="self-start">
<q-item-tile label>{{ title }}</q-item-tile>
<q-item-tile class="monospace break-all" sublabel>{{ address }}</q-item-tile>
<q-item-tile sublabel class="title">{{ title }}</q-item-tile>
<q-item-tile class="break-all" label>{{ address }}</q-item-tile>
<q-item-tile v-if="payment_id" sublabel>Payment id: {{ payment_id }}</q-item-tile>
<q-item-tile v-if="extra" sublabel>{{ extra }}</q-item-tile>
<q-item-tile v-if="extra" sublabel class="extra">{{ extra }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-item-side v-if="showCopy">
<q-btn
color="primary" style="width:25px;"
color="primary"
style="width:25px;"
size="sm" icon="file_copy"
ref="copy"
@click="copyAddress">
@ -28,11 +26,6 @@
@click.native="copyAddress(address, $event)">
<q-item-main label="Copy address" />
</q-item>
<q-item v-close-overlay
@click.native="$refs.identicon.saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
</q-context-menu>
@ -60,6 +53,11 @@ export default {
extra: {
type: String,
required: false
},
showCopy: {
type: Boolean,
required: false,
default: true
}
},
data () {
@ -125,7 +123,20 @@ export default {
.q-item-main {
.q-item-label {
font-size:2em;
font-weight: 400;
}
.q-item-sublabel, .q-list-header {
font-size: 13px;
}
.title {
font-size: 14px;
margin-bottom: 2px;
}
.extra {
margin-top: 8px;
}
}
}

View file

@ -1,21 +1,24 @@
<template>
<q-layout-footer class="status-footer">
<div class="status-line">
<div class="status-line row items-center">
<div class="status row items-center">
<span>Status:</span>
<span class="status-text" :class="[status]">{{ status | upperCase }}</span>
</div>
<div class="row">
<template v-if="config_daemon.type !== 'remote'">
<div>Daemon: {{ daemon.info.height_without_bootstrap }} / {{ target_height }} ({{ daemon_local_pct }}%)</div>
</template>
<template v-if="config.daemon.type !== 'remote'">
<div>Daemon: {{ daemon.info.height_without_bootstrap }} / {{ target_height }} ({{ daemon_local_pct }}%)</div>
</template>
<template v-if="config_daemon.type !== 'local'">
<div>Remote: {{ daemon.info.height }}</div>
</template>
<template v-if="config.daemon.type !== 'local'">
<div>Remote: {{ daemon.info.height }}</div>
</template>
<div>Wallet: {{ wallet.info.height }} / {{ target_height }} ({{ wallet_pct }}%)</div>
<div>{{ status }}</div>
<div>Wallet: {{ wallet.info.height }} / {{ target_height }} ({{ wallet_pct }}%)</div>
</div>
</div>
<div class="status-bars">
<div class="status-bars" :class="[status]">
<div v-bind:style="{ width: daemon_pct+'%' }"></div>
<div v-bind:style="{ width: wallet_pct+'%' }"></div>
</div>
@ -32,19 +35,22 @@ export default {
daemon: state => state.gateway.daemon,
wallet: state => state.gateway.wallet,
config_daemon (state) {
return this.config.daemons[this.config.app.net_type]
},
target_height (state) {
if(this.config.daemon.type === "local" && !this.daemon.info.is_ready)
if(this.config_daemon.type === "local")
return Math.max(this.daemon.info.height, this.daemon.info.target_height)
else
return this.daemon.info.height
},
daemon_pct (state) {
if(this.config.daemon.type === "local")
if(this.config_daemon.type === "local")
return this.daemon_local_pct
return 0
},
daemon_local_pct (state) {
if(this.config.daemon.type === "remote")
if(this.config_daemon.type === "remote")
return 0
let pct = (100 * this.daemon.info.height_without_bootstrap / this.target_height).toFixed(1)
if(pct == 100.0 && this.daemon.info.height_without_bootstrap < this.target_height)
@ -60,27 +66,32 @@ export default {
return pct
},
status(state) {
if(this.config.daemon.type === "local") {
if(this.daemon.info.height_without_bootstrap < this.target_height || !this.daemon.info.is_ready) {
return "Syncing..."
if(this.config_daemon.type === "local") {
if(this.daemon.info.height_without_bootstrap < this.target_height) {
return "syncing"
} else if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {
return "Scanning..."
return "scanning"
} else {
return "Ready"
return "ready"
}
} 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) {
return "Syncing..."
return "scanning"
} else if(this.config_daemon.type === "local_remote" && this.daemon.info.height_without_bootstrap < this.target_height) {
return "syncing"
} else {
return "Ready"
return "ready"
}
}
return
}
}),
filters: {
upperCase: function (status) {
return status.toUpperCase();
}
},
data () {
return {
}

View file

@ -1,12 +1,12 @@
<template>
<span>
{{ value }} Ryo
{{ value }} LOKI
</span>
</template>
<script>
export default {
name: "FormatRyo",
name: "FormatLoki",
props: {
amount: {
required: true

View file

@ -0,0 +1,95 @@
<template>
<div class="loki-field" :class="{disable, 'disable-hover': disableHover}">
<div class="label row items-center" v-if="label" :disabled="disable">
{{ label }}
<span v-if="optional" class="optional">(Optional)</span>
</div>
<div class="content row items-center" :class="{error}">
<slot></slot>
</div>
<div class="error-label" v-if="error && errorLabel" :disabled="disable">{{ errorLabel }}</div>
</div>
</template>
<script>
export default {
name: "LokiField",
props: {
label: {
type: String,
required: false
},
error: {
type: Boolean,
required: false,
default: false
},
errorLabel: {
type: String,
required: false
},
optional: {
type: Boolean,
required: false,
default: false
},
disable: {
type: Boolean,
required: false,
default: false
},
disableHover: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
}
},
}
</script>
<style lang="scss">
.loki-field {
.label {
margin: 6px 0;
font-weight: bold;
font-size: 12px;
text-transform: uppercase;
// Disable text selection
-webkit-user-select: none;
user-select: none;
cursor: default;
.optional {
font-weight: 400;
margin-left: 4px;
}
}
.content {
border-radius: 3px;
padding: 6px 8px;
min-height: 46px;
> * {
margin-right: 4px;
}
> *:last-child {
margin-right: 0px;
}
.q-input, .q-select, .q-datetime-input {
flex: 1;
}
.q-btn {
padding: 4px 8px;
font-size: 12px !important;
}
}
}
</style>

View file

@ -1,6 +1,6 @@
<template>
<div>
<q-btn class="menu" icon="menu" label="" size="md" flat>
<q-btn class="menu" icon="menu" size="md" flat>
<q-popover>
<q-list separator link>
<q-item v-close-overlay @click.native="switchWallet" v-if="!disableSwitchWallet">
@ -20,7 +20,7 @@
</q-item>
<q-item v-close-overlay @click.native="exit">
<q-item-main>
<q-item-tile label>Exit Ryo GUI Wallet</q-item-tile>
<q-item-tile label>Exit Loki GUI Wallet</q-item-tile>
</q-item-main>
</q-item>
</q-list>
@ -31,20 +31,22 @@
<q-modal minimized ref="aboutModal">
<div class="about-modal">
<img class="q-mb-md" src="statics/ryo-wallet.svg" height="42" />
<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>
<div class="q-mt-md q-mb-lg external-links">
<p>
<a @click="openExternal('https://ryo-currency.com/')" href="#">https://ryo-currency.com/</a>
<a @click="openExternal('https://loki.network/')" href="#">https://loki.network/</a>
</p>
<p>
<a @click="openExternal('https://t.me/ryocurrency')" href="#">Telegram</a> -
<a @click="openExternal('https://discord.gg/GFQmFtx')" href="#">Discord</a> -
<a @click="openExternal('https://www.reddit.com/r/ryocurrency/')" href="#">Reddit</a>
<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/loki-project/loki-electron-wallet')" href="#">Github</a>
</p>
</div>
@ -102,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

@ -11,6 +11,7 @@
<q-btn-toggle
v-model="page"
toggle-color="primary"
color="tertiary"
size="md"
:options="tabs"
/>
@ -25,22 +26,6 @@
</div>
</div>
<div v-if="page=='appearance'">
<div class="q-pa-md">
<h6 class="q-mb-md q-mt-none" style="font-weight: 300">Select Appearance:</h6>
<q-btn-toggle
v-model="theme"
toggle-color="primary"
size="md"
:options="[
{label: 'Light theme', value: 'light', icon: 'brightness_5'},
{label: 'Dark theme', value: 'dark', icon: 'brightness_2'},
]"
/>
</div>
</div>
<div v-if="page=='peers'">
<q-list :dark="theme=='dark'" no-border>
<q-list-header>Peer list</q-list-header>
@ -79,15 +64,16 @@ import SettingsGeneral from "components/settings_general"
export default {
name: "SettingsModal",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
daemon: state => state.gateway.daemon,
pending_config: state => state.gateway.app.pending_config,
config: state => state.gateway.app.config,
tabs: function(state) {
const { app, daemons } = state.gateway.app.config;
let tabs = [
{label: 'General', value: 'general', icon: 'settings'},
{label: 'Appearance', value: 'appearance', icon: 'visibility'},
]
if(state.gateway.app.config.daemon.type != 'remote') {
if(daemons[app.net_type].type != 'remote') {
tabs.push({label: 'Peers', value: 'peers', icon: 'cloud_queue'})
}
return tabs
@ -96,22 +82,10 @@ export default {
data () {
return {
page: "general",
theme: null,
isVisible: false
}
},
mounted: function () {
this.theme = this.config.appearance.theme
},
watch: {
theme: function (theme, old) {
if(old == null) return
this.$gateway.send("core", "quick_save_config", {
appearance: {
theme: this.theme
}
})
},
isVisible: function () {
if(this.isVisible == false) {
this.$store.dispatch("gateway/resetPendingConfig")

View file

@ -1,113 +1,157 @@
<template>
<div class="settings-general">
<div class="row justify-between q-mb-md">
<div><q-radio v-model="config.daemon.type" val="local_remote" label="Local + Remote Daemon" /></div>
<div><q-radio v-model="config.daemon.type" val="local" label="Local Daemon Only" /></div>
<div><q-radio v-model="config.daemon.type" val="remote" label="Remote Daemon Only" /></div>
<div><q-radio v-model="config_daemon.type" val="remote" label="Remote Daemon Only" /></div>
<div><q-radio v-model="config_daemon.type" val="local_remote" label="Local + Remote Daemon" /></div>
<div><q-radio v-model="config_daemon.type" val="local" label="Local Daemon Only" /></div>
</div>
<p v-if="config.daemon.type == 'local_remote'">
<p v-if="config_daemon.type == 'local_remote'">
Get started quickly with this default option. Wallet will download the full blockchain, but use a remote node while syncing.
</p>
<p v-if="config.daemon.type == 'local'">
<p v-if="config_daemon.type == 'local'">
Full security, wallet will download the full blockchain. You will not be able to transact until sync is completed.
</p>
<p v-if="config.daemon.type == 'remote'">
<p v-if="is_remote">
Less security, wallet will connect to a remote node to make all transactions.
</p>
<q-field v-if="config.daemon.type != 'remote'">
<div class="row gutter-sm">
<div class="col-8">
<q-input v-model="config.daemon.rpc_bind_ip" float-label="Local Daemon IP"
:dark="theme=='dark'" disable />
</div>
<div class="col-4">
<q-input v-model="config.daemon.rpc_bind_port" float-label="Local Daemon Port (RPC)" type="number" :decimals="0" :step="1" min="1024" max="65535" :dark="theme=='dark'" />
</div>
<template v-if="config_daemon.type != 'remote'">
<div class="row pl-sm">
<LokiField class="col-8" label="Local Daemon IP" disable>
<q-input
v-model="config_daemon.rpc_bind_ip"
:placeholder="daemon_defaults.rpc_bind_ip"
:dark="theme=='dark'"
disable
hide-underline
/>
</LokiField>
<LokiField class="col-4" label="Local Daemon Port (RPC)">
<q-input
v-model="config_daemon.rpc_bind_port"
:placeholder="toString(daemon_defaults.rpc_bind_port)"
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
</div>
</q-field>
</template>
<q-field v-if="config.daemon.type != 'local'">
<div class="row gutter-sm">
<div class="col-8">
<q-input v-model="config.daemon.remote_host" float-label="Remote Node Host" :dark="theme=='dark'" />
</div>
<div class="col-4">
<q-input v-model="config.daemon.remote_port" float-label="Remote Node Port" type="number" :decimals="0" :step="1" min="1024" max="65535" :dark="theme=='dark'" />
</div>
<template v-if="config_daemon.type != 'local'">
<div class="row q-mt-md pl-sm">
<LokiField class="col-8" label="Remote Node Host">
<q-input
v-model="config_daemon.remote_host"
:placeholder="daemon_defaults.remote_host"
:dark="theme=='dark'"
hide-underline
/>
<!-- Remote node presets -->
<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>
<q-item-tile label>{{ option.host }}:{{ option.port }}</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</q-btn-dropdown>
</LokiField>
<LokiField class="col-4" label="Remote Node Port">
<q-input
v-model="config_daemon.remote_port"
:placeholder="toString(daemon_defaults.remote_port)"
type="number"
:decimals="0"
:step="1"
min="1024"
max="65535"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
</div>
</q-field>
</template>
<q-field>
<div class="row gutter-sm">
<div class="col-8">
<q-input v-model="config.app.data_dir" stack-label="Data Storage Path" disable :dark="theme=='dark'" />
<input type="file" webkitdirectory directory id="dataPath" v-on:change="setDataPath" ref="fileInput" hidden />
</div>
<div class="col-4">
<q-btn v-on:click="selectPath" :text-color="theme=='dark'?'white':'dark'">Select Location</q-btn>
</div>
<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">
<div class="row pl-sm q-mt-sm">
<LokiField class="col-6" label="Daemon Log Level" :disable="is_remote">
<q-input v-model="config_daemon.log_level" :placeholder="toString(daemon_defaults.log_level)" :disable="is_remote" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="0" max="4" hide-underline />
</LokiField>
<LokiField class="col-6" label="Wallet Log Level">
<q-input v-model="config.wallet.log_level" :placeholder="toString(defaults.wallet.log_level)" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="0" max="4" hide-underline />
</LokiField>
</div>
</q-field>
<q-collapsible label="Advanced Options" header-class="non-selectable row reverse advanced-options-label">
<q-field>
<div class="row gutter-sm">
<div class="col-3">
<q-input v-model="config.daemon.log_level" :disable="config.daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Daemon Log Level" type="number" :decimals="0" :step="1" min="0" max="4" />
</div>
<div class="col-3">
<q-input v-model="config.wallet.log_level" :dark="theme=='dark'"
float-label="Wallet Log Level" type="number" :decimals="0" :step="1" min="0" max="4" />
</div>
<div class="col-3">
<q-checkbox v-model="config.app.testnet" label="Testnet" />
</div>
</div>
</q-field>
<q-field>
<div class="row gutter-sm">
<div class="col-3">
<q-input v-model="config.daemon.in_peers" :disable="config.daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Max Incoming Peers" type="number" :decimals="0" :step="1" min="-1" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.daemon.out_peers" :disable="config.daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Max Outgoing Peers" type="number" :decimals="0" :step="1" min="-1" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.daemon.limit_rate_up" :disable="config.daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Limit Upload Rate" type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.daemon.limit_rate_down" :disable="config.daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Limit Download Rate" type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" />
</div>
</div>
</q-field>
<q-field>
<div class="row gutter-sm">
<div class="col-3">
<q-input v-model="config.daemon.p2p_bind_port" :disable="config.daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Daemon P2P Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.daemon.zmq_rpc_bind_port" :disable="config.daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Daemon ZMQ Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.app.ws_bind_port" :dark="theme=='dark'"
float-label="Internal Wallet Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
<div class="col-3">
<q-input v-model="config.wallet.rpc_bind_port" :disable="config.daemon.type == 'remote'" :dark="theme=='dark'"
float-label="Wallet RPC Port" type="number" :decimals="0" :step="1" min="1024" max="65535" />
</div>
</div>
<div class="row pl-sm q-mt-md">
<LokiField class="col-3" label="Max Incoming Peers" :disable="is_remote">
<q-input v-model="config_daemon.in_peers" :placeholder="toString(daemon_defaults.in_peers)" :disable="is_remote" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Max Outgoing Peers" :disable="is_remote">
<q-input v-model="config_daemon.out_peers" :placeholder="toString(daemon_defaults.out_peers)" :disable="is_remote" :dark="theme=='dark'"
type="number" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Limit Upload Rate" :disable="is_remote">
<q-input v-model="config_daemon.limit_rate_up" :placeholder="toString(daemon_defaults.limit_rate_up)" :disable="is_remote" :dark="theme=='dark'"
type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Limit Download Rate" :disable="is_remote">
<q-input v-model="config_daemon.limit_rate_down" :placeholder="toString(daemon_defaults.limit_rate_down)" :disable="is_remote" :dark="theme=='dark'"
type="number" suffix="Kb/s" :decimals="0" :step="1" min="-1" max="65535" hide-underline />
</LokiField>
</div>
<div class="row pl-sm q-mt-md">
<LokiField class="col-3" label="Daemon P2P Port" :disable="is_remote">
<q-input v-model="config_daemon.p2p_bind_port" :placeholder="toString(daemon_defaults.p2p_bind_port)" :disable="is_remote" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Daemon ZMQ Port" :disable="is_remote">
<q-input v-model="config_daemon.zmq_rpc_bind_port" :placeholder="toString(daemon_defaults.zmq_rpc_bind_port)" :disable="is_remote" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Internal Wallet Port">
<q-input v-model="config.app.ws_bind_port" :placeholder="toString(defaults.app.ws_bind_port)" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
<LokiField class="col-3" label="Wallet RPC Port" :disable="is_remote">
<q-input v-model="config.wallet.rpc_bind_port" :placeholder="toString(defaults.wallet.rpc_bind_port)" :disable="is_remote" :dark="theme=='dark'"
float- type="number" :decimals="0" :step="1" min="1024" max="65535" hide-underline />
</LokiField>
</div>
<q-field helper="Choose a network" label="Network" orientation="vertical">
<q-option-group
type="radio"
v-model="config.app.net_type"
:options="[
{ label: 'Main Net', value: 'mainnet' },
{ label: 'Stage Net', value: 'stagenet' },
{ label: 'Test Net', value: 'testnet' }
]"
/>
</q-field>
</q-collapsible>
@ -116,19 +160,66 @@
<script>
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,
config: state => state.gateway.app.pending_config,
config_daemon (state) {
return this.config.daemons[this.config.app.net_type]
},
is_remote (state) {
return this.config_daemon.type === 'remote'
},
defaults: state => state.gateway.app.defaults,
daemon_defaults (state) {
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()
},
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;
const { host, port } = option;
if (host) this.config_daemon.remote_host = host;
if (port) this.config_daemon.remote_port = port;
},
toString (value) {
if (!value && typeof value !== "number") return ""
return String(value);
}
},
data () {
return {
select: 0,
}
},
components: {
LokiField,
}
}
</script>
@ -150,5 +241,21 @@ export default {
.q-collapsible-sub-item {
padding: 0;
}
.row.pl-sm {
> * + * {
padding-left: 16px;
}
}
.col.pt-sm {
> * + * {
padding-top: 16px;
}
}
.remote-dropdown {
padding: 0 !important;
}
}
</style>

View file

@ -1,5 +1,5 @@
<template>
<q-modal v-model="isVisible" maximized :content-css="{padding: '50px'}">
<q-modal v-model="isVisible" maximized>
<q-modal-layout>
<q-toolbar slot="header" color="dark" inverted>
<q-btn
@ -46,14 +46,14 @@
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Amount</span></div>
<div class="value"><span><FormatRyo :amount="tx.amount" /></span></div>
<div class="value"><span><FormatLoki :amount="tx.amount" /></span></div>
</div>
</div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Fee <template v-if="tx.type=='in'||tx.type=='pool'">(paid by sender)</template></span></div>
<div class="value"><span><FormatRyo :amount="tx.fee" /></span></div>
<div class="value"><span><FormatLoki :amount="tx.fee" /></span></div>
</div>
</div>
@ -85,9 +85,6 @@
<q-list no-border>
<q-list-header class="q-px-none">Incoming transaction sent to:</q-list-header>
<q-item class="q-px-none">
<q-item-side>
<Identicon :address="in_tx_address_used.address" ref="identicon" />
</q-item-side>
<q-item-main>
<q-item-tile label>{{ in_tx_address_used.address_index_text }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ in_tx_address_used.address }}</q-item-tile>
@ -99,11 +96,6 @@
@click.native="copyAddress(in_tx_address_used.address, $event)">
<q-item-main label="Copy address" />
</q-item>
<q-item v-close-overlay
@click.native="$refs.identicon.saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
</q-context-menu>
@ -116,26 +108,17 @@
<q-list-header class="q-px-none">Outgoing transaction sent to:</q-list-header>
<template v-if="out_destinations">
<q-item class="q-px-none" v-for="destination in out_destinations">
<q-item-side>
<Identicon :address="destination.address" ref="identicon" />
</q-item-side>
<q-item-main>
<q-item-tile label>{{ destination.name }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ destination.address }}</q-item-tile>
<q-item-tile sublabel><FormatRyo :amount="destination.amount" /></q-item-tile>
<q-item-tile sublabel><FormatLoki :amount="destination.amount" /></q-item-tile>
</q-item-main>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@click.native="copyAddress(destination.address, $event)">
<q-item-main label="Copy address" />
</q-item>
<q-item v-close-overlay
@click.native="$refs.identicon.saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
</q-context-menu>
@ -143,9 +126,6 @@
</template>
<template v-else>
<q-item class="q-px-none">
<q-item-side>
<Identicon address="" />
</q-item-side>
<q-item-main>
<q-item-tile label>Destination unknown</q-item-tile>
</q-item-main>
@ -180,9 +160,8 @@ const { clipboard } = require("electron")
import { mapState } from "vuex"
import { date } from "quasar"
const { formatDate } = date
import Identicon from "components/identicon"
import TxTypeIcon from "components/tx_type_icon"
import FormatRyo from "components/format_ryo"
import FormatLoki from "components/format_loki"
export default {
name: "TxDetails",
computed: mapState({
@ -217,9 +196,10 @@ export default {
let destination = this.tx.destinations[i]
destination.name = ""
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
const { name, description} = address_book[j]
const separator = description === "" ? "" : " - "
destination.name = `${name}${separator}${description}`
break;
}
}
@ -296,9 +276,8 @@ export default {
}
},
components: {
Identicon,
TxTypeIcon,
FormatRyo
FormatLoki
}
}
</script>

View file

@ -1,5 +1,5 @@
<template>
<div>
<div class="tx-list">
<template v-if="tx_list_paged.length === 0">
<p class="q-pa-md q-mb-none">No transactions found</p>
@ -7,24 +7,23 @@
<template v-else>
<q-infinite-scroll :handler="loadMore" ref="scroller">
<q-list link no-border :dark="theme=='dark'" class="tx-list">
<q-item v-for="(tx, index) in tx_list_paged" :key="tx.txid"
<q-list link no-border :dark="theme=='dark'" class="loki-list tx-list">
<q-item class="loki-list-item transaction" v-for="(tx, index) in tx_list_paged" :key="tx.txid"
@click.native="details(tx)" :class="'tx-'+tx.type">
<q-item-side>
<TxTypeIcon :type="tx.type" />
<q-item-side class="type">
<div>{{ tx.type | typeToString }}</div>
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ tx.txid }}</q-item-tile>
<q-item-tile sublabel>{{ formatHeight(tx) }}</q-item-tile>
<q-item-main class="main">
<q-item-tile class="amount" label>
<FormatLoki :amount="tx.amount" />
</q-item-tile>
<q-item-tile sublabel>{{ tx.txid }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-item-side class="meta">
<q-item-tile label>
<FormatRyo :amount="tx.amount" />
</q-item-tile>
<q-item-tile sublabel>
<timeago :datetime="tx.timestamp*1000" :auto-update="60">
</timeago>
<timeago :datetime="tx.timestamp*1000" :auto-update="60" />
</q-item-tile>
<q-item-tile sublabel>{{ formatHeight(tx) }}</q-item-tile>
</q-item-side>
<q-context-menu>
@ -64,7 +63,8 @@ import { QSpinnerDots } from "quasar"
import Identicon from "components/identicon"
import TxTypeIcon from "components/tx_type_icon"
import TxDetails from "components/tx_details"
import FormatRyo from "components/format_ryo"
import FormatLoki from "components/format_loki"
export default {
name: "TxList",
props: {
@ -105,7 +105,7 @@ export default {
theme: state => state.gateway.app.config.appearance.theme,
current_height: state => state.gateway.daemon.info.height,
wallet_height: state => state.gateway.wallet.info.height,
tx_list: state => state.gateway.wallet.transactions.tx_list
tx_list: state => state.gateway.wallet.transactions.tx_list,
}),
created () {
this.filterTxList()
@ -120,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()
}
@ -153,11 +157,52 @@ export default {
}
},
},
filters: {
typeToString: function (value) {
switch (value) {
case "in":
return "Received"
case "out":
return "Sent"
case "failed":
return "Failed"
case "pending":
case "pool":
return "Pending"
case "miner":
return "Miner"
case "snode":
return "Service Node"
case "gov":
return "Governance"
case "stake":
return "Stake"
default:
return "-"
}
}
},
methods: {
filterTxList () {
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
if(this.type !== "all" && this.type !== tx.type) {
if (this.type === "all_in" && !all_in.includes(tx.type)) {
return false
}
if (this.type === "all_out" && !all_out.includes(tx.type)) {
return false
}
if (this.type === "all_pending" && !all_pending.includes(tx.type)) {
return false
}
if(!this.type.startsWith("all") && this.type !== tx.type) {
valid = false
return valid
}
@ -236,10 +281,34 @@ export default {
Identicon,
TxTypeIcon,
TxDetails,
FormatRyo
FormatLoki
}
}
</script>
<style lang="scss">
.tx-list {
.loki-list-item {
padding-top: 0;
padding-bottom: 0;
}
.transaction {
.main {
margin: 0;
padding: 8px 10px;
div {
overflow: hidden;
text-overflow: ellipsis;
}
}
.type {
div {
min-width: 100px;
margin-right: 8px;
}
}
}
}
</style>

View file

@ -0,0 +1,106 @@
<template>
<div class="column wallet-info">
<div class="row justify-between items-center wallet-header loki-green">
<div class="title">{{ info.name }}</div>
<WalletSettings />
</div>
<div class="wallet-content">
<div class="row justify-center">
<div class="funds column items-center">
<div class="balance">
<div class="text"><span>Balance</span></div>
<div class="value"><span><FormatLoki :amount="info.balance" /></span></div>
</div>
<div class="row unlocked">
<span>Unlocked: <FormatLoki :amount="info.unlocked_balance" /></span>
</div>
</div>
</div>
<div class="wallet-address row justify-center items-center">
<div class="address">{{ info.address }}</div>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
ref="copy"
@click="copyAddress">
<q-tooltip anchor="center right" self="center left" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</div>
</div>
</div>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import FormatLoki from "components/format_loki"
import WalletSettings from "components/wallet_settings"
export default {
name: "WalletDetails",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info,
}),
methods: {
copyAddress () {
this.$refs.copy.$el.blur()
clipboard.writeText(this.info.address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: "Address copied to clipboard"
})
},
},
components: {
FormatLoki,
WalletSettings
},
}
</script>
<style lang="scss">
.wallet-info {
.wallet-header {
padding: 0.8rem 1.5rem;
.title {
font-weight: bold;
}
}
.wallet-content {
text-align: center;
background-color: #0A0A0A;
padding: 2em;
.balance {
.text {
font-size: 16px;
}
.value {
font-size: 35px;
}
}
.wallet-address {
margin-top: 12px;
.address {
overflow: hidden;
text-overflow: ellipsis;
margin: 4px 0;
}
.q-btn {
margin-left: 8px;
}
}
.unlocked {
font-size: 14px;
font-weight: 500;
}
}
}
</style>

View file

@ -0,0 +1,510 @@
<template>
<div class="wallet-settings">
<q-btn icon-right="more_vert" label="Settings" size="md" flat>
<q-popover anchor="bottom right" self="top right">
<q-list separator link>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="getPrivateKeys()">
<q-item-main>
<q-item-tile label>Show Private Keys</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="showModal('change_password')">
<q-item-main>
<q-item-tile label>Change Password</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="showModal('rescan')">
<q-item-main>
<q-item-tile label>Rescan Wallet</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="showModal('key_image')">
<q-item-main>
<q-item-tile label>Manage Key Images</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="deleteWallet()">
<q-item-main>
<q-item-tile label>Delete Wallet</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</q-popover>
</q-btn>
<!-- Modals -->
<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">
<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="col-auto">
<q-btn
class="copy-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 }}
</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)">
<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">Spend key</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.spend_key }}
</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)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy spend key
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<div class="q-mt-lg">
<q-btn
color="primary"
@click="hideModal('private_keys')"
label="Close"
/>
</div>
</div>
</q-modal>
<q-modal minimized v-model="modals.rescan.visible">
<div class="modal-header">Rescan wallet</div>
<div class="q-ma-lg">
<p>Select full rescan or rescan of spent outputs only.</p>
<div class="q-mt-lg">
<q-radio v-model="modals.rescan.type" val="full" label="Rescan full blockchain" />
</div>
<div class="q-mt-sm">
<q-radio v-model="modals.rescan.type" val="spent" label="Rescan spent outputs" />
</div>
<div class="q-mt-xl text-right">
<q-btn
flat class="q-mr-sm"
@click="hideModal('rescan')"
label="Close"
/>
<q-btn
color="primary"
@click="rescanWallet()"
label="Rescan"
/>
</div>
</div>
</q-modal>
<q-modal minimized v-model="modals.key_image.visible">
<div class="modal-header">{{modals.key_image.type}} key images</div>
<div class="q-ma-lg">
<div class="row q-mb-md">
<div class="q-mr-xl">
<q-radio v-model="modals.key_image.type" val="Export" label="Export" />
</div>
<div>
<q-radio v-model="modals.key_image.type" val="Import" label="Import" />
</div>
</div>
<template v-if="modals.key_image.type == 'Export'">
<q-field style="width:450px">
<div class="row gutter-sm">
<div class="col-9">
<q-input v-model="modals.key_image.export_path" stack-label="Key image export directory" disable />
<input type="file" webkitdirectory directory id="keyImageExportPath" v-on:change="setKeyImageExportPath" ref="keyImageExportSelect" hidden />
</div>
<div class="col-3">
<q-btn class="float-right" v-on:click="selectKeyImageExportPath">Browse</q-btn>
</div>
</div>
</q-field>
</template>
<template v-if="modals.key_image.type == 'Import'">
<q-field style="width:450px">
<div class="row gutter-sm">
<div class="col-9">
<q-input v-model="modals.key_image.import_path" stack-label="Key image import file" disable />
<input type="file" id="keyImageImportPath" v-on:change="setKeyImageImportPath" ref="keyImageImportSelect" hidden />
</div>
<div class="col-3">
<q-btn class="float-right" v-on:click="selectKeyImageImportPath">Browse</q-btn>
</div>
</div>
</q-field>
</template>
<div class="q-mt-xl text-right">
<q-btn
flat class="q-mr-sm"
@click="hideModal('key_image')"
label="Close"
/>
<q-btn
color="primary"
@click="doKeyImages()"
:label="modals.key_image.type"
/>
</div>
</div>
</q-modal>
<q-modal minimized v-model="modals.change_password.visible" @hide="clearChangePassword()">
<div class="modal-header">Change password</div>
<div class="q-ma-lg">
<q-field>
<q-input v-model="modals.change_password.old_password" type="password" float-label="Old Password" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-input v-model="modals.change_password.new_password" type="password" float-label="New Password" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-input v-model="modals.change_password.new_password_confirm" type="password" float-label="Confirm New Password" :dark="theme=='dark'" />
</q-field>
<div class="q-mt-xl text-right">
<q-btn
flat class="q-mr-sm"
@click="hideModal('change_password')"
label="Close"
/>
<q-btn
color="primary"
@click="doChangePassword()"
label="Change"
/>
</div>
</div>
</q-modal>
</div>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import WalletPassword from "src/mixins/wallet_password"
export default {
name: "WalletSettings",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info,
secret: state => state.gateway.wallet.secret,
data_dir: state => state.gateway.app.config.app.data_dir,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
}
}),
data () {
return {
modals: {
private_keys: {
visible: false,
},
rescan: {
visible: false,
type: "full",
},
key_image: {
visible: false,
type: "Export",
export_path: "",
import_path: "",
},
change_password: {
visible: false,
old_password: "",
new_password: "",
new_password_confirm: "",
},
}
}
},
mounted () {
const path = require("path")
this.modals.key_image.export_path = path.join(this.data_dir, "gui")
this.modals.key_image.import_path = path.join(this.data_dir, "gui", "key_image_export")
},
watch: {
secret: {
handler(val, old) {
if(val.view_key == old.view_key) return
switch(this.secret.view_key) {
case "":
break
case -1:
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.secret.mnemonic
})
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
})
break
default:
this.showModal("private_keys")
break
}
},
deep: true
}
},
methods: {
showModal (which) {
if(!this.is_ready) return
this.modals[which].visible = true
},
hideModal (which) {
this.modals[which].visible = false
},
copyPrivateKey (type, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
if(this.secret[type] == null) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Error copying private key",
})
return
}
clipboard.writeText(this.secret[type])
let type_human = type.substring(0,1).toUpperCase()+type.substring(1).replace("_"," ")
this.$q.dialog({
title: "Copy "+type_human,
message: "Be careful who you send your private keys to as they control your funds.",
ok: {
label: "OK"
},
}).then(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: type_human+" copied to clipboard"
})
}).catch(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: type_human+" copied to clipboard"
})
})
},
getPrivateKeys () {
if(!this.is_ready) return
this.showPasswordConfirmation({
title: "Show private keys",
noPasswordMessage: "Do you want to view your private keys?",
ok: {
label: "SHOW"
},
}).then(password => {
this.$gateway.send("wallet", "get_private_keys", {password})
}).catch(() => {
})
},
closePrivateKeys () {
this.hideModal("private_keys")
setTimeout(() => {
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
})
}, 500)
},
rescanWallet () {
this.hideModal("rescan")
if(this.modals.rescan.type == "full") {
this.$q.dialog({
title: "Rescan wallet",
message: "Warning: Some information about previous transactions\nsuch as the recipient's address will be lost.",
ok: {
label: "RESCAN"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$gateway.send("wallet", "rescan_blockchain")
}).catch(() => {
})
} else {
this.$gateway.send("wallet", "rescan_spent")
}
},
selectKeyImageExportPath () {
this.$refs.keyImageExportSelect.click()
},
setKeyImageExportPath (file) {
this.modals.key_image.export_path = file.target.files[0].path
},
selectKeyImageImportPath () {
this.$refs.keyImageImportSelect.click()
},
setKeyImageImportPath (file) {
this.modals.key_image.import_path = file.target.files[0].path
},
doKeyImages () {
this.hideModal("key_image")
this.showPasswordConfirmation({
title: this.modals.key_image.type + " key images",
noPasswordMessage: `Do you want to ${this.modals.key_image.type.toLowerCase()} key images?`,
ok: {
label: this.modals.key_image.type
},
}).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})
else if(this.modals.key_image.type == "Import")
this.$gateway.send("wallet", "import_key_images", {password: password, path: this.modals.key_image.import_path})
}).catch(() => {
})
},
doChangePassword () {
let old_password = this.modals.change_password.old_password
let new_password = this.modals.change_password.new_password
let new_password_confirm = this.modals.change_password.new_password_confirm
if(new_password == old_password) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "New password must be different"
})
} else if(new_password != new_password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "New passwords do not match"
})
} else {
this.hideModal("change_password")
this.$gateway.send("wallet", "change_wallet_password", {old_password, new_password})
}
},
clearChangePassword () {
this.modals.change_password.old_password = ""
this.modals.change_password.new_password = ""
this.modals.change_password.new_password_confirm = ""
},
deleteWallet () {
this.$q.dialog({
title: "Delete wallet",
message: "Are you absolutely sure you want to delete your wallet?\nMake sure you have your private keys backed up.\nTHIS PROCESS IS NOT REVERSIBLE!",
ok: {
label: "DELETE",
color: "red"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(() => {
return this.hasPassword()
}).then(hasPassword => {
if (!hasPassword) return ""
return this.$q.dialog({
title: "Delete wallet",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: "DELETE",
color: "red"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
})
}).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

@ -1,4 +1,5 @@
// app global css
@import '~variables'
@font-face {
font-family: 'RobotoMono-Light';
@ -12,16 +13,11 @@
background: #63c9f3;
}
:root {
--q-color-primary: #497dc6;
}
* {
scrollbar-track-color: #fff;
scrollbar-arrow-color: #fff;
&::-webkit-scrollbar-corner { background-color: #fff;}
&::-webkit-scrollbar-track-piece { background-color: #fff;}
scrollbar-track-color: #111;
scrollbar-arrow-color: #111;
&::-webkit-scrollbar-corner { background-color: #111;}
&::-webkit-scrollbar-track-piece { background-color: #111;}
scrollbar-face-color: #646464;
scrollbar-base-color: #646464;
@ -81,7 +77,7 @@ footer,
}
.q-item-sublabel {
color: #121212;
color: #cecece;
}
.advanced-options-label .q-item-side-right {
@ -89,14 +85,15 @@ footer,
text-align: left;
}
.q-layout-page {
min-height: 0 !important;
.q-layout {
background: $loki-black-80;
color:white;
}
.q-layout-header {
background: white;
background: #0A0A0A;
box-shadow: none;
border-bottom: 1px solid #ddd;
border-bottom: 1px solid #333;
height: 48px;
.q-toolbar-title {
@ -154,7 +151,10 @@ footer,
}
}
}
.q-layout-page {
min-height: 0 !important;
}
.infoBox {
@ -169,7 +169,7 @@ footer,
right: 10px;
top: 10px;
width: 64px;
color: #212529;
color: #cecece;
svg {
width: 60px;
@ -183,7 +183,7 @@ footer,
.text {
font-size: 13px;
margin-top: 8px;
color: #212529;
color: #cecece;
text-transform: uppercase;
}
@ -191,7 +191,7 @@ footer,
font-size: 24px;
margin-top: 4px;
font-weight: 800;
color: #212529;
color: #cecece;
font-weight:300;
}
}
@ -213,27 +213,44 @@ footer,
background-position: center center;
background-repeat: no-repeat;
background-color: #cb8fe1;
box-shadow: inset rgba(255, 255, 255, 0.6) 0 2px 2px, inset rgba(0, 0, 0, 0.3) 0 -2px 6px;
box-shadow: inset rgba(0, 0, 0, 0.1) 0 2px 2px, inset rgba(0, 0, 0, 0.3) 0 -2px 6px;
border-radius: 2px;
}
.q-layout-footer.status-footer {
border-top: 1px solid #ccc;
background: #000000;
color: rgba(255, 255, 255, 0.51);
border-top: 1px solid #333;
padding-top: 2px;
background: #fff;
box-shadow: none;
font-size: 12px;
.status-line {
padding: 8px;
margin-bottom: 5px;
div {
display: inline-block;
padding: 0 8px;
.status {
flex: 1;
span {
margin-right: 4px;
}
.status-text {
font-weight: bold;
text-transform: uppercase;
}
.ready {
color: $loki-green-solid;
}
.scanning, .syncing {
color: goldenrod;
}
}
div:last-child {
float:right;
div {
padding: 0 8px;
}
}
@ -244,17 +261,27 @@ footer,
position: absolute;
bottom: 0;
left: 0;
-webkit-transition: width 0.5s ease-out;
transition: width 0.5s ease-out;
}
div:first-child {
background-color: goldenrod;
background-color: $loki-green-dark-solid;
}
div:last-child {
background-color: green;
background-color: $loki-green-solid;
}
}
.status-bars.syncing, .status-bars.scanning {
div:first-child {
background-color: #bc8f1c;
}
div:last-child {
background-color:goldenrod;
}
}
}
@ -262,219 +289,292 @@ footer,
.tx-list {
.meta {
text-align:right;
}
.q-item.tx-in,
.q-item.tx-pool {
.q-icon {
color: #333;
}
.q-item-label {
color: green;
}
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: green;
&:before {
content: "+";
color: green;
}
}
.q-item.tx-pool,
.q-item.tx-miner,
.q-item.tx-snode,
.q-item.tx-gov {
.amount span {
color: #43bd43;
&:before {
content: "+";
color: #43bd43;
}
}
}
.q-item.tx-stake {
.amount span {
color: goldenrod;
}
}
.q-item.tx-out,
.q-item.tx-pending {
.q-icon {
color: #333;
}
.q-item-label {
color: purple;
}
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: purple;
&:before {
content: "-";
color: purple;
}
}
.amount span {
color: white;
&:before {
content: "-";
font-weight: bold;
color: white;
}
}
}
.q-item.tx-failed {
.amount span {
color: orangered;
}
}
}
.q-list-dark .q-item,
.q-item-dark .q-item,
.q-list-header {
color: #cecece;
}
body.dark {
.q-stepper-tab.step-waiting,
.q-stepper-step-content{
color: #cecece;
}
&, * {
scrollbar-track-color: #111;
scrollbar-arrow-color: #111;
&::-webkit-scrollbar-corner { background-color: #111;}
&::-webkit-scrollbar-track-piece { background-color: #111;}
}
.q-popover {
background: #222;
color:#cecece;
.q-item-sublabel {
color: #cecece;
}
.infoBoxIcon {
color: #cecece;
}
.infoBoxContent {
.text {
color: #cecece;
}
.value {
color: #cecece;
}
}
.identicon {
box-shadow: inset rgba(0, 0, 0, 0.1) 0 2px 2px, inset rgba(0, 0, 0, 0.3) 0 -2px 6px;
border-radius: 2px;
}
.q-layout {
background: #111;
color:#cecece;
.q-layout-header {
background: #111;
box-shadow: none;
border-bottom: 1px solid #333;
}
}
.q-layout-footer.status-footer {
background: #111;
color: #cecece;
.q-list-separator > .q-item-division + .q-item-division, .q-item-division + .q-item-separator {
border-top: 1px solid #333;
}
}
.q-list-dark .q-item,
.q-item-dark .q-item,
.q-list-header {
color: #cecece;
.header-popover.q-popover {
background: $primary;
color: white;
.q-list-separator > .q-item-division + .q-item-division, .q-item-division + .q-item-separator {
border-top: 1px solid $loki-green-dark-solid;
}
}
.q-stepper-tab.step-waiting,
.q-stepper-step-content{
color: #cecece;
.modal.minimized {
.q-input {
.q-input-target {
color: #cecece;
}
&.q-if:hover:before {
color: #cecece;
}
}
}
.modal {
.q-popover {
background: #222;
.modal-content,
.modal-body {
background: $loki-black-80;
color:#cecece;
.q-list-separator > .q-item-division + .q-item-division, .q-item-division + .q-item-separator {
border-top: 1px solid #333;
}
}
.modal.minimized {
.q-input {
.q-input-target {
color: #cecece;
}
&.q-if:hover:before {
.q-layout-header {
.q-toolbar {
&>* {
color: #cecece;
}
}
}
.modal {
.modal-content,
.modal-body {
background: #111;
color:#cecece;
}
.q-layout-header {
background: #111;
box-shadow: none;
border-bottom: 1px solid #333;
.q-toolbar {
&>* {
color: #cecece;
}
}
}
}
.tx-list {
.q-item.tx-in,
.q-item.tx-pool {
.q-icon {
color: #cecece;
}
.q-item-label {
color: lightgreen;
}
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: lightgreen;
&:before {
content: "+";
color: lightgreen;
}
}
}
}
}
.q-item.tx-out,
.q-item.tx-pending {
.q-icon {
color: #cecece;
}
.q-item-label {
color: mediumpurple;
}
&>div:last-child {
text-align:right;
&>div:first-child {
span {
color: mediumpurple;
&:before {
content: "-";
color: mediumpurple;
}
}
}
}
}
}
.q-layout-footer.status-footer {
.status-bars {
div:first-child {
background-color: goldenrod;
}
div:last-child {
background-color: #497dc6;
}
}
}
}
.loki-green {
background: $loki-green;
color: white;
}
.startup-icons {
.solid {
color:$loki-green-solid;
g,path {
fill: $loki-green-solid;
}
}
}
.hr-separator {
height: 2px;
background: $secondary;
opacity: 0.4;
}
.navigation {
.q-btn {
color: white;
background: $loki-black-90;
}
.router-link-exact-active > .q-btn {
background: $loki-green;
}
}
.wallet-list {
.q-item-label {
color: white;
}
.q-item {
background: $secondary;
.wallet-icon {
color: $tertiary;
g,path {
fill: $tertiary;
}
}
}
.q-item:hover, .q-item.selected {
background: $primary !important;
.wallet-icon {
color:$loki-green-solid;
g,path {
fill: $loki-green-solid;
}
}
.q-icon {
color: white;
}
.q-item-sublabel {
color: white
}
}
}
.receive {
.q-list-header {
color: #FFFFFF;
}
.q-item-sublabel {
color: $loki-black-50;
}
.q-item-separator-component {
background-color: $secondary;
opacity: 0.4;
}
.primary-address {
background: #3eb13e !important;
.q-item, .q-item-side {
color: white;
}
.q-item-sublabel {
color: rgba(255,255,255,0.9);
}
}
.primary-address:hover {
background: $loki-green-solid !important;
}
}
.loki-list {
.loki-list-item {
background: #313131;
-webkit-transition: background-color 0.2s ease-in;
transition: background-color 0.2s ease-in;
margin: 0 16px;
border-radius: 3px;
+ .loki-list-item {
margin-top: 10px;
}
}
.loki-list-item:hover {
background: rgba(117,117,117,0.3);
}
}
.tx-list {
.transaction .main {
border-left: 1px solid #252525;
}
}
.loki-field {
.content {
border: 1px solid #484848;
-webkit-transition: background-color 0.2s ease-in, border-color 0.2s ease-in;
transition: background-color 0.2s ease-in, border-color 0.2s ease-in;
}
&:not(.disable):not(.disable-hover) {
.content:hover {
background: #2e2e2e;
}
}
&.disable {
.content {
border-color: #404040 !important;
}
}
.content.error {
border-color: red;
}
.label {
color: white;
.optional {
color: #7C7C7C;
}
}
}
.service-node-staking {
.address-type {
color: $loki-green-solid;
&.not-ours {
color: goldenrod;
}
}
}
.service-node-registration {
.description{
color: #b7b7b7;
font-style: normal;
b {
color: white;
font-style: italic;
}
}
}
.welcome {
.q-layout-footer {
background: $secondary
}
}
.address-book, .address-header {
.q-item-label {
color: white;
}
}

View file

@ -14,12 +14,21 @@
// It"s highly recommended to change the default colors
// to match your app"s branding.
$primary = #027be3
$secondary = #26A69A
$tertiary = #555
$primary = $loki-green
$secondary = $loki-black-90
$tertiary = $loki-black-80
$neutral = #E0E1E2
$positive = #21BA45
$negative = #DB2828
$info = #31CCEC
$warning = #F2C037
$loki-green = linear-gradient(180deg, #419B41 0%, #43BD43 100%)
$loki-green-solid = #5BCA5B;
$loki-green-dark-solid = #419B41;
$loki-black-90 = #0A0A0A
$loki-black-80 = #252525
$loki-black-60 = #313131
$loki-black-50 = #7E7E7E;

View file

@ -22,70 +22,70 @@
SOFTWARE.
*/
const crypto = require("crypto");
const crypto = require("crypto")
const ALGORITHM_NAME = "aes-128-gcm";
const ALGORITHM_NONCE_SIZE = 12;
const ALGORITHM_TAG_SIZE = 16;
const ALGORITHM_KEY_SIZE = 16;
const PBKDF2_NAME = "sha256";
const PBKDF2_SALT_SIZE = 16;
const PBKDF2_ITERATIONS = 32767;
const ALGORITHM_NAME = "aes-128-gcm"
const ALGORITHM_NONCE_SIZE = 12
const ALGORITHM_TAG_SIZE = 16
const ALGORITHM_KEY_SIZE = 16
const PBKDF2_NAME = "sha256"
const PBKDF2_SALT_SIZE = 16
const PBKDF2_ITERATIONS = 32767
export class SCEE {
encryptString(plaintext, password) {
encryptString (plaintext, password) {
// Generate a 128-bit salt using a CSPRNG.
let salt = crypto.randomBytes(PBKDF2_SALT_SIZE);
let salt = crypto.randomBytes(PBKDF2_SALT_SIZE)
// Derive a key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME)
// Encrypt and prepend salt.
let ciphertextAndNonceAndSalt = Buffer.concat([ salt, this.encrypt(new Buffer(plaintext, "utf8"), key) ]);
let ciphertextAndNonceAndSalt = Buffer.concat([ salt, this.encrypt(new Buffer(plaintext, "utf8"), key) ])
// Return as base64 string.
return ciphertextAndNonceAndSalt.toString("base64");
return ciphertextAndNonceAndSalt.toString("base64")
}
decryptString(base64CiphertextAndNonceAndSalt, password) {
decryptString (base64CiphertextAndNonceAndSalt, password) {
// Decode the base64.
let ciphertextAndNonceAndSalt = new Buffer(base64CiphertextAndNonceAndSalt, "base64");
let ciphertextAndNonceAndSalt = new Buffer(base64CiphertextAndNonceAndSalt, "base64")
// Create buffers of salt and ciphertextAndNonce.
let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE);
let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE);
let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE)
let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE)
// Derive the key using PBKDF2.
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);
let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME)
// Decrypt and return result.
return this.decrypt(ciphertextAndNonce, key).toString("utf8");
return this.decrypt(ciphertextAndNonce, key).toString("utf8")
}
encrypt(plaintext, key) {
encrypt (plaintext, key) {
// Generate a 96-bit nonce using a CSPRNG.
let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE);
let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE)
// Create the cipher instance.
let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce);
let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce)
// Encrypt and prepend nonce.
let ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final() ]);
let ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final() ])
return Buffer.concat([ nonce, ciphertext, cipher.getAuthTag() ]);
return Buffer.concat([ nonce, ciphertext, cipher.getAuthTag() ])
}
decrypt(ciphertextAndNonce, key) {
decrypt (ciphertextAndNonce, key) {
// Create buffers of nonce, ciphertext and tag.
let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE);
let ciphertext = ciphertextAndNonce.slice(ALGORITHM_NONCE_SIZE, ciphertextAndNonce.length - ALGORITHM_TAG_SIZE);
let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE);
let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE)
let ciphertext = ciphertextAndNonce.slice(ALGORITHM_NONCE_SIZE, ciphertextAndNonce.length - ALGORITHM_TAG_SIZE)
let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE)
// Create the cipher instance.
let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce);
let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce)
// Decrypt and return result.
cipher.setAuthTag(tag);
return Buffer.concat([ cipher.update(ciphertext), cipher.final() ]);
cipher.setAuthTag(tag)
return Buffer.concat([ cipher.update(ciphertext), cipher.final() ])
}
}

View file

@ -1,26 +1,25 @@
import { ipcRenderer } from "electron"
import { Notify, Dialog, Loading, LocalStorage } from "quasar"
import { SCEE } from "./SCEE-Node";
import * as WebSocket from "ws"
export class Gateway {
constructor(app, router) {
import { EventEmitter } from "events"
import { SCEE } from "./SCEE-Node"
export class Gateway extends EventEmitter {
constructor (app, router) {
super()
this.app = app
this.router = router
this.token = null
this.scee = new SCEE()
let theme = LocalStorage.has("theme") ? LocalStorage.get.item("theme") : "light"
let theme = LocalStorage.has("theme") ? LocalStorage.get.item("theme") : "dark"
this.app.store.commit("gateway/set_app_data", {
config: {
appearance: {
theme
}
}
});
this.app.store.watch( state => state.gateway.app.config.appearance.theme, (theme) => {
})
this.app.store.watch(state => state.gateway.app.config.appearance.theme, (theme) => {
LocalStorage.set("theme", theme)
})
@ -30,34 +29,33 @@ export class Gateway {
status: {
code: 1 // Connecting to backend
}
});
})
ipcRenderer.on("initialize", (event, data) => {
this.token = data.token
setTimeout(() => {
this.ws = new WebSocket("ws://127.0.0.1:"+data.port);
this.ws.on("open", () => {this.open()});
this.ws.on("message", (message) => {this.receive(message)});
}, 1000);
});
this.ws = new WebSocket("ws://127.0.0.1:" + data.port)
this.ws.addEventListener("open", () => { this.open() })
this.ws.addEventListener("message", (e) => { this.receive(e.data) })
}, 1000)
})
ipcRenderer.on("confirmClose", () => {
this.confirmClose("Are you sure you want to exit?")
});
})
}
open() {
open () {
this.app.store.commit("gateway/set_app_data", {
status: {
code: 2 // Loading config
}
});
this.send("core", "init");
})
this.send("core", "init")
}
confirmClose(msg) {
if(this.closeDialog) {
confirmClose (msg) {
if (this.closeDialog) {
return
}
this.closeDialog = true
@ -70,7 +68,7 @@ export class Gateway {
cancel: {
flat: true,
label: "CANCEL",
color: this.app.store.state.gateway.app.config.appearance.theme=="dark"?"white":"dark"
color: this.app.store.state.gateway.app.config.appearance.theme == "dark" ? "white" : "dark"
}
}).then(() => {
this.closeDialog = false
@ -80,76 +78,88 @@ export class Gateway {
}).catch(() => {
this.closeDialog = false
})
}
send(module, method, data={}) {
send (module, method, data = {}) {
let message = {
module,
method,
data
}
let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token);
this.ws.send(encrypted_data);
let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token)
this.ws.send(encrypted_data)
}
receive(message) {
receive (message) {
// should wrap this in a try catch, and if fail redirect to error screen
// shouldn't happen outside of dev environment
let decrypted_data = JSON.parse(this.scee.decryptString(message, this.token));
let decrypted_data = JSON.parse(this.scee.decryptString(message, this.token))
if (typeof decrypted_data !== "object" ||
!decrypted_data.hasOwnProperty("event") ||
!decrypted_data.hasOwnProperty("data"))
return
!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
case "set_app_data":
this.app.store.commit("gateway/set_app_data", decrypted_data.data)
break
case "set_app_data":
this.app.store.commit("gateway/set_app_data", decrypted_data.data)
break
case "set_daemon_data":
this.app.store.commit("gateway/set_daemon_data", decrypted_data.data)
break
case "set_daemon_data":
this.app.store.commit("gateway/set_daemon_data", decrypted_data.data)
break
case "set_wallet_data":
case "set_wallet_error":
this.app.store.commit("gateway/set_wallet_data", decrypted_data.data)
break
case "set_wallet_data":
case "set_wallet_error":
this.app.store.commit("gateway/set_wallet_data", decrypted_data.data)
break
case "reset_wallet_error":
this.app.store.dispatch("gateway/resetWalletStatus")
break
case "set_tx_status":
this.app.store.commit("gateway/set_tx_status", decrypted_data.data)
break
case "set_tx_status":
this.app.store.commit("gateway/set_tx_status", decrypted_data.data)
break
case "wallet_list":
this.app.store.commit("gateway/set_wallet_list", decrypted_data.data)
break
case "set_snode_status":
this.app.store.commit("gateway/set_snode_status", decrypted_data.data)
break
case "settings_changed_reboot":
this.confirmClose("Changes require restart. Would you like to exit now?")
break
case "set_old_gui_import_status":
this.app.store.commit("gateway/set_old_gui_import_status", decrypted_data.data)
break
case "show_notification":
let notification = {
type: "positive",
timeout: 1000,
message: ""
}
Notify.create(Object.assign(notification, decrypted_data.data))
break
case "wallet_list":
this.app.store.commit("gateway/set_wallet_list", decrypted_data.data)
break
case "return_to_wallet_select":
this.router.replace({ path: "/wallet-select" })
setTimeout(() => {
// short delay to prevent wallet data reaching the
// websocket moments after we close and reset data
this.app.store.dispatch("gateway/resetWalletData")
}, 250);
break
case "settings_changed_reboot":
this.confirmClose("Changes require restart. Would you like to exit now?")
break
case "show_notification":
let notification = {
type: "positive",
timeout: 1000,
message: ""
}
Notify.create(Object.assign(notification, decrypted_data.data))
break
case "return_to_wallet_select":
this.router.replace({ path: "/wallet-select" })
setTimeout(() => {
// short delay to prevent wallet data reaching the
// websocket moments after we close and reset data
this.app.store.dispatch("gateway/resetWalletData")
}, 250)
break
}
}
}

View file

@ -11,9 +11,9 @@
@click="cancel()" />
</template>
<q-toolbar-title v-if="page_title=='Ryo'">
<div style="margin-top:7px">
<img src="statics/ryo-wallet.svg" height="32">
<q-toolbar-title v-if="page_title=='Loki'">
<div class="flex items-center justify-center" style="margin-top:7px">
<img src="statics/loki.svg" height="32">
</div>
</q-toolbar-title>
<q-toolbar-title v-else>
@ -59,12 +59,14 @@ 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"
default:
case "wallet-select":
return "Ryo"
return "Loki"
}
}
},

View file

@ -2,29 +2,62 @@
<q-layout view="hHh Lpr lFf">
<q-layout-header class="shift-title">
<main-menu />
<q-tabs class="col" align="justify" :color="theme == 'dark' ? 'light' : 'dark'" inverted>
<q-route-tab to="/wallet" default slot="title">
<span><q-icon name="attach_money" /> Wallet</span>
</q-route-tab>
<q-route-tab to="/wallet/receive" slot="title">
<span><q-icon name="call_received" /> Receive</span>
</q-route-tab>
<q-route-tab to="/wallet/send" slot="title">
<span><q-icon name="call_made" /> Send</span>
</q-route-tab>
<q-route-tab to="/wallet/addressbook" slot="title">
<span><q-icon name="person" /> Address Book</span>
</q-route-tab>
<q-route-tab to="/wallet/txhistory" slot="title">
<span><q-icon name="history" /> TX History</span>
</q-route-tab>
</q-tabs>
<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>
<!-- <AddressHeader :address="info.address" :title="info.name" /> -->
<WalletDetails />
<div class="navigation row items-end">
<router-link to="/wallet">
<q-btn
class="single-icon"
size="md"
icon="swap_horiz"
/>
</router-link>
<router-link to="/wallet/send">
<q-btn
class="large-btn"
label="Send"
size="md"
icon-right="arrow_right_alt"
align="left"
/>
</router-link>
<router-link to="/wallet/receive">
<q-btn
class="large-btn"
label="Receive"
size="md"
icon-right="save_alt"
align="left"
/>
</router-link>
<router-link to="/wallet/servicenode">
<q-btn
class="large-btn"
label="Service node"
size="md"
icon-right="router"
align="left"
/>
</router-link>
<router-link to="/wallet/addressbook" class="address">
<q-btn
class="single-icon"
size="md"
icon="person"
/>
</router-link>
</div>
<div class="hr-separator" />
<keep-alive>
<router-view />
</keep-alive>
@ -36,14 +69,18 @@
</template>
<script>
const { clipboard } = require("electron")
import { openURL } from "quasar"
import { mapState } from "vuex"
import WalletDetails from "components/wallet_details"
import FormatLoki from "components/format_loki"
import StatusFooter from "components/footer"
import MainMenu from "components/mainmenu"
export default {
name: "LayoutDefault",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info,
}),
data() {
return {
@ -55,10 +92,41 @@ export default {
},
components: {
StatusFooter,
MainMenu
MainMenu,
WalletDetails
}
}
</script>
<style>
<style lang="scss">
.navigation {
padding: 8px 12px;
> * {
margin: 2px 0;
margin-right: 12px;
}
> *:last-child {
margin-right: 0px;
}
.address {
margin-left: auto;
}
.single-icon {
width: 38px;
padding: 0;
}
.large-btn {
width: 160px;
.q-btn-inner > *:last-child {
margin-left: auto;
}
}
}
</style>

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

@ -2,32 +2,7 @@
<q-page>
<div class="init-screen-page text-center">
<div class="absolute-center">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="640" viewBox="0 0 859.4011 184.7379">
<defs>
<linearGradient id="b">
<stop offset="0" stop-color="#323232"/>
<stop offset="1" stop-color="#b4b4b4"/>
</linearGradient>
<linearGradient id="a" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#62c9f3"/>
<stop offset="1" stop-color="#3d58b0"/>
</linearGradient>
<linearGradient id="d" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="h" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
<linearGradient id="c" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="e" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="f" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="i" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
</defs>
<path fill="url(#c)" d="M41.7535 41.89H75.105v33.0785H41.7535zm-4.2333-4.2334v41.5453h41.8181V37.6565zM58.4295 4.2333c29.9566 0 54.1957 24.2396 54.1957 54.1962 0 29.9566-24.239 54.1957-54.1957 54.1957-29.9566 0-54.1962-24.239-54.1962-54.1957 0-29.9566 24.2396-54.1962 54.1962-54.1962zm0-4.2333C26.185 0 0 26.185 0 58.4295s26.185 58.429 58.4295 58.429 58.429-26.1845 58.429-58.429S90.674 0 58.4295 0z"/>
<g fill="url(#d)" transform="translate(12 -183.1415)">
<path fill="url(#e)" d="M191.1345 287.4374l-15.7334-39.4667q-1.7333.2667-5.4666.2667h-34v39.2h-4.8v-91.7333h40.8q10.5333 0 15.7333 3.6 5.2 3.6 6.5333 9.0666 1.3334 5.3334 1.3334 13.6 0 6.6667-.9334 11.4667-.9333 4.6667-4.2666 8.4-3.3334 3.7333-10 5.3333l16.1333 40.2667zm-21.4667-43.7333q9.3334 0 13.8667-2.6667 4.5333-2.6667 5.8666-7.0667 1.3334-4.5333 1.3334-12 0-7.6-1.2-12-1.0667-4.4-5.2-7.0666-4-2.6667-12.6667-2.6667h-35.7333v43.4667zm63.6625 43.7333v-37.4667l-34.8-54.2666h5.6l31.2 49.2h.9333l31.3334-49.2h5.4666l-34.9333 54.2666v37.4667zm79.7979 1.0666q-17.8666 0-25.0666-3.0666-7.2-3.0667-9.3334-12.1333-2-9.0667-2-31.7334 0-22.6666 2-31.7333 2.1334-9.0666 9.3334-12.1333 7.2-3.0667 25.0666-3.0667 17.8667 0 25.0667 3.0667 7.2 3.0666 9.2 12.1333 2.1333 9.0667 2.1333 31.7333 0 22.6667-2.1333 31.7334-2 9.0666-9.2 12.1333-7.2 3.0667-25.0667 3.0667zm0-4.5333q16.1334 0 21.8667-2 5.8667-2.1333 7.7333-10.2666 2-8.2667 2-30.1334 0-21.8666-2-30-1.8666-8.2666-7.7333-10.2666-5.7333-2.1334-21.8667-2.1334-16.1333 0-22 2.1334-5.7333 2-7.7333 10.2666-1.8667 8.1334-1.8667 30 0 21.8667 1.8667 30.1334 2 8.1333 7.7333 10.2666 5.8667 2 22 2zm153.3563 3.4667l-27.0667-83.0667h-.5333l-27.0667 83.0667h-4.6666l-27.2-91.7333h5.2l24.1333 83.0666h.8l26.6667-83.0666h5.0666l26.6667 83.0666h.8l24.1333-83.0666h4.9333l-27.2 91.7333zm104.6416 0l-12.1333-29.6h-45.8667l-12.1333 29.6h-5.0667l37.8667-91.7333h4.6667l37.8666 91.7333zm-34.6666-84.9333h-.6667l-20.8 50.8h42.1333zm51.6125 84.9333v-91.7333h4.8v87.2h46.6666v4.5333zm62.3541 0v-91.7333h4.8v87.2h46.6667v4.5333z"/>
<path fill="url(#f)" d="M712.7803 287.4374v-91.7333h56.8v4.5333h-52v38.5333h45.7333v4.5334h-45.7333v39.6h52v4.5333z" letter-spacing="-2"/>
<path fill="url(#g)" d="M811.5345 287.4374v-87.2h-31.0667v-4.5333h66.9333v4.5333h-31.0666v87.2z" font-size="133.3333"/>
</g>
<path fill="url(#i)" d="M359.2174 367.2394l-7.28-17.76h-27.52l-7.28 17.76h-3.04l22.72-55.04h2.8l22.72 55.04zm-20.8-50.96h-.4l-12.48 30.48h25.28zm41.0388 50.96v-52.32h-18.64v-2.72h40.16v2.72h-18.64v52.32zm53.7625.64q-10.72 0-15.04-1.84-4.32-1.84-5.6-7.28-1.2-5.44-1.2-19.04 0-13.6 1.2-19.04 1.28-5.44 5.6-7.28 4.32-1.84 15.04-1.84 10.72 0 15.04 1.84 4.32 1.84 5.52 7.28 1.28 5.44 1.28 19.04 0 13.6-1.28 19.04-1.2 5.44-5.52 7.28-4.32 1.84-15.04 1.84zm0-2.72q9.68 0 13.12-1.2 3.52-1.28 4.64-6.16 1.2-4.96 1.2-18.08 0-13.12-1.2-18-1.12-4.96-4.64-6.16-3.44-1.28-13.12-1.28t-13.2 1.28q-3.44 1.2-4.64 6.16-1.12 4.88-1.12 18 0 13.12 1.12 18.08 1.2 4.88 4.64 6.16 3.52 1.2 13.2 1.2zm90.205 2.08v-49.44h-.32l-22.96 49.44h-2.56l-22.96-49.44h-.32v49.44h-2.8v-55.04h3.68l23.52 50.8h.4l23.6-50.8h3.6v55.04z" transform="translate(9.5 -183.1415)"/>
</svg>
<img src="statics/loki.svg" width="400" class="q-mb-md">
<div class="startup-icons q-mt-xl q-mb-lg">
<div ref="backend">
@ -106,7 +81,7 @@ export default {
this.$q.notify({
type: "warning",
timeout: 2000,
message: "Warning: ryod not found, using remote node"
message: "Warning: lokid not found, using remote node"
})
break;
case 6:
@ -149,50 +124,21 @@ export default {
&>div {
display: inline-block;
margin: 0 15px;
color: lightgrey;
color: #444;
g,path {
fill: lightgrey;
}
}
.solid {
color: var(--q-color-primary);
g,path {
fill: var(--q-color-primary);
fill: #444;
}
}
.pulse {
color:black;
color:#cecece;
opacity: 0.6;
animation: fade 2s infinite;
g,path {
fill:black;
}
}
}
.dark {
.startup-icons {
&>div {
display: inline-block;
margin: 0 15px;
color: #444;
g,path {
fill: #444;
}
}
.solid {
color: var(--q-color-primary);
g,path {
fill: var(--q-color-primary);
}
}
.pulse {
color: #cecece;
g,path {
fill: #cecece;
}
fill:#cecece;
}
}
}
@keyframes fade {
0%,100% { opacity: 0.3 }
50% { opacity: 0.6 }

View file

@ -2,32 +2,7 @@
<q-page>
<div class="init-screen-page text-center">
<div class="absolute-center">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="640" viewBox="0 0 859.4011 184.7379">
<defs>
<linearGradient id="b">
<stop offset="0" stop-color="#323232"/>
<stop offset="1" stop-color="#b4b4b4"/>
</linearGradient>
<linearGradient id="a" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#62c9f3"/>
<stop offset="1" stop-color="#3d58b0"/>
</linearGradient>
<linearGradient id="d" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="h" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
<linearGradient id="c" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="e" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="f" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="i" x1="341.1354" x2="341.1354" y1="186.9957" y2="128.2946" gradientTransform="translate(69.6895 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
</defs>
<path fill="url(#c)" d="M41.7535 41.89H75.105v33.0785H41.7535zm-4.2333-4.2334v41.5453h41.8181V37.6565zM58.4295 4.2333c29.9566 0 54.1957 24.2396 54.1957 54.1962 0 29.9566-24.239 54.1957-54.1957 54.1957-29.9566 0-54.1962-24.239-54.1962-54.1957 0-29.9566 24.2396-54.1962 54.1962-54.1962zm0-4.2333C26.185 0 0 26.185 0 58.4295s26.185 58.429 58.4295 58.429 58.429-26.1845 58.429-58.429S90.674 0 58.4295 0z"/>
<g fill="url(#d)" transform="translate(12 -183.1415)">
<path fill="url(#e)" d="M191.1345 287.4374l-15.7334-39.4667q-1.7333.2667-5.4666.2667h-34v39.2h-4.8v-91.7333h40.8q10.5333 0 15.7333 3.6 5.2 3.6 6.5333 9.0666 1.3334 5.3334 1.3334 13.6 0 6.6667-.9334 11.4667-.9333 4.6667-4.2666 8.4-3.3334 3.7333-10 5.3333l16.1333 40.2667zm-21.4667-43.7333q9.3334 0 13.8667-2.6667 4.5333-2.6667 5.8666-7.0667 1.3334-4.5333 1.3334-12 0-7.6-1.2-12-1.0667-4.4-5.2-7.0666-4-2.6667-12.6667-2.6667h-35.7333v43.4667zm63.6625 43.7333v-37.4667l-34.8-54.2666h5.6l31.2 49.2h.9333l31.3334-49.2h5.4666l-34.9333 54.2666v37.4667zm79.7979 1.0666q-17.8666 0-25.0666-3.0666-7.2-3.0667-9.3334-12.1333-2-9.0667-2-31.7334 0-22.6666 2-31.7333 2.1334-9.0666 9.3334-12.1333 7.2-3.0667 25.0666-3.0667 17.8667 0 25.0667 3.0667 7.2 3.0666 9.2 12.1333 2.1333 9.0667 2.1333 31.7333 0 22.6667-2.1333 31.7334-2 9.0666-9.2 12.1333-7.2 3.0667-25.0667 3.0667zm0-4.5333q16.1334 0 21.8667-2 5.8667-2.1333 7.7333-10.2666 2-8.2667 2-30.1334 0-21.8666-2-30-1.8666-8.2666-7.7333-10.2666-5.7333-2.1334-21.8667-2.1334-16.1333 0-22 2.1334-5.7333 2-7.7333 10.2666-1.8667 8.1334-1.8667 30 0 21.8667 1.8667 30.1334 2 8.1333 7.7333 10.2666 5.8667 2 22 2zm153.3563 3.4667l-27.0667-83.0667h-.5333l-27.0667 83.0667h-4.6666l-27.2-91.7333h5.2l24.1333 83.0666h.8l26.6667-83.0666h5.0666l26.6667 83.0666h.8l24.1333-83.0666h4.9333l-27.2 91.7333zm104.6416 0l-12.1333-29.6h-45.8667l-12.1333 29.6h-5.0667l37.8667-91.7333h4.6667l37.8666 91.7333zm-34.6666-84.9333h-.6667l-20.8 50.8h42.1333zm51.6125 84.9333v-91.7333h4.8v87.2h46.6666v4.5333zm62.3541 0v-91.7333h4.8v87.2h46.6667v4.5333z"/>
<path fill="url(#f)" d="M712.7803 287.4374v-91.7333h56.8v4.5333h-52v38.5333h45.7333v4.5334h-45.7333v39.6h52v4.5333z" letter-spacing="-2"/>
<path fill="url(#g)" d="M811.5345 287.4374v-87.2h-31.0667v-4.5333h66.9333v4.5333h-31.0666v87.2z" font-size="133.3333"/>
</g>
<path fill="url(#i)" d="M359.2174 367.2394l-7.28-17.76h-27.52l-7.28 17.76h-3.04l22.72-55.04h2.8l22.72 55.04zm-20.8-50.96h-.4l-12.48 30.48h25.28zm41.0388 50.96v-52.32h-18.64v-2.72h40.16v2.72h-18.64v52.32zm53.7625.64q-10.72 0-15.04-1.84-4.32-1.84-5.6-7.28-1.2-5.44-1.2-19.04 0-13.6 1.2-19.04 1.28-5.44 5.6-7.28 4.32-1.84 15.04-1.84 10.72 0 15.04 1.84 4.32 1.84 5.52 7.28 1.28 5.44 1.28 19.04 0 13.6-1.28 19.04-1.2 5.44-5.52 7.28-4.32 1.84-15.04 1.84zm0-2.72q9.68 0 13.12-1.2 3.52-1.28 4.64-6.16 1.2-4.96 1.2-18.08 0-13.12-1.2-18-1.12-4.96-4.64-6.16-3.44-1.28-13.12-1.28t-13.2 1.28q-3.44 1.2-4.64 6.16-1.12 4.88-1.12 18 0 13.12 1.12 18.08 1.2 4.88 4.64 6.16 3.52 1.2 13.2 1.2zm90.205 2.08v-49.44h-.32l-22.96 49.44h-2.56l-22.96-49.44h-.32v49.44h-2.8v-55.04h3.68l23.52 50.8h.4l23.6-50.8h3.6v55.04z" transform="translate(9.5 -183.1415)"/>
</svg>
<img src="statics/loki.svg" width="400" class="q-mb-md">
<div class="q-mt-xl q-mb-lg">
<q-spinner color="primary" :size="30" />

View file

@ -1,113 +1,52 @@
<template>
<q-page>
<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">
<q-step default title="Welcome" class="first-step">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 859.4011 116.8585" height="64">
<defs>
<linearGradient id="a" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#62c9f3"/>
<stop offset="1" stop-color="#3d58b0"/>
</linearGradient>
<linearGradient id="c" x2="0" y1="168.1192" y2="286.5673" gradientTransform="translate(82.8992 -169.0231)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="e" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="f" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="138.7479" x2="138.7479" y1="10.1293" y2="104.051" gradientTransform="translate(0 183.1415)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
</defs>
<path fill="url(#c)" d="M41.7535 41.89H75.105v33.0785H41.7535zm-4.2333-4.2334v41.5453h41.8181V37.6565zM58.4295 4.2333c29.9566 0 54.1957 24.2396 54.1957 54.1962 0 29.9566-24.239 54.1957-54.1957 54.1957-29.9566 0-54.1962-24.239-54.1962-54.1957 0-29.9566 24.2396-54.1962 54.1962-54.1962zm0-4.2333C26.185 0 0 26.185 0 58.4295s26.185 58.429 58.4295 58.429 58.429-26.1845 58.429-58.429S90.674 0 58.4295 0z"/>
<g transform="translate(12 -183.1415)">
<path fill="url(#e)" d="M191.1345 287.4374l-15.7334-39.4667q-1.7333.2667-5.4666.2667h-34v39.2h-4.8v-91.7333h40.8q10.5333 0 15.7333 3.6 5.2 3.6 6.5333 9.0666 1.3334 5.3334 1.3334 13.6 0 6.6667-.9334 11.4667-.9333 4.6667-4.2666 8.4-3.3334 3.7333-10 5.3333l16.1333 40.2667zm-21.4667-43.7333q9.3334 0 13.8667-2.6667 4.5333-2.6667 5.8666-7.0667 1.3334-4.5333 1.3334-12 0-7.6-1.2-12-1.0667-4.4-5.2-7.0666-4-2.6667-12.6667-2.6667h-35.7333v43.4667zm63.6625 43.7333v-37.4667l-34.8-54.2666h5.6l31.2 49.2h.9333l31.3334-49.2h5.4666l-34.9333 54.2666v37.4667zm79.7979 1.0666q-17.8666 0-25.0666-3.0666-7.2-3.0667-9.3334-12.1333-2-9.0667-2-31.7334 0-22.6666 2-31.7333 2.1334-9.0666 9.3334-12.1333 7.2-3.0667 25.0666-3.0667 17.8667 0 25.0667 3.0667 7.2 3.0666 9.2 12.1333 2.1333 9.0667 2.1333 31.7333 0 22.6667-2.1333 31.7334-2 9.0666-9.2 12.1333-7.2 3.0667-25.0667 3.0667zm0-4.5333q16.1334 0 21.8667-2 5.8667-2.1333 7.7333-10.2666 2-8.2667 2-30.1334 0-21.8666-2-30-1.8666-8.2666-7.7333-10.2666-5.7333-2.1334-21.8667-2.1334-16.1333 0-22 2.1334-5.7333 2-7.7333 10.2666-1.8667 8.1334-1.8667 30 0 21.8667 1.8667 30.1334 2 8.1333 7.7333 10.2666 5.8667 2 22 2zm153.3563 3.4667l-27.0667-83.0667h-.5333l-27.0667 83.0667h-4.6666l-27.2-91.7333h5.2l24.1333 83.0666h.8l26.6667-83.0666h5.0666l26.6667 83.0666h.8l24.1333-83.0666h4.9333l-27.2 91.7333zm104.6416 0l-12.1333-29.6h-45.8667l-12.1333 29.6h-5.0667l37.8667-91.7333h4.6667l37.8666 91.7333zm-34.6666-84.9333h-.6667l-20.8 50.8h42.1333zm51.6125 84.9333v-91.7333h4.8v87.2h46.6666v4.5333zm62.3541 0v-91.7333h4.8v87.2h46.6667v4.5333z"/>
<path fill="url(#f)" d="M712.7803 287.4374v-91.7333h56.8v4.5333h-52v38.5333h45.7333v4.5334h-45.7333v39.6h52v4.5333z"/>
<path fill="url(#g)" d="M811.5345 287.4374v-87.2h-31.0667v-4.5333h66.9333v4.5333h-31.0666v87.2z"/>
</g>
</svg>
<div class="welcome-container">
<img src="statics/loki.svg" height="100" class="q-mb-md">
<div>Version: ATOM v{{version}}-v{{daemonVersion}}</div>
<div>Version: ATOM v{{version}}-v{{daemonVersion}}</div>
<h6 class="q-mb-md" style="font-weight: 300">Select language:</h6>
<h6 class="q-mb-md" style="font-weight: 300">Select Appearance:</h6>
<q-btn-toggle
v-model="choose_theme"
toggle-color="primary"
size="md"
:options="[
{label: 'Light theme', value: 'light', icon: 'brightness_5'},
{label: 'Dark theme', value: 'dark', icon: 'brightness_2'},
]"
<q-btn
color="primary"
size="md"
icon="language"
label="English"
@click="clickNext()"
/>
<h6 class="q-mb-md" style="font-weight: 300">Select language:</h6>
<q-btn-toggle
v-model="choose_lang"
toggle-color="primary"
size="md"
:options="[
{label: 'English', value: 'EN', icon: 'language'},
]"
/>
<p class="q-mt-md">More languages coming soon</p>
<p class="q-mt-md">More languages coming soon</p>
</div>
</q-step>
<q-step title="Configure">
<SettingsGeneral ref="settingsGeneral"></SettingsGeneral>
<SettingsGeneral randomise_remote ref="settingsGeneral" />
</q-step>
<q-step title="Review">
<h2 class="q-mt-none q-mb-none text-weight-thin">You're almost set!</h2>
<h6 class="q-mb-md q-mt-md" style="font-weight: 300">Review settings:</h6>
<p>You are using a
<template v-if="pending_config.daemon.type == 'local'">
<code>local node</code>
</template>
<template v-if="pending_config.daemon.type == 'local_remote'">
<code>local + remote node</code>
</template>
<template v-if="pending_config.daemon.type == 'remote'">
<code>remote node</code>
</template>
<template v-if="pending_config.app.testnet">
<code>on testnet</code>
</template>
and will store data in
<code>{{ pending_config.app.data_dir }}</code>
</p>
<p>Press next to get started!</p>
</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>
@ -120,27 +59,19 @@ import SettingsGeneral from "components/settings_general"
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
pending_config: state => state.gateway.app.pending_config
pending_config: state => state.gateway.app.pending_config,
config_daemon (state) {
return this.pending_config.daemons[this.pending_config.app.net_type]
},
}),
data() {
return {
choose_theme: "light",
is_first_page: true,
choose_lang: "EN",
version: "",
daemonVersion: ""
}
},
watch: {
choose_theme: function (val) {
this.$store.commit("gateway/set_app_data", {
config: {
appearance: {
theme: val
}
}
});
}
},
mounted () {
this.version = version
@ -154,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);
@ -174,6 +108,21 @@ export default {
<style lang="scss">
.welcome {
.welcome-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.first-step .q-stepper-step-inner {
min-height: 250px;
height: calc(100vh - 102px);
}
}
.language-item {
padding: 10px 30px 10px 20px;

View file

@ -1,46 +1,43 @@
<template>
<q-page>
<div class="q-mx-md">
<q-field class="q-mt-none">
<q-page class="create-wallet">
<div class="fields q-mx-md q-mt-md">
<LokiField label="Wallet name" :error="$v.wallet.name.$error">
<q-input
v-model="wallet.name"
float-label="Wallet name"
@blur="$v.wallet.name.$touch"
:error="$v.wallet.name.$error"
:dark="theme=='dark'"
/>
</q-field>
placeholder="A name for your wallet"
hide-underline
/>
</LokiField>
<q-field>
<LokiField label="Seed Language">
<q-select
v-model="wallet.language"
float-label="Seed language"
:options="languageOptions"
:dark="theme=='dark'"
/>
</q-field>
hide-underline
/>
</LokiField>
<q-field>
<div class="row gutter-md">
<div><q-radio v-model="wallet.type" val="long" label="Long address" /></div>
<div><q-radio v-model="wallet.type" val="kurz" label="Short (kurz) address" /></div>
</div>
</q-field>
<LokiField label="Password" optional>
<q-input
v-model="wallet.password"
type="password"
:dark="theme=='dark'"
placeholder="An optional password for the wallet"
hide-underline
/>
</LokiField>
<p v-if="wallet.type == 'long'">
Create both public/private view & spend keys. Allows creation of view-only wallets.
</p>
<p v-if="wallet.type == 'kurz'">
Create shorter style address with only private view & spend keys. Does NOT support view-only wallets.
</p>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" :dark="theme=='dark'" />
</q-field>
<LokiField label="Confirm Password">
<q-input
v-model="wallet.password_confirm"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-field>
<q-btn color="primary" @click="create" label="Create wallet" />
@ -53,13 +50,13 @@
<script>
import { required } from "vuelidate/lib/validators"
import { mapState } from "vuex"
import LokiField from "components/loki_field"
export default {
data () {
return {
wallet: {
name: "",
language: "English",
type: "long",
password: "",
password_confirm: ""
},
@ -136,18 +133,48 @@ 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" });
}
},
components: {
LokiField
}
}
</script>
<style>
<style lang="scss">
.create-wallet {
.fields {
> * {
margin-bottom: 16px;
}
}
}
</style>

View file

@ -1,32 +1,10 @@
<template>
<q-page padding>
<AddressHeader :address="info.address" :title="info.name" />
<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>
@ -75,6 +93,9 @@ export default {
computed: mapState({
info: state => state.gateway.wallet.info,
secret: state => state.gateway.wallet.secret,
walletName (state) {
return `Wallet: ${this.info.name}`
}
}),
methods: {
open() {
@ -138,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

@ -137,7 +137,18 @@ export default {
validations: {
wallet: {
name: { required },
address: { required, address },
address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
},
viewkey: { required, privkey },
refresh_start_height: { numeric }
}

View file

@ -1,36 +1,30 @@
<template>
<q-page>
<div class="q-mx-md">
<div class="q-mx-md import-wallet">
<q-field class="q-mt-none">
<LokiField label="New wallet name" :error="$v.wallet.name.$error">
<q-input
v-model="wallet.name"
float-label="New wallet name"
placeholder="A name for your wallet"
@blur="$v.wallet.name.$touch"
:error="$v.wallet.name.$error"
:dark="theme=='dark'"
hide-underline
/>
</q-field>
</LokiField>
<q-field>
<div class="row gutter-sm">
<div class="col">
<q-input v-model="wallet.path" stack-label="Wallet file" disable :dark="theme=='dark'" />
<input type="file" id="walletPath" v-on:change="setWalletPath" ref="fileInput" hidden />
</div>
<div class="col-auto">
<q-btn v-on:click="selectFile" class="float-right" :text-color="theme=='dark'?'white':'dark'">Select wallet file</q-btn>
</div>
</div>
</q-field>
<LokiField label="Wallet file" disable-hover>
<q-input v-model="wallet.path" placeholder="Please select a file" disable :dark="theme=='dark'" hide-underline/>
<input type="file" id="walletPath" v-on:change="setWalletPath" ref="fileInput" hidden />
<q-btn color="secondary" v-on:click="selectFile" :text-color="theme=='dark'?'white':'dark'">Select wallet file</q-btn>
</LokiField>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" :dark="theme=='dark'" />
</q-field>
<LokiField label="Password">
<q-input v-model="wallet.password" placeholder="An optional password for the wallet" type="password" :dark="theme=='dark'" hide-underline />
</LokiField>
<q-field>
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" :dark="theme=='dark'" />
</q-field>
<LokiField label="Confirm Password">
<q-input v-model="wallet.password_confirm" type="password" :dark="theme=='dark'" hide-underline />
</LokiField>
<q-field>
<q-btn color="primary" @click="import_wallet" label="Import wallet" />
@ -44,6 +38,7 @@
<script>
import { required } from "vuelidate/lib/validators"
import { mapState } from "vuex"
import LokiField from "components/loki_field"
export default {
data () {
return {
@ -124,9 +119,24 @@ export default {
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
},
components: {
LokiField
}
}
</script>
<style>
<style lang="scss">
.import-wallet {
.q-if-disabled {
cursor: default !important;
.q-input-target {
cursor: default !important;
}
}
.loki-field {
margin-top: 16px;
}
}
</style>

View file

@ -1,27 +1,31 @@
<template>
<q-page>
<q-list link no-border :dark="theme=='dark'">
<q-list class="wallet-list" link no-border :dark="theme=='dark'">
<template v-if="wallets.list.length">
<q-list-header>Open wallet</q-list-header>
<q-item v-for="(wallet, index) in wallets.list" @click.native="openWallet(wallet)">
<div class="header row justify-between items-center">
<div class="header-title">Your wallets</div>
<q-btn class="add" icon="add" size="md" color="primary" v-if="wallets.list.length">
<q-popover class="header-popover">
<q-list separator link>
<q-item v-for="action in actions" @click.native="action.handler" :key="action.name">
<q-item-main :label="action.name" />
</q-item>
</q-list>
</q-popover>
</q-btn>
</div>
<div class="hr-separator" />
<q-item v-for="wallet in wallets.list" @click.native="openWallet(wallet)" :key="`${wallet.address}-${wallet.name}`">
<q-item-side>
<Identicon :address="wallet.address" :ref="`${index}-identicon`" />
<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>
</div>
</q-item-side>
<q-item-main>
<q-item-tile label>{{ wallet.name }}</q-item-tile>
<q-item-tile class="monospace ellipsis" sublabel>{{ wallet.address }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress(wallet.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
@ -34,32 +38,15 @@
@click.native="copyAddress(wallet.address, $event)">
<q-item-main label="Copy address" />
</q-item>
<q-item v-close-overlay
@click.native="$refs[`${index}-identicon`][0].saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
<q-item-separator />
</template>
<q-item @click.native="createNewWallet()">
<q-item-main label="Create new wallet" />
</q-item>
<q-item @click.native="restoreWallet()">
<q-item-main label="Restore wallet from seed" />
</q-item>
<q-item @click.native="restoreViewWallet()">
<q-item-main label="Restore view-only wallet" />
</q-item>
<q-item @click.native="importWallet()">
<q-item-main label="Import wallet from file" />
</q-item>
<template v-if="wallets.legacy.length">
<q-item @click.native="importLegacyWallet()">
<q-item-main label="Import wallet from legacy gui" />
<template v-else>
<q-item v-for="action in actions" @click.native="action.handler" :key="action.name">
<q-item-main :label="action.name" />
</q-item>
</template>
</q-list>
@ -71,12 +58,45 @@
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,
wallets: state => state.gateway.wallets,
status: state => state.gateway.wallet.status
status: state => state.gateway.wallet.status,
actions (status) {
// TODO: Add this in once LOKI has the functionality
// <q-item @click.native="restoreViewWallet()">
// <q-item-main label="Restore view-only wallet" />
// </q-item>
const actions = [
{
name: "Create new wallet",
handler: this.createNewWallet,
},
{
name: "Restore wallet from seed",
handler: this.restoreWallet,
},
{
name: "Import wallet from file",
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) {
@ -122,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" });
},
@ -175,5 +198,32 @@ export default {
}
</script>
<style>
<style lang="scss">
.header-popover.q-popover {
max-width: 250px !important;
}
.wallet-list {
.header {
margin: 0 16px;
margin-bottom: 8px;
min-height: 36px;
.header-title {
font-size: 14px;
font-weight: 500;
}
.add {
width: 38px;
padding: 0;
}
}
.q-item {
margin: 10px 16px;
margin-bottom: 0px;
padding: 14px;
border-radius: 3px;
}
}
</style>

View file

@ -1,74 +1,83 @@
<template>
<q-page>
<div class="q-mx-md">
<q-field class="q-mt-none">
<LokiField class="q-mt-md" label="Wallet name" :error="$v.wallet.name.$error">
<q-input
v-model="wallet.name"
float-label="Wallet name"
placeholder="A name for your wallet"
@blur="$v.wallet.name.$touch"
:error="$v.wallet.name.$error"
:dark="theme=='dark'"
hide-underline
/>
</q-field>
</LokiField>
<q-field>
<LokiField class="q-mt-md" label="Mnemonic seed" :error="$v.wallet.seed.$error">
<q-input
v-model="wallet.seed"
float-label="Mnemonic seed"
placeholder="25 (or 24) word mnemonic seed"
type="textarea"
@blur="$v.wallet.seed.$touch"
:error="$v.wallet.seed.$error"
:dark="theme=='dark'"
hide-underline
/>
</q-field>
</LokiField>
<q-field>
<div class="row items-center gutter-sm">
<div class="col">
<template v-if="wallet.refresh_type=='date'">
<q-datetime v-model="wallet.refresh_start_date" type="date"
float-label="Restore from date"
modal :min="1492486495000" :max="Date.now()"
:dark="theme=='dark'"
/>
</template>
<template v-else-if="wallet.refresh_type=='height'">
<q-input v-model="wallet.refresh_start_height" type="number"
min="0" float-label="Restore from block height"
@blur="$v.wallet.refresh_start_height.$touch"
:error="$v.wallet.refresh_start_height.$error"
:dark="theme=='dark'"
/>
</template>
</div>
<div class="col-auto">
<template v-if="wallet.refresh_type=='date'">
<q-btn @click="wallet.refresh_type='height'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="clear_all" />
<div style="font-size:10px">Switch to<br/>height select</div>
</div>
</q-btn>
</template>
<template v-else-if="wallet.refresh_type=='height'">
<q-btn @click="wallet.refresh_type='date'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="today" />
<div style="font-size:10px">Switch to<br/>date select</div>
</div>
</q-btn>
</template>
</div>
<div class="row items-end q-mt-md">
<div class="col">
<LokiField v-if="wallet.refresh_type=='date'" label="Restore from date">
<q-datetime v-model="wallet.refresh_start_date" type="date"
modal :min="1492486495000" :max="Date.now()"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<LokiField v-else-if="wallet.refresh_type=='height'" label="Restore from block height" :error="$v.wallet.refresh_start_height.$error">
<q-input v-model="wallet.refresh_start_height" type="number"
min="0"
@blur="$v.wallet.refresh_start_height.$touch"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
</div>
</q-field>
<div class="col-auto q-ml-sm">
<template v-if="wallet.refresh_type=='date'">
<q-btn @click="wallet.refresh_type='height'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="clear_all" />
<div style="font-size:10px">Switch to<br/>height select</div>
</div>
</q-btn>
</template>
<template v-else-if="wallet.refresh_type=='height'">
<q-btn @click="wallet.refresh_type='date'" class="float-right" :text-color="theme=='dark'?'white':'dark'" flat>
<div style="width: 80px;" class="text-center">
<q-icon class="block" name="today" />
<div style="font-size:10px">Switch to<br/>date select</div>
</div>
</q-btn>
</template>
</div>
</div>
<q-field>
<q-input v-model="wallet.password" type="password" float-label="Password" :dark="theme=='dark'" />
</q-field>
<LokiField class="q-mt-md" label="Password">
<q-input
v-model="wallet.password"
placeholder="An optional password for the wallet"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-field>
<q-input v-model="wallet.password_confirm" type="password" float-label="Confirm Password" :dark="theme=='dark'" />
</q-field>
<LokiField class="q-mt-md" label="Confirm Password">
<q-input
v-model="wallet.password_confirm"
type="password"
:dark="theme=='dark'"
hide-underline
/>
</LokiField>
<q-field>
<q-btn color="primary" @click="restore_wallet" label="Restore wallet" />
@ -81,6 +90,7 @@
<script>
import { required, numeric } from "vuelidate/lib/validators"
import { mapState } from "vuex"
import LokiField from "components/loki_field"
export default {
data () {
return {
@ -89,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: ""
},
@ -151,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",
@ -187,6 +201,9 @@ export default {
cancel() {
this.$router.replace({ path: "/wallet-select" });
}
},
components: {
LokiField
}
}
</script>

View file

@ -1,27 +1,16 @@
<template>
<q-page>
<q-page class="address-book">
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="person" size="24px" /> Address book
</div>
<div class="col-4">
</div>
Address book
</div>
<template v-if="address_book_starred.length || address_book.length">
<q-list link no-border :dark="theme=='dark'">
<q-item v-for="(entry, index) in address_book_starred" @click.native="details(entry)">
<q-item-side>
<Identicon :address="entry.address" :ref="`${index}-starredIdenticon`" />
</q-item-side>
<template v-if="address_book_combined.length">
<q-list link no-border :dark="theme=='dark'" class="loki-list">
<q-item class="loki-list-item" v-for="(entry, index) in address_book_combined" @click.native="details(entry)" :key="entry.address">
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ entry.address }}</q-item-tile>
<q-item-tile sublabel>{{ entry.name }}</q-item-tile>
<q-item-tile class="ellipsis" label>{{ entry.address }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
@ -33,7 +22,7 @@
Send coins
</q-tooltip>
</q-btn>
<q-icon size="24px" name="star" />
<q-icon size="24px" :name="entry.starred ? 'star' : 'star_border'" />
</q-item-side>
<q-context-menu>
@ -52,57 +41,6 @@
@click.native="copyAddress(entry, $event)">
<q-item-main label="Copy address" />
</q-item>
<q-item v-close-overlay
@click.native="$refs[`${index}-starredIdenticon`][0].saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
<q-item v-for="(entry, index) in address_book" @click.native="details(entry)">
<q-item-side>
<Identicon :address="entry.address" :ref="`${index}-normalIdenticon`" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ entry.address }}</q-item-tile>
<q-item-tile sublabel>{{ entry.name }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px; margin-right: 10px;"
size="sm" icon="call_made"
:disabled="view_only"
@click="sendToAddress(entry, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Send coins
</q-tooltip>
</q-btn>
<q-icon size="24px" name="star_border" />
</q-item-side>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@click.native="details(entry)">
<q-item-main label="Show details" />
</q-item>
<q-item v-close-overlay
@click.native="sendToAddress(entry, $event)">
<q-item-main label="Send to this address" />
</q-item>
<q-item v-close-overlay
@click.native="copyAddress(entry, $event)">
<q-item-main label="Copy address" />
</q-item>
<q-item v-close-overlay
@click.native="$refs[`${index}-normalIdenticon`][0].saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
</q-context-menu>
@ -140,6 +78,13 @@ export default {
address_book_starred: state => state.gateway.wallet.address_list.address_book_starred,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
address_book_combined (state) {
const starred = this.address_book_starred.map(a => ({ ...a, starred: true }));
return [
...starred,
...this.address_book
]
}
}),
methods: {
@ -209,5 +154,15 @@ export default {
}
</script>
<style>
<style lang="scss">
.address-book {
.q-item-label {
font-weight: 400;
}
.q-item-sublabel, .q-list-header {
font-size: 14px;
margin-top: 0px;
}
}
</style>

View file

@ -1,39 +1,55 @@
<template>
<q-page>
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="call_received" size="24px" /> Receive Ryo
</div>
<div class="col-4">
</div>
</div>
<q-list link no-border :dark="theme=='dark'">
<q-page class="receive">
<q-list link no-border :dark="theme=='dark'" class="loki-list">
<q-list-header>My primary address</q-list-header>
<q-item v-for="(address, index) in address_list.primary" @click.native="details(address)">
<q-item-side>
<Identicon :address="address.address" ref="primaryIdenticon" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Primary address</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
<q-list class="loki-list-item primary-address" no-border v-for="address in address_list.primary" :key="address.address" @click.native="details(address)">
<q-item>
<q-item-main>
<q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<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="bottom right" self="top right" :offset="[0, 5]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
</q-item>
<q-item-separator />
<q-item class="info">
<q-item-main class="flex justify-between">
<div class="column">
<span>Balance</span>
<span class="value">{{address.balance | currency}}</span>
</div>
<div class="column">
<span>Unlocked balance</span>
<span class="value">{{ address.unlocked_balance | currency }}</span>
</div>
<div class="column">
<span>Unspent outputs</span>
<span class="value">{{ address.num_unspent_outputs | toString }}</span>
</div>
</q-item-main>
</q-item>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
<q-item v-close-overlay
@ -45,36 +61,58 @@
@click.native="copyAddress(address.address, $event)">
<q-item-main label="Copy address" />
</q-item>
<q-item v-close-overlay
@click.native="$refs.primaryIdenticon[0].saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
<template v-if="address_list.used.length">
<q-list-header>My used addresses</q-list-header>
<q-item v-for="(address, index) in address_list.used" @click.native="details(address)">
<q-item-side>
<Identicon :address="address.address" :ref="`${index}-usedIdenticon`" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
<q-list class="loki-list-item" no-border v-for="address in address_list.used" @click.native="details(address)" :key="address.address">
<q-item>
<q-item-main>
<q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<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="bottom right" self="top right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
</q-item>
<q-item-separator />
<q-item class="info">
<q-item-main class="flex justify-between">
<div class="column">
<span>Balance</span>
<span class="value">{{ address.balance | currency }}</span>
</div>
<div class="column">
<span>Unlocked balance</span>
<span class="value">{{ address.unlocked_balance | currency }}</span>
</div>
<div class="column">
<span>Unspent outputs</span>
<span class="value">{{ address.num_unspent_outputs | toString }}</span>
</div>
</q-item-main>
</q-item>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
@ -87,38 +125,43 @@
@click.native="copyAddress(address.address, $event)">
<q-item-main label="Copy address" />
</q-item>
<q-item v-close-overlay
@click.native="$refs[`${index}-usedIdenticon`][0].saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
</template>
<template v-if="address_list.unused.length">
<q-list-header>My unused addresses</q-list-header>
<q-item v-for="(address, index) in address_list.unused" @click.native="details(address)">
<q-item-side>
<Identicon :address="address.address" :ref="`${index}-unusedIdenticon`" />
</q-item-side>
<q-item-main>
<q-item-tile class="monospace ellipsis" label>{{ address.address }}</q-item-tile>
<q-item-tile sublabel>Sub-address (Index {{ address.address_index }})</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress(address.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
<q-list class="loki-list-item" no-border v-for="address in address_list.unused" @click.native="details(address)" :key="address.address">
<q-item>
<q-item-main>
<q-item-tile class="ellipsis" label>{{ address.address }}</q-item-tile>
<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="bottom right" self="top right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
</q-item>
<q-context-menu>
<q-list link separator style="min-width: 150px; max-height: 300px;">
@ -131,40 +174,101 @@
@click.native="copyAddress(address.address, $event)">
<q-item-main label="Copy address" />
</q-item>
<q-item v-close-overlay
@click.native="$refs[`${index}-unusedIdenticon`][0].saveIdenticon()">
<q-item-main label="Save identicon to file" />
</q-item>
</q-list>
</q-context-menu>
</q-item>
</q-list>
</template>
</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>
<style>
</style>
<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,
address_list: state => state.gateway.wallet.address_list
}),
filters: {
toString: function (value) {
if (typeof value !== "number") return "N/A";
return String(value);
},
currency: function (value) {
if (typeof value !== "number") return "N/A";
const amount = value / 1e9
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++) {
@ -184,6 +288,45 @@ export default {
components: {
Identicon,
AddressDetails,
QrcodeVue
}
}
</script>
<style lang="scss">
.receive {
.q-item-label {
font-weight: 400;
}
.q-item-sublabel, .q-list-header {
font-size: 13px;
}
.loki-list-item {
cursor: pointer;
.q-item {
padding-top: 4px;
padding-bottom: 4px;
}
.q-item-side {
display: flex;
justify-content: center;
align-items: center;
}
.info {
span {
font-size: 14px;
}
.value {
font-size: 16px;
font-weight: bold;
}
}
}
}
</style>

View file

@ -1,118 +1,109 @@
<template>
<q-page>
<q-page class="send">
<template v-if="view_only">
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="call_made" size="24px" /> Send Ryo
</div>
<div class="col-4">
</div>
</div>
<div class="q-pa-md">
View-only mode. Please load full wallet in order to send coins.
</div>
</template>
<template v-else>
<div class="row q-pt-sm q-mx-md q-mb-none items-center non-selectable" style="height: 44px;">
<div class="col-8">
<q-icon name="call_made" size="24px" /> Send Ryo
</div>
<div class="col-4">
</div>
</div>
<div class="q-pa-md">
<div class="row items-end gutter-md">
<div class="col">
<q-field class="q-ma-none">
<q-input v-model="newTx.amount" float-label="Amount" :dark="theme=='dark'"
type="number" min="0" :max="unlocked_balance / 1e9" />
</q-field>
</div>
<div>
<q-btn @click="newTx.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">All coins</q-btn>
</div>
</div>
<q-item class="q-pa-none">
<q-item-side>
<Identicon :address="newTx.address" menu />
</q-item-side>
<q-item-main>
<q-field>
<q-input v-model="newTx.address" float-label="Address"
:dark="theme=='dark'"
@blur="$v.newTx.address.$touch"
:error="$v.newTx.address.$error"
/>
</q-field>
</q-item-main>
</q-item>
<q-field style="margin-top:0">
<q-input v-model="newTx.payment_id" float-label="Payment ID (optional)"
:dark="theme=='dark'"
@blur="$v.newTx.payment_id.$touch"
:error="$v.newTx.payment_id.$error"
/>
</q-field>
<div class="row gutter-md">
<!-- Amount -->
<div class="col-6">
<q-field>
<q-select :dark="theme=='dark'"
v-model="newTx.mixin"
float-label="Mixin"
:options="mixinOptions"
/>
</q-field>
</div>
<div class="col-6">
<q-field>
<q-select :dark="theme=='dark'"
v-model="newTx.priority"
float-label="Priority"
:options="priorityOptions"
/>
</q-field>
<LokiField label="Amount" :error="$v.newTx.amount.$error">
<q-input v-model="newTx.amount"
:dark="theme=='dark'"
type="number"
min="0"
:max="unlocked_balance / 1e9"
placeholder="0"
@blur="$v.newTx.amount.$touch"
hide-underline
/>
<q-btn color="secondary" @click="newTx.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">All</q-btn>
</LokiField>
</div>
<!-- Priority -->
<div class="col-6">
<LokiField label="Priority">
<q-select :dark="theme=='dark'"
v-model="newTx.priority"
:options="priorityOptions"
hide-underline
/>
</LokiField>
</div>
</div>
<!-- Address -->
<div class="col q-mt-sm">
<LokiField label="Address" :error="$v.newTx.address.$error">
<q-input v-model="newTx.address"
:dark="theme=='dark'"
@blur="$v.newTx.address.$touch"
:placeholder="address_placeholder"
hide-underline
/>
<q-btn color="secondary" :text-color="theme=='dark'?'white':'dark'" to="addressbook">Contacts</q-btn>
</LokiField>
</div>
<!-- Payment ID -->
<div class="col q-mt-sm">
<LokiField label="Payment id" :error="$v.newTx.payment_id.$error" optional>
<q-input v-model="newTx.payment_id"
:dark="theme=='dark'"
@blur="$v.newTx.payment_id.$touch"
placeholder="16 or 64 hexadecimal characters"
hide-underline
/>
</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'" />
</q-field>
<div v-if="newTx.address_book.save">
<q-field>
<q-input v-model="newTx.address_book.name" float-label="Name" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-input v-model="newTx.address_book.description" type="textarea" rows="2" float-label="Notes" :dark="theme=='dark'" />
</q-field>
<LokiField label="Name" optional>
<q-input v-model="newTx.address_book.name"
:dark="theme=='dark'"
placeholder="Name that belongs to this address"
hide-underline
/>
</LokiField>
<LokiField class="q-mt-sm" label="Notes" optional>
<q-input v-model="newTx.address_book.description"
type="textarea"
rows="2"
:dark="theme=='dark'"
placeholder="Additional notes"
hide-underline
/>
</LokiField>
</div>
<q-field class="q-pt-sm">
<q-btn
class="send-btn"
:disable="!is_able_to_send"
color="primary" @click="send()" label="Send" />
</q-field>
@ -131,9 +122,12 @@
<script>
import { mapState } from "vuex"
import { required, decimal } from "vuelidate/lib/validators"
import { payment_id, address } from "src/validators/common"
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,
@ -145,6 +139,11 @@ export default {
},
is_able_to_send (state) {
return this.$store.getters["gateway/isAbleToSend"]
},
address_placeholder (state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
}
}),
data () {
@ -154,7 +153,6 @@ export default {
amount: 0,
address: "",
payment_id: "",
mixin: 12,
priority: 0,
address_book: {
save: false,
@ -162,17 +160,12 @@ export default {
description: ""
}
},
mixinOptions: [
{label: "12 mixins (default)", value: 12},
{label: "48 mixins (top secret)", value: 48},
{label: "96 mixins (paranoid)", value: 60},
],
priorityOptions: [
{label: "Normal (x1 fee)", value: 0},
{label: "High (x2 fee)", value: 1},
{label: "High (x4 fee)", value: 2},
{label: "High (x20 fee)", value: 3},
{label: "Highest (x144 fee)", value: 4},
{label: "Automatic", value: 0},
{label: "Slow", value: 1},
{label: "Normal", value: 2},
{label: "Fast", value: 3},
{label: "Fastest", value: 4},
],
}
},
@ -180,9 +173,21 @@ export default {
newTx: {
amount: {
required,
decimal
decimal,
greater_than_zero
},
address: {
required,
isAddress(value) {
if (value === '') return true
return new Promise(resolve => {
address(value, this.$gateway)
.then(() => resolve(true))
.catch(e => resolve(false))
});
}
},
address: { required, address },
payment_id: { payment_id }
}
},
@ -202,13 +207,13 @@ export default {
amount: 0,
address: "",
payment_id: "",
mixin: 12,
priority: 0,
address_book: {
save: false,
name: "",
description: ""
}
},
note: ""
}
break;
case -1:
@ -241,7 +246,6 @@ export default {
},
send: function () {
this.$v.newTx.$touch()
if(this.newTx.amount < 0) {
@ -293,38 +297,36 @@ 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,
message: "Sending transaction",
sending: true
})
let newTx = objectAssignDeep.noMutate(this.newTx, {password})
const newTx = objectAssignDeep.noMutate(this.newTx, {password})
this.$gateway.send("wallet", "transfer", newTx)
}).catch(() => {
})
}
},
mixins: [WalletPassword],
components: {
Identicon
Identicon,
LokiField
}
}
</script>
<style>
<style lang="scss">
.send {
.send-btn {
width: 200px;
}
}
</style>

View file

@ -0,0 +1,40 @@
<template>
<q-page class="service-node-page">
<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>
import ServiceNodeStaking from "components/service_node_staking"
import ServiceNodeRegistration from "components/service_node_registration"
import ServiceNodeUnlock from "components/service_node_unlock"
export default {
data () {
return {
screen: "staking",
}
},
components: {
ServiceNodeStaking,
ServiceNodeRegistration,
ServiceNodeUnlock
}
}
</script>
<style lang="scss">
</style>

View file

@ -1,26 +1,27 @@
<template>
<q-page>
<div class="row q-pt-sm q-mx-md q-mb-sm items-center non-selectable">
<div class="row q-pt-sm q-mx-md q-mb-sm items-end non-selectable">
<div class="col-5">
<q-icon name="history" size="24px" /> Transaction history
Transactions
</div>
<div class="col-5 q-px-sm">
<LokiField class="col-5 q-px-sm" label="Filter by txid">
<q-input v-model="tx_txid"
stack-label="Filter by txid"
:dark="theme=='dark'"
placeholder="Enter an ID"
hide-underline
/>
</div>
</LokiField>
<div class="col-2">
<LokiField class="col-2" label="Filter by transaction type">
<q-select :dark="theme=='dark'"
v-model="tx_type"
float-label="Filter by transaction type"
:options="tx_type_options"
hide-underline
/>
</div>
</LokiField>
</div>
<TxList :type="tx_type" :txid="tx_txid" />
@ -30,6 +31,7 @@
<script>
import { mapState } from "vuex"
import TxList from "components/tx_list"
import LokiField from "components/loki_field"
export default {
data () {
return {
@ -39,8 +41,11 @@ export default {
{label: "All", value: "all"},
{label: "Incoming", value: "in"},
{label: "Outgoing", value: "out"},
{label: "Pending incoming", value: "pool"},
{label: "Pending outgoing", value: "pending"},
{label: "Pending", value: "all_pending"},
{label: "Miner", value: "miner"},
{label: "Service Node", value: "snode"},
{label: "Governance", value: "gov"},
{label: "Stake", value: "stake"},
{label: "Failed", value: "failed"},
]
@ -52,7 +57,8 @@ export default {
}),
components: {
TxList
TxList,
LokiField
}
}

View file

@ -1,15 +1,17 @@
/**
This is an unused class in LOKI
*/
<template>
<q-page padding>
<AddressHeader :address="info.address" :title="info.name" />
<div class="row">
<div class="infoBoxBalance">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Balance</span></div>
<div class="value"><span><FormatRyo :amount="info.balance" /></span></div>
<div class="value"><span><FormatLoki :amount="info.balance" /></span></div>
</div>
</div>
</div>
@ -18,7 +20,7 @@
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Unlocked balance</span></div>
<div class="value"><span><FormatRyo :amount="info.unlocked_balance" /></span></div>
<div class="value"><span><FormatLoki :amount="info.unlocked_balance" /></span></div>
</div>
</div>
</div>
@ -262,7 +264,7 @@
const { clipboard } = require("electron")
import { mapState } from "vuex"
import AddressHeader from "components/address_header"
import FormatRyo from "components/format_ryo"
import FormatLoki from "components/format_loki"
import TxList from "components/tx_list"
export default {
computed: mapState({
@ -299,7 +301,7 @@ export default {
}
}
},
mounted() {
mounted () {
const path = require("path")
this.modals.key_image.export_path = path.join(this.data_dir, "gui")
this.modals.key_image.import_path = path.join(this.data_dir, "gui", "key_image_export")
@ -545,7 +547,7 @@ export default {
}
},
components: {
FormatRyo,
FormatLoki,
AddressHeader,
TxList
},

View file

@ -1,4 +1,4 @@
import VueTimeago from 'vue-timeago'
import VueTimeago from "vue-timeago"
export default ({
app,
router,
@ -6,7 +6,7 @@ export default ({
Vue
}) => {
Vue.use(VueTimeago, {
name: 'Timeago',
locale: 'en'
name: "Timeago",
locale: "en"
})
}

View file

@ -1,4 +1,4 @@
import Vuelidate from 'vuelidate'
import Vuelidate from "vuelidate"
export default ({ Vue }) => {
Vue.use(Vuelidate)

View file

@ -2,115 +2,121 @@ export default [
{
path: "/",
component: () =>
import ("layouts/init/loading"),
import("layouts/init/loading"),
children: [
{
path: "",
component: () =>
import ("pages/init/index")
import("pages/init/index")
},
{
path: "/quit",
component: () =>
import ("pages/init/quit")
},
import("pages/init/quit")
}
]
},
{
path: "/welcome",
component: () =>
import ("layouts/init/welcome"),
import("layouts/init/welcome"),
children: [{
path: "",
component: () =>
import ("pages/init/welcome")
import("pages/init/welcome")
}]
},
{
path: "/wallet-select",
component: () =>
import ("layouts/wallet-select/main"),
import("layouts/wallet-select/main"),
children: [
{
path: "",
name: "wallet-select",
component: () =>
import ("pages/wallet-select/index")
import("pages/wallet-select/index")
},
{
path: "create",
name: "wallet-create",
component: () =>
import ("pages/wallet-select/create")
import("pages/wallet-select/create")
},
{
path: "restore",
name: "wallet-restore",
component: () =>
import ("pages/wallet-select/restore")
import("pages/wallet-select/restore")
},
{
path: "import-view-only",
name: "wallet-import-view-only",
component: () =>
import ("pages/wallet-select/import-view-only")
import("pages/wallet-select/import-view-only")
},
{
path: "import",
name: "wallet-import",
component: () =>
import ("pages/wallet-select/import")
import("pages/wallet-select/import")
},
{
path: "import-legacy",
name: "wallet-import-legacy",
component: () =>
import ("pages/wallet-select/import-legacy")
import("pages/wallet-select/import-legacy")
},
{
path: "created",
name: "wallet-created",
component: () =>
import ("pages/wallet-select/created")
import("pages/wallet-select/created")
},
{
path: "import-old-gui",
name: "wallet-import-old-gui",
component: () =>
import("pages/wallet-select/import-old-gui")
}
]
},
{
path: "/wallet",
component: () =>
import ("layouts/wallet/main"),
import("layouts/wallet/main"),
children: [
{
path: "",
component: () =>
import ("pages/wallet/wallet")
import("pages/wallet/txhistory")
},
{
path: "receive",
component: () =>
import ("pages/wallet/receive")
import("pages/wallet/receive")
},
{
path: "send",
component: () =>
import ("pages/wallet/send")
import("pages/wallet/send")
},
{
path: "addressbook",
component: () =>
import ("pages/wallet/addressbook")
import("pages/wallet/addressbook")
},
{
path: "txhistory",
path: "servicenode",
component: () =>
import ("pages/wallet/txhistory")
},
import("pages/wallet/service-node")
}
]
},
{ // Always leave this as last one
path: "*",
component: () =>
import ("pages/404")
import("pages/404")
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 51 KiB

66
src/statics/loki.svg Normal file
View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 368" style="enable-background:new 0 0 1000 368;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:url(#SVGID_1_);}
.st2{fill:#333333;}
.st3{fill:url(#SVGID_2_);}
.st4{fill:url(#SVGID_3_);}
.st5{fill:#00263A;}
.st6{fill:url(#SVGID_4_);}
.st7{fill:url(#SVGID_5_);}
.st8{fill:url(#SVGID_6_);}
.st9{fill:url(#SVGID_7_);}
.st10{fill:url(#SVGID_8_);}
.st11{fill:url(#SVGID_9_);}
.st12{fill:url(#SVGID_10_);}
.st13{fill:url(#SVGID_11_);}
.st14{fill:url(#SVGID_12_);}
.st15{fill:url(#SVGID_13_);}
.st16{fill:url(#SVGID_14_);}
.st17{fill:url(#SVGID_15_);}
.st18{fill:url(#SVGID_16_);}
.st19{fill:url(#SVGID_17_);}
.st20{opacity:6.000000e-02;}
.st21{opacity:4.000000e-02;fill:#FFFFFF;}
.st22{opacity:7.000000e-02;fill:#FFFFFF;}
.st23{fill:#008522;}
.st24{fill:#78BE20;}
.st25{fill:#005F61;}
.st26{fill:url(#SVGID_18_);}
</style>
<g>
<path class="st0" d="M366.6,78h37.1v178.9H497v32.7H366.6V78z"/>
<path class="st0" d="M619.8,74.5C683.3,74.5,728,120.8,728,184c0,63.1-44.7,109.5-108.2,109.5c-63.5,0-108.2-46.3-108.2-109.5
C511.6,120.8,556.3,74.5,619.8,74.5z M619.8,107.5c-42.8,0-70.1,32.7-70.1,76.5c0,43.5,27.3,76.5,70.1,76.5
c42.5,0,70.1-33,70.1-76.5C689.9,140.2,662.3,107.5,619.8,107.5z"/>
<path class="st0" d="M819.4,200.5L801,222v67.6h-37.1V78H801v100.9L883.8,78h46l-86,99.9l92.3,111.7h-45.7L819.4,200.5z"/>
<path class="st0" d="M960.9,78H998v211.6h-37.1V78z"/>
</g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="86.8402" y1="268.7968" x2="86.8402" y2="0.426">
<stop offset="0" style="stop-color:#78BE20"/>
<stop offset="0.1197" style="stop-color:#58AF21"/>
<stop offset="0.3682" style="stop-color:#199122"/>
<stop offset="0.486" style="stop-color:#008522"/>
<stop offset="0.6925" style="stop-color:#007242"/>
<stop offset="0.8806" style="stop-color:#006459"/>
<stop offset="1" style="stop-color:#005F61"/>
</linearGradient>
<polygon class="st1" points="132.1,268.8 0.3,137 136.9,0.4 173.3,36.8 73.1,137 168.5,232.4 "/>
</g>
<g>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="212.9564" y1="367.5197" x2="212.9564" y2="99.1484">
<stop offset="0" style="stop-color:#78BE20"/>
<stop offset="0.1197" style="stop-color:#58AF21"/>
<stop offset="0.3682" style="stop-color:#199122"/>
<stop offset="0.486" style="stop-color:#008522"/>
<stop offset="0.6925" style="stop-color:#007242"/>
<stop offset="0.8806" style="stop-color:#006459"/>
<stop offset="1" style="stop-color:#005F61"/>
</linearGradient>
<polygon class="st3" points="162.9,367.5 126.5,331.1 226.7,230.9 131.3,135.6 167.7,99.1 299.5,230.9 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 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

@ -1,5 +1,4 @@
export const resetWalletData = (state) => {
state.commit("set_wallet_data", {
status: {
@ -20,23 +19,29 @@ export const resetWalletData = (state) => {
spend_key: ""
},
transactions: {
tx_list: [],
tx_list: []
},
address_list: {
used: [],
unused: [],
address_book: [],
address_book: []
}
})
}
export const resetWalletStatus = (state) => {
state.commit("set_wallet_data", {
status: {
code: 1,
message: null
}
})
}
export const resetPendingConfig = (state) => {
state.commit("set_app_data", {
pending_config: state.state.app.config
})
}

View file

@ -1,33 +1,31 @@
export const isReady = (state) => {
const { daemons, app } = state.app.config
const config_daemon = daemons[app.net_type]
let target_height
if(state.app.config.daemon.type === "local" && !state.daemon.info.is_ready) {
if (config_daemon.type === "local") {
target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height)
} else {
target_height = state.daemon.info.height
}
if(state.app.config.daemon.type === "local") {
return state.daemon.info.is_ready && state.wallet.info.height >= target_height - 1
} else {
return state.wallet.info.height >= target_height - 1
}
return false
return state.wallet.info.height >= target_height - 1
}
export const isAbleToSend = (state) => {
const { daemons, app } = state.app.config
const config_daemon = daemons[app.net_type]
let target_height
if(state.app.config.daemon.type === "local" && !state.daemon.info.is_ready) {
if (config_daemon.type === "local") {
target_height = Math.max(state.daemon.info.height, state.daemon.info.target_height)
} else {
target_height = state.daemon.info.height
}
if(state.app.config.daemon.type === "local") {
return state.daemon.info.is_ready && state.wallet.info.height >= target_height - 1
} else if(state.app.config.daemon.type === "local_remote") {
if (config_daemon.type === "local_remote") {
return state.daemon.info.height_without_bootstrap >= target_height && state.wallet.info.height >= target_height - 1
} else {
return state.wallet.info.height >= target_height - 1
}
return false
}

View file

@ -4,9 +4,9 @@ import * as mutations from "./mutations"
import * as actions from "./actions"
export default {
namespaced: true,
state,
getters,
mutations,
actions
namespaced: true,
state,
getters,
mutations,
actions
}

View file

@ -1,4 +1,4 @@
const objectAssignDeep = require("object-assign-deep");
const objectAssignDeep = require("object-assign-deep")
export const set_app_data = (state, data) => {
state.app = objectAssignDeep.noMutate(state.app, data)
@ -12,6 +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_snode_status = (state, data) => {
state.service_node_status = objectAssignDeep.noMutate(state.service_node_status, data)
}

View file

@ -5,15 +5,24 @@ export default {
},
config: {
appearance: {
theme: "light"
theme: "dark"
}
},
pending_config: {
},
remotes: {
}
},
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: {
@ -34,18 +43,35 @@ export default {
spend_key: ""
},
transactions: {
tx_list: [],
tx_list: []
},
address_list: {
used: [],
unused: [],
address_book: [],
address_book: []
}
},
tx_status: {
code: 0,
message: ""
},
service_node_status: {
stake: {
code: 0,
message: "",
sending: false
},
registration: {
code: 0,
message: "",
sending: false
},
unlock: {
code: 0,
message: "",
sending: false
}
},
daemon: {
info: {
alt_blocks_count: 0,

View file

@ -1945,13 +1945,12 @@ return{_strlen:lb,_ge_mul8:Va,_keccak:db,_ge_scalarmult:Ta,_ge_fromfe_frombytes_
var ryoConfig = {
coinUnitPlaces: 9,
coinSymbol: 'RYO',
coinName: 'Ryo',
coinUriPrefix: 'ryo:',
longAddrPrefix: 0x2ce192,
kurzAddrPrefix: 0x2c6192
var lokiConfig = {
coinUnitPlaces: 12,
coinSymbol: 'LOKI',
coinName: 'Loki',
coinUriPrefix: 'loki:',
addressPrefix: 114,
};
var cnUtilGen = function(initConfig) {
@ -1962,7 +1961,7 @@ var cnUtilGen = function(initConfig) {
var HASH_STATE_BYTES = 200;
var HASH_SIZE = 32;
var ADDRESS_CHECKSUM_SIZE = 4;
var CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = config.longAddrPrefix;
var CRYPTONOTE_PUBLIC_ADDRESS_BASE58_PREFIX = config.addressPrefix;
var UINT64_MAX = new JSBigInt(2).pow(64);
var CURRENT_TX_VERSION = 1;
var TX_EXTRA_NONCE_MAX_COUNT = 255;
@ -2092,13 +2091,13 @@ var cnUtilGen = function(initConfig) {
this.pubkeys_to_string = function(spend, view, use_kurz) {
if(!use_kurz) {
var prefix = this.encode_varint(config.longAddrPrefix);
var prefix = this.encode_varint(config.addressPrefix);
var data = prefix + spend + view;
var checksum = this.cn_fast_hash(data);
return cnBase58.encode(data + checksum.slice(0, ADDRESS_CHECKSUM_SIZE * 2));
}
else {
var prefix = this.encode_varint(config.kurzAddrPrefix);
var prefix = this.encode_varint(config.addressPrefix);
var data = prefix + spend;
var checksum = this.cn_fast_hash(data);
return cnBase58.encode(data + checksum.slice(0, ADDRESS_CHECKSUM_SIZE * 2));
@ -2390,7 +2389,7 @@ var cnUtilGen = function(initConfig) {
return this;
};
var cnUtil = cnUtilGen(ryoConfig);
var cnUtil = cnUtilGen(lokiConfig);
/*
mnemonic.js : Converts between 4-byte aligned strings and a human-readable
sequence of words. Uses 1626 common words taken from wikipedia article:

View file

@ -1,4 +1,8 @@
//import { validateAddress } from "./address_tools"
/* eslint-disable prefer-promise-reject-errors */
export const greater_than_zero = (input) => {
return input > 0
}
export const payment_id = (input) => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && (input.length == 16 || input.length == 64))
@ -8,36 +12,28 @@ export const privkey = (input) => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && input.length == 64)
}
export const address = (input) => {
if(!(/^[0-9A-Za-z]+$/.test(input))) return false
switch (input.substring(0,4)) {
case "Sumo":
case "RYoL":
case "Suto":
case "RYoT":
return input.length === 99
case "Subo":
case "Suso":
return input.length == 98
case "RYoS":
case "RYoU":
return input.length == 99
case "Sumi":
case "RYoN":
case "Suti":
case "RYoE":
return input.length === 110
case "RYoK":
case "RYoH":
return input.length === 55
default:
return false
}
export const service_node_key = (input) => {
return input.length === 64 && /^[0-9A-Za-z]+$/.test(input)
}
export const address = (input, gateway) => {
if (!(/^[0-9A-Za-z]+$/.test(input))) return false
// Validate the address
return new Promise((resolve, reject) => {
gateway.once("validate_address", (data) => {
if (data.address && data.address !== input) {
reject()
} else {
if (data.valid) {
resolve()
} else {
reject()
}
}
})
gateway.send("wallet", "validate_address", {
address: input
})
})
}