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 -
-
- -
- - close - - - -

-

-
- Back -
- Next -
-
- -
- - 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)
-
-

- - Copy - - - -
-
Private key (Password)
-
-

- - Copy - - - -
- Sign in with this private key -
-
-
-
-
- - - - -
-

Point your camera towards QR Code.
- *Only works with RanchiMall Pay. -

-
- -
-

Show this QR Code to receive money using RanchiMall Pay

- - Download my QR code - -
-
-
- - -

- Send wallet money to any FLO address. -

-
- - How to send rupee? - -
Step 1
-

Enter receiver's FLO address.

-
Step 2
-

Enter the amount you want to send.

-
Step 3
-

Press Send button.

-
- - - rupee-symbol - - - - -
- - -

- 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 - - - - - - - - - - - - - - - - - - - - - -
-
- - failure - - - - -
-

-

-
-
Transaction ID
-

- - Copy - - - -
- Done -
- - - - - - - - - - - - - - - - - -
-

-

-
- - - - - remove - - - - -
-
-
FLO ID
-
-

- - Copy - - - -
-
-
- - - -

Send rupee

-
-
- - request - - - - -

Request rupee

-
-
-
- - - - - - - -
- -
- - qr scanner - - - - - - -
-
- -
-
-
-

Rupee actions

-
-
- - - -

Send

-
-
- - deposit - - - - - - -

deposit

-
-
- - withdraw - - - - - - - -

Withdraw

-
-
- - request - - - - -

Request

-
-
- - - - -

pay through cashier

- -
-
-
-

Contacts

-
-
-
-

S

-

Sairaj Mote

-
-
-
-
-
-

My Balances

- -
-
-
-
-
?
-

Your wallet balance

-
-
Wallet
-

0

-
-
-
-
?
-

Money that is in-process after deposit.

-
-
In-process
-

0

-
-
-
-
?
-

Your FLO balance. This is required for every transaction.

-
-
FLO
-

0

-
-
-
-
-
-
-

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

- -
-
-
-
-
?
-

Your wallet balance

-
-
Wallet
-

0

-
-
-
-
?
-

Your FLO balance. This is required for every transaction.

-
-
FLO
-

0

-
-
-
-
-
-

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.

-
-
-
-
-
-

Complaints

-

-
Select Cashier
- - - Deposit - Withdraw - Pay through cashier - - - -
-

No deposit complaints.

-
- -
-

No withdraw complaints.

-
- -
-

No pay through cashier complaints.

-
-
-
-
-

Settings

-
-

My FLO address

-
-

- - Copy - - - -
-
- Sign out -

Theme

-
-

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