Compare commits

..

No commits in common. "master" and "flosight" have entirely different histories.

14 changed files with 567 additions and 1377 deletions

File diff suppressed because it is too large Load Diff

1
btcOperator.min.js vendored

File diff suppressed because one or more lines are too long

1
compactIDB.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,13 +1,13 @@
(function (EXPORTS) { //floBlockchainAPI v3.1.4 (function (EXPORTS) { //floBlockchainAPI v2.5.6b
/* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/ /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/
'use strict'; 'use strict';
const floBlockchainAPI = EXPORTS; const floBlockchainAPI = EXPORTS;
const DEFAULT = { const DEFAULT = {
blockchain: floGlobals.blockchain, blockchain: floGlobals.blockchain,
apiURL: { apiURL: {
FLO: ['https://blockbook.ranchimall.net/'], FLO: ['https://flosight.ranchimall.net/'],
FLO_TEST: ['https://blockbook-testnet.ranchimall.net/'] FLO_TEST: ['https://flosight-testnet.ranchimall.net/']
}, },
sendAmt: 0.0003, sendAmt: 0.0003,
fee: 0.0002, fee: 0.0002,
@ -15,29 +15,9 @@
receiverID: floGlobals.adminID receiverID: floGlobals.adminID
}; };
floBlockchainAPI.apiURL = DEFAULT.apiURL[DEFAULT.blockchain][0];
const SATOSHI_IN_BTC = 1e8; const SATOSHI_IN_BTC = 1e8;
const isUndefined = val => typeof val === 'undefined'; const isUndefined = val => typeof val === 'undefined';
const checkIfTor = floBlockchainAPI.checkIfTor = () => {
return fetch('https://check.torproject.org/api/ip')
.then(res => res.json())
.then(res => {
return res.IsTor
}).catch(e => {
console.error(e)
return false
})
}
let isTor = false;
checkIfTor().then(result => {
isTor = result
if (isTor) {
DEFAULT.apiURL.FLO.push('http://xge4kejxl6xs4cad3u3a7dnw7idndlkn3vmyo33t3a4ctk566y65eoad.onion/')
DEFAULT.apiURL.FLO_TEST.push('http://fdjrsde2qhfecvx6fkgmcidwkp34bdek7jo4y2fpqatrhzxtxkk6f4ad.onion/')
}
});
const util = floBlockchainAPI.util = {}; const util = floBlockchainAPI.util = {};
util.Sat_to_FLO = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8)); util.Sat_to_FLO = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8));
@ -81,9 +61,9 @@
var serverList = Array.from(allServerList); var serverList = Array.from(allServerList);
var curPos = floCrypto.randInt(0, serverList.length - 1); var curPos = floCrypto.randInt(0, serverList.length - 1);
function fetch_retry(apicall, rm_node) { function fetch_retry(apicall, rm_flosight) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let i = serverList.indexOf(rm_node) let i = serverList.indexOf(rm_flosight)
if (i != -1) serverList.splice(i, 1); if (i != -1) serverList.splice(i, 1);
curPos = floCrypto.randInt(0, serverList.length - 1); curPos = floCrypto.randInt(0, serverList.length - 1);
fetch_api(apicall, false) fetch_api(apicall, false)
@ -102,19 +82,19 @@
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)); .catch(error => reject(error));
} else } else
reject("No FLO blockbook server working"); reject("No floSight server working");
} else { } else {
let serverURL = serverList[curPos]; let flosight = serverList[curPos];
fetch(serverURL + apicall).then(response => { fetch(flosight + apicall).then(response => {
if (response.ok) if (response.ok)
response.json().then(data => resolve(data)); response.json().then(data => resolve(data));
else { else {
fetch_retry(apicall, serverURL) fetch_retry(apicall, flosight)
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)); .catch(error => reject(error));
} }
}).catch(error => { }).catch(error => {
fetch_retry(apicall, serverURL) fetch_retry(apicall, flosight)
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)); .catch(error => reject(error));
}) })
@ -144,27 +124,43 @@
} }
//Get balance for the given Address //Get balance for the given Address
const getBalance = floBlockchainAPI.getBalance = function (addr) { const getBalance = floBlockchainAPI.getBalance = function (addr, after = null) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let api = `api/address/${addr}`; let api = `api/addr/${addr}/balance`, query_params = {};
promisedAPI(api, { details: "basic" }) if (after) {
.then(result => resolve(result["balance"])) if (typeof after === 'string' && /^[0-9a-z]{64}$/i.test(after))
.catch(error => reject(error)) query_params.after = after;
else return reject("Invalid 'after' parameter");
}
promisedAPI(api, query_params).then(result => {
if (typeof result === 'object' && result.lastItem) {
getBalance(addr, result.lastItem)
.then(r => resolve(util.toFixed(r + result.data)))
.catch(error => reject(error))
} else resolve(result);
}).catch(error => reject(error))
}); });
} }
function getScriptPubKey(address) {
var tx = bitjs.transaction();
tx.addoutput(address, 0);
let outputBuffer = tx.outputs.pop().script;
return Crypto.util.bytesToHex(outputBuffer)
}
const getUTXOs = address => new Promise((resolve, reject) => { const getUTXOs = address => new Promise((resolve, reject) => {
promisedAPI(`api/utxo/${address}`, { confirmed: true }).then(utxos => { promisedAPI(`api/addr/${address}/utxo`)
let scriptPubKey = getScriptPubKey(address); .then(utxo => resolve(utxo))
utxos.forEach(u => u.scriptPubKey = scriptPubKey); .catch(error => reject(error))
resolve(utxos); })
const getUnconfirmedSpent = address => new Promise((resolve, reject) => {
readTxs(address, { mempool: "only" }).then(result => {
let unconfirmedSpent = {};
for (let tx of result.items)
if (tx.confirmations == 0)
for (let vin of tx.vin)
if (vin.addr === address) {
if (Array.isArray(unconfirmedSpent[vin.txid]))
unconfirmedSpent[vin.txid].push(vin.vout);
else
unconfirmedSpent[vin.txid] = [vin.vout];
}
resolve(unconfirmedSpent);
}).catch(error => reject(error)) }).catch(error => reject(error))
}) })
@ -184,28 +180,32 @@
var fee = DEFAULT.fee; var fee = DEFAULT.fee;
if (balance < sendAmt + fee) if (balance < sendAmt + fee)
return reject("Insufficient FLO balance!"); return reject("Insufficient FLO balance!");
getUTXOs(senderAddr).then(utxos => { getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => {
//form/construct the transaction data getUTXOs(senderAddr).then(utxos => {
var trx = bitjs.transaction(); //form/construct the transaction data
var utxoAmt = 0.0; var trx = bitjs.transaction();
for (var i = utxos.length - 1; var utxoAmt = 0.0;
(i >= 0) && (utxoAmt < sendAmt + fee); i--) { for (var i = utxos.length - 1;
//use only utxos with confirmations (strict_utxo mode) (i >= 0) && (utxoAmt < sendAmt + fee); i--) {
if (utxos[i].confirmations || !strict_utxo) { //use only utxos with confirmations (strict_utxo mode)
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); if (utxos[i].confirmations || !strict_utxo) {
utxoAmt += utxos[i].amount; 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);
if (utxoAmt < sendAmt + fee) utxoAmt += utxos[i].amount;
reject("Insufficient FLO: Some UTXOs are unconfirmed"); };
else { }
trx.addoutput(receiverAddr, sendAmt); if (utxoAmt < sendAmt + fee)
var change = utxoAmt - sendAmt - fee; reject("Insufficient FLO: Some UTXOs are unconfirmed");
if (change > DEFAULT.minChangeAmt) else {
trx.addoutput(senderAddr, change); trx.addoutput(receiverAddr, sendAmt);
trx.addflodata(floData.replace(/\n/g, ' ')); var change = utxoAmt - sendAmt - fee;
resolve(trx); if (change > DEFAULT.minChangeAmt)
} trx.addoutput(senderAddr, change);
trx.addflodata(floData.replace(/\n/g, ' '));
resolve(trx);
}
}).catch(error => reject(error))
}).catch(error => reject(error)) }).catch(error => reject(error))
}).catch(error => reject(error)) }).catch(error => reject(error))
}) })
@ -293,30 +293,34 @@
if (balance < totalAmt + fee) if (balance < totalAmt + fee)
return reject("Insufficient FLO balance!"); return reject("Insufficient FLO balance!");
//get unconfirmed tx list //get unconfirmed tx list
getUTXOs(floID).then(utxos => { getUnconfirmedSpent(floID).then(unconfirmedSpent => {
var trx = bitjs.transaction(); getUTXOs(floID).then(utxos => {
var utxoAmt = 0.0; var trx = bitjs.transaction();
for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + fee); i--) { var utxoAmt = 0.0;
//use only utxos with confirmations (strict_utxo mode) for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + fee); i--) {
if (utxos[i].confirmations || !strict_utxo) { //use only utxos with confirmations (strict_utxo mode)
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); if (utxos[i].confirmations || !strict_utxo) {
utxoAmt += utxos[i].amount; 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);
if (utxoAmt < totalAmt + fee) utxoAmt += utxos[i].amount;
reject("Insufficient FLO: Some UTXOs are unconfirmed"); };
else { }
for (let i = 0; i < count; i++) if (utxoAmt < totalAmt + fee)
trx.addoutput(floID, splitAmt); reject("Insufficient FLO: Some UTXOs are unconfirmed");
var change = utxoAmt - totalAmt - fee; else {
if (change > DEFAULT.minChangeAmt) for (let i = 0; i < count; i++)
trx.addoutput(floID, change); trx.addoutput(floID, splitAmt);
trx.addflodata(floData.replace(/\n/g, ' ')); var change = utxoAmt - totalAmt - fee;
var signedTxHash = trx.sign(privKey, 1); if (change > DEFAULT.minChangeAmt)
broadcastTx(signedTxHash) trx.addoutput(floID, change);
.then(txid => resolve(txid)) trx.addflodata(floData.replace(/\n/g, ' '));
.catch(error => reject(error)) 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))
}).catch(error => reject(error)) }).catch(error => reject(error))
}) })
@ -547,29 +551,33 @@
var fee = DEFAULT.fee; var fee = DEFAULT.fee;
if (balance < sendAmt + fee) if (balance < sendAmt + fee)
return reject("Insufficient FLO balance!"); return reject("Insufficient FLO balance!");
getUTXOs(senderAddr).then(utxos => { getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => {
//form/construct the transaction data getUTXOs(senderAddr).then(utxos => {
var trx = bitjs.transaction(); //form/construct the transaction data
var utxoAmt = 0.0; var trx = bitjs.transaction();
for (var i = utxos.length - 1; var utxoAmt = 0.0;
(i >= 0) && (utxoAmt < sendAmt + fee); i--) { for (var i = utxos.length - 1;
//use only utxos with confirmations (strict_utxo mode) (i >= 0) && (utxoAmt < sendAmt + fee); i--) {
if (utxos[i].confirmations || !strict_utxo) { //use only utxos with confirmations (strict_utxo mode)
trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript if (utxos[i].confirmations || !strict_utxo) {
utxoAmt += utxos[i].amount; 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, redeemScript); //for multisig, script=redeemScript
if (utxoAmt < sendAmt + fee) utxoAmt += utxos[i].amount;
reject("Insufficient FLO: Some UTXOs are unconfirmed"); };
else { }
for (let i in receivers) if (utxoAmt < sendAmt + fee)
trx.addoutput(receivers[i], amounts[i]); reject("Insufficient FLO: Some UTXOs are unconfirmed");
var change = utxoAmt - sendAmt - fee; else {
if (change > DEFAULT.minChangeAmt) for (let i in receivers)
trx.addoutput(senderAddr, change); trx.addoutput(receivers[i], amounts[i]);
trx.addflodata(floData.replace(/\n/g, ' ')); var change = utxoAmt - sendAmt - fee;
resolve(trx); if (change > DEFAULT.minChangeAmt)
} trx.addoutput(senderAddr, change);
trx.addflodata(floData.replace(/\n/g, ' '));
resolve(trx);
}
}).catch(error => reject(error))
}).catch(error => reject(error)) }).catch(error => reject(error))
}).catch(error => reject(error)) }).catch(error => reject(error))
}); });
@ -765,11 +773,20 @@
const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) { const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (signedTxHash.length < 1) if (signedTxHash.length < 1)
return reject("Empty Transaction Data"); return reject("Empty Signature");
var url = serverList[curPos] + 'api/tx/send';
promisedAPI('/api/sendtx/' + signedTxHash) fetch(url, {
.then(response => resolve(response["result"])) method: "POST",
.catch(error => reject(error)) 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));
}) })
} }
@ -808,96 +825,61 @@
}) })
} }
//Read Txs of Address //Read Txs of Address between from and to
const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) { const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let api = `api/addrs/${addr}/txs`;
//API options //API options
let query_params = { details: 'txs' }; let query_params = {};
//page options if (!isUndefined(options.after) || !isUndefined(options.before)) {
if (!isUndefined(options.page) && Number.isInteger(options.page)) if (!isUndefined(options.after))
query_params.page = options.page; query_params.after = options.after;
if (!isUndefined(options.pageSize) && Number.isInteger(options.pageSize)) if (!isUndefined(options.before))
query_params.pageSize = options.pageSize; query_params.before = options.before;
//only confirmed tx } else {
if (options.confirmed) //Default is false in server, so only add confirmed filter if confirmed has a true value if (!isUndefined(options.from))
query_params.confirmed = true; query_params.from = options.from;
if (!isUndefined(options.to))
promisedAPI(`api/address/${addr}`, query_params).then(response => { query_params.to = options.to;
if (!Array.isArray(response.txs)) //set empty array if address doesnt have any tx }
response.txs = []; if (!isUndefined(options.latest))
resolve(response) query_params.latest = options.latest;
}).catch(error => reject(error)) if (!isUndefined(options.mempool))
query_params.mempool = options.mempool;
promisedAPI(api, query_params)
.then(response => resolve(response))
.catch(error => reject(error))
}); });
} }
//backward support (floBlockchainAPI < v2.5.6)
function readAllTxs_oldSupport(addr, options, ignoreOld = 0, cacheTotal = 0) {
return new Promise((resolve, reject) => {
readTxs(addr, options).then(response => {
cacheTotal += response.txs.length;
let n_remaining = response.txApperances - cacheTotal
if (n_remaining < ignoreOld) { // must remove tx that would have been fetch during prev call
let n_remove = ignoreOld - n_remaining;
resolve(response.txs.slice(0, -n_remove));
} else if (response.page == response.totalPages) //last page reached
resolve(response.txs);
else {
options.page = response.page + 1;
readAllTxs_oldSupport(addr, options, ignoreOld, cacheTotal)
.then(result => resolve(response.txs.concat(result)))
.catch(error => reject(error))
}
}).catch(error => reject(error))
})
}
function readAllTxs_new(addr, options, lastItem) {
return new Promise((resolve, reject) => {
readTxs(addr, options).then(response => {
let i = response.txs.findIndex(t => t.txid === lastItem);
if (i != -1) //found lastItem
resolve(response.txs.slice(0, i))
else if (response.page == response.totalPages) //last page reached
resolve(response.txs);
else {
options.page = response.page + 1;
readAllTxs_new(addr, options, lastItem)
.then(result => resolve(response.txs.concat(result)))
.catch(error => reject(error))
}
}).catch(error => reject(error))
})
}
//Read All Txs of Address (newest first) //Read All Txs of Address (newest first)
const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options = {}) { const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (Number.isInteger(options.ignoreOld)) //backward support: data from floBlockchainAPI < v2.5.6 readTxs(addr, options).then(response => {
readAllTxs_oldSupport(addr, options, options.ignoreOld).then(txs => { if (response.incomplete) {
let last_tx = txs.find(t => t.confirmations > 0); let next_options = Object.assign({}, options);
let new_lastItem = last_tx ? last_tx.txid : options.ignoreOld; if (options.latest)
next_options.before = response.initItem; //update before for chain query (latest 1st)
else
next_options.after = response.lastItem; //update after for chain query (oldest 1st)
readAllTxs(addr, next_options).then(r => {
r.items = r.items.concat(response.items); //latest tx are 1st in array
resolve(r);
}).catch(error => reject(error))
} else
resolve({ resolve({
lastItem: new_lastItem, lastItem: response.lastItem || options.after,
items: txs items: response.items
}) });
})
}).catch(error => reject(error)) });
else //New format for floBlockchainAPI >= v2.5.6
readAllTxs_new(addr, options, options.after).then(txs => {
let last_tx = txs.find(t => t.confirmations > 0);
let new_lastItem = last_tx ? last_tx.txid : options.after;
resolve({
lastItem: new_lastItem,
items: txs
})
}).catch(error => reject(error))
})
} }
/*Read flo Data from txs of given Address /*Read flo Data from txs of given Address
options can be used to filter data options can be used to filter data
after : query after the given txid after : query after the given txid
confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx) before : query before the given txid
mempool : query mempool tx or not (options same as readAllTx, DEFAULT=false: ignore unconfirmed tx)
ignoreOld : ignore old txs (deprecated: support for backward compatibility only, cannot be used with 'after') ignoreOld : ignore old txs (deprecated: support for backward compatibility only, cannot be used with 'after')
sentOnly : filters only sent data sentOnly : filters only sent data
receivedOnly: filters only received data receivedOnly: filters only received data
@ -912,15 +894,19 @@
//fetch options //fetch options
let query_options = {}; let query_options = {};
query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: ignore unconfirmed tx query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx
if (!isUndefined(options.after) || !isUndefined(options.before)) {
if (!isUndefined(options.after)) if (!isUndefined(options.ignoreOld)) //Backward support
return reject("Invalid options: cannot use after/before and ignoreOld in same query");
//use passed after and/or before options (options remain undefined if not passed)
query_options.after = options.after; query_options.after = options.after;
else if (!isUndefined(options.ignoreOld)) query_options.before = options.before;
query_options.ignoreOld = options.ignoreOld; }
readAllTxs(addr, query_options).then(response => { readAllTxs(addr, query_options).then(response => {
if (Number.isInteger(options.ignoreOld)) //backward support, cannot be used with options.after or options.before
response.items.splice(-options.ignoreOld); //negative to count from end of the array
if (typeof options.senders === "string") options.senders = [options.senders]; if (typeof options.senders === "string") options.senders = [options.senders];
if (typeof options.receivers === "string") options.receivers = [options.receivers]; if (typeof options.receivers === "string") options.receivers = [options.receivers];
@ -930,9 +916,9 @@
if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query
return false; return false;
if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr)) if (options.sentOnly && !tx.vin.some(vin => vin.addr === addr))
return false; return false;
else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0]))) else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addr)))
return false; return false;
if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr))
@ -958,7 +944,7 @@
txid: tx.txid, txid: tx.txid,
time: tx.time, time: tx.time,
blockheight: tx.blockheight, blockheight: tx.blockheight,
senders: new Set(tx.vin.map(v => v.addresses[0])), senders: new Set(tx.vin.map(v => v.addr)),
receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])), receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])),
data: tx.floData data: tx.floData
} : tx.floData); } : tx.floData);
@ -978,7 +964,8 @@
caseFn: (function) flodata => return bool value caseFn: (function) flodata => return bool value
options can be used to filter data options can be used to filter data
after : query after the given txid after : query after the given txid
confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx) before : query before the given txid
mempool : query mempool tx or not (options same as readAllTx, DEFAULT=false: ignore unconfirmed tx)
sentOnly : filters only sent data sentOnly : filters only sent data
receivedOnly: filters only received data receivedOnly: filters only received data
tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details) tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details)
@ -988,37 +975,23 @@
const getLatestData = floBlockchainAPI.getLatestData = function (addr, caseFn, options = {}) { const getLatestData = floBlockchainAPI.getLatestData = function (addr, caseFn, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
//fetch options //fetch options
let query_options = {}; let query_options = { latest: true };
query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: confirmed tx only query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx
if (!isUndefined(options.page)) if (!isUndefined(options.after)) query_options.after = options.after;
query_options.page = options.page; if (!isUndefined(options.before)) query_options.before = options.before;
//if (!isUndefined(options.after)) query_options.after = options.after;
let new_lastItem;
readTxs(addr, query_options).then(response => { readTxs(addr, query_options).then(response => {
//lastItem confirmed tx checked
if (!new_lastItem) {
let last_tx = response.items.find(t => t.confirmations > 0);
if (last_tx)
new_lastItem = last_tx.txid;
}
if (typeof options.senders === "string") options.senders = [options.senders]; if (typeof options.senders === "string") options.senders = [options.senders];
if (typeof options.receivers === "string") options.receivers = [options.receivers]; if (typeof options.receivers === "string") options.receivers = [options.receivers];
//check if `after` txid is in the response
let i_after = response.txs.findIndex(t => t.txid === options.after);
if (i_after != -1) //found lastItem, hence remove it and all txs before that
response.items.splice(i_after);
var item = response.items.find(tx => { var item = response.items.find(tx => {
if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query
return false; return false;
if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr)) if (options.sentOnly && !tx.vin.some(vin => vin.addr === addr))
return false; return false;
else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0]))) else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addr)))
return false; return false;
if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr))
@ -1031,34 +1004,35 @@
//if item found, then resolve the result //if item found, then resolve the result
if (!isUndefined(item)) { if (!isUndefined(item)) {
const result = { lastItem: new_lastItem || item.txid }; const result = { lastItem: response.lastItem };
if (options.tx) { if (options.tx) {
result.item = { result.item = {
txid: item.txid, txid: tx.txid,
time: item.time, time: tx.time,
blockheight: item.blockheight, blockheight: tx.blockheight,
senders: new Set(item.vin.map(v => v.addresses[0])), senders: new Set(tx.vin.map(v => v.addr)),
receivers: new Set(item.vout.map(v => v.scriptPubKey.addresses[0])), receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])),
data: item.floData data: tx.floData
} }
} else } else
result.data = item.floData; result.data = tx.floData;
return resolve(result); return resolve(result);
} }
if (response.page == response.totalPages || i_after != -1) //reached last page to check
resolve({ lastItem: new_lastItem || options.after }); //no data match the caseFn, resolve just the lastItem
//else if address needs chain query //else if address needs chain query
else { else if (response.incomplete) {
options.page = response.page + 1; let next_options = Object.assign({}, options);
getLatestData(addr, caseFn, options) options.before = response.initItem; //this fn uses latest option, so using before to chain query
.then(result => resolve(result)) getLatestData(addr, caseFn, next_options).then(r => {
.catch(error => reject(error)) r.lastItem = response.lastItem; //update last key as it should be the newest tx
resolve(r);
}).catch(error => reject(error))
} }
//no data match the caseFn, resolve just the lastItem
else
resolve({ lastItem: response.lastItem });
}).catch(error => reject(error)) }).catch(error => reject(error))
}) })
} }
})('object' === typeof module ? module.exports : window.floBlockchainAPI = {}); })('object' === typeof module ? module.exports : window.floBlockchainAPI = {});

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
(function (EXPORTS) { //floCloudAPI v2.4.5a (function (EXPORTS) { //floCloudAPI v2.4.3a
/* FLO Cloud operations to send/request application data*/ /* FLO Cloud operations to send/request application data*/
'use strict'; 'use strict';
const floCloudAPI = EXPORTS; const floCloudAPI = EXPORTS;
@ -195,24 +195,14 @@
floCloudAPI.init = function startCloudProcess(nodes) { floCloudAPI.init = function startCloudProcess(nodes) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
// accept only plain-ish objects
nodes = (nodes && typeof nodes === 'object' && !Array.isArray(nodes)) ? nodes : {};
supernodes = nodes; supernodes = nodes;
// reset liveness bookkeeping for the new set
if (_inactive && typeof _inactive.clear === 'function') _inactive.clear();
// (re)build the bucket with current IDs
kBucket = new K_Bucket(DEFAULT.SNStorageID, Object.keys(supernodes)); kBucket = new K_Bucket(DEFAULT.SNStorageID, Object.keys(supernodes));
resolve('Cloud init successful'); resolve('Cloud init successful');
} catch (error) { } catch (error) {
reject(error); reject(error);
} }
}); })
}; }
Object.defineProperty(floCloudAPI, 'kBucket', { Object.defineProperty(floCloudAPI, 'kBucket', {
get: () => kBucket get: () => kBucket
@ -237,27 +227,24 @@
function ws_activeConnect(snID, reverse = false) { function ws_activeConnect(snID, reverse = false) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Safe guard: uninitialized kBucket, empty list, or all inactive if (_inactive.size === kBucket.list.length)
if (!kBucket || !kBucket.list || !kBucket.list.length || _inactive.size === kBucket.list.length)
return reject('Cloud offline'); return reject('Cloud offline');
if (!(snID in supernodes))
if (!(snID in supernodes)) { snID = kBucket.closestNode(proxyID(snID));
var closest = kBucket.closestNode(proxyID(snID));
if (!closest) return reject('Cloud offline'); // no candidate to try
snID = closest;
}
ws_connect(snID) ws_connect(snID)
.then(node => resolve(node)) .then(node => resolve(node))
.catch(error => { .catch(error => {
var nxtNode = reverse ? kBucket.prevNode(snID) : kBucket.nextNode(snID); if (reverse)
if (!nxtNode || nxtNode === snID) return reject('Cloud offline'); // nothing else to try var nxtNode = kBucket.prevNode(snID);
ws_activeConnect(nxtNode, reverse).then(resolve).catch(reject); else
}); var nxtNode = kBucket.nextNode(snID);
}); ws_activeConnect(nxtNode, reverse)
.then(node => resolve(node))
.catch(error => reject(error))
})
})
} }
function fetch_API(snID, data) { function fetch_API(snID, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (_inactive.has(snID)) if (_inactive.has(snID))
@ -278,28 +265,25 @@
function fetch_ActiveAPI(snID, data, reverse = false) { function fetch_ActiveAPI(snID, data, reverse = false) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Safe guard: uninitialized kBucket, empty list, or all inactive if (_inactive.size === kBucket.list.length)
if (!kBucket || !kBucket.list || !kBucket.list.length || _inactive.size === kBucket.list.length)
return reject('Cloud offline'); return reject('Cloud offline');
if (!(snID in supernodes))
if (!(snID in supernodes)) { snID = kBucket.closestNode(proxyID(snID));
var closest = kBucket.closestNode(proxyID(snID));
if (!closest) return reject('Cloud offline'); // no candidate available
snID = closest;
}
fetch_API(snID, data) fetch_API(snID, data)
.then(resolve) .then(result => resolve(result))
.catch(error => { .catch(error => {
_inactive.add(snID); _inactive.add(snID)
var nxtNode = reverse ? kBucket.prevNode(snID) : kBucket.nextNode(snID); if (reverse)
if (!nxtNode || nxtNode === snID) return reject('Cloud offline'); // nothing else to try var nxtNode = kBucket.prevNode(snID);
fetch_ActiveAPI(nxtNode, data, reverse).then(resolve).catch(reject); 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") { function singleRequest(floID, data_obj, method = "POST") {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let data; let data;
@ -390,31 +374,13 @@
const util = floCloudAPI.util = {}; const util = floCloudAPI.util = {};
//Updating encoding/Decoding to modern standards
const encodeMessage = util.encodeMessage = function (message) { const encodeMessage = util.encodeMessage = function (message) {
const bytes = new TextEncoder().encode(JSON.stringify(message)); return btoa(unescape(encodeURIComponent(JSON.stringify(message))))
let bin = ""; }
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
};
const decodeMessage = util.decodeMessage = function (message) { const decodeMessage = util.decodeMessage = function (message) {
try { return JSON.parse(decodeURIComponent(escape(atob(message))))
// try modern decode first }
const bin = atob(message);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return JSON.parse(new TextDecoder().decode(bytes));
} catch (e1) {
try {
// fallback to legacy decode
return JSON.parse(decodeURIComponent(escape(atob(message))));
} catch (e2) {
// final fallback: return raw string to avoid hard crash
return message;
}
}
};
const filterKey = util.filterKey = function (type, options = {}) { const filterKey = util.filterKey = function (type, options = {}) {
return type + (options.comment ? ':' + options.comment : '') + return type + (options.comment ? ':' + options.comment : '') +
@ -502,59 +468,21 @@
function storeGeneral(fk, dataSet) { function storeGeneral(fk, dataSet) {
try { try {
if (!dataSet || typeof dataSet !== "object") return; console.log(dataSet)
if (typeof generalData[fk] !== "object")
// Ensure containers exist generalData[fk] = {}
if (typeof generalData[fk] !== "object" || generalData[fk] === null) for (let vc in dataSet) {
generalData[fk] = {}; generalData[fk][vc] = dataSet[vc];
if (typeof lastVC[fk] !== "number") if (dataSet[vc].log_time > lastVC[fk])
lastVC[fk] = 0; lastVC[fk] = dataSet[vc].log_time;
// Merge data and track latest log_time
let updated = false;
let newLast = lastVC[fk];
for (const vc in dataSet) {
const rec = dataSet[vc];
// Skip bad records
if (!rec || typeof rec !== "object") continue;
// Assign only if changed reference (cheap check)
if (generalData[fk][vc] !== rec) {
generalData[fk][vc] = rec;
updated = true;
}
if (typeof rec.log_time === "number" && rec.log_time > newLast) {
newLast = rec.log_time;
updated = true;
}
} }
compactIDB.writeData("lastVC", lastVC[fk], fk)
if (!updated) return; // nothing new, avoid IDB writes compactIDB.writeData("generalData", generalData[fk], fk)
lastVC[fk] = newLast;
// --- Debounce writes per fk to avoid IDB thrash ---
storeGeneral._pending = storeGeneral._pending || Object.create(null);
const pend = storeGeneral._pending;
clearTimeout(pend[fk]);
pend[fk] = setTimeout(() => {
try {
// Fire-and-forget; callers dont wait on these
compactIDB.writeData("lastVC", lastVC[fk], fk);
compactIDB.writeData("generalData", generalData[fk], fk);
} catch (e) {
console.error(e);
}
}, 50);
} catch (error) { } catch (error) {
console.error(error); console.error(error)
} }
} }
function objectifier(data) { function objectifier(data) {
if (!Array.isArray(data)) if (!Array.isArray(data))
data = [data]; data = [data];
@ -623,7 +551,7 @@
} }
//request any data from supernode cloud //request any data from supernode cloud
const _requestApplicationData = function (type, options = {}) { const requestApplicationData = floCloudAPI.requestApplicationData = function (type, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var request = { var request = {
receiverID: options.receiverID || DEFAULT.adminID, receiverID: options.receiverID || DEFAULT.adminID,
@ -654,17 +582,6 @@
}) })
} }
floCloudAPI.requestApplicationData = function (type, options = {}) {
return new Promise((resolve, reject) => {
let single_request_mode = !(options.callback instanceof Function);
_requestApplicationData(type, options).then(data => {
if (single_request_mode)
resolve(objectifier(data))
else resolve(data);
}).catch(error => reject(error))
})
}
/*(NEEDS UPDATE) /*(NEEDS UPDATE)
//delete data from supernode cloud (received only) //delete data from supernode cloud (received only)
floCloudAPI.deleteApplicationData = function(vectorClocks, options = {}) { floCloudAPI.deleteApplicationData = function(vectorClocks, options = {}) {
@ -692,39 +609,49 @@
}) })
} }
*/ */
//edit comment of data in supernode cloud (sender only) /*(NEEDS UPDATE)
floCloudAPI.editApplicationData = function (vectorClock, comment_edit, options = {}) { //edit comment of data in supernode cloud (mutable comments only)
floCloudAPI.editApplicationData = function(vectorClock, newComment, oldData, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
//request the data from cloud for resigning let p0
let req_options = Object.assign({}, options); if (!oldData) {
req_options.atVectorClock = vectorClock; options.atVectorClock = vectorClock;
_requestApplicationData(undefined, req_options).then(result => { options.callback = false;
if (!result.length) p0 = requestApplicationData(false, options)
return reject("Data not found"); } else
let data = result[0]; p0 = Promise.resolve({
if (data.senderID !== user.id) vectorClock: {
return reject("Only sender can edit comment"); ...oldData
data.comment = comment_edit; }
let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"] })
.map(d => data[d]).join("|"); p0.then(d => {
let re_sign = user.sign(hashcontent); if (d.senderID != user.id)
var request = { return reject("Invalid requestorID")
receiverID: options.receiverID || DEFAULT.adminID, else if (!d.comment.startsWith("EDIT:"))
return reject("Data immutable")
let data = {
requestorID: user.id, requestorID: user.id,
pubKey: user.public, receiverID: d.receiverID,
time: Date.now(), time: Date.now(),
vectorClock: vectorClock, application: d.application,
edit: comment_edit, edit: {
re_sign: re_sign vectorClock: vectorClock,
comment: newComment
}
} }
let request_hash = ["time", "vectorClock", "edit", "re_sign"].map(d => request[d]).join("|"); d.comment = data.edit.comment;
request.sign = user.sign(request_hash); let hashcontent = ["receiverID", "time", "application", "type", "message",
singleRequest(request.receiverID, request) "comment"
.then(result => resolve(result)) ]
.map(x => d[x]).join("|")
data.edit.sign = user.sign(hashcontent)
singleRequest(data.receiverID, data)
.then(result => resolve("Data comment updated"))
.catch(error => reject(error)) .catch(error => reject(error))
}).catch(error => reject(error)) })
}) })
} }
*/
//tag data in supernode cloud (subAdmin access only) //tag data in supernode cloud (subAdmin access only)
floCloudAPI.tagApplicationData = function (vectorClock, tag, options = {}) { floCloudAPI.tagApplicationData = function (vectorClock, tag, options = {}) {
@ -792,11 +719,11 @@
storeGeneral(fk, d); storeGeneral(fk, d);
options.callback(d, e) options.callback(d, e)
} }
_requestApplicationData(type, new_options) requestApplicationData(type, new_options)
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)) .catch(error => reject(error))
} else { } else {
_requestApplicationData(type, options).then(dataSet => { requestApplicationData(type, options).then(dataSet => {
storeGeneral(fk, objectifier(dataSet)) storeGeneral(fk, objectifier(dataSet))
resolve(dataSet) resolve(dataSet)
}).catch(error => reject(error)) }).catch(error => reject(error))
@ -821,7 +748,7 @@
} }
delete options.callback; delete options.callback;
} }
_requestApplicationData(objectName, options).then(dataSet => { requestApplicationData(objectName, options).then(dataSet => {
updateObject(objectName, objectifier(dataSet)); updateObject(objectName, objectifier(dataSet));
delete options.comment; delete options.comment;
options.lowerVectorClock = lastVC[objectName] + 1; options.lowerVectorClock = lastVC[objectName] + 1;
@ -829,11 +756,11 @@
if (callback) { if (callback) {
let new_options = Object.create(options); let new_options = Object.create(options);
new_options.callback = callback; new_options.callback = callback;
_requestApplicationData(objectName, new_options) requestApplicationData(objectName, new_options)
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)) .catch(error => reject(error))
} else { } else {
_requestApplicationData(objectName, options).then(dataSet => { requestApplicationData(objectName, options).then(dataSet => {
updateObject(objectName, objectifier(dataSet)) updateObject(objectName, objectifier(dataSet))
resolve(appObjects[objectName]) resolve(appObjects[objectName])
}).catch(error => reject(error)) }).catch(error => reject(error))
@ -884,67 +811,6 @@
}) })
} }
//upload file
floCloudAPI.uploadFile = function (fileBlob, type, options = {}) {
return new Promise((resolve, reject) => {
if (!(fileBlob instanceof File) && !(fileBlob instanceof Blob))
return reject("file must be instance of File/Blob");
fileBlob.arrayBuffer().then(arraybuf => {
let file_data = { type: fileBlob.type, name: fileBlob.name };
file_data.content = Crypto.util.bytesToBase64(new Uint8Array(arraybuf));
if (options.encrypt) {
let encryptionKey = options.encrypt === true ?
floGlobals.settings.encryptionKey : options.encrypt
file_data = floCrypto.encryptData(JSON.stringify(file_data), encryptionKey)
}
sendApplicationData(file_data, type, options)
.then(({ vectorClock, receiverID, type, application }) => resolve({ vectorClock, receiverID, type, application }))
.catch(error => reject(error))
}).catch(error => reject(error))
})
}
//download file
floCloudAPI.downloadFile = function (vectorClock, options = {}) {
return new Promise((resolve, reject) => {
options.atVectorClock = vectorClock;
_requestApplicationData(options.type, options).then(result => {
if (!result.length)
return reject("File not found");
result = result[0];
try {
let file_data = decodeMessage(result.message);
//file is encrypted: decryption required
if (file_data instanceof Object && "secret" in file_data) {
if (!options.decrypt)
return reject("Data is encrypted");
let decryptionKey = (options.decrypt === true) ? Crypto.AES.decrypt(user_private, aes_key) : options.decrypt;
if (!Array.isArray(decryptionKey))
decryptionKey = [decryptionKey];
let flag = false;
for (let key of decryptionKey) {
try {
let tmp = floCrypto.decryptData(file_data, key);
file_data = JSON.parse(tmp);
flag = true;
break;
} catch (error) { }
}
if (!flag)
return reject("Unable to decrypt file: Invalid private key");
}
//reconstruct the file
let arraybuf = new Uint8Array(Crypto.util.base64ToBytes(file_data.content))
result.file = new File([arraybuf], file_data.name, { type: file_data.type });
resolve(result)
} catch (error) {
console.error(error);
reject("Data is not a file");
}
}).catch(error => reject(error))
})
}
/* /*
Functions: Functions:
findDiff(original, updatedObj) returns an object with the added, deleted and updated differences findDiff(original, updatedObj) returns an object with the added, deleted and updated differences
@ -1186,4 +1052,4 @@
})(); })();
})('object' === typeof module ? module.exports : window.floCloudAPI = {}); })('object' === typeof module ? module.exports : window.floCloudAPI = {});

1
floCloudAPI.min.js vendored

File diff suppressed because one or more lines are too long

1
floCrypto.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
(function (EXPORTS) { //floDapps v2.4.1 (function (EXPORTS) { //floDapps v2.4.0
/* General functions for FLO Dapps*/ /* General functions for FLO Dapps*/
'use strict'; 'use strict';
const floDapps = EXPORTS; const floDapps = EXPORTS;
@ -144,7 +144,7 @@
} }
}); });
var subAdmins = [], trustedIDs = [], settings = {}; var subAdmins, trustedIDs, settings;
Object.defineProperties(floGlobals, { Object.defineProperties(floGlobals, {
subAdmins: { subAdmins: {
get: () => subAdmins get: () => subAdmins
@ -172,7 +172,12 @@
//general //general
lastTx: {}, lastTx: {},
//supernode (cloud list) //supernode (cloud list)
supernodes: {} supernodes: {
indexes: {
uri: null,
pubKey: null
}
}
} }
var obs_a = { var obs_a = {
//login credentials //login credentials
@ -249,154 +254,103 @@
const startUpFunctions = []; const startUpFunctions = [];
startUpFunctions.push(function readSupernodeListFromAPI() { startUpFunctions.push(function readSupernodeListFromAPI() {
return new Promise((resolve) => { return new Promise((resolve, reject) => {
if (!startUpOptions.cloud) if (!startUpOptions.cloud)
return resolve("No cloud for this app"); return resolve("No cloud for this app");
compactIDB.readData("lastTx", floCloudAPI.SNStorageID, DEFAULT.root).then(lastTx => {
const CLOUD_KEY = "floCloudAPI#" + floCloudAPI.SNStorageID; var query_options = { sentOnly: true, pattern: floCloudAPI.SNStorageName };
if (typeof lastTx == 'number') //lastTx is tx count (*backward support)
// Fallback: init from cached nodes (never reject)
const initFromCache = (tag) =>
compactIDB.readData("supernodes", CLOUD_KEY, DEFAULT.root)
.then(nodes => {
nodes = nodes || {};
return floCloudAPI.init(nodes)
.then(r => resolve(`${tag} (from cache)\n${r}`))
.catch(() => resolve(`${tag} (cache present, init skipped)`));
})
.catch(() => resolve(`${tag} (no cache)`));
compactIDB.readData("lastTx", CLOUD_KEY, DEFAULT.root).then(lastTx => {
const query_options = { sentOnly: true, pattern: floCloudAPI.SNStorageName };
if (typeof lastTx === 'number') // backward support (tx count)
query_options.ignoreOld = lastTx; query_options.ignoreOld = lastTx;
else if (typeof lastTx === 'string') // last txid else if (typeof lastTx == 'string') //lastTx is txid of last tx
query_options.after = lastTx; query_options.after = lastTx;
//fetch data from flosight
// Try online; if it fails, fall back to cache
floBlockchainAPI.readData(floCloudAPI.SNStorageID, query_options).then(result => { floBlockchainAPI.readData(floCloudAPI.SNStorageID, query_options).then(result => {
compactIDB.readData("supernodes", CLOUD_KEY, DEFAULT.root).then(nodes => { for (var i = result.data.length - 1; i >= 0; i--) {
nodes = nodes || {}; var content = JSON.parse(result.data[i])[floCloudAPI.SNStorageName];
for (let i = result.data.length - 1; i >= 0; i--) { for (let sn in content.removeNodes)
const content = JSON.parse(result.data[i])[floCloudAPI.SNStorageName]; compactIDB.removeData("supernodes", sn, DEFAULT.root);
if (!content || typeof content !== 'object') continue; for (let sn in content.newNodes)
if (content.removeNodes) compactIDB.writeData("supernodes", content.newNodes[sn], sn, DEFAULT.root);
for (let sn in content.removeNodes) delete nodes[sn]; for (let sn in content.updateNodes)
if (content.newNodes) compactIDB.readData("supernodes", sn, DEFAULT.root).then(r => {
for (let sn in content.newNodes) nodes[sn] = content.newNodes[sn]; r = r || {}
if (content.updateNodes) r.uri = content.updateNodes[sn];
for (let sn in content.updateNodes) compactIDB.writeData("supernodes", r, sn, DEFAULT.root);
if (sn in nodes) nodes[sn].uri = content.updateNodes[sn]; });
} }
Promise.all([ compactIDB.writeData("lastTx", result.lastItem, floCloudAPI.SNStorageID, DEFAULT.root);
compactIDB.writeData("lastTx", result.lastItem, CLOUD_KEY, DEFAULT.root), compactIDB.readAllData("supernodes", DEFAULT.root).then(nodes => {
compactIDB.writeData("supernodes", nodes, CLOUD_KEY, DEFAULT.root) floCloudAPI.init(nodes)
]).then(() => { .then(result => resolve("Loaded Supernode list\n" + result))
floCloudAPI.init(nodes) .catch(error => reject(error))
.then(r => resolve("Loaded Supernode list\n" + r)) })
.catch(() => resolve("Loaded Supernode list (init deferred)")); })
}).catch(() => resolve("Supernode list updated (persist partial)")); }).catch(error => reject(error))
}).catch(() => initFromCache("Supernode list read failed")); })
}).catch(() => initFromCache("Supernode network fetch failed"));
}).catch(() => initFromCache("Supernode lastTx read failed"));
});
}); });
startUpFunctions.push(function readAppConfigFromAPI() { startUpFunctions.push(function readAppConfigFromAPI() {
return new Promise((resolve) => { return new Promise((resolve, reject) => {
if (!startUpOptions.app_config) if (!startUpOptions.app_config)
return resolve("No configs for this app"); return resolve("No configs for this app");
compactIDB.readData("lastTx", `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root).then(lastTx => {
// small helper: load cached directives into memory and resolve var query_options = { sentOnly: true, pattern: DEFAULT.application };
const loadFromIDB = (msg) => Promise.all([ if (typeof lastTx == 'number') //lastTx is tx count (*backward support)
compactIDB.readAllData("subAdmins"), query_options.ignoreOld = lastTx;
compactIDB.readAllData("trustedIDs"), else if (typeof lastTx == 'string') //lastTx is txid of last tx
compactIDB.readAllData("settings") query_options.after = lastTx;
]).then(([sub, trust, set]) => { //fetch data from flosight
subAdmins = Object.keys(sub || {}); // arrays of IDs
trustedIDs = Object.keys(trust || {});
settings = set || {};
resolve(msg);
}).catch(() => {
// safe defaults if cache missing
subAdmins = []; trustedIDs = []; settings = {};
resolve(msg + " (no local cache)");
});
// If cloud is disabled, use cached config and move on
if (!startUpOptions.cloud)
return loadFromIDB("Read app configuration from local cache (offline)");
const lastKey = `${DEFAULT.application}|${DEFAULT.adminID}`;
// Try to read lastTx; on failure, just use cache (dont block startup)
compactIDB.readData("lastTx", lastKey, DEFAULT.root).then(lastTx => {
const query_options = { sentOnly: true, pattern: DEFAULT.application };
if (typeof lastTx === 'number') query_options.ignoreOld = lastTx;
else if (typeof lastTx === 'string') query_options.after = lastTx;
// Fetch deltas from chain; on failure, fall back to cache
floBlockchainAPI.readData(DEFAULT.adminID, query_options).then(result => { floBlockchainAPI.readData(DEFAULT.adminID, query_options).then(result => {
for (let i = result.data.length - 1; i >= 0; i--) { for (var i = result.data.length - 1; i >= 0; i--) {
const content = JSON.parse(result.data[i])[DEFAULT.application]; var content = JSON.parse(result.data[i])[DEFAULT.application];
if (!content || typeof content !== "object") continue; if (!content || typeof content !== "object")
continue;
if (Array.isArray(content.removeSubAdmin)) if (Array.isArray(content.removeSubAdmin))
for (let j = 0; j < content.removeSubAdmin.length; j++) for (var j = 0; j < content.removeSubAdmin.length; j++)
compactIDB.removeData("subAdmins", content.removeSubAdmin[j]); compactIDB.removeData("subAdmins", content.removeSubAdmin[j]);
if (Array.isArray(content.addSubAdmin)) if (Array.isArray(content.addSubAdmin))
for (let k = 0; k < content.addSubAdmin.length; k++) for (var k = 0; k < content.addSubAdmin.length; k++)
compactIDB.writeData("subAdmins", true, content.addSubAdmin[k]); compactIDB.writeData("subAdmins", true, content.addSubAdmin[k]);
if (Array.isArray(content.removeTrustedID)) if (Array.isArray(content.removeTrustedID))
for (let j = 0; j < content.removeTrustedID.length; j++) for (var j = 0; j < content.removeTrustedID.length; j++)
compactIDB.removeData("trustedIDs", content.removeTrustedID[j]); compactIDB.removeData("trustedIDs", content.removeTrustedID[j]);
if (Array.isArray(content.addTrustedID)) if (Array.isArray(content.addTrustedID))
for (let k = 0; k < content.addTrustedID.length; k++) for (var k = 0; k < content.addTrustedID.length; k++)
compactIDB.writeData("trustedIDs", true, content.addTrustedID[k]); compactIDB.writeData("trustedIDs", true, content.addTrustedID[k]);
if (content.settings) if (content.settings)
for (let l in content.settings) for (let l in content.settings)
compactIDB.writeData("settings", content.settings[l], l); compactIDB.writeData("settings", content.settings[l], l)
} }
compactIDB.writeData("lastTx", result.lastItem, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root);
// persist last item marker (best effort) compactIDB.readAllData("subAdmins").then(result => {
compactIDB.writeData("lastTx", result.lastItem, lastKey, DEFAULT.root).catch(() => {}); subAdmins = Object.keys(result);
compactIDB.readAllData("trustedIDs").then(result => {
// load fresh values from IDB into memory and finish trustedIDs = Object.keys(result);
loadFromIDB("Read app configuration from blockchain"); compactIDB.readAllData("settings").then(result => {
}).catch(() => { settings = result;
// network failed → boot from cache resolve("Read app configuration from blockchain");
loadFromIDB("Read app configuration from local cache (network fail)"); })
}); })
}).catch(() => { })
// couldn't read lastTx → still boot from cache })
loadFromIDB("Read app configuration from local cache (no lastTx)"); }).catch(error => reject(error))
}); })
});
}); });
startUpFunctions.push(function loadDataFromAppIDB() { startUpFunctions.push(function loadDataFromAppIDB() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const loadData = ["appObjects", "generalData", "lastVC"]; if (!startUpOptions.cloud)
return resolve("No cloud for this app");
// If cloud is disabled AND no IDB stores are expected, skip early var loadData = ["appObjects", "generalData", "lastVC"]
if (!startUpOptions.cloud && (!initIndexedDB.appObs || Object.keys(initIndexedDB.appObs).length === 0)) var promises = []
return resolve("No cloud and no local data to load"); for (var i = 0; i < loadData.length; i++)
promises[i] = compactIDB.readAllData(loadData[i])
// Otherwise, read from IDB Promise.all(promises).then(results => {
Promise.all(loadData.map(item => compactIDB.readAllData(item))) for (var i = 0; i < loadData.length; i++)
.then(results => { floGlobals[loadData[i]] = results[i]
for (let i = 0; i < loadData.length; i++) resolve("Loaded Data from app IDB")
floGlobals[loadData[i]] = results[i]; }).catch(error => reject(error))
resolve("Loaded Data from app IDB"); })
})
.catch(error => reject(error));
});
}); });
var keyInput = type => new Promise((resolve, reject) => { var keyInput = type => new Promise((resolve, reject) => {
@ -888,4 +842,4 @@
.catch(error => reject(error)) .catch(error => reject(error))
}).catch(error => reject(error)) }).catch(error => reject(error))
}); });
})('object' === typeof module ? module.exports : window.floDapps = {}); })('object' === typeof module ? module.exports : window.floDapps = {});

1
floDapps.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
floTokenAPI.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -26,14 +26,14 @@
})(); })();
</script> </script>
<script src="lib.js" type="text/javascript"></script> <script src="lib.js"></script>
<script src="floCrypto.js" type="text/javascript"></script> <script src="floCrypto.js"></script>
<script src="btcOperator.js" type="text/javascript"></script> <script src="btcOperator.js"></script>
<script src="floBlockchainAPI.js" type="text/javascript"></script> <script src="floBlockchainAPI.js"></script>
<script src="floTokenAPI.js" type="text/javascript"></script> <script src="floTokenAPI.js"></script>
<script src="compactIDB.js" type="text/javascript"></script> <script src="compactIDB.js"></script>
<script src="floCloudAPI.js" type="text/javascript"></script> <script src="floCloudAPI.js"></script>
<script src="floDapps.js" type="text/javascript"></script> <script src="floDapps.js"></script>
<script id="onLoadStartUp"> <script id="onLoadStartUp">
function onLoadStartUp() { function onLoadStartUp() {
if (window.quick) return; if (window.quick) return;

57
lib.min.js vendored

File diff suppressed because one or more lines are too long