diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..168b657
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.tmp*
\ No newline at end of file
diff --git a/LICENCE b/LICENCE
new file mode 100644
index 0000000..d3f4c4c
--- /dev/null
+++ b/LICENCE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Sai Raj
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 48c4363..339f67f 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-# rupee-token
\ No newline at end of file
+# rupee-token
diff --git a/old_index.html b/old_index.html
deleted file mode 100644
index 84cbc1b..0000000
--- a/old_index.html
+++ /dev/null
@@ -1,14749 +0,0 @@
-
-
-
-
-
-
- RanchiMall Pay
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cancel
- OK
-
-
-
- Some input required
-
-
- Cancel
- OK
-
-
-
-
-
-
-
- Loader
-
-
- Loading RanchiMall Pay
- Sign Out
-
-
-
-
-
RanchiMall Pay
- Send & request money using UPI on Blockchain
-
-
-
- Sign In
- Sign Up
-
-
-
- Welcome back
- Just enter your FLO private key to continue.
-
-
-
- Get started
- Create your FLO public and private key pair. Don't forget to store them
- securely. Once lost, private key can't be recovered.
- Get FLO credentials
-
-
- FLO ID (User ID)
-
- Private key (Password)
-
- Sign in with this private key
-
-
-
-
-
-
-
-
-
-
- Add balance to your RanchiMall pay wallet.
-
-
-
- How to deposit money?
-
- Step 1
- Copy UPI address shown below.
- Step 2
- Open any UPI app of your preference and send money to copied UPI address. (Do not close this
- browser)
- Step 3
- Copy the UPI transaction ID shown after transaction was successful.
- Step 4
- Enter the amount you sent to copied address
- Step 5
- Then enter the UPI transaction ID you copied when was transaction completed.
- Step 6
- Press Deposit button
-
- Send money to UPI ID below.
-
-
Loading cashier UPI...
-
- Copy
-
-
-
-
-
-
- rupee-symbol
-
-
-
-
-
-
-
-
- Transfer wallet balance to your bank account using specified UPI address.
-
-
-
- How to withdraw rupee?
-
- Step 1
- Enter amount you want to withdraw
- Step 2
- Select UPI address you want to withdraw rupee to. If you haven't added UPI address, add one using
- 'Add UPI address' button.
- Step 3
- Press Withdraw button
-
-
-
- rupee-symbol
-
-
-
-
- Withdraw To
-
- + Add UPI
- address
-
-
-
-
- Request money from others using their FLO address.
-
-
-
- How to request rupee?
-
- Step 1
- Enter amount you want to request
- Step 2
- Enter the FLO address of person you want to request rupee from.
- Step 3
- Press Request button
-
-
-
- rupee-symbol
-
-
-
-
-
-
-
-
-
- What is this?
-
-
- Don't have enough wallet balance? Pay to our cashier through UPI and our cashier will complete that
- transaction on your behalf.
-
-
-
-
- How to pay through cashier?
-
- Step 1
- Copy UPI address shown below.
- Step 2
- Open any UPI app of your preference and send money to copied UPI address. (Do not close this
- browser)
- Step 3
- Enter the amount you sent to copied address
- Step 4
- Then enter the FLO address of person you want send rupee to.
- Step 5
- Select UPI address that you used when sending money copied UPI address. If you haven't added UPI
- address, add one using 'Add UPI address' button.
- Step 6
- Press Pay button
-
- Send money to UPI ID below.
-
-
Loading cashier UPI...
-
- Copy
-
-
-
-
-
-
- rupee-symbol
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- success-art
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Transaction ID
-
-
- Copy
-
-
-
-
- Done
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- RanchiMall
-
-
-
RanchiMall Pay
-
-
-
- qr scanner
-
-
-
-
-
-
-
-
-
-
-
- RanchiMall
-
-
-
RanchiMall Pay
-
-
-
-
-
- request
-
-
-
-
-
requests
-
-
-
-
-
-
-
-
-
-
Complaints
-
-
-
-
-
-
-
Rupee actions
-
-
-
-
- deposit
-
-
-
-
-
-
-
deposit
-
-
-
- withdraw
-
-
-
-
-
-
-
-
Withdraw
-
-
-
- request
-
-
-
-
-
Request
-
-
-
-
-
-
-
pay through cashier
-
-
-
-
-
-
-
-
-
My Balances
-
-
- Refresh
-
-
-
-
-
-
-
-
-
-
Requests
-
- Deposits
- Withdrawals
- Pay Through Cashier
- Unconfirmed
-
-
-
-
- Load pending
-
-
-
-
-
-
-
- Empty icon
-
-
-
-
-
-
-
-
-
-
No deposit requests.
-
-
-
-
- Load pending
-
-
-
-
-
-
-
- Empty icon
-
-
-
-
-
-
-
-
-
-
No withdraw requests.
-
-
-
-
- Load pending
-
-
-
-
-
-
-
-
- Empty icon
-
-
-
-
-
-
-
-
-
-
No pay requests.
-
-
-
-
- Load pending
-
-
-
-
-
-
-
-
- Empty icon
-
-
-
-
-
-
-
-
-
-
No unconfirmed requests.
-
-
-
-
-
-
-
My Balances
-
-
- Refresh
-
-
-
-
-
-
-
-
- Requests
-
- Pending
- Paid
- Declined
-
-
-
-
- You don't have any pending requests.
-
-
-
- No paid requests.
-
-
-
- No declined requests.
-
-
-
-
-
-
Activity
- Refresh
-
-
- Sent
- Received
- Deposits
- Withdrawals
- Paid through cashier
- System Notifications
-
-
-
-
- You haven't sent any rupee yet.
-
-
-
- You haven't sent any rupee yet.
-
-
-
- You haven't deposited rupee yet.
-
-
-
- You haven't withdrawn rupee yet.
-
-
-
- You haven't paid through cashier yet.
-
-
-
- No messages from cashier.
-
-
-
-
-
- Select Cashier
-
-
- Deposit
- Withdraw
- Pay through cashier
-
-
-
-
- No deposit complaints.
-
-
-
- No withdraw complaints.
-
-
-
- No pay through cashier complaints.
-
-
-
-
- Settings
-
- Sign out
- Theme
-
- Haptic feedback
-
-
Toggle haptic feedback. This will turn vibration feedback on/off.
-
-
-
-
My UPI ID
-
-
Add your UPI addresses for withdrawing money from wallet back to your bank account.
-
+ Add UPI address
-
-
-
-
-
-
- Go to activity page
-
-
-
-
-
-
- Report
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/scripts/btcOperator.js b/scripts/btcOperator.js
index ff86fb9..2abb80f 100644
--- a/scripts/btcOperator.js
+++ b/scripts/btcOperator.js
@@ -1,10 +1,13 @@
-(function (EXPORTS) { //btcOperator v1.1.2a
+(function (EXPORTS) { //btcOperator v1.1.3b
/* BTC Crypto and API Operator */
const btcOperator = EXPORTS;
//This library uses API provided by chain.so (https://chain.so/)
const URL = "https://blockchain.info/";
+ const DUST_AMT = 546,
+ MIN_FEE_UPDATE = 219;
+
const fetch_api = btcOperator.fetch = function (api, json_res = true) {
return new Promise((resolve, reject) => {
console.debug(URL + api);
@@ -400,7 +403,12 @@
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_amount = total_amount - (fee_from_receiver ? result.fee : 0);
result.total_size = BASE_TX_SIZE + output_size + result.input_size;
@@ -456,7 +464,7 @@
let size_per_input = _sizePerInput(addr, rs);
fetch_api(`unspent?active=${addr}`).then(result => {
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++) {
if (!utxos[i].confirmations) //ignore unconfirmed utxo
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 = {}) {
return new Promise((resolve, reject) => {
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 => {
let tx = result.transaction;
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());
resolve(result);
}).catch(error => reject(error));
@@ -723,7 +831,7 @@
.catch(error => reject(error))
});
- btcOperator.parseTransaction = function (tx) {
+ const parseTransaction = btcOperator.parseTransaction = function (tx) {
return new Promise((resolve, reject) => {
tx = deserializeTx(tx);
let result = {};
@@ -780,7 +888,7 @@
.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 => {
getLatestBlock().then(latest_block => resolve({
block: result.block_height,
@@ -797,7 +905,7 @@
}).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)
.then(result => resolve(result))
.catch(error => reject(error))
diff --git a/scripts/compactIDB.js b/scripts/compactIDB.js
index 624db0a..ba843ec 100644
--- a/scripts/compactIDB.js
+++ b/scripts/compactIDB.js
@@ -1,4 +1,4 @@
-(function(EXPORTS) { //compactIDB v2.1.0
+(function (EXPORTS) { //compactIDB v2.1.2
/* Compact IndexedDB operations */
'use strict';
const compactIDB = EXPORTS;
@@ -59,7 +59,7 @@
})
}
- compactIDB.initDB = function(dbName, objectStores = {}) {
+ compactIDB.initDB = function (dbName, objectStores = {}) {
return new Promise((resolve, reject) => {
if (!(objectStores instanceof Object))
return reject('ObjectStores must be an object or array')
@@ -87,14 +87,14 @@
resolve("Initiated IndexedDB");
else
upgradeDB(dbName, a_obs, d_obs)
- .then(result => resolve(result))
- .catch(error => reject(error))
+ .then(result => resolve(result))
+ .catch(error => reject(error))
db.close();
}
});
}
- const openDB = compactIDB.openDB = function(dbName = defaultDB) {
+ const openDB = compactIDB.openDB = function (dbName = defaultDB) {
return new Promise((resolve, reject) => {
var idb = indexedDB.open(dbName);
idb.onerror = (event) => reject("Error in opening IndexedDB");
@@ -106,7 +106,7 @@
});
}
- const deleteDB = compactIDB.deleteDB = function(dbName = defaultDB) {
+ const deleteDB = compactIDB.deleteDB = function (dbName = defaultDB) {
return new Promise((resolve, reject) => {
var deleteReq = indexedDB.deleteDatabase(dbName);;
deleteReq.onerror = (event) => reject("Error deleting database!");
@@ -114,7 +114,7 @@
});
}
- compactIDB.writeData = function(obsName, data, key = false, dbName = defaultDB) {
+ compactIDB.writeData = function (obsName, data, key = false, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
@@ -128,7 +128,7 @@
});
}
- compactIDB.addData = function(obsName, data, key = false, dbName = defaultDB) {
+ compactIDB.addData = function (obsName, data, key = false, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
@@ -142,7 +142,7 @@
});
}
- compactIDB.removeData = function(obsName, key, dbName = defaultDB) {
+ compactIDB.removeData = function (obsName, key, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
@@ -156,7 +156,7 @@
});
}
- compactIDB.clearData = function(obsName, dbName = defaultDB) {
+ compactIDB.clearData = function (obsName, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readwrite").objectStore(obsName);
@@ -168,7 +168,7 @@
});
}
- compactIDB.readData = function(obsName, key, dbName = defaultDB) {
+ compactIDB.readData = function (obsName, key, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName);
@@ -182,7 +182,7 @@
});
}
- compactIDB.readAllData = function(obsName, dbName = defaultDB) {
+ compactIDB.readAllData = function (obsName, dbName = defaultDB) {
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
var obs = db.transaction(obsName, "readonly").objectStore(obsName);
@@ -223,13 +223,12 @@
})
}*/
- compactIDB.searchData = function(obsName, options = {}, dbName = defaultDB) {
+ compactIDB.searchData = function (obsName, options = {}, dbName = defaultDB) {
options.lowerKey = options.atKey || options.lowerKey || 0
options.upperKey = options.atKey || options.upperKey || false
- options.patternEval = options.patternEval || ((k, v) => {
- return true
- })
+ options.patternEval = options.patternEval || ((k, v) => true);
options.limit = options.limit || false;
+ options.reverse = options.reverse || false;
options.lastOnly = options.lastOnly || false
return new Promise((resolve, reject) => {
openDB(dbName).then(db => {
@@ -237,17 +236,16 @@
var filteredResult = {}
let curReq = obs.openCursor(
options.upperKey ? IDBKeyRange.bound(options.lowerKey, options.upperKey) : IDBKeyRange.lowerBound(options.lowerKey),
- options.lastOnly ? "prev" : "next");
+ options.lastOnly || options.reverse ? "prev" : "next");
curReq.onsuccess = (evt) => {
var cursor = evt.target.result;
- if (cursor) {
- if (options.patternEval(cursor.primaryKey, cursor.value)) {
- filteredResult[cursor.primaryKey] = cursor.value;
- options.lastOnly ? resolve(filteredResult) : cursor.continue();
- } else
- cursor.continue();
+ if (!cursor || (options.limit && options.limit <= Object.keys(filteredResult).length))
+ return resolve(filteredResult); //reached end of key list or limit reached
+ else if (options.patternEval(cursor.primaryKey, cursor.value)) {
+ filteredResult[cursor.primaryKey] = cursor.value;
+ options.lastOnly ? resolve(filteredResult) : cursor.continue();
} else
- resolve(filteredResult);
+ cursor.continue();
}
curReq.onerror = (evt) => reject(`Search unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`);
db.close();
diff --git a/scripts/floBlockchainAPI.js b/scripts/floBlockchainAPI.js
index 4b13f39..9dedb90 100644
--- a/scripts/floBlockchainAPI.js
+++ b/scripts/floBlockchainAPI.js
@@ -1,13 +1,13 @@
-(function (EXPORTS) { //floBlockchainAPI v2.5.1
- /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/
+(function (EXPORTS) { //floBlockchainAPI v3.0.1b
+ /* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/
'use strict';
const floBlockchainAPI = EXPORTS;
const DEFAULT = {
blockchain: floGlobals.blockchain,
apiURL: {
- FLO: ['https://flosight.duckdns.org/', 'https://flosight.ranchimall.net/'],
- FLO_TEST: ['https://testnet-flosight.duckdns.org', 'https://testnet.flocha.in/']
+ FLO: ['https://blockbook.ranchimall.net/'],
+ FLO_TEST: []
},
sendAmt: 0.0003,
fee: 0.0002,
@@ -16,6 +16,7 @@
};
const SATOSHI_IN_BTC = 1e8;
+ const isUndefined = val => typeof val === 'undefined';
const util = floBlockchainAPI.util = {};
@@ -60,9 +61,9 @@
var serverList = Array.from(allServerList);
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) => {
- let i = serverList.indexOf(rm_flosight)
+ let i = serverList.indexOf(rm_node)
if (i != -1) serverList.splice(i, 1);
curPos = floCrypto.randInt(0, serverList.length - 1);
fetch_api(apicall, false)
@@ -81,19 +82,19 @@
.then(result => resolve(result))
.catch(error => reject(error));
} else
- reject("No floSight server working");
+ reject("No FLO blockbook server working");
} else {
- let flosight = serverList[curPos];
- fetch(flosight + apicall).then(response => {
+ let serverURL = serverList[curPos];
+ fetch(serverURL + apicall).then(response => {
if (response.ok)
response.json().then(data => resolve(data));
else {
- fetch_retry(apicall, flosight)
+ fetch_retry(apicall, serverURL)
.then(result => resolve(result))
.catch(error => reject(error));
}
}).catch(error => {
- fetch_retry(apicall, flosight)
+ fetch_retry(apicall, serverURL)
.then(result => resolve(result))
.catch(error => reject(error));
})
@@ -111,9 +112,11 @@
});
//Promised function to get data from API
- const promisedAPI = floBlockchainAPI.promisedAPI = floBlockchainAPI.fetch = function (apicall) {
+ const promisedAPI = floBlockchainAPI.promisedAPI = floBlockchainAPI.fetch = function (apicall, query_params = undefined) {
return new Promise((resolve, reject) => {
- //console.log(apicall);
+ if (!isUndefined(query_params))
+ apicall += '?' + new URLSearchParams(JSON.parse(JSON.stringify(query_params))).toString();
+ //console.debug(apicall);
fetch_api(apicall)
.then(result => resolve(result))
.catch(error => reject(error));
@@ -121,43 +124,27 @@
}
//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) => {
- let api = `api/addr/${addr}/balance`;
- if (after) {
- if (typeof after === 'string' && /^[0-9a-z]{64}$/i.test(after))
- api += '?after=' + after;
- 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))
+ let api = `api/address/${addr}`;
+ promisedAPI(api, { details: "basic" })
+ .then(result => resolve(result["balance"]))
+ .catch(error => reject(error))
});
}
- const getUTXOs = address => new Promise((resolve, reject) => {
- promisedAPI(`api/addr/${address}/utxo`)
- .then(utxo => resolve(utxo))
- .catch(error => reject(error))
- })
+ function getScriptPubKey(address) {
+ var tx = bitjs.transaction();
+ tx.addoutput(address, 0);
+ let outputBuffer = tx.outputs.pop().script;
+ return Crypto.util.bytesToHex(outputBuffer)
+ }
- const 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);
+ const getUTXOs = address => new Promise((resolve, reject) => {
+ promisedAPI(`api/utxo/${address}`, { confirmed: true }).then(utxos => {
+ let scriptPubKey = getScriptPubKey(address);
+ utxos.forEach(u => u.scriptPubKey = scriptPubKey);
+ resolve(utxos);
}).catch(error => reject(error))
})
@@ -177,32 +164,28 @@
var fee = DEFAULT.fee;
if (balance < sendAmt + fee)
return reject("Insufficient FLO balance!");
- getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => {
- getUTXOs(senderAddr).then(utxos => {
- //form/construct the transaction data
- var trx = bitjs.transaction();
- var utxoAmt = 0.0;
- for (var i = utxos.length - 1;
- (i >= 0) && (utxoAmt < sendAmt + fee); i--) {
- //use only utxos with confirmations (strict_utxo mode)
- if (utxos[i].confirmations || !strict_utxo) {
- if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
- continue; //A transaction has already used the utxo, but is unconfirmed.
- trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
- utxoAmt += utxos[i].amount;
- };
- }
- if (utxoAmt < sendAmt + fee)
- reject("Insufficient FLO: Some UTXOs are unconfirmed");
- else {
- trx.addoutput(receiverAddr, sendAmt);
- var change = utxoAmt - sendAmt - fee;
- if (change > DEFAULT.minChangeAmt)
- trx.addoutput(senderAddr, change);
- trx.addflodata(floData.replace(/\n/g, ' '));
- resolve(trx);
- }
- }).catch(error => reject(error))
+ getUTXOs(senderAddr).then(utxos => {
+ //form/construct the transaction data
+ var trx = bitjs.transaction();
+ var utxoAmt = 0.0;
+ for (var i = utxos.length - 1;
+ (i >= 0) && (utxoAmt < sendAmt + fee); i--) {
+ //use only utxos with confirmations (strict_utxo mode)
+ if (utxos[i].confirmations || !strict_utxo) {
+ 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))
})
@@ -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
* @param {Array} senderPrivKeys List of sender private-keys
* @param {string} data FLO data of the txn
@@ -498,33 +527,29 @@
var fee = DEFAULT.fee;
if (balance < sendAmt + fee)
return reject("Insufficient FLO balance!");
- getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => {
- getUTXOs(senderAddr).then(utxos => {
- //form/construct the transaction data
- var trx = bitjs.transaction();
- var utxoAmt = 0.0;
- for (var i = utxos.length - 1;
- (i >= 0) && (utxoAmt < sendAmt + fee); i--) {
- //use only utxos with confirmations (strict_utxo mode)
- if (utxos[i].confirmations || !strict_utxo) {
- if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
- continue; //A transaction has already used the utxo, but is unconfirmed.
- trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript
- utxoAmt += utxos[i].amount;
- };
- }
- if (utxoAmt < sendAmt + fee)
- reject("Insufficient FLO: Some UTXOs are unconfirmed");
- else {
- for (let i in receivers)
- trx.addoutput(receivers[i], amounts[i]);
- var change = utxoAmt - sendAmt - fee;
- if (change > DEFAULT.minChangeAmt)
- trx.addoutput(senderAddr, change);
- trx.addflodata(floData.replace(/\n/g, ' '));
- resolve(trx);
- }
- }).catch(error => reject(error))
+ getUTXOs(senderAddr).then(utxos => {
+ //form/construct the transaction data
+ var trx = bitjs.transaction();
+ var utxoAmt = 0.0;
+ for (var i = utxos.length - 1;
+ (i >= 0) && (utxoAmt < sendAmt + fee); i--) {
+ //use only utxos with confirmations (strict_utxo mode)
+ if (utxos[i].confirmations || !strict_utxo) {
+ trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript
+ utxoAmt += utxos[i].amount;
+ };
+ }
+ if (utxoAmt < sendAmt + fee)
+ reject("Insufficient FLO: Some UTXOs are unconfirmed");
+ else {
+ for (let i in receivers)
+ trx.addoutput(receivers[i], amounts[i]);
+ var change = utxoAmt - sendAmt - fee;
+ if (change > DEFAULT.minChangeAmt)
+ trx.addoutput(senderAddr, change);
+ trx.addflodata(floData.replace(/\n/g, ' '));
+ resolve(trx);
+ }
}).catch(error => reject(error))
}).catch(error => reject(error))
});
@@ -720,24 +745,15 @@
const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) {
return new Promise((resolve, reject) => {
if (signedTxHash.length < 1)
- return reject("Empty Signature");
- var url = serverList[curPos] + 'api/tx/send';
- fetch(url, {
- method: "POST",
- headers: {
- 'Content-Type': 'application/json'
- },
- body: `{"rawtx":"${signedTxHash}"}`
- }).then(response => {
- if (response.ok)
- response.json().then(data => resolve(data.txid.result));
- else
- response.text().then(data => resolve(data));
- }).catch(error => reject(error));
+ return reject("Empty Transaction Data");
+
+ promisedAPI('/api/sendtx/' + signedTxHash)
+ .then(response => resolve(response["result"]))
+ .catch(error => reject(error))
})
}
- floBlockchainAPI.getTx = function (txid) {
+ const getTx = floBlockchainAPI.getTx = function (txid) {
return new Promise((resolve, reject) => {
promisedAPI(`api/tx/${txid}`)
.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 = {}) {
return new Promise((resolve, reject) => {
- let api = `api/addrs/${addr}/txs`;
//API options
- let api_options = [];
- if (!isUndefined(options.after))
- api_options.push(`after=${options.after}`);
- else {
- if (!isUndefined(options.from))
- api_options.push(`from=${options.from}`);
- if (!isUndefined(options.to))
- api_options.push(`to=${options.to}`);
- }
- if (!isUndefined(options.mempool))
- api_options.push(`mempool=${options.mempool}`)
- if (api_options.length)
- api += "?" + api_options.join('&');
- promisedAPI(api)
- .then(response => resolve(response))
- .catch(error => reject(error))
+ let query_params = { details: 'txs' };
+ //page options
+ if (!isUndefined(options.page) && Number.isInteger(options.page))
+ query_params.page = options.page;
+ if (!isUndefined(options.pageSize) && Number.isInteger(options.pageSize))
+ query_params.pageSize = options.pageSize;
+ //only confirmed tx
+ if (options.confirmed) //Default is false in server, so only add confirmed filter if confirmed has a true value
+ query_params.confirmed = true;
+
+ promisedAPI(`api/address/${addr}`, query_params).then(response => {
+ if (!Array.isArray(response.txs)) //set empty array if address doesnt have any tx
+ response.txs = [];
+ resolve(response)
+ }).catch(error => reject(error))
});
}
- //Read All Txs of Address (newest first)
- const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options) {
+ //backward support (floBlockchainAPI < v2.5.6)
+ function readAllTxs_oldSupport(addr, options, ignoreOld = 0, cacheTotal = 0) {
return new Promise((resolve, reject) => {
readTxs(addr, options).then(response => {
- if (response.incomplete) {
- let next_options = Object.assign({}, options);
- next_options.after = response.lastItem;
- 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
+ cacheTotal += response.txs.length;
+ let n_remaining = response.txApperances - cacheTotal
+ if (n_remaining < ignoreOld) { // must remove tx that would have been fetch during prev call
+ let n_remove = ignoreOld - n_remaining;
+ resolve(response.txs.slice(0, -n_remove));
+ } else if (response.page == response.totalPages) //last page reached
+ resolve(response.txs);
+ else {
+ options.page = response.page + 1;
+ readAllTxs_oldSupport(addr, options, ignoreOld, cacheTotal)
+ .then(result => resolve(response.txs.concat(result)))
+ .catch(error => reject(error))
+ }
+ }).catch(error => reject(error))
+ })
+ }
+
+ function readAllTxs_new(addr, options, lastItem) {
+ return new Promise((resolve, reject) => {
+ readTxs(addr, options).then(response => {
+ let i = response.txs.findIndex(t => t.txid === lastItem);
+ if (i != -1) //found lastItem
+ resolve(response.txs.slice(0, i))
+ else if (response.page == response.totalPages) //last page reached
+ resolve(response.txs);
+ else {
+ options.page = response.page + 1;
+ readAllTxs_new(addr, options, lastItem)
+ .then(result => resolve(response.txs.concat(result)))
+ .catch(error => reject(error))
+ }
+ }).catch(error => reject(error))
+ })
+ }
+
+ //Read All Txs of Address (newest first)
+ 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({
- lastKey: response.lastItem || options.after,
- items: response.items
- });
- })
- });
+ lastItem: new_lastItem,
+ 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
options can be used to filter data
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')
sentOnly : filters only sent data
receivedOnly: filters only received data
@@ -808,18 +891,15 @@
return new Promise((resolve, reject) => {
//fetch options
- let fetch_options = {};
- fetch_options.mempool = isUndefined(options.mempool) ? 'false' : options.mempool; //DEFAULT: ignore unconfirmed tx
- if (!isUndefined(options.after)) {
- 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 => {
+ let query_options = {};
+ query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: ignore unconfirmed tx
- if (Number.isInteger(options.ignoreOld)) //backward support, cannot be used with options.after
- response.items.splice(-options.ignoreOld); //negative to count from end of the array
+ if (!isUndefined(options.after))
+ 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.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
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;
- 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;
if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr))
@@ -858,12 +938,12 @@
txid: tx.txid,
time: tx.time,
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])),
data: tx.floData
} : tx.floData);
- const result = { lastKey: response.lastKey };
+ const result = { lastItem: response.lastItem };
if (options.tx)
result.items = filteredData;
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 = {});
\ No newline at end of file
diff --git a/scripts/floCloudAPI.js b/scripts/floCloudAPI.js
index 921fa69..c2d2c3f 100644
--- a/scripts/floCloudAPI.js
+++ b/scripts/floCloudAPI.js
@@ -1,4 +1,4 @@
-(function (EXPORTS) { //floCloudAPI v2.4.3
+(function (EXPORTS) { //floCloudAPI v2.4.3a
/* FLO Cloud operations to send/request application data*/
'use strict';
const floCloudAPI = EXPORTS;
@@ -8,6 +8,7 @@
SNStorageID: floGlobals.SNStorageID || "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk",
adminID: floGlobals.adminID,
application: floGlobals.application,
+ SNStorageName: "SuperNodeStorage",
callback: (d, e) => console.debug(d, e)
};
@@ -58,6 +59,9 @@
SNStorageID: {
get: () => DEFAULT.SNStorageID
},
+ SNStorageName: {
+ get: () => DEFAULT.SNStorageName
+ },
adminID: {
get: () => DEFAULT.adminID
},
diff --git a/scripts/floDapps.js b/scripts/floDapps.js
index 4668e4e..4d525a2 100644
--- a/scripts/floDapps.js
+++ b/scripts/floDapps.js
@@ -1,4 +1,4 @@
-(function (EXPORTS) { //floDapps v2.3.4
+(function (EXPORTS) { //floDapps v2.4.0
/* General functions for FLO Dapps*/
'use strict';
const floDapps = EXPORTS;
@@ -144,11 +144,14 @@
}
});
- var subAdmins, settings
+ var subAdmins, trustedIDs, settings;
Object.defineProperties(floGlobals, {
subAdmins: {
get: () => subAdmins
},
+ trustedIDs: {
+ get: () => trustedIDs
+ },
settings: {
get: () => settings
},
@@ -255,13 +258,15 @@
if (!startUpOptions.cloud)
return resolve("No cloud for this app");
compactIDB.readData("lastTx", floCloudAPI.SNStorageID, DEFAULT.root).then(lastTx => {
- floBlockchainAPI.readData(floCloudAPI.SNStorageID, {
- ignoreOld: lastTx,
- sentOnly: true,
- pattern: "SuperNodeStorage"
- }).then(result => {
+ var query_options = { sentOnly: true, pattern: floCloudAPI.SNStorageName };
+ if (typeof lastTx == 'number') //lastTx is tx count (*backward support)
+ query_options.ignoreOld = lastTx;
+ else if (typeof lastTx == 'string') //lastTx is txid of last tx
+ query_options.after = lastTx;
+ //fetch data from flosight
+ floBlockchainAPI.readData(floCloudAPI.SNStorageID, query_options).then(result => {
for (var i = result.data.length - 1; i >= 0; i--) {
- var content = JSON.parse(result.data[i]).SuperNodeStorage;
+ var content = JSON.parse(result.data[i])[floCloudAPI.SNStorageName];
for (let sn in content.removeNodes)
compactIDB.removeData("supernodes", sn, DEFAULT.root);
for (let sn in content.newNodes)
@@ -273,7 +278,7 @@
compactIDB.writeData("supernodes", r, sn, DEFAULT.root);
});
}
- compactIDB.writeData("lastTx", result.totalTxs, floCloudAPI.SNStorageID, DEFAULT.root);
+ compactIDB.writeData("lastTx", result.lastItem, floCloudAPI.SNStorageID, DEFAULT.root);
compactIDB.readAllData("supernodes", DEFAULT.root).then(nodes => {
floCloudAPI.init(nodes)
.then(result => resolve("Loaded Supernode list\n" + result))
@@ -289,11 +294,13 @@
if (!startUpOptions.app_config)
return resolve("No configs for this app");
compactIDB.readData("lastTx", `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root).then(lastTx => {
- floBlockchainAPI.readData(DEFAULT.adminID, {
- ignoreOld: lastTx,
- sentOnly: true,
- pattern: DEFAULT.application
- }).then(result => {
+ var query_options = { sentOnly: true, pattern: DEFAULT.application };
+ if (typeof lastTx == 'number') //lastTx is tx count (*backward support)
+ query_options.ignoreOld = lastTx;
+ else if (typeof lastTx == 'string') //lastTx is txid of last tx
+ query_options.after = lastTx;
+ //fetch data from flosight
+ floBlockchainAPI.readData(DEFAULT.adminID, query_options).then(result => {
for (var i = result.data.length - 1; i >= 0; i--) {
var content = JSON.parse(result.data[i])[DEFAULT.application];
if (!content || typeof content !== "object")
@@ -314,12 +321,15 @@
for (let l in content.settings)
compactIDB.writeData("settings", content.settings[l], l)
}
- compactIDB.writeData("lastTx", result.totalTxs, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root);
+ compactIDB.writeData("lastTx", result.lastItem, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root);
compactIDB.readAllData("subAdmins").then(result => {
subAdmins = Object.keys(result);
- compactIDB.readAllData("settings").then(result => {
- settings = result;
- resolve("Read app configuration from blockchain");
+ compactIDB.readAllData("trustedIDs").then(result => {
+ trustedIDs = Object.keys(result);
+ compactIDB.readAllData("settings").then(result => {
+ settings = result;
+ resolve("Read app configuration from blockchain");
+ })
})
})
})
diff --git a/scripts/floExchangeAPI.js b/scripts/floExchangeAPI.js
index 93f261f..685ba12 100644
--- a/scripts/floExchangeAPI.js
+++ b/scripts/floExchangeAPI.js
@@ -1,6 +1,6 @@
'use strict';
-(function (EXPORTS) { //floExchangeAPI v1.2.0
+(function (EXPORTS) { //floExchangeAPI v1.2.0a
const exchangeAPI = EXPORTS;
const DEFAULT = {
@@ -1731,19 +1731,20 @@
if (typeof nodes !== 'object' || nodes === null)
throw Error('nodes must be an object')
else
- lastTx = parseInt(localStorage.getItem(_l('lastTx'))) || 0;
+ lastTx = localStorage.getItem(_l('lastTx'));
} catch (error) {
nodes = {};
trusted = new Set();
assets = new Set();
tags = new Set();
- lastTx = 0;
}
- floBlockchainAPI.readData(DEFAULT.marketID, {
- ignoreOld: lastTx,
- sentOnly: true,
- pattern: DEFAULT.marketApp
- }).then(result => {
+
+ var query_options = { sentOnly: true, pattern: DEFAULT.marketApp };
+ if (typeof lastTx == 'string' && /^[0-9a-f]{64}/i.test(lastTx))//lastTx is txid of last tx
+ query_options.after = lastTx;
+ 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 => {
var content = JSON.parse(data)[DEFAULT.marketApp];
//Node List
@@ -1782,7 +1783,7 @@
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('trusted'), Array.from(trusted).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 = {});
\ No newline at end of file
diff --git a/scripts/floTokenAPI.js b/scripts/floTokenAPI.js
index efbd2ae..2456b88 100644
--- a/scripts/floTokenAPI.js
+++ b/scripts/floTokenAPI.js
@@ -1,4 +1,4 @@
-(function (EXPORTS) { //floTokenAPI v1.0.3c
+(function (EXPORTS) { //floTokenAPI v1.0.4a
/* Token Operator to send/receive tokens via blockchain using API calls*/
'use strict';
const tokenAPI = EXPORTS;
@@ -77,6 +77,70 @@
});
}
+ function sendTokens_raw(privKey, receiverID, token, amount, utxo, vout, scriptPubKey) {
+ return new Promise((resolve, reject) => {
+ var trx = bitjs.transaction();
+ trx.addinput(utxo, vout, scriptPubKey)
+ trx.addoutput(receiverID, floBlockchainAPI.sendAmt);
+ trx.addflodata(`send ${amount} ${token}#`);
+ var signedTxHash = trx.sign(privKey, 1);
+ floBlockchainAPI.broadcastTx(signedTxHash)
+ .then(txid => resolve([receiverID, txid]))
+ .catch(error => reject([receiverID, error]))
+ })
+ }
+
+ //bulk transfer tokens
+ tokenAPI.bulkTransferTokens = function (sender, privKey, token, receivers) {
+ return new Promise((resolve, reject) => {
+ if (typeof receivers !== 'object')
+ return reject("receivers must be object in format {receiver1: amount1, receiver2:amount2...}")
+
+ let receiver_list = Object.keys(receivers), amount_list = Object.values(receivers);
+ let invalidReceivers = receiver_list.filter(id => !floCrypto.validateFloID(id));
+ let invalidAmount = amount_list.filter(val => typeof val !== 'number' || val <= 0);
+ if (invalidReceivers.length)
+ return reject(`Invalid receivers: ${invalidReceivers}`);
+ else if (invalidAmount.length)
+ return reject(`Invalid amounts: ${invalidAmount}`);
+
+ if (receiver_list.length == 0)
+ return reject("Receivers cannot be empty");
+
+ if (receiver_list.length == 1) {
+ let receiver = receiver_list[0], amount = amount_list[0];
+ floTokenAPI.sendToken(privKey, amount, receiver, "", token)
+ .then(txid => resolve({ success: { [receiver]: txid } }))
+ .catch(error => reject(error))
+ } else {
+ //check for token balance
+ floTokenAPI.getBalance(sender, token).then(token_balance => {
+ let total_token_amout = amount_list.reduce((a, e) => a + e, 0);
+ if (total_token_amout > token_balance)
+ return reject(`Insufficient ${token}# balance`);
+
+ //split utxos
+ floBlockchainAPI.splitUTXOs(sender, privKey, receiver_list.length).then(split_txid => {
+ //wait for the split utxo to get confirmation
+ floBlockchainAPI.waitForConfirmation(split_txid).then(split_tx => {
+ //send tokens using the split-utxo
+ var scriptPubKey = split_tx.vout[0].scriptPubKey.hex;
+ let promises = [];
+ for (let i in receiver_list)
+ promises.push(sendTokens_raw(privKey, receiver_list[i], token, amount_list[i], split_txid, i, scriptPubKey));
+ Promise.allSettled(promises).then(results => {
+ let success = Object.fromEntries(results.filter(r => r.status == 'fulfilled').map(r => r.value));
+ let failed = Object.fromEntries(results.filter(r => r.status == 'rejected').map(r => r.reason));
+ resolve({ success, failed });
+ })
+ }).catch(error => reject(error))
+ }).catch(error => reject(error))
+ }).catch(error => reject(error))
+ }
+
+ })
+ }
+
tokenAPI.getAllTxs = function (floID, token = DEFAULT.currency) {
return new Promise((resolve, reject) => {
fetch_api(`api/v1.0/getFloAddressTransactions?token=${token}&floAddress=${floID}`)