const fs = require('fs'); const os = require('os'); const path = require('path'); let childProcess = require('child_process'); let textEncoding = require('text-encoding'); let TextDecoder = textEncoding.TextDecoder; import { platform, cliPath } from './binaries'; export class WowRpc { // ps -ef | grep defunct | grep -v grep | cut -b8-20 | xargs kill -9 constructor(wowdir, cli_path) { this._wowdir = wowdir; this._cli_process = null; this._cli_log_path = path.join(os.tmpdir(), 'wowlight-wallet.log'); if(!cli_path) { this._cli_path = cliPath; } else { this._cli_path = cli_path; } console.log('WowRPC bin path: ' + this._cli_path); this._wallet_path = ''; this._cli_wallet_password = ''; this._cli_wallet_address = null; this._cli_daemon_address = ''; this._cli_wallet_selected_account = null; this._cli_balance_unlocked = null; this._cli_balance = null; this._cli_txs = []; this._version = ""; this._buffer = ""; this._sending = false; // this will hold the newly created wallet details: address, view_key, seed, password this._create_wallet = {}; this._create_wallet_tmp_path = path.join(os.tmpdir(), 'wowtmp'); this._cli_args_default = [ '--use-english-language-names', '--log-file', this._cli_log_path ] this._cli_args_create_wallet = [ '--generate-new-wallet', this._create_wallet_tmp_path ] // '--restore-deterministic-wallet' recover from seed // lil' state machine // 0: not spawned // 1: spawned - not authed // 2: spawned - authed, not refreshed // 3: spawned - authed/refreshed (shows prompt) this._states = { 'cw': 'create wallet', 0: 'not spawned', 1: 'spawned - not authed', 2: 'spawned - authed', 3: 'spawned - starting refresh', 4: 'spawned - ended refresh', 5: 'spawned - showing prompt' }; this._state = 0; this._checkMemPoolTimeout = 10000 this.shown_transfers = false; } commitWallet(wallet_name) { // moves `/tmp/tmpwallet to destination` console.log(`moving into: ${this._wowdir}/${wallet_name}`); let cmd = process.platform === 'win32' ? 'move' : 'mv'; let dest_path = path.join(this._wowdir, wallet_name); console.log(dest_path); console.log(`${cmd} "${this._create_wallet_tmp_path}" "${dest_path}"`); let cmd_wcache = `${cmd} "${this._create_wallet_tmp_path}" "${dest_path}"`; let cmd_wkeys = `${cmd} "${this._create_wallet_tmp_path}.keys" "${dest_path}.keys"`; console.log(`[CMD] ${cmd_wcache}`); console.log(`[CMD] ${cmd_wkeys}`); let wow = childProcess.execSync(cmd_wcache); let wow2 = childProcess.execSync(cmd_wkeys); return 1; } _checkMemPool(){ if(this._state === 5){ this._sendCmd('show_transfers'); this._sendCmd('balance'); setTimeout(this._checkMemPool.bind(this), this._checkMemPoolTimeout); } } _sendPassword(){ this.log('sending pass'); this._cli_process.stdin.write(`${this._cli_wallet_password}\n`); } _sendCmd(cmd){ if(this._sending && cmd === 'show_transfers') { // skip `show_transfers` command when making a transaction return; } if(cmd === 'show_transfers') { this.shown_transfers = true; if(this._state !== 5){ return; } } this.log(`Sending cmd "${cmd}"`); try { this._cli_process.stdin.write(`${cmd}\n`); } catch(err) { console.log(err); return; } if(cmd.startsWith('set')){ this._sendPassword(); } } _parse_stdout(data){ if(platform === 'win'){ data = data.replace(/\r/g, ""); } // detect incoming transaction let re_incoming_tx = /Height \d+, txid \<([a-zA-Z0-9]+)\>, ([0-9]+.[0-9]+), idx/g; if(data.match(re_incoming_tx)){ let matches; while ((matches = re_incoming_tx.exec(data)) !== null) { this._paymentReceived(matches[1], matches[2]); } } if(this._state === 5 && data.match(/Error: invalid password/)){ this._sendCmd(this._cli_wallet_password); } // detect show_transfers output let re_transfers_in = /([0-9]+|pool|pending)[ ]+(in|out)[ ]+(.+?(?= ))[ ]+([0-9]+.[0-9]+)[ ]+([a-zA-Z0-9]+)/g; if(this._state === 5 && data.match(re_transfers_in)){ let matches; let pushed = 0; while ((matches = re_transfers_in.exec(data)) !== null) { let blockheight = matches[1]; if(WowRpc.isNumeric(blockheight)){ blockheight = parseInt(blockheight); } let tx = { blockheight: blockheight, in: matches[2] === 'in' ? 'in' : 'out', date: matches[3], amount: matches[4], id: matches[5] }; if(this.getTxId(tx.id) === false){ this._cli_txs.push(tx); pushed += 1; } } if(pushed > 0){ this.log(`discovered ${pushed} tx's`); this.wowCall(this.onWalletTxsChanged, this._cli_txs); } return; } // detect cli startup if(data.match(/This is the command line wownero wallet./)){ this._setState(1); } // detect successful transfer if(data.match(/Transaction successfully submitted/)){ this._sending = false; this.onTransactionCompleted(); } if(data.match(/Opened wallet: W/)){ this._cli_wallet_address = data.substring(15, data.indexOf('\n')); this.log(`Wallet: ${this._cli_wallet_address}`); this._setState(2); } // if(data === `Logging to ${this._cli_log_path}\n`){ // this._sendPassword(); // } if(data.match(/Starting refresh.../)){ this._setState(3); } if(data.match(/Error: /)){ let re_error = /Error: (.*)/g; let re_error_offline_daemon = /wallet failed to connect to daemon: (.*):(\d+)./g; console.log(`||${data}||`); let re_error_offline_daemon_matched = re_error_offline_daemon.exec(data); if(re_error_offline_daemon_matched){ let msg = `Remote node '${re_error_offline_daemon_matched[1]}:${re_error_offline_daemon_matched[2]}' offline, please use another.`; this.onError(msg); return; } let re_error_matched = re_error.exec(data); if(re_error_matched){ this.onError(re_error_matched[1]); } else { this.onError("Unknown error occurred"); } } if(data.match(/Refresh done, blocks received:/)){ this._setState(4); this.onWalletOpened({ 'wallet_path': this._wallet_path, 'address': this._cli_wallet_address }); } let currently_selected = data.match(/Currently selected account: \[([0-9])\]/); if(currently_selected){ this._cli_wallet_selected_account = parseInt(currently_selected[1]); } let balance = data.match('Balance: ([0-9]+.[0-9]+)'); if(balance){ this._setBalance(balance[1]); } let balance_unlocked = data.match('unlocked balance: ([0-9]+.[0-9]+)'); if(balance_unlocked) { this._setBalanceUnlocked(balance_unlocked[1]); } // detect output from command `show_transfers` if(data.match(/\[wallet [a-zA-Z0-9]+\]:/)){ this._setState(5); } let re_height_refresh = /Height (\d+) \/ (\d+)/g; let re_height_refresh_data = re_height_refresh.exec(data); if(re_height_refresh_data){ let from = re_height_refresh_data[1]; let to = re_height_refresh_data[2]; this.wowCall(this.onHeightRefresh, {'from': from, 'to': to}); } } _parse_stdout_create_wallet(data){ if(platform === 'win'){ data = data.replace(/\r/g, ""); } // lol windows this._buffer += data; if(data === `Logging to ${this._cli_log_path}\n`){ this._cli_process.stdin.write(`wow\n`); this._cli_process.stdin.write(`wow\n`); // ?? :D } // if(data.match(/List of available languages for your wallet's seed:/)){ // this._cli_process.stdin.write(`1\n`); // } // // if(data.match(/Error: invalid language choice entered./)){ // this._cli_process.stdin.write(`1\n`); // } if(data.match(/Background refresh thread started/)){ let re_addy = /Generated new wallet: (W[o|W][a-zA-Z0-9]{95})/ let re_addy_match = this._buffer.match(re_addy); if(re_addy_match){ this._create_wallet['address'] = re_addy_match[1]; } let re_view_key = /View key: ([0-9a-fA-F]+)\n/ let re_view_key_match = this._buffer.match(re_view_key); if(re_view_key_match){ this._create_wallet['view_key'] = re_view_key_match[1]; } let re_seed = /\*\*\*\*\*\*SEED\n(.*)\n\*\*/ let re_seed_match = this._buffer.match(re_seed); if(re_seed_match){ let seed = re_seed_match[1].trim(); if(seed.split(' ').length !== 25){ this.onCreateWalletFinished("could not get seed; invalid num words"); } this._create_wallet['seed'] = seed; this.kill(); this.onCreateWalletFinished(this._create_wallet); } else { this.kill(); this.onCreateWalletFinished("could not get seed"); } } } createWallet(wowdir, name, password){ try { fs.unlinkSync(`${this._create_wallet_tmp_path}`); } catch(err) { } try { fs.unlinkSync(`${this._create_wallet_tmp_path}.keys`); } catch(err) { } let wowdir_name = path.join(wowdir, name); if(fs.existsSync(`${wowdir_name}`) || fs.existsSync(`${wowdir_name}.keys`)){ this.onCreateWalletFinished(`Wallet already exists: ${wowdir_name}`); return; } let args = this._cli_args_default.concat(this._cli_args_create_wallet); args.push('--password'); if(!password) { args.push(''); } else { args.push(password); } this._setState('cw'); console.log('FINAL ARGS:', args); this._cli_process = childProcess.spawn(this._cli_path, args); this._create_wallet['name'] = name; this._create_wallet['password'] = password; this._cli_process.stdout.on('data', function(data) { data = new TextDecoder("utf-8").decode(data); console.log("[cli] " + data); this._parse_stdout_create_wallet(data); }.bind(this)); this._cli_process.stdout.on('end', function(data) { this._buffer = ""; console.log('PROCESS ENDED!') }); this._cli_process.on('exit', function(code) { this._buffer = ""; if (code !== 0) { console.log('Failed: ' + code) } }); } sendMonies(address, amount){ let cmd = `transfer ${address} ${amount}`; console.log(cmd); this._sending = true; this._sendCmd(cmd) } connect(wallet_path, wallet_password){ this._wallet_path = wallet_path; this._cli_wallet_password = wallet_password; let cli_args = ['--daemon-address', this._cli_daemon_address]; cli_args = cli_args.concat(this._cli_args_default); cli_args.push('--wallet-file'); cli_args.push(wallet_path); cli_args.push('--password'); cli_args.push(this._cli_wallet_password); console.log('FINAL ARGS:', cli_args); this._cli_process = childProcess.spawn(this._cli_path, cli_args); this._cli_process.stdout.on('data', function(data) { data = new TextDecoder("utf-8").decode(data); console.log("[cli] " + data); this._parse_stdout(data); }.bind(this)); this._cli_process.stdout.on('end', function(data) { this._buffer = ""; console.log('PROCESS ENDED!') }); this._cli_process.on("error", function(e) { console.log(e); }); this._cli_process.on('exit', function(code) { this._buffer = ""; if (code !== 0) { console.log('Failed: ' + code) } }); }; exit(){ this._state = 0; this._sendCmd('exit'); // seeya this.log(`Wallet closed: ${this._cli_wallet_address}`); } kill(){ console.log('Wallet kill'); this._buffer = ""; this._state = 0; if(this._cli_process) { this._cli_process.stdout.pause(); this._cli_process.stdin.pause(); this._cli_process.kill('SIGKILL'); // F this.log(`Wallet closed: ${this._cli_wallet_address}`); } } _paymentReceived(tx_id, amount){ this.log(`Incoming tx: ${tx_id}, ${amount}`); } _setBalance(balance){ this.log(`New balance: ${balance}`); this._cli_balance = parseFloat(balance); this.wowCall(this.onWalletBalanceChanged, this._cli_balance); } _setBalanceUnlocked(balance){ this.log(`New balance unlocked: ${balance}`); this._cli_balance_unlocked = parseFloat(balance); this.wowCall(this.onWalletBalanceUnlockedChanged, this._cli_balance_unlocked); } _setState(state){ if(state === this._state) return; this.log(`New state: ${this._states[state]}`); this._state = state; if(state === 5){ // connect check mempool loop // this._sendCmd('set ask-password 0'); this._checkMemPool(); } this.onStateChanged(state); } getTxId(txid){ let found = false; this._cli_txs.forEach(function(obj){ if(obj.id === txid){ found = true; } }); return found; } log(msg){ console.log('\x1b[36m%s\x1b[0m', msg); } wowCall(fn, data){ // ¯\_(ツ)_/¯ if(fn != null){ fn(data); } } getEmbeddedVersion(){ console.log('Retrieving embedded version.'); let args = ['--version']; this._cli_process = childProcess.spawn(this._cli_path, args); this._cli_process.stdout.on('data', (data) => { data = new TextDecoder("utf-8").decode(data); console.log("[cli] " + data); let version = data.trim().split(" ").slice(1).join(" ").trim(); this._cli_process.kill(); this.onEmbeddedVersion(version); }); } onEmbeddedVersion(version){ // overloaded } onWalletOpened(data){ // overloaded } onCreateWalletFinished(){ // overloaded } onWalletAddressChanged(){ // overloaded } onWalletSelectedAccountChanged() { // overloaded } onWalletBalanceUnlockedChanged() { // overloaded } onWalletBalanceChanged() { // overloaded } onWalletTxsChanged(data) { // overloaded } onTransactionCompleted(){ // overloaded } onHeightRefresh(data){ // overloaded } onError(msg){ // overloaded } onStateChanged(state){ // overloaded } static isNumeric(value) { return /^\d+$/.test(value); } }