oxen-electron-gui-wallet/src-electron/main-process/modules/backend.js
Mikunj bcf21c3804 Redesign
Main screen redesign

Removed dark mode styling and made it all dark.

Fix large button styling on navigation

Receive page styling

Startup pages redesign

Updating field stylings.
Fix value display in recieve

Updated footer.

Added service node page.

Added wallet settings.

Added disable prop to loki field.

Update settings page.
Added merging config with default daemon option incase user provides invalid port (empty, null, etc...)

Removed theme selection

Update wallet-select pages

Fixed converting numbers to string

Update layout on address page

Added loki logo.
Made header a bit smaller.

Updated wallet init styling.
Highlight primary address in receive.

updated packages.

Updated transaction styling.

Simpler tx json handling.

Added address validation

Fixed up wallet restoration

Default node to remote.
Added drop down button to the remote node input instead of having it as a seperate field.

Removed review page.
Center align welcome page.

Replaced ryo wallet images with loki image.

Updated transaction styling.

Fix wallet errors only showing once which causes the next error to just show the loading overlay.

Added staking

Fix up status display in footer.
remove is_ready as lokid doesn't return it.

Fixed balance display in receive.
Center unlock in wallet details.

Updated README
other updates.
2019-03-13 15:38:34 +11:00

457 lines
15 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")
const path = require("path")
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.config_file = null
this.config_data = {}
this.scee = new SCEE()
}
init (config) {
if (os.platform() === "win32") {
this.config_dir = "C:\\ProgramData\\loki-wallet"
} else {
this.config_dir = path.join(os.homedir(), ".loki-wallet")
}
if (!fs.existsSync(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"))
}
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 = {
main: {
...daemon,
remote_host: "doopool.xyz",
remote_port: 22020
},
staging: {
...daemon,
type: "local",
p2p_bind_port: 38153,
rpc_bind_port: 38154,
zmq_rpc_bind_port: 38155
},
test: {
...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,
ws_bind_port: 12213,
net_type: "main"
},
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: "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({
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 "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":
if (params.type == "tx") {
require("electron").shell.openExternal("https://lokiblocks.com/tx/" + 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 = new Buffer(base64Data, "base64").toString("binary")
fs.writeFile(filename, binaryData, "binary", (err) => {
if (err) { this.send("show_notification", {type: "negative", message: "Error saving " + params.type, timeout: 2000}) } else { this.send("show_notification", {message: params.type + " saved to " + filename, timeout: 2000}) }
})
}
break
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
})
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")
}
// 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.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
}
})
}).catch(error => {
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
})
})
}).catch(error => {
if (this.config_data.daemons[net_type].type == "remote") {
this.send("show_notification", {type: "negative", message: "Remote daemon cannot be reached", timeout: 2000})
} else {
this.send("show_notification", {type: "negative", message: "Local daemon internal error", timeout: 2000})
}
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
})
})
}).catch(error => {
this.send("set_app_data", {
status: {
code: -1 // Return to config screen
}
})
})
})
}
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
}
}