diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..168b657 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.tmp* \ No newline at end of file diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..d3f4c4c --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Sai Raj + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/compactIDB.js b/scripts/compactIDB.js index 624db0a..ba843ec 100644 --- a/scripts/compactIDB.js +++ b/scripts/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/scripts/flo-webwallet.js b/scripts/flo-webwallet.js index e83b5db..70a46ce 100644 --- a/scripts/flo-webwallet.js +++ b/scripts/flo-webwallet.js @@ -52,46 +52,31 @@ floWebWallet.syncTransactions = function (addr) { return new Promise((resolve, reject) => { compactIDB.readData('lastSync', addr).then(lastSync => { - lastSync = lastSync | 0; - getNewTxs(addr, lastSync).then(APIresult => { + const old_support = Number.isInteger(lastSync); //backward support + let fetch_options = {}; + if (typeof lastSync == 'string' && /^[a-f0-9]{64}$/i.test(lastSync)) //txid as lastSync + fetch_options.after = lastSync; + floBlockchainAPI.readAllTxs(addr, fetch_options).then(response => { + let newItems = response.items.map(({ time, txid, floData, isCoinBase, vin, vout }) => ({ + time, txid, floData, isCoinBase, + sender: isCoinBase ? `(mined)${vin[0].coinbase}` : vin[0].addr, + receiver: isCoinBase ? addr : vout[0].scriptPubKey.addresses[0] + })).reverse(); compactIDB.readData('transactions', addr).then(IDBresult => { - if (IDBresult === undefined) - var promise1 = compactIDB.addData('transactions', APIresult.items, addr) - else - var promise1 = compactIDB.writeData('transactions', IDBresult.concat(APIresult.items), addr) - var promise2 = compactIDB.writeData('lastSync', APIresult.totalItems, addr) - Promise.all([promise1, promise2]).then(values => resolve(APIresult.items)) + if ((IDBresult === undefined || old_support))//backward support + IDBresult = []; + compactIDB.writeData('transactions', IDBresult.concat(newItems), addr).then(result => { + compactIDB.writeData('lastSync', response.lastItem, addr) + .then(result => resolve(newItems)) + .catch(error => reject(error)) + }).catch(error => reject(error)) }) - }) + + }).catch(error => reject(error)) }).catch(error => reject(error)) }) } - //Get new Tx in blockchain since last sync using API - async function getNewTxs(addr, ignoreOld) { - try { - const { totalItems } = await floBlockchainAPI.readTxs(addr, 0, 1); - const newItems = totalItems - ignoreOld; - if (newItems > 0) { - const { items: newTxs } = await floBlockchainAPI.readTxs(addr, 0, newItems * 2); - const filteredData = [] - newTxs - .slice(0, newItems) - .forEach(({ time, txid, floData, isCoinBase, vin, vout }) => { - const sender = isCoinBase ? `(mined)${vin[0].coinbase}` : vin[0].addr; - const receiver = isCoinBase ? addr : vout[0].scriptPubKey.addresses[0]; - filteredData.unshift({ time, txid, floData, sender, receiver }); - }) - return { totalItems, items: filteredData }; - } else { - return { totalItems, items: [] }; - } - } catch (error) { - throw new Error(`Failed to get new transactions for ${addr}: ${error.message}`); - } - } - - //read transactions stored in IDB : resolves Array(storedItems) floWebWallet.readTransactions = function (addr) { return new Promise((resolve, reject) => { @@ -110,82 +95,12 @@ }) } - function waitForConfirmation(txid, max_retry = -1, retry_timeout = 20) { - return new Promise((resolve, reject) => { - setTimeout(function () { - floBlockchainAPI.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(false); - 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) - }) - } - - function sendRawTransaction(receiver, utxo, vout, scriptPubKey, data, wif) { - var trx = bitjs.transaction(); - trx.addinput(utxo, vout, scriptPubKey) - trx.addoutput(receiver, floBlockchainAPI.sendAmt); - trx.addflodata(data); - var signedTxHash = trx.sign(wif, 1); - return floBlockchainAPI.broadcastTx(signedTxHash); - } - - function sendTokens_raw(privKey, receiverID, token, amount, utxo, vout, scriptPubKey) { - return new Promise((resolve, reject) => { - sendRawTransaction(receiverID, utxo, vout, scriptPubKey, `send ${amount} ${token}#`, privKey) - .then(txid => resolve([receiverID, txid])) - .catch(error => reject([receiverID, error])) - }) - } - //bulk transfer tokens floWebWallet.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}`); - - //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 - 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)) + floTokenAPI.bulkTransferTokens(sender, privKey, token, receivers) + .then(result => resolve(result)) + .catch(error => reject(error)) }) } diff --git a/scripts/floBlockchainAPI.js b/scripts/floBlockchainAPI.js index 1d8a751..460775b 100644 --- a/scripts/floBlockchainAPI.js +++ b/scripts/floBlockchainAPI.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floBlockchainAPI v2.4.3 +(function (EXPORTS) { //floBlockchainAPI v2.5.6a /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/ 'use strict'; const floBlockchainAPI = EXPORTS; @@ -9,18 +9,20 @@ FLO: ['https://flosight.ranchimall.net/'], FLO_TEST: ['https://flosight-testnet.ranchimall.net/'] }, - sendAmt: 0.001, - fee: 0.0005, - minChangeAmt: 0.0005, + sendAmt: 0.0003, + fee: 0.0002, + minChangeAmt: 0.0002, receiverID: floGlobals.adminID }; const SATOSHI_IN_BTC = 1e8; + const isUndefined = val => typeof val === 'undefined'; const util = floBlockchainAPI.util = {}; util.Sat_to_FLO = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8)); util.FLO_to_Sat = value => parseInt(value * SATOSHI_IN_BTC); + util.toFixed = value => parseFloat((value).toFixed(8)); Object.defineProperties(floBlockchainAPI, { sendAmt: { @@ -110,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)); @@ -120,14 +124,46 @@ } //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) => { - promisedAPI(`api/addr/${addr}/balance`) - .then(balance => resolve(parseFloat(balance))) - .catch(error => reject(error)); + let api = `api/addr/${addr}/balance`, query_params = {}; + if (after) { + if (typeof after === 'string' && /^[0-9a-z]{64}$/i.test(after)) + 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)) }); } + const getUTXOs = address => new Promise((resolve, reject) => { + promisedAPI(`api/addr/${address}/utxo`) + .then(utxo => resolve(utxo)) + .catch(error => reject(error)) + }) + + 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)) + }) + //create a transaction with single sender const createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) { return new Promise((resolve, reject) => { @@ -144,45 +180,31 @@ var fee = DEFAULT.fee; if (balance < sendAmt + fee) return reject("Insufficient FLO balance!"); - //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; - 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: Some UTXOs are unconfirmed"); - else { - trx.addoutput(receiverAddr, sendAmt); - var change = utxoAmt - sendAmt - fee; - if (change > DEFAULT.minChangeAmt) - trx.addoutput(senderAddr, change); - trx.addflodata(floData.replace(/\n/g, ' ')); - resolve(trx); - } - }).catch(error => reject(error)) + getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => { + getUTXOs(senderAddr).then(utxos => { + //form/construct the transaction data + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + 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: Some UTXOs are unconfirmed"); + else { + trx.addoutput(receiverAddr, sendAmt); + var change = utxoAmt - sendAmt - fee; + 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)) @@ -238,7 +260,7 @@ var trx = bitjs.transaction(); var utxoAmt = 0.0; var fee = DEFAULT.fee; - promisedAPI(`api/addr/${floID}/utxo`).then(utxos => { + getUTXOs(floID).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); @@ -254,6 +276,7 @@ }) } + //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)) @@ -264,58 +287,42 @@ return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); var fee = DEFAULT.fee; var splitAmt = DEFAULT.sendAmt + fee; - var requiredAmt = splitAmt * count; + var totalAmt = splitAmt * count; getBalance(floID).then(balance => { var fee = DEFAULT.fee; - if (balance < requiredAmt + fee) + if (balance < totalAmt + fee) return reject("Insufficient FLO balance!"); //get unconfirmed tx list - promisedAPI(`api/addr/${floID}`).then(result => { - readTxs(floID, 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 === floID) { - if (Array.isArray(unconfirmedSpent[vin.txid])) - unconfirmedSpent[vin.txid].push(vin.vout); - else - unconfirmedSpent[vin.txid] = [vin.vout]; - } - //get utxos list - promisedAPI(`api/addr/${floID}/utxo`).then(utxos => { - //form/construct the transaction data - var trx = bitjs.transaction(); - var utxoAmt = 0.0; - for (let i = utxos.length - 1; - (i >= 0) && (utxoAmt < requiredAmt + 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 < requiredAmt + fee) - reject("Insufficient FLO: Some UTXOs are unconfirmed"); - else { - for (let i = 0; i < count; i++) - trx.addoutput(floID, splitAmt); - var change = utxoAmt - requiredAmt - 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)) + 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)) - }) } @@ -326,11 +333,11 @@ * @param {boolean} preserveRatio (optional) preserve ratio or equal contribution * @return {Promise} */ - floBlockchainAPI.writeDataMultiple = function (senderPrivKeys, data, receivers = [DEFAULT.receiverID], preserveRatio = true) { + floBlockchainAPI.writeDataMultiple = function (senderPrivKeys, data, receivers = [DEFAULT.receiverID], options = {}) { return new Promise((resolve, reject) => { if (!Array.isArray(senderPrivKeys)) return reject("Invalid senderPrivKeys: SenderPrivKeys must be Array"); - if (!preserveRatio) { + if (options.preserveRatio === false) { let tmp = {}; let amount = (DEFAULT.sendAmt * receivers.length) / senderPrivKeys.length; senderPrivKeys.forEach(key => tmp[key] = amount); @@ -340,7 +347,7 @@ return reject("Invalid receivers: Receivers must be Array"); else { let tmp = {}; - let amount = DEFAULT.sendAmt; + let amount = options.sendAmt || DEFAULT.sendAmt; receivers.forEach(floID => tmp[floID] = amount); receivers = tmp } @@ -470,7 +477,7 @@ //Get the UTXOs of the senders let promises = []; for (let floID in senders) - promises.push(promisedAPI(`api/addr/${floID}/utxo`)); + promises.push(getUTXOs(floID)); Promise.all(promises).then(results => { var trx = bitjs.transaction(); for (let floID in senders) { @@ -544,46 +551,32 @@ var fee = DEFAULT.fee; if (balance < sendAmt + fee) return reject("Insufficient FLO balance!"); - //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; - 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, redeemScript); //for multisig, script=redeemScript - utxoAmt += utxos[i].amount; - }; - } - if (utxoAmt < sendAmt + fee) - reject("Insufficient FLO: Some UTXOs are unconfirmed"); - else { - for (let i in receivers) - trx.addoutput(receivers[i], amounts[i]); - var change = utxoAmt - sendAmt - fee; - if (change > DEFAULT.minChangeAmt) - trx.addoutput(senderAddr, change); - trx.addflodata(floData.replace(/\n/g, ' ')); - resolve(trx); - } - }).catch(error => reject(error)) + getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => { + getUTXOs(senderAddr).then(utxos => { + //form/construct the transaction data + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + 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, redeemScript); //for multisig, script=redeemScript + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < sendAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + for (let i in receivers) + trx.addoutput(receivers[i], amounts[i]); + var change = utxoAmt - sendAmt - fee; + 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)) @@ -722,7 +715,7 @@ } const getTxOutput = (txid, i) => new Promise((resolve, reject) => { - fetch_api(`api/tx/${txid}`) + promisedAPI(`api/tx/${txid}`) .then(result => resolve(result.vout[i])) .catch(error => reject(error)) }); @@ -797,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)) @@ -805,30 +798,89 @@ }) } - //Read Txs of Address between from and to - const readTxs = floBlockchainAPI.readTxs = function (addr, from, to) { + /**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) => { - promisedAPI(`api/addrs/${addr}/txs?from=${from}&to=${to}`) + 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 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)) + query_params.from = options.from; + if (!isUndefined(options.to)) + query_params.to = options.to; + } + if (!isUndefined(options.latest)) + query_params.latest = latest; + if (!isUndefined(options.mempool)) + query_params.mempool = options.mempool; + promisedAPI(api, query_params) .then(response => resolve(response)) .catch(error => reject(error)) }); } //Read All Txs of Address (newest first) - floBlockchainAPI.readAllTxs = function (addr) { + const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options = {}) { 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)) + readTxs(addr, options).then(response => { + if (response.incomplete) { + let next_options = Object.assign({}, options); + 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({ + lastItem: response.lastItem || options.after, + items: response.items + }); + }) }); } /*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) + 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 receivedOnly: filters only received data pattern : filters data that with JSON pattern @@ -838,98 +890,149 @@ receiver : flo-id(s) of receiver */ floBlockchainAPI.readData = function (addr, options = {}) { - options.limit = options.limit || 0; - options.ignoreOld = options.ignoreOld || 0; - if (typeof options.senders === "string") options.senders = [options.senders]; - if (typeof options.receivers === "string") options.receivers = [options.receivers]; 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.senders)) { - let flag = false; - for (let vin of response.items[i].vin) - if (options.senders.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.receivers)) { - let flag = false; - for (let vout of response.items[i].vout) - if (options.receivers.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.senders = new Set(response.items[i].vin.map(v => v.addr)); - d.receivers = new Set(response.items[i].vout.map(v => v.scriptPubKey.addresses[0])); - d.data = response.items[i].floData; - filteredData.push(d); - } else - filteredData.push(response.items[i].floData); + //fetch options + 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/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, 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.receivers === "string") options.receivers = [options.receivers]; + + //filter the txs based on options + const filteredData = response.items.filter(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; + + if (options.pattern) { + try { + let jsonContent = JSON.parse(tx.floData); + if (!Object.keys(jsonContent).includes(options.pattern)) + return false; + } catch { + return false; + } } - resolve({ - totalTxs: response.totalItems - unconfirmedCount, - data: filteredData - }); - }).catch(error => { - reject(error); - }); - }).catch(error => { - reject(error); - }); - }); + + if (options.filter && !options.filter(tx.floData)) + return false; + + return true; + }).map(tx => options.tx ? { + 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 + } : tx.floData); + + const result = { lastItem: response.lastItem }; + if (options.tx) + result.items = filteredData; + else + result.data = filteredData + resolve(result); + + }).catch(error => reject(error)) + }) } + /*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/scripts/floCrypto.js b/scripts/floCrypto.js index ec41da4..f1e14d3 100644 --- a/scripts/floCrypto.js +++ b/scripts/floCrypto.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floCrypto v2.3.5a +(function (EXPORTS) { //floCrypto v2.3.6a /* FLO Crypto Operators */ 'use strict'; const floCrypto = EXPORTS; @@ -152,6 +152,19 @@ newID: { get: () => generateNewID() }, + hashID: { + value: (str) => { + let bytes = ripemd160(Crypto.SHA256(str, { asBytes: true }), { asBytes: true }); + bytes.unshift(bitjs.pub); + var hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + return bitjs.Base58.encode(bytes.concat(checksum)); + } + }, tmpID: { get: () => { let bytes = Crypto.util.randomBytes(20); @@ -323,6 +336,21 @@ return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4))); } + //Convert raw address bytes to floID + floCrypto.rawToFloID = function (raw_bytes) { + if (typeof raw_bytes === 'string') + raw_bytes = Crypto.util.hexToBytes(raw_bytes); + if (raw_bytes.length != 20) + return null; + raw_bytes.unshift(bitjs.pub); + let hash = Crypto.SHA256(Crypto.SHA256(raw_bytes, { + asBytes: true + }), { + asBytes: true + }); + return bitjs.Base58.encode(raw_bytes.concat(hash.slice(0, 4))); + } + //Convert the given multisig address (any blockchain) to equivalent multisig floID floCrypto.toMultisigFloID = function (address, options = null) { if (!address) diff --git a/scripts/floTokenAPI.js b/scripts/floTokenAPI.js index efbd2ae..2456b88 100644 --- a/scripts/floTokenAPI.js +++ b/scripts/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}`)