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

1091 lines
38 KiB
JavaScript

import child_process from "child_process";
const request = require("request-promise");
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.wallet_dir = null
this.auth = []
this.id = 0
this.testnet = false
this.heartbeat = null
this.wallet_state = {
open: false,
name: "",
password_hash: null,
balance: null,
unlocked_balance: null
}
this.wallet_rpc_errors = {
"WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR": "Unknown error",
"WALLET_RPC_ERROR_CODE_WRONG_ADDRESS": "Invalid address format",
"WALLET_RPC_ERROR_CODE_DAEMON_IS_BUSY": "Daemon is busy",
"WALLET_RPC_ERROR_CODE_GENERIC_TRANSFER_ERROR": "Unknown transfer error",
"WALLET_RPC_ERROR_CODE_WRONG_PAYMENT_ID": "Invalid payment ID format",
"WALLET_RPC_ERROR_CODE_TRANSFER_TYPE": "Wrong transfer type",
"WALLET_RPC_ERROR_CODE_DENIED": "Transaction was denied",
"WALLET_RPC_ERROR_CODE_WRONG_TXID": "Wrong transaction ID",
"WALLET_RPC_ERROR_CODE_WRONG_SIGNATURE": "Wrong signature",
"WALLET_RPC_ERROR_CODE_WRONG_KEY_IMAGE": "Wrong key image",
"WALLET_RPC_ERROR_CODE_WRONG_URI": "Wrong URI",
"WALLET_RPC_ERROR_CODE_WRONG_INDEX": "Wrong index",
"WALLET_RPC_ERROR_CODE_NOT_OPEN": "Wallet not open",
}
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 function will take an options object for testnet, data-dir, etc
start(options) {
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}`
}
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],
"--disable-rpc-login",
"--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",
]
let log_file
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)
}
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)
} else {
this.walletRPCProcess = child_process.spawn(path.join(__ryo_bin, "ryo-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, params.type)
break
case "restore_wallet":
this.restoreWallet(params.name, params.password, params.seed, 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.mixin, 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
default:
}
}
createWallet(filename, password, language, type) {
let short_address = type == "kurz"
this.sendRPC("create_wallet", {
filename,
password,
language,
short_address
}).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_start_height=0) {
if(!Number.isInteger(refresh_start_height)) {
refresh_start_height = 0
}
seed = seed.trim().replace(/\s{2,}/g, " ")
this.sendRPC("restore_wallet", {
filename,
password,
seed,
refresh_start_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)
//});
});
}
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"}})
return
} 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
},
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
}
}
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()
})
}
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
}
// 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.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", wallet)
})
}
}
})
}
transfer (password, amount, address, payment_id, mixin, 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": mixin
}
if(payment_id) {
params.payment_id = payment_id
}
this.sendRPC("sweep_all", params).then((data) => {
if(data.hasOwnProperty("error")) {
let error = "Unknown error"
for(let n of Object.keys(this.wallet_rpc_errors)) {
if(data.error.message.indexOf(n) === 0) {
error = this.wallet_rpc_errors[n]
break
}
}
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": mixin
}
if(payment_id) {
params.payment_id = payment_id
}
this.sendRPC("transfer_split", params).then((data) => {
if(data.hasOwnProperty("error")) {
let error = "Unknown error"
for(let n of Object.keys(this.wallet_rpc_errors)) {
if(data.error.message.indexOf(n) === 0) {
error = this.wallet_rpc_errors[n]
break
}
}
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
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
}
console.log("send secrets")
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)
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" ? true : false
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(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)
})
})
}
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
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\\RyoGUIWallet", "C:\\ProgramData\\RyoLITEWallet"]
} else {
legacy_paths = [path.join(os.homedir(), "RyoGUIWallet"), path.join(os.homedir(), "RyoLITEWallet")]
}
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
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)
}
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,
balance: null,
unlocked_balance: null,
password_hash: 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 = {
forever: true,
json: {
jsonrpc: "2.0",
id: id,
method: method
},
/*
auth: {
user: this.auth[0],
pass: this.auth[1],
sendImmediately: false
}
*/
};
if (Object.keys(params).length !== 0) {
options.json.params = params
}
if(timeout) {
options.timeout = timeout
}
return request.post(`${this.protocol}${this.hostname}:${this.port}/json_rpc`, 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 => {
resolve()
})
this.walletRPCProcess.kill()
}, 2500)
} else {
resolve()
}
})
}
}