Merge pull request #23 from ranchimall/dev

Blockbook API
This commit is contained in:
Sai Raj 2023-07-06 18:49:08 +05:30 committed by GitHub
commit 94fd567a0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 599 additions and 14974 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 +1 @@
# rupee-token # rupee-token

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,13 @@
(function (EXPORTS) { //btcOperator v1.1.2a (function (EXPORTS) { //btcOperator v1.1.3b
/* BTC Crypto and API Operator */ /* BTC Crypto and API Operator */
const btcOperator = EXPORTS; const btcOperator = EXPORTS;
//This library uses API provided by chain.so (https://chain.so/) //This library uses API provided by chain.so (https://chain.so/)
const URL = "https://blockchain.info/"; const URL = "https://blockchain.info/";
const DUST_AMT = 546,
MIN_FEE_UPDATE = 219;
const fetch_api = btcOperator.fetch = function (api, json_res = true) { const fetch_api = btcOperator.fetch = function (api, json_res = true) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.debug(URL + api); console.debug(URL + api);
@ -400,7 +403,12 @@
return reject("Send amount is less than fee"); return reject("Send amount is less than fee");
} }
tx.outs = tx.outs.filter(o => o.value != 0); //remove all output with value 0 //remove all output with value less than DUST amount
let filtered_outputs = [], dust_value = 0;
tx.outs.forEach(o => o.value >= DUST_AMT ? filtered_outputs.push(o) : dust_value += o.value);
tx.outs = filtered_outputs;
//update result values
result.fee += util.Sat_to_BTC(dust_value);
result.output_size = output_size; result.output_size = output_size;
result.output_amount = total_amount - (fee_from_receiver ? result.fee : 0); result.output_amount = total_amount - (fee_from_receiver ? result.fee : 0);
result.total_size = BASE_TX_SIZE + output_size + result.input_size; result.total_size = BASE_TX_SIZE + output_size + result.input_size;
@ -456,7 +464,7 @@
let size_per_input = _sizePerInput(addr, rs); let size_per_input = _sizePerInput(addr, rs);
fetch_api(`unspent?active=${addr}`).then(result => { fetch_api(`unspent?active=${addr}`).then(result => {
let utxos = result.unspent_outputs; let utxos = result.unspent_outputs;
console.debug("add-utxo", addr, rs, required_amount, utxos); //console.debug("add-utxo", addr, rs, required_amount, utxos);
for (let i = 0; i < utxos.length && required_amount > 0; i++) { for (let i = 0; i < utxos.length && required_amount > 0; i++) {
if (!utxos[i].confirmations) //ignore unconfirmed utxo if (!utxos[i].confirmations) //ignore unconfirmed utxo
continue; continue;
@ -536,6 +544,106 @@
} }
*/ */
function tx_fetch_for_editing(tx) {
return new Promise((resolve, reject) => {
if (typeof tx == 'string' && /^[0-9a-f]{64}$/i.test(tx)) { //tx is txid
getTx.hex(tx)
.then(txhex => resolve(deserializeTx(txhex)))
.catch(error => reject(error))
} else resolve(deserializeTx(tx));
})
}
btcOperator.editFee = function (tx_hex, new_fee, private_keys, change_only = true) {
return new Promise((resolve, reject) => {
if (!Array.isArray(private_keys))
private_keys = [private_keys];
tx_fetch_for_editing(tx_hex).then(tx => {
parseTransaction(tx).then(tx_parsed => {
if (tx_parsed.fee >= new_fee)
return reject("Fees can only be increased");
//editable addresses in output values (for fee increase)
var edit_output_address = new Set();
if (change_only === true) //allow only change values (ie, sender address) to be edited to inc fee
tx_parsed.inputs.forEach(inp => edit_output_address.add(inp.address));
else if (change_only === false) //allow all output values to be edited
tx_parsed.outputs.forEach(out => edit_output_address.add(out.address));
else if (typeof change_only == 'string') // allow only given receiver id output to be edited
edit_output_address.add(change_only);
else if (Array.isArray(change_only)) //allow only given set of receiver id outputs to be edited
change_only.forEach(id => edit_output_address.add(id));
//edit output values to increase fee
let inc_fee = util.BTC_to_Sat(new_fee - tx_parsed.fee);
if (inc_fee < MIN_FEE_UPDATE)
return reject(`Insufficient additional fee. Minimum increment: ${MIN_FEE_UPDATE}`);
for (let i = tx.outs.length - 1; i >= 0 && inc_fee > 0; i--) //reduce in reverse order
if (edit_output_address.has(tx_parsed.outputs[i].address)) {
let current_value = tx.outs[i].value;
if (current_value instanceof BigInteger) //convert BigInteger class to inv value
current_value = current_value.intValue();
//edit the value as required
if (current_value > inc_fee) {
tx.outs[i].value = current_value - inc_fee;
inc_fee = 0;
} else {
inc_fee -= current_value;
tx.outs[i].value = 0;
}
}
if (inc_fee > 0) {
let max_possible_fee = util.BTC_to_Sat(new_fee) - inc_fee; //in satoshi
return reject(`Insufficient output values to increase fee. Maximum fee possible: ${util.Sat_to_BTC(max_possible_fee)}`);
}
tx.outs = tx.outs.filter(o => o.value >= DUST_AMT); //remove all output with value less than DUST amount
//remove existing signatures and reset the scripts
let wif_keys = [];
for (let i in tx.ins) {
var addr = tx_parsed.inputs[i].address,
value = util.BTC_to_Sat(tx_parsed.inputs[i].value);
let addr_decode = coinjs.addressDecode(addr);
//find the correct key for addr
var privKey = private_keys.find(pk => verifyKey(addr, pk));
if (!privKey)
return reject(`Private key missing for ${addr}`);
//find redeemScript (if any)
const rs = _redeemScript(addr, privKey);
rs === false ? wif_keys.unshift(privKey) : wif_keys.push(privKey); //sorting private-keys (wif)
//reset the script for re-signing
var script;
if (!rs || !rs.length) {
//legacy script (derive from address)
let s = coinjs.script();
s.writeOp(118); //OP_DUP
s.writeOp(169); //OP_HASH160
s.writeBytes(addr_decode.bytes);
s.writeOp(136); //OP_EQUALVERIFY
s.writeOp(172); //OP_CHECKSIG
script = Crypto.util.bytesToHex(s.buffer);
} else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi)) || addr_decode.type === 'multisigBech32') {
//redeemScript for segwit/bech32 and multisig (bech32)
let s = coinjs.script();
s.writeBytes(Crypto.util.hexToBytes(rs));
s.writeOp(0);
s.writeBytes(coinjs.numToBytes(value.toFixed(0), 8));
script = Crypto.util.bytesToHex(s.buffer);
} else //redeemScript for multisig (segwit)
script = rs;
tx.ins[i].script = coinjs.script(script);
}
tx.witness = false; //remove all witness signatures
console.debug("Unsigned:", tx.serialize());
//re-sign the transaction
new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF
resolve(tx.serialize());
}).catch(error => reject(error))
}).catch(error => reject(error))
})
}
btcOperator.sendTx = function (senders, privkeys, receivers, amounts, fee = null, options = {}) { btcOperator.sendTx = function (senders, privkeys, receivers, amounts, fee = null, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
createSignedTx(senders, privkeys, receivers, amounts, fee, options).then(result => { createSignedTx(senders, privkeys, receivers, amounts, fee, options).then(result => {
@ -579,7 +687,7 @@
createTransaction(senders, redeemScripts, receivers, amounts, fee, options.change_address || senders[0], options.fee_from_receiver).then(result => { createTransaction(senders, redeemScripts, receivers, amounts, fee, options.change_address || senders[0], options.fee_from_receiver).then(result => {
let tx = result.transaction; let tx = result.transaction;
console.debug("Unsigned:", tx.serialize()); console.debug("Unsigned:", tx.serialize());
new Set(wif_keys).forEach(key => console.debug("Signing key:", key, tx.sign(key, 1 /*sighashtype*/))); //Sign the tx using private key WIF new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF
console.debug("Signed:", tx.serialize()); console.debug("Signed:", tx.serialize());
resolve(result); resolve(result);
}).catch(error => reject(error)); }).catch(error => reject(error));
@ -723,7 +831,7 @@
.catch(error => reject(error)) .catch(error => reject(error))
}); });
btcOperator.parseTransaction = function (tx) { const parseTransaction = btcOperator.parseTransaction = function (tx) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
tx = deserializeTx(tx); tx = deserializeTx(tx);
let result = {}; let result = {};
@ -780,7 +888,7 @@
.catch(error => reject(error)) .catch(error => reject(error))
}) })
btcOperator.getTx = txid => new Promise((resolve, reject) => { const getTx = btcOperator.getTx = txid => new Promise((resolve, reject) => {
fetch_api(`rawtx/${txid}`).then(result => { fetch_api(`rawtx/${txid}`).then(result => {
getLatestBlock().then(latest_block => resolve({ getLatestBlock().then(latest_block => resolve({
block: result.block_height, block: result.block_height,
@ -797,7 +905,7 @@
}).catch(error => reject(error)) }).catch(error => reject(error))
}); });
btcOperator.getTx.hex = txid => new Promise((resolve, reject) => { getTx.hex = txid => new Promise((resolve, reject) => {
fetch_api(`rawtx/${txid}?format=hex`, false) fetch_api(`rawtx/${txid}?format=hex`, false)
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)) .catch(error => reject(error))

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

@ -1,13 +1,13 @@
(function (EXPORTS) { //floBlockchainAPI v2.5.1 (function (EXPORTS) { //floBlockchainAPI v3.0.1b
/* FLO Blockchain Operator to send/receive data from blockchain using API calls*/ /* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/
'use strict'; 'use strict';
const floBlockchainAPI = EXPORTS; const floBlockchainAPI = EXPORTS;
const DEFAULT = { const DEFAULT = {
blockchain: floGlobals.blockchain, blockchain: floGlobals.blockchain,
apiURL: { apiURL: {
FLO: ['https://flosight.duckdns.org/', 'https://flosight.ranchimall.net/'], FLO: ['https://blockbook.ranchimall.net/'],
FLO_TEST: ['https://testnet-flosight.duckdns.org', 'https://testnet.flocha.in/'] FLO_TEST: []
}, },
sendAmt: 0.0003, sendAmt: 0.0003,
fee: 0.0002, fee: 0.0002,
@ -16,6 +16,7 @@
}; };
const SATOSHI_IN_BTC = 1e8; const SATOSHI_IN_BTC = 1e8;
const isUndefined = val => typeof val === 'undefined';
const util = floBlockchainAPI.util = {}; const util = floBlockchainAPI.util = {};
@ -60,9 +61,9 @@
var serverList = Array.from(allServerList); var serverList = Array.from(allServerList);
var curPos = floCrypto.randInt(0, serverList.length - 1); var curPos = floCrypto.randInt(0, serverList.length - 1);
function fetch_retry(apicall, rm_flosight) { function fetch_retry(apicall, rm_node) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let i = serverList.indexOf(rm_flosight) let i = serverList.indexOf(rm_node)
if (i != -1) serverList.splice(i, 1); if (i != -1) serverList.splice(i, 1);
curPos = floCrypto.randInt(0, serverList.length - 1); curPos = floCrypto.randInt(0, serverList.length - 1);
fetch_api(apicall, false) fetch_api(apicall, false)
@ -81,19 +82,19 @@
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)); .catch(error => reject(error));
} else } else
reject("No floSight server working"); reject("No FLO blockbook server working");
} else { } else {
let flosight = serverList[curPos]; let serverURL = serverList[curPos];
fetch(flosight + apicall).then(response => { fetch(serverURL + apicall).then(response => {
if (response.ok) if (response.ok)
response.json().then(data => resolve(data)); response.json().then(data => resolve(data));
else { else {
fetch_retry(apicall, flosight) fetch_retry(apicall, serverURL)
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)); .catch(error => reject(error));
} }
}).catch(error => { }).catch(error => {
fetch_retry(apicall, flosight) fetch_retry(apicall, serverURL)
.then(result => resolve(result)) .then(result => resolve(result))
.catch(error => reject(error)); .catch(error => reject(error));
}) })
@ -111,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));
@ -121,43 +124,27 @@
} }
//Get balance for the given Address //Get balance for the given Address
const getBalance = floBlockchainAPI.getBalance = function (addr, after = null) { const getBalance = floBlockchainAPI.getBalance = function (addr) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let api = `api/addr/${addr}/balance`; let api = `api/address/${addr}`;
if (after) { promisedAPI(api, { details: "basic" })
if (typeof after === 'string' && /^[0-9a-z]{64}$/i.test(after)) .then(result => resolve(result["balance"]))
api += '?after=' + after; .catch(error => reject(error))
else return reject("Invalid 'after' parameter");
}
promisedAPI(api).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) => { function getScriptPubKey(address) {
promisedAPI(`api/addr/${address}/utxo`) var tx = bitjs.transaction();
.then(utxo => resolve(utxo)) tx.addoutput(address, 0);
.catch(error => reject(error)) let outputBuffer = tx.outputs.pop().script;
}) return Crypto.util.bytesToHex(outputBuffer)
}
const getUnconfirmedSpent = address => new Promise((resolve, reject) => { const getUTXOs = address => new Promise((resolve, reject) => {
readTxs(address, { mempool: "only" }).then(result => { promisedAPI(`api/utxo/${address}`, { confirmed: true }).then(utxos => {
let unconfirmedSpent = {}; let scriptPubKey = getScriptPubKey(address);
for (let tx of result.items) utxos.forEach(u => u.scriptPubKey = scriptPubKey);
if (tx.confirmations == 0) resolve(utxos);
for (let vin of tx.vin)
if (vin.addr === address) {
if (Array.isArray(unconfirmedSpent[vin.txid]))
unconfirmedSpent[vin.txid].push(vin.vout);
else
unconfirmedSpent[vin.txid] = [vin.vout];
}
resolve(unconfirmedSpent);
}).catch(error => reject(error)) }).catch(error => reject(error))
}) })
@ -177,32 +164,28 @@
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!");
getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => { getUTXOs(senderAddr).then(utxos => {
getUTXOs(senderAddr).then(utxos => { //form/construct the transaction data
//form/construct the transaction data var trx = bitjs.transaction();
var trx = bitjs.transaction(); var utxoAmt = 0.0;
var utxoAmt = 0.0; for (var i = utxos.length - 1;
for (var i = utxos.length - 1; (i >= 0) && (utxoAmt < sendAmt + fee); i--) {
(i >= 0) && (utxoAmt < sendAmt + fee); i--) { //use only utxos with confirmations (strict_utxo mode)
//use only utxos with confirmations (strict_utxo mode) if (utxos[i].confirmations || !strict_utxo) {
if (utxos[i].confirmations || !strict_utxo) { trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) utxoAmt += utxos[i].amount;
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 {
if (utxoAmt < sendAmt + fee) trx.addoutput(receiverAddr, sendAmt);
reject("Insufficient FLO: Some UTXOs are unconfirmed"); var change = utxoAmt - sendAmt - fee;
else { if (change > DEFAULT.minChangeAmt)
trx.addoutput(receiverAddr, sendAmt); trx.addoutput(senderAddr, change);
var change = utxoAmt - sendAmt - fee; trx.addflodata(floData.replace(/\n/g, ' '));
if (change > DEFAULT.minChangeAmt) resolve(trx);
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))
}) })
@ -273,6 +256,52 @@
}) })
} }
//split sufficient UTXOs of a given floID for a parallel sending
floBlockchainAPI.splitUTXOs = function (floID, privKey, count, floData = '') {
return new Promise((resolve, reject) => {
if (!floCrypto.validateFloID(floID, true))
return reject(`Invalid floID`);
if (!floCrypto.verifyPrivKey(privKey, floID))
return reject("Invalid Private Key");
if (!floCrypto.validateASCII(floData))
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
var fee = DEFAULT.fee;
var splitAmt = DEFAULT.sendAmt + fee;
var totalAmt = splitAmt * count;
getBalance(floID).then(balance => {
var fee = DEFAULT.fee;
if (balance < totalAmt + fee)
return reject("Insufficient FLO balance!");
//get unconfirmed tx list
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) {
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))
})
}
/**Write data into blockchain from (and/or) to multiple floID /**Write data into blockchain from (and/or) to multiple floID
* @param {Array} senderPrivKeys List of sender private-keys * @param {Array} senderPrivKeys List of sender private-keys
* @param {string} data FLO data of the txn * @param {string} data FLO data of the txn
@ -498,33 +527,29 @@
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!");
getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => { getUTXOs(senderAddr).then(utxos => {
getUTXOs(senderAddr).then(utxos => { //form/construct the transaction data
//form/construct the transaction data var trx = bitjs.transaction();
var trx = bitjs.transaction(); var utxoAmt = 0.0;
var utxoAmt = 0.0; for (var i = utxos.length - 1;
for (var i = utxos.length - 1; (i >= 0) && (utxoAmt < sendAmt + fee); i--) {
(i >= 0) && (utxoAmt < sendAmt + fee); i--) { //use only utxos with confirmations (strict_utxo mode)
//use only utxos with confirmations (strict_utxo mode) if (utxos[i].confirmations || !strict_utxo) {
if (utxos[i].confirmations || !strict_utxo) { trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) utxoAmt += utxos[i].amount;
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 {
if (utxoAmt < sendAmt + fee) for (let i in receivers)
reject("Insufficient FLO: Some UTXOs are unconfirmed"); trx.addoutput(receivers[i], amounts[i]);
else { var change = utxoAmt - sendAmt - fee;
for (let i in receivers) if (change > DEFAULT.minChangeAmt)
trx.addoutput(receivers[i], amounts[i]); trx.addoutput(senderAddr, change);
var change = utxoAmt - sendAmt - fee; trx.addflodata(floData.replace(/\n/g, ' '));
if (change > DEFAULT.minChangeAmt) resolve(trx);
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))
}); });
@ -720,24 +745,15 @@
const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) { const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (signedTxHash.length < 1) if (signedTxHash.length < 1)
return reject("Empty Signature"); return reject("Empty Transaction Data");
var url = serverList[curPos] + 'api/tx/send';
fetch(url, { promisedAPI('/api/sendtx/' + signedTxHash)
method: "POST", .then(response => resolve(response["result"]))
headers: { .catch(error => reject(error))
'Content-Type': 'application/json'
},
body: `{"rawtx":"${signedTxHash}"}`
}).then(response => {
if (response.ok)
response.json().then(data => resolve(data.txid.result));
else
response.text().then(data => resolve(data));
}).catch(error => reject(error));
}) })
} }
floBlockchainAPI.getTx = function (txid) { 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))
@ -745,56 +761,123 @@
}) })
} }
const isUndefined = val => typeof val === 'undefined'; /**Wait for the given txid to get confirmation in blockchain
* @param {string} txid of the transaction to wait for
* @param {int} max_retry: maximum number of retries before exiting wait. negative number = Infinite retries (DEFAULT: -1 ie, infinite retries)
* @param {Array} retry_timeout: time (seconds) between retries (DEFAULT: 20 seconds)
* @return {Promise} resolves when tx gets confirmation
*/
const waitForConfirmation = floBlockchainAPI.waitForConfirmation = function (txid, max_retry = -1, retry_timeout = 20) {
return new Promise((resolve, reject) => {
setTimeout(function () {
getTx(txid).then(tx => {
if (!tx)
return reject("Transaction not found");
if (tx.confirmations)
return resolve(tx);
else if (max_retry === 0) //no more retries
return reject("Waiting timeout: tx still not confirmed");
else {
max_retry = max_retry < 0 ? -1 : max_retry - 1; //decrease retry count (unless infinite retries)
waitForConfirmation(txid, max_retry, retry_timeout)
.then(result => resolve(result))
.catch(error => reject(error))
}
}).catch(error => reject(error))
}, retry_timeout * 1000)
})
}
//Read Txs of Address between from and to //Read Txs of Address
const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) { const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let api = `api/addrs/${addr}/txs`;
//API options //API options
let api_options = []; let query_params = { details: 'txs' };
if (!isUndefined(options.after)) //page options
api_options.push(`after=${options.after}`); if (!isUndefined(options.page) && Number.isInteger(options.page))
else { query_params.page = options.page;
if (!isUndefined(options.from)) if (!isUndefined(options.pageSize) && Number.isInteger(options.pageSize))
api_options.push(`from=${options.from}`); query_params.pageSize = options.pageSize;
if (!isUndefined(options.to)) //only confirmed tx
api_options.push(`to=${options.to}`); if (options.confirmed) //Default is false in server, so only add confirmed filter if confirmed has a true value
} query_params.confirmed = true;
if (!isUndefined(options.mempool))
api_options.push(`mempool=${options.mempool}`) promisedAPI(`api/address/${addr}`, query_params).then(response => {
if (api_options.length) if (!Array.isArray(response.txs)) //set empty array if address doesnt have any tx
api += "?" + api_options.join('&'); response.txs = [];
promisedAPI(api) resolve(response)
.then(response => resolve(response)) }).catch(error => reject(error))
.catch(error => reject(error))
}); });
} }
//Read All Txs of Address (newest first) //backward support (floBlockchainAPI < v2.5.6)
const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options) { function readAllTxs_oldSupport(addr, options, ignoreOld = 0, cacheTotal = 0) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
readTxs(addr, options).then(response => { readTxs(addr, options).then(response => {
if (response.incomplete) { cacheTotal += response.txs.length;
let next_options = Object.assign({}, options); let n_remaining = response.txApperances - cacheTotal
next_options.after = response.lastItem; if (n_remaining < ignoreOld) { // must remove tx that would have been fetch during prev call
readAllTxs(addr, next_options).then(r => { let n_remove = ignoreOld - n_remaining;
r.items = r.items.concat(response.items); //latest tx are 1st in array resolve(response.txs.slice(0, -n_remove));
resolve(r); } else if (response.page == response.totalPages) //last page reached
}).catch(error => reject(error)) resolve(response.txs);
} else else {
options.page = response.page + 1;
readAllTxs_oldSupport(addr, options, ignoreOld, cacheTotal)
.then(result => resolve(response.txs.concat(result)))
.catch(error => reject(error))
}
}).catch(error => reject(error))
})
}
function readAllTxs_new(addr, options, lastItem) {
return new Promise((resolve, reject) => {
readTxs(addr, options).then(response => {
let i = response.txs.findIndex(t => t.txid === lastItem);
if (i != -1) //found lastItem
resolve(response.txs.slice(0, i))
else if (response.page == response.totalPages) //last page reached
resolve(response.txs);
else {
options.page = response.page + 1;
readAllTxs_new(addr, options, lastItem)
.then(result => resolve(response.txs.concat(result)))
.catch(error => reject(error))
}
}).catch(error => reject(error))
})
}
//Read All Txs of Address (newest first)
const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options = {}) {
return new Promise((resolve, reject) => {
if (Number.isInteger(options.ignoreOld)) //backward support: data from floBlockchainAPI < v2.5.6
readAllTxs_oldSupport(addr, options, options.ignoreOld).then(txs => {
let last_tx = txs.find(t => t.confirmations > 0);
let new_lastItem = last_tx ? last_tx.txid : options.ignoreOld;
resolve({ resolve({
lastKey: response.lastItem || options.after, lastItem: new_lastItem,
items: response.items items: txs
}); })
})
}); }).catch(error => reject(error))
else //New format for floBlockchainAPI >= v2.5.6
readAllTxs_new(addr, options, options.after).then(txs => {
let last_tx = txs.find(t => t.confirmations > 0);
let new_lastItem = last_tx ? last_tx.txid : options.after;
resolve({
lastItem: new_lastItem,
items: txs
})
}).catch(error => reject(error))
})
} }
/*Read flo Data from txs of given Address /*Read flo Data from txs of given Address
options can be used to filter data options can be used to filter data
after : query after the given txid after : query after the given txid
mempool : query mempool tx or not (options same as readAllTx, DEFAULT=false: ignore unconfirmed tx) confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx)
ignoreOld : ignore old txs (deprecated: support for backward compatibility only, cannot be used with 'after') ignoreOld : ignore old txs (deprecated: support for backward compatibility only, cannot be used with 'after')
sentOnly : filters only sent data sentOnly : filters only sent data
receivedOnly: filters only received data receivedOnly: filters only received data
@ -808,18 +891,15 @@
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
//fetch options //fetch options
let fetch_options = {}; let query_options = {};
fetch_options.mempool = isUndefined(options.mempool) ? 'false' : options.mempool; //DEFAULT: ignore unconfirmed tx query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: ignore unconfirmed tx
if (!isUndefined(options.after)) {
if (!isUndefined(options.ignoreOld)) //Backward support
return reject("Invalid options: cannot use after and ignoreOld in same query");
else
fetch_options.after = options.after;
}
readAllTxs(addr, fetch_options).then(response => {
if (Number.isInteger(options.ignoreOld)) //backward support, cannot be used with options.after if (!isUndefined(options.after))
response.items.splice(-options.ignoreOld); //negative to count from end of the array query_options.after = options.after;
else if (!isUndefined(options.ignoreOld))
query_options.ignoreOld = options.ignoreOld;
readAllTxs(addr, query_options).then(response => {
if (typeof options.senders === "string") options.senders = [options.senders]; if (typeof options.senders === "string") options.senders = [options.senders];
if (typeof options.receivers === "string") options.receivers = [options.receivers]; if (typeof options.receivers === "string") options.receivers = [options.receivers];
@ -830,9 +910,9 @@
if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query
return false; return false;
if (options.sentOnly && !tx.vin.some(vin => vin.addr === addr)) if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr))
return false; return false;
else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addr))) else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0])))
return false; return false;
if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr))
@ -858,12 +938,12 @@
txid: tx.txid, txid: tx.txid,
time: tx.time, time: tx.time,
blockheight: tx.blockheight, blockheight: tx.blockheight,
senders: new Set(tx.vin.map(v => v.addr)), senders: new Set(tx.vin.map(v => v.addresses[0])),
receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])), receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])),
data: tx.floData data: tx.floData
} : tx.floData); } : tx.floData);
const result = { lastKey: response.lastKey }; const result = { lastItem: response.lastItem };
if (options.tx) if (options.tx)
result.items = filteredData; result.items = filteredData;
else else
@ -874,4 +954,91 @@
}) })
} }
/*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
confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_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 = {};
query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: confirmed tx only
if (!isUndefined(options.page))
query_options.page = options.page;
//if (!isUndefined(options.after)) query_options.after = options.after;
let new_lastItem;
readTxs(addr, query_options).then(response => {
//lastItem confirmed tx checked
if (!new_lastItem) {
let last_tx = response.items.find(t => t.confirmations > 0);
if (last_tx)
new_lastItem = last_tx.txid;
}
if (typeof options.senders === "string") options.senders = [options.senders];
if (typeof options.receivers === "string") options.receivers = [options.receivers];
//check if `after` txid is in the response
let i_after = response.txs.findIndex(t => t.txid === options.after);
if (i_after != -1) //found lastItem, hence remove it and all txs before that
response.items.splice(i_after);
var item = response.items.find(tx => {
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.addresses[0] === addr))
return false;
else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0])))
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: new_lastItem || item.txid };
if (options.tx) {
result.item = {
txid: item.txid,
time: item.time,
blockheight: item.blockheight,
senders: new Set(item.vin.map(v => v.addresses[0])),
receivers: new Set(item.vout.map(v => v.scriptPubKey.addresses[0])),
data: item.floData
}
} else
result.data = item.floData;
return resolve(result);
}
if (response.page == response.totalPages || i_after != -1) //reached last page to check
resolve({ lastItem: new_lastItem || options.after }); //no data match the caseFn, resolve just the lastItem
//else if address needs chain query
else {
options.page = response.page + 1;
getLatestData(addr, caseFn, options)
.then(result => resolve(result))
.catch(error => reject(error))
}
}).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) { //floCloudAPI v2.4.3 (function (EXPORTS) { //floCloudAPI v2.4.3a
/* FLO Cloud operations to send/request application data*/ /* FLO Cloud operations to send/request application data*/
'use strict'; 'use strict';
const floCloudAPI = EXPORTS; const floCloudAPI = EXPORTS;
@ -8,6 +8,7 @@
SNStorageID: floGlobals.SNStorageID || "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk", SNStorageID: floGlobals.SNStorageID || "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk",
adminID: floGlobals.adminID, adminID: floGlobals.adminID,
application: floGlobals.application, application: floGlobals.application,
SNStorageName: "SuperNodeStorage",
callback: (d, e) => console.debug(d, e) callback: (d, e) => console.debug(d, e)
}; };
@ -58,6 +59,9 @@
SNStorageID: { SNStorageID: {
get: () => DEFAULT.SNStorageID get: () => DEFAULT.SNStorageID
}, },
SNStorageName: {
get: () => DEFAULT.SNStorageName
},
adminID: { adminID: {
get: () => DEFAULT.adminID get: () => DEFAULT.adminID
}, },

View File

@ -1,4 +1,4 @@
(function (EXPORTS) { //floDapps v2.3.4 (function (EXPORTS) { //floDapps v2.4.0
/* General functions for FLO Dapps*/ /* General functions for FLO Dapps*/
'use strict'; 'use strict';
const floDapps = EXPORTS; const floDapps = EXPORTS;
@ -144,11 +144,14 @@
} }
}); });
var subAdmins, settings var subAdmins, trustedIDs, settings;
Object.defineProperties(floGlobals, { Object.defineProperties(floGlobals, {
subAdmins: { subAdmins: {
get: () => subAdmins get: () => subAdmins
}, },
trustedIDs: {
get: () => trustedIDs
},
settings: { settings: {
get: () => settings get: () => settings
}, },
@ -255,13 +258,15 @@
if (!startUpOptions.cloud) if (!startUpOptions.cloud)
return resolve("No cloud for this app"); return resolve("No cloud for this app");
compactIDB.readData("lastTx", floCloudAPI.SNStorageID, DEFAULT.root).then(lastTx => { compactIDB.readData("lastTx", floCloudAPI.SNStorageID, DEFAULT.root).then(lastTx => {
floBlockchainAPI.readData(floCloudAPI.SNStorageID, { var query_options = { sentOnly: true, pattern: floCloudAPI.SNStorageName };
ignoreOld: lastTx, if (typeof lastTx == 'number') //lastTx is tx count (*backward support)
sentOnly: true, query_options.ignoreOld = lastTx;
pattern: "SuperNodeStorage" else if (typeof lastTx == 'string') //lastTx is txid of last tx
}).then(result => { query_options.after = lastTx;
//fetch data from flosight
floBlockchainAPI.readData(floCloudAPI.SNStorageID, query_options).then(result => {
for (var i = result.data.length - 1; i >= 0; i--) { for (var i = result.data.length - 1; i >= 0; i--) {
var content = JSON.parse(result.data[i]).SuperNodeStorage; var content = JSON.parse(result.data[i])[floCloudAPI.SNStorageName];
for (let sn in content.removeNodes) for (let sn in content.removeNodes)
compactIDB.removeData("supernodes", sn, DEFAULT.root); compactIDB.removeData("supernodes", sn, DEFAULT.root);
for (let sn in content.newNodes) for (let sn in content.newNodes)
@ -273,7 +278,7 @@
compactIDB.writeData("supernodes", r, sn, DEFAULT.root); compactIDB.writeData("supernodes", r, sn, DEFAULT.root);
}); });
} }
compactIDB.writeData("lastTx", result.totalTxs, floCloudAPI.SNStorageID, DEFAULT.root); compactIDB.writeData("lastTx", result.lastItem, floCloudAPI.SNStorageID, DEFAULT.root);
compactIDB.readAllData("supernodes", DEFAULT.root).then(nodes => { compactIDB.readAllData("supernodes", DEFAULT.root).then(nodes => {
floCloudAPI.init(nodes) floCloudAPI.init(nodes)
.then(result => resolve("Loaded Supernode list\n" + result)) .then(result => resolve("Loaded Supernode list\n" + result))
@ -289,11 +294,13 @@
if (!startUpOptions.app_config) if (!startUpOptions.app_config)
return resolve("No configs for this app"); return resolve("No configs for this app");
compactIDB.readData("lastTx", `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root).then(lastTx => { compactIDB.readData("lastTx", `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root).then(lastTx => {
floBlockchainAPI.readData(DEFAULT.adminID, { var query_options = { sentOnly: true, pattern: DEFAULT.application };
ignoreOld: lastTx, if (typeof lastTx == 'number') //lastTx is tx count (*backward support)
sentOnly: true, query_options.ignoreOld = lastTx;
pattern: DEFAULT.application else if (typeof lastTx == 'string') //lastTx is txid of last tx
}).then(result => { query_options.after = lastTx;
//fetch data from flosight
floBlockchainAPI.readData(DEFAULT.adminID, query_options).then(result => {
for (var i = result.data.length - 1; i >= 0; i--) { for (var i = result.data.length - 1; i >= 0; i--) {
var content = JSON.parse(result.data[i])[DEFAULT.application]; var content = JSON.parse(result.data[i])[DEFAULT.application];
if (!content || typeof content !== "object") if (!content || typeof content !== "object")
@ -314,12 +321,15 @@
for (let l in content.settings) for (let l in content.settings)
compactIDB.writeData("settings", content.settings[l], l) compactIDB.writeData("settings", content.settings[l], l)
} }
compactIDB.writeData("lastTx", result.totalTxs, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root); compactIDB.writeData("lastTx", result.lastItem, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root);
compactIDB.readAllData("subAdmins").then(result => { compactIDB.readAllData("subAdmins").then(result => {
subAdmins = Object.keys(result); subAdmins = Object.keys(result);
compactIDB.readAllData("settings").then(result => { compactIDB.readAllData("trustedIDs").then(result => {
settings = result; trustedIDs = Object.keys(result);
resolve("Read app configuration from blockchain"); compactIDB.readAllData("settings").then(result => {
settings = result;
resolve("Read app configuration from blockchain");
})
}) })
}) })
}) })

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
(function (EXPORTS) { //floExchangeAPI v1.2.0 (function (EXPORTS) { //floExchangeAPI v1.2.0a
const exchangeAPI = EXPORTS; const exchangeAPI = EXPORTS;
const DEFAULT = { const DEFAULT = {
@ -1731,19 +1731,20 @@
if (typeof nodes !== 'object' || nodes === null) if (typeof nodes !== 'object' || nodes === null)
throw Error('nodes must be an object') throw Error('nodes must be an object')
else else
lastTx = parseInt(localStorage.getItem(_l('lastTx'))) || 0; lastTx = localStorage.getItem(_l('lastTx'));
} catch (error) { } catch (error) {
nodes = {}; nodes = {};
trusted = new Set(); trusted = new Set();
assets = new Set(); assets = new Set();
tags = new Set(); tags = new Set();
lastTx = 0;
} }
floBlockchainAPI.readData(DEFAULT.marketID, {
ignoreOld: lastTx, var query_options = { sentOnly: true, pattern: DEFAULT.marketApp };
sentOnly: true, if (typeof lastTx == 'string' && /^[0-9a-f]{64}/i.test(lastTx))//lastTx is txid of last tx
pattern: DEFAULT.marketApp query_options.after = lastTx;
}).then(result => { else if (!isNaN(lastTx))//lastTx is tx count (*backward support)
query_options.ignoreOld = parseInt(lastTx);
floBlockchainAPI.readData(DEFAULT.marketID, query_options).then(result => {
result.data.reverse().forEach(data => { result.data.reverse().forEach(data => {
var content = JSON.parse(data)[DEFAULT.marketApp]; var content = JSON.parse(data)[DEFAULT.marketApp];
//Node List //Node List
@ -1782,7 +1783,7 @@
tags.add(t); tags.add(t);
} }
}); });
localStorage.setItem(_l('lastTx'), result.totalTxs); localStorage.setItem(_l('lastTx'), result.lastItem);
localStorage.setItem(_l('nodes'), JSON.stringify(nodes)); localStorage.setItem(_l('nodes'), JSON.stringify(nodes));
localStorage.setItem(_l('trusted'), Array.from(trusted).join(",")); localStorage.setItem(_l('trusted'), Array.from(trusted).join(","));
localStorage.setItem(_l('assets'), Array.from(assets).join(",")); localStorage.setItem(_l('assets'), Array.from(assets).join(","));
@ -1910,4 +1911,4 @@
} }
} }
})('object' === typeof module ? module.exports : window.floExchangeAPI = {}); })('object' === typeof module ? module.exports : window.floExchangeAPI = {});

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}`)