Merge pull request #9 from ranchimall/dev

flosight update
This commit is contained in:
Sai Raj 2023-05-12 01:22:51 +05:30 committed by GitHub
commit 0b2a5fb322
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 503 additions and 373 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.tmp*

21
LICENCE Normal file
View File

@ -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.

View File

@ -1,4 +1,4 @@
(function(EXPORTS) { //compactIDB v2.1.0 (function (EXPORTS) { //compactIDB v2.1.2
/* Compact IndexedDB operations */ /* Compact IndexedDB operations */
'use strict'; 'use strict';
const compactIDB = EXPORTS; const compactIDB = EXPORTS;
@ -59,7 +59,7 @@
}) })
} }
compactIDB.initDB = function(dbName, objectStores = {}) { compactIDB.initDB = function (dbName, objectStores = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!(objectStores instanceof Object)) if (!(objectStores instanceof Object))
return reject('ObjectStores must be an object or array') return reject('ObjectStores must be an object or array')
@ -87,14 +87,14 @@
resolve("Initiated IndexedDB"); resolve("Initiated IndexedDB");
else else
upgradeDB(dbName, a_obs, d_obs) upgradeDB(dbName, a_obs, d_obs)
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)) .catch(error => reject(error))
db.close(); db.close();
} }
}); });
} }
const openDB = compactIDB.openDB = function(dbName = defaultDB) { const openDB = compactIDB.openDB = function (dbName = defaultDB) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var idb = indexedDB.open(dbName); var idb = indexedDB.open(dbName);
idb.onerror = (event) => reject("Error in opening IndexedDB"); 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) => { return new Promise((resolve, reject) => {
var deleteReq = indexedDB.deleteDatabase(dbName);; var deleteReq = indexedDB.deleteDatabase(dbName);;
deleteReq.onerror = (event) => reject("Error deleting database!"); 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) => { return new Promise((resolve, reject) => {
openDB(dbName).then(db => { openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName); 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) => { return new Promise((resolve, reject) => {
openDB(dbName).then(db => { openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName); 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) => { return new Promise((resolve, reject) => {
openDB(dbName).then(db => { openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName); 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) => { return new Promise((resolve, reject) => {
openDB(dbName).then(db => { openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName); 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) => { return new Promise((resolve, reject) => {
openDB(dbName).then(db => { openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName); 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) => { return new Promise((resolve, reject) => {
openDB(dbName).then(db => { openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName); 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.lowerKey = options.atKey || options.lowerKey || 0
options.upperKey = options.atKey || options.upperKey || false options.upperKey = options.atKey || options.upperKey || false
options.patternEval = options.patternEval || ((k, v) => { options.patternEval = options.patternEval || ((k, v) => true);
return true
})
options.limit = options.limit || false; options.limit = options.limit || false;
options.reverse = options.reverse || false;
options.lastOnly = options.lastOnly || false options.lastOnly = options.lastOnly || false
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
openDB(dbName).then(db => { openDB(dbName).then(db => {
@ -237,17 +236,16 @@
var filteredResult = {} var filteredResult = {}
let curReq = obs.openCursor( let curReq = obs.openCursor(
options.upperKey ? IDBKeyRange.bound(options.lowerKey, options.upperKey) : IDBKeyRange.lowerBound(options.lowerKey), 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) => { curReq.onsuccess = (evt) => {
var cursor = evt.target.result; var cursor = evt.target.result;
if (cursor) { if (!cursor || (options.limit && options.limit <= Object.keys(filteredResult).length))
if (options.patternEval(cursor.primaryKey, cursor.value)) { return resolve(filteredResult); //reached end of key list or limit reached
filteredResult[cursor.primaryKey] = cursor.value; else if (options.patternEval(cursor.primaryKey, cursor.value)) {
options.lastOnly ? resolve(filteredResult) : cursor.continue(); filteredResult[cursor.primaryKey] = cursor.value;
} else options.lastOnly ? resolve(filteredResult) : cursor.continue();
cursor.continue();
} else } else
resolve(filteredResult); cursor.continue();
} }
curReq.onerror = (evt) => reject(`Search unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`); curReq.onerror = (evt) => reject(`Search unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`);
db.close(); db.close();

View File

@ -52,46 +52,31 @@
floWebWallet.syncTransactions = function (addr) { floWebWallet.syncTransactions = function (addr) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
compactIDB.readData('lastSync', addr).then(lastSync => { compactIDB.readData('lastSync', addr).then(lastSync => {
lastSync = lastSync | 0; const old_support = Number.isInteger(lastSync); //backward support
getNewTxs(addr, lastSync).then(APIresult => { 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 => { compactIDB.readData('transactions', addr).then(IDBresult => {
if (IDBresult === undefined) if ((IDBresult === undefined || old_support))//backward support
var promise1 = compactIDB.addData('transactions', APIresult.items, addr) IDBresult = [];
else compactIDB.writeData('transactions', IDBresult.concat(newItems), addr).then(result => {
var promise1 = compactIDB.writeData('transactions', IDBresult.concat(APIresult.items), addr) compactIDB.writeData('lastSync', response.lastItem, addr)
var promise2 = compactIDB.writeData('lastSync', APIresult.totalItems, addr) .then(result => resolve(newItems))
Promise.all([promise1, promise2]).then(values => resolve(APIresult.items)) .catch(error => reject(error))
}).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) //read transactions stored in IDB : resolves Array(storedItems)
floWebWallet.readTransactions = function (addr) { floWebWallet.readTransactions = function (addr) {
return new Promise((resolve, reject) => { 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 //bulk transfer tokens
floWebWallet.bulkTransferTokens = function (sender, privKey, token, receivers) { floWebWallet.bulkTransferTokens = function (sender, privKey, token, receivers) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (typeof receivers !== 'object') floTokenAPI.bulkTransferTokens(sender, privKey, token, receivers)
return reject("receivers must be object in format {receiver1: amount1, receiver2:amount2...}") .then(result => resolve(result))
.catch(error => reject(error))
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))
}) })
} }

View File

@ -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*/ /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/
'use strict'; 'use strict';
const floBlockchainAPI = EXPORTS; const floBlockchainAPI = EXPORTS;
@ -9,18 +9,20 @@
FLO: ['https://flosight.ranchimall.net/'], FLO: ['https://flosight.ranchimall.net/'],
FLO_TEST: ['https://flosight-testnet.ranchimall.net/'] FLO_TEST: ['https://flosight-testnet.ranchimall.net/']
}, },
sendAmt: 0.001, sendAmt: 0.0003,
fee: 0.0005, fee: 0.0002,
minChangeAmt: 0.0005, minChangeAmt: 0.0002,
receiverID: floGlobals.adminID receiverID: floGlobals.adminID
}; };
const SATOSHI_IN_BTC = 1e8; const SATOSHI_IN_BTC = 1e8;
const isUndefined = val => typeof val === 'undefined';
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));
util.FLO_to_Sat = value => parseInt(value * SATOSHI_IN_BTC); util.FLO_to_Sat = value => parseInt(value * SATOSHI_IN_BTC);
util.toFixed = value => parseFloat((value).toFixed(8));
Object.defineProperties(floBlockchainAPI, { Object.defineProperties(floBlockchainAPI, {
sendAmt: { sendAmt: {
@ -110,9 +112,11 @@
}); });
//Promised function to get data from API //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) => { 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) fetch_api(apicall)
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)); .catch(error => reject(error));
@ -120,14 +124,46 @@
} }
//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) => {
promisedAPI(`api/addr/${addr}/balance`) let api = `api/addr/${addr}/balance`, query_params = {};
.then(balance => resolve(parseFloat(balance))) if (after) {
.catch(error => reject(error)); 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 //create a transaction with single sender
const createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) { const createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -144,45 +180,31 @@
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!");
//get unconfirmed tx list getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => {
promisedAPI(`api/addr/${senderAddr}`).then(result => { getUTXOs(senderAddr).then(utxos => {
readTxs(senderAddr, 0, result.unconfirmedTxApperances).then(result => { //form/construct the transaction data
let unconfirmedSpent = {}; var trx = bitjs.transaction();
for (let tx of result.items) var utxoAmt = 0.0;
if (tx.confirmations == 0) for (var i = utxos.length - 1;
for (let vin of tx.vin) (i >= 0) && (utxoAmt < sendAmt + fee); i--) {
if (vin.addr === senderAddr) { //use only utxos with confirmations (strict_utxo mode)
if (Array.isArray(unconfirmedSpent[vin.txid])) if (utxos[i].confirmations || !strict_utxo) {
unconfirmedSpent[vin.txid].push(vin.vout); if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
else continue; //A transaction has already used the utxo, but is unconfirmed.
unconfirmedSpent[vin.txid] = [vin.vout]; trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
} utxoAmt += utxos[i].amount;
//get utxos list };
promisedAPI(`api/addr/${senderAddr}/utxo`).then(utxos => { }
//form/construct the transaction data if (utxoAmt < sendAmt + fee)
var trx = bitjs.transaction(); reject("Insufficient FLO: Some UTXOs are unconfirmed");
var utxoAmt = 0.0; else {
for (var i = utxos.length - 1; trx.addoutput(receiverAddr, sendAmt);
(i >= 0) && (utxoAmt < sendAmt + fee); i--) { var change = utxoAmt - sendAmt - fee;
//use only utxos with confirmations (strict_utxo mode) if (change > DEFAULT.minChangeAmt)
if (utxos[i].confirmations || !strict_utxo) { trx.addoutput(senderAddr, change);
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) trx.addflodata(floData.replace(/\n/g, ' '));
continue; //A transaction has already used the utxo, but is unconfirmed. resolve(trx);
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))
}).catch(error => reject(error)) }).catch(error => reject(error))
}).catch(error => reject(error)) }).catch(error => reject(error))
@ -238,7 +260,7 @@
var trx = bitjs.transaction(); var trx = bitjs.transaction();
var utxoAmt = 0.0; var utxoAmt = 0.0;
var fee = DEFAULT.fee; var fee = DEFAULT.fee;
promisedAPI(`api/addr/${floID}/utxo`).then(utxos => { getUTXOs(floID).then(utxos => {
for (var i = utxos.length - 1; i >= 0; i--) for (var i = utxos.length - 1; i >= 0; i--)
if (utxos[i].confirmations) { if (utxos[i].confirmations) {
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); 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 = '') { floBlockchainAPI.splitUTXOs = function (floID, privKey, count, floData = '') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!floCrypto.validateFloID(floID, true)) if (!floCrypto.validateFloID(floID, true))
@ -264,58 +287,42 @@
return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
var fee = DEFAULT.fee; var fee = DEFAULT.fee;
var splitAmt = DEFAULT.sendAmt + fee; var splitAmt = DEFAULT.sendAmt + fee;
var requiredAmt = splitAmt * count; var totalAmt = splitAmt * count;
getBalance(floID).then(balance => { getBalance(floID).then(balance => {
var fee = DEFAULT.fee; var fee = DEFAULT.fee;
if (balance < requiredAmt + fee) if (balance < totalAmt + fee)
return reject("Insufficient FLO balance!"); return reject("Insufficient FLO balance!");
//get unconfirmed tx list //get unconfirmed tx list
promisedAPI(`api/addr/${floID}`).then(result => { getUnconfirmedSpent(floID).then(unconfirmedSpent => {
readTxs(floID, 0, result.unconfirmedTxApperances).then(result => { getUTXOs(floID).then(utxos => {
let unconfirmedSpent = {}; var trx = bitjs.transaction();
for (let tx of result.items) var utxoAmt = 0.0;
if (tx.confirmations == 0) for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + fee); i--) {
for (let vin of tx.vin) //use only utxos with confirmations (strict_utxo mode)
if (vin.addr === floID) { if (utxos[i].confirmations || !strict_utxo) {
if (Array.isArray(unconfirmedSpent[vin.txid])) if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
unconfirmedSpent[vin.txid].push(vin.vout); continue; //A transaction has already used the utxo, but is unconfirmed.
else trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
unconfirmedSpent[vin.txid] = [vin.vout]; utxoAmt += utxos[i].amount;
} };
//get utxos list }
promisedAPI(`api/addr/${floID}/utxo`).then(utxos => { if (utxoAmt < totalAmt + fee)
//form/construct the transaction data reject("Insufficient FLO: Some UTXOs are unconfirmed");
var trx = bitjs.transaction(); else {
var utxoAmt = 0.0; for (let i = 0; i < count; i++)
for (let i = utxos.length - 1; trx.addoutput(floID, splitAmt);
(i >= 0) && (utxoAmt < requiredAmt + fee); i--) { var change = utxoAmt - totalAmt - fee;
//use only utxos with confirmations (strict_utxo mode) if (change > DEFAULT.minChangeAmt)
if (utxos[i].confirmations || !strict_utxo) { trx.addoutput(floID, change);
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) trx.addflodata(floData.replace(/\n/g, ' '));
continue; //A transaction has already used the utxo, but is unconfirmed. var signedTxHash = trx.sign(privKey, 1);
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); broadcastTx(signedTxHash)
utxoAmt += utxos[i].amount; .then(txid => resolve(txid))
}; .catch(error => reject(error))
} }
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))
}).catch(error => reject(error)) }).catch(error => reject(error))
}).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 * @param {boolean} preserveRatio (optional) preserve ratio or equal contribution
* @return {Promise} * @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) => { return new Promise((resolve, reject) => {
if (!Array.isArray(senderPrivKeys)) if (!Array.isArray(senderPrivKeys))
return reject("Invalid senderPrivKeys: SenderPrivKeys must be Array"); return reject("Invalid senderPrivKeys: SenderPrivKeys must be Array");
if (!preserveRatio) { if (options.preserveRatio === false) {
let tmp = {}; let tmp = {};
let amount = (DEFAULT.sendAmt * receivers.length) / senderPrivKeys.length; let amount = (DEFAULT.sendAmt * receivers.length) / senderPrivKeys.length;
senderPrivKeys.forEach(key => tmp[key] = amount); senderPrivKeys.forEach(key => tmp[key] = amount);
@ -340,7 +347,7 @@
return reject("Invalid receivers: Receivers must be Array"); return reject("Invalid receivers: Receivers must be Array");
else { else {
let tmp = {}; let tmp = {};
let amount = DEFAULT.sendAmt; let amount = options.sendAmt || DEFAULT.sendAmt;
receivers.forEach(floID => tmp[floID] = amount); receivers.forEach(floID => tmp[floID] = amount);
receivers = tmp receivers = tmp
} }
@ -470,7 +477,7 @@
//Get the UTXOs of the senders //Get the UTXOs of the senders
let promises = []; let promises = [];
for (let floID in senders) for (let floID in senders)
promises.push(promisedAPI(`api/addr/${floID}/utxo`)); promises.push(getUTXOs(floID));
Promise.all(promises).then(results => { Promise.all(promises).then(results => {
var trx = bitjs.transaction(); var trx = bitjs.transaction();
for (let floID in senders) { for (let floID in senders) {
@ -544,46 +551,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!");
//get unconfirmed tx list getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => {
promisedAPI(`api/addr/${senderAddr}`).then(result => { getUTXOs(senderAddr).then(utxos => {
readTxs(senderAddr, 0, result.unconfirmedTxApperances).then(result => { //form/construct the transaction data
let unconfirmedSpent = {}; var trx = bitjs.transaction();
for (let tx of result.items) var utxoAmt = 0.0;
if (tx.confirmations == 0) for (var i = utxos.length - 1;
for (let vin of tx.vin) (i >= 0) && (utxoAmt < sendAmt + fee); i--) {
if (vin.addr === senderAddr) { //use only utxos with confirmations (strict_utxo mode)
if (Array.isArray(unconfirmedSpent[vin.txid])) if (utxos[i].confirmations || !strict_utxo) {
unconfirmedSpent[vin.txid].push(vin.vout); if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
else continue; //A transaction has already used the utxo, but is unconfirmed.
unconfirmedSpent[vin.txid] = [vin.vout]; trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript
} utxoAmt += utxos[i].amount;
//get utxos list };
promisedAPI(`api/addr/${senderAddr}/utxo`).then(utxos => { }
//form/construct the transaction data if (utxoAmt < sendAmt + fee)
var trx = bitjs.transaction(); reject("Insufficient FLO: Some UTXOs are unconfirmed");
var utxoAmt = 0.0; else {
for (var i = utxos.length - 1; for (let i in receivers)
(i >= 0) && (utxoAmt < sendAmt + fee); i--) { trx.addoutput(receivers[i], amounts[i]);
//use only utxos with confirmations (strict_utxo mode) var change = utxoAmt - sendAmt - fee;
if (utxos[i].confirmations || !strict_utxo) { if (change > DEFAULT.minChangeAmt)
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) trx.addoutput(senderAddr, change);
continue; //A transaction has already used the utxo, but is unconfirmed. trx.addflodata(floData.replace(/\n/g, ' '));
trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript resolve(trx);
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))
}).catch(error => reject(error)) }).catch(error => reject(error))
}).catch(error => reject(error)) }).catch(error => reject(error))
@ -722,7 +715,7 @@
} }
const getTxOutput = (txid, i) => new Promise((resolve, reject) => { const getTxOutput = (txid, i) => new Promise((resolve, reject) => {
fetch_api(`api/tx/${txid}`) promisedAPI(`api/tx/${txid}`)
.then(result => resolve(result.vout[i])) .then(result => resolve(result.vout[i]))
.catch(error => reject(error)) .catch(error => reject(error))
}); });
@ -797,7 +790,7 @@
}) })
} }
floBlockchainAPI.getTx = function (txid) { const getTx = floBlockchainAPI.getTx = function (txid) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
promisedAPI(`api/tx/${txid}`) promisedAPI(`api/tx/${txid}`)
.then(response => resolve(response)) .then(response => resolve(response))
@ -805,30 +798,89 @@
}) })
} }
//Read Txs of Address between from and to /**Wait for the given txid to get confirmation in blockchain
const readTxs = floBlockchainAPI.readTxs = function (addr, from, to) { * @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) => { 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)) .then(response => resolve(response))
.catch(error => reject(error)) .catch(error => reject(error))
}); });
} }
//Read All Txs of Address (newest first) //Read All Txs of Address (newest first)
floBlockchainAPI.readAllTxs = function (addr) { const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => { readTxs(addr, options).then(response => {
promisedAPI(`api/addrs/${addr}/txs?from=0&to=${response.totalItems}0`) if (response.incomplete) {
.then(response => resolve(response.items)) let next_options = Object.assign({}, options);
.catch(error => reject(error)); if (options.latest)
}).catch(error => reject(error)) 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 /*Read flo Data from txs of given Address
options can be used to filter data options can be used to filter data
limit : maximum number of filtered data (default = 1000, negative = no limit) after : query after the given txid
ignoreOld : ignore old txs (default = 0) 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 sentOnly : filters only sent data
receivedOnly: filters only received data receivedOnly: filters only received data
pattern : filters data that with JSON pattern pattern : filters data that with JSON pattern
@ -838,98 +890,149 @@
receiver : flo-id(s) of receiver receiver : flo-id(s) of receiver
*/ */
floBlockchainAPI.readData = function (addr, options = {}) { 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) => { 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) { //fetch options
let d = {} let query_options = {};
d.txid = response.items[i].txid; query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx
d.time = response.items[i].time; if (!isUndefined(options.after) || !isUndefined(options.before)) {
d.blockheight = response.items[i].blockheight; if (!isUndefined(options.ignoreOld)) //Backward support
d.senders = new Set(response.items[i].vin.map(v => v.addr)); return reject("Invalid options: cannot use after/before and ignoreOld in same query");
d.receivers = new Set(response.items[i].vout.map(v => v.scriptPubKey.addresses[0])); //use passed after and/or before options (options remain undefined if not passed)
d.data = response.items[i].floData; query_options.after = options.after;
filteredData.push(d); query_options.before = options.before;
} else }
filteredData.push(response.items[i].floData); 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, if (options.filter && !options.filter(tx.floData))
data: filteredData return false;
});
}).catch(error => { return true;
reject(error); }).map(tx => options.tx ? {
}); txid: tx.txid,
}).catch(error => { time: tx.time,
reject(error); 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 = {}); })('object' === typeof module ? module.exports : window.floBlockchainAPI = {});

View File

@ -1,4 +1,4 @@
(function (EXPORTS) { //floCrypto v2.3.5a (function (EXPORTS) { //floCrypto v2.3.6a
/* FLO Crypto Operators */ /* FLO Crypto Operators */
'use strict'; 'use strict';
const floCrypto = EXPORTS; const floCrypto = EXPORTS;
@ -152,6 +152,19 @@
newID: { newID: {
get: () => generateNewID() 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: { tmpID: {
get: () => { get: () => {
let bytes = Crypto.util.randomBytes(20); let bytes = Crypto.util.randomBytes(20);
@ -323,6 +336,21 @@
return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4))); 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 //Convert the given multisig address (any blockchain) to equivalent multisig floID
floCrypto.toMultisigFloID = function (address, options = null) { floCrypto.toMultisigFloID = function (address, options = null) {
if (!address) if (!address)

View File

@ -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*/ /* Token Operator to send/receive tokens via blockchain using API calls*/
'use strict'; 'use strict';
const tokenAPI = EXPORTS; 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) { tokenAPI.getAllTxs = function (floID, token = DEFAULT.currency) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch_api(`api/v1.0/getFloAddressTransactions?token=${token}&floAddress=${floID}`) fetch_api(`api/v1.0/getFloAddressTransactions?token=${token}&floAddress=${floID}`)