oxen-electron-gui-wallet/src-electron/main-process/modules/backend.js

585 lines
20 KiB
JavaScript

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-extra")
const path = require("upath")
const objectAssignDeep = require("object-assign-deep")
export class Backend {
constructor (mainWindow) {
this.mainWindow = mainWindow
this.daemon = null
this.walletd = null
this.wss = null
this.token = null
this.config_dir = null
this.wallet_dir = null
this.config_file = null
this.config_data = {}
this.scee = new SCEE()
}
init (config) {
if (os.platform() === "win32") {
this.config_dir = "C:\\ProgramData\\loki"
this.wallet_dir = `${os.homedir()}\\Documents\\Loki`
} else {
this.config_dir = path.join(os.homedir(), ".loki")
this.wallet_dir = path.join(os.homedir(), "Loki")
}
if (!fs.existsSync(this.config_dir)) {
fs.mkdirpSync(this.config_dir)
}
if (!fs.existsSync(path.join(this.config_dir, "gui"))) {
fs.mkdirpSync(path.join(this.config_dir, "gui"))
}
this.config_file = path.join(this.config_dir, "gui", "config.json")
const daemon = {
type: "remote",
p2p_bind_ip: "0.0.0.0",
p2p_bind_port: 22022,
rpc_bind_ip: "127.0.0.1",
rpc_bind_port: 22023,
zmq_rpc_bind_ip: "127.0.0.1",
zmq_rpc_bind_port: 22024,
out_peers: -1,
in_peers: -1,
limit_rate_up: -1,
limit_rate_down: -1,
log_level: 0
}
const daemons = {
mainnet: {
...daemon,
remote_host: "doopool.xyz",
remote_port: 22020
},
stagenet: {
...daemon,
type: "local",
p2p_bind_port: 38153,
rpc_bind_port: 38154,
zmq_rpc_bind_port: 38155
},
testnet: {
...daemon,
type: "local",
p2p_bind_port: 38156,
rpc_bind_port: 38157,
zmq_rpc_bind_port: 38158
}
}
// Default values
this.defaults = {
daemons: objectAssignDeep({}, daemons),
app: {
data_dir: this.config_dir,
wallet_data_dir: this.wallet_dir,
ws_bind_port: 12313,
net_type: "mainnet"
},
wallet: {
rpc_bind_port: 18082,
log_level: 0
}
}
this.config_data = {
// Copy all the properties of defaults
...objectAssignDeep({}, this.defaults),
appearance: {
theme: "dark"
}
}
this.remotes = [
{
host: "doopool.xyz",
port: "22020"
},
{
host: "daemons.cryptopool.space",
port: "22023"
},
{
host: "node.loki-pool.com",
port: "18081"
},
{
host: "imaginary.stream",
port: "22023"
},
{
host: "nodes.hashvault.pro",
port: "22023"
},
{
host: "rpc.stakeit.io",
port: "22023"
}
]
this.token = config.token
this.wss = new WebSocket.Server({
port: config.port,
maxPayload: Number.POSITIVE_INFINITY
})
this.wss.on("connection", ws => {
ws.on("message", data => this.receive(data))
})
}
send (event, data = {}) {
let message = {
event,
data
}
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))
// 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
}
}
handle (data) {
let params = data.data
switch (data.method) {
case "set_language":
this.send("set_language", { lang: params.lang })
break
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
})
})
break
case "save_config":
// check if config has changed
let config_changed = false
Object.keys(this.config_data).map(i => {
if (i == "appearance") return
Object.keys(this.config_data[i]).map(j => {
if (this.config_data[i][j] !== params[i][j]) { config_changed = true }
})
})
case "save_config_init":
Object.keys(params).map(key => {
this.config_data[key] = Object.assign(this.config_data[key], params[key])
})
const validated = Object.keys(this.defaults)
.filter(k => k in this.config_data)
.map(k => [k, this.validate_values(this.config_data[k], this.defaults[k])])
.reduce((map, obj) => {
map[obj[0]] = obj[1]
return map
}, {})
// Validate deamon data
this.config_data = {
...this.config_data,
...validated
}
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => {
if (data.method == "save_config_init") {
this.startup()
} else {
this.send("set_app_data", {
config: this.config_data,
pending_config: this.config_data
})
if (config_changed) {
this.send("settings_changed_reboot")
}
}
})
break
case "init":
this.startup()
break
case "open_explorer":
const { net_type } = this.config_data.app
let path = null
if (params.type === "tx") {
path = "tx"
} else if (params.type === "service_node") {
path = "service_node"
}
if (path) {
const baseUrl = net_type === "testnet" ? "https://lokitestnet.com" : "https://lokiblocks.com"
const url = `${baseUrl}/${path}/`
require("electron").shell.openExternal(url + params.id)
}
break
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 = Buffer.from(base64Data, "base64").toString("binary")
fs.writeFile(filename, binaryData, "binary", (err) => {
if (err) {
this.send("show_notification", {
type: "negative",
i18n: ["notification.errors.errorSavingItem", { item: params.type }],
timeout: 2000
})
} else {
this.send("show_notification", {
i18n: ["notification.positive.itemSaved", { item: params.type, filename }],
timeout: 2000
})
}
})
}
break
default:
}
}
startup () {
this.send("set_app_data", {
remotes: this.remotes,
defaults: this.defaults
})
fs.readFile(this.config_file, "utf8", (err, data) => {
if (err) {
this.send("set_app_data", {
status: {
code: -1 // Config not found
},
config: this.config_data,
pending_config: this.config_data
})
return
}
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] = {} }
this.config_data[key] = Object.assign(this.config_data[key], disk_config_data[key])
})
// here we may want to check if config data is valid, if not also send code -1
// i.e. check ports are integers and > 1024, check that data dir path exists, etc
const validated = Object.keys(this.defaults)
.filter(k => k in this.config_data)
.map(k => [k, this.validate_values(this.config_data[k], this.defaults[k])])
.reduce((map, obj) => {
map[obj[0]] = obj[1]
return map
}, {})
// Make sure the daemon data is valid
this.config_data = {
...this.config_data,
...validated
}
// save config file back to file, so updated options are stored on disk
fs.writeFile(this.config_file, JSON.stringify(this.config_data, null, 4), "utf8", () => {})
this.send("set_app_data", {
config: this.config_data,
pending_config: this.config_data
})
// Make the wallet dir
const { wallet_data_dir, data_dir } = this.config_data.app
if (!fs.existsSync(wallet_data_dir)) { fs.mkdirpSync(wallet_data_dir) }
// Check to see if data and wallet directories exist
const dirs_to_check = [{
path: data_dir,
error: "notification.errors.dataPathNotFound"
},
{
path: wallet_data_dir,
error: "notification.errors.walletPathNotFound"
}]
for (const dir of dirs_to_check) {
// Check to see if dir exists
if (!fs.existsSync(dir.path)) {
this.send("show_notification", {
type: "negative",
i18n: dir.error,
timeout: 2000
})
// Go back to config
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
})
return
}
}
const { net_type } = this.config_data.app
const dirs = {
"mainnet": this.config_data.app.data_dir,
"stagenet": path.join(this.config_data.app.data_dir, "stagenet"),
"testnet": path.join(this.config_data.app.data_dir, "testnet")
}
// Make sure we have the directories we need
const net_dir = dirs[net_type]
if (!fs.existsSync(net_dir)) { fs.mkdirpSync(net_dir) }
const log_dir = path.join(net_dir, "logs")
if (!fs.existsSync(log_dir)) { fs.mkdirpSync(log_dir) }
this.daemon = new Daemon(this)
this.walletd = new WalletRPC(this)
this.send("set_app_data", {
status: {
code: 3 // Starting daemon
}
})
// Make sure the remote node provided is accessible
const config_daemon = this.config_data.daemons[net_type]
this.daemon.checkRemote(config_daemon).then(data => {
if (data.error) {
// If we can default to local then we do so, otherwise we tell the user to re-set the node
if (config_daemon.type === "local_remote") {
this.config_data.daemons[net_type].type = "local"
this.send("set_app_data", {
config: this.config_data,
pending_config: this.config_data
})
this.send("show_notification", {
type: "warning",
textColor: "black",
i18n: "notification.warnings.usingLocalNode",
timeout: 2000
})
} else {
this.send("show_notification", {
type: "negative",
i18n: "notification.errors.cannotAccessRemoteNode",
timeout: 2000
})
// Go back to config
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
})
return
}
}
// If we got a net type back then check if ours match
if (data.net_type && data.net_type !== net_type) {
this.send("show_notification", {
type: "negative",
i18n: "notification.errors.differentNetType",
timeout: 2000
})
// Go back to config
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
})
return
}
this.daemon.checkVersion().then((version) => {
if (version) {
this.send("set_app_data", {
status: {
code: 4,
message: version
}
})
} else {
// daemon not found, probably removed by AV, set to remote node
this.config_data.daemons[net_type].type = "remote"
this.send("set_app_data", {
status: {
code: 5
},
config: this.config_data,
pending_config: this.config_data
})
}
this.daemon.start(this.config_data).then(() => {
this.send("set_app_data", {
status: {
code: 6 // Starting wallet
}
})
this.walletd.start(this.config_data).then(() => {
this.send("set_app_data", {
status: {
code: 7 // Reading wallet list
}
})
this.walletd.listWallets(true)
this.send("set_app_data", {
status: {
code: 0 // Ready
}
})
// eslint-disable-next-line
}).catch(error => {
this.daemon.killProcess()
this.send("show_notification", { type: "negative", message: error.message, timeout: 3000 })
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
})
})
// eslint-disable-next-line
}).catch(error => {
if (this.config_data.daemons[net_type].type == "remote") {
this.send("show_notification", {
type: "negative",
i18n: "notification.errors.remoteCannotBeReached",
timeout: 3000
})
} else {
this.send("show_notification", {
type: "negative",
message: error.message,
timeout: 3000
})
}
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
})
})
// eslint-disable-next-line
}).catch(error => {
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
})
})
})
})
}
quit () {
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() }
Promise.all(process).then(() => {
resolve()
})
})
}
// Replace any invalid value with default values
validate_values (values, defaults) {
const isDictionary = (v) => typeof v === "object" && v !== null && !(v instanceof Array) && !(v instanceof Date)
const modified = { ...values }
// Make sure we have valid defaults
if (!isDictionary(defaults)) return modified
for (const key in modified) {
// Only modify if we have a default
if (!(key in defaults)) continue
const defaultValue = defaults[key]
const invalidDefault = defaultValue === null || defaultValue === undefined || Number.isNaN(defaultValue)
if (invalidDefault) continue
const value = modified[key]
// If we have a object then recurse through it
if (isDictionary(value)) {
modified[key] = this.validate_values(value, defaultValue)
} else {
// Check if we need to replace the value
const isValidValue = !(value === undefined || value === null || value === "" || Number.isNaN(value))
if (isValidValue) continue
// Otherwise set the default value
modified[key] = defaultValue
}
}
return modified
}
}