diff --git a/compactIDB.js b/compactIDB.js index 624db0a..ba843ec 100644 --- a/compactIDB.js +++ b/compactIDB.js @@ -1,4 +1,4 @@ -(function(EXPORTS) { //compactIDB v2.1.0 +(function (EXPORTS) { //compactIDB v2.1.2 /* Compact IndexedDB operations */ 'use strict'; const compactIDB = EXPORTS; @@ -59,7 +59,7 @@ }) } - compactIDB.initDB = function(dbName, objectStores = {}) { + compactIDB.initDB = function (dbName, objectStores = {}) { return new Promise((resolve, reject) => { if (!(objectStores instanceof Object)) return reject('ObjectStores must be an object or array') @@ -87,14 +87,14 @@ resolve("Initiated IndexedDB"); else upgradeDB(dbName, a_obs, d_obs) - .then(result => resolve(result)) - .catch(error => reject(error)) + .then(result => resolve(result)) + .catch(error => reject(error)) db.close(); } }); } - const openDB = compactIDB.openDB = function(dbName = defaultDB) { + 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"); @@ -106,7 +106,7 @@ }); } - const deleteDB = compactIDB.deleteDB = function(dbName = defaultDB) { + const deleteDB = compactIDB.deleteDB = function (dbName = defaultDB) { return new Promise((resolve, reject) => { var deleteReq = indexedDB.deleteDatabase(dbName);; deleteReq.onerror = (event) => reject("Error deleting database!"); @@ -114,7 +114,7 @@ }); } - compactIDB.writeData = function(obsName, data, key = false, dbName = defaultDB) { + 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); @@ -128,7 +128,7 @@ }); } - compactIDB.addData = function(obsName, data, key = false, dbName = defaultDB) { + 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); @@ -142,7 +142,7 @@ }); } - compactIDB.removeData = function(obsName, key, dbName = defaultDB) { + compactIDB.removeData = function (obsName, key, dbName = defaultDB) { return new Promise((resolve, reject) => { openDB(dbName).then(db => { var obs = db.transaction(obsName, "readwrite").objectStore(obsName); @@ -156,7 +156,7 @@ }); } - compactIDB.clearData = function(obsName, dbName = defaultDB) { + compactIDB.clearData = function (obsName, dbName = defaultDB) { return new Promise((resolve, reject) => { openDB(dbName).then(db => { var obs = db.transaction(obsName, "readwrite").objectStore(obsName); @@ -168,7 +168,7 @@ }); } - compactIDB.readData = function(obsName, key, dbName = defaultDB) { + compactIDB.readData = function (obsName, key, dbName = defaultDB) { return new Promise((resolve, reject) => { openDB(dbName).then(db => { var obs = db.transaction(obsName, "readonly").objectStore(obsName); @@ -182,7 +182,7 @@ }); } - compactIDB.readAllData = function(obsName, dbName = defaultDB) { + compactIDB.readAllData = function (obsName, dbName = defaultDB) { return new Promise((resolve, reject) => { openDB(dbName).then(db => { var obs = db.transaction(obsName, "readonly").objectStore(obsName); @@ -223,13 +223,12 @@ }) }*/ - compactIDB.searchData = function(obsName, options = {}, dbName = defaultDB) { + 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.patternEval = options.patternEval || ((k, v) => true); options.limit = options.limit || false; + options.reverse = options.reverse || false; options.lastOnly = options.lastOnly || false return new Promise((resolve, reject) => { openDB(dbName).then(db => { @@ -237,17 +236,16 @@ var filteredResult = {} let curReq = obs.openCursor( options.upperKey ? IDBKeyRange.bound(options.lowerKey, options.upperKey) : IDBKeyRange.lowerBound(options.lowerKey), - options.lastOnly ? "prev" : "next"); + options.lastOnly || options.reverse ? "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(); + if (!cursor || (options.limit && options.limit <= Object.keys(filteredResult).length)) + return resolve(filteredResult); //reached end of key list or limit reached + else if (options.patternEval(cursor.primaryKey, cursor.value)) { + filteredResult[cursor.primaryKey] = cursor.value; + options.lastOnly ? resolve(filteredResult) : cursor.continue(); } else - resolve(filteredResult); + cursor.continue(); } curReq.onerror = (evt) => reject(`Search unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`); db.close(); diff --git a/floBlockchainAPI.js b/floBlockchainAPI.js index 4b13f39..460775b 100644 --- a/floBlockchainAPI.js +++ b/floBlockchainAPI.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floBlockchainAPI v2.5.1 +(function (EXPORTS) { //floBlockchainAPI v2.5.6a /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/ 'use strict'; const floBlockchainAPI = EXPORTS; @@ -6,8 +6,8 @@ const DEFAULT = { blockchain: floGlobals.blockchain, apiURL: { - FLO: ['https://flosight.duckdns.org/', 'https://flosight.ranchimall.net/'], - FLO_TEST: ['https://testnet-flosight.duckdns.org', 'https://testnet.flocha.in/'] + FLO: ['https://flosight.ranchimall.net/'], + FLO_TEST: ['https://flosight-testnet.ranchimall.net/'] }, sendAmt: 0.0003, fee: 0.0002, @@ -16,6 +16,7 @@ }; const SATOSHI_IN_BTC = 1e8; + const isUndefined = val => typeof val === 'undefined'; const util = floBlockchainAPI.util = {}; @@ -111,9 +112,11 @@ }); //Promised function to get data from API - const promisedAPI = floBlockchainAPI.promisedAPI = floBlockchainAPI.fetch = function (apicall) { + const promisedAPI = floBlockchainAPI.promisedAPI = floBlockchainAPI.fetch = function (apicall, query_params = undefined) { return new Promise((resolve, reject) => { - //console.log(apicall); + if (!isUndefined(query_params)) + apicall += '?' + new URLSearchParams(JSON.parse(JSON.stringify(query_params))).toString(); + //console.debug(apicall); fetch_api(apicall) .then(result => resolve(result)) .catch(error => reject(error)); @@ -123,13 +126,13 @@ //Get balance for the given Address const getBalance = floBlockchainAPI.getBalance = function (addr, after = null) { return new Promise((resolve, reject) => { - let api = `api/addr/${addr}/balance`; + let api = `api/addr/${addr}/balance`, query_params = {}; if (after) { if (typeof after === 'string' && /^[0-9a-z]{64}$/i.test(after)) - api += '?after=' + after; + query_params.after = after; else return reject("Invalid 'after' parameter"); } - promisedAPI(api).then(result => { + promisedAPI(api, query_params).then(result => { if (typeof result === 'object' && result.lastItem) { getBalance(addr, result.lastItem) .then(r => resolve(util.toFixed(r + result.data))) @@ -273,6 +276,56 @@ }) } + //split sufficient UTXOs of a given floID for a parallel sending + floBlockchainAPI.splitUTXOs = function (floID, privKey, count, floData = '') { + return new Promise((resolve, reject) => { + if (!floCrypto.validateFloID(floID, true)) + 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 fee = DEFAULT.fee; + var splitAmt = DEFAULT.sendAmt + fee; + var totalAmt = splitAmt * count; + getBalance(floID).then(balance => { + var fee = DEFAULT.fee; + if (balance < totalAmt + fee) + return reject("Insufficient FLO balance!"); + //get unconfirmed tx list + getUnconfirmedSpent(floID).then(unconfirmedSpent => { + getUTXOs(floID).then(utxos => { + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + 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 < totalAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + for (let i = 0; i < count; i++) + trx.addoutput(floID, splitAmt); + var change = utxoAmt - totalAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(floID, 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 from (and/or) to multiple floID * @param {Array} senderPrivKeys List of sender private-keys * @param {string} data FLO data of the txn @@ -737,7 +790,7 @@ }) } - floBlockchainAPI.getTx = function (txid) { + const getTx = floBlockchainAPI.getTx = function (txid) { return new Promise((resolve, reject) => { promisedAPI(`api/tx/${txid}`) .then(response => resolve(response)) @@ -745,46 +798,77 @@ }) } - const isUndefined = val => typeof val === 'undefined'; + /**Wait for the given txid to get confirmation in blockchain + * @param {string} txid of the transaction to wait for + * @param {int} max_retry: maximum number of retries before exiting wait. negative number = Infinite retries (DEFAULT: -1 ie, infinite retries) + * @param {Array} retry_timeout: time (seconds) between retries (DEFAULT: 20 seconds) + * @return {Promise} resolves when tx gets confirmation + */ + const waitForConfirmation = floBlockchainAPI.waitForConfirmation = function (txid, max_retry = -1, retry_timeout = 20) { + return new Promise((resolve, reject) => { + setTimeout(function () { + getTx(txid).then(tx => { + if (!tx) + return reject("Transaction not found"); + if (tx.confirmations) + return resolve(tx); + else if (max_retry === 0) //no more retries + return reject("Waiting timeout: tx still not confirmed"); + else { + max_retry = max_retry < 0 ? -1 : max_retry - 1; //decrease retry count (unless infinite retries) + waitForConfirmation(txid, max_retry, retry_timeout) + .then(result => resolve(result)) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }, retry_timeout * 1000) + }) + } //Read Txs of Address between from and to const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) { return new Promise((resolve, reject) => { let api = `api/addrs/${addr}/txs`; //API options - let api_options = []; - if (!isUndefined(options.after)) - api_options.push(`after=${options.after}`); - else { + let query_params = {}; + if (!isUndefined(options.after) || !isUndefined(options.before)) { + if (!isUndefined(options.after)) + query_params.after = options.after; + if (!isUndefined(options.before)) + query_params.before = options.before; + } else { if (!isUndefined(options.from)) - api_options.push(`from=${options.from}`); + query_params.from = options.from; if (!isUndefined(options.to)) - api_options.push(`to=${options.to}`); + query_params.to = options.to; } + if (!isUndefined(options.latest)) + query_params.latest = latest; if (!isUndefined(options.mempool)) - api_options.push(`mempool=${options.mempool}`) - if (api_options.length) - api += "?" + api_options.join('&'); - promisedAPI(api) + query_params.mempool = options.mempool; + promisedAPI(api, query_params) .then(response => resolve(response)) .catch(error => reject(error)) }); } //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) => { readTxs(addr, options).then(response => { if (response.incomplete) { let next_options = Object.assign({}, options); - next_options.after = response.lastItem; + 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({ - lastKey: response.lastItem || options.after, + lastItem: response.lastItem || options.after, items: response.items }); }) @@ -794,6 +878,7 @@ /*Read flo Data from txs of given Address options can be used to filter data after : query after the given txid + 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') sentOnly : filters only sent data @@ -808,17 +893,18 @@ return new Promise((resolve, reject) => { //fetch options - let fetch_options = {}; - fetch_options.mempool = isUndefined(options.mempool) ? 'false' : options.mempool; //DEFAULT: ignore unconfirmed tx - if (!isUndefined(options.after)) { + let query_options = {}; + query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx + if (!isUndefined(options.after) || !isUndefined(options.before)) { if (!isUndefined(options.ignoreOld)) //Backward support - return reject("Invalid options: cannot use after and ignoreOld in same query"); - else - fetch_options.after = options.after; + 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.before = options.before; } - readAllTxs(addr, fetch_options).then(response => { + readAllTxs(addr, query_options).then(response => { - if (Number.isInteger(options.ignoreOld)) //backward support, cannot be used with options.after + 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]; @@ -863,7 +949,7 @@ data: tx.floData } : tx.floData); - const result = { lastKey: response.lastKey }; + const result = { lastItem: response.lastItem }; if (options.tx) result.items = filteredData; else @@ -874,4 +960,79 @@ }) } + /*Get the latest flo Data that match the caseFn from txs of given Address + caseFn: (function) flodata => return bool value + options can be used to filter data + after : query after the given txid + 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 + receivedOnly: filters only received data + 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 + */ + const getLatestData = floBlockchainAPI.getLatestData = function (addr, caseFn, options = {}) { + return new Promise((resolve, reject) => { + //fetch options + let query_options = { latest: true }; + query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx + if (!isUndefined(options.after)) query_options.after = options.after; + if (!isUndefined(options.before)) query_options.before = options.before; + + readTxs(addr, query_options).then(response => { + + if (typeof options.senders === "string") options.senders = [options.senders]; + if (typeof options.receivers === "string") options.receivers = [options.receivers]; + + var item = response.items.find(tx => { + if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query + return false; + + if (options.sentOnly && !tx.vin.some(vin => vin.addr === addr)) + return false; + else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addr))) + return false; + + if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) + return false; + else if (Array.isArray(options.receivers) && !tx.vout.some(vout => options.receivers.includes(vout.scriptPubKey.addresses[0]))) + return false; + + return caseFn(tx.floData) ? true : false; //return only bool for find fn + }); + + //if item found, then resolve the result + if (!isUndefined(item)) { + const result = { lastItem: response.lastItem }; + if (options.tx) { + result.item = { + txid: tx.txid, + time: tx.time, + blockheight: tx.blockheight, + senders: new Set(tx.vin.map(v => v.addr)), + receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])), + data: tx.floData + } + } else + result.data = tx.floData; + return resolve(result); + } + //else if address needs chain query + else if (response.incomplete) { + let next_options = Object.assign({}, options); + options.before = response.initItem; //this fn uses latest option, so using before to chain query + getLatestData(addr, caseFn, next_options).then(r => { + 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)) + }) + } + })('object' === typeof module ? module.exports : window.floBlockchainAPI = {}); \ No newline at end of file diff --git a/floCloudAPI.js b/floCloudAPI.js index 921fa69..c2d2c3f 100644 --- a/floCloudAPI.js +++ b/floCloudAPI.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floCloudAPI v2.4.3 +(function (EXPORTS) { //floCloudAPI v2.4.3a /* FLO Cloud operations to send/request application data*/ 'use strict'; const floCloudAPI = EXPORTS; @@ -8,6 +8,7 @@ SNStorageID: floGlobals.SNStorageID || "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk", adminID: floGlobals.adminID, application: floGlobals.application, + SNStorageName: "SuperNodeStorage", callback: (d, e) => console.debug(d, e) }; @@ -58,6 +59,9 @@ SNStorageID: { get: () => DEFAULT.SNStorageID }, + SNStorageName: { + get: () => DEFAULT.SNStorageName + }, adminID: { get: () => DEFAULT.adminID }, diff --git a/floDapps.js b/floDapps.js index 4668e4e..4d525a2 100644 --- a/floDapps.js +++ b/floDapps.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floDapps v2.3.4 +(function (EXPORTS) { //floDapps v2.4.0 /* General functions for FLO Dapps*/ 'use strict'; const floDapps = EXPORTS; @@ -144,11 +144,14 @@ } }); - var subAdmins, settings + var subAdmins, trustedIDs, settings; Object.defineProperties(floGlobals, { subAdmins: { get: () => subAdmins }, + trustedIDs: { + get: () => trustedIDs + }, settings: { get: () => settings }, @@ -255,13 +258,15 @@ if (!startUpOptions.cloud) return resolve("No cloud for this app"); compactIDB.readData("lastTx", floCloudAPI.SNStorageID, DEFAULT.root).then(lastTx => { - floBlockchainAPI.readData(floCloudAPI.SNStorageID, { - ignoreOld: lastTx, - sentOnly: true, - pattern: "SuperNodeStorage" - }).then(result => { + var query_options = { sentOnly: true, pattern: floCloudAPI.SNStorageName }; + if (typeof lastTx == 'number') //lastTx is tx count (*backward support) + query_options.ignoreOld = lastTx; + else if (typeof lastTx == 'string') //lastTx is txid of last tx + query_options.after = lastTx; + //fetch data from flosight + floBlockchainAPI.readData(floCloudAPI.SNStorageID, query_options).then(result => { for (var i = result.data.length - 1; i >= 0; i--) { - var content = JSON.parse(result.data[i]).SuperNodeStorage; + var content = JSON.parse(result.data[i])[floCloudAPI.SNStorageName]; for (let sn in content.removeNodes) compactIDB.removeData("supernodes", sn, DEFAULT.root); for (let sn in content.newNodes) @@ -273,7 +278,7 @@ compactIDB.writeData("supernodes", r, sn, DEFAULT.root); }); } - compactIDB.writeData("lastTx", result.totalTxs, floCloudAPI.SNStorageID, DEFAULT.root); + compactIDB.writeData("lastTx", result.lastItem, floCloudAPI.SNStorageID, DEFAULT.root); compactIDB.readAllData("supernodes", DEFAULT.root).then(nodes => { floCloudAPI.init(nodes) .then(result => resolve("Loaded Supernode list\n" + result)) @@ -289,11 +294,13 @@ if (!startUpOptions.app_config) return resolve("No configs for this app"); compactIDB.readData("lastTx", `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root).then(lastTx => { - floBlockchainAPI.readData(DEFAULT.adminID, { - ignoreOld: lastTx, - sentOnly: true, - pattern: DEFAULT.application - }).then(result => { + var query_options = { sentOnly: true, pattern: DEFAULT.application }; + if (typeof lastTx == 'number') //lastTx is tx count (*backward support) + query_options.ignoreOld = lastTx; + else if (typeof lastTx == 'string') //lastTx is txid of last tx + query_options.after = lastTx; + //fetch data from flosight + floBlockchainAPI.readData(DEFAULT.adminID, query_options).then(result => { for (var i = result.data.length - 1; i >= 0; i--) { var content = JSON.parse(result.data[i])[DEFAULT.application]; if (!content || typeof content !== "object") @@ -314,12 +321,15 @@ for (let l in content.settings) compactIDB.writeData("settings", content.settings[l], l) } - compactIDB.writeData("lastTx", result.totalTxs, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root); + compactIDB.writeData("lastTx", result.lastItem, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root); compactIDB.readAllData("subAdmins").then(result => { subAdmins = Object.keys(result); - compactIDB.readAllData("settings").then(result => { - settings = result; - resolve("Read app configuration from blockchain"); + compactIDB.readAllData("trustedIDs").then(result => { + trustedIDs = Object.keys(result); + compactIDB.readAllData("settings").then(result => { + settings = result; + resolve("Read app configuration from blockchain"); + }) }) }) }) diff --git a/floTokenAPI.js b/floTokenAPI.js index efbd2ae..2456b88 100644 --- a/floTokenAPI.js +++ b/floTokenAPI.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floTokenAPI v1.0.3c +(function (EXPORTS) { //floTokenAPI v1.0.4a /* Token Operator to send/receive tokens via blockchain using API calls*/ 'use strict'; const tokenAPI = EXPORTS; @@ -77,6 +77,70 @@ }); } + function sendTokens_raw(privKey, receiverID, token, amount, utxo, vout, scriptPubKey) { + return new Promise((resolve, reject) => { + var trx = bitjs.transaction(); + trx.addinput(utxo, vout, scriptPubKey) + trx.addoutput(receiverID, floBlockchainAPI.sendAmt); + trx.addflodata(`send ${amount} ${token}#`); + var signedTxHash = trx.sign(privKey, 1); + floBlockchainAPI.broadcastTx(signedTxHash) + .then(txid => resolve([receiverID, txid])) + .catch(error => reject([receiverID, error])) + }) + } + + //bulk transfer tokens + tokenAPI.bulkTransferTokens = function (sender, privKey, token, receivers) { + return new Promise((resolve, reject) => { + if (typeof receivers !== 'object') + return reject("receivers must be object in format {receiver1: amount1, receiver2:amount2...}") + + let receiver_list = Object.keys(receivers), amount_list = Object.values(receivers); + let invalidReceivers = receiver_list.filter(id => !floCrypto.validateFloID(id)); + let invalidAmount = amount_list.filter(val => typeof val !== 'number' || val <= 0); + if (invalidReceivers.length) + return reject(`Invalid receivers: ${invalidReceivers}`); + else if (invalidAmount.length) + return reject(`Invalid amounts: ${invalidAmount}`); + + if (receiver_list.length == 0) + return reject("Receivers cannot be empty"); + + if (receiver_list.length == 1) { + let receiver = receiver_list[0], amount = amount_list[0]; + floTokenAPI.sendToken(privKey, amount, receiver, "", token) + .then(txid => resolve({ success: { [receiver]: txid } })) + .catch(error => reject(error)) + } else { + //check for token balance + floTokenAPI.getBalance(sender, token).then(token_balance => { + let total_token_amout = amount_list.reduce((a, e) => a + e, 0); + if (total_token_amout > token_balance) + return reject(`Insufficient ${token}# balance`); + + //split utxos + floBlockchainAPI.splitUTXOs(sender, privKey, receiver_list.length).then(split_txid => { + //wait for the split utxo to get confirmation + floBlockchainAPI.waitForConfirmation(split_txid).then(split_tx => { + //send tokens using the split-utxo + var scriptPubKey = split_tx.vout[0].scriptPubKey.hex; + let promises = []; + for (let i in receiver_list) + promises.push(sendTokens_raw(privKey, receiver_list[i], token, amount_list[i], split_txid, i, scriptPubKey)); + Promise.allSettled(promises).then(results => { + let success = Object.fromEntries(results.filter(r => r.status == 'fulfilled').map(r => r.value)); + let failed = Object.fromEntries(results.filter(r => r.status == 'rejected').map(r => r.reason)); + resolve({ success, failed }); + }) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + } + + }) + } + tokenAPI.getAllTxs = function (floID, token = DEFAULT.currency) { return new Promise((resolve, reject) => { fetch_api(`api/v1.0/getFloAddressTransactions?token=${token}&floAddress=${floID}`)