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

1220 lines
46 KiB
JavaScript

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) {
this.backend = backend
this.data_dir = null
this.wallet_dir = null
this.auth = []
this.id = 0
this.net_type = "main"
this.heartbeat = null
this.wallet_state = {
open: false,
name: "",
password_hash: null,
balance: null,
unlocked_balance: null
}
this.last_height_send_time = Date.now()
this.height_regex1 = /Processed block: <([a-f0-9]+)>, height (\d+)/
this.height_regex2 = /Skipped block by height: (\d+)/
this.height_regex3 = /Skipped block by timestamp, height: (\d+)/
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 = `${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) => {
if (err) throw err
let auth = buffer.toString("hex")
this.auth = [
auth.substr(0, 64), // rpc username
auth.substr(64, 64), // rpc password
auth.substr(128, 32) // password salt
]
const args = [
"--rpc-login", this.auth[0] + ":" + this.auth[1],
"--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"
]
const { net_type } = options.app
this.net_type = net_type
this.data_dir = options.app.data_dir
const dirs = {
"main": this.data_dir,
"staging": path.join(this.data_dir, "staging"),
"test": path.join(this.data_dir, "testnet")
}
this.wallet_dir = path.join(dirs[net_type], "wallets")
args.push("--wallet-dir", 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, "loki-wallet-rpc.exe"), args)
} else {
this.walletRPCProcess = child_process.spawn(path.join(__ryo_bin, "loki-wallet-rpc"), args, {
detached: true
})
}
// save this info for later RPC calls
this.protocol = "http://"
this.hostname = "127.0.0.1"
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 match, height = null
lines.forEach((line) => {
match = line.match(this.height_regex1)
if (match) {
height = match[2]
} else {
match = line.match(this.height_regex2)
if (match) {
height = match[1]
} else {
match = line.match(this.height_regex3)
if (match) {
height = match[1]
}
}
}
})
if (height && Date.now() - this.last_height_send_time > 1000) {
this.last_height_send_time = Date.now()
this.sendGateway("set_wallet_data", {
info: {
height
}
})
}
})
this.walletRPCProcess.on("error", err => process.stderr.write(`Wallet: ${err}`))
this.walletRPCProcess.on("close", code => process.stderr.write(`Wallet: exited with code ${code}`))
// To let caller know when the wallet is ready
let intrvl = setInterval(() => {
this.sendRPC("get_languages").then((data) => {
if (!data.hasOwnProperty("error")) {
clearInterval(intrvl)
resolve()
} else {
if (data.error.cause &&
data.error.cause.code === "ECONNREFUSED") {
// Ignore
} else {
clearInterval(intrvl)
reject(error)
}
}
})
}, 1000)
})
})
}
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)
break
case "restore_wallet":
this.restoreWallet(params.name, params.password, params.seed,
params.refresh_type, params.refresh_type == "date" ? params.refresh_start_date : params.refresh_start_height)
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
case "import_wallet":
this.importWallet(params.name, params.password, params.path)
break
case "open_wallet":
this.openWallet(params.name, params.password)
break
case "close_wallet":
this.closeWallet()
break
case "transfer":
this.transfer(params.password, params.amount, params.address, params.payment_id, params.priority, params.address_book)
break
case "add_address_book":
this.addAddressBook(params.address, params.payment_id,
params.description, params.name, params.starred,
params.hasOwnProperty("index") ? params.index : false
)
break
case "delete_address_book":
this.deleteAddressBook(params.hasOwnProperty("index") ? params.index : false)
break
case "save_tx_notes":
this.saveTxNotes(params.txid, params.note)
break
case "rescan_blockchain":
this.rescanBlockchain()
break
case "rescan_spent":
this.rescanSpent()
break
case "get_private_keys":
this.getPrivateKeys(params.password)
break
case "export_key_images":
this.exportKeyImages(params.password, params.path)
break
case "import_key_images":
this.importKeyImages(params.password, params.path)
break
case "change_wallet_password":
this.changeWalletPassword(params.old_password, params.new_password)
break
case "delete_wallet":
this.deleteWallet(params.password)
break
default:
}
}
createWallet (filename, password, language) {
this.sendRPC("create_wallet", {
filename,
password,
language
}).then((data) => {
if (data.hasOwnProperty("error")) {
this.sendGateway("set_wallet_error", {status: data.error})
return
}
// store hash of the password so we can check against it later when requesting private keys, or for sending txs
this.wallet_state.password_hash = crypto.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512").toString("hex")
this.wallet_state.name = filename
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
let timestamp = refresh_start_timestamp_or_height
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) }
})
return
}
let restore_height = refresh_start_timestamp_or_height
if (!Number.isInteger(restore_height)) {
restore_height = 0
}
seed = seed.trim().replace(/\s{2,}/g, " ")
this.sendRPC("restore_deterministic_wallet", {
filename,
password,
seed,
restore_height
}).then((data) => {
if (data.hasOwnProperty("error")) {
this.sendGateway("set_wallet_error", {status: data.error})
return
}
// restore wallet rpc does not automatically open the wallet after restoring
// ^ above behavior is now fixed, no need to open wallet manually
// this.sendRPC("open_wallet", {
// filename,
// password
// }).then((data) => {
// if(data.hasOwnProperty("error")) {
// this.sendGateway("set_wallet_error", {status:data.error})
// return
// }
// store hash of the password so we can check against it later when requesting private keys, or for sending txs
this.wallet_state.password_hash = crypto.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512").toString("hex")
this.wallet_state.name = filename
this.wallet_state.open = true
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
let timestamp = refresh_start_timestamp_or_height
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) }
})
return
}
let refresh_start_height = refresh_start_timestamp_or_height
if (!Number.isInteger(refresh_start_height)) {
refresh_start_height = 0
}
this.sendRPC("restore_view_wallet", {
filename,
password,
address,
viewkey,
refresh_start_height
}).then((data) => {
if (data.hasOwnProperty("error")) {
this.sendGateway("set_wallet_error", {status: data.error})
return
}
// store hash of the password so we can check against it later when requesting private keys, or for sending txs
this.wallet_state.password_hash = crypto.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512").toString("hex")
this.wallet_state.name = filename
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)
} else if (import_path.endsWith(".address.txt")) {
import_path = import_path.substring(0, import_path.length - ".address.txt".length)
}
if (!fs.existsSync(import_path)) {
this.sendGateway("set_wallet_error", {status: {code: -1, message: "Invalid wallet path"}})
} else {
let destination = path.join(this.wallet_dir, filename)
if (fs.existsSync(destination) || fs.existsSync(destination + ".keys")) {
this.sendGateway("set_wallet_error", {status: {code: -1, message: "Wallet with name already exists"}})
return
}
fs.copyFileSync(import_path, destination, fs.constants.COPYFILE_EXCL)
if (fs.existsSync(import_path + ".keys")) {
fs.copyFileSync(import_path + ".keys", destination + ".keys", fs.constants.COPYFILE_EXCL)
}
this.sendRPC("open_wallet", {
filename,
password
}).then((data) => {
if (data.hasOwnProperty("error")) {
fs.unlinkSync(destination)
fs.unlinkSync(destination + ".keys")
this.sendGateway("set_wallet_error", {status: data.error})
return
}
// store hash of the password so we can check against it later when requesting private keys, or for sending txs
this.wallet_state.password_hash = crypto.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512").toString("hex")
this.wallet_state.name = filename
this.wallet_state.open = true
this.finalizeNewWallet(filename)
})
}
}
finalizeNewWallet (filename) {
Promise.all([
this.sendRPC("get_address"),
this.sendRPC("getheight"),
this.sendRPC("getbalance", {account_index: 0}),
this.sendRPC("query_key", {key_type: "mnemonic"}),
this.sendRPC("query_key", {key_type: "spend_key"}),
this.sendRPC("query_key", {key_type: "view_key"})
]).then((data) => {
let wallet = {
info: {
name: filename,
address: "",
balance: 0,
unlocked_balance: 0,
height: 0,
view_only: false
},
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
}
for (let n of data) {
if (n.hasOwnProperty("error") || !n.hasOwnProperty("result")) {
continue
}
if (n.method == "get_address") {
wallet.info.address = n.result.address
} else if (n.method == "getheight") {
wallet.info.height = n.result.height
} else if (n.method == "getbalance") {
wallet.info.balance = n.result.balance
wallet.info.unlocked_balance = n.result.unlocked_balance
} else if (n.method == "query_key") {
wallet.secret[n.params.key_type] = n.result.key
if (n.params.key_type == "spend_key") {
if (/^0*$/.test(n.result.key)) {
wallet.info.view_only = true
}
}
}
}
this.saveWallet().then(() => {
let address_txt_path = path.join(this.wallet_dir, filename + ".address.txt")
if (!fs.existsSync(address_txt_path)) {
fs.writeFile(address_txt_path, wallet.info.address, "utf8", () => {
this.listWallets()
})
} else {
this.listWallets()
}
})
this.sendGateway("set_wallet_data", wallet)
this.startHeartbeat()
})
}
openWallet (filename, password) {
this.sendRPC("open_wallet", {
filename,
password
}).then((data) => {
if (data.hasOwnProperty("error")) {
this.sendGateway("set_wallet_error", {status: data.error})
return
}
let address_txt_path = path.join(this.wallet_dir, filename + ".address.txt")
if (!fs.existsSync(address_txt_path)) {
this.sendRPC("get_address", {account_index: 0}).then((data) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
return
}
fs.writeFile(address_txt_path, data.result.address, "utf8", () => {
this.listWallets()
})
})
}
// store hash of the password so we can check against it later when requesting private keys, or for sending txs
this.wallet_state.password_hash = crypto.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512").toString("hex")
this.wallet_state.name = filename
this.wallet_state.open = true
this.startHeartbeat()
// Check if we have a view only wallet by querying the spend key
this.sendRPC("query_key", {key_type: "spend_key"}).then((data) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
return
}
if (/^0*$/.test(data.result.key)) {
this.sendGateway("set_wallet_data", {
info: {
view_only: true
}
})
}
})
})
}
startHeartbeat () {
clearInterval(this.heartbeat)
this.heartbeat = setInterval(() => {
this.heartbeatAction()
}, 5000)
this.heartbeatAction(true)
}
heartbeatAction (extended = false) {
Promise.all([
this.sendRPC("getheight", {}, 5000),
this.sendRPC("getbalance", {account_index: 0}, 5000)
]).then((data) => {
let wallet = {
status: {
code: 0,
message: "OK"
},
info: {
name: this.wallet_state.name
},
transactions: {
tx_list: []
},
address_list: {
primary: [],
used: [],
unused: [],
address_book: [],
address_book_starred: []
}
}
for (let n of data) {
if (n.hasOwnProperty("error") || !n.hasOwnProperty("result")) {
continue
}
if (n.method == "getheight") {
wallet.info.height = n.result.height
this.sendGateway("set_wallet_data", {
info: {
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) {
// continue
}
this.wallet_state.balance = wallet.info.balance = n.result.balance
this.wallet_state.unlocked_balance = wallet.info.unlocked_balance = n.result.unlocked_balance
this.sendGateway("set_wallet_data", {
info: wallet.info
})
// if balance has recently changed, get updated list of transactions and used addresses
let actions = [
this.getTransactions(),
this.getAddressList()
]
if (true || extended) {
actions.push(this.getAddressBook())
}
Promise.all(actions).then((data) => {
for (let n of data) {
Object.keys(n).map(key => {
wallet[key] = Object.assign(wallet[key], n[key])
})
}
this.sendGateway("set_wallet_data", wallet)
})
}
}
})
}
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", {
code: -1,
message: "Internal error",
sending: false
})
return
}
if (this.wallet_state.password_hash !== password_hash.toString("hex")) {
this.sendGateway("set_tx_status", {
code: -1,
message: "Invalid password",
sending: false
})
return
}
amount = parseFloat(amount).toFixed(9) * 1e9
let sweep_all = amount == this.wallet_state.unlocked_balance
if (sweep_all) {
let params = {
"address": address,
"account_index": 0,
"priority": priority,
"mixin": 9 // Always force a ring size of 10 (ringsize = mixin + 1)
}
if (payment_id) {
params.payment_id = payment_id
}
this.sendRPC("sweep_all", params).then((data) => {
if (data.hasOwnProperty("error")) {
let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1)
this.sendGateway("set_tx_status", {
code: -1,
message: error,
sending: false
})
return
}
this.sendGateway("set_tx_status", {
code: 0,
message: "Transaction successfully sent",
sending: false
})
})
} else {
let params = {
"destinations": [{"amount": amount, "address": address}],
"priority": priority,
"mixin": 9
}
if (payment_id) {
params.payment_id = payment_id
}
this.sendRPC("transfer_split", params).then((data) => {
if (data.hasOwnProperty("error")) {
let error = data.error.message.charAt(0).toUpperCase() + data.error.message.slice(1)
this.sendGateway("set_tx_status", {
code: -1,
message: error,
sending: false
})
return
}
this.sendGateway("set_tx_status", {
code: 0,
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) }
})
}
rescanBlockchain () {
this.sendRPC("rescan_blockchain")
}
rescanSpent () {
this.sendRPC("rescan_spent")
}
getPrivateKeys (password) {
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
if (err) {
this.sendGateway("set_wallet_data", {
secret: {
mnemonic: "Internal error",
spend_key: -1,
view_key: -1
}
})
return
}
if (this.wallet_state.password_hash !== password_hash.toString("hex")) {
this.sendGateway("set_wallet_data", {
secret: {
mnemonic: "Invalid password",
spend_key: -1,
view_key: -1
}
})
return
}
Promise.all([
this.sendRPC("query_key", {key_type: "mnemonic"}),
this.sendRPC("query_key", {key_type: "spend_key"}),
this.sendRPC("query_key", {key_type: "view_key"})
]).then((data) => {
let wallet = {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
}
for (let n of data) {
if (n.hasOwnProperty("error") || !n.hasOwnProperty("result")) {
continue
}
wallet.secret[n.params.key_type] = n.result.key
}
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({})
return
}
}
let num_unused_addresses = 10
let wallet = {
info: {
address: data[0].result.address,
balance: data[1].result.balance,
unlocked_balance: data[1].result.unlocked_balance
// num_unspent_outputs: data[1].result.num_unspent_outputs
},
address_list: {
primary: [],
used: [],
unused: []
}
}
for (let address of data[0].result.addresses) {
address.balance = null
address.unlocked_balance = null
address.num_unspent_outputs = null
if (data[1].result.hasOwnProperty("per_subaddress")) {
for (let address_balance of data[1].result.per_subaddress) {
if (address_balance.address_index == address.address_index) {
address.balance = address_balance.balance
address.unlocked_balance = address_balance.unlocked_balance
address.num_unspent_outputs = address_balance.num_unspent_outputs
break
}
}
}
if (address.address_index == 0) {
wallet.address_list.primary.push(address)
} else if (address.used) {
wallet.address_list.used.push(address)
} else {
wallet.address_list.unused.push(address)
}
}
// limit to 10 unused addresses
wallet.address_list.unused = wallet.address_list.unused.slice(0, 10)
if (wallet.address_list.unused.length < num_unused_addresses &&
!wallet.address_list.primary[0].address.startsWith("RYoK") &&
!wallet.address_list.primary[0].address.startsWith("RYoH")) {
for (let n = wallet.address_list.unused.length; n < num_unused_addresses; n++) {
this.sendRPC("create_address", {account_index: 0}).then((data) => {
wallet.address_list.unused.push(data.result)
if (wallet.address_list.unused.length == num_unused_addresses) {
// should sort them here
resolve(wallet)
}
})
}
} 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) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
resolve({})
return
}
let wallet = {
transactions: {
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) }
for (let i = 0; i < wallet.transactions.tx_list.length; i++) {
if (/^0*$/.test(wallet.transactions.tx_list[i].payment_id)) {
wallet.transactions.tx_list[i].payment_id = ""
} else if (/^0*$/.test(wallet.transactions.tx_list[i].payment_id.substring(16))) {
wallet.transactions.tx_list[i].payment_id = wallet.transactions.tx_list[i].payment_id.substring(0, 16)
}
}
wallet.transactions.tx_list.sort(function (a, b) {
if (a.timestamp < b.timestamp) return 1
if (a.timestamp > b.timestamp) return -1
return 0
})
resolve(wallet)
})
})
}
getAddressBook () {
return new Promise((resolve, reject) => {
this.sendRPC("get_address_book").then((data) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
resolve({})
return
}
let wallet = {
address_list: {
address_book: [],
address_book_starred: []
}
}
if (data.result.entries) {
let i
for (i = 0; i < data.result.entries.length; i++) {
let entry = data.result.entries[i]
let desc = entry.description.split("::")
if (desc.length == 3) {
entry.starred = desc[0] == "starred"
entry.name = desc[1]
entry.description = desc[2]
} else if (desc.length == 2) {
entry.starred = false
entry.name = desc[0]
entry.description = desc[1]
} else {
entry.starred = false
entry.name = entry.description
entry.description = ""
}
if (/^0*$/.test(entry.payment_id)) {
entry.payment_id = ""
} else if (/^0*$/.test(entry.payment_id.substring(16))) {
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) }
}
}
resolve(wallet)
})
})
}
deleteAddressBook (index = false) {
if (index !== false) {
this.sendRPC("delete_address_book", {index: index}).then(() => {
this.saveWallet().then(() => {
this.getAddressBook().then((data) => {
this.sendGateway("set_wallet_data", data)
})
})
})
}
}
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)
})
return
}
let params = {
address
}
if (payment_id != null) { params.payment_id = payment_id }
let desc = [
]
if (starred) {
desc.push("starred")
}
desc.push(name, description)
params.description = desc.join("::")
this.sendRPC("add_address_book", params).then((data) => {
this.saveWallet().then(() => {
this.getAddressBook().then((data) => {
this.sendGateway("set_wallet_data", data)
})
})
})
}
saveTxNotes (txid, note) {
this.sendRPC("set_tx_notes", {txids: [txid], notes: [note]}).then((data) => {
this.getTransactions().then((wallet) => {
this.sendGateway("set_wallet_data", wallet)
})
})
}
exportKeyImages (password, filename = null) {
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
if (err) {
this.sendGateway("show_notification", {type: "negative", message: "Internal error", timeout: 2000})
return
}
if (this.wallet_state.password_hash !== password_hash.toString("hex")) {
this.sendGateway("show_notification", {type: "negative", message: "Invalid password", timeout: 2000})
return
}
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")) {
this.sendGateway("show_notification", {type: "negative", message: "Error exporting key images", timeout: 2000})
return
}
this.sendGateway("show_notification", {message: "Key images exported to " + filename, timeout: 2000})
})
})
}
importKeyImages (password, filename = null) {
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
if (err) {
this.sendGateway("show_notification", {type: "negative", message: "Internal error", timeout: 2000})
return
}
if (this.wallet_state.password_hash !== password_hash.toString("hex")) {
this.sendGateway("show_notification", {type: "negative", message: "Invalid password", timeout: 2000})
return
}
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")) {
this.sendGateway("show_notification", {type: "negative", message: "Error importing key images", timeout: 2000})
return
}
this.sendGateway("show_notification", {message: "Key images imported", timeout: 2000})
})
})
}
listWallets (legacy = false) {
let wallets = {
list: []
}
fs.readdirSync(this.wallet_dir).forEach(filename => {
if (filename.endsWith(".keys") ||
filename.endsWith(".meta.json") ||
filename.endsWith(".address.txt") ||
filename.endsWith(".bkp-old") ||
filename.endsWith(".unportable")) { return }
switch (filename) {
case ".DS_Store":
case ".DS_Store?":
case "._.DS_Store":
case ".Spotlight-V100":
case ".Trashes":
case "ehthumbs.db":
case "Thumbs.db":
return
}
let wallet_data = {
name: filename,
address: null,
password_protected: null
}
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) {
wallet_data.address = address
}
}
wallets.list.push(wallet_data)
})
// Check for legacy wallet files
if (legacy) {
wallets.legacy = []
let legacy_paths = []
if (os.platform() == "win32") {
legacy_paths = ["C:\\ProgramData\\Loki"]
} else {
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.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 }
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) {
crypto.pbkdf2(old_password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
if (err) {
this.sendGateway("show_notification", {type: "negative", message: "Internal error", timeout: 2000})
return
}
if (this.wallet_state.password_hash !== password_hash.toString("hex")) {
this.sendGateway("show_notification", {type: "negative", message: "Invalid old password", timeout: 2000})
return
}
this.sendRPC("change_wallet_password", {old_password, new_password}).then((data) => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
this.sendGateway("show_notification", {type: "negative", message: "Error changing password", timeout: 2000})
return
}
// store hash of the password so we can check against it later when requesting private keys, or for sending txs
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})
})
})
}
deleteWallet (password) {
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", (err, password_hash) => {
if (err) {
this.sendGateway("show_notification", {type: "negative", message: "Internal error", timeout: 2000})
return
}
if (this.wallet_state.password_hash !== password_hash.toString("hex")) {
this.sendGateway("show_notification", {type: "negative", message: "Invalid password", timeout: 2000})
return
}
let wallet_path = path.join(this.wallet_dir, this.wallet_state.name)
this.closeWallet().then(() => {
fs.unlinkSync(wallet_path)
fs.unlinkSync(wallet_path + ".keys")
fs.unlinkSync(wallet_path + ".address.txt")
this.listWallets()
this.sendGateway("return_to_wallet_select")
})
})
}
saveWallet () {
return new Promise((resolve, reject) => {
this.sendRPC("store").then(() => {
resolve()
})
})
}
closeWallet () {
return new Promise((resolve, reject) => {
clearInterval(this.heartbeat)
this.wallet_state = {
open: false,
name: "",
password_hash: null,
balance: null,
unlocked_balance: null
}
this.saveWallet().then(() => {
this.sendRPC("close_wallet").then(() => {
resolve()
})
})
})
}
sendGateway (method, data) {
// 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 }
this.backend.send(method, data)
}
sendRPC (method, params = {}, timeout = 0) {
let id = this.id++
let options = {
uri: `${this.protocol}${this.hostname}:${this.port}/json_rpc`,
method: "POST",
json: {
jsonrpc: "2.0",
id: id,
method: method
},
auth: {
user: this.auth[0],
pass: this.auth[1],
sendImmediately: false
},
agent: this.agent
}
if (Object.keys(params).length !== 0) {
options.json.params = params
}
if (timeout) {
options.timeout = timeout
}
return this.queue.add(() => {
return request(options)
.then((response) => {
if (response.hasOwnProperty("error")) {
return {
method: method,
params: params,
error: response.error
}
}
return {
method: method,
params: params,
result: response.result
}
}).catch(error => {
return {
method: method,
params: params,
error: {
code: -1,
message: "Cannot connect to wallet-rpc",
cause: error.cause
}
}
})
})
}
getRPC (parameter, params = {}) {
return this.sendRPC(`get_${parameter}`, params)
}
quit () {
return new Promise((resolve, reject) => {
if (this.walletRPCProcess) {
this.closeWallet().then(() => {
// normally we would exit wallet after this promise
// however if the wallet is not responsive to RPC
// requests then we must forcefully close it below
})
setTimeout(() => {
this.walletRPCProcess.on("close", code => {
this.agent.destroy()
resolve()
})
this.walletRPCProcess.kill()
}, 2500)
} else {
resolve()
}
})
}
}