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

12397
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

@ -59,6 +59,7 @@ module.exports = function (ctx) {
"QField",
"QInput",
"QRadio",
"QOptionGroup",
"QBtn",
"QBtnToggle",
"QIcon",
@ -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

@ -21,19 +21,19 @@ let forceQuit = false
const portInUse = function (port, callback) {
var server = net.createServer(function (socket) {
socket.write("Echo server\r\n");
socket.pipe(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 () {
/**
@ -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) {
// 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) {
// 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) {
// 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) {
// 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,12 @@
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) {
@ -22,58 +22,101 @@ export class Backend {
}
init (config) {
if(os.platform() == "win32") {
this.config_dir = "C:\\ProgramData\\ryo";
//this.config_dir = path.join(os.homedir(), "ryo");
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"));
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,9 +125,8 @@ 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 = {}) {
@ -93,39 +135,37 @@ export class Backend {
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) {
if (client.readyState === WebSocket.OPEN) {
client.send(encrypted_data)
}
});
})
}
receive (data) {
let decrypted_data = JSON.parse(this.scee.decryptString(data, this.token));
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;
this.handle(decrypted_data)
break
case "daemon":
if (this.daemon) {
this.daemon.handle(decrypted_data);
this.daemon.handle(decrypted_data)
}
break;
break
case "wallet":
if (this.walletd) {
this.walletd.handle(decrypted_data);
this.walletd.handle(decrypted_data)
}
break;
break
}
}
handle (data) {
let params = data.data
switch (data.method) {
@ -134,7 +174,7 @@ export class Backend {
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', () => {
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => {
this.send("set_app_data", {
config: params,
pending_config: params
@ -148,42 +188,40 @@ export class Backend {
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
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', () => {
})
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => {
if (data.method == "save_config_init") {
this.startup();
this.startup()
} else {
this.send("set_app_data", {
config: this.config_data,
pending_config: this.config_data,
pending_config: this.config_data
})
if (config_changed) {
this.send("settings_changed_reboot")
}
}
});
break;
})
break
case "init":
this.startup();
break;
this.startup()
break
case "open_explorer":
if (params.type == "tx") {
require("electron").shell.openExternal("https://explorer.ryo-currency.com/tx/"+params.id)
require("electron").shell.openExternal("https://lokiblocks.com/tx/" + params.id)
}
break;
break
case "open_url":
require("electron").shell.openExternal(params.url)
break;
break
case "save_png":
let filename = dialog.showSaveDialog(this.mainWindow, {
@ -193,21 +231,22 @@ export class Backend {
})
if (filename) {
let base64Data = params.img.replace(/^data:image\/png;base64,/, "")
let binaryData = new Buffer(base64Data, 'base64').toString("binary")
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 (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:
}
}
startup () {
this.send("set_app_data", {
remotes: this.remotes
})
fs.readFile(this.config_file, "utf8", (err, data) => {
if (err) {
this.send("set_app_data", {
@ -215,95 +254,87 @@ export class Backend {
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) {
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 () {
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,9 +1,9 @@
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) {
@ -11,35 +11,29 @@ export class Daemon {
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 () {
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)
})
}
@ -47,15 +41,15 @@ export class Daemon {
}
start (options) {
if(options.daemon.type === "remote") {
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) => {
@ -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}`))
@ -127,15 +129,15 @@ export class Daemon {
this.sendRPC("get_info").then((data) => {
if (!data.hasOwnProperty("error")) {
this.startHeartbeat()
clearInterval(intrvl);
resolve();
clearInterval(intrvl)
resolve()
} else {
if (data.error.cause &&
data.error.cause.code === "ECONNREFUSED") {
// Ignore
} else {
clearInterval(intrvl);
reject(error);
clearInterval(intrvl)
reject(error)
}
}
})
@ -144,25 +146,19 @@ export class Daemon {
}
handle (data) {
let params = data.data
switch (data.method) {
case "ban_peer":
this.banPeer(params.host, params.seconds)
break
default:
}
}
banPeer (host, seconds = 3600) {
if(!seconds)
seconds=3600
if (!seconds) { seconds = 3600 }
let params = {
bans: [{
@ -183,22 +179,18 @@ export class Daemon {
// Send updated peer and ban list
this.heartbeatSlowAction()
})
}
timestampToHeight (timestamp, pivot = null, recursion_limit = null) {
return new Promise((resolve, reject) => {
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
@ -212,10 +204,8 @@ export class Daemon {
}
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
this.getRPC("last_block_header").then((data) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
return reject()
@ -250,14 +240,11 @@ export class Daemon {
// 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
})
@ -275,7 +262,6 @@ export class Daemon {
this.heartbeatSlowAction()
}, 30 * 1000) // 30 seconds
this.heartbeatSlowAction()
}
heartbeatAction () {
@ -296,8 +282,7 @@ export class Daemon {
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_info") {
daemon_info.info = n.result
}
@ -311,7 +296,7 @@ export class Daemon {
if (this.local) {
actions = [
this.getRPC("connections"),
this.getRPC("bans"),
this.getRPC("bans")
// this.getRPC("txpool_backlog"),
]
} else {
@ -326,8 +311,7 @@ export class Daemon {
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")) {
@ -355,9 +339,9 @@ export class Daemon {
method: method
},
agent: this.agent
};
}
if (Object.keys(params).length !== 0) {
options.json.params = params;
options.json.params = params
}
return this.queue.add(() => {
@ -393,11 +377,11 @@ export class Daemon {
* Call one of the get_* RPC calls
*/
getRPC (parameter, args) {
return this.sendRPC(`get_${parameter}`, args);
return this.sendRPC(`get_${parameter}`, args)
}
quit () {
clearInterval(this.heartbeat);
clearInterval(this.heartbeat)
return new Promise((resolve, reject) => {
if (this.daemonProcess) {
this.daemonProcess.on("close", code => {

View File

@ -1,11 +1,11 @@
import child_process from "child_process";
const request = require("request-promise");
const queue = require("promise-queue");
const http = require("http");
const os = require("os");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
import child_process from "child_process"
const request = require("request-promise")
const queue = require("promise-queue")
const http = require("http")
const os = require("os")
const fs = require("fs")
const path = require("path")
const crypto = require("crypto")
export class WalletRPC {
constructor (backend) {
@ -14,7 +14,7 @@ export class WalletRPC {
this.wallet_dir = null
this.auth = []
this.id = 0
this.testnet = false
this.net_type = "main"
this.heartbeat = null
this.wallet_state = {
open: false,
@ -32,16 +32,16 @@ export class WalletRPC {
this.agent = new http.Agent({keepAlive: true, maxSockets: 1})
this.queue = new queue(1, Infinity)
}
// this function will take an options object for testnet, data-dir, etc
start (options) {
const { net_type } = options.app
const daemon = options.daemons[net_type]
return new Promise((resolve, reject) => {
let daemon_address = `${options.daemon.rpc_bind_ip}:${options.daemon.rpc_bind_port}`
if(options.daemon.type == "remote") {
daemon_address = `${options.daemon.remote_host}:${options.daemon.remote_port}`
let daemon_address = `${daemon.rpc_bind_ip}:${daemon.rpc_bind_port}`
if (daemon.type == "remote") {
daemon_address = `${daemon.remote_host}:${daemon.remote_port}`
}
crypto.randomBytes(64 + 64 + 32, (err, buffer) => {
@ -52,7 +52,7 @@ export class WalletRPC {
this.auth = [
auth.substr(0, 64), // rpc username
auth.substr(64, 64), // rpc password
auth.substr(128,32), // password salt
auth.substr(128, 32) // password salt
]
const args = [
@ -60,37 +60,39 @@ export class WalletRPC {
"--rpc-bind-port", options.wallet.rpc_bind_port,
"--daemon-address", daemon_address,
// "--log-level", options.wallet.log_level,
"--log-level", "*:WARNING,net*:FATAL,net.http:DEBUG,global:INFO,verify:FATAL,stacktrace:INFO",
"--log-level", "*:WARNING,net*:FATAL,net.http:DEBUG,global:INFO,verify:FATAL,stacktrace:INFO"
]
let log_file
const { net_type } = options.app
this.net_type = net_type
this.data_dir = options.app.data_dir
if(options.app.testnet) {
this.testnet = true
this.wallet_dir = path.join(options.app.data_dir, "testnet", "wallets")
log_file = path.join(options.app.data_dir, "testnet", "logs", "wallet-rpc.log")
args.push("--testnet")
args.push("--log-file", log_file)
args.push("--wallet-dir", this.wallet_dir)
} else {
this.wallet_dir = path.join(options.app.data_dir, "wallets")
log_file = path.join(options.app.data_dir, "logs", "wallet-rpc.log")
args.push("--log-file", log_file)
args.push("--wallet-dir", this.wallet_dir)
const dirs = {
"main": this.data_dir,
"staging": path.join(this.data_dir, "staging"),
"test": path.join(this.data_dir, "testnet")
}
if (fs.existsSync(log_file))
fs.truncateSync(log_file, 0)
this.wallet_dir = path.join(dirs[net_type], "wallets")
args.push("--wallet-dir", this.wallet_dir)
if (!fs.existsSync(this.wallet_dir))
fs.mkdirSync(this.wallet_dir)
const log_file = path.join(dirs[net_type], "logs", "wallet-rpc.log")
args.push("--log-file", log_file)
if (net_type === "test") {
args.push("--testnet")
} else if (net_type === "staging") {
args.push("--stagenet")
}
if (fs.existsSync(log_file)) { fs.truncateSync(log_file, 0) }
if (!fs.existsSync(this.wallet_dir)) { fs.mkdirSync(this.wallet_dir) }
if (process.platform === "win32") {
this.walletRPCProcess = child_process.spawn(path.join(__ryo_bin, "ryo-wallet-rpc.exe"), args)
this.walletRPCProcess = child_process.spawn(path.join(__ryo_bin, "loki-wallet-rpc.exe"), args)
} else {
this.walletRPCProcess = child_process.spawn(path.join(__ryo_bin, "ryo-wallet-rpc"), args, {
this.walletRPCProcess = child_process.spawn(path.join(__ryo_bin, "loki-wallet-rpc"), args, {
detached: true
})
}
@ -101,10 +103,9 @@ export class WalletRPC {
this.port = options.wallet.rpc_bind_port
this.walletRPCProcess.stdout.on("data", (data) => {
process.stdout.write(`Wallet: ${data}`)
let lines = data.toString().split("\n");
let lines = data.toString().split("\n")
let match, height = null
lines.forEach((line) => {
match = line.match(this.height_regex1)
@ -156,17 +157,15 @@ export class WalletRPC {
}
handle (data) {
let params = data.data
switch (data.method) {
case "list_wallets":
this.listWallets()
break
case "create_wallet":
this.createWallet(params.name, params.password, params.language, params.type)
this.createWallet(params.name, params.password, params.language)
break
case "restore_wallet":
@ -175,6 +174,7 @@ export class WalletRPC {
break
case "restore_view_wallet":
// TODO: Decide if we want this for loki
this.restoreViewWallet(params.name, params.password, params.address, params.viewkey,
params.refresh_type, params.refresh_type == "date" ? params.refresh_start_date : params.refresh_start_height)
break
@ -192,7 +192,7 @@ export class WalletRPC {
break
case "transfer":
this.transfer(params.password, params.amount, params.address, params.payment_id, params.mixin, params.priority, params.address_book)
this.transfer(params.password, params.amount, params.address, params.payment_id, params.priority, params.address_book)
break
case "add_address_book":
@ -238,16 +238,11 @@ export class WalletRPC {
}
}
createWallet(filename, password, language, type) {
let short_address = type == "kurz"
createWallet (filename, password, language) {
this.sendRPC("create_wallet", {
filename,
password,
language,
short_address
language
}).then((data) => {
if (data.hasOwnProperty("error")) {
this.sendGateway("set_wallet_error", {status: data.error})
@ -260,14 +255,10 @@ export class WalletRPC {
this.wallet_state.open = true
this.finalizeNewWallet(filename)
})
}
restoreWallet (filename, password, seed, refresh_type, refresh_start_timestamp_or_height) {
if (refresh_type == "date") {
// Convert timestamp to 00:00 and move back a day
// Core code also moved back some amount of blocks
@ -275,26 +266,23 @@ export class WalletRPC {
timestamp = timestamp - (timestamp % 86400000) - 86400000
this.backend.daemon.timestampToHeight(timestamp).then((height) => {
if(height === false)
this.sendGateway("set_wallet_error", {status:{code: -1, message: "Invalid restore date"}})
else
this.restoreWallet(filename, password, seed, "height", height)
if (height === false) { this.sendGateway("set_wallet_error", {status: {code: -1, message: "Invalid restore date"}}) } else { this.restoreWallet(filename, password, seed, "height", height) }
})
return
}
let refresh_start_height = refresh_start_timestamp_or_height
let restore_height = refresh_start_timestamp_or_height
if(!Number.isInteger(refresh_start_height)) {
refresh_start_height = 0
if (!Number.isInteger(restore_height)) {
restore_height = 0
}
seed = seed.trim().replace(/\s{2,}/g, " ")
this.sendRPC("restore_wallet", {
this.sendRPC("restore_deterministic_wallet", {
filename,
password,
seed,
refresh_start_height
restore_height
}).then((data) => {
if (data.hasOwnProperty("error")) {
this.sendGateway("set_wallet_error", {status: data.error})
@ -320,12 +308,10 @@ export class WalletRPC {
this.finalizeNewWallet(filename)
// });
});
})
}
restoreViewWallet (filename, password, address, viewkey, refresh_type, refresh_start_timestamp_or_height) {
if (refresh_type == "date") {
// Convert timestamp to 00:00 and move back a day
// Core code also moved back some amount of blocks
@ -333,10 +319,7 @@ export class WalletRPC {
timestamp = timestamp - (timestamp % 86400000) - 86400000
this.backend.daemon.timestampToHeight(timestamp).then((height) => {
if(height === false)
this.sendGateway("set_wallet_error", {status:{code: -1, message: "Invalid restore date"}})
else
this.restoreViewWallet(filename, password, address, viewkey, "height", height)
if (height === false) { this.sendGateway("set_wallet_error", {status: {code: -1, message: "Invalid restore date"}}) } else { this.restoreViewWallet(filename, password, address, viewkey, "height", height) }
})
return
}
@ -365,12 +348,10 @@ export class WalletRPC {
this.wallet_state.open = true
this.finalizeNewWallet(filename)
});
})
}
importWallet (filename, password, import_path) {
// trim off suffix if exists
if (import_path.endsWith(".keys")) {
import_path = import_path.substring(0, import_path.length - ".keys".length)
@ -380,9 +361,7 @@ export class WalletRPC {
if (!fs.existsSync(import_path)) {
this.sendGateway("set_wallet_error", {status: {code: -1, message: "Invalid wallet path"}})
return
} else {
let destination = path.join(this.wallet_dir, filename)
if (fs.existsSync(destination) || fs.existsSync(destination + ".keys")) {
@ -401,7 +380,6 @@ export class WalletRPC {
password
}).then((data) => {
if (data.hasOwnProperty("error")) {
fs.unlinkSync(destination)
fs.unlinkSync(destination + ".keys")
@ -415,15 +393,11 @@ export class WalletRPC {
this.wallet_state.open = true
this.finalizeNewWallet(filename)
})
}
}
finalizeNewWallet (filename) {
Promise.all([
this.sendRPC("get_address"),
this.sendRPC("getheight"),
@ -482,13 +456,10 @@ export class WalletRPC {
this.sendGateway("set_wallet_data", wallet)
this.startHeartbeat()
})
}
openWallet (filename, password) {
this.sendRPC("open_wallet", {
filename,
password
@ -530,7 +501,6 @@ export class WalletRPC {
})
}
})
})
}
@ -556,19 +526,18 @@ export class WalletRPC {
name: this.wallet_state.name
},
transactions: {
tx_list: [],
tx_list: []
},
address_list: {
primary: [],
used: [],
unused: [],
address_book: [],
address_book_starred: [],
address_book_starred: []
}
}
for (let n of data) {
if (n.hasOwnProperty("error") || !n.hasOwnProperty("result")) {
continue
}
@ -580,7 +549,6 @@ export class WalletRPC {
height: n.result.height
}
})
} else if (n.method == "getbalance") {
if (this.wallet_state.balance == n.result.balance &&
this.wallet_state.unlocked_balance == n.result.unlocked_balance) {
@ -612,11 +580,9 @@ export class WalletRPC {
}
}
})
}
transfer (password, amount, address, payment_id, mixin, priority, address_book={}) {
transfer (password, amount, address, payment_id, priority, address_book = {}) {
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
if (err) {
this.sendGateway("set_tx_status", {
@ -640,12 +606,11 @@ export class WalletRPC {
let sweep_all = amount == this.wallet_state.unlocked_balance
if (sweep_all) {
let params = {
"address": address,
"account_index": 0,
"priority": priority,
"mixin": mixin
"mixin": 9 // Always force a ring size of 10 (ringsize = mixin + 1)
}
if (payment_id) {
@ -654,7 +619,7 @@ export class WalletRPC {
this.sendRPC("sweep_all", params).then((data) => {
if (data.hasOwnProperty("error")) {
let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1);
let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1)
this.sendGateway("set_tx_status", {
code: -1,
message: error,
@ -668,15 +633,12 @@ export class WalletRPC {
message: "Transaction successfully sent",
sending: false
})
})
} else {
let params = {
"destinations": [{"amount": amount, "address": address}],
"priority": priority,
"mixin": mixin
"mixin": 9
}
if (payment_id) {
@ -685,7 +647,7 @@ export class WalletRPC {
this.sendRPC("transfer_split", params).then((data) => {
if (data.hasOwnProperty("error")) {
let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1);
let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1)
this.sendGateway("set_tx_status", {
code: -1,
message: error,
@ -699,16 +661,11 @@ export class WalletRPC {
message: "Transaction successfully sent",
sending: false
})
})
}
if(address_book.hasOwnProperty("save") && address_book.save)
this.addAddressBook(address, payment_id, address_book.description, address_book.name)
if (address_book.hasOwnProperty("save") && address_book.save) { this.addAddressBook(address, payment_id, address_book.description, address_book.name) }
})
}
rescanBlockchain () {
@ -720,7 +677,6 @@ export class WalletRPC {
}
getPrivateKeys (password) {
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
if (err) {
this.sendGateway("set_wallet_data", {
@ -731,7 +687,6 @@ export class WalletRPC {
}
})
return
return
}
if (this.wallet_state.password_hash !== password_hash.toString("hex")) {
this.sendGateway("set_wallet_data", {
@ -763,22 +718,16 @@ export class WalletRPC {
}
this.sendGateway("set_wallet_data", wallet)
})
})
}
getAddressList () {
return new Promise((resolve, reject) => {
Promise.all([
this.sendRPC("get_address", {account_index: 0}),
this.sendRPC("getbalance", {account_index: 0})
]).then((data) => {
for (let n of data) {
if (n.hasOwnProperty("error") || !n.hasOwnProperty("result")) {
resolve({})
@ -792,7 +741,7 @@ export class WalletRPC {
info: {
address: data[0].result.address,
balance: data[1].result.balance,
unlocked_balance: data[1].result.unlocked_balance,
unlocked_balance: data[1].result.unlocked_balance
// num_unspent_outputs: data[1].result.num_unspent_outputs
},
address_list: {
@ -803,7 +752,6 @@ export class WalletRPC {
}
for (let address of data[0].result.addresses) {
address.balance = null
address.unlocked_balance = null
address.num_unspent_outputs = null
@ -846,14 +794,10 @@ export class WalletRPC {
} else {
resolve(wallet)
}
})
})
}
getTransactions () {
return new Promise((resolve, reject) => {
this.sendRPC("get_transfers", {in: true, out: true, pending: true, failed: true, pool: true}).then((data) => {
@ -863,20 +807,15 @@ export class WalletRPC {
}
let wallet = {
transactions: {
tx_list: [],
tx_list: []
}
}
if(data.result.hasOwnProperty("in"))
wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.in)
if(data.result.hasOwnProperty("out"))
wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.out)
if(data.result.hasOwnProperty("pending"))
wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.pending)
if(data.result.hasOwnProperty("failed"))
wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.failed)
if(data.result.hasOwnProperty("pool"))
wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.pool)
if (data.result.hasOwnProperty("in")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.in) }
if (data.result.hasOwnProperty("out")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.out) }
if (data.result.hasOwnProperty("pending")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.pending) }
if (data.result.hasOwnProperty("failed")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.failed) }
if (data.result.hasOwnProperty("pool")) { wallet.transactions.tx_list = wallet.transactions.tx_list.concat(data.result.pool) }
for (let i = 0; i < wallet.transactions.tx_list.length; i++) {
if (/^0*$/.test(wallet.transactions.tx_list[i].payment_id)) {
@ -896,7 +835,6 @@ export class WalletRPC {
})
}
getAddressBook () {
return new Promise((resolve, reject) => {
this.sendRPC("get_address_book").then((data) => {
@ -917,7 +855,7 @@ export class WalletRPC {
let entry = data.result.entries[i]
let desc = entry.description.split("::")
if (desc.length == 3) {
entry.starred = desc[0] == "starred" ? true : false
entry.starred = desc[0] == "starred"
entry.name = desc[1]
entry.description = desc[2]
} else if (desc.length == 2) {
@ -936,15 +874,11 @@ export class WalletRPC {
entry.payment_id = entry.payment_id.substring(0, 16)
}
if(entry.starred)
wallet.address_list.address_book_starred.push(entry)
else
wallet.address_list.address_book.push(entry)
if (entry.starred) { wallet.address_list.address_book_starred.push(entry) } else { wallet.address_list.address_book.push(entry) }
}
}
resolve(wallet)
})
})
}
@ -961,9 +895,7 @@ export class WalletRPC {
}
}
addAddressBook (address, payment_id = null, description = "", name = "", starred = false, index = false) {
if (index !== false) {
this.sendRPC("delete_address_book", {index: index}).then((data) => {
this.addAddressBook(address, payment_id, description, name, starred)
@ -974,8 +906,7 @@ export class WalletRPC {
let params = {
address
}
if(payment_id != null)
params.payment_id = payment_id
if (payment_id != null) { params.payment_id = payment_id }
let desc = [
]
@ -1003,7 +934,6 @@ export class WalletRPC {
})
}
exportKeyImages (password, filename = null) {
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
if (err) {
@ -1015,10 +945,7 @@ export class WalletRPC {
return
}
if(filename == null)
filename = path.join(this.data_dir, "gui", "key_image_export")
else
filename = path.join(filename, "key_image_export")
if (filename == null) { filename = path.join(this.data_dir, "gui", "key_image_export") } else { filename = path.join(filename, "key_image_export") }
this.sendRPC("export_key_images", {filename}).then((data) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
@ -1027,10 +954,8 @@ export class WalletRPC {
}
this.sendGateway("show_notification", {message: "Key images exported to " + filename, timeout: 2000})
})
})
}
importKeyImages (password, filename = null) {
@ -1044,8 +969,7 @@ export class WalletRPC {
return
}
if(filename == null)
filename = path.join(this.data_dir, "gui", "key_image_export")
if (filename == null) { filename = path.join(this.data_dir, "gui", "key_image_export") }
this.sendRPC("import_key_images", {filename}).then((data) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
@ -1056,14 +980,11 @@ export class WalletRPC {
this.sendGateway("show_notification", {message: "Key images imported", timeout: 2000})
})
})
}
listWallets (legacy = false) {
let wallets = {
list: [],
list: []
}
fs.readdirSync(this.wallet_dir).forEach(filename => {
@ -1071,8 +992,7 @@ export class WalletRPC {
filename.endsWith(".meta.json") ||
filename.endsWith(".address.txt") ||
filename.endsWith(".bkp-old") ||
filename.endsWith(".unportable"))
return
filename.endsWith(".unportable")) { return }
switch (filename) {
case ".DS_Store":
@ -1092,14 +1012,12 @@ export class WalletRPC {
}
if (fs.existsSync(path.join(this.wallet_dir, filename + ".meta.json"))) {
let meta = fs.readFileSync(path.join(this.wallet_dir, filename + ".meta.json"), "utf8")
if (meta) {
meta = JSON.parse(meta)
wallet_data.address = meta.address
wallet_data.password_protected = meta.password_protected
}
} else if (fs.existsSync(path.join(this.wallet_dir, filename + ".address.txt"))) {
let address = fs.readFileSync(path.join(this.wallet_dir, filename + ".address.txt"), "utf8")
if (address) {
@ -1108,7 +1026,6 @@ export class WalletRPC {
}
wallets.list.push(wallet_data)
})
// Check for legacy wallet files
@ -1116,31 +1033,28 @@ export class WalletRPC {
wallets.legacy = []
let legacy_paths = []
if (os.platform() == "win32") {
legacy_paths = ["C:\\ProgramData\\RyoGUIWallet", "C:\\ProgramData\\RyoLITEWallet"]
legacy_paths = ["C:\\ProgramData\\Loki"]
} else {
legacy_paths = [path.join(os.homedir(), "RyoGUIWallet"), path.join(os.homedir(), "RyoLITEWallet")]
legacy_paths = [path.join(os.homedir(), "Loki")]
}
for (var i = 0; i < legacy_paths.length; i++) {
let legacy_config_path = path.join(legacy_paths[i], "config", "wallet_info.json")
if(this.testnet)
legacy_config_path = path.join(legacy_paths[i], "testnet", "config", "wallet_info.json")
if(!fs.existsSync(legacy_config_path))
continue
if (this.net_type === "test") { legacy_config_path = path.join(legacy_paths[i], "testnet", "config", "wallet_info.json") }
if (!fs.existsSync(legacy_config_path)) { continue }
let legacy_config = JSON.parse(fs.readFileSync(legacy_config_path, "utf8"))
let legacy_wallet_path = legacy_config.wallet_filepath
if(!fs.existsSync(legacy_wallet_path))
continue
if (!fs.existsSync(legacy_wallet_path)) { continue }
let legacy_address = ""
if (fs.existsSync(legacy_wallet_path + ".address.txt")) {
legacy_address = fs.readFileSync(legacy_wallet_path + ".address.txt", "utf8")
}
wallets.legacy.push({path: legacy_wallet_path, address: legacy_address})
}
}
this.sendGateway("wallet_list", wallets)
}
changeWalletPassword (old_password, new_password) {
@ -1164,9 +1078,7 @@ export class WalletRPC {
this.wallet_state.password_hash = crypto.pbkdf2Sync(new_password, this.auth[2], 1000, 64, "sha512").toString("hex")
this.sendGateway("show_notification", {message: "Password updated", timeout: 2000})
})
})
}
@ -1223,8 +1135,7 @@ export class WalletRPC {
// if wallet is closed, do not send any wallet data to gateway
// this is for the case that we close the wallet at the same
// after another action has started, but before it has finished
if(!this.wallet_state.open && method == "set_wallet_data")
return
if (!this.wallet_state.open && method == "set_wallet_data") { return }
this.backend.send(method, data)
}
@ -1244,7 +1155,7 @@ export class WalletRPC {
sendImmediately: false
},
agent: this.agent
};
}
if (Object.keys(params).length !== 0) {
options.json.params = params
}

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) {
// 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) {
// 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) {
// 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) {
// 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) {
this.app = app
this.router = router
this.token = null
@ -19,7 +16,7 @@ export class Gateway {
theme
}
}
});
})
this.app.store.watch(state => state.gateway.app.config.appearance.theme, (theme) => {
LocalStorage.set("theme", theme)
})
@ -30,21 +27,20 @@ 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 () {
@ -52,8 +48,8 @@ export class Gateway {
status: {
code: 2 // Loading config
}
});
this.send("core", "init");
})
this.send("core", "init")
}
confirmClose (msg) {
@ -80,7 +76,6 @@ export class Gateway {
}).catch(() => {
this.closeDialog = false
})
}
send (module, method, data = {}) {
@ -89,24 +84,20 @@ export class Gateway {
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) {
// 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
@ -147,9 +138,8 @@ export class Gateway {
// short delay to prevent wallet data reaching the
// websocket moments after we close and reset data
this.app.store.dispatch("gateway/resetWalletData")
}, 250);
}, 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,18 +74,6 @@
/>
</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"
@ -93,9 +81,6 @@
:options="priorityOptions"
/>
</q-field>
</div>
</div>
<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

@ -13,7 +13,7 @@ export default [
path: "/quit",
component: () =>
import("pages/init/quit")
},
}
]
},
{
@ -105,6 +105,11 @@ export default [
component: () =>
import("pages/wallet/txhistory")
},
{
path: "servicenode",
component: () =>
import("pages/wallet/service-node")
}
]
},

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

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

@ -8,8 +8,11 @@ export const privkey = (input) => {
return input.length === 0 || (/^[0-9A-Fa-f]+$/.test(input) && input.length == 64)
}
export const address = (input) => {
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
switch (input.substring(0, 4)) {