Loki changes

Update package json

Linting.

Fixed websocket not working.

Move to lokid and rpc.

Re-branding

Fixed rpc call for wallet restoration.

Force ring size of 10
Other fix.

Added service node ui.

Added different daemons

Added remote preset loading.
Fixed priority options.
Fixed links.
This commit is contained in:
Mikunj 2019-02-13 10:09:18 +11:00
parent 9ce31d6ea3
commit 7b761877d1
42 changed files with 8072 additions and 6757 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

12459
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,17 @@
{
"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",
"description": "Modern GUI interface for Loki Currency",
"productName": "Loki Wallet Atom",
"cordovaId": "com.ryo-currency.ryo-gui-wallet",
"author": "Ryo-currency <contact@ryo-currency.com>",
"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": {
@ -42,8 +45,9 @@
"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"
"quasar-cli": "^0.17.23",
"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",
@ -118,30 +119,30 @@ module.exports = function (ctx) {
background_color: "#ffffff",
theme_color: "#027be3",
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 +151,7 @@ module.exports = function (ctx) {
},
electron: {
bundler: "builder", // or "packager"
extendWebpack(cfg) {
extendWebpack (cfg) {
// do something with Electron process Webpack cfg
},
packager: {
@ -166,14 +167,14 @@ module.exports = function (ctx) {
// win32metadata: { ... }
extraResource: [
"bin",
"bin"
]
},
builder: {
// https://www.electron.build/configuration/configuration
appId: "com.ryo-currency.wallet",
productName: "Ryo Wallet Atom",
appId: "com.lokinetwork.wallet",
productName: "Loki Wallet Atom",
copyright: "Copyright © 2018 Ryo Currency Project",
// directories: {

View File

@ -19,23 +19,23 @@ 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
*/
@ -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,15 +1,15 @@
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")
const path = require("path")
export class Backend {
constructor(mainWindow) {
constructor (mainWindow) {
this.mainWindow = mainWindow
this.daemon = null
this.walletd = null
@ -21,59 +21,102 @@ export class Backend {
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-wallet"
} else {
this.config_dir = path.join(os.homedir(), ".ryo");
this.config_dir = path.join(os.homedir(), ".loki-wallet")
}
if (!fs.existsSync(this.config_dir)) {
fs.mkdirSync(this.config_dir);
fs.mkdirSync(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.mkdirSync(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")
const daemon = {
type: "local_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
}
this.config_data = {
app: {
data_dir: this.config_dir,
ws_bind_port: 12213,
testnet: false
net_type: "main"
},
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
daemons: {
main: {
...daemon,
remote_host: "doopool.xyz",
remote_port: 22020
},
staging: {
...daemon,
p2p_bind_port: 38153,
rpc_bind_port: 38154,
zmq_rpc_bind_port: 38155
},
test: {
...daemon,
p2p_bind_port: 38156,
rpc_bind_port: 38157,
zmq_rpc_bind_port: 38158
}
},
wallet: {
rpc_bind_port: 12214,
rpc_bind_port: 18082,
log_level: 0
}
}
this.remotes = [
{
host: "doopool.xyz",
port: "22020"
},
{
host: "rpc.nodes.rentals",
port: "22023"
},
{
host: "daemons.cryptopool.space",
port: "22023"
},
{
host: "node.loki-pool.com",
port: "18081"
},
{
host: "uk.loki.cash",
port: "22020"
},
{
host: "imaginary.stream",
port: "22023"
}
]
this.token = config.token
this.wss = new WebSocket.Server({
@ -82,228 +125,216 @@ 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])
})
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
})
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
// 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) {
let testnet_dir = path.join(this.config_data.app.data_dir, "testnet")
if (!fs.existsSync(testnet_dir))
fs.mkdirSync(testnet_dir);
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);
const { net_type } = this.config_data.app
const dirs = {
"main": this.config_data.app.data_dir,
"staging": path.join(this.config_data.app.data_dir, "staging"),
"test": path.join(this.config_data.app.data_dir, "testnet")
}
this.daemon = new Daemon(this);
this.walletd = new WalletRPC(this);
// Make sure we have the directories we need
const net_dir = dirs[net_type]
if (!fs.existsSync(net_dir)) { fs.mkdirSync(net_dir) }
const log_dir = path.join(net_dir, "logs")
if (!fs.existsSync(log_dir)) { fs.mkdirSync(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) {
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.config_data.daemons[net_type].type = "remote"
this.send("set_app_data", {
status: {
code: 5
},
config: this.config_data,
pending_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(() => {
this.send("set_app_data", {
status: {
code: 7 // Reading wallet list
}
});
})
this.walletd.listWallets(true)
@ -311,18 +342,16 @@ export class Backend {
status: {
code: 0 // Ready
}
});
})
}).catch(error => {
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
});
return;
});
})
})
}).catch(error => {
if(this.config_data.daemon.type == "remote") {
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})
@ -331,31 +360,24 @@ export class Backend {
status: {
code: -1 // Return to config screen
}
});
return;
});
})
})
}).catch(error => {
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
});
return;
});
});
})
})
})
}
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()

View File

@ -1,65 +1,59 @@
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 = "main"
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) {
if(options.daemon.type === "remote") {
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 +63,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 = {
"main": options.app.data_dir,
"staging": path.join(options.app.data_dir, "staging"),
"test": 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 === "test") {
args.push("--testnet")
} else if (net_type === "staging") {
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 === "main") {
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 +127,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 +145,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 +169,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 +216,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 +234,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 +262,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 +282,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 +291,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,11 +324,11 @@ export class Daemon {
})
}
sendGateway(method, data) {
sendGateway (method, data) {
this.backend.send(method, data)
}
sendRPC(method, params={}) {
sendRPC (method, params = {}) {
let id = this.id++
let options = {
uri: `${this.protocol}${this.hostname}:${this.port}/json_rpc`,
@ -355,15 +339,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) {
options.json.params = params
}
return this.queue.add(() => {
return request(options)
.then((response) => {
if(response.hasOwnProperty("error")) {
if (response.hasOwnProperty("error")) {
return {
method: method,
params: params,
@ -392,12 +376,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

@ -32,14 +32,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>
@ -128,7 +128,7 @@
import { mapState } from "vuex"
const {clipboard} = 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 {
@ -159,7 +159,7 @@ export default {
components: {
AddressHeader,
TxList,
FormatRyo,
FormatLoki,
QrcodeVue
}
}

View File

@ -2,11 +2,11 @@
<q-layout-footer class="status-footer">
<div class="status-line">
<template v-if="config.daemon.type !== 'remote'">
<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'">
<template v-if="config_daemon.type !== 'local'">
<div>Remote: {{ daemon.info.height }}</div>
</template>
@ -32,19 +32,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" && !this.daemon.info.is_ready)
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,7 +63,7 @@ export default {
return pct
},
status(state) {
if(this.config.daemon.type === "local") {
if(this.config_daemon.type === "local") {
if(this.daemon.info.height_without_bootstrap < this.target_height || !this.daemon.info.is_ready) {
return "Syncing..."
} else if(this.wallet.info.height < this.target_height - 1 && this.wallet.info.height != 0) {

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

@ -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>
@ -39,12 +39,12 @@
<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>
</p>
</div>

View File

@ -83,11 +83,12 @@ export default {
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

View File

@ -1,40 +1,40 @@
<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="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>
<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="config_daemon.type == 'remote'">
Less security, wallet will connect to a remote node to make all transactions.
</p>
<q-field v-if="config.daemon.type != 'remote'">
<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"
<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'" />
<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>
</div>
</q-field>
<q-field v-if="config.daemon.type != 'local'">
<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'" />
<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'" />
<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>
</div>
@ -52,39 +52,58 @@
</div>
</q-field>
<div v-if="(config_daemon.type !== 'local') && (config.app.net_type === 'main')">
<hr>
<div class="presets">
<div class="q-body-1">Remote Node Presets</div>
<q-field>
<div class="row gutter-sm">
<div class="col-8">
<q-select
v-model="select"
:options="remoteOptions"
:dark="theme=='dark'"
/>
</div>
<div class="col-4">
<q-btn v-on:click="loadPreset" :text-color="theme=='dark'?'white':'dark'">Load Preset</q-btn>
</div>
</div>
</q-field>
</div>
</div>
<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'"
<div class="col-6">
<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">
<div class="col-6">
<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'"
<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'"
<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'"
<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'"
<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>
@ -92,11 +111,11 @@
<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'"
<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'"
<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">
@ -104,11 +123,22 @@
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'"
<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>
</q-field>
<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: 'main' },
{ label: 'Stage Net', value: 'staging' },
{ label: 'Test Net', value: 'test' }
]"
/>
</q-field>
</q-collapsible>
</div>
@ -120,7 +150,17 @@ export default {
name: "SettingsGeneral",
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
remotes: state => state.gateway.app.remotes,
remoteOptions (state) {
return this.remotes.map((r, index) => ({
label: `${r.host}:${r.port}`,
value: index,
}));
},
config: state => state.gateway.app.pending_config,
config_daemon (state) {
return this.config.daemons[this.config.app.net_type]
},
}),
methods: {
selectPath () {
@ -128,8 +168,20 @@ export default {
},
setDataPath (file) {
this.config.app.data_dir = file.target.files[0].path
},
loadPreset () {
if (!this.remotes || this.remotes.length === 0) return;
const { host, port } = this.remotes[this.select];
this.config_daemon.remote_host = host;
this.config_daemon.remote_port = port;
},
},
data () {
return {
select: 0,
}
}
},
}
</script>
@ -150,5 +202,9 @@ export default {
.q-collapsible-sub-item {
padding: 0;
}
.presets {
margin-top: 20px;
}
}
</style>

View File

@ -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>
@ -122,7 +122,7 @@
<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>
@ -182,7 +182,7 @@ 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({
@ -298,7 +298,7 @@ export default {
components: {
Identicon,
TxTypeIcon,
FormatRyo
FormatLoki
}
}
</script>

View File

@ -19,7 +19,7 @@
</q-item-main>
<q-item-side>
<q-item-tile label>
<FormatRyo :amount="tx.amount" />
<FormatLoki :amount="tx.amount" />
</q-item-tile>
<q-item-tile sublabel>
<timeago :datetime="tx.timestamp*1000" :auto-update="60">
@ -64,7 +64,7 @@ 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: {
@ -236,7 +236,7 @@ export default {
Identicon,
TxTypeIcon,
TxDetails,
FormatRyo
FormatLoki
}
}
</script>

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,12 +1,9 @@
import { ipcRenderer } from "electron"
import { Notify, Dialog, Loading, LocalStorage } from "quasar"
import { SCEE } from "./SCEE-Node";
import * as WebSocket from "ws"
import { SCEE } from "./SCEE-Node"
export class Gateway {
constructor(app, router) {
constructor (app, router) {
this.app = app
this.router = router
this.token = null
@ -19,8 +16,8 @@ export class Gateway {
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 +27,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 +66,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 +76,70 @@ 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_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 "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 "wallet_list":
this.app.store.commit("gateway/set_wallet_list", decrypted_data.data)
break
case "settings_changed_reboot":
this.confirmClose("Changes require restart. Would you like to exit now?")
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
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,7 +11,7 @@
@click="cancel()" />
</template>
<q-toolbar-title v-if="page_title=='Ryo'">
<q-toolbar-title v-if="page_title=='Loki'">
<div style="margin-top:7px">
<img src="statics/ryo-wallet.svg" height="32">
</div>
@ -64,7 +64,7 @@ export default {
default:
case "wallet-select":
return "Ryo"
return "Loki"
}
}
},

View File

@ -17,6 +17,9 @@
<q-route-tab to="/wallet/addressbook" slot="title">
<span><q-icon name="person" /> Address Book</span>
</q-route-tab>
<q-route-tab to="/wallet/servicenode" slot="title">
<span><q-icon name="router" /> Service Node</span>
</q-route-tab>
<q-route-tab to="/wallet/txhistory" slot="title">
<span><q-icon name="history" /> TX History</span>
</q-route-tab>

View File

@ -106,7 +106,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:

View File

@ -67,18 +67,21 @@
<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'">
<template v-if="config_daemon.type == 'local'">
<code>local node</code>
</template>
<template v-if="pending_config.daemon.type == 'local_remote'">
<template v-if="config_daemon.type == 'local_remote'">
<code>local + remote node</code>
</template>
<template v-if="pending_config.daemon.type == 'remote'">
<template v-if="config_daemon.type == 'remote'">
<code>remote node</code>
</template>
<template v-if="pending_config.app.testnet">
<template v-if="pending_config.app.net_type == 'test'">
<code>on testnet</code>
</template>
<template v-if="pending_config.app.net_type == 'staging'">
<code>on staging</code>
</template>
and will store data in
<code>{{ pending_config.app.data_dir }}</code>
</p>
@ -120,7 +123,10 @@ 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 {

View File

@ -20,20 +20,6 @@
/>
</q-field>
<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>
<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>
@ -59,7 +45,6 @@ export default {
wallet: {
name: "",
language: "English",
type: "long",
password: "",
password_confirm: ""
},

View File

@ -49,11 +49,12 @@
<q-item-main label="Create new wallet" />
</q-item>
<q-item @click.native="restoreWallet()">
<q-item-main label="Restore wallet from seed" />
<q-item-main label="Restore wallet from seed (Currently not working)" />
</q-item>
<q-item @click.native="restoreViewWallet()">
<!-- TODO: Re-enable this when LOKI has the functionality -->
<!-- <q-item @click.native="restoreViewWallet()">
<q-item-main label="Restore view-only wallet" />
</q-item>
</q-item> -->
<q-item @click.native="importWallet()">
<q-item-main label="Import wallet from file" />
</q-item>

View File

@ -4,7 +4,7 @@
<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
<q-icon name="call_received" size="24px" /> Receive Loki
</div>
<div class="col-4">

View File

@ -5,7 +5,7 @@
<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
<q-icon name="call_made" size="24px" /> Send Loki
</div>
<div class="col-4">
@ -25,7 +25,7 @@
<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
<q-icon name="call_made" size="24px" /> Send Loki
</div>
<div class="col-4">
@ -74,28 +74,13 @@
/>
</q-field>
<div class="row gutter-md">
<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>
</div>
</div>
<q-field>
<q-select :dark="theme=='dark'"
v-model="newTx.priority"
float-label="Priority"
:options="priorityOptions"
/>
</q-field>
<q-field>
@ -154,7 +139,6 @@ export default {
amount: 0,
address: "",
payment_id: "",
mixin: 12,
priority: 0,
address_book: {
save: false,
@ -162,17 +146,11 @@ 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: "Slow (x0.25 fee)", value: 1},
{label: "Fast (x5 fee)", value: 2},
{label: "Fastest (x41.5 fee)", value: 3},
],
}
},
@ -202,7 +180,6 @@ export default {
amount: 0,
address: "",
payment_id: "",
mixin: 12,
priority: 0,
address_book: {
save: false,

View File

@ -0,0 +1,216 @@
<template>
<q-page class="service-node-page">
<template>
<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="router" size="24px" /> Service Nodes
</div>
<div class="col-4">
</div>
</div>
<div class="q-pa-md">
<q-field>
<q-input v-model="serviceNode.key" float-label="Service Node Key"
:dark="theme=='dark'"
@blur="$v.serviceNode.key.$touch"
:error="$v.serviceNode.key.$error"
/>
</q-field>
<q-item>
<q-item-main>
<q-item-tile label class="recepient-address">Award Recepient's Address (yours)</q-item-tile>
<q-item-tile class="monospace break-all" label>{{ info.address }}</q-item-tile>
</q-item-main>
<q-item-side>
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyAddress(info.address, $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy address
</q-tooltip>
</q-btn>
</q-item-side>
</q-item>
<div class="row items-end gutter-md">
<div class="col">
<q-field class="q-ma-none">
<q-input v-model="serviceNode.amount" float-label="Amount" :dark="theme=='dark'"
type="number" min="0" :max="unlocked_balance / 1e9" />
</q-field>
</div>
<div>
<q-btn @click="serviceNode.amount = unlocked_balance / 1e9" :text-color="theme=='dark'?'white':'dark'">All coins</q-btn>
</div>
</div>
<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="tx_status.sending" :dark="theme=='dark'">
<q-spinner color="primary" :size="30" />
</q-inner-loading> -->
</template>
</q-page>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import { required, decimal } from "vuelidate/lib/validators"
import { payment_id, service_node_key } from "src/validators/common"
import Identicon from "components/identicon"
const objectAssignDeep = require("object-assign-deep");
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
unlocked_balance: state => state.gateway.wallet.info.unlocked_balance,
info: state => state.gateway.wallet.info,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
},
is_able_to_send (state) {
return this.$store.getters["gateway/isAbleToSend"]
}
}),
data () {
return {
staking: false,
serviceNode: {
key: "",
amount: 0,
},
}
},
validations: {
serviceNode: {
amount: {
required,
decimal
},
key: { required, service_node_key },
}
},
methods: {
copyAddress (address, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
clipboard.writeText(address)
this.$q.notify({
type: "positive",
timeout: 1000,
message: "Address copied to clipboard"
})
},
stake: function () {
this.$v.serviceNode.$touch()
if(this.serviceNode.amount < 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount cannot be negative"
})
return
} else if(this.serviceNode.amount == 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount must be greater than zero"
})
return
} else if(this.serviceNode.amount > this.unlocked_balance / 1e9) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Not enough unlocked balance"
})
return
} else if (this.$v.serviceNode.amount.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Amount not valid"
})
return
}
if (this.$v.serviceNode.key.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Service node key not valid"
})
return
}
this.$q.dialog({
title: "Stake",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: "STAKE"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
// this.$store.commit("gateway/set_tx_status", {
// code: 1,
// message: "Sending transaction",
// sending: true
// })
// let newTx = objectAssignDeep.noMutate(this.newTx, {password})
// this.$gateway.send("wallet", "transfer", newTx)
}).catch(() => {
})
}
},
components: {
Identicon,
}
}
</script>
<style lang="scss">
.service-node-page {
.q-item {
padding-left: 0;
padding-right: 0;
}
.recepient-address {
margin-bottom: 8px;
font-size: 1rem;
}
}
</style>

View File

@ -9,7 +9,7 @@
<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 +18,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 +262,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({
@ -545,7 +545,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,120 @@ 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: "/wallet",
component: () =>
import ("layouts/wallet/main"),
import("layouts/wallet/main"),
children: [
{
path: "",
component: () =>
import ("pages/wallet/wallet")
import("pages/wallet/wallet")
},
{
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",
component: () =>
import ("pages/wallet/txhistory")
import("pages/wallet/txhistory")
},
{
path: "servicenode",
component: () =>
import("pages/wallet/service-node")
}
]
},
{ // Always leave this as last one
path: "*",
component: () =>
import ("pages/404")
import("pages/404")
}
]

View File

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

View File

@ -1,12 +1,15 @@
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" && !state.daemon.info.is_ready) {
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") {
if (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
@ -15,16 +18,19 @@ export const isReady = (state) => {
}
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" && !state.daemon.info.is_ready) {
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") {
if (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") {
} else 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

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)

View File

@ -9,6 +9,8 @@ export default {
}
},
pending_config: {
},
remotes: {
}
},
wallets: {
@ -34,12 +36,12 @@ export default {
spend_key: ""
},
transactions: {
tx_list: [],
tx_list: []
},
address_list: {
used: [],
unused: [],
address_book: [],
address_book: []
}
},
tx_status: {

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,4 @@
//import { validateAddress } from "./address_tools"
// import { validateAddress } from "./address_tools"
export const payment_id = (input) => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && (input.length == 16 || input.length == 64))
@ -8,36 +8,39 @@ export const privkey = (input) => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && input.length == 64)
}
export const service_node_key = (input) => {
return input.length === 64 && /^[0-9A-Za-z]+$/.test(input)
}
export const address = (input) => {
if (!(/^[0-9A-Za-z]+$/.test(input))) return false
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
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 "Subo":
case "Suso":
return input.length == 98
case "RYoS":
case "RYoU":
return input.length == 99
case "RYoS":
case "RYoU":
return input.length == 99
case "Sumi":
case "RYoN":
case "Suti":
case "RYoE":
return input.length === 110
case "Sumi":
case "RYoN":
case "Suti":
case "RYoE":
return input.length === 110
case "RYoK":
case "RYoH":
return input.length === 55
case "RYoK":
case "RYoH":
return input.length === 55
default:
return false
default:
return false
}
}