diff --git a/compactIDB.js b/compactIDB.js new file mode 100644 index 0000000..ab4c3c0 --- /dev/null +++ b/compactIDB.js @@ -0,0 +1,259 @@ +(function(EXPORTS) { //compactIDB v2.1.0 + /* Compact IndexedDB operations */ + 'use strict'; + const compactIDB = EXPORTS; + + var defaultDB; + + const indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; + const IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction; + const IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange; + + if (!indexedDB) { + console.error("Your browser doesn't support a stable version of IndexedDB."); + return; + } + + compactIDB.setDefaultDB = dbName => defaultDB = dbName; + + Object.defineProperty(compactIDB, 'default', { + get: () => defaultDB, + set: dbName => defaultDB = dbName + }); + + function getDBversion(dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + resolve(db.version) + db.close() + }).catch(error => reject(error)) + }) + } + + function upgradeDB(dbName, createList = null, deleteList = null) { + return new Promise((resolve, reject) => { + getDBversion(dbName).then(version => { + var idb = indexedDB.open(dbName, version + 1); + idb.onerror = (event) => reject("Error in opening IndexedDB"); + idb.onupgradeneeded = (event) => { + let db = event.target.result; + if (createList instanceof Object) { + if (Array.isArray(createList)) { + let tmp = {} + createList.forEach(o => tmp[o] = {}) + createList = tmp + } + for (let o in createList) { + let obs = db.createObjectStore(o, createList[o].options || {}); + if (createList[o].indexes instanceof Object) + for (let i in createList[o].indexes) + obs.createIndex(i, i, createList[o].indexes || {}); + } + } + if (Array.isArray(deleteList)) + deleteList.forEach(o => db.deleteObjectStore(o)); + resolve('Database upgraded') + } + idb.onsuccess = (event) => event.target.result.close(); + }).catch(error => reject(error)) + }) + } + + compactIDB.initDB = function(dbName, objectStores = {}) { + return new Promise((resolve, reject) => { + if (!(objectStores instanceof Object)) + return reject('ObjectStores must be an object or array') + defaultDB = defaultDB || dbName; + var idb = indexedDB.open(dbName); + idb.onerror = (event) => reject("Error in opening IndexedDB"); + idb.onsuccess = (event) => { + var db = event.target.result; + let cList = Object.values(db.objectStoreNames); + var obs = {}, + a_obs = {}, + d_obs = []; + if (!Array.isArray(objectStores)) + var obs = objectStores + else + objectStores.forEach(o => obs[o] = {}) + let nList = Object.keys(obs) + for (let o of nList) + if (!cList.includes(o)) + a_obs[o] = obs[o] + for (let o of cList) + if (!nList.includes(o)) + d_obs.push(o) + if (!Object.keys(a_obs).length && !d_obs.length) + resolve("Initiated IndexedDB"); + else + upgradeDB(dbName, a_obs, d_obs) + .then(result => resolve(result)) + .catch(error => reject(error)) + db.close(); + } + }); + } + + const openDB = compactIDB.openDB = function(dbName = defaultDB) { + return new Promise((resolve, reject) => { + var idb = indexedDB.open(dbName); + idb.onerror = (event) => reject("Error in opening IndexedDB"); + idb.onupgradeneeded = (event) => { + event.target.result.close(); + deleteDB(dbName).then(_ => null).catch(_ => null).finally(_ => reject("Datebase not found")) + } + idb.onsuccess = (event) => resolve(event.target.result); + }); + } + + const deleteDB = compactIDB.deleteDB = function(dbName = defaultDB) { + return new Promise((resolve, reject) => { + var deleteReq = indexedDB.deleteDatabase(dbName);; + deleteReq.onerror = (event) => reject("Error deleting database!"); + deleteReq.onsuccess = (event) => resolve("Database deleted successfully"); + }); + } + + compactIDB.writeData = function(obsName, data, key = false, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readwrite").objectStore(obsName); + let writeReq = (key ? obs.put(data, key) : obs.put(data)); + writeReq.onsuccess = (evt) => resolve(`Write data Successful`); + writeReq.onerror = (evt) => reject( + `Write data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.addData = function(obsName, data, key = false, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readwrite").objectStore(obsName); + let addReq = (key ? obs.add(data, key) : obs.add(data)); + addReq.onsuccess = (evt) => resolve(`Add data successful`); + addReq.onerror = (evt) => reject( + `Add data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.removeData = function(obsName, key, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readwrite").objectStore(obsName); + let delReq = obs.delete(key); + delReq.onsuccess = (evt) => resolve(`Removed Data ${key}`); + delReq.onerror = (evt) => reject( + `Remove data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.clearData = function(obsName, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readwrite").objectStore(obsName); + let clearReq = obs.clear(); + clearReq.onsuccess = (evt) => resolve(`Clear data Successful`); + clearReq.onerror = (evt) => reject(`Clear data Unsuccessful`); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.readData = function(obsName, key, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + let getReq = obs.get(key); + getReq.onsuccess = (evt) => resolve(evt.target.result); + getReq.onerror = (evt) => reject( + `Read data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.readAllData = function(obsName, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + var tmpResult = {} + let curReq = obs.openCursor(); + curReq.onsuccess = (evt) => { + var cursor = evt.target.result; + if (cursor) { + tmpResult[cursor.primaryKey] = cursor.value; + cursor.continue(); + } else + resolve(tmpResult); + } + curReq.onerror = (evt) => reject( + `Read-All data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + /* compactIDB.searchData = function (obsName, options = {}, dbName = defaultDB) { + + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + var filteredResult = {} + let keyRange; + if(options.lowerKey!==null && options.upperKey!==null) + keyRange = IDBKeyRange.bound(options.lowerKey, options.upperKey); + else if(options.lowerKey!==null) + keyRange = IDBKeyRange.lowerBound(options.lowerKey); + else if (options.upperKey!==null) + keyRange = IDBKeyRange.upperBound(options.upperBound); + else if (options.atKey) + let curReq = obs.openCursor(keyRange, ) + }).catch(error => reject(error)) + }) + }*/ + + compactIDB.searchData = function(obsName, options = {}, dbName = defaultDB) { + options.lowerKey = options.atKey || options.lowerKey || 0 + options.upperKey = options.atKey || options.upperKey || false + options.patternEval = options.patternEval || ((k, v) => { + return true + }) + options.limit = options.limit || false; + options.lastOnly = options.lastOnly || false + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + var filteredResult = {} + let curReq = obs.openCursor( + options.upperKey ? IDBKeyRange.bound(options.lowerKey, options.upperKey) : IDBKeyRange.lowerBound(options.lowerKey), + options.lastOnly ? "prev" : "next"); + curReq.onsuccess = (evt) => { + var cursor = evt.target.result; + if (cursor) { + if (options.patternEval(cursor.primaryKey, cursor.value)) { + filteredResult[cursor.primaryKey] = cursor.value; + options.lastOnly ? resolve(filteredResult) : cursor.continue(); + } else + cursor.continue(); + } else + resolve(filteredResult); + } + curReq.onerror = (evt) => reject(`Search unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`); + db.close(); + }).catch(error => reject(error)); + }); + } + + +})(window.compactIDB = {}) \ No newline at end of file diff --git a/floBlockchainAPI.js b/floBlockchainAPI.js new file mode 100644 index 0000000..8d3d857 --- /dev/null +++ b/floBlockchainAPI.js @@ -0,0 +1,514 @@ +(function(EXPORTS) { //floBlockchainAPI v2.3.0 + /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/ + 'use strict'; + const floBlockchainAPI = EXPORTS; + + const serverList = floGlobals.apiURL[floGlobals.blockchain].slice(0); + var curPos = floCrypto.randInt(0, serverList - 1); + + function fetch_retry(apicall, rm_flosight) { + return new Promise((resolve, reject) => { + let i = serverList.indexOf(rm_flosight) + if (i != -1) serverList.splice(i, 1); + curPos = floCrypto.randInt(0, serverList.length - 1); + fetch_api(apicall) + .then(result => resolve(result)) + .catch(error => reject(error)); + }) + } + + function fetch_api(apicall) { + return new Promise((resolve, reject) => { + if (serverList.length === 0) + reject("No floSight server working"); + else { + let flosight = serverList[curPos]; + fetch(flosight + apicall).then(response => { + if (response.ok) + response.json().then(data => resolve(data)); + else { + fetch_retry(apicall, flosight) + .then(result => resolve(result)) + .catch(error => reject(error)); + } + }).catch(error => { + fetch_retry(apicall, flosight) + .then(result => resolve(result)) + .catch(error => reject(error)); + }) + } + }) + } + + Object.defineProperty(floBlockchainAPI, 'current_server', { + get: () => serverList[curPos] + }); + + //Promised function to get data from API + const promisedAPI = floBlockchainAPI.promisedAPI = function(apicall) { + return new Promise((resolve, reject) => { + //console.log(apicall); + fetch_api(apicall) + .then(result => resolve(result)) + .catch(error => reject(error)); + }); + } + + //Get balance for the given Address + const getBalance = floBlockchainAPI.getBalance = function(addr) { + return new Promise((resolve, reject) => { + promisedAPI(`api/addr/${addr}/balance`) + .then(balance => resolve(parseFloat(balance))) + .catch(error => reject(error)); + }); + } + + //Send Tx to blockchain + const sendTx = floBlockchainAPI.sendTx = function(senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + else if (!floCrypto.validateAddr(senderAddr)) + return reject(`Invalid address : ${senderAddr}`); + else if (!floCrypto.validateAddr(receiverAddr)) + return reject(`Invalid address : ${receiverAddr}`); + else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr)) + return reject("Invalid Private key!"); + else if (typeof sendAmt !== 'number' || sendAmt <= 0) + return reject(`Invalid sendAmt : ${sendAmt}`); + + //get unconfirmed tx list + promisedAPI(`api/addr/${senderAddr}`).then(result => { + readTxs(senderAddr, 0, result.unconfirmedTxApperances).then(result => { + let unconfirmedSpent = {}; + for (let tx of result.items) + if (tx.confirmations == 0) + for (let vin of tx.vin) + if (vin.addr === senderAddr) { + if (Array.isArray(unconfirmedSpent[vin.txid])) + unconfirmedSpent[vin.txid].push(vin.vout); + else + unconfirmedSpent[vin.txid] = [vin.vout]; + } + //get utxos list + promisedAPI(`api/addr/${senderAddr}/utxo`).then(utxos => { + //form/construct the transaction data + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + var fee = floGlobals.fee; + for (var i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) + continue; //A transaction has already used the utxo, but is unconfirmed. + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < sendAmt + fee) + reject("Insufficient FLO balance!"); + else { + trx.addoutput(receiverAddr, sendAmt); + var change = utxoAmt - sendAmt - fee; + if (change > 0) + trx.addoutput(senderAddr, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + var signedTxHash = trx.sign(privKey, 1); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }); + } + + //Write Data into blockchain + floBlockchainAPI.writeData = function(senderAddr, data, privKey, receiverAddr = floGlobals.adminID, strict_utxo = true) { + return new Promise((resolve, reject) => { + if (typeof data != "string") + data = JSON.stringify(data); + sendTx(senderAddr, receiverAddr, floGlobals.sendAmt, privKey, data, strict_utxo) + .then(txid => resolve(txid)) + .catch(error => reject(error)); + }); + } + + //merge all UTXOs of a given floID into a single UTXO + floBlockchainAPI.mergeUTXOs = function(floID, privKey, floData = '') { + return new Promise((resolve, reject) => { + if (!floCrypto.validateAddr(floID)) + return reject(`Invalid floID`); + if (!floCrypto.verifyPrivKey(privKey, floID)) + return reject("Invalid Private Key"); + if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + var fee = floGlobals.fee; + promisedAPI(`api/addr/${floID}/utxo`).then(utxos => { + for (var i = utxos.length - 1; i >= 0; i--) + if (utxos[i].confirmations) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + } + trx.addoutput(floID, utxoAmt - fee); + trx.addflodata(floData.replace(/\n/g, ' ')); + var signedTxHash = trx.sign(privKey, 1); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + /**Write data into blockchain from (and/or) to multiple floID + * @param {Array} senderPrivKeys List of sender private-keys + * @param {string} data FLO data of the txn + * @param {Array} receivers List of receivers + * @param {boolean} preserveRatio (optional) preserve ratio or equal contribution + * @return {Promise} + */ + floBlockchainAPI.writeDataMultiple = function(senderPrivKeys, data, receivers = [floGlobals.adminID], preserveRatio = true) { + return new Promise((resolve, reject) => { + if (!Array.isArray(senderPrivKeys)) + return reject("Invalid senderPrivKeys: SenderPrivKeys must be Array"); + if (!preserveRatio) { + let tmp = {}; + let amount = (floGlobals.sendAmt * receivers.length) / senderPrivKeys.length; + senderPrivKeys.forEach(key => tmp[key] = amount); + senderPrivKeys = tmp; + } + if (!Array.isArray(receivers)) + return reject("Invalid receivers: Receivers must be Array"); + else { + let tmp = {}; + let amount = floGlobals.sendAmt; + receivers.forEach(floID => tmp[floID] = amount); + receivers = tmp + } + if (typeof data != "string") + data = JSON.stringify(data); + sendTxMultiple(senderPrivKeys, receivers, data) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }) + } + + /**Send Tx from (and/or) to multiple floID + * @param {Array or Object} senderPrivKeys List of sender private-key (optional: with coins to be sent) + * @param {Object} receivers List of receivers with respective amount to be sent + * @param {string} floData FLO data of the txn + * @return {Promise} + */ + const sendTxMultiple = floBlockchainAPI.sendTxMultiple = function(senderPrivKeys, receivers, floData = '') { + return new Promise((resolve, reject) => { + if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + let senders = {}, + preserveRatio; + //check for argument validations + try { + let invalids = { + InvalidSenderPrivKeys: [], + InvalidSenderAmountFor: [], + InvalidReceiverIDs: [], + InvalidReceiveAmountFor: [] + } + let inputVal = 0, + outputVal = 0; + //Validate sender privatekeys (and send amount if passed) + //conversion when only privateKeys are passed (preserveRatio mode) + if (Array.isArray(senderPrivKeys)) { + senderPrivKeys.forEach(key => { + try { + if (!key) + invalids.InvalidSenderPrivKeys.push(key); + else { + let floID = floCrypto.getFloID(key); + senders[floID] = { + wif: key + } + } + } catch (error) { + invalids.InvalidSenderPrivKeys.push(key) + } + }) + preserveRatio = true; + } + //conversion when privatekeys are passed with send amount + else { + for (let key in senderPrivKeys) { + try { + if (!key) + invalids.InvalidSenderPrivKeys.push(key); + else { + if (typeof senderPrivKeys[key] !== 'number' || senderPrivKeys[key] <= 0) + invalids.InvalidSenderAmountFor.push(key); + else + inputVal += senderPrivKeys[key]; + let floID = floCrypto.getFloID(key); + senders[floID] = { + wif: key, + coins: senderPrivKeys[key] + } + } + } catch (error) { + invalids.InvalidSenderPrivKeys.push(key) + } + } + preserveRatio = false; + } + //Validate the receiver IDs and receive amount + for (let floID in receivers) { + if (!floCrypto.validateAddr(floID)) + invalids.InvalidReceiverIDs.push(floID); + if (typeof receivers[floID] !== 'number' || receivers[floID] <= 0) + invalids.InvalidReceiveAmountFor.push(floID); + else + outputVal += receivers[floID]; + } + //Reject if any invalids are found + for (let i in invalids) + if (!invalids[i].length) + delete invalids[i]; + if (Object.keys(invalids).length) + return reject(invalids); + //Reject if given inputVal and outputVal are not equal + if (!preserveRatio && inputVal != outputVal) + return reject(`Input Amount (${inputVal}) not equal to Output Amount (${outputVal})`); + } catch (error) { + return reject(error) + } + //Get balance of senders + let promises = []; + for (let floID in senders) + promises.push(getBalance(floID)); + Promise.all(promises).then(results => { + let totalBalance = 0, + totalFee = floGlobals.fee, + balance = {}; + //Divide fee among sender if not for preserveRatio + if (!preserveRatio) + var dividedFee = totalFee / Object.keys(senders).length; + //Check if balance of each sender is sufficient enough + let insufficient = []; + for (let floID in senders) { + balance[floID] = parseFloat(results.shift()); + if (isNaN(balance[floID]) || (preserveRatio && balance[floID] <= totalFee) || + (!preserveRatio && balance[floID] < senders[floID].coins + dividedFee)) + insufficient.push(floID); + totalBalance += balance[floID]; + } + if (insufficient.length) + return reject({ + InsufficientBalance: insufficient + }) + //Calculate totalSentAmount and check if totalBalance is sufficient + let totalSendAmt = totalFee; + for (floID in receivers) + totalSendAmt += receivers[floID]; + if (totalBalance < totalSendAmt) + return reject("Insufficient total Balance"); + //Get the UTXOs of the senders + let promises = []; + for (floID in senders) + promises.push(promisedAPI(`api/addr/${floID}/utxo`)); + Promise.all(promises).then(results => { + let wifSeq = []; + var trx = bitjs.transaction(); + for (floID in senders) { + let utxos = results.shift(); + let sendAmt; + if (preserveRatio) { + let ratio = (balance[floID] / totalBalance); + sendAmt = totalSendAmt * ratio; + } else + sendAmt = senders[floID].coins + dividedFee; + let wif = senders[floID].wif; + let utxoAmt = 0.0; + for (let i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt); i--) { + if (utxos[i].confirmations) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + wifSeq.push(wif); + utxoAmt += utxos[i].amount; + } + } + if (utxoAmt < sendAmt) + return reject("Insufficient balance:" + floID); + let change = (utxoAmt - sendAmt); + if (change > 0) + trx.addoutput(floID, change); + } + for (floID in receivers) + trx.addoutput(floID, receivers[floID]); + trx.addflodata(floData.replace(/\n/g, ' ')); + for (let i = 0; i < wifSeq.length; i++) + trx.signinput(i, wifSeq[i], 1); + var signedTxHash = trx.serialize(); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //Broadcast signed Tx in blockchain using API + const broadcastTx = floBlockchainAPI.broadcastTx = function(signedTxHash) { + return new Promise((resolve, reject) => { + if (signedTxHash.length < 1) + return reject("Empty Signature"); + var url = serverList[curPos] + 'api/tx/send'; + fetch(url, { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: `{"rawtx":"${signedTxHash}"}` + }).then(response => { + if (response.ok) + response.json().then(data => resolve(data.txid.result)); + else + response.text().then(data => resolve(data)); + }).catch(error => reject(error)); + }) + } + + floBlockchainAPI.getTx = function(txid) { + return new Promise((resolve, reject) => { + promisedAPI(`api/tx/${txid}`) + .then(response => resolve(response)) + .catch(error => reject(error)) + }) + } + + //Read Txs of Address between from and to + const readTxs = floBlockchainAPI.readTxs = function(addr, from, to) { + return new Promise((resolve, reject) => { + promisedAPI(`api/addrs/${addr}/txs?from=${from}&to=${to}`) + .then(response => resolve(response)) + .catch(error => reject(error)) + }); + } + + //Read All Txs of Address (newest first) + floBlockchainAPI.readAllTxs = function(addr) { + return new Promise((resolve, reject) => { + promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => { + promisedAPI(`api/addrs/${addr}/txs?from=0&to=${response.totalItems}0`) + .then(response => resolve(response.items)) + .catch(error => reject(error)); + }).catch(error => reject(error)) + }); + } + + /*Read flo Data from txs of given Address + options can be used to filter data + limit : maximum number of filtered data (default = 1000, negative = no limit) + ignoreOld : ignore old txs (default = 0) + sentOnly : filters only sent data + receivedOnly: filters only received data + pattern : filters data that with JSON pattern + filter : custom filter funtion for floData (eg . filter: d => {return d[0] == '$'}) + tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details) + sender : flo-id(s) of sender + receiver : flo-id(s) of receiver + */ + floBlockchainAPI.readData = function(addr, options = {}) { + options.limit = options.limit || 0; + options.ignoreOld = options.ignoreOld || 0; + if (typeof options.sender === "string") options.sender = [options.sender]; + if (typeof options.receiver === "string") options.receiver = [options.receiver]; + return new Promise((resolve, reject) => { + promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => { + var newItems = response.totalItems - options.ignoreOld; + promisedAPI(`api/addrs/${addr}/txs?from=0&to=${newItems*2}`).then(response => { + if (options.limit <= 0) + options.limit = response.items.length; + var filteredData = []; + let numToRead = response.totalItems - options.ignoreOld, + unconfirmedCount = 0; + for (let i = 0; i < numToRead && filteredData.length < options.limit; i++) { + if (!response.items[i].confirmations) { //unconfirmed transactions + unconfirmedCount++; + if (numToRead < response.items[i].length) + numToRead++; + continue; + } + if (options.pattern) { + try { + let jsonContent = JSON.parse(response.items[i].floData); + if (!Object.keys(jsonContent).includes(options.pattern)) + continue; + } catch (error) { + continue; + } + } + if (options.sentOnly) { + let flag = false; + for (let vin of response.items[i].vin) + if (vin.addr === addr) { + flag = true; + break; + } + if (!flag) continue; + } + if (Array.isArray(options.sender)) { + let flag = false; + for (let vin of response.items[i].vin) + if (options.sender.includes(vin.addr)) { + flag = true; + break; + } + if (!flag) continue; + } + if (options.receivedOnly) { + let flag = false; + for (let vout of response.items[i].vout) + if (vout.scriptPubKey.addresses[0] === addr) { + flag = true; + break; + } + if (!flag) continue; + } + if (Array.isArray(options.receiver)) { + let flag = false; + for (let vout of response.items[i].vout) + if (options.receiver.includes(vout.scriptPubKey.addresses[0])) { + flag = true; + break; + } + if (!flag) continue; + } + if (options.filter && !options.filter(response.items[i].floData)) + continue; + + if (options.tx) { + let d = {} + d.txid = response.items[i].txid; + d.time = response.items[i].time; + d.blockheight = response.items[i].blockheight; + d.data = response.items[i].floData; + filteredData.push(d); + } else + filteredData.push(response.items[i].floData); + } + resolve({ + totalTxs: response.totalItems - unconfirmedCount, + data: filteredData + }); + }).catch(error => { + reject(error); + }); + }).catch(error => { + reject(error); + }); + }); + } + + +})('object' === typeof module ? module.exports : window.floBlockchainAPI = {}); \ No newline at end of file diff --git a/floCloudAPI.js b/floCloudAPI.js new file mode 100644 index 0000000..3758337 --- /dev/null +++ b/floCloudAPI.js @@ -0,0 +1,920 @@ +(function(EXPORTS) { //floCloudAPI v2.2.0a + /* FLO Cloud operations to send/request application data*/ + 'use strict'; + const floCloudAPI = EXPORTS; + + var kBucket; + const K_Bucket = floCloudAPI.K_Bucket = function(masterID, nodeList) { + + const decodeID = floID => { + let k = bitjs.Base58.decode(floID); + k.shift(); + k.splice(-4, 4); + let decodedId = Crypto.util.bytesToHex(k); + let nodeIdBigInt = new BigInteger(decodedId, 16); + let nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned(); + let nodeIdNewInt8Array = new Uint8Array(nodeIdBytes); + return nodeIdNewInt8Array; + }; + + const _KB = new BuildKBucket({ + localNodeId: decodeID(masterID) + }); + nodeList.forEach(id => _KB.add({ + id: decodeID(id), + floID: id + })); + + const _CO = nodeList.map(id => [_KB.distance(_KB.localNodeId, decodeID(id)), id]) + .sort((a, b) => a[0] - b[0]) + .map(a => a[1]); + + const self = this; + Object.defineProperty(self, 'tree', { + get: () => _KB + }); + Object.defineProperty(self, 'list', { + get: () => Array.from(_CO) + }); + + self.isNode = floID => _CO.includes(floID); + self.innerNodes = function(id1, id2) { + if (!_CO.includes(id1) || !_CO.includes(id2)) + throw Error('Given nodes are not supernode'); + let iNodes = [] + for (let i = _CO.indexOf(id1) + 1; _CO[i] != id2; i++) { + if (i < _CO.length) + iNodes.push(_CO[i]) + else i = -1 + } + return iNodes + } + self.outterNodes = function(id1, id2) { + if (!_CO.includes(id1) || !_CO.includes(id2)) + throw Error('Given nodes are not supernode'); + let oNodes = [] + for (let i = _CO.indexOf(id2) + 1; _CO[i] != id1; i++) { + if (i < _CO.length) + oNodes.push(_CO[i]) + else i = -1 + } + return oNodes + } + self.prevNode = function(id, N = 1) { + let n = N || _CO.length; + if (!_CO.includes(id)) + throw Error('Given node is not supernode'); + let pNodes = [] + for (let i = 0, j = _CO.indexOf(id) - 1; i < n; j--) { + if (j == _CO.indexOf(id)) + break; + else if (j > -1) + pNodes[i++] = _CO[j] + else j = _CO.length + } + return (N == 1 ? pNodes[0] : pNodes) + } + self.nextNode = function(id, N = 1) { + let n = N || _CO.length; + if (!_CO.includes(id)) + throw Error('Given node is not supernode'); + if (!n) n = _CO.length; + let nNodes = [] + for (let i = 0, j = _CO.indexOf(id) + 1; i < n; j++) { + if (j == _CO.indexOf(id)) + break; + else if (j < _CO.length) + nNodes[i++] = _CO[j] + else j = -1 + } + return (N == 1 ? nNodes[0] : nNodes) + } + self.closestNode = function(id, N = 1) { + let decodedId = decodeID(id); + let n = N || _CO.length; + let cNodes = _KB.closest(decodedId, n) + .map(k => k.floID) + return (N == 1 ? cNodes[0] : cNodes) + } + } + + floCloudAPI.init = function startCloudProcess(SNStorageID = floGlobals.SNStorageID, nodeList = null) { + return new Promise((resolve, reject) => { + try { + nodeList = nodeList || Object.keys(floGlobals.supernodes); + kBucket = new K_Bucket(SNStorageID, nodeList); + resolve('Cloud init successful'); + } catch (error) { + reject(error); + } + }) + } + + Object.defineProperty(floCloudAPI, 'kBucket', { + get: () => kBucket + }); + + const _inactive = new Set(); + + function ws_connect(snID) { + return new Promise((resolve, reject) => { + if (!(snID in floGlobals.supernodes)) + return reject(`${snID} is not a supernode`) + if (_inactive.has(snID)) + return reject(`${snID} is not active`) + var wsConn = new WebSocket("wss://" + floGlobals.supernodes[snID].uri + "/"); + wsConn.onopen = evt => resolve(wsConn); + wsConn.onerror = evt => { + _inactive.add(snID) + reject(`${snID} is unavailable`) + } + }) + } + + function ws_activeConnect(snID, reverse = false) { + return new Promise((resolve, reject) => { + if (_inactive.size === kBucket.list.length) + return reject('Cloud offline'); + if (!(snID in floGlobals.supernodes)) + snID = kBucket.closestNode(snID); + ws_connect(snID) + .then(node => resolve(node)) + .catch(error => { + if (reverse) + var nxtNode = kBucket.prevNode(snID); + else + var nxtNode = kBucket.nextNode(snID); + ws_activeConnect(nxtNode, reverse) + .then(node => resolve(node)) + .catch(error => reject(error)) + }) + }) + } + + function fetch_API(snID, data) { + return new Promise((resolve, reject) => { + if (_inactive.has(snID)) + return reject(`${snID} is not active`); + let fetcher, sn_url = "https://" + floGlobals.supernodes[snID].uri; + if (typeof data === "string") + fetcher = fetch(sn_url + "?" + data); + else if (typeof data === "object" && data.method === "POST") + fetcher = fetch(sn_url, data); + fetcher.then(response => { + if (response.ok || response.status === 400 || response.status === 500) + resolve(response); + else + reject(response); + }).catch(error => reject(error)) + }) + } + + function fetch_ActiveAPI(snID, data, reverse = false) { + return new Promise((resolve, reject) => { + if (_inactive.size === kBucket.list.length) + return reject('Cloud offline'); + if (!(snID in floGlobals.supernodes)) + snID = kBucket.closestNode(snID); + fetch_API(snID, data) + .then(result => resolve(result)) + .catch(error => { + _inactive.add(snID) + if (reverse) + var nxtNode = kBucket.prevNode(snID); + else + var nxtNode = kBucket.nextNode(snID); + fetch_ActiveAPI(nxtNode, data, reverse) + .then(result => resolve(result)) + .catch(error => reject(error)); + }) + }) + } + + function singleRequest(floID, data_obj, method = "POST") { + return new Promise((resolve, reject) => { + let data; + if (method === "POST") + data = { + method: "POST", + body: JSON.stringify(data_obj) + }; + else + data = new URLSearchParams(JSON.parse(JSON.stringify(data_obj))).toString(); + fetch_ActiveAPI(floID, data).then(response => { + if (response.ok) + response.json() + .then(result => resolve(result)) + .catch(error => reject(error)) + else response.text() + .then(result => reject(response.status + ": " + result)) //Error Message from Node + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + const _liveRequest = {}; + + function liveRequest(floID, request, callback) { + const filterData = typeof request.status !== 'undefined' ? + data => { + if (request.status) + return data; + else { + let filtered = {}; + for (let i in data) + if (request.trackList.includes(i)) + filtered[i] = data[i]; + return filtered; + } + } : + data => { + data = objectifier(data); + let filtered = {}, + r = request; + for (let v in data) { + let d = data[v]; + if ((!r.atVectorClock || r.atVectorClock == v) && + (r.atVectorClock || !r.lowerVectorClock || r.lowerVectorClock <= v) && + (r.atVectorClock || !r.upperVectorClock || r.upperVectorClock >= v) && + (!r.afterTime || r.afterTime < d.log_time) && + r.application == d.application && + r.receiverID == d.receiverID && + (!r.comment || r.comment == d.comment) && + (!r.type || r.type == d.type) && + (!r.senderID || r.senderID.includes(d.senderID))) + filtered[v] = data[v]; + } + return filtered; + }; + + return new Promise((resolve, reject) => { + ws_activeConnect(floID).then(node => { + let randID = floCrypto.randString(5); + node.send(JSON.stringify(request)); + node.onmessage = (evt) => { + let d = null, + e = null; + try { + d = filterData(JSON.parse(evt.data)); + } catch (error) { + e = evt.data + } finally { + callback(d, e) + } + } + _liveRequest[randID] = node; + _liveRequest[randID].request = request; + resolve(randID); + }).catch(error => reject(error)); + }); + } + + Object.defineProperty(floCloudAPI, 'liveRequest', { + get: () => _liveRequest + }); + + Object.defineProperty(floCloudAPI, 'inactive', { + get: () => _inactive + }); + + const util = floCloudAPI.util = {}; + + const encodeMessage = util.encodeMessage = function(message) { + return btoa(unescape(encodeURIComponent(JSON.stringify(message)))) + } + + const decodeMessage = util.decodeMessage = function(message) { + return JSON.parse(decodeURIComponent(escape(atob(message)))) + } + + const filterKey = util.filterKey = function(type, options) { + return type + (options.comment ? ':' + options.comment : '') + + '|' + (options.group || options.receiverID || floGlobals.adminID) + + '|' + (options.application || floGlobals.application); + } + + const lastCommit = {}; + Object.defineProperty(lastCommit, 'get', { + value: objName => JSON.parse(lastCommit[objName]) + }); + Object.defineProperty(lastCommit, 'set', { + value: objName => lastCommit[objName] = JSON.stringify(floGlobals.appObjects[objName]) + }); + + function updateObject(objectName, dataSet) { + try { + console.log(dataSet) + let vcList = Object.keys(dataSet).sort(); + for (let vc of vcList) { + if (vc < floGlobals.lastVC[objectName] || dataSet[vc].type !== objectName) + continue; + switch (dataSet[vc].comment) { + case "RESET": + if (dataSet[vc].message.reset) + floGlobals.appObjects[objectName] = dataSet[vc].message.reset; + break; + case "UPDATE": + if (dataSet[vc].message.diff) + floGlobals.appObjects[objectName] = diff.merge(floGlobals.appObjects[objectName], dataSet[vc].message.diff); + } + floGlobals.lastVC[objectName] = vc; + } + lastCommit.set(objectName); + compactIDB.writeData("appObjects", floGlobals.appObjects[objectName], objectName); + compactIDB.writeData("lastVC", floGlobals.lastVC[objectName], objectName); + } catch (error) { + console.error(error) + } + } + + function storeGeneral(fk, dataSet) { + try { + console.log(dataSet) + if (typeof floGlobals.generalData[fk] !== "object") + floGlobals.generalData[fk] = {} + for (let vc in dataSet) { + floGlobals.generalData[fk][vc] = dataSet[vc]; + if (dataSet[vc].log_time > floGlobals.lastVC[fk]) + floGlobals.lastVC[fk] = dataSet[vc].log_time; + } + compactIDB.writeData("lastVC", floGlobals.lastVC[fk], fk) + compactIDB.writeData("generalData", floGlobals.generalData[fk], fk) + } catch (error) { + console.error(error) + } + } + + function objectifier(data) { + if (!Array.isArray(data)) + data = [data]; + return Object.fromEntries(data.map(d => { + d.message = decodeMessage(d.message); + return [d.vectorClock, d]; + })); + } + + //set status as online for myFloID + floCloudAPI.setStatus = function(options = {}) { + return new Promise((resolve, reject) => { + let callback = options.callback instanceof Function ? options.callback : (d, e) => console.debug(d, e); + var request = { + floID: myFloID, + application: options.application || floGlobals.application, + time: Date.now(), + status: true, + pubKey: myPubKey + } + let hashcontent = ["time", "application", "floID"].map(d => request[d]).join("|"); + request.sign = floCrypto.signData(hashcontent, myPrivKey); + liveRequest(options.refID || floGlobals.adminID, request, callback) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //request status of floID(s) in trackList + floCloudAPI.requestStatus = function(trackList, options = {}) { + return new Promise((resolve, reject) => { + if (!Array.isArray(trackList)) + trackList = [trackList]; + let callback = options.callback instanceof Function ? options.callback : (d, e) => console.debug(d, e); + let request = { + status: false, + application: options.application || floGlobals.application, + trackList: trackList + } + liveRequest(options.refID || floGlobals.adminID, request, callback) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //send any message to supernode cloud storage + const sendApplicationData = floCloudAPI.sendApplicationData = function(message, type, options = {}) { + return new Promise((resolve, reject) => { + var data = { + senderID: myFloID, + receiverID: options.receiverID || floGlobals.adminID, + pubKey: myPubKey, + message: encodeMessage(message), + time: Date.now(), + application: options.application || floGlobals.application, + type: type, + comment: options.comment || "" + } + let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"] + .map(d => data[d]).join("|") + data.sign = floCrypto.signData(hashcontent, myPrivKey); + singleRequest(data.receiverID, data) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //request any data from supernode cloud + const requestApplicationData = floCloudAPI.requestApplicationData = function(type, options = {}) { + return new Promise((resolve, reject) => { + var request = { + receiverID: options.receiverID || floGlobals.adminID, + senderID: options.senderID || undefined, + application: options.application || floGlobals.application, + type: type, + comment: options.comment || undefined, + lowerVectorClock: options.lowerVectorClock || undefined, + upperVectorClock: options.upperVectorClock || undefined, + atVectorClock: options.atVectorClock || undefined, + afterTime: options.afterTime || undefined, + mostRecent: options.mostRecent || undefined, + } + + if (options.callback instanceof Function) { + liveRequest(request.receiverID, request, options.callback) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + if (options.method === "POST") + request = { + time: Date.now(), + request + }; + singleRequest(request.receiverID, request, options.method || "GET") + .then(data => resolve(data)).catch(error => reject(error)) + } + }) + } + + //(NEEDS UPDATE) delete data from supernode cloud (received only) + floCloudAPI.deleteApplicationData = function(vectorClocks, options = {}) { + return new Promise((resolve, reject) => { + var delreq = { + requestorID: myFloID, + pubKey: myPubKey, + time: Date.now(), + delete: (Array.isArray(vectorClocks) ? vectorClocks : [vectorClocks]), + application: options.application || floGlobals.application + } + let hashcontent = ["time", "application", "delete"] + .map(d => delreq[d]).join("|") + delreq.sign = floCrypto.signData(hashcontent, myPrivKey) + singleRequest(delreq.requestorID, delreq).then(result => { + let success = [], + failed = []; + result.forEach(r => r.status === 'fulfilled' ? + success.push(r.value) : failed.push(r.reason)); + resolve({ + success, + failed + }) + }).catch(error => reject(error)) + }) + } + + //(NEEDS UPDATE) edit comment of data in supernode cloud (mutable comments only) + floCloudAPI.editApplicationData = function(vectorClock, newComment, oldData, options = {}) { + return new Promise((resolve, reject) => { + let p0 + if (!oldData) { + options.atVectorClock = vectorClock; + options.callback = false; + p0 = requestApplicationData(false, options) + } else + p0 = Promise.resolve({ + vectorClock: { + ...oldData + } + }) + p0.then(d => { + if (d.senderID != myFloID) + return reject("Invalid requestorID") + else if (!d.comment.startsWith("EDIT:")) + return reject("Data immutable") + let data = { + requestorID: myFloID, + receiverID: d.receiverID, + time: Date.now(), + application: d.application, + edit: { + vectorClock: vectorClock, + comment: newComment + } + } + d.comment = data.edit.comment; + let hashcontent = ["receiverID", "time", "application", "type", "message", + "comment" + ] + .map(x => d[x]).join("|") + data.edit.sign = floCrypto.signData(hashcontent, myPrivKey) + singleRequest(data.receiverID, data) + .then(result => resolve("Data comment updated")) + .catch(error => reject(error)) + }) + }) + } + + //tag data in supernode cloud (subAdmin access only) + floCloudAPI.tagApplicationData = function(vectorClock, tag, options = {}) { + return new Promise((resolve, reject) => { + if (!floGlobals.subAdmins.includes(myFloID)) + return reject("Only subAdmins can tag data") + var request = { + receiverID: options.receiverID || floGlobals.adminID, + requestorID: myFloID, + pubKey: myPubKey, + time: Date.now(), + vectorClock: vectorClock, + tag: tag, + } + let hashcontent = ["time", "vectorClock", 'tag'].map(d => request[d]).join("|"); + request.sign = floCrypto.signData(hashcontent, myPrivKey); + singleRequest(request.receiverID, request) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //note data in supernode cloud (receiver only or subAdmin allowed if receiver is adminID) + floCloudAPI.noteApplicationData = function(vectorClock, note, options = {}) { + return new Promise((resolve, reject) => { + var request = { + receiverID: options.receiverID || floGlobals.adminID, + requestorID: myFloID, + pubKey: myPubKey, + time: Date.now(), + vectorClock: vectorClock, + note: note, + } + let hashcontent = ["time", "vectorClock", 'note'].map(d => request[d]).join("|"); + request.sign = floCrypto.signData(hashcontent, myPrivKey); + singleRequest(request.receiverID, request) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //send general data + floCloudAPI.sendGeneralData = function(message, type, options = {}) { + return new Promise((resolve, reject) => { + if (options.encrypt) { + let encryptionKey = options.encrypt === true ? + floGlobals.settings.encryptionKey : options.encrypt + message = floCrypto.encryptData(JSON.stringify(message), encryptionKey) + } + sendApplicationData(message, type, options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //request general data + floCloudAPI.requestGeneralData = function(type, options = {}) { + return new Promise((resolve, reject) => { + var fk = filterKey(type, options) + floGlobals.lastVC[fk] = parseInt(floGlobals.lastVC[fk]) || 0; + options.afterTime = options.afterTime || floGlobals.lastVC[fk]; + if (options.callback instanceof Function) { + let new_options = Object.create(options) + new_options.callback = (d, e) => { + storeGeneral(fk, d); + options.callback(d, e) + } + requestApplicationData(type, new_options) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + requestApplicationData(type, options).then(dataSet => { + storeGeneral(fk, objectifier(dataSet)) + resolve(dataSet) + }).catch(error => reject(error)) + } + }) + } + + //request an object data from supernode cloud + floCloudAPI.requestObjectData = function(objectName, options = {}) { + return new Promise((resolve, reject) => { + options.lowerVectorClock = options.lowerVectorClock || floGlobals.lastVC[objectName] + 1; + options.senderID = [false, null].includes(options.senderID) ? null : + options.senderID || floGlobals.subAdmins; + options.mostRecent = true; + options.comment = 'RESET'; + let callback = null; + if (options.callback instanceof Function) { + let old_callback = options.callback; + callback = (d, e) => { + updateObject(objectName, d); + old_callback(d, e); + } + delete options.callback; + } + requestApplicationData(objectName, options).then(dataSet => { + updateObject(objectName, objectifier(dataSet)); + delete options.comment; + options.lowerVectorClock = floGlobals.lastVC[objectName] + 1; + delete options.mostRecent; + if (callback) { + let new_options = Object.create(options); + new_options.callback = callback; + requestApplicationData(objectName, new_options) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + requestApplicationData(objectName, options).then(dataSet => { + updateObject(objectName, objectifier(dataSet)) + resolve(floGlobals.appObjects[objectName]) + }).catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + } + + floCloudAPI.closeRequest = function(requestID) { + return new Promise((resolve, reject) => { + let conn = _liveRequest[requestID] + if (!conn) + return reject('Request not found') + conn.onclose = evt => { + delete _liveRequest[requestID]; + resolve('Request connection closed') + } + conn.close() + }) + } + + //reset or initialize an object and send it to cloud + floCloudAPI.resetObjectData = function(objectName, options = {}) { + return new Promise((resolve, reject) => { + let message = { + reset: floGlobals.appObjects[objectName] + } + options.comment = 'RESET'; + sendApplicationData(message, objectName, options).then(result => { + lastCommit.set(objectName); + resolve(result) + }).catch(error => reject(error)) + }) + } + + //update the diff and send it to cloud + floCloudAPI.updateObjectData = function(objectName, options = {}) { + return new Promise((resolve, reject) => { + let message = { + diff: diff.find(lastCommit.get(objectName), floGlobals.appObjects[ + objectName]) + } + options.comment = 'UPDATE'; + sendApplicationData(message, objectName, options).then(result => { + lastCommit.set(objectName); + resolve(result) + }).catch(error => reject(error)) + }) + } + + /* + Functions: + findDiff(original, updatedObj) returns an object with the added, deleted and updated differences + mergeDiff(original, allDiff) returns a new object from original object merged with all differences (allDiff is returned object of findDiff) + */ + var diff = (function() { + const isDate = d => d instanceof Date; + const isEmpty = o => Object.keys(o).length === 0; + const isObject = o => o != null && typeof o === 'object'; + const properObject = o => isObject(o) && !o.hasOwnProperty ? { + ...o + } : o; + const getLargerArray = (l, r) => l.length > r.length ? l : r; + + const preserve = (diff, left, right) => { + if (!isObject(diff)) return diff; + return Object.keys(diff).reduce((acc, key) => { + const leftArray = left[key]; + const rightArray = right[key]; + if (Array.isArray(leftArray) && Array.isArray(rightArray)) { + const array = [...getLargerArray(leftArray, rightArray)]; + return { + ...acc, + [key]: array.reduce((acc2, item, index) => { + if (diff[key].hasOwnProperty(index)) { + acc2[index] = preserve(diff[key][index], leftArray[index], rightArray[index]); // diff recurse and check for nested arrays + return acc2; + } + delete acc2[index]; // no diff aka empty + return acc2; + }, array) + }; + } + return { + ...acc, + [key]: diff[key] + }; + }, {}); + }; + + const updatedDiff = (lhs, rhs) => { + if (lhs === rhs) return {}; + if (!isObject(lhs) || !isObject(rhs)) return rhs; + const l = properObject(lhs); + const r = properObject(rhs); + if (isDate(l) || isDate(r)) { + if (l.valueOf() == r.valueOf()) return {}; + return r; + } + return Object.keys(r).reduce((acc, key) => { + if (l.hasOwnProperty(key)) { + const difference = updatedDiff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; + return { + ...acc, + [key]: difference + }; + } + return acc; + }, {}); + }; + + + const diff = (lhs, rhs) => { + if (lhs === rhs) return {}; // equal return no diff + if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs + const l = properObject(lhs); + const r = properObject(rhs); + const deletedValues = Object.keys(l).reduce((acc, key) => { + return r.hasOwnProperty(key) ? acc : { + ...acc, + [key]: null + }; + }, {}); + if (isDate(l) || isDate(r)) { + if (l.valueOf() == r.valueOf()) return {}; + return r; + } + return Object.keys(r).reduce((acc, key) => { + if (!l.hasOwnProperty(key)) return { + ...acc, + [key]: r[key] + }; // return added r key + const difference = diff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff + return { + ...acc, + [key]: difference + }; // return updated key + }, deletedValues); + }; + + const addedDiff = (lhs, rhs) => { + if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {}; + const l = properObject(lhs); + const r = properObject(rhs); + return Object.keys(r).reduce((acc, key) => { + if (l.hasOwnProperty(key)) { + const difference = addedDiff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference)) return acc; + return { + ...acc, + [key]: difference + }; + } + return { + ...acc, + [key]: r[key] + }; + }, {}); + }; + + const arrayDiff = (lhs, rhs) => { + if (lhs === rhs) return {}; // equal return no diff + if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs + const l = properObject(lhs); + const r = properObject(rhs); + const deletedValues = Object.keys(l).reduce((acc, key) => { + return r.hasOwnProperty(key) ? acc : { + ...acc, + [key]: null + }; + }, {}); + if (isDate(l) || isDate(r)) { + if (l.valueOf() == r.valueOf()) return {}; + return r; + } + if (Array.isArray(r) && Array.isArray(l)) { + const deletedValues = l.reduce((acc, item, index) => { + return r.hasOwnProperty(index) ? acc.concat(item) : acc.concat(null); + }, []); + return r.reduce((acc, rightItem, index) => { + if (!deletedValues.hasOwnProperty(index)) { + return acc.concat(rightItem); + } + const leftItem = l[index]; + const difference = diff(rightItem, leftItem); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) { + delete acc[index]; + return acc; // return no diff + } + return acc.slice(0, index).concat(rightItem).concat(acc.slice(index + 1)); // return updated key + }, deletedValues); + } + + return Object.keys(r).reduce((acc, key) => { + if (!l.hasOwnProperty(key)) return { + ...acc, + [key]: r[key] + }; // return added r key + const difference = diff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff + return { + ...acc, + [key]: difference + }; // return updated key + }, deletedValues); + }; + + const deletedDiff = (lhs, rhs) => { + if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {}; + const l = properObject(lhs); + const r = properObject(rhs); + return Object.keys(l).reduce((acc, key) => { + if (r.hasOwnProperty(key)) { + const difference = deletedDiff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference)) return acc; + return { + ...acc, + [key]: difference + }; + } + return { + ...acc, + [key]: null + }; + }, {}); + }; + + const mergeRecursive = (obj1, obj2) => { + for (var p in obj2) { + try { + if (obj2[p].constructor == Object) + obj1[p] = mergeRecursive(obj1[p], obj2[p]); + // Property in destination object set; update its value. + else if (Ext.isArray(obj2[p])) { + // obj1[p] = []; + if (obj2[p].length < 1) + obj1[p] = obj2[p]; + else + obj1[p] = mergeRecursive(obj1[p], obj2[p]); + } else + obj1[p] = obj2[p]; + } catch (e) { + // Property in destination object not set; create it and set its value. + obj1[p] = obj2[p]; + } + } + return obj1; + } + + const cleanse = (obj) => { + Object.keys(obj).forEach(key => { + var value = obj[key]; + if (typeof value === "object" && value !== null) { + // Recurse... + cleanse(value); + // ...and remove if now "empty" (NOTE: insert your definition of "empty" here) + //if (!Object.keys(value).length) + // delete obj[key]; + } else if (value === null) + delete obj[key]; // null, remove it + }); + if (obj.constructor.toString().indexOf("Array") != -1) { + obj = obj.filter(function(el) { + return el != null; + }); + } + return obj; + } + + + const findDiff = (lhs, rhs) => ({ + added: addedDiff(lhs, rhs), + deleted: deletedDiff(lhs, rhs), + updated: updatedDiff(lhs, rhs), + }); + + /*obj is original object or array, diff is the output of findDiff */ + const mergeDiff = (obj, diff) => { + if (Object.keys(diff.updated).length !== 0) + obj = mergeRecursive(obj, diff.updated) + if (Object.keys(diff.deleted).length !== 0) { + obj = mergeRecursive(obj, diff.deleted) + obj = cleanse(obj) + } + if (Object.keys(diff.added).length !== 0) + obj = mergeRecursive(obj, diff.added) + return obj + } + + return { + find: findDiff, + merge: mergeDiff + } + })(); + + +})('object' === typeof module ? module.exports : window.floCloudAPI = {}) \ No newline at end of file diff --git a/floCrypto.js b/floCrypto.js new file mode 100644 index 0000000..4ab4d5c --- /dev/null +++ b/floCrypto.js @@ -0,0 +1,313 @@ +(function(EXPORTS) { //floCrypto v2.3.0a + /* FLO Crypto Operators */ + 'use strict'; + const floCrypto = EXPORTS; + + const p = BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16); + const ecparams = EllipticCurve.getSECCurveByName("secp256k1"); + const ascii_alternatives = `‘ '\n’ '\n“ "\n” "\n– --\n— ---\n≥ >=\n≤ <=\n≠ !=\n× *\n÷ /\n← <-\n→ ->\n↔ <->\n⇒ =>\n⇐ <=\n⇔ <=>`; + const exponent1 = () => p.add(BigInteger.ONE).divide(BigInteger("4")); + + function calculateY(x) { + let exp = exponent1(); + // x is x value of public key in BigInteger format without 02 or 03 or 04 prefix + return x.modPow(BigInteger("3"), p).add(BigInteger("7")).mod(p).modPow(exp, p) + } + + function getUncompressedPublicKey(compressedPublicKey) { + // Fetch x from compressedPublicKey + let pubKeyBytes = Crypto.util.hexToBytes(compressedPublicKey); + const prefix = pubKeyBytes.shift() // remove prefix + let prefix_modulus = prefix % 2; + pubKeyBytes.unshift(0) // add prefix 0 + let x = new BigInteger(pubKeyBytes) + let xDecimalValue = x.toString() + // Fetch y + let y = calculateY(x); + let yDecimalValue = y.toString(); + // verify y value + let resultBigInt = y.mod(BigInteger("2")); + let check = resultBigInt.toString() % 2; + if (prefix_modulus !== check) + yDecimalValue = y.negate().mod(p).toString(); + return { + x: xDecimalValue, + y: yDecimalValue + }; + } + + function getSenderPublicKeyString() { + let privateKey = ellipticCurveEncryption.senderRandom(); + var senderPublicKeyString = ellipticCurveEncryption.senderPublicString(privateKey); + return { + privateKey: privateKey, + senderPublicKeyString: senderPublicKeyString + } + } + + function deriveSharedKeySender(receiverPublicKeyHex, senderPrivateKey) { + let receiverPublicKeyString = getUncompressedPublicKey(receiverPublicKeyHex); + var senderDerivedKey = ellipticCurveEncryption.senderSharedKeyDerivation( + receiverPublicKeyString.x, receiverPublicKeyString.y, senderPrivateKey); + return senderDerivedKey; + } + + function deriveSharedKeyReceiver(senderPublicKeyString, receiverPrivateKey) { + return ellipticCurveEncryption.receiverSharedKeyDerivation( + senderPublicKeyString.XValuePublicString, senderPublicKeyString.YValuePublicString, receiverPrivateKey); + } + + function getReceiverPublicKeyString(privateKey) { + return ellipticCurveEncryption.receiverPublicString(privateKey); + } + + function wifToDecimal(pk_wif, isPubKeyCompressed = false) { + let pk = Bitcoin.Base58.decode(pk_wif) + pk.shift() + pk.splice(-4, 4) + //If the private key corresponded to a compressed public key, also drop the last byte (it should be 0x01). + if (isPubKeyCompressed == true) pk.pop() + pk.unshift(0) + let privateKeyDecimal = BigInteger(pk).toString() + let privateKeyHex = Crypto.util.bytesToHex(pk) + return { + privateKeyDecimal: privateKeyDecimal, + privateKeyHex: privateKeyHex + } + } + + //generate a random Interger within range + floCrypto.randInt = function(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + //generate a random String within length (options : alphaNumeric chars only) + floCrypto.randString = function(length, alphaNumeric = true) { + var result = ''; + var characters = alphaNumeric ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' : + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+-./*?@#&$<>=[]{}():'; + for (var i = 0; i < length; i++) + result += characters.charAt(Math.floor(Math.random() * characters.length)); + return result; + } + + //Encrypt Data using public-key + floCrypto.encryptData = function(data, receiverPublicKeyHex) { + var senderECKeyData = getSenderPublicKeyString(); + var senderDerivedKey = deriveSharedKeySender(receiverPublicKeyHex, senderECKeyData.privateKey); + let senderKey = senderDerivedKey.XValue + senderDerivedKey.YValue; + let secret = Crypto.AES.encrypt(data, senderKey); + return { + secret: secret, + senderPublicKeyString: senderECKeyData.senderPublicKeyString + }; + } + + //Decrypt Data using private-key + floCrypto.decryptData = function(data, privateKeyHex) { + var receiverECKeyData = {}; + if (typeof privateKeyHex !== "string") throw new Error("No private key found."); + let privateKey = wifToDecimal(privateKeyHex, true); + if (typeof privateKey.privateKeyDecimal !== "string") throw new Error("Failed to detremine your private key."); + receiverECKeyData.privateKey = privateKey.privateKeyDecimal; + var receiverDerivedKey = deriveSharedKeyReceiver(data.senderPublicKeyString, receiverECKeyData.privateKey); + let receiverKey = receiverDerivedKey.XValue + receiverDerivedKey.YValue; + let decryptMsg = Crypto.AES.decrypt(data.secret, receiverKey); + return decryptMsg; + } + + //Sign data using private-key + floCrypto.signData = function(data, privateKeyHex) { + var key = new Bitcoin.ECKey(privateKeyHex); + key.setCompressed(true); + var privateKeyArr = key.getBitcoinPrivateKeyByteArray(); + var privateKey = BigInteger.fromByteArrayUnsigned(privateKeyArr); + var messageHash = Crypto.SHA256(data); + var messageHashBigInteger = new BigInteger(messageHash); + var messageSign = Bitcoin.ECDSA.sign(messageHashBigInteger, key.priv); + var sighex = Crypto.util.bytesToHex(messageSign); + return sighex; + } + + //Verify signatue of the data using public-key + floCrypto.verifySign = function(data, signatureHex, publicKeyHex) { + var msgHash = Crypto.SHA256(data); + var messageHashBigInteger = new BigInteger(msgHash); + var sigBytes = Crypto.util.hexToBytes(signatureHex); + var signature = Bitcoin.ECDSA.parseSig(sigBytes); + var publicKeyPoint = ecparams.getCurve().decodePointHex(publicKeyHex); + var verify = Bitcoin.ECDSA.verifyRaw(messageHashBigInteger, signature.r, signature.s, publicKeyPoint); + return verify; + } + + //Generates a new flo ID and returns private-key, public-key and floID + const generateNewID = floCrypto.generateNewID = function() { + var key = new Bitcoin.ECKey(false); + key.setCompressed(true); + return { + floID: key.getBitcoinAddress(), + pubKey: key.getPubKeyHex(), + privKey: key.getBitcoinWalletImportFormat() + } + } + + Object.defineProperty(floCrypto, 'newID', { + get: () => generateNewID() + }); + + //Returns public-key from private-key + floCrypto.getPubKeyHex = function(privateKeyHex) { + if (!privateKeyHex) + return null; + var key = new Bitcoin.ECKey(privateKeyHex); + if (key.priv == null) + return null; + key.setCompressed(true); + return key.getPubKeyHex(); + } + + //Returns flo-ID from public-key or private-key + floCrypto.getFloID = function(keyHex) { + if (!keyHex) + return null; + try { + var key = new Bitcoin.ECKey(keyHex); + if (key.priv == null) + key.setPub(keyHex); + return key.getBitcoinAddress(); + } catch { + return null; + } + } + + //Verify the private-key for the given public-key or flo-ID + floCrypto.verifyPrivKey = function(privateKeyHex, pubKey_floID, isfloID = true) { + if (!privateKeyHex || !pubKey_floID) + return false; + try { + var key = new Bitcoin.ECKey(privateKeyHex); + if (key.priv == null) + return false; + key.setCompressed(true); + if (isfloID && pubKey_floID == key.getBitcoinAddress()) + return true; + else if (!isfloID && pubKey_floID == key.getPubKeyHex()) + return true; + else + return false; + } catch { + return null; + } + } + + //Check if the given Address is valid or not + floCrypto.validateFloID = floCrypto.validateAddr = function(inpAddr) { + if (!inpAddr) + return false; + try { + let addr = new Bitcoin.Address(inpAddr); + return true; + } catch { + return false; + } + } + + //Split the str using shamir's Secret and Returns the shares + floCrypto.createShamirsSecretShares = function(str, total_shares, threshold_limit) { + try { + if (str.length > 0) { + var strHex = shamirSecretShare.str2hex(str); + var shares = shamirSecretShare.share(strHex, total_shares, threshold_limit); + return shares; + } + return false; + } catch { + return false + } + } + + //Returns the retrived secret by combining the shamirs shares + const retrieveShamirSecret = floCrypto.retrieveShamirSecret = function(sharesArray) { + try { + if (sharesArray.length > 0) { + var comb = shamirSecretShare.combine(sharesArray.slice(0, sharesArray.length)); + comb = shamirSecretShare.hex2str(comb); + return comb; + } + return false; + } catch { + return false; + } + } + + //Verifies the shares and str + floCrypto.verifyShamirsSecret = function(sharesArray, str) { + if (!str) + return null; + else if (retrieveShamirSecret(sharesArray) === str) + return true; + else + return false; + } + + const validateASCII = floCrypto.validateASCII = function(string, bool = true) { + if (typeof string !== "string") + return null; + if (bool) { + let x; + for (let i = 0; i < string.length; i++) { + x = string.charCodeAt(i); + if (x < 32 || x > 127) + return false; + } + return true; + } else { + let x, invalids = {}; + for (let i = 0; i < string.length; i++) { + x = string.charCodeAt(i); + if (x < 32 || x > 127) + if (x in invalids) + invalids[string[i]].push(i) + else + invalids[string[i]] = [i]; + } + if (Object.keys(invalids).length) + return invalids; + else + return true; + } + } + + floCrypto.convertToASCII = function(string, mode = 'soft-remove') { + let chars = validateASCII(string, false); + if (chars === true) + return string; + else if (chars === null) + return null; + let convertor, result = string, + refAlt = {}; + ascii_alternatives.split('\n').forEach(a => refAlt[a[0]] = a.slice(2)); + mode = mode.toLowerCase(); + if (mode === "hard-unicode") + convertor = (c) => `\\u${('000'+c.charCodeAt().toString(16)).slice(-4)}`; + else if (mode === "soft-unicode") + convertor = (c) => refAlt[c] || `\\u${('000'+c.charCodeAt().toString(16)).slice(-4)}`; + else if (mode === "hard-remove") + convertor = c => ""; + else if (mode === "soft-remove") + convertor = c => refAlt[c] || ""; + else + return null; + for (let c in chars) + result = result.replaceAll(c, convertor(c)); + return result; + } + + floCrypto.revertUnicode = function(string) { + return string.replace(/\\u[\dA-F]{4}/gi, + m => String.fromCharCode(parseInt(m.replace(/\\u/g, ''), 16))); + } + +})('object' === typeof module ? module.exports : window.floCrypto = {}); \ No newline at end of file diff --git a/floDapps.js b/floDapps.js new file mode 100644 index 0000000..18d818d --- /dev/null +++ b/floDapps.js @@ -0,0 +1,603 @@ +(function(EXPORTS) { //floDapps v2.2.0 + /* General functions for FLO Dapps*/ + //'use strict'; + const floDapps = EXPORTS; + + function initIndexedDB() { + return new Promise((resolve, reject) => { + var obs_g = { + //general + lastTx: {}, + //supernode (cloud list) + supernodes: { + indexes: { + uri: null, + pubKey: null + } + } + } + var obs_a = { + //login credentials + credentials: {}, + //for Dapps + subAdmins: {}, + settings: {}, + appObjects: {}, + generalData: {}, + lastVC: {} + } + //add other given objectStores + initIndexedDB.appObs = initIndexedDB.appObs || {} + for (o in initIndexedDB.appObs) + if (!(o in obs_a)) + obs_a[o] = initIndexedDB.appObs[o] + Promise.all([ + compactIDB.initDB(floGlobals.application, obs_a), + compactIDB.initDB("floDapps", obs_g) + ]).then(result => { + compactIDB.setDefaultDB(floGlobals.application) + resolve("IndexedDB App Storage Initated Successfully") + }).catch(error => reject(error)); + }) + } + + function initUserDB(floID) { + return new Promise((resolve, reject) => { + var obs = { + contacts: {}, + pubKeys: {}, + messages: {} + } + compactIDB.initDB(`floDapps#${floID}`, obs).then(result => { + resolve("UserDB Initated Successfully") + }).catch(error => reject('Init userDB failed')); + }) + } + + function loadUserDB(floID) { + return new Promise((resolve, reject) => { + var loadData = ["contacts", "pubKeys", "messages"] + var promises = [] + for (var i = 0; i < loadData.length; i++) + promises[i] = compactIDB.readAllData(loadData[i], `floDapps#${floID}`) + Promise.all(promises).then(results => { + for (var i = 0; i < loadData.length; i++) + floGlobals[loadData[i]] = results[i] + resolve("Loaded Data from userDB") + }).catch(error => reject('Load userDB failed')) + }) + } + + const startUpFunctions = []; + + startUpFunctions.push(function readSupernodeListFromAPI() { + return new Promise((resolve, reject) => { + compactIDB.readData("lastTx", floGlobals.SNStorageID, "floDapps").then(lastTx => { + floBlockchainAPI.readData(floGlobals.SNStorageID, { + ignoreOld: lastTx, + sentOnly: true, + pattern: "SuperNodeStorage" + }).then(result => { + for (var i = result.data.length - 1; i >= 0; i--) { + var content = JSON.parse(result.data[i]).SuperNodeStorage; + for (sn in content.removeNodes) + compactIDB.removeData("supernodes", sn, "floDapps"); + for (sn in content.newNodes) + compactIDB.writeData("supernodes", content.newNodes[sn], sn, "floDapps"); + } + compactIDB.writeData("lastTx", result.totalTxs, floGlobals.SNStorageID, "floDapps"); + compactIDB.readAllData("supernodes", "floDapps").then(result => { + floGlobals.supernodes = result; + floCloudAPI.init() + .then(result => resolve("Loaded Supernode list\n" + result)) + .catch(error => reject(error)) + }) + }) + }).catch(error => reject(error)) + }) + }); + + startUpFunctions.push(function readAppConfigFromAPI() { + return new Promise((resolve, reject) => { + compactIDB.readData("lastTx", `${floGlobals.application}|${floGlobals.adminID}`, "floDapps").then(lastTx => { + floBlockchainAPI.readData(floGlobals.adminID, { + ignoreOld: lastTx, + sentOnly: true, + pattern: floGlobals.application + }).then(result => { + for (var i = result.data.length - 1; i >= 0; i--) { + var content = JSON.parse(result.data[i])[floGlobals.application]; + if (!content || typeof content !== "object") + continue; + if (Array.isArray(content.removeSubAdmin)) + for (var j = 0; j < content.removeSubAdmin.length; j++) + compactIDB.removeData("subAdmins", content.removeSubAdmin[j]); + if (Array.isArray(content.addSubAdmin)) + for (var k = 0; k < content.addSubAdmin.length; k++) + compactIDB.writeData("subAdmins", true, content.addSubAdmin[k]); + if (content.settings) + for (let l in content.settings) + compactIDB.writeData("settings", content.settings[l], l) + } + compactIDB.writeData("lastTx", result.totalTxs, `${floGlobals.application}|${floGlobals.adminID}`, "floDapps"); + compactIDB.readAllData("subAdmins").then(result => { + floGlobals.subAdmins = Object.keys(result); + compactIDB.readAllData("settings").then(result => { + floGlobals.settings = result; + resolve("Read app configuration from blockchain"); + }) + }) + }) + }).catch(error => reject(error)) + }) + }); + + startUpFunctions.push(function loadDataFromAppIDB() { + return new Promise((resolve, reject) => { + var loadData = ["appObjects", "generalData", "lastVC"] + var promises = [] + for (var i = 0; i < loadData.length; i++) + promises[i] = compactIDB.readAllData(loadData[i]) + Promise.all(promises).then(results => { + for (var i = 0; i < loadData.length; i++) + floGlobals[loadData[i]] = results[i] + resolve("Loaded Data from app IDB") + }).catch(error => reject(error)) + }) + }); + + function getCredentials() { + + const inputFn = getCredentials.privKeyInput || + (type => new Promise((resolve, reject) => { + let inputVal = prompt(`Enter ${type}: `) + if (inputVal === null) + reject(null) + else + resolve(inputVal) + })); + + const readSharesFromIDB = indexArr => new Promise((resolve, reject) => { + var promises = [] + for (var i = 0; i < indexArr.length; i++) + promises.push(compactIDB.readData('credentials', indexArr[i])) + Promise.all(promises).then(shares => { + var secret = floCrypto.retrieveShamirSecret(shares) + if (secret) + resolve(secret) + else + reject("Shares are insufficient or incorrect") + }).catch(error => { + clearCredentials(); + location.reload(); + }) + }); + + const writeSharesToIDB = (shares, i = 0, resultIndexes = []) => new Promise(resolve => { + if (i >= shares.length) + return resolve(resultIndexes) + var n = floCrypto.randInt(0, 100000) + compactIDB.addData("credentials", shares[i], n).then(res => { + resultIndexes.push(n) + writeSharesToIDB(shares, i + 1, resultIndexes) + .then(result => resolve(result)) + }).catch(error => { + writeSharesToIDB(shares, i, resultIndexes) + .then(result => resolve(result)) + }) + }); + + const getPrivateKeyCredentials = () => new Promise((resolve, reject) => { + var indexArr = localStorage.getItem(`${floGlobals.application}#privKey`) + if (indexArr) { + readSharesFromIDB(JSON.parse(indexArr)) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + var privKey; + inputFn("PRIVATE_KEY").then(result => { + if (!result) + return reject("Empty Private Key") + var floID = floCrypto.getFloID(result) + if (!floID || !floCrypto.validateAddr(floID)) + return reject("Invalid Private Key") + privKey = result; + }).catch(error => { + console.log(error, "Generating Random Keys") + privKey = floCrypto.generateNewID().privKey + }).finally(_ => { + if (!privKey) + return; + var threshold = floCrypto.randInt(10, 20) + var shares = floCrypto.createShamirsSecretShares(privKey, threshold, threshold) + writeSharesToIDB(shares).then(resultIndexes => { + //store index keys in localStorage + localStorage.setItem(`${floGlobals.application}#privKey`, JSON.stringify(resultIndexes)) + //also add a dummy privatekey to the IDB + var randomPrivKey = floCrypto.generateNewID().privKey + var randomThreshold = floCrypto.randInt(10, 20) + var randomShares = floCrypto.createShamirsSecretShares(randomPrivKey, randomThreshold, randomThreshold) + writeSharesToIDB(randomShares) + //resolve private Key + resolve(privKey) + }) + }) + } + }); + + const checkIfPinRequired = key => new Promise((resolve, reject) => { + if (key.length == 52) + resolve(key) + else { + inputFn("PIN/Password").then(pwd => { + try { + let privKey = Crypto.AES.decrypt(key, pwd); + resolve(privKey) + } catch (error) { + reject("Access Denied: Incorrect PIN/Password") + } + }).catch(error => reject("Access Denied: PIN/Password required")) + } + }); + + return new Promise((resolve, reject) => { + getPrivateKeyCredentials().then(key => { + checkIfPinRequired(key).then(privKey => { + try { + myPrivKey = privKey + myPubKey = floCrypto.getPubKeyHex(myPrivKey) + myFloID = floCrypto.getFloID(myPubKey) + resolve('Login Credentials loaded successful') + } catch (error) { + console.log(error) + reject("Corrupted Private Key") + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + var startUpLog = (status, log) => status ? console.log(log) : console.error(log); + + const callStartUpFunction = i => new Promise((resolve, reject) => { + startUpFunctions[i]().then(result => { + callStartUpFunction.completed += 1; + startUpLog(true, `${result}\nCompleted ${callStartUpFunction.completed}/${callStartUpFunction.total} Startup functions`) + resolve(true) + }).catch(error => { + callStartUpFunction.failed += 1; + startUpLog(false, `${error}\nFailed ${callStartUpFunction.failed}/${callStartUpFunction.total} Startup functions`) + reject(false) + }) + }); + + var _midFunction; + const midStartUp = () => new Promise((res, rej) => { + if (_midFunction instanceof Function) { + _midFunction() + .then(r => res("Mid startup function completed")) + .catch(e => rej("Mid startup function failed")) + } else + res("No mid startup function") + }); + + const callAndLog = p => new Promise((res, rej) => { + p.then(r => { + startUpLog(true, r) + res(r) + }).catch(e => { + startUpLog(false, e) + rej(e) + }) + }); + + floDapps.launchStartUp = function() { + return new Promise((resolve, reject) => { + initIndexedDB().then(log => { + console.log(log) + callStartUpFunction.total = startUpFunctions.length; + callStartUpFunction.completed = 0; + callStartUpFunction.failed = 0; + let p1 = new Promise((res, rej) => { + Promise.all(startUpFunctions.map((f, i) => callStartUpFunction(i))).then(r => { + callAndLog(midStartUp()) + .then(r => res(true)) + .catch(e => rej(false)) + }) + }); + let p2 = new Promise((res, rej) => { + callAndLog(getCredentials()).then(r => { + callAndLog(initUserDB(myFloID)).then(r => { + callAndLog(loadUserDB(myFloID)) + .then(r => res(true)) + .catch(e => rej(false)) + }).catch(e => rej(false)) + }).catch(e => rej(false)) + }) + Promise.all([p1, p2]) + .then(r => resolve('App Startup finished successful')) + .catch(e => reject('App Startup failed')) + }).catch(error => reject("App database initiation failed")) + }) + } + + floDapps.addStartUpFunction = fn => fn instanceof Function && !startUpFunctions.includes(fn) ? startUpFunctions.push(fn) : false; + + floDapps.setMidStartup = fn => fn instanceof Function ? _midFunction = fn : false; + + floDapps.setCustomStartupLogger = fn => fn instanceof Function ? startUpLog = fn : false; + + floDapps.setCustomPrivKeyInput = fn => fn instanceof Function ? customFn = fn : false; + + floDapps.setAppObjectStores = appObs => initIndexedDB.appObs = appObs; + + floDapps.storeContact = function(floID, name) { + return new Promise((resolve, reject) => { + if (!floCrypto.validateAddr(floID)) + return reject("Invalid floID!") + compactIDB.writeData("contacts", name, floID, `floDapps#${myFloID}`).then(result => { + floGlobals.contacts[floID] = name; + resolve("Contact stored") + }).catch(error => reject(error)) + }); + } + + floDapps.storePubKey = function(floID, pubKey) { + return new Promise((resolve, reject) => { + if (floID in floGlobals.pubKeys) + return resolve("pubKey already stored") + if (!floCrypto.validateAddr(floID)) + return reject("Invalid floID!") + if (floCrypto.getFloID(pubKey) != floID) + return reject("Incorrect pubKey") + compactIDB.writeData("pubKeys", pubKey, floID, `floDapps#${myFloID}`).then(result => { + floGlobals.pubKeys[floID] = pubKey; + resolve("pubKey stored") + }).catch(error => reject(error)) + }); + } + + floDapps.sendMessage = function(floID, message) { + return new Promise((resolve, reject) => { + let options = { + receiverID: floID, + application: "floDapps", + comment: floGlobals.application + } + if (floID in floGlobals.pubKeys) + message = floCrypto.encryptData(JSON.stringify(message), floGlobals.pubKeys[floID]) + floCloudAPI.sendApplicationData(message, "Message", options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + floDapps.requestInbox = function(callback) { + return new Promise((resolve, reject) => { + let lastVC = Object.keys(floGlobals.messages).sort().pop() + let options = { + receiverID: myFloID, + application: "floDapps", + lowerVectorClock: lastVC + 1 + } + options.callback = (d, e) => { + for (let v in d) { + try { + if (d[v].message instanceof Object && "secret" in d[v].message) + d[v].message = floCrypto.decryptData(d[v].message, myPrivKey) + } catch (error) {} + compactIDB.writeData("messages", d[v], v, `floDapps#${myFloID}`) + floGlobals.messages[v] = d[v] + } + if (callback instanceof Function) + callback(d, e) + } + floCloudAPI.requestApplicationData("Message", options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + floDapps.manageAppConfig = function(adminPrivKey, addList, rmList, settings) { + return new Promise((resolve, reject) => { + if (!Array.isArray(addList) || !addList.length) addList = undefined; + if (!Array.isArray(rmList) || !rmList.length) rmList = undefined; + if (!settings || typeof settings !== "object" || !Object.keys(settings).length) settings = undefined; + if (!addList && !rmList && !settings) + return reject("No configuration change") + var floData = { + [floGlobals.application]: { + addSubAdmin: addList, + removeSubAdmin: rmList, + settings: settings + } + } + var floID = floCrypto.getFloID(adminPrivKey) + if (floID != floGlobals.adminID) + reject('Access Denied for Admin privilege') + else + floBlockchainAPI.writeData(floID, JSON.stringify(floData), adminPrivKey) + .then(result => resolve(['Updated App Configuration', result])) + .catch(error => reject(error)) + }) + } + + const clearCredentials = floDapps.clearCredentials = function() { + return new Promise((resolve, reject) => { + compactIDB.clearData('credentials', floGlobals.application).then(result => { + localStorage.removeItem(`${floGlobals.application}#privKey`) + myPrivKey = myPubKey = myFloID = undefined; + resolve("privKey credentials deleted!") + }).catch(error => reject(error)) + }) + } + + floDapps.deleteUserData = function(credentials = false) { + return new Promise((resolve, reject) => { + let p = [] + p.push(compactIDB.deleteDB(`floDapps#${myFloID}`)) + if (credentials) + p.push(clearCredentials()) + Promise.all(p) + .then(result => resolve('User database(local) deleted')) + .catch(error => reject(error)) + }) + } + + floDapps.deleteAppData = function() { + return new Promise((resolve, reject) => { + compactIDB.deleteDB(floGlobals.application).then(result => { + localStorage.removeItem(`${floGlobals.application}#privKey`) + myPrivKey = myPubKey = myFloID = undefined; + compactIDB.removeData('lastTx', `${floGlobals.application}|${floGlobals.adminID}`, 'floDapps') + .then(result => resolve("App database(local) deleted")) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + floDapps.securePrivKey = function(pwd) { + return new Promise((resolve, reject) => { + let indexArr = localStorage.getItem(`${floGlobals.application}#privKey`) + if (!indexArr) + return reject("PrivKey not found"); + indexArr = JSON.parse(indexArr) + let encryptedKey = Crypto.AES.encrypt(myPrivKey, pwd); + let threshold = indexArr.length; + let shares = floCrypto.createShamirsSecretShares(encryptedKey, threshold, threshold) + let promises = []; + let overwriteFn = (share, index) => + compactIDB.writeData("credentials", share, index, floGlobals.application); + for (var i = 0; i < threshold; i++) + promises.push(overwriteFn(shares[i], indexArr[i])); + Promise.all(promises) + .then(results => resolve("Private Key Secured")) + .catch(error => reject(error)) + }) + } + + floDapps.verifyPin = function(pin = null) { + const readSharesFromIDB = function(indexArr) { + return new Promise((resolve, reject) => { + var promises = [] + for (var i = 0; i < indexArr.length; i++) + promises.push(compactIDB.readData('credentials', indexArr[i])) + Promise.all(promises).then(shares => { + var secret = floCrypto.retrieveShamirSecret(shares) + console.info(shares, secret) + if (secret) + resolve(secret) + else + reject("Shares are insufficient or incorrect") + }).catch(error => { + clearCredentials(); + location.reload(); + }) + }) + } + return new Promise((resolve, reject) => { + var indexArr = localStorage.getItem(`${floGlobals.application}#privKey`) + console.info(indexArr) + if (!indexArr) + reject('No login credentials found') + readSharesFromIDB(JSON.parse(indexArr)).then(key => { + if (key.length == 52) { + if (pin === null) + resolve("Private key not secured") + else + reject("Private key not secured") + } else { + if (pin === null) + return reject("PIN/Password required") + try { + let privKey = Crypto.AES.decrypt(key, pin); + resolve("PIN/Password verified") + } catch (error) { + reject("Incorrect PIN/Password") + } + } + }).catch(error => reject(error)) + }) + } + + const getNextGeneralData = floDapps.getNextGeneralData = function(type, vectorClock = null, options = {}) { + var fk = floCloudAPI.util.filterKey(type, options) + vectorClock = vectorClock || getNextGeneralData[fk] || '0'; + var filteredResult = {} + if (floGlobals.generalData[fk]) { + for (let d in floGlobals.generalData[fk]) + if (d > vectorClock) + filteredResult[d] = JSON.parse(JSON.stringify(floGlobals.generalData[fk][d])) + } else if (options.comment) { + let comment = options.comment; + delete options.comment; + let fk = floCloudAPI.util.filterKey(type, options); + for (let d in floGlobals.generalData[fk]) + if (d > vectorClock && floGlobals.generalData[fk][d].comment == comment) + filteredResult[d] = JSON.parse(JSON.stringify(floGlobals.generalData[fk][d])) + } + if (options.decrypt) { + let decryptionKey = (options.decrypt === true) ? myPrivKey : options.decrypt; + if (!Array.isArray(decryptionKey)) + decryptionKey = [decryptionKey]; + for (let f in filteredResult) { + let data = filteredResult[f] + try { + if (data.message instanceof Object && "secret" in data.message) { + for (let key of decryptionKey) { + try { + let tmp = floCrypto.decryptData(data.message, key) + data.message = JSON.parse(tmp) + break; + } catch (error) {} + } + } + } catch (error) {} + } + } + getNextGeneralData[fk] = Object.keys(filteredResult).sort().pop(); + return filteredResult; + } + + const syncData = floDapps.syncData = {}; + + syncData.oldDevice = () => new Promise((resolve, reject) => { + let sync = { + contacts: floGlobals.contacts, + pubKeys: floGlobals.pubKeys, + messages: floGlobals.messages + } + let message = Crypto.AES.encrypt(JSON.stringify(sync), myPrivKey) + let options = { + receiverID: myFloID, + application: "floDapps" + } + floCloudAPI.sendApplicationData(message, "syncData", options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }); + + syncData.newDevice = () => new Promise((resolve, reject) => { + var options = { + receiverID: myFloID, + senderID: myFloID, + application: "floDapps", + mostRecent: true, + } + floCloudAPI.requestApplicationData("syncData", options).then(response => { + let vc = Object.keys(response).sort().pop() + let sync = JSON.parse(Crypto.AES.decrypt(response[vc].message, myPrivKey)) + let promises = [] + let store = (key, val, obs) => promises.push(compactIDB.writeData(obs, val, key, `floDapps#${floID}`)); + ["contacts", "pubKeys", "messages"].forEach(c => { + for (let i in sync[c]) { + store(i, sync[c][i], c) + floGlobals[c][i] = sync[c][i] + } + }) + Promise.all(promises) + .then(results => resolve("Sync data successful")) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }); +})('object' === typeof module ? module.exports : window.floDapps = {}) \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..eced94e --- /dev/null +++ b/index.html @@ -0,0 +1,61 @@ + + + +
+