From 1ddc98649342368795d5cdc7f5dd9871333da7c3 Mon Sep 17 00:00:00 2001 From: n9lsjr Date: Sat, 30 Dec 2023 23:52:11 +0100 Subject: [PATCH] Refactoor, added messages.cjs, wallet.cjs --- build.config.json | 2 + src/backend/electron.cjs | 1387 ++------------------------------------ src/backend/messages.cjs | 989 +++++++++++++++++++++++++++ src/backend/utils.cjs | 49 +- src/backend/wallet.cjs | 276 ++++++++ 5 files changed, 1353 insertions(+), 1350 deletions(-) create mode 100644 src/backend/messages.cjs create mode 100644 src/backend/wallet.cjs diff --git a/build.config.json b/build.config.json index 42dc5e2a..519cf69b 100644 --- a/build.config.json +++ b/build.config.json @@ -40,6 +40,8 @@ "src/backend/utils.cjs", "src/backend/crypto.cjs", "src/backend/swarm.cjs", + "src/backend/messages.cjs", + "src/backend/wallet.cjs", { "from": "build", "to": "" diff --git a/src/backend/electron.cjs b/src/backend/electron.cjs index 780aec40..b94c4325 100644 --- a/src/backend/electron.cjs +++ b/src/backend/electron.cjs @@ -12,85 +12,40 @@ const serve = require('electron-serve') const { join } = require('path') const { JSONFile, Low } = require('@commonify/lowdb') const fs = require('fs') -const files = require('fs/promises') const WB = require('kryptokrona-wallet-backend-js') -const { default: fetch } = require('electron-fetch') const nacl = require('tweetnacl') -const naclUtil = require('tweetnacl-util') -const naclSealed = require('tweetnacl-sealed-box') -const { extraDataToMessage } = require('hugin-crypto') -const sanitizeHtml = require('sanitize-html') -const en = require('int-encoder') -const sqlite3 = require('sqlite3').verbose() const { autoUpdater } = require('electron-updater') -const notifier = require('node-notifier') -const {createReadStream} = require("fs"); const { expand_sdp_answer, expand_sdp_offer, parse_sdp } = require("./sdp.cjs") const { sleep, - trimExtra, - fromHex, - nonceFromTimestamp, randomKey, - hexToUint, - toHex, parseCall, hash, - sanitize_pm_message } = require("./utils.cjs") + hexToUint } = require("./utils.cjs") const { loadDB, - saveHash, - loadGroups, loadKeys, getGroups, - saveGroupMsg, - unBlockContact, - blockContact, - removeMessages, - removeContact, - removeGroup, - addGroup, - removeBoard, loadBlockList, - getConversation, - getConversations, - loadKnownTxs, getMyBoardList, - getBoardMsgs, - getMessages, - getReplies, getGroupReply, - getReply, printGroup, - printBoard, - saveMsg, - saveBoardMessage, - saveThisContact, - groupMessageExists, - messageExists, - getContacts, firstContact, welcomeMessage, welcomeBoardMessage, addBoard } = require("./database.cjs") -const { - Address, - Crypto, - CryptoNote, -} = require('kryptokrona-utils') +const { loadDaemon, createWallet, importFromSeed, loginWallet, loadWallet, saveWallet, saveWalletToFile, pickNode, saveNode, loadMiscData, checkPassword, createMessageSubWallet, getPrivKeys, getXKRKeypair} = require('./wallet.cjs') +const { newBeam, endBeam, addLocalFile, requestDownload, removeLocalFile } = require("./beam.cjs") +const { newSwarm, endSwarm} = require("./swarm.cjs") +const { startMessageSyncer, sendMessage, optimizeMessages} = require('./messages.cjs') -const { newBeam, endBeam, sendBeamMessage, addLocalFile, requestDownload, removeLocalFile } = require("./beam.cjs") -const { newSwarm, sendSwarmMessage, endSwarm} = require("./swarm.cjs") const Store = require('electron-store'); const appRoot = require('app-root-dir').get().replace('app.asar', '') const appBin = appRoot + '/bin/' -const crypto = new Crypto() -const xkrUtils = new CryptoNote() const userDataDir = app.getPath('userData') -const appPath = app.getAppPath() const downloadDir = app.getPath('downloads') const dbPath = userDataDir + '/SQLmessages.db' const serveURL = serve({ directory: '.' }) @@ -101,11 +56,6 @@ const DHT = require('@hyperswarm/dht') let mainWindow -let daemon -let nodeUrl -let nodePort -//node will contain {node: nodeUrl, port: nodePort} -let node //Create misc.db const file = join(userDataDir, 'misc.db') @@ -117,13 +67,7 @@ const welcomeAddress = 'SEKReYU57DLLvUjNzmjVhaK7jqc8SdZZ3cyKJS5f4gWXK4NQQYChzKUUwzCGhgqUPkWQypeR94rqpgMPjXWG9ijnZKNw2LWXnZU1' let js_wallet -let walletName -let known_keys = [] -let known_pool_txs = [] -let myPassword -let my_boards = [] let block_list = [] -let hashed_pass = "" try { require('electron-reloader')(module) @@ -274,6 +218,8 @@ ipcMain.on('app', (data) => { startCheck() startDatabase() + wallet() + if (dev) { console.log('Running in development') mainWindow.openDevTools() @@ -305,24 +251,21 @@ ipcMain.on('app', (data) => { } }) +const sender = (channel, data) => { + mainWindow.webContents.send(channel, data) +} function startDatabase() { loadDB(userDataDir, dbPath) - } -const sender = (channel, data) => { - mainWindow.webContents.send(channel, data) -} - -function getXKRKeypair() { - const [privateSpendKey, privateViewKey] = js_wallet.getPrimaryAddressPrivateKeys() - return { privateSpendKey: privateSpendKey, privateViewKey: privateViewKey } +function wallet() { + loadWallet(sender) } function getKeyPair() { // return new Promise((resolve) => setTimeout(resolve, ms)); - const [privateSpendKey, privateViewKey] = js_wallet.getPrimaryAddressPrivateKeys() + const [privateSpendKey, privateViewKey] = getPrivKeys() let secretKey = hexToUint(privateSpendKey) let keyPair = nacl.box.keyPair.fromSecretKey(secretKey) return keyPair @@ -333,22 +276,6 @@ function getMsgKey() { return Buffer.from(naclPubKey).toString('hex') } -const checkNodeStatus = async (node) => { - - try { - const req = await fetch('http://' + node.node + ':' + node.port.toString() + '/getinfo') - - const res = await req.json() - - if (res.status === 'OK') return true - } catch (e) { - console.log('Node error') - } - - mainWindow.webContents.send('node-not-ok') - return false -} - async function startCheck() { store.set({ @@ -358,9 +285,9 @@ async function startCheck() { }); if (fs.existsSync(userDataDir + '/misc.db')) { - loadHugin() + loadHugin() ipcMain.on('login', async (event, data) => { - loadAccount(data) + loadAccount(data) }) } else { @@ -371,13 +298,10 @@ async function startCheck() { } async function loadHugin() { - await db.read() - let walletName = db.data.walletNames - nodeUrl = db.data.node.node - nodePort = db.data.node.port - let node = { node: nodeUrl, port: nodePort } + const [node, walletName] = await loadMiscData() + console.log("Loaded misc",[node, walletName] ) mainWindow.webContents.send('wallet-exist', true, walletName, node) - daemon = new WB.Daemon(nodeUrl, nodePort) + loadDaemon(node.node, node.port) } const startWallet = async (data, node) => { @@ -385,35 +309,29 @@ const startWallet = async (data, node) => { } async function loadAccount(data) { - nodeUrl = data.node - nodePort = data.port - node = { node: nodeUrl, port: nodePort } - db.data.node = node - await db.write() + let node = {node: data.node, port: data.port} + console.log("load acc node", node) + saveNode(node) startWallet(data, node) } + //Create account async function createAccount(accountData) { - let walletName = accountData.walletName - let myPassword = accountData.password - nodeUrl = accountData.node - nodePort = accountData.port - let node = { node: nodeUrl, port: nodePort } - - daemon = new WB.Daemon(nodeUrl, nodePort) + const walletName = accountData.walletName + const myPassword = accountData.password + const node = { node: accountData.node, port: accountData.port } + + loadDaemon(node.node, node.port) if (!accountData.blockheight) { accountData.blockheight = 1 } const [js_wallet, error] = accountData.mnemonic.length > 0 - ? await WB.WalletBackend.importWalletFromSeed( - daemon, + ? await importFromSeed( accountData.blockheight, - accountData.mnemonic - ) - : [await WB.WalletBackend.createWallet(daemon), null] - + accountData.mnemonic) + : await createWallet() //Create welcome PM message welcomeMessage() //Create Hugin welcome contact @@ -441,107 +359,22 @@ async function createAccount(accountData) { } - -async function logIntoWallet(walletName, password) { - let parsed_wallet - let json_wallet = await openWallet(walletName, password) - try { - //Try parse json wallet - parsed_wallet = JSON.parse(json_wallet) - //Open encrypted json wallet - const [js_wallet, error] = await WB.WalletBackend.openWalletFromEncryptedString( - daemon, - parsed_wallet, - password - ) - if (error) { - console.log('Failed to open wallet: ' + error.toString()) - return 'Wrong password' - } - return js_wallet - } catch (err) { - console.log('error parsing wallet', err) - let wallet = await openWalletFromFile(walletName, password) - return wallet - //Probably a wallet file, return this - } -} - -async function openWalletFromFile(walletName, password) { - console.log('Open wallet from file') - const [js_wallet, error] = await WB.WalletBackend.openWalletFromFile( - daemon, - userDataDir + '/' + walletName + '.wallet', - password - ) - if (error) { - console.log('Failed to open wallet: ' + error.toString()) - return 'Wrong password' - } - return js_wallet -} - -async function verifyPassword(password, hashed_pass) { - if (await checkPass(password, hashed_pass)) { - await sleep(1000) - mainWindow.webContents.send('login-success') - return true - } else { - mainWindow.webContents.send('login-failed') - return false - } -} - async function login(walletName, password) { - js_wallet = await logIntoWallet(walletName, password) - if (js_wallet === 'Wrong password') { - mainWindow.webContents.send('login-failed') - return false - } - return true -} - -async function checkNodeAndPassword(password, node) { - //If we are already logged in - if (hashed_pass.length) { - verifyPassword(password, hashed_pass) - return false - } - - let nodeOnline = await checkNodeStatus(node) + const [loggedIn, wallet] = await loginWallet(walletName, password) + if (!loggedIn) return false + js_wallet = wallet return true } -async function loadCheckedTxs() { - - //Load known pool txs from db. - let checkedTxs = await loadKnownTxs() - let arrayLength = checkedTxs.length - - if (arrayLength > 0) { - checkedTxs = checkedTxs.slice(arrayLength - 200, arrayLength - 1).map(function (knownTX) { - return knownTX.hash - }) - - } else { - checkedTxs = [] - } - - return checkedTxs -} - async function start_js_wallet(walletName, password, node) { - if (!await checkNodeAndPassword(password, node)) return + if (await checkPassword(password, node)) return if (!await login(walletName, password)) return - hashed_pass = await hash(password) - pickNode(node.node + ":" + node.port.toString()) //Load known public keys and contacts let [myContacts, keys] = await loadKeys((start = true)) - known_keys = keys mainWindow.webContents.send('contacts', myContacts) //Sleep 300ms await sleep(300) @@ -550,7 +383,6 @@ async function start_js_wallet(walletName, password, node) { //Start wallet sync process await js_wallet.start() await createMessageSubWallet(); - let checkedTxs = await loadCheckedTxs() let my_groups = await getGroups() block_list = await loadBlockList() my_boards = await getMyBoardList() @@ -599,43 +431,9 @@ async function start_js_wallet(walletName, password, node) { console.log('Started wallet') await sleep(500) console.log('Loading Sync') - //Load knownTxsIds to backgroundSyncMessages on startup - backgroundSyncMessages(checkedTxs) - while (true) { - try { - //Start syncing - await sleep(1000 * 10) - - backgroundSyncMessages() - - const [walletBlockCount, localDaemonBlockCount, networkBlockCount] = - js_wallet.getSyncStatus() - mainWindow.webContents.send('node-sync-data', { - walletBlockCount, - localDaemonBlockCount, - networkBlockCount, - }) - - if (localDaemonBlockCount - walletBlockCount < 2) { - // Diff between wallet height and node height is 1 or 0, we are synced - console.log('**********SYNCED**********') - console.log('My Wallet ', walletBlockCount) - console.log('The Network', networkBlockCount) - mainWindow.webContents.send('sync', 'Synced') - } else { - //If wallet is somehow stuck at block 0 for new users due to bad node connection, reset to the last 100 blocks. - if (walletBlockCount === 0) { - await js_wallet.reset(networkBlockCount - 100) - } - console.log('*.[~~~].SYNCING BLOCKS.[~~~].*') - console.log('My Wallet ', walletBlockCount) - console.log('The Network', networkBlockCount) - mainWindow.webContents.send('sync', 'Syncing') - } - } catch (err) { - console.log(err) - } - } + + startMessageSyncer(sender, keys, block_list) + } function incomingTx(transaction) { @@ -660,902 +458,6 @@ function sendNodeInfo() { } -async function encryptWallet(wallet, pass) { - const encrypted_wallet = await wallet.encryptWalletToString(pass) - return encrypted_wallet -} - -async function saveWallet(wallet, walletName, password) { - let my_wallet = await encryptWallet(wallet, password) - let wallet_json = JSON.stringify(my_wallet) - - try { - await files.writeFile(userDataDir + '/' + walletName + '.json', wallet_json) - } catch (err) { - console.log('Wallet json saving error, revert to saving wallet file') - saveWalletToFile(wallet, walletName, password) - } -} - -function saveWalletToFile(wallet, walletName, password) { - wallet.saveWalletToFile(userDataDir + '/' + walletName + '.wallet', password) -} - -async function openWallet(walletName, password) { - let json_wallet - - try { - json_wallet = await files.readFile(userDataDir + '/' + walletName + '.json') - return json_wallet - } catch (err) { - console.log('Json wallet error, try backup wallet file', err) - } -} - -//Checks the message for a view tag -async function checkForViewTag(extra) { - try { - const rawExtra = trimExtra(extra) - const parsed_box = JSON.parse(rawExtra) - if (parsed_box.vt) { - const [privateSpendKey, privateViewKey] = js_wallet.getPrimaryAddressPrivateKeys() - const derivation = await crypto.generateKeyDerivation(parsed_box.txKey, privateViewKey); - const hashDerivation = await crypto.cn_fast_hash(derivation) - const possibleTag = hashDerivation.substring(0,2) - const view_tag = parsed_box.vt - if (possibleTag === view_tag) { - console.log('**** FOUND VIEWTAG ****') - return true - } - } - } catch (err) { - } - return false -} - -//Try decrypt extra data -async function checkForPrivateMessage(thisExtra) { - let message = await extraDataToMessage(thisExtra, known_keys, getXKRKeypair()) - if (!message) return false - if (message.type === 'sealedbox' || 'box') { - message.sent = false - saveMessage(message) - return true - } -} -//Checks if hugin message is from a group -async function checkForGroupMessage(thisExtra, thisHash) { - try { - let group = trimExtra(thisExtra) - let message = JSON.parse(group) - if (message.sb) { - await decryptGroupMessage(message, thisHash) - return true - } - } catch { - - } - return false -} - -//Validate extradata, here we can add more conditions -function validateExtra(thisExtra, thisHash) { - //Extra too long - if (thisExtra.length > 7000) { - known_pool_txs.push(thisHash) - if (!saveHash(thisHash)) return false - return false; - } - //Check if known tx - if (known_pool_txs.indexOf(thisHash) === -1) { - known_pool_txs.push(thisHash) - return true - } else { - //Tx already known - return false - } -} - -//Set known pool txs on start -function setKnownPoolTxs(checkedTxs) { - //Here we can adjust number of known we send to the node - known_pool_txs = checkedTxs - //Can't send undefined to node, it wont respond - let known = known_pool_txs.filter(a => a !== undefined) - return known -} - -async function backgroundSyncMessages(checkedTxs = false) { - console.log('Background syncing...') - - //First start, set known pool txs - if (checkedTxs) { - known_pool_txs = await setKnownPoolTxs(checkedTxs) - } - - let transactions = await fetchHuginMessages() - if (!transactions) return - decryptHuginMessages(transactions) -} - -async function fetchHuginMessages() { - try { - const resp = await fetch( - 'http://' + nodeUrl + ':' + nodePort.toString() + '/get_pool_changes_lite', - { - method: 'POST', - body: JSON.stringify({ knownTxsIds: known_pool_txs }), - } - ) - - let json = await resp.json() - json = JSON.stringify(json) - .replaceAll('.txPrefix', '') - .replaceAll('transactionPrefixInfo.txHash', 'transactionPrefixInfotxHash') - - json = JSON.parse(json) - - let transactions = json.addedTxs - //Try clearing known pool txs from checked - known_pool_txs = known_pool_txs.filter((n) => !json.deletedTxsIds.includes(n)) - if (transactions.length === 0) { - console.log('Empty array...') - console.log('No incoming messages...') - return false - } - - return transactions - - } catch (e) { - mainWindow.webContents.send('sync', 'Error') - return false - } -} - -async function decryptHuginMessages(transactions) { - - for (const transaction of transactions) { - try { - let thisExtra = transaction.transactionPrefixInfo.extra - let thisHash = transaction.transactionPrefixInfotxHash - if (!validateExtra(thisExtra, thisHash)) continue - if (thisExtra !== undefined && thisExtra.length > 200) { - if (!saveHash(thisHash)) continue - //Check for viewtag - let checkTag = await checkForViewTag(thisExtra) - if (checkTag) { - await checkForPrivateMessage(thisExtra, thisHash) - continue - } - //Check for private message //TODO remove this when viewtags are active - if (await checkForPrivateMessage(thisExtra, thisHash)) continue - //Check for group message - if (await checkForGroupMessage(thisExtra, thisHash)) continue - } - } catch (err) { - console.log(err) - } - } -} - -//Saves contact and nickname to db. -async function saveContact(hugin_address, nickname = false, first = false) { - - let name - if (!nickname) { - name = 'Anon' - } else { - name = nickname - } - let addr = hugin_address.substring(0, 99) - let key = hugin_address.substring(99, 163) - - if (known_keys.indexOf(key) == -1) { - known_keys.push(key) - } - - mainWindow.webContents.send('saved-addr', hugin_address) - - saveThisContact(addr, key, name) - - if (first) { - saveMessage({ - msg: 'New friend added!', - k: key, - from: addr, - chat: addr, - sent: true, - t: Date.now(), - }) - known_keys.pop(key) - } -} - -async function checkPass(pass, oldHash) { - let passHash = await hash(pass) - if (oldHash === passHash) return true - return false -} - -//Saves board message. -async function saveBoardMsg(msg, hash, follow = false) { - return - saveBoardMessage(msg, hash) - saveHash(hash) - if (msg.sent || !follow) return - //Send new board message to frontend. - mainWindow.webContents.send('boardMsg', message) - mainWindow.webContents.send('newBoardMessage', message) -} - -async function saveGroupMessage(msg, hash, time, offchain, channel = false) { - console.log("Savin group message") - let message = await saveGroupMsg(msg, hash, time, offchain, channel) - if (!message) return false - if (!offchain) { - //Send new board message to frontend. - mainWindow.webContents.send('groupMsg', message) - mainWindow.webContents.send('newGroupMessage', message) - } else if (offchain) { - if (message.message === 'ᛊNVITᛊ') return - mainWindow.webContents.send('groupRtcMsg', message) - } -} - - -//Saves private message -async function saveMessage(msg, offchain = false) { - - let [message, addr, key, timestamp, sent] = sanitize_pm_message(msg) - - if (!message) return - - if (await messageExists(timestamp)) return - - //Checking if private msg is a call - let [text, data, is_call, if_sent] = parseCall(msg.msg, addr, sent, offchain, timestamp) - - if (text === "Audio call started" || text === "Video call started" && is_call && !if_sent) { - //Incoming calll - mainWindow.webContents.send('call-incoming', data) - } else if (text === "Call answered" && is_call && !if_sent) { - //Callback - mainWindow.webContents.send('got-callback', data) - } - - //If sent set chat to chat instead of from - if (msg.chat && sent) { - addr = msg.chat - } - - //New message from unknown contact - if (msg.type === 'sealedbox' && !sent) { - let hugin = addr + key - await saveContact(hugin) - } - - message = sanitizeHtml(text) - let newMsg = await saveMsg(message, addr, sent, timestamp, offchain) - if (sent) { - //If sent, update conversation list - mainWindow.webContents.send('sent', newMsg) - return - } - //Send message to front end - mainWindow.webContents.send('newMsg', newMsg) - mainWindow.webContents.send('privateMsg', newMsg) -} - -async function encryptMessage(message, messageKey, sealed = false, toAddr) { - let timestamp = Date.now() - let my_address = await js_wallet.getPrimaryAddress() - const addr = await Address.fromAddress(toAddr) - const [privateSpendKey, privateViewKey] = js_wallet.getPrimaryAddressPrivateKeys() - let xkr_private_key = privateSpendKey - let box - - //Create the view tag using a one time private key and the receiver view key - const keys = await crypto.generateKeys(); - const toKey = addr.m_keys.m_viewKeys.m_publicKey - const outDerivation = await crypto.generateKeyDerivation(toKey, keys.private_key); - const hashDerivation = await crypto.cn_fast_hash(outDerivation) - const viewTag = hashDerivation.substring(0,2) - - if (sealed) { - - let signature = await xkrUtils.signMessage(message, xkr_private_key) - let payload_json = { - from: my_address, - k: Buffer.from(getKeyPair().publicKey).toString('hex'), - msg: message, - s: signature, - } - let payload_json_decoded = naclUtil.decodeUTF8(JSON.stringify(payload_json)) - box = new naclSealed.sealedbox( - payload_json_decoded, - nonceFromTimestamp(timestamp), - hexToUint(messageKey) - ) - } else if (!sealed) { - console.log('Has history, not using sealedbox') - let payload_json = { from: my_address, msg: message } - let payload_json_decoded = naclUtil.decodeUTF8(JSON.stringify(payload_json)) - - box = nacl.box( - payload_json_decoded, - nonceFromTimestamp(timestamp), - hexToUint(messageKey), - getKeyPair().secretKey - ) - } - //Box object - let payload_box = { box: Buffer.from(box).toString('hex'), t: timestamp, txKey: keys.public_key, vt: viewTag } - // Convert json to hex - let payload_hex = toHex(JSON.stringify(payload_box)) - - return payload_hex -} - -async function sendGroupsMessage(message, offchain = false, swarm = false) { - console.log("Sending group msg!") - if (message.m.length === 0) return - const my_address = message.k - const [privateSpendKey, privateViewKey] = js_wallet.getPrimaryAddressPrivateKeys() - const signature = await xkrUtils.signMessage(message.m, privateSpendKey) - const timestamp = parseInt(Date.now()) - const nonce = nonceFromTimestamp(timestamp) - - let group - let reply = '' - - group = message.g - - if (group === undefined) return - if (group.length !== 64) { - return - } - - if (!offchain) { - let balance = await checkBalance() - if (!balance) return - } - - let message_json = { - m: message.m, - k: my_address, - s: signature, - g: group, - n: message.n, - r: reply, - } - - if (message.r) { - message_json.r = message.r - } - - if (message.c) { - message_json.c = message.c - } - - let [mainWallet, subWallet, messageSubWallet] = js_wallet.subWallets.getAddresses() - const payload_unencrypted = naclUtil.decodeUTF8(JSON.stringify(message_json)) - const secretbox = nacl.secretbox(payload_unencrypted, nonce, hexToUint(group)) - - const payload_encrypted = { - sb: Buffer.from(secretbox).toString('hex'), - t: timestamp, - } - - const payload_encrypted_hex = toHex(JSON.stringify(payload_encrypted)) - - if (!offchain) { - let result = await js_wallet.sendTransactionAdvanced( - [[messageSubWallet, 1000]], // destinations, - 3, // mixin - { fixedFee: 1000, isFixedFee: true }, // fee - undefined, //paymentID - [subWallet, messageSubWallet], // subWalletsToTakeFrom - undefined, // changeAddress - true, // relayToNetwork - false, // sneedAll - Buffer.from(payload_encrypted_hex, 'hex') - ) - - if (result.success) { - console.log("Succces sending tx") - message_json.sent = true - saveGroupMessage(message_json, result.transactionHash, timestamp) - mainWindow.webContents.send('sent_group', { - hash: result.transactionHash, - time: message.t, - }) - known_pool_txs.push(result.transactionHash) - saveHash(result.transactionHash) - optimizeMessages() - } else { - let error = { - message: 'Failed to send, please wait a couple of minutes.', - name: 'Error', - hash: Date.now(), - } - mainWindow.webContents.send('error_msg', error) - console.log(`Failed to send transaction: ${result.error.toString()}`) - optimizeMessages(true) - } - } else if (offchain) { - //Generate a random hash - let random_key = randomKey() - let sentMsg = Buffer.from(payload_encrypted_hex, 'hex') - let sendMsg = random_key + '99' + sentMsg - message_json.sent = true - if (swarm) { - sendSwarmMessage(sendMsg, group) - saveGroupMessage(message_json, random_key, timestamp, false, true) - mainWindow.webContents.send('sent_rtc_group', { - hash: random_key, - time: message.t, - }) - return - } - let messageArray = [sendMsg] - mainWindow.webContents.send('rtc_message', messageArray, true) - mainWindow.webContents.send('sent_rtc_group', { - hash: random_key, - time: message.t, - }) - - } -} - - -async function decryptRtcMessage(message) { - let hash = message.substring(0, 64) - let newMsg = await extraDataToMessage(message, known_keys, getXKRKeypair()) - - if (newMsg) { - newMsg.sent = false - } - - let group = newMsg.msg.msg - - if (group && 'key' in group) { - if (group.key === undefined) return - let invite_key = sanitizeHtml(group.key) - if (invite_key.length !== 64) return - - mainWindow.webContents.send('group-call', {invite_key, group}) - - if (group.type == 'invite') { - console.log('Group invite, thanks.') - return - } - - sleep(100) - - let video = false - if (group.type === true) { - video = true - } - - let invite = true - group.invite.forEach((call) => { - let contact = sanitizeHtml(call) - if (contact.length !== 163) { - mainWindow.webContents.send('error-notify-message', 'Error reading invite address') - } - console.log('Invited to call, joining...') - mainWindow.webContents.send('start-call', contact, video, invite) - sleep(1500) - }) - - return - - } else { - console.log('Not an invite') - } - - if (!newMsg) return - - saveMessage(newMsg, true) -} - -async function decryptGroupRtcMessage(message, key) { - try { - let hash = message.substring(0, 64) - let [groupMessage, time, txHash] = await decryptGroupMessage(message, hash, key) - - if (!groupMessage) { - return - } - if (groupMessage.m === 'ᛊNVITᛊ') { - if (groupMessage.r.length === 163) { - let invited = sanitizeHtml(groupMessage.r) - mainWindow.webContents.send('group_invited_contact', invited) - console.log('Invited') - } - } - } catch (e) { - console.log('Not an invite') - } -} - -async function decryptGroupMessage(tx, hash, group_key = false) { - - try { - let decryptBox = false - let offchain = false - let groups = await loadGroups() - - if (group_key.length === 64) { - let msg = tx - tx = JSON.parse(trimExtra(msg)) - groups.unshift({ key: group_key }) - offchain = true - } - - let key - - let i = 0 - - while (!decryptBox && i < groups.length) { - let possibleKey = groups[i].key - - i += 1 - - try { - decryptBox = nacl.secretbox.open( - hexToUint(tx.sb), - nonceFromTimestamp(tx.t), - hexToUint(possibleKey) - ) - - key = possibleKey - } catch (err) { - } - } - - if (!decryptBox) { - return false - } - - const message_dec = naclUtil.encodeUTF8(decryptBox) - const payload_json = JSON.parse(message_dec) - const from = payload_json.k - const this_addr = await Address.fromAddress(from) - - const verified = await xkrUtils.verifyMessageSignature( - payload_json.m, - this_addr.spend.publicKey, - payload_json.s - ) - - if (!verified) return false - if (block_list.some(a => a.address === from)) return false - - payload_json.sent = false - - let saved = saveGroupMessage(payload_json, hash, tx.t, offchain) - - if (!saved) return false - - return [payload_json, tx.t, hash] - - } catch { - return false - } -} - -// async function sendBoardMessage(message) { -// console.log('sending board', message) -// let reply = message.r -// let to_board = message.brd -// let my_address = message.k -// let nickname = message.n -// let msg = message.m -// try { -// let timestamp = parseInt(Date.now() / 1000) -// let [privateSpendKey, privateViewKey] = js_wallet.getPrimaryAddressPrivateKeys() -// let xkr_private_key = privateSpendKey -// let signature = await xkrUtils.signMessage(msg, xkr_private_key) - -// let payload_json = { -// m: msg, -// k: my_address, -// s: signature, -// brd: to_board, -// t: timestamp, -// n: nickname, -// } - -// if (reply) { -// payload_json.r = reply -// } - -// payload_hex = toHex(JSON.stringify(payload_json)) - -// let [mainWallet, subWallet] = js_wallet.subWallets.getAddresses() - -// let result = await js_wallet.sendTransactionAdvanced( -// [[subWallet, 1000]], // destinations, -// 3, // mixin -// { fixedFee: 1000, isFixedFee: true }, // fee -// undefined, //paymentID -// [subWallet], // subWalletsToTakeFrom -// undefined, // changeAddress -// true, // relayToNetwork -// false, // sneedAll -// Buffer.from(payload_hex, 'hex') -// ) - -// if (result.success) { -// console.log( -// `Sent transaction, hash ${result.transactionHash}, fee ${WB.prettyPrintAmount( -// result.fee -// )}` -// ) -// mainWindow.webContents.send('sent_board', { -// hash: result.transactionHash, -// time: message.t, -// }) -// known_pool_txs.push(result.transactionHash) -// const sentMsg = payload_json -// sentMsg.sent = true -// sentMsg.type = 'board' -// saveBoardMsg(sentMsg, result.transactionHash) -// } else { -// let error = { -// message: 'Failed to send', -// name: 'Error', -// hash: Date.now(), -// } -// mainWindow.webContents.send('error_msg', error) -// console.log(`Failed to send transaction: ${result.error.toString()}`) -// } -// } catch (err) { -// mainWindow.webContents.send('error_msg') -// console.log('Error', err) -// } - -// optimizeMessages() -// } - -async function checkHistory(messageKey) { - //Check history - if (known_keys.indexOf(messageKey) > -1) { - let [conv] = await getConversation() - if (parseInt(conv.timestamp) < parseInt(store.get("db.versionDate"))) return false - return true - } else { - known_keys.push(messageKey) - return false - } - - -} - -async function checkBalance() { - try { - let [munlockedBalance, mlockedBalance] = await js_wallet.getBalance() - - if (munlockedBalance < 11) { - mainWindow.webContents.send('error-notify-message', 'Not enough unlocked funds.') - return false - } - } catch (err) { - return false - } - return true -} - -async function sendMessage(message, receiver, off_chain = false, group = false, beam_this = false) { - //Assert address length - if (receiver.length !== 163) { - return - } - if (message.length === 0) { - return - } - //Split address and check history - let address = receiver.substring(0, 99) - let messageKey = receiver.substring(99, 163) - let has_history = await checkHistory(messageKey) - - if (!beam_this) { - let balance = await checkBalance() - if (!balance) return - } - - let timestamp = Date.now() - let payload_hex - - if (!has_history) { - payload_hex = await encryptMessage(message, messageKey, true, address) - } else { - payload_hex = await encryptMessage(message, messageKey, false, address) - } - - //Choose subwallet with message inputs - let messageWallet = js_wallet.subWallets.getAddresses()[1] - let messageSubWallet = js_wallet.subWallets.getAddresses()[2] - - if (!off_chain) { - let result = await js_wallet.sendTransactionAdvanced( - [[messageWallet, 1000]], // destinations, - 3, // mixin - { fixedFee: 1000, isFixedFee: true }, // fee - undefined, //paymentID - [messageWallet, messageSubWallet], // subWalletsToTakeFrom - undefined, // changeAddresss - true, // relayToNetwork - false, // sneedAll - Buffer.from(payload_hex, 'hex') - ) - let sentMsg = { - msg: message, - k: messageKey, - sent: true, - t: timestamp, - chat: address, - } - if (result.success) { - known_pool_txs.push(result.transactionHash) - saveHash(result.transactionHash) - console.log( - `Sent transaction, hash ${result.transactionHash}, fee ${WB.prettyPrintAmount( - result.fee - )}` - ) - saveMessage(sentMsg) - optimizeMessages() - } else { - let error = { - message: `Failed to send, please wait a couple of minutes.`, - name: 'Error', - hash: Date.now(), - } - optimizeMessages(true) - console.log(`Failed to send transaction: ${result.error.toString()}`) - mainWindow.webContents.send('error_msg', error) - } - } else if (off_chain) { - //Offchain messages - let random_key = randomKey() - let sentMsg = Buffer.from(payload_hex, 'hex') - let sendMsg = random_key + '99' + sentMsg - let messageArray = [] - messageArray.push(sendMsg) - messageArray.push(address) - if (group) { - messageArray.push('group') - } - if (beam_this) { - sendBeamMessage(sendMsg, address) - } else { - mainWindow.webContents.send('rtc_message', messageArray) - } - //Do not save invite message. - if (message.msg && 'invite' in message.msg) { - return - } - else { - let saveThisMessage = { - msg: message, - k: messageKey, - sent: true, - t: timestamp, - chat: address, - } - saveMessage(saveThisMessage, true) - } - } -} - -async function createMessageSubWallet() { - - if (js_wallet.subWallets.getAddresses().length < 3) { - if (js_wallet.subWallets.getAddresses().length === 1) { - const [address, error] = await js_wallet.addSubWallet() - if (error) { - return - } - } - const [spendKey, viewKey] = await js_wallet.getPrimaryAddressPrivateKeys() - const subWalletKeys = await crypto.generateDeterministicSubwalletKeys(spendKey, 1) - await js_wallet.importSubWallet(subWalletKeys.private_key) - } -} - -async function optimizeMessages(force = false) { - - let [mainWallet, subWallet, messageSubWallet] = js_wallet.subWallets.getAddresses() - console.log("Message wallets", [mainWallet, subWallet, messageSubWallet]) - const [walletHeight, localHeight, networkHeight] = await js_wallet.getSyncStatus() - - let inputs = await js_wallet.subWallets.getSpendableTransactionInputs( - [subWallet, messageSubWallet], - networkHeight - ) - - console.log("Inputs", inputs.length) - - if (inputs.length > 25 && !force) { - mainWindow.webContents.send('optimized', true) - return - } - - if (store.get('wallet.optimized')) { - return - } - - let subWallets = js_wallet.subWallets.subWallets - let txs - subWallets.forEach((value, name) => { - txs = value.unconfirmedIncomingAmounts.length - }) - - let payments = [] - let i = 0 - /* User payment */ - while (i <= 49) { - payments.push([messageSubWallet, 1000]) - i += 1 - } - - let result = await js_wallet.sendTransactionAdvanced( - payments, // destinations, - 3, // mixin - { fixedFee: 1000, isFixedFee: true }, // fee - undefined, //paymentID - [mainWallet], // subWalletsToTakeFrom - undefined, // changeAddress - true, // relayToNetwork - false, // sneedAll - undefined - ) - - if (result.success) { - mainWindow.webContents.send('optimized', true) - - store.set({ - wallet: { - optimized: true - } - }); - - resetOptimizeTimer() - - let sent = { - message: 'Your wallet is creating message inputs, please wait', - name: 'Optimizing', - hash: parseInt(Date.now()), - key: mainWallet, - optimized: true - } - - mainWindow.webContents.send('sent_tx', sent) - console.log('optimize completed') - return true - } else { - - store.set({ - wallet: { - optimized: false - } - }); - - mainWindow.webContents.send('optimized', false) - let error = { - message: 'Optimize failed', - name: 'Optimizing wallet failed', - hash: parseInt(Date.now()), - key: mainWallet, - } - mainWindow.webContents.send('error_msg', error) - return false - } - -} - async function sendTx(tx) { console.log('transactions', tx) console.log(`✅ SENDING ${tx.amount} TO ${tx.to}`) @@ -1595,29 +497,6 @@ async function sendTx(tx) { } } -async function resetOptimizeTimer() { - await sleep(600 * 1000) - store.set({ - wallet: { - optimized: false - } - }); -} - -async function pickNode(node) { - console.log(`Switching node to ${node}`) - nodeUrl = node.split(':')[0] - nodePort = parseInt(node.split(':')[1]) - node = { node: nodeUrl, port: nodePort } - if (!checkNodeStatus(node)) return - const daemon = new WB.Daemon(nodeUrl, nodePort) - await js_wallet.swapNode(daemon) - mainWindow.webContents.send('switch-node', node) - db.data.node = node - await db.write() -} - - async function shareScreen(start, conference) { const windows = [] const { desktopCapturer } = require('electron') @@ -1633,71 +512,6 @@ const { desktopCapturer } = require('electron') }) } -//Check if it is an image or video with allowed type -async function checkImageOrVideoType(path) { - if (path === undefined) return false - const types = ['.png','.jpg','.gif', '.jpeg', '.mp4', '.webm', '.avi', '.webp', '.mov','.wmv', '.mkv', '.mpeg']; - for (a in types) { - if (path.endsWith(types[a])) { - return true - } - } - return false -} - -async function load_file(path) { - let imgArray = [] - if (checkImageOrVideoType(path)) { - //Read the file as an image - try { - return new Promise((resolve, reject) => { - const stream = createReadStream(path) - stream.on('data', (data) => { - imgArray.push(data) - }) - stream.on('error', (data) => { - return "File not found" - }) - - stream.on('end', () => { - resolve(Buffer.concat(imgArray)) - }) - }) - - } catch (err) { - return "File not found" - } - } else { - return "File" - } -} - -async function syncGroupHistory(timeframe, recommended_api, key=false, page=1) { - if (recommended_api === undefined) return - fetch(`${recommended_api.url}/api/v1/posts-encrypted-group?from=${timeframe}&to=${Date.now() / 1000}&size=50&page=` + page) - .then((response) => response.json()) - .then(async (json) => { - console.log(timeframe + " " + key) - const items = json.encrypted_group_posts; - - for (message in items) { - try { - let tx = {} - tx.sb = items[message].tx_sb - tx.t = items[message].tx_timestamp - await decryptGroupMessage(tx, items[message].tx_hash, key) - - } - catch { - } - } - if(json.current_page != json.total_pages) { - syncGroupHistory(timeframe, recommended_api, key, page+1) - } - }) -} - - function get_sdp(data) { if (data.type == 'offer') @@ -1731,9 +545,6 @@ ipcMain.on("end-beam", async (e, chat) => { //SWARM -ipcMain.on('sendGroupsMessage', (e, msg, offchain, swarm) => { - sendGroupsMessage(msg, offchain, swarm) -}) ipcMain.on('new-swarm', async (e, data) => { newSwarm(data, sender, getXKRKeypair()) @@ -1760,11 +571,6 @@ ipcMain.on('remove-local-file', async (e, filename, address, time) => { removeLocalFile(filename, address, time) }) - -ipcMain.handle('load-file', async (e, path) => { - return await load_file(path) -}) - //TOAST NOTIFY ipcMain.on('error-notify-message-main', async (e, error) => { mainWindow.webContents.send('error-notify-message', error) @@ -1800,101 +606,6 @@ ipcMain.handle('createGroup', async () => { return randomKey() }) -ipcMain.on('unblock', async (e, address) => { - await unBlockContact(address) - block_list = await loadBlockList() - mainWindow.webContents.send('update-blocklist', block_list) -}) - -ipcMain.on('block', async (e, block) => { - blockContact(block.address, block.name) - block_list = await loadBlockList() - mainWindow.webContents.send('update-blocklist', block_list) -}) - - -ipcMain.on('addGroup', async (e, grp) => { - addGroup(grp) - saveGroupMessage(grp, grp.hash, parseInt(Date.now())) -}) - -ipcMain.on('removeGroup', async (e, grp) => { - removeGroup(grp) -}) - -ipcMain.on('fetchGroupHistory', async (e, settings) => { - let timeframe = Date.now() / 1000 - settings.timeframe * 86400 - //If key is not undefined we know which group to search messages from - if (settings.key === undefined) settings.key = false - //Clear known pool txs to look through messages we already marked as known - known_pool_txs = [] - await syncGroupHistory(timeframe, settings.recommended_api, settings.key) -}) - - -//BOARDS - -ipcMain.on('sendBoardMsg', (e, msg) => { - sendBoardMessage(msg) -}) - -//Listens for ipc call from RightMenu board picker and prints any board chosen -ipcMain.handle('printBoard', async (e, board) => { - return await printBoard(board) -}) - -ipcMain.handle('getAllBoards', async () => { - return await getBoardMsgs() -}) - -ipcMain.handle('getMyBoards', async () => { - return await getMyBoardList() -}) -//Adds board to my_boards array so backgroundsync is up to date wich boards we are following. -ipcMain.on('addBoard', async (e, board) => { - my_boards.push(board) - addBoard(board) -}) - -ipcMain.on('removeBoard', async (e, board) => { - my_boards.pop(board) - removeBoard(board) -}) - -ipcMain.handle('getReply', async (e, data) => { - return await getReply(data) -}) - - -//PRIVATE MESSAGES - -ipcMain.handle('getConversations', async (e) => { - let contacts = await getConversations() - return contacts.reverse() -}) - -ipcMain.handle('getMessages', async (data) => { - return await getMessages() -}) - -ipcMain.on('sendMsg', (e, msg, receiver, off_chain, grp, beam) => { - sendMessage(msg, receiver, off_chain, grp, beam) - console.log(msg, receiver, off_chain, grp, beam) -}) - -//Listens for event from frontend and saves contact and nickname. -ipcMain.on('addChat', async (e, hugin_address, nickname, first = false) => { - saveContact(hugin_address, nickname, first) -}) - - -ipcMain.on('removeContact', (e, contact) => { - removeContact(contact) - removeMessages(contact) - mainWindow.webContents.send('sent') -}) - - //NODE ipcMain.handle('getHeight', async () => { @@ -1902,11 +613,6 @@ ipcMain.handle('getHeight', async () => { return { walletHeight, networkHeight } }) -ipcMain.on('switchNode', (e, node) => { - pickNode(node) -}) - - //CALLS ipcMain.on('answerCall', (e, msg, contact, key, offchain = false) => { @@ -1979,18 +685,12 @@ ipcMain.on('rescan', async (e, height) => { js_wallet.reset(parseInt(height)) }) -//Optimize messages -ipcMain.on('optimize', async (e) => { - optimizeMessages(force = true) -}) - ipcMain.on('sendTx', (e, tx) => { sendTx(tx) }) ipcMain.handle('getPrivateKeys', async () => { - const [spendKey, viewKey] = await js_wallet.getPrimaryAddressPrivateKeys() - return [spendKey, viewKey] + return getPrivKeys() }) ipcMain.handle('getMnemonic', async () => { @@ -2031,17 +731,6 @@ ipcMain.handle('getAddress', async () => { }) -//WEBRTC MESSAGES - -ipcMain.on('decrypt_message', async (e, message) => { - decryptRtcMessage(message) -}) - -ipcMain.on('decrypt_rtc_group_message', async (e, message, key) => { - decryptGroupRtcMessage(message, key) -}) - - //MISC ipcMain.on('create-account', async (e, accountData) => { diff --git a/src/backend/messages.cjs b/src/backend/messages.cjs new file mode 100644 index 00000000..aabc2247 --- /dev/null +++ b/src/backend/messages.cjs @@ -0,0 +1,989 @@ +const { + loadMiscData, + getSyncStatus, + getPrivKeys, + getAddress, + getAddresses, + getSpendableTransactionInputs, + sendTransactionAdvanced, + resetWallet, + getXKRKeypair, + getSubWallets} = require('./wallet.cjs') +const { + loadKnownTxs, + saveHash, + saveGroupMsg, + messageExists, + saveMsg, + saveThisContact, + getConversation, + getConversations, + getMessages, + removeMessages, + removeContact, + addGroup, + removeGroup, + unBlockContact, + loadBlockList, + blockContact} = require("./database.cjs") +const { + trimExtra, + sanitize_pm_message, + parseCall, + sleep, + hexToUint, + randomKey, + nonceFromTimestamp +} = require('./utils.cjs') + +const { sendBeamMessage} = require("./beam.cjs") +const { sendSwarmMessage } = require("./swarm.cjs") + +const { Address, Crypto, CryptoNote} = require('kryptokrona-utils') +const { extraDataToMessage } = require('hugin-crypto') +const { default: fetch } = require('electron-fetch') +const naclUtil = require('tweetnacl-util') +const sanitizeHtml = require('sanitize-html') +const crypto = new Crypto() +const xkrUtils = new CryptoNote() +const { ipcMain } = require('electron') +const Store = require('electron-store'); +const store = new Store() + +let known_pool_txs = [] +let sender +let known_keys = [] +let block_list = [] + +//IPC MAIN LISTENERS + +ipcMain.on('sendGroupsMessage', (e, msg, offchain, swarm) => { + sendGroupsMessage(msg, offchain, swarm) +}) + +//Optimize messages +ipcMain.on('optimize', async (e) => { + optimizeMessages(force = true) +}) + +//GROUPS + +ipcMain.on('addGroup', async (e, grp) => { + addGroup(grp) + saveGroupMessage(grp, grp.hash, parseInt(Date.now())) +}) + +ipcMain.on('removeGroup', async (e, grp) => { + removeGroup(grp) +}) + +ipcMain.on('unblock', async (e, address) => { + unBlockContact(address) + block_list = await loadBlockList() + sender('update-blocklist', block_list) +}) + +ipcMain.on('block', async (e, block) => { + blockContact(block.address, block.name) + block_list = await loadBlockList() + sendTransactionAdvanced('update-blocklist', block_list) +}) + + +//PRIVATE MESSAGES + +ipcMain.handle('getConversations', async (e) => { + let contacts = await getConversations() + return contacts.reverse() +}) + +ipcMain.handle('getMessages', async (data) => { + return await getMessages() +}) + +ipcMain.on('sendMsg', (e, msg, receiver, off_chain, grp, beam) => { + sendMessage(msg, receiver, off_chain, grp, beam) + console.log(msg, receiver, off_chain, grp, beam) +}) + +//Listens for event from frontend and saves contact and nickname. +ipcMain.on('addChat', async (e, hugin_address, nickname, first = false) => { + saveContact(hugin_address, nickname, first) +}) + + +ipcMain.on('removeContact', (e, contact) => { + removeContact(contact) + removeMessages(contact) + sender('sent') +}) + +//WEBRTC MESSAGES + +ipcMain.on('decrypt_message', async (e, message) => { + decryptRtcMessage(message) +}) + +ipcMain.on('decrypt_rtc_group_message', async (e, message, key) => { + decryptGroupRtcMessage(message, key) +}) + + +const startMessageSyncer = async (ipc, keys, bl) => { + //Load knownTxsIds to backgroundSyncMessages on startup + sender = ipc + known_keys = keys + block_list = bl + backgroundSyncMessages(await loadCheckedTxs()) + while (true) { + try { + //Start syncing + await sleep(1000 * 10) + + backgroundSyncMessages() + + const [walletBlockCount, localDaemonBlockCount, networkBlockCount] = getSyncStatus() + + sender('node-sync-data', { + walletBlockCount, + localDaemonBlockCount, + networkBlockCount, + }) + + if (localDaemonBlockCount - walletBlockCount < 2) { + // Diff between wallet height and node height is 1 or 0, we are synced + console.log('**********SYNCED**********') + console.log('My Wallet ', walletBlockCount) + console.log('The Network', networkBlockCount) + sender('sync', 'Synced') + } else { + //If wallet is somehow stuck at block 0 for new users due to bad node connection, reset to the last 100 blocks. + if (walletBlockCount === 0) { + await resetWallet(networkBlockCount - 100) + } + console.log('*.[~~~].SYNCING BLOCKS.[~~~].*') + console.log('My Wallet ', walletBlockCount) + console.log('The Network', networkBlockCount) + sender('sync', 'Syncing') + } + } catch (err) { + console.log(err) + } + } +} + +async function backgroundSyncMessages(checkedTxs = false) { + console.log('Background syncing...') + + //First start, set known pool txs + if (checkedTxs) { + known_pool_txs = await setKnownPoolTxs(checkedTxs) + } + + let transactions = await fetchHuginMessages() + if (!transactions) return + decryptHuginMessages(transactions) +} + + +async function decryptHuginMessages(transactions) { + + for (const transaction of transactions) { + try { + let thisExtra = transaction.transactionPrefixInfo.extra + let thisHash = transaction.transactionPrefixInfotxHash + if (!validateExtra(thisExtra, thisHash)) continue + if (thisExtra !== undefined && thisExtra.length > 200) { + if (!saveHash(thisHash)) continue + //Check for viewtag + let checkTag = await checkForViewTag(thisExtra) + if (checkTag) { + await checkForPrivateMessage(thisExtra, thisHash) + continue + } + //Check for private message //TODO remove this when viewtags are active + if (await checkForPrivateMessage(thisExtra, thisHash)) continue + //Check for group message + if (await checkForGroupMessage(thisExtra, thisHash)) continue + } + } catch (err) { + console.log(err) + } + } +} + +//Try decrypt extra data +async function checkForPrivateMessage(thisExtra) { + let message = await extraDataToMessage(thisExtra, known_keys, getXKRKeypair()) + if (!message) return false + if (message.type === 'sealedbox' || 'box') { + message.sent = false + saveMessage(message) + return true + } +} + +//Checks the message for a view tag +async function checkForViewTag(extra) { + try { + const rawExtra = trimExtra(extra) + const parsed_box = JSON.parse(rawExtra) + if (parsed_box.vt) { + const [privateSpendKey, privateViewKey] = getPrivKeys() + const derivation = await crypto.generateKeyDerivation(parsed_box.txKey, privateViewKey); + const hashDerivation = await crypto.cn_fast_hash(derivation) + const possibleTag = hashDerivation.substring(0,2) + const view_tag = parsed_box.vt + if (possibleTag === view_tag) { + console.log('**** FOUND VIEWTAG ****') + return true + } + } + } catch (err) { + } + return false +} + + +//Checks if hugin message is from a group +async function checkForGroupMessage(thisExtra, thisHash) { + try { + let group = trimExtra(thisExtra) + let message = JSON.parse(group) + if (message.sb) { + await decryptGroupMessage(message, thisHash) + return true + } + } catch { + + } + return false +} + +//Validate extradata, here we can add more conditions +function validateExtra(thisExtra, thisHash) { + //Extra too long + if (thisExtra.length > 7000) { + known_pool_txs.push(thisHash) + if (!saveHash(thisHash)) return false + return false; + } + //Check if known tx + if (known_pool_txs.indexOf(thisHash) === -1) { + known_pool_txs.push(thisHash) + return true + } else { + //Tx already known + return false + } +} + +async function loadCheckedTxs() { + + //Load known pool txs from db. + let checkedTxs = await loadKnownTxs() + let arrayLength = checkedTxs.length + + if (arrayLength > 0) { + checkedTxs = checkedTxs.slice(arrayLength - 200, arrayLength - 1).map(function (knownTX) { + return knownTX.hash + }) + + } else { + checkedTxs = [] + } + + return checkedTxs +} + + +//Set known pool txs on start +function setKnownPoolTxs(checkedTxs) { + //Here we can adjust number of known we send to the node + known_pool_txs = checkedTxs + //Can't send undefined to node, it wont respond + let known = known_pool_txs.filter(a => a !== undefined) + return known +} + + +async function fetchHuginMessages() { + const [node] = await loadMiscData() + try { + const resp = await fetch( + 'http://' + node.node + ':' + node.port.toString() + '/get_pool_changes_lite', + { + method: 'POST', + body: JSON.stringify({ knownTxsIds: known_pool_txs }), + } + ) + + let json = await resp.json() + json = JSON.stringify(json) + .replaceAll('.txPrefix', '') + .replaceAll('transactionPrefixInfo.txHash', 'transactionPrefixInfotxHash') + + json = JSON.parse(json) + + let transactions = json.addedTxs + //Try clearing known pool txs from checked + known_pool_txs = known_pool_txs.filter((n) => !json.deletedTxsIds.includes(n)) + if (transactions.length === 0) { + console.log('Empty array...') + console.log('No incoming messages...') + return false + } + + return transactions + + } catch (e) { + sender('sync', 'Error') + return false + } +} + + +async function sendMessage(message, receiver, off_chain = false, group = false, beam_this = false) { + //Assert address length + if (receiver.length !== 163) { + return + } + if (message.length === 0) { + return + } + //Split address and check history + let address = receiver.substring(0, 99) + let messageKey = receiver.substring(99, 163) + let has_history = await checkHistory(messageKey) + + if (!beam_this) { + let balance = await checkBalance() + if (!balance) return + } + + let timestamp = Date.now() + let payload_hex + + if (!has_history) { + payload_hex = await encryptMessage(message, messageKey, true, address) + } else { + payload_hex = await encryptMessage(message, messageKey, false, address) + } + + //Choose subwallet with message inputs + let messageWallet = getAddresses()[1] + let messageSubWallet = getAddresses()[2] + + if (!off_chain) { + let result = await sendTransactionAdvanced( + [[messageWallet, 1000]], // destinations, + 3, // mixin + { fixedFee: 1000, isFixedFee: true }, // fee + undefined, //paymentID + [messageWallet, messageSubWallet], // subWalletsToTakeFrom + undefined, // changeAddresss + true, // relayToNetwork + false, // sneedAll + Buffer.from(payload_hex, 'hex') + ) + let sentMsg = { + msg: message, + k: messageKey, + sent: true, + t: timestamp, + chat: address, + } + if (result.success) { + known_pool_txs.push(result.transactionHash) + saveHash(result.transactionHash) + console.log( + `Sent transaction, hash ${result.transactionHash}, fee ${WB.prettyPrintAmount( + result.fee + )}` + ) + saveMessage(sentMsg) + optimizeMessages() + } else { + let error = { + message: `Failed to send, please wait a couple of minutes.`, + name: 'Error', + hash: Date.now(), + } + optimizeMessages(true) + console.log(`Failed to send transaction: ${result.error.toString()}`) + sender('error_msg', error) + } + } else if (off_chain) { + //Offchain messages + let random_key = randomKey() + let sentMsg = Buffer.from(payload_hex, 'hex') + let sendMsg = random_key + '99' + sentMsg + let messageArray = [] + messageArray.push(sendMsg) + messageArray.push(address) + if (group) { + messageArray.push('group') + } + if (beam_this) { + sendBeamMessage(sendMsg, address) + } else { + sender('rtc_message', messageArray) + } + //Do not save invite message. + if (message.msg && 'invite' in message.msg) { + return + } + else { + let saveThisMessage = { + msg: message, + k: messageKey, + sent: true, + t: timestamp, + chat: address, + } + saveMessage(saveThisMessage, true) + } + } +} + + + +async function optimizeMessages(force = false) { + + let [mainWallet, subWallet, messageSubWallet] = getAddresses() + const [walletHeight, localHeight, networkHeight] = getSyncStatus() + + let inputs = await getSpendableTransactionInputs( + [subWallet, messageSubWallet], + networkHeight + ) + + if (inputs.length > 25 && !force) { + sender('optimized', true) + return + } + + if (store.get('wallet.optimized')) { + return + } + + let subWallets = getSubWallets() + let txs + subWallets.forEach((value, name) => { + txs = value.unconfirmedIncomingAmounts.length + }) + + let payments = [] + let i = 0 + /* User payment */ + while (i <= 49) { + payments.push([messageSubWallet, 1000]) + i += 1 + } + + let result = await sendTransactionAdvanced( + payments, // destinations, + 3, // mixin + { fixedFee: 1000, isFixedFee: true }, // fee + undefined, //paymentID + [mainWallet], // subWalletsToTakeFrom + undefined, // changeAddress + true, // relayToNetwork + false, // sneedAll + undefined + ) + + if (result.success) { + sender('optimized', true) + + store.set({ + wallet: { + optimized: true + } + }); + + resetOptimizeTimer() + + let sent = { + message: 'Your wallet is creating message inputs, please wait', + name: 'Optimizing', + hash: parseInt(Date.now()), + key: mainWallet, + optimized: true + } + + sender('sent_tx', sent) + console.log('optimize completed') + return true + } else { + + store.set({ + wallet: { + optimized: false + } + }); + + sender('optimized', false) + let error = { + message: 'Optimize failed', + name: 'Optimizing wallet failed', + hash: parseInt(Date.now()), + key: mainWallet, + } + sender('error_msg', error) + return false + } + +} + +async function resetOptimizeTimer() { + await sleep(600 * 1000) + store.set({ + wallet: { + optimized: false + } + }); +} + + +async function encryptMessage(message, messageKey, sealed = false, toAddr) { + let timestamp = Date.now() + let my_address = getAddress() + const addr = await Address.fromAddress(toAddr) + const [privateSpendKey, privateViewKey] = getPrivKeys() + let xkr_private_key = privateSpendKey + let box + + //Create the view tag using a one time private key and the receiver view key + const keys = await crypto.generateKeys(); + const toKey = addr.m_keys.m_viewKeys.m_publicKey + const outDerivation = await crypto.generateKeyDerivation(toKey, keys.private_key); + const hashDerivation = await crypto.cn_fast_hash(outDerivation) + const viewTag = hashDerivation.substring(0,2) + + if (sealed) { + + let signature = await xkrUtils.signMessage(message, xkr_private_key) + let payload_json = { + from: my_address, + k: Buffer.from(getKeyPair().publicKey).toString('hex'), + msg: message, + s: signature, + } + let payload_json_decoded = naclUtil.decodeUTF8(JSON.stringify(payload_json)) + box = new naclSealed.sealedbox( + payload_json_decoded, + nonceFromTimestamp(timestamp), + hexToUint(messageKey) + ) + } else if (!sealed) { + console.log('Has history, not using sealedbox') + let payload_json = { from: my_address, msg: message } + let payload_json_decoded = naclUtil.decodeUTF8(JSON.stringify(payload_json)) + + box = nacl.box( + payload_json_decoded, + nonceFromTimestamp(timestamp), + hexToUint(messageKey), + getKeyPair().secretKey + ) + } + //Box object + let payload_box = { box: Buffer.from(box).toString('hex'), t: timestamp, txKey: keys.public_key, vt: viewTag } + // Convert json to hex + let payload_hex = toHex(JSON.stringify(payload_box)) + + return payload_hex +} + + +async function sendGroupsMessage(message, offchain = false, swarm = false) { + console.log("Sending group msg!") + if (message.m.length === 0) return + const my_address = message.k + const [privateSpendKey, privateViewKey] = getPrivKeys() + const signature = await xkrUtils.signMessage(message.m, privateSpendKey) + const timestamp = parseInt(Date.now()) + const nonce = nonceFromTimestamp(timestamp) + + let group + let reply = '' + + group = message.g + + if (group === undefined) return + if (group.length !== 64) { + return + } + + if (!offchain) { + let balance = await checkBalance() + if (!balance) return + } + + let message_json = { + m: message.m, + k: my_address, + s: signature, + g: group, + n: message.n, + r: reply, + } + + if (message.r) { + message_json.r = message.r + } + + if (message.c) { + message_json.c = message.c + } + + let [mainWallet, subWallet, messageSubWallet] = getAddresses() + const payload_unencrypted = naclUtil.decodeUTF8(JSON.stringify(message_json)) + const secretbox = nacl.secretbox(payload_unencrypted, nonce, hexToUint(group)) + + const payload_encrypted = { + sb: Buffer.from(secretbox).toString('hex'), + t: timestamp, + } + + const payload_encrypted_hex = toHex(JSON.stringify(payload_encrypted)) + + if (!offchain) { + let result = await sendTransactionAdvanced( + [[messageSubWallet, 1000]], // destinations, + 3, // mixin + { fixedFee: 1000, isFixedFee: true }, // fee + undefined, //paymentID + [subWallet, messageSubWallet], // subWalletsToTakeFrom + undefined, // changeAddress + true, // relayToNetwork + false, // sneedAll + Buffer.from(payload_encrypted_hex, 'hex') + ) + + if (result.success) { + console.log("Succces sending tx") + message_json.sent = true + saveGroupMessage(message_json, result.transactionHash, timestamp) + sender('sent_group', { + hash: result.transactionHash, + time: message.t, + }) + known_pool_txs.push(result.transactionHash) + saveHash(result.transactionHash) + optimizeMessages() + } else { + let error = { + message: 'Failed to send, please wait a couple of minutes.', + name: 'Error', + hash: Date.now(), + } + sender('error_msg', error) + console.log(`Failed to send transaction: ${result.error.toString()}`) + optimizeMessages(true) + } + } else if (offchain) { + //Generate a random hash + let random_key = randomKey() + let sentMsg = Buffer.from(payload_encrypted_hex, 'hex') + let sendMsg = random_key + '99' + sentMsg + message_json.sent = true + if (swarm) { + sendSwarmMessage(sendMsg, group) + saveGroupMessage(message_json, random_key, timestamp, false, true) + sender('sent_rtc_group', { + hash: random_key, + time: message.t, + }) + return + } + let messageArray = [sendMsg] + sender('rtc_message', messageArray, true) + sender('sent_rtc_group', { + hash: random_key, + time: message.t, + }) + + } +} + + +async function decryptRtcMessage(message) { + let hash = message.substring(0, 64) + let newMsg = await extraDataToMessage(message, known_keys, getXKRKeypair()) + + if (newMsg) { + newMsg.sent = false + } + + let group = newMsg.msg.msg + + if (group && 'key' in group) { + if (group.key === undefined) return + let invite_key = sanitizeHtml(group.key) + if (invite_key.length !== 64) return + + sender('group-call', {invite_key, group}) + + if (group.type == 'invite') { + console.log('Group invite, thanks.') + return + } + + sleep(100) + + let video = false + if (group.type === true) { + video = true + } + + let invite = true + group.invite.forEach((call) => { + let contact = sanitizeHtml(call) + if (contact.length !== 163) { + sender('error-notify-message', 'Error reading invite address') + } + console.log('Invited to call, joining...') + sender('start-call', contact, video, invite) + sleep(1500) + }) + + return + + } else { + console.log('Not an invite') + } + + if (!newMsg) return + + saveMessage(newMsg, true) +} + + +async function syncGroupHistory(timeframe, recommended_api, key=false, page=1) { + if (recommended_api === undefined) return + fetch(`${recommended_api.url}/api/v1/posts-encrypted-group?from=${timeframe}&to=${Date.now() / 1000}&size=50&page=` + page) + .then((response) => response.json()) + .then(async (json) => { + console.log(timeframe + " " + key) + const items = json.encrypted_group_posts; + + for (message in items) { + try { + let tx = {} + tx.sb = items[message].tx_sb + tx.t = items[message].tx_timestamp + await decryptGroupMessage(tx, items[message].tx_hash, key) + + } + catch { + } + } + if(json.current_page != json.total_pages) { + syncGroupHistory(timeframe, recommended_api, key, page+1) + } + }) +} + +async function decryptGroupMessage(tx, hash, group_key = false) { + + try { + let decryptBox = false + let offchain = false + let groups = await loadGroups() + + if (group_key.length === 64) { + let msg = tx + tx = JSON.parse(trimExtra(msg)) + groups.unshift({ key: group_key }) + offchain = true + } + + let key + + let i = 0 + + while (!decryptBox && i < groups.length) { + let possibleKey = groups[i].key + + i += 1 + + try { + decryptBox = nacl.secretbox.open( + hexToUint(tx.sb), + nonceFromTimestamp(tx.t), + hexToUint(possibleKey) + ) + + key = possibleKey + } catch (err) { + } + } + + if (!decryptBox) { + return false + } + + const message_dec = naclUtil.encodeUTF8(decryptBox) + const payload_json = JSON.parse(message_dec) + const from = payload_json.k + const this_addr = await Address.fromAddress(from) + + const verified = await xkrUtils.verifyMessageSignature( + payload_json.m, + this_addr.spend.publicKey, + payload_json.s + ) + + if (!verified) return false + if (block_list.some(a => a.address === from)) return false + + payload_json.sent = false + + let saved = saveGroupMessage(payload_json, hash, tx.t, offchain) + + if (!saved) return false + + return [payload_json, tx.t, hash] + + } catch { + return false + } +} + +async function decryptGroupRtcMessage(message, key) { + try { + let hash = message.substring(0, 64) + let [groupMessage, time, txHash] = await decryptGroupMessage(message, hash, key) + + if (!groupMessage) { + return + } + if (groupMessage.m === 'ᛊNVITᛊ') { + if (groupMessage.r.length === 163) { + let invited = sanitizeHtml(groupMessage.r) + sender('group_invited_contact', invited) + console.log('Invited') + } + } + } catch (e) { + console.log('Not an invite') + } +} + +async function saveGroupMessage(msg, hash, time, offchain, channel = false) { + console.log("Savin group message") + let message = await saveGroupMsg(msg, hash, time, offchain, channel) + if (!message) return false + if (!offchain) { + //Send new board message to frontend. + sender('groupMsg', message) + sender('newGroupMessage', message) + } else if (offchain) { + if (message.message === 'ᛊNVITᛊ') return + sender('groupRtcMsg', message) + } +} + + +//Saves private message +async function saveMessage(msg, offchain = false) { + + let [message, addr, key, timestamp, sent] = sanitize_pm_message(msg) + + if (!message) return + + if (await messageExists(timestamp)) return + + //Checking if private msg is a call + let [text, data, is_call, if_sent] = parseCall(msg.msg, addr, sent, offchain, timestamp) + + if (text === "Audio call started" || text === "Video call started" && is_call && !if_sent) { + //Incoming calll + sender('call-incoming', data) + } else if (text === "Call answered" && is_call && !if_sent) { + //Callback + sender('got-callback', data) + } + + //If sent set addr to chat instead of from + if (msg.chat && sent) { + addr = msg.chat + } + + //New message from unknown contact + if (msg.type === 'sealedbox' && !sent) { + let hugin = addr + key + await saveContact(hugin) + } + + message = sanitizeHtml(text) + let newMsg = await saveMsg(message, addr, sent, timestamp, offchain) + if (sent) { + //If sent, update conversation list + sender('sent', newMsg) + return + } + //Send message to front end + sender('newMsg', newMsg) + sender('privateMsg', newMsg) +} + +//Saves contact and nickname to db. +async function saveContact(hugin_address, nickname = false, first = false) { + + let name + if (!nickname) { + name = 'Anon' + } else { + name = nickname + } + let addr = hugin_address.substring(0, 99) + let key = hugin_address.substring(99, 163) + + if (known_keys.indexOf(key) == -1) { + known_keys.push(key) + } + + sender('saved-addr', hugin_address) + + saveThisContact(addr, key, name) + + if (first) { + saveMessage({ + msg: 'New friend added!', + k: key, + from: addr, + chat: addr, + sent: true, + t: Date.now(), + }) + known_keys.pop(key) + } +} + +async function checkHistory(messageKey) { + //Check history + if (known_keys.indexOf(messageKey) > -1) { + let [conv] = await getConversation() + if (parseInt(conv.timestamp) < parseInt(store.get("db.versionDate"))) return false + return true + } else { + known_keys.push(messageKey) + return false + } + + +} + +ipcMain.on('fetchGroupHistory', async (e, settings) => { + let timeframe = Date.now() / 1000 - settings.timeframe * 86400 + //If key is not undefined we know which group to search messages from + if (settings.key === undefined) settings.key = false + //Clear known pool txs to look through messages we already marked as known + known_pool_txs = [] + await syncGroupHistory(timeframe, settings.recommended_api, settings.key) +}) + +module.exports = {checkHistory, saveMessage, startMessageSyncer, sendMessage, optimizeMessages} \ No newline at end of file diff --git a/src/backend/utils.cjs b/src/backend/utils.cjs index 7075ffb5..eeb8d369 100644 --- a/src/backend/utils.cjs +++ b/src/backend/utils.cjs @@ -1,8 +1,55 @@ const nacl = require('tweetnacl') const sanitizeHtml = require('sanitize-html') const { Crypto } = require('kryptokrona-utils') - +const {ipcMain} = require('electron') const crypto = new Crypto() +const {createReadStream} = require("fs"); + +ipcMain.handle('load-file', async (e, path) => { + return await load_file(path) +}) + + +//Check if it is an image or video with allowed type +async function checkImageOrVideoType(path) { + if (path === undefined) return false + const types = ['.png','.jpg','.gif', '.jpeg', '.mp4', '.webm', '.avi', '.webp', '.mov','.wmv', '.mkv', '.mpeg']; + for (a in types) { + if (path.endsWith(types[a])) { + return true + } + } + return false +} + +async function load_file(path, size) { + let imgArray = [] + //TODO ADD SIZE CHECK + if (checkImageOrVideoType(path)) { + //Read the file as an image + try { + return new Promise((resolve, reject) => { + const stream = createReadStream(path) + stream.on('data', (data) => { + imgArray.push(data) + }) + stream.on('error', (data) => { + return "File not found" + }) + + stream.on('end', () => { + resolve(Buffer.concat(imgArray)) + }) + }) + + } catch (err) { + return "File not found" + } + } else { + return "File" + } +} + const hexToUint = (hexString) => new Uint8Array(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))) diff --git a/src/backend/wallet.cjs b/src/backend/wallet.cjs new file mode 100644 index 00000000..3f9a6276 --- /dev/null +++ b/src/backend/wallet.cjs @@ -0,0 +1,276 @@ +const WB = require('kryptokrona-wallet-backend-js') +const fs = require('fs') +const files = require('fs/promises') +const {hash} = require('./utils.cjs') +const { join } = require('path') +const { app, ipcMain } = require('electron') +const userDataDir = app.getPath('userData') +const file = join(userDataDir, 'misc.db') +const { JSONFile, Low } = require('@commonify/lowdb') +const { default: fetch } = require('electron-fetch') +const adapter = new JSONFile(file) +const db = new Low(adapter) + +let js_wallet +let daemon +let hashed_pass = "" + +//IPC LISTENERS + +ipcMain.on('switchNode', (e, node) => { + pickNode(node) +}) + + +///FUNCTIONS + + + +const loadWallet = (ipc) => { + sender = ipc +} + +const getXKRKeypair = () => { + const [privateSpendKey, privateViewKey] = getPrivKeys() + return { privateSpendKey, privateViewKey } +} + +const createWallet = async () => { + return [await WB.WalletBackend.createWallet(daemon), null] +} + +const loadDaemon = (nodeUrl, nodePort) => { + console.log("Log daemon", nodeUrl, nodePort) + daemon = new WB.Daemon(nodeUrl, nodePort) +} + +const getPrivKeys = () => { + return js_wallet.getPrimaryAddressPrivateKeys() +} + +const checkPassword = async (password, node) => { + //If we are already logged in + if (hashed_pass.length) { + verifyPassword(password, hashed_pass) + return true + } + + await checkNodeStatus(node) + return false +} + +const verifyPassword = async (password, hashed_pass) => { + if (await checkPass(password, hashed_pass)) { + await sleep(1000) + sender('login-success') + return true + } else { + sender('login-failed') + return false + } +} + +const checkPass = async (pass, oldHash) => { + let passHash = await hash(pass) + if (oldHash === passHash) return true + return false +} + +const importFromSeed = async (daemon, blockHeight, mnemonic) => { + return await WB.WalletBackend.importWalletFromSeed( + daemon, + blockHeight, + mnemonic + ) +} + +const loginWallet = async (walletName, password) => { + js_wallet = await logIntoWallet(walletName, password) + if (js_wallet === 'Wrong password') { + sender('login-failed') + return [false, undefined] + } + + hashed_pass = await hash(password) + return [true, js_wallet] +} + +const saveWallet = async (wallet, walletName, password) => { + let my_wallet = await encryptWallet(wallet, password) + let wallet_json = JSON.stringify(my_wallet) + + try { + await files.writeFile(userDataDir + '/' + walletName + '.json', wallet_json) + } catch (err) { + console.log('Wallet json saving error, revert to saving wallet file') + wallet.saveWalletToFile(userDataDir + '/' + walletName + '.wallet', password) + } +} + +const saveWalletToFile = (wallet, walletName, password) => { + wallet.saveWalletToFile(userDataDir + '/' + walletName + '.wallet', password) +} + +const logIntoWallet = async (walletName, password) => { + let parsed_wallet + let json_wallet = await openWallet(walletName, password) + try { + //Try parse json wallet + parsed_wallet = JSON.parse(json_wallet) + //Open encrypted json wallet + const [js_wallet, error] = await WB.WalletBackend.openWalletFromEncryptedString( + daemon, + parsed_wallet, + password + ) + if (error) { + console.log('Failed to open wallet: ' + error.toString()) + return 'Wrong password' + } + return js_wallet + } catch (err) { + console.log('error parsing wallet', err) + let wallet = await openWalletFromFile(walletName, password) + return wallet + //Probably a wallet file, return this + } +} + +const openWalletFromFile = async (walletName, password) => { + console.log('Open wallet from file') + const [js_wallet, error] = await WB.WalletBackend.openWalletFromFile( + daemon, + userDataDir + '/' + walletName + '.wallet', + password + ) + if (error) { + console.log('Failed to open wallet: ' + error.toString()) + return 'Wrong password' + } + return js_wallet +} +const encryptWallet = async (wallet, pass) => { + return await wallet.encryptWalletToString(pass) +} + + +const openWallet = async (walletName, password) => { + let json_wallet + + try { + json_wallet = await files.readFile(userDataDir + '/' + walletName + '.json') + return json_wallet + } catch (err) { + console.log('Json wallet error, try backup wallet file', err) + } +} + +const getSyncStatus = () => { + return js_wallet.getSyncStatus() +} + +const getSpendableTransactionInputs = async ([subWallet, messageSubWallet], networkHeight) => { + return await js_wallet.subWallets.getSpendableTransactionInputs( + [subWallet, messageSubWallet], + networkHeight + ) +} + +async function createMessageSubWallet() { + + if (js_wallet.subWallets.getAddresses().length < 3) { + if (js_wallet.subWallets.getAddresses().length === 1) { + const [address, error] = await js_wallet.addSubWallet() + if (error) { + return + } + } + const [spendKey, viewKey] = getPrivKeys() + const subWalletKeys = await crypto.generateDeterministicSubwalletKeys(spendKey, 1) + await js_wallet.importSubWallet(subWalletKeys.private_key) + } +} + +const pickNode = async (node) => { + let nodeUrl = node.split(':')[0] + let nodePort = parseInt(node.split(':')[1]) + const newNode = { node: nodeUrl, port: nodePort } + if (!await checkNodeStatus(newNode)) return + loadDaemon(nodeUrl, nodePort) + await js_wallet.swapNode(daemon) + sender('switch-node', newNode) + saveNode(node) +} + +const saveNode = async (node) => { + db.data.node = node + await db.write() +} + +const loadMiscData = async () => { + await db.read() + let node = db.data.node.node + let port = db.data.node.port + + if (node || port === undefined) { + node = 'privacymine.net' + port = 11898 + } + + return [{ node, port}, db.data.walletNames] +} + + +async function checkNodeStatus (node) { + try { + const req = await fetch('http://' + node.node + ':' + node.port.toString() + '/getinfo') + + const res = await req.json() + + if (res.status === 'OK') return true + } catch (e) { + console.log('Node error') + } + + sender('node-not-ok') + return false +} + +const getAddress = async () => { + return await js_wallet.getPrimaryAddress() +} + +const getAddresses = () => { + return js_wallet.subWallets.getAddresses() +} + + +const checkBalance = async () => { + try { + let [munlockedBalance, mlockedBalance] = await js_wallet.getBalance() + + if (munlockedBalance < 11) { + mainWindow.webContents.send('error-notify-message', 'Not enough unlocked funds.') + return false + } + } catch (err) { + return false + } + return true +} + +const resetWallet = (block) => { + js_wallet.reset(block) +} + +const getSubWallets = () => { + return js_wallet.subWallets.subWallets +} + +const sendTransactionAdvanced = (destinations, mixin, fee, paymentID, walletsToTaeFrom, changeAddress, relay, sendAll, extra) => { + return js_wallet.sendTransactionAdvanced(destinations, mixin, fee, paymentID, walletsToTaeFrom, changeAddress, relay, sendAll, extra) +} + + +module.exports = {loadDaemon, createWallet, importFromSeed, loginWallet, loadWallet, saveWallet, saveWalletToFile, pickNode, saveNode, loadMiscData, checkPassword, createMessageSubWallet, getXKRKeypair, getPrivKeys, getSyncStatus, getAddress, getAddresses, getSubWallets, checkBalance, getSpendableTransactionInputs, sendTransactionAdvanced, resetWallet} +