2852 lines
79 KiB
JavaScript
2852 lines
79 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-extra");
|
|
const path = require("upath");
|
|
const crypto = require("crypto");
|
|
const portscanner = require("portscanner");
|
|
|
|
export class WalletRPC {
|
|
constructor(backend) {
|
|
this.backend = backend;
|
|
this.data_dir = null;
|
|
this.wallet_dir = null;
|
|
this.auth = [];
|
|
this.id = 0;
|
|
this.net_type = "mainnet";
|
|
this.heartbeat = null;
|
|
this.lnsHeartbeat = null;
|
|
this.wallet_state = {
|
|
open: false,
|
|
name: "",
|
|
password_hash: null,
|
|
balance: null,
|
|
unlocked_balance: null,
|
|
lnsRecords: []
|
|
};
|
|
this.isRPCSyncing = false;
|
|
this.dirs = null;
|
|
this.last_height_send_time = Date.now();
|
|
|
|
// A mapping of name => type
|
|
this.purchasedNames = {};
|
|
|
|
this.height_regexes = [
|
|
{
|
|
string: /Processed block: <([a-f0-9]+)>, height (\d+)/,
|
|
height: match => match[2]
|
|
},
|
|
{
|
|
string: /Skipped block by height: (\d+)/,
|
|
height: match => match[1]
|
|
},
|
|
{
|
|
string: /Skipped block by timestamp, height: (\d+)/,
|
|
height: match => match[1]
|
|
},
|
|
{
|
|
string: /Blockchain sync progress: <([a-f0-9]+)>, height (\d+)/,
|
|
height: match => match[2]
|
|
}
|
|
];
|
|
|
|
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, wallet_data_dir, data_dir } = options.app;
|
|
this.net_type = net_type;
|
|
this.data_dir = data_dir;
|
|
this.wallet_data_dir = wallet_data_dir;
|
|
|
|
this.dirs = {
|
|
mainnet: this.wallet_data_dir,
|
|
stagenet: path.join(this.wallet_data_dir, "stagenet"),
|
|
testnet: path.join(this.wallet_data_dir, "testnet")
|
|
};
|
|
|
|
this.wallet_dir = path.join(this.dirs[net_type], "wallets");
|
|
args.push("--wallet-dir", this.wallet_dir);
|
|
|
|
const log_file = path.join(
|
|
this.dirs[net_type],
|
|
"logs",
|
|
"wallet-rpc.log"
|
|
);
|
|
args.push("--log-file", log_file);
|
|
|
|
if (net_type === "testnet") {
|
|
args.push("--testnet");
|
|
} else if (net_type === "stagenet") {
|
|
args.push("--stagenet");
|
|
}
|
|
|
|
if (fs.existsSync(log_file)) {
|
|
fs.truncateSync(log_file, 0);
|
|
}
|
|
|
|
if (!fs.existsSync(this.wallet_dir)) {
|
|
fs.mkdirpSync(this.wallet_dir);
|
|
}
|
|
|
|
// save this info for later RPC calls
|
|
this.protocol = "http://";
|
|
this.hostname = "127.0.0.1";
|
|
this.port = options.wallet.rpc_bind_port;
|
|
|
|
const rpcExecutable =
|
|
process.platform === "win32"
|
|
? "loki-wallet-rpc.exe"
|
|
: "loki-wallet-rpc";
|
|
// eslint-disable-next-line no-undef
|
|
const rpcPath = path.join(__ryo_bin, rpcExecutable);
|
|
|
|
// Check if the rpc exists
|
|
if (!fs.existsSync(rpcPath)) {
|
|
reject(
|
|
new Error(
|
|
"Failed to find Loki Wallet RPC. Please make sure you anti-virus has not removed it."
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
portscanner
|
|
.checkPortStatus(this.port, this.hostname)
|
|
.catch(() => "closed")
|
|
.then(status => {
|
|
if (status === "closed") {
|
|
const options =
|
|
process.platform === "win32" ? {} : { detached: true };
|
|
this.walletRPCProcess = child_process.spawn(
|
|
rpcPath,
|
|
args,
|
|
options
|
|
);
|
|
|
|
this.walletRPCProcess.stdout.on("data", data => {
|
|
process.stdout.write(`Wallet: ${data}`);
|
|
|
|
let lines = data.toString().split("\n");
|
|
let match,
|
|
height = null;
|
|
let isRPCSyncing = false;
|
|
for (const line of lines) {
|
|
for (const regex of this.height_regexes) {
|
|
match = line.match(regex.string);
|
|
if (match) {
|
|
height = regex.height(match);
|
|
isRPCSyncing = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep track on wether a wallet is syncing or not
|
|
this.sendGateway("set_wallet_data", {
|
|
isRPCSyncing
|
|
});
|
|
this.isRPCSyncing = isRPCSyncing;
|
|
|
|
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} \n`);
|
|
this.walletRPCProcess = null;
|
|
this.agent.destroy();
|
|
if (code === null) {
|
|
reject(new Error("Failed to start wallet RPC"));
|
|
}
|
|
});
|
|
|
|
// 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 (
|
|
this.walletRPCProcess &&
|
|
data.error.cause &&
|
|
data.error.cause.code === "ECONNREFUSED"
|
|
) {
|
|
// Ignore
|
|
} else {
|
|
clearInterval(intrvl);
|
|
if (this.walletRPCProcess) this.walletRPCProcess.kill();
|
|
this.walletRPCProcess = null;
|
|
reject(new Error("Could not connect to wallet RPC"));
|
|
}
|
|
}
|
|
});
|
|
}, 1000);
|
|
} else {
|
|
reject(new Error(`Wallet RPC port ${this.port} is in use`));
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async handle(data) {
|
|
let params = data.data;
|
|
|
|
switch (data.method) {
|
|
case "has_password":
|
|
this.hasPassword();
|
|
break;
|
|
|
|
case "validate_address":
|
|
this.validateAddress(params.address);
|
|
break;
|
|
|
|
case "decrypt_record": {
|
|
const record = await this.decryptLNSRecord(params.type, params.name);
|
|
this.sendGateway("set_decrypt_record_result", {
|
|
record,
|
|
decrypted: !!record
|
|
});
|
|
break;
|
|
}
|
|
|
|
case "copy_old_gui_wallets":
|
|
this.copyOldGuiWallets(params.wallets || []);
|
|
break;
|
|
|
|
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 "stake":
|
|
this.stake(
|
|
params.password,
|
|
params.amount,
|
|
params.key,
|
|
params.destination
|
|
);
|
|
break;
|
|
|
|
case "register_service_node":
|
|
this.registerSnode(params.password, params.string);
|
|
break;
|
|
|
|
case "update_service_node_list":
|
|
this.updateServiceNodeList();
|
|
break;
|
|
|
|
case "unlock_stake":
|
|
this.unlockStake(
|
|
params.password,
|
|
params.service_node_key,
|
|
params.confirmed || false
|
|
);
|
|
break;
|
|
|
|
case "transfer":
|
|
this.transfer(
|
|
params.password,
|
|
params.amount,
|
|
params.address,
|
|
params.payment_id,
|
|
params.priority,
|
|
!!params.isSweepAll
|
|
);
|
|
break;
|
|
case "relay_tx":
|
|
this.relayTransaction(
|
|
params.metadataList,
|
|
params.isBlink,
|
|
params.addressSave,
|
|
params.note,
|
|
!!params.isSweepAll
|
|
);
|
|
break;
|
|
case "purchase_lns":
|
|
this.purchaseLNS(
|
|
params.password,
|
|
params.type,
|
|
params.name,
|
|
params.value,
|
|
params.owner || "",
|
|
params.backup_owner || ""
|
|
);
|
|
break;
|
|
case "lns_renew_mapping":
|
|
this.lnsRenewMapping(params.password, params.type, params.name);
|
|
break;
|
|
case "lns_known_names":
|
|
this.lnsKnownNames();
|
|
break;
|
|
case "update_lns_mapping":
|
|
this.updateLNSMapping(
|
|
params.password,
|
|
params.type,
|
|
params.name,
|
|
params.value,
|
|
params.owner || "",
|
|
params.backup_owner || ""
|
|
);
|
|
|
|
break;
|
|
|
|
case "prove_transaction":
|
|
this.proveTransaction(params.txid, params.address, params.message);
|
|
break;
|
|
|
|
case "check_transaction":
|
|
this.checkTransactionProof(
|
|
params.signature,
|
|
params.txid,
|
|
params.address,
|
|
params.message
|
|
);
|
|
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:
|
|
}
|
|
}
|
|
|
|
isValidPasswordHash(password_hash) {
|
|
if (this.wallet_state.password_hash === null) return true;
|
|
return this.wallet_state.password_hash === password_hash.toString("hex");
|
|
}
|
|
|
|
hasPassword() {
|
|
if (this.wallet_state.password_hash === null) {
|
|
this.sendGateway("set_has_password", false);
|
|
return;
|
|
}
|
|
|
|
// We need to check if the hash generated with an empty string is the same as the password_hash we are storing
|
|
crypto.pbkdf2(
|
|
"",
|
|
this.auth[2],
|
|
1000,
|
|
64,
|
|
"sha512",
|
|
(err, password_hash) => {
|
|
if (err) {
|
|
this.sendGateway("set_has_password", false);
|
|
return;
|
|
}
|
|
|
|
// If the pass hash doesn't match empty string then we don't have a password
|
|
this.sendGateway(
|
|
"set_has_password",
|
|
this.wallet_state.password_hash !== password_hash.toString("hex")
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
validateAddress(address) {
|
|
this.sendRPC("validate_address", {
|
|
address
|
|
}).then(data => {
|
|
if (data.hasOwnProperty("error")) {
|
|
this.sendGateway("set_valid_address", {
|
|
address,
|
|
valid: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
const { valid, nettype } = data.result;
|
|
|
|
const netMatches = this.net_type === nettype;
|
|
const isValid = valid && netMatches;
|
|
|
|
this.sendGateway("set_valid_address", {
|
|
address,
|
|
valid: isValid,
|
|
nettype
|
|
});
|
|
});
|
|
}
|
|
|
|
createWallet(filename, password, language) {
|
|
// Reset the status error
|
|
this.sendGateway("reset_wallet_error");
|
|
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);
|
|
});
|
|
}
|
|
|
|
// the date should be in ms from epoch (Jan 1 1970)
|
|
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.sendGateway("reset_wallet_error");
|
|
this.backend.daemon.timestampToHeight(timestamp).then(height => {
|
|
if (height === false) {
|
|
this.sendGateway("set_wallet_error", {
|
|
status: {
|
|
code: -1,
|
|
i18n: "notification.errors.invalidRestoreDate"
|
|
}
|
|
});
|
|
} else {
|
|
this.restoreWallet(filename, password, seed, "height", height);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
let restore_height = Number.parseInt(refresh_start_timestamp_or_height);
|
|
|
|
// if the height can't be parsed just start from block 0
|
|
if (!restore_height) {
|
|
restore_height = 0;
|
|
}
|
|
seed = seed.trim().replace(/\s{2,}/g, " ");
|
|
|
|
this.sendGateway("reset_wallet_error");
|
|
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;
|
|
}
|
|
|
|
// 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,
|
|
i18n: "notification.errors.invalidRestoreDate"
|
|
}
|
|
});
|
|
} 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(wallet_name, password, import_path) {
|
|
// Reset the status error
|
|
this.sendGateway("reset_wallet_error");
|
|
|
|
// 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,
|
|
i18n: "notification.errors.invalidWalletPath"
|
|
}
|
|
});
|
|
return;
|
|
} else {
|
|
let destination = path.join(this.wallet_dir, wallet_name);
|
|
if (fs.existsSync(destination) || fs.existsSync(destination + ".keys")) {
|
|
this.sendGateway("set_wallet_error", {
|
|
status: {
|
|
code: -1,
|
|
i18n: "notification.errors.walletAlreadyExists"
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
fs.copySync(import_path, destination, { errorOnExist: true });
|
|
if (fs.existsSync(import_path + ".keys")) {
|
|
fs.copySync(import_path + ".keys", destination + ".keys", {
|
|
errorOnExist: true
|
|
});
|
|
}
|
|
} catch (e) {
|
|
this.sendGateway("set_wallet_error", {
|
|
status: {
|
|
code: -1,
|
|
i18n: "notification.errors.copyWalletFail"
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
this.sendRPC("open_wallet", {
|
|
filename: wallet_name,
|
|
password
|
|
})
|
|
.then(data => {
|
|
if (data.hasOwnProperty("error")) {
|
|
if (fs.existsSync(destination)) fs.unlinkSync(destination);
|
|
if (fs.existsSync(destination + ".keys"))
|
|
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 = wallet_name;
|
|
this.wallet_state.open = true;
|
|
this.finalizeNewWallet(wallet_name);
|
|
})
|
|
.catch(() => {
|
|
this.sendGateway("set_wallet_error", {
|
|
status: {
|
|
code: -1,
|
|
i18n: "notification.errors.unknownError"
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
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.sendGateway("reset_wallet_error");
|
|
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();
|
|
|
|
this.purchasedNames = {};
|
|
|
|
// 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);
|
|
|
|
clearInterval(this.lnsHeartbeat);
|
|
this.lnsHeartbeat = setInterval(() => {
|
|
this.updateLocalLNSRecords();
|
|
}, 30 * 1000); // Every 30 seconds
|
|
this.updateLocalLNSRecords();
|
|
}
|
|
|
|
heartbeatAction(extended = false) {
|
|
Promise.all([
|
|
this.sendRPC("get_address", { account_index: 0 }, 5000),
|
|
this.sendRPC("getheight", {}, 5000),
|
|
this.sendRPC("getbalance", { account_index: 0 }, 5000)
|
|
]).then(data => {
|
|
let didError = false;
|
|
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")) {
|
|
// Maybe we also need to look into the other error codes it could give us
|
|
// Error -13: No wallet file - This occurs when you call open wallet while another wallet is still syncing
|
|
if (extended && n.error && n.error.code === -13) {
|
|
didError = true;
|
|
}
|
|
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 == "get_address") {
|
|
wallet.info.address = n.result.address;
|
|
this.sendGateway("set_wallet_data", {
|
|
info: {
|
|
address: n.result.address
|
|
}
|
|
});
|
|
} 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()];
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Set the wallet state on initial heartbeat
|
|
if (extended) {
|
|
if (!didError) {
|
|
this.sendGateway("set_wallet_data", wallet);
|
|
} else {
|
|
this.closeWallet().then(() => {
|
|
this.sendGateway("set_wallet_error", {
|
|
status: {
|
|
code: -1,
|
|
i18n: "notification.errors.failedWalletOpen"
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async updateLocalLNSRecords() {
|
|
console.log("calling updateLocalLNSRecords");
|
|
try {
|
|
const addressData = await this.sendRPC(
|
|
"get_address",
|
|
{ account_index: 0 },
|
|
5000
|
|
);
|
|
if (
|
|
addressData.hasOwnProperty("error") ||
|
|
!addressData.hasOwnProperty("result")
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Pull out all our addresses from the data and make sure they're valid
|
|
const results = addressData.result.addresses || [];
|
|
const addresses = results.map(a => a.address).filter(a => !!a);
|
|
if (addresses.length === 0) return;
|
|
|
|
const records = await this.backend.daemon.getLNSRecordsForOwners(
|
|
addresses
|
|
);
|
|
|
|
// We need to ensure that we decrypt any incoming records that we already have
|
|
const currentRecords = this.wallet_state.lnsRecords;
|
|
const recordsToUpdate = { ...this.purchasedNames };
|
|
const newRecords = records.map(record => {
|
|
// If we have a new record or we haven't decrypted our current record then we should return the new record
|
|
const current = currentRecords.find(
|
|
c => c.name_hash === record.name_hash
|
|
);
|
|
if (!current || !current.name) return record;
|
|
|
|
// We need to check if we need to re-decrypt the record.
|
|
// This is only necessary if the encrypted_value changed.
|
|
const needsToUpdate =
|
|
current.encrypted_value !== record.encrypted_value;
|
|
if (needsToUpdate) {
|
|
const { name, type } = current;
|
|
recordsToUpdate[name] = type;
|
|
|
|
return {
|
|
name,
|
|
...record
|
|
};
|
|
}
|
|
|
|
// Otherwise just update our current record with new information (in the case that owner or backup_owner was updated)
|
|
return {
|
|
...current,
|
|
...record
|
|
};
|
|
});
|
|
|
|
this.wallet_state.lnsRecords = newRecords;
|
|
|
|
// console.log("New LNS records found in update:");
|
|
// console.log(newRecords);
|
|
|
|
// ========= FETCH THE CACHED RECORDS HERE AND JOIN WITH THE OTHER RECORDS =====
|
|
// ===== UI DISPLAYS UNLOCKED RECORDS IF THE ENTRY HAS A 'name' and 'value' field
|
|
// const isSession = record => record.type === "session";
|
|
// let nonSessionRecords = newRecords.filter(record => !isSession(record));
|
|
// console.log("non session records");
|
|
// console.log(nonSessionRecords);
|
|
|
|
// fetch the known (cached) records from the wallet and add the data
|
|
// to the records being set in state
|
|
// let known_names = await this.lnsKnownNames();
|
|
// let known_name_hashes = known_names.map(k => k.hashed);
|
|
// console.log("Known name hashes");
|
|
// console.log(known_name_hashes);
|
|
|
|
// where the known matches the records we need to set values here
|
|
// let cached_records = newRecords.find(r => known_name_hashes.includes(r));
|
|
// console.log("Cached records:");
|
|
// console.log(cached_records);
|
|
|
|
// for (let r of newRecords) {
|
|
// for (let k of known_names) {
|
|
// if (k.hashed === r.name_hash) {
|
|
// r["name"] = k.name;
|
|
// // r["value"] = k.value;
|
|
// }
|
|
// }
|
|
// }
|
|
// let final = newRecords.forEach(c => {
|
|
// known_name_hashes.for
|
|
// })
|
|
|
|
// console.log("Records being set to lnsRecords");
|
|
// console.log(newRecords);
|
|
|
|
this.sendGateway("set_wallet_data", { lnsRecords: newRecords });
|
|
|
|
// Decrypt the records serially
|
|
let updatePromise = Promise.resolve();
|
|
for (const [name, type] of Object.entries(recordsToUpdate)) {
|
|
updatePromise = updatePromise.then(() => {
|
|
this.decryptLNSRecord(type, name);
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.debug("Something went wrong when updating lns records: ", e);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Get the LNS records cached in this (local) wallet. Call alongside the other call to update as
|
|
we go
|
|
*/
|
|
async lnsKnownNames() {
|
|
try {
|
|
let data = await this.sendRPC("lns_known_names");
|
|
|
|
for (let r of data.result.known_names) {
|
|
console.log(r);
|
|
}
|
|
if (data.result && data.result.known_names) {
|
|
return data.result.known_names;
|
|
} else {
|
|
return [];
|
|
}
|
|
} catch (e) {
|
|
console.log("There was an error getting known records: " + e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/*
|
|
Renews an LNS (Lokinet) mapping, since they can expire
|
|
type can be:
|
|
lokinet_1y, lokinet_2y, lokinet_5y, lokinet_10y
|
|
*/
|
|
lnsRenewMapping(password, type, name) {
|
|
let _name = name.trim().toLowerCase();
|
|
|
|
// the RPC accepts names with the .loki already appeneded only
|
|
// can be lokinet_1y, lokinet_2y, lokinet_5y, lokinet_10y
|
|
if (type.startsWith("lokinet")) {
|
|
_name = _name + ".loki";
|
|
}
|
|
|
|
crypto.pbkdf2(
|
|
password,
|
|
this.auth[2],
|
|
1000,
|
|
64,
|
|
"sha512",
|
|
(err, password_hash) => {
|
|
if (err) {
|
|
this.sendGateway("set_lns_status", {
|
|
code: -1,
|
|
i18n: "notification.errors.internalError",
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("set_lns_status", {
|
|
code: -1,
|
|
i18n: "notification.errors.invalidPassword",
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
const params = {
|
|
type,
|
|
name: _name
|
|
};
|
|
|
|
this.sendRPC("lns_renew_mapping", params).then(data => {
|
|
if (data.hasOwnProperty("error")) {
|
|
let error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
this.sendGateway("set_lns_status", {
|
|
code: -1,
|
|
message: error,
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.purchasedNames[name.trim()] = type;
|
|
|
|
setTimeout(() => this.updateLocalLNSRecords(), 5000);
|
|
|
|
this.sendGateway("set_lns_status", {
|
|
code: 0,
|
|
i18n: "notification.positive.nameRenewed",
|
|
sending: false
|
|
});
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
/*
|
|
Get our LNS record and update our wallet state with decrypted values.
|
|
This will return `null` if the record is not in our currently stored records.
|
|
*/
|
|
async decryptLNSRecord(type, name) {
|
|
let _type = type;
|
|
// type can initially be "lokinet_1y" etc. on a purchase
|
|
if (type.startsWith("lokinet")) {
|
|
_type = "lokinet";
|
|
}
|
|
try {
|
|
const record = await this.getLNSRecord(_type, name);
|
|
if (!record) return null;
|
|
|
|
// Update our current records with the new decrypted record
|
|
const currentRecords = this.wallet_state.lnsRecords;
|
|
const isOurRecord = currentRecords.find(
|
|
c => c.name_hash === record.name_hash
|
|
);
|
|
if (!isOurRecord) return null;
|
|
|
|
const newRecords = currentRecords.map(current => {
|
|
if (current.name_hash === record.name_hash) {
|
|
return record;
|
|
}
|
|
return current;
|
|
});
|
|
this.wallet_state.lnsRecords = newRecords;
|
|
this.sendGateway("set_wallet_data", { lnsRecords: newRecords });
|
|
return record;
|
|
} catch (e) {
|
|
console.debug("Something went wrong when updating lns record: ", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Get a LNS record associated with the given name
|
|
*/
|
|
async getLNSRecord(type, name) {
|
|
// We currently only support session and lokinet
|
|
const types = ["session", "lokinet"];
|
|
if (!types.includes(type)) return null;
|
|
|
|
if (!name || name.trim().length === 0) return null;
|
|
|
|
const lowerCaseName = name.toLowerCase();
|
|
|
|
let fullName = lowerCaseName;
|
|
if (type === "lokinet" && !name.endsWith(".loki")) {
|
|
fullName = fullName + ".loki";
|
|
}
|
|
|
|
const nameHash = await this.hashLNSName(type, lowerCaseName);
|
|
if (!nameHash) return null;
|
|
|
|
const record = await this.backend.daemon.getLNSRecord(nameHash);
|
|
if (!record || !record.encrypted_value) return null;
|
|
|
|
// Decrypt the value if possible
|
|
const value = await this.decryptLNSValue(
|
|
type,
|
|
fullName,
|
|
record.encrypted_value
|
|
);
|
|
|
|
return {
|
|
name: fullName,
|
|
value,
|
|
...record
|
|
};
|
|
}
|
|
|
|
async hashLNSName(type, name) {
|
|
if (!type || !name) return null;
|
|
|
|
let fullName = name;
|
|
if (type === "lokinet" && !name.endsWith(".loki")) {
|
|
fullName = fullName + ".loki";
|
|
}
|
|
|
|
try {
|
|
const data = await this.sendRPC("lns_hash_name", {
|
|
type,
|
|
name: fullName
|
|
});
|
|
|
|
if (data.hasOwnProperty("error")) {
|
|
let error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
throw new Error(error);
|
|
}
|
|
|
|
return (data.result && data.result.name) || null;
|
|
} catch (e) {
|
|
console.debug("Failed to hash lns name: ", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async decryptLNSValue(type, name, encrypted_value) {
|
|
if (!type || !name || !encrypted_value) return null;
|
|
|
|
let fullName = name;
|
|
if (type === "lokinet" && !name.endsWith(".loki")) {
|
|
fullName = fullName + ".loki";
|
|
}
|
|
|
|
try {
|
|
const data = await this.sendRPC("lns_decrypt_value", {
|
|
type,
|
|
name: fullName,
|
|
encrypted_value
|
|
});
|
|
|
|
if (data.hasOwnProperty("error")) {
|
|
let error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
throw new Error(error);
|
|
}
|
|
|
|
return (data.result && data.result.value) || null;
|
|
} catch (e) {
|
|
console.debug("Failed to decrypt lns value: ", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
stake(password, amount, service_node_key, destination) {
|
|
crypto.pbkdf2(
|
|
password,
|
|
this.auth[2],
|
|
1000,
|
|
64,
|
|
"sha512",
|
|
(err, password_hash) => {
|
|
if (err) {
|
|
this.sendGateway("set_snode_status", {
|
|
stake: {
|
|
code: -1,
|
|
i18n: "notification.errors.internalError",
|
|
sending: false
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("set_snode_status", {
|
|
stake: {
|
|
code: -1,
|
|
i18n: "notification.errors.invalidPassword",
|
|
sending: false
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
amount = (parseFloat(amount) * 1e9).toFixed(0);
|
|
|
|
this.sendRPC("stake", {
|
|
amount,
|
|
destination,
|
|
service_node_key
|
|
}).then(data => {
|
|
if (data.hasOwnProperty("error")) {
|
|
let error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
this.sendGateway("set_snode_status", {
|
|
stake: {
|
|
code: -1,
|
|
message: error,
|
|
sending: false
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Update the new snode list
|
|
this.backend.daemon.updateServiceNodes();
|
|
|
|
this.sendGateway("set_snode_status", {
|
|
stake: {
|
|
code: 0,
|
|
i18n: "notification.positive.stakeSuccess",
|
|
sending: false
|
|
}
|
|
});
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
registerSnode(password, register_service_node_str) {
|
|
crypto.pbkdf2(
|
|
password,
|
|
this.auth[2],
|
|
1000,
|
|
64,
|
|
"sha512",
|
|
(err, password_hash) => {
|
|
if (err) {
|
|
this.sendGateway("set_snode_status", {
|
|
registration: {
|
|
code: -1,
|
|
i18n: "notification.errors.internalError",
|
|
sending: false
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("set_snode_status", {
|
|
registration: {
|
|
code: -1,
|
|
i18n: "notification.errors.invalidPassword",
|
|
sending: false
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.sendRPC("register_service_node", {
|
|
register_service_node_str
|
|
}).then(data => {
|
|
if (data.hasOwnProperty("error")) {
|
|
const error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
this.sendGateway("set_snode_status", {
|
|
registration: {
|
|
code: -1,
|
|
message: error,
|
|
sending: false
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Update the new snode list
|
|
this.backend.daemon.updateServiceNodes();
|
|
|
|
this.sendGateway("set_snode_status", {
|
|
registration: {
|
|
code: 0,
|
|
i18n: "notification.positive.registerServiceNodeSuccess",
|
|
sending: false
|
|
}
|
|
});
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
async updateServiceNodeList() {
|
|
this.backend.daemon.updateServiceNodes();
|
|
}
|
|
|
|
unlockStake(password, service_node_key, confirmed = false) {
|
|
const sendError = (message, i18n = true) => {
|
|
const key = i18n ? "i18n" : "message";
|
|
this.sendGateway("set_snode_status", {
|
|
unlock: {
|
|
code: -1,
|
|
[key]: message,
|
|
sending: false
|
|
}
|
|
});
|
|
};
|
|
|
|
// Unlock code 0 means success, 1 means can unlock, -1 means error
|
|
crypto.pbkdf2(
|
|
password,
|
|
this.auth[2],
|
|
1000,
|
|
64,
|
|
"sha512",
|
|
(err, password_hash) => {
|
|
if (err) {
|
|
sendError("notification.errors.internalError");
|
|
return;
|
|
}
|
|
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
sendError("notification.errors.invalidPassword");
|
|
return;
|
|
}
|
|
|
|
const sendRPC = path => {
|
|
return this.sendRPC(path, {
|
|
service_node_key
|
|
}).then(data => {
|
|
if (data.hasOwnProperty("error")) {
|
|
const error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
sendError(error, false);
|
|
return null;
|
|
}
|
|
|
|
if (!data.hasOwnProperty("result")) {
|
|
sendError("notification.errors.failedServiceNodeUnlock");
|
|
return null;
|
|
}
|
|
|
|
return data.result;
|
|
});
|
|
};
|
|
|
|
if (confirmed) {
|
|
sendRPC("request_stake_unlock").then(data => {
|
|
if (!data) return;
|
|
|
|
const unlock = {
|
|
code: data.unlocked ? 0 : -1,
|
|
message: data.msg,
|
|
sending: false
|
|
};
|
|
|
|
// Update the new snode list
|
|
if (data.unlocked) {
|
|
this.backend.daemon.updateServiceNodes();
|
|
}
|
|
|
|
this.sendGateway("set_snode_status", { unlock });
|
|
});
|
|
} else {
|
|
sendRPC("can_request_stake_unlock").then(data => {
|
|
if (!data) return;
|
|
|
|
const unlock = {
|
|
code: data.can_unlock ? 1 : -1,
|
|
message: data.msg,
|
|
sending: false
|
|
};
|
|
|
|
this.sendGateway("set_snode_status", { unlock });
|
|
});
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// submits the transaction to the blockchain, irreversible from here
|
|
async relayTransaction(metadataList, isBlink, addressSave, note, isSweepAll) {
|
|
// for a sweep these don't exist
|
|
let address = "";
|
|
let payment_id = "";
|
|
let address_book = "";
|
|
if (addressSave) {
|
|
address = addressSave.address;
|
|
payment_id = addressSave.payment_id;
|
|
address_book = addressSave.address_book;
|
|
}
|
|
|
|
let failed = false;
|
|
let errorMessage = "Failed to relay transaction";
|
|
|
|
// submit each transaction individually
|
|
for (const hex of metadataList) {
|
|
const params = {
|
|
hex,
|
|
blink: isBlink
|
|
};
|
|
// don't try submit more txs if a prev one failed
|
|
if (failed) break;
|
|
try {
|
|
const data = await this.sendRPC("relay_tx", params);
|
|
if (data.hasOwnProperty("error")) {
|
|
errorMessage = data.error.message || errorMessage;
|
|
failed = true;
|
|
break;
|
|
} else if (data.hasOwnProperty("result")) {
|
|
const tx_hash = data.result.tx_hash;
|
|
if (note && note !== "") {
|
|
this.saveTxNotes(tx_hash, note);
|
|
}
|
|
} else {
|
|
errorMessage = "Invalid format of relay_tx RPC return message";
|
|
failed = true;
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
failed = true;
|
|
errorMessage = e.toString();
|
|
}
|
|
}
|
|
|
|
// for updating state on the correct page
|
|
const gatewayEndpoint = isSweepAll
|
|
? "set_sweep_all_status"
|
|
: "set_tx_status";
|
|
|
|
if (!failed) {
|
|
this.sendGateway(gatewayEndpoint, {
|
|
code: 0,
|
|
i18n: "notification.positive.sendSuccess",
|
|
sending: false
|
|
});
|
|
|
|
if (address_book.hasOwnProperty("save") && address_book.save) {
|
|
this.addAddressBook(
|
|
address,
|
|
payment_id,
|
|
address_book.description,
|
|
address_book.name
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.sendGateway(gatewayEndpoint, {
|
|
code: -1,
|
|
message: errorMessage,
|
|
sending: false
|
|
});
|
|
}
|
|
|
|
// prepares params and provides a "confirm" popup to allow the user to check
|
|
// send address and tx fees before sending
|
|
// isSweepAll refers to if it's the sweep from service nodes page
|
|
transfer(password, amount, address, payment_id, priority, isSweepAll) {
|
|
const cryptoCallback = (err, password_hash) => {
|
|
if (err) {
|
|
this.sendGateway("set_tx_status", {
|
|
code: -1,
|
|
i18n: "notification.errors.internalError",
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("set_tx_status", {
|
|
code: -1,
|
|
i18n: "notification.errors.invalidPassword",
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
amount = (parseFloat(amount) * 1e9).toFixed(0);
|
|
|
|
// if sending "All" the funds, then we need to send all - fee (sweep_all)
|
|
// To be amended after the hardfork, v8.
|
|
// https://github.com/loki-project/loki-electron-gui-wallet/issues/181
|
|
const isSweepAllRPC = amount == this.wallet_state.unlocked_balance;
|
|
const rpc_endpoint = isSweepAllRPC ? "sweep_all" : "transfer_split";
|
|
|
|
const rpcSpecificParams = isSweepAllRPC
|
|
? {
|
|
address,
|
|
account_index: 0
|
|
}
|
|
: {
|
|
destinations: [{ amount: amount, address: address }]
|
|
};
|
|
const params = {
|
|
...rpcSpecificParams,
|
|
priority,
|
|
do_not_relay: true,
|
|
get_tx_metadata: true
|
|
};
|
|
|
|
if (payment_id) {
|
|
params.payment_id = payment_id;
|
|
}
|
|
|
|
// for updating state on the correct page
|
|
const gatewayEndpoint = isSweepAll
|
|
? "set_sweep_all_status"
|
|
: "set_tx_status";
|
|
|
|
this.sendRPC(rpc_endpoint, params)
|
|
.then(data => {
|
|
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
|
|
let error = "";
|
|
if (data.error && data.error.message) {
|
|
error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
} else {
|
|
error = `Incorrect result from ${rpc_endpoint} RPC call`;
|
|
}
|
|
this.sendGateway(gatewayEndpoint, {
|
|
code: -1,
|
|
message: error,
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
// update state to show a confirm popup
|
|
this.sendGateway(gatewayEndpoint, {
|
|
code: 1,
|
|
i18n: "strings.awaitingConfirmation",
|
|
sending: false,
|
|
txData: {
|
|
// target address for a sweep all
|
|
address: data.params.address,
|
|
isSweepAll: isSweepAllRPC,
|
|
amountList: data.result.amount_list,
|
|
metadataList: data.result.tx_metadata_list,
|
|
feeList: data.result.fee_list,
|
|
priority: data.params.priority,
|
|
// for a "send" tx
|
|
destinations: data.params.destinations
|
|
}
|
|
});
|
|
})
|
|
.catch(err => {
|
|
this.sendGateway(gatewayEndpoint, {
|
|
code: -1,
|
|
message: err.message,
|
|
sending: false
|
|
});
|
|
});
|
|
};
|
|
|
|
crypto.pbkdf2(password, this.auth[2], 1000, 64, "sha512", cryptoCallback);
|
|
}
|
|
|
|
purchaseLNS(password, type, name, value, owner, backupOwner) {
|
|
let _name = name.trim().toLowerCase();
|
|
const _owner = owner.trim() === "" ? null : owner;
|
|
const backup_owner = backupOwner.trim() === "" ? null : backupOwner;
|
|
|
|
// the RPC accepts names with the .loki already appeneded only
|
|
// can be lokinet_1y, lokinet_2y, lokinet_5y, lokinet_10y
|
|
if (type.startsWith("lokinet")) {
|
|
_name = _name + ".loki";
|
|
value = value + ".loki";
|
|
}
|
|
|
|
crypto.pbkdf2(
|
|
password,
|
|
this.auth[2],
|
|
1000,
|
|
64,
|
|
"sha512",
|
|
(err, password_hash) => {
|
|
if (err) {
|
|
this.sendGateway("set_lns_status", {
|
|
code: -1,
|
|
i18n: "notification.errors.internalError",
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("set_lns_status", {
|
|
code: -1,
|
|
i18n: "notification.errors.invalidPassword",
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
const params = {
|
|
type,
|
|
owner: _owner,
|
|
backup_owner,
|
|
name: _name,
|
|
value
|
|
};
|
|
|
|
this.sendRPC("lns_buy_mapping", params).then(data => {
|
|
if (data.hasOwnProperty("error")) {
|
|
let error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
this.sendGateway("set_lns_status", {
|
|
code: -1,
|
|
message: error,
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.purchasedNames[name.trim()] = type;
|
|
|
|
// Fetch new records and then get the decrypted record for the one we just inserted
|
|
setTimeout(() => this.updateLocalLNSRecords(), 5000);
|
|
|
|
this.sendGateway("set_lns_status", {
|
|
code: 0,
|
|
i18n: "notification.positive.namePurchased",
|
|
sending: false
|
|
});
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
updateLNSMapping(password, type, name, value, owner, backupOwner) {
|
|
let _name = name.trim().toLowerCase();
|
|
const _owner = owner.trim() === "" ? null : owner;
|
|
const backup_owner = backupOwner.trim() === "" ? null : backupOwner;
|
|
|
|
// updated records have type "lokinet" or "session"
|
|
// UI passes the values without the extension
|
|
if (type === "lokinet") {
|
|
_name = _name + ".loki";
|
|
value = value + ".loki";
|
|
}
|
|
|
|
crypto.pbkdf2(
|
|
password,
|
|
this.auth[2],
|
|
1000,
|
|
64,
|
|
"sha512",
|
|
(err, password_hash) => {
|
|
if (err) {
|
|
this.sendGateway("set_lns_status", {
|
|
code: -1,
|
|
i18n: "notification.errors.internalError",
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("set_lns_status", {
|
|
code: -1,
|
|
i18n: "notification.errors.invalidPassword",
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
const params = {
|
|
type,
|
|
owner: _owner,
|
|
backup_owner,
|
|
name: _name,
|
|
value
|
|
};
|
|
|
|
this.sendRPC("lns_update_mapping", params).then(data => {
|
|
if (data.hasOwnProperty("error")) {
|
|
let error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
this.sendGateway("set_lns_status", {
|
|
code: -1,
|
|
message: error,
|
|
sending: false
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.purchasedNames[name.trim()] = type;
|
|
|
|
// Fetch new records and then get the decrypted record for the one we just inserted
|
|
setTimeout(() => this.updateLocalLNSRecords(), 5000);
|
|
|
|
// Optimistically update our record
|
|
const { lnsRecords } = this.wallet_state;
|
|
const newRecords = lnsRecords.map(record => {
|
|
if (
|
|
record.type === type &&
|
|
record.name &&
|
|
record.name.toLowerCase() === _name
|
|
) {
|
|
return {
|
|
...record,
|
|
owner: _owner,
|
|
backup_owner,
|
|
value
|
|
};
|
|
}
|
|
|
|
return record;
|
|
});
|
|
this.wallet_state.lnsRecords = newRecords;
|
|
this.sendGateway("set_wallet_data", { lnsRecords: newRecords });
|
|
|
|
this.sendGateway("set_lns_status", {
|
|
code: 0,
|
|
i18n: "notification.positive.lnsRecordUpdated",
|
|
sending: false
|
|
});
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
proveTransaction(txid, address, message) {
|
|
const _address = address.trim() === "" ? null : address;
|
|
const _message = message.trim() === "" ? null : message;
|
|
|
|
const rpc_endpoint = _address ? "get_tx_proof" : "get_spend_proof";
|
|
const params = {
|
|
txid,
|
|
address: _address,
|
|
message: _message
|
|
};
|
|
|
|
this.sendGateway("set_prove_transaction_status", {
|
|
code: 1,
|
|
message: "",
|
|
state: {}
|
|
});
|
|
|
|
this.sendRPC(rpc_endpoint, params).then(data => {
|
|
if (data.hasOwnProperty("error")) {
|
|
let error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
this.sendGateway("set_prove_transaction_status", {
|
|
code: -1,
|
|
message: error,
|
|
state: {}
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.sendGateway("set_prove_transaction_status", {
|
|
code: 0,
|
|
message: "",
|
|
state: {
|
|
txid,
|
|
...(data.result || {})
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
checkTransactionProof(signature, txid, address, message) {
|
|
const _address = address.trim() === "" ? null : address;
|
|
const _message = message.trim() === "" ? null : message;
|
|
|
|
const rpc_endpoint = _address ? "check_tx_proof" : "check_spend_proof";
|
|
const params = {
|
|
txid,
|
|
signature,
|
|
address: _address,
|
|
message: _message
|
|
};
|
|
|
|
this.sendGateway("set_check_transaction_status", {
|
|
code: 1,
|
|
message: "",
|
|
state: {}
|
|
});
|
|
|
|
this.sendRPC(rpc_endpoint, params).then(data => {
|
|
if (data.hasOwnProperty("error")) {
|
|
let error =
|
|
data.error.message.charAt(0).toUpperCase() +
|
|
data.error.message.slice(1);
|
|
this.sendGateway("set_check_transaction_status", {
|
|
code: -1,
|
|
message: error,
|
|
state: {}
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.sendGateway("set_check_transaction_status", {
|
|
code: 0,
|
|
message: "",
|
|
state: {
|
|
txid,
|
|
...(data.result || {})
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
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: "notification.errors.internalError",
|
|
spend_key: -1,
|
|
view_key: -1
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("set_wallet_data", {
|
|
secret: {
|
|
mnemonic: "notification.errors.invalidPassword",
|
|
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 => {
|
|
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) {
|
|
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 => {
|
|
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: []
|
|
}
|
|
};
|
|
|
|
const types = [
|
|
"in",
|
|
"out",
|
|
"pending",
|
|
"failed",
|
|
"pool",
|
|
"miner",
|
|
"snode",
|
|
"gov",
|
|
"stake"
|
|
];
|
|
types.forEach(type => {
|
|
if (data.result.hasOwnProperty(type)) {
|
|
wallet.transactions.tx_list = wallet.transactions.tx_list.concat(
|
|
data.result[type]
|
|
);
|
|
}
|
|
});
|
|
|
|
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 => {
|
|
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: []
|
|
}
|
|
};
|
|
|
|
const entries = data.result.entries || [];
|
|
const addresses = entries.map(e => {
|
|
const entry = { ...e };
|
|
const 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);
|
|
}
|
|
|
|
return entry;
|
|
});
|
|
|
|
for (const entry of addresses) {
|
|
const list = entry.starred
|
|
? wallet.address_list.address_book_starred
|
|
: wallet.address_list.address_book;
|
|
const hasAddress = list.find(a => {
|
|
return (
|
|
a.address === entry.address &&
|
|
a.name === entry.name &&
|
|
a.payment_id === entry.payment_id
|
|
);
|
|
});
|
|
if (!hasAddress) {
|
|
list.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(() => {
|
|
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(() => {
|
|
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(() => {
|
|
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",
|
|
i18n: "notification.errors.internalError",
|
|
timeout: 2000
|
|
});
|
|
return;
|
|
}
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("show_notification", {
|
|
type: "negative",
|
|
i18n: "notification.errors.invalidPassword",
|
|
timeout: 2000
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (filename == null) {
|
|
filename = path.join(
|
|
this.wallet_data_dir,
|
|
"images",
|
|
this.wallet_state.name,
|
|
"key_image_export"
|
|
);
|
|
} else {
|
|
filename = path.join(filename, "key_image_export");
|
|
}
|
|
|
|
const onError = () =>
|
|
this.sendGateway("show_notification", {
|
|
type: "negative",
|
|
i18n: "notification.errors.keyImages.exporting",
|
|
timeout: 2000
|
|
});
|
|
|
|
this.sendRPC("export_key_images")
|
|
.then(data => {
|
|
if (
|
|
data.hasOwnProperty("error") ||
|
|
!data.hasOwnProperty("result")
|
|
) {
|
|
onError();
|
|
return;
|
|
}
|
|
|
|
if (data.result.signed_key_images) {
|
|
fs.outputJSONSync(filename, data.result.signed_key_images);
|
|
this.sendGateway("show_notification", {
|
|
i18n: [
|
|
"notification.positive.keyImages.exported",
|
|
{ filename }
|
|
],
|
|
timeout: 2000
|
|
});
|
|
} else {
|
|
this.sendGateway("show_notification", {
|
|
type: "warning",
|
|
textColor: "black",
|
|
i18n: "notification.warnings.noKeyImageExport",
|
|
timeout: 2000
|
|
});
|
|
}
|
|
})
|
|
.catch(onError);
|
|
}
|
|
);
|
|
}
|
|
|
|
importKeyImages(password, filename = null) {
|
|
crypto.pbkdf2(
|
|
password,
|
|
this.auth[2],
|
|
1000,
|
|
64,
|
|
"sha512",
|
|
(err, password_hash) => {
|
|
if (err) {
|
|
this.sendGateway("show_notification", {
|
|
type: "negative",
|
|
i18n: "notification.errors.internalError",
|
|
timeout: 2000
|
|
});
|
|
return;
|
|
}
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("show_notification", {
|
|
type: "negative",
|
|
i18n: "notification.errors.invalidPassword",
|
|
timeout: 2000
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (filename == null) {
|
|
filename = path.join(
|
|
this.wallet_data_dir,
|
|
"images",
|
|
this.wallet_state.name,
|
|
"key_image_export"
|
|
);
|
|
}
|
|
|
|
const onError = i18n =>
|
|
this.sendGateway("show_notification", {
|
|
type: "negative",
|
|
i18n,
|
|
timeout: 2000
|
|
});
|
|
|
|
fs.readJSON(filename)
|
|
.then(signed_key_images => {
|
|
this.sendRPC("import_key_images", {
|
|
signed_key_images
|
|
}).then(data => {
|
|
if (
|
|
data.hasOwnProperty("error") ||
|
|
!data.hasOwnProperty("result")
|
|
) {
|
|
onError("notification.errors.keyImages.importing");
|
|
return;
|
|
}
|
|
|
|
this.sendGateway("show_notification", {
|
|
i18n: "notification.positive.keyImages.imported",
|
|
timeout: 2000
|
|
});
|
|
});
|
|
})
|
|
.catch(() => onError("notification.errors.keyImages.reading"));
|
|
}
|
|
);
|
|
}
|
|
|
|
copyOldGuiWallets(wallets) {
|
|
this.sendGateway("set_old_gui_import_status", {
|
|
code: 1,
|
|
failed_wallets: []
|
|
});
|
|
|
|
/*
|
|
Old wallets were in the following format:
|
|
wallets:
|
|
<name>:
|
|
<name>
|
|
<name>.keys
|
|
<name>.address.txt
|
|
|
|
We need to change it so it becomes:
|
|
wallets:
|
|
<name>
|
|
<name>.keys
|
|
<name>.address.txt
|
|
*/
|
|
|
|
const failed_wallets = [];
|
|
|
|
for (const wallet of wallets) {
|
|
const { type, directory } = wallet;
|
|
|
|
const old_gui_path = path.join(this.wallet_dir, "old-gui");
|
|
const dir_path = path.join(this.wallet_dir, directory);
|
|
const stat = fs.statSync(dir_path);
|
|
if (!stat.isDirectory()) continue;
|
|
|
|
// Make sure the directory has the keys file
|
|
const wallet_file = path.join(dir_path, directory);
|
|
const key_file = wallet_file + ".keys";
|
|
|
|
// If we don't have them then don't bother copying
|
|
if (!fs.existsSync(key_file)) {
|
|
failed_wallets.push(directory);
|
|
continue;
|
|
}
|
|
|
|
// Copy out the file into the relevant directory
|
|
const destination = path.join(this.dirs[type], "wallets");
|
|
if (!fs.existsSync(destination)) fs.mkdirpSync(destination);
|
|
|
|
try {
|
|
// Don't move file if we already have copied the keys file
|
|
if (fs.existsSync(path.join(destination, directory) + ".keys")) {
|
|
failed_wallets.push(directory);
|
|
continue;
|
|
}
|
|
|
|
// Archive the folder
|
|
if (!fs.existsSync(old_gui_path)) fs.mkdirpSync(old_gui_path);
|
|
const archive_path = path.join(old_gui_path, directory);
|
|
fs.moveSync(dir_path, archive_path, { overwrite: true });
|
|
|
|
// Copy contents of archived folder into the wallet folder
|
|
fs.copySync(archive_path, this.wallet_dir, { overwrite: true });
|
|
} catch (e) {
|
|
failed_wallets.push(directory);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
this.sendGateway("set_old_gui_import_status", {
|
|
code: 0,
|
|
failed_wallets
|
|
});
|
|
this.listWallets();
|
|
}
|
|
|
|
listWallets(legacy = false) {
|
|
let wallets = {
|
|
list: [],
|
|
directories: []
|
|
};
|
|
|
|
let walletFiles = [];
|
|
try {
|
|
walletFiles = fs.readdirSync(this.wallet_dir);
|
|
} catch (e) {
|
|
this.sendGateway("show_notification", {
|
|
type: "negative",
|
|
i18n: "notification.errors.failedWalletRead",
|
|
timeout: 2000
|
|
});
|
|
return;
|
|
}
|
|
|
|
walletFiles.forEach(filename => {
|
|
try {
|
|
switch (filename) {
|
|
case ".DS_Store":
|
|
case ".DS_Store?":
|
|
case "._.DS_Store":
|
|
case ".Spotlight-V100":
|
|
case ".Trashes":
|
|
case "ehthumbs.db":
|
|
case "Thumbs.db":
|
|
case "old-gui":
|
|
return;
|
|
}
|
|
|
|
// If it's a directory then check if it's an old gui wallet
|
|
const name = path.join(this.wallet_dir, filename);
|
|
const stat = fs.statSync(name);
|
|
if (stat.isDirectory()) {
|
|
// Make sure the directory has keys file
|
|
const wallet_file = path.join(name, filename);
|
|
const key_file = wallet_file + ".keys";
|
|
|
|
// If we have them then it is an old gui wallet
|
|
if (fs.existsSync(key_file)) {
|
|
wallets.directories.push(filename);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Exclude all files without a keys extension
|
|
if (path.extname(filename) !== ".keys") return;
|
|
|
|
const wallet_name = path.parse(filename).name;
|
|
if (!wallet_name) return;
|
|
|
|
let wallet_data = {
|
|
name: wallet_name,
|
|
address: null,
|
|
password_protected: null
|
|
};
|
|
|
|
if (
|
|
fs.existsSync(path.join(this.wallet_dir, wallet_name + ".meta.json"))
|
|
) {
|
|
let meta = fs.readFileSync(
|
|
path.join(this.wallet_dir, wallet_name + ".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, wallet_name + ".address.txt")
|
|
)
|
|
) {
|
|
let address = fs.readFileSync(
|
|
path.join(this.wallet_dir, wallet_name + ".address.txt"),
|
|
"utf8"
|
|
);
|
|
if (address) {
|
|
wallet_data.address = address;
|
|
}
|
|
}
|
|
|
|
wallets.list.push(wallet_data);
|
|
} catch (e) {
|
|
// Something went wrong
|
|
}
|
|
});
|
|
|
|
// 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++) {
|
|
try {
|
|
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
|
|
});
|
|
} catch (e) {
|
|
// Something went wrong
|
|
}
|
|
}
|
|
}
|
|
|
|
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",
|
|
i18n: "notification.errors.internalError",
|
|
timeout: 2000
|
|
});
|
|
return;
|
|
}
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("show_notification", {
|
|
type: "negative",
|
|
i18n: "notification.errors.invalidOldPassword",
|
|
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",
|
|
i18n: "notification.errors.changingPassword",
|
|
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", {
|
|
i18n: "notification.positive.passwordUpdated",
|
|
timeout: 2000
|
|
});
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
deleteWallet(password) {
|
|
crypto.pbkdf2(
|
|
password,
|
|
this.auth[2],
|
|
1000,
|
|
64,
|
|
"sha512",
|
|
(err, password_hash) => {
|
|
if (err) {
|
|
this.sendGateway("show_notification", {
|
|
type: "negative",
|
|
i18n: "notification.errors.internalError",
|
|
timeout: 2000
|
|
});
|
|
return;
|
|
}
|
|
if (!this.isValidPasswordHash(password_hash)) {
|
|
this.sendGateway("show_notification", {
|
|
type: "negative",
|
|
i18n: "notification.errors.invalidPassword",
|
|
timeout: 2000
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.sendGateway("show_loading", {
|
|
message: "Deleting wallet"
|
|
});
|
|
|
|
let wallet_path = path.join(this.wallet_dir, this.wallet_state.name);
|
|
this.closeWallet().then(() => {
|
|
try {
|
|
if (fs.existsSync(wallet_path + ".keys"))
|
|
fs.unlinkSync(wallet_path + ".keys");
|
|
if (fs.existsSync(wallet_path + ".address.txt"))
|
|
fs.unlinkSync(wallet_path + ".address.txt");
|
|
if (fs.existsSync(wallet_path)) fs.unlinkSync(wallet_path);
|
|
} catch (e) {
|
|
console.warn(`Failed to delete wallet files: ${e}`);
|
|
}
|
|
|
|
this.listWallets();
|
|
this.sendGateway("hide_loading");
|
|
this.sendGateway("return_to_wallet_select");
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
async saveWallet() {
|
|
await this.sendRPC("store");
|
|
}
|
|
|
|
async closeWallet() {
|
|
clearInterval(this.heartbeat);
|
|
clearInterval(this.lnsHeartbeat);
|
|
this.wallet_state = {
|
|
open: false,
|
|
name: "",
|
|
password_hash: null,
|
|
balance: null,
|
|
unlocked_balance: null,
|
|
lnsRecords: []
|
|
};
|
|
|
|
this.purchasedNames = {};
|
|
|
|
await this.saveWallet();
|
|
await this.sendRPC("close_wallet");
|
|
}
|
|
|
|
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 > 0) {
|
|
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);
|
|
}
|
|
|
|
async quit() {
|
|
return new Promise(resolve => {
|
|
if (!this.walletRPCProcess) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
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(() => {
|
|
if (this.walletRPCProcess) {
|
|
this.walletRPCProcess.on("close", () => {
|
|
this.agent.destroy();
|
|
clearTimeout(this.forceKill);
|
|
resolve();
|
|
});
|
|
|
|
// Force kill after 20 seconds
|
|
this.forceKill = setTimeout(() => {
|
|
if (this.walletRPCProcess) {
|
|
this.walletRPCProcess.kill("SIGKILL");
|
|
}
|
|
}, 20000);
|
|
|
|
// Force kill if the rpc is syncing
|
|
const signal = this.isRPCSyncing ? "SIGKILL" : "SIGTERM";
|
|
this.walletRPCProcess.kill(signal);
|
|
} else {
|
|
resolve();
|
|
}
|
|
}, 2500);
|
|
});
|
|
}
|
|
}
|