diff --git a/btcOperator.js b/btcOperator.js new file mode 100644 index 0000000..2abb80f --- /dev/null +++ b/btcOperator.js @@ -0,0 +1,996 @@ +(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); + fetch(URL + api).then(response => { + if (response.ok) { + (json_res ? response.json() : response.text()) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + response.json() + .then(result => reject(result)) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + }; + + const SATOSHI_IN_BTC = 1e8; + + const util = btcOperator.util = {}; + + util.Sat_to_BTC = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8)); + util.BTC_to_Sat = value => parseInt(value * SATOSHI_IN_BTC); + + function get_fee_rate() { + return new Promise((resolve, reject) => { + fetch('https://api.blockchain.info/mempool/fees').then(response => { + if (response.ok) + response.json() + .then(result => resolve(util.Sat_to_BTC(result.regular))) + .catch(error => reject(error)); + else + reject(response); + }).catch(error => reject(error)) + }) + } + + const broadcastTx = btcOperator.broadcastTx = rawTxHex => new Promise((resolve, reject) => { + let url = 'https://coinb.in/api/?uid=1&key=12345678901234567890123456789012&setmodule=bitcoin&request=sendrawtransaction'; + fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: "rawtx=" + rawTxHex + }).then(response => { + response.text().then(resultText => { + let r = resultText.match(/.*<\/result>/); + if (!r) + reject(resultText); + else { + r = r.pop().replace('', '').replace('', ''); + if (r == '1') { + let txid = resultText.match(/.*<\/txid>/).pop().replace('', '').replace('', ''); + resolve(txid); + } else if (r == '0') { + let error = resultText.match(/.*<\/response>/).pop().replace('', '').replace('', ''); + reject(decodeURIComponent(error.replace(/\+/g, " "))); + } else reject(resultText); + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }); + + Object.defineProperties(btcOperator, { + newKeys: { + get: () => { + let r = coinjs.newKeys(); + r.segwitAddress = coinjs.segwitAddress(r.pubkey).address; + r.bech32Address = coinjs.bech32Address(r.pubkey).address; + return r; + } + }, + pubkey: { + value: key => key.length >= 66 ? key : (key.length == 64 ? coinjs.newPubkey(key) : coinjs.wif2pubkey(key).pubkey) + }, + address: { + value: (key, prefix = undefined) => coinjs.pubkey2address(btcOperator.pubkey(key), prefix) + }, + segwitAddress: { + value: key => coinjs.segwitAddress(btcOperator.pubkey(key)).address + }, + bech32Address: { + value: key => coinjs.bech32Address(btcOperator.pubkey(key)).address + } + }); + + coinjs.compressed = true; + + const verifyKey = btcOperator.verifyKey = function (addr, key) { + if (!addr || !key) + return undefined; + switch (coinjs.addressDecode(addr).type) { + case "standard": + return btcOperator.address(key) === addr; + case "multisig": + return btcOperator.segwitAddress(key) === addr; + case "bech32": + return btcOperator.bech32Address(key) === addr; + default: + return null; + } + } + + const validateAddress = btcOperator.validateAddress = function (addr) { + if (!addr) + return undefined; + let type = coinjs.addressDecode(addr).type; + if (["standard", "multisig", "bech32", "multisigBech32"].includes(type)) + return type; + else + return false; + } + + btcOperator.multiSigAddress = function (pubKeys, minRequired, bech32 = true) { + if (!Array.isArray(pubKeys)) + throw "pubKeys must be an array of public keys"; + else if (pubKeys.length < minRequired) + throw "minimum required should be less than the number of pubKeys"; + if (bech32) + return coinjs.pubkeys2MultisigAddressBech32(pubKeys, minRequired); + else + return coinjs.pubkeys2MultisigAddress(pubKeys, minRequired); + } + + btcOperator.decodeRedeemScript = function (redeemScript, bech32 = true) { + let script = coinjs.script(); + let decoded = (bech32) ? + script.decodeRedeemScriptBech32(redeemScript) : + script.decodeRedeemScript(redeemScript); + if (!decoded) + return null; + return { + address: decoded.address, + pubKeys: decoded.pubkeys, + redeemScript: decoded.redeemscript, + required: decoded.signaturesRequired + } + + } + + //convert from one blockchain to another blockchain (target version) + btcOperator.convert = {}; + + btcOperator.convert.wif = function (source_wif, target_version = coinjs.priv) { + let keyHex = util.decodeLegacy(source_wif).hex; + if (!keyHex || keyHex.length < 66 || !/01$/.test(keyHex)) + return null; + else + return util.encodeLegacy(keyHex, target_version); + } + + btcOperator.convert.legacy2legacy = function (source_addr, target_version = coinjs.pub) { + let rawHex = util.decodeLegacy(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeLegacy(rawHex, target_version); + } + + btcOperator.convert.legacy2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) { + let rawHex = util.decodeLegacy(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeBech32(rawHex, target_version, target_hrp); + } + + btcOperator.convert.bech2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) { + let rawHex = util.decodeBech32(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeBech32(rawHex, target_version, target_hrp); + } + + btcOperator.convert.bech2legacy = function (source_addr, target_version = coinjs.pub) { + let rawHex = util.decodeBech32(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeLegacy(rawHex, target_version); + } + + btcOperator.convert.multisig2multisig = function (source_addr, target_version = coinjs.multisig) { + let rawHex = util.decodeLegacy(source_addr).hex; + if (!rawHex) + return null; + else + return util.encodeLegacy(rawHex, target_version); + } + + btcOperator.convert.bech2multisig = function (source_addr, target_version = coinjs.multisig) { + let rawHex = util.decodeBech32(source_addr).hex; + if (!rawHex) + return null; + else { + rawHex = Crypto.util.bytesToHex(ripemd160(Crypto.util.hexToBytes(rawHex), { asBytes: true })); + return util.encodeLegacy(rawHex, target_version); + } + } + + util.decodeLegacy = function (source) { + var decode = coinjs.base58decode(source); + var raw = decode.slice(0, decode.length - 4), + checksum = decode.slice(decode.length - 4); + var hash = Crypto.SHA256(Crypto.SHA256(raw, { + asBytes: true + }), { + asBytes: true + }); + if (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3]) + return false; + let version = raw.shift(); + return { + version: version, + hex: Crypto.util.bytesToHex(raw) + } + } + + util.encodeLegacy = function (hex, version) { + var bytes = Crypto.util.hexToBytes(hex); + bytes.unshift(version); + var hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + return coinjs.base58encode(bytes.concat(checksum)); + } + + util.decodeBech32 = function (source) { + let decode = coinjs.bech32_decode(source); + if (!decode) + return false; + var raw = decode.data; + let version = raw.shift(); + raw = coinjs.bech32_convert(raw, 5, 8, false); + return { + hrp: decode.hrp, + version: version, + hex: Crypto.util.bytesToHex(raw) + } + } + + util.encodeBech32 = function (hex, version, hrp) { + var bytes = Crypto.util.hexToBytes(hex); + bytes = coinjs.bech32_convert(bytes, 8, 5, true); + bytes.unshift(version) + return coinjs.bech32_encode(hrp, bytes); + } + + //BTC blockchain APIs + + btcOperator.getBalance = addr => new Promise((resolve, reject) => { + fetch_api(`q/addressbalance/${addr}`) + .then(result => resolve(util.Sat_to_BTC(result))) + .catch(error => reject(error)) + }); + + const BASE_TX_SIZE = 12, + BASE_INPUT_SIZE = 41, + LEGACY_INPUT_SIZE = 107, + BECH32_INPUT_SIZE = 27, + BECH32_MULTISIG_INPUT_SIZE = 35, + SEGWIT_INPUT_SIZE = 59, + MULTISIG_INPUT_SIZE_ES = 351, + BASE_OUTPUT_SIZE = 9, + LEGACY_OUTPUT_SIZE = 25, + BECH32_OUTPUT_SIZE = 23, + BECH32_MULTISIG_OUTPUT_SIZE = 34, + SEGWIT_OUTPUT_SIZE = 23; + + function _redeemScript(addr, key) { + let decode = coinjs.addressDecode(addr); + switch (decode.type) { + case "standard": + return false; + case "multisig": + return key ? coinjs.segwitAddress(btcOperator.pubkey(key)).redeemscript : null; + case "bech32": + return decode.redeemscript; + default: + return null; + } + } + + function _sizePerInput(addr, rs) { + switch (coinjs.addressDecode(addr).type) { + case "standard": + return BASE_INPUT_SIZE + LEGACY_INPUT_SIZE; + case "bech32": + return BASE_INPUT_SIZE + BECH32_INPUT_SIZE; + case "multisigBech32": + return BASE_INPUT_SIZE + BECH32_MULTISIG_INPUT_SIZE; + case "multisig": + switch (coinjs.script().decodeRedeemScript(rs).type) { + case "segwit__": + return BASE_INPUT_SIZE + SEGWIT_INPUT_SIZE; + case "multisig__": + return BASE_INPUT_SIZE + MULTISIG_INPUT_SIZE_ES; + default: + return null; + }; + default: + return null; + } + } + + function _sizePerOutput(addr) { + switch (coinjs.addressDecode(addr).type) { + case "standard": + return BASE_OUTPUT_SIZE + LEGACY_OUTPUT_SIZE; + case "bech32": + return BASE_OUTPUT_SIZE + BECH32_OUTPUT_SIZE; + case "multisigBech32": + return BASE_OUTPUT_SIZE + BECH32_MULTISIG_OUTPUT_SIZE; + case "multisig": + return BASE_OUTPUT_SIZE + SEGWIT_OUTPUT_SIZE; + default: + return null; + } + } + + function validateTxParameters(parameters) { + let invalids = []; + //sender-ids + if (parameters.senders) { + if (!Array.isArray(parameters.senders)) + parameters.senders = [parameters.senders]; + parameters.senders.forEach(id => !validateAddress(id) ? invalids.push(id) : null); + if (invalids.length) + throw "Invalid senders:" + invalids; + } + if (parameters.privkeys) { + if (!Array.isArray(parameters.privkeys)) + parameters.privkeys = [parameters.privkeys]; + if (parameters.senders.length != parameters.privkeys.length) + throw "Array length for senders and privkeys should be equal"; + parameters.senders.forEach((id, i) => { + let key = parameters.privkeys[i]; + if (!verifyKey(id, key)) //verify private-key + invalids.push(id); + if (key.length === 64) //convert Hex to WIF if needed + parameters.privkeys[i] = coinjs.privkey2wif(key); + }); + if (invalids.length) + throw "Invalid private key for address:" + invalids; + } + //receiver-ids (and change-id) + if (!Array.isArray(parameters.receivers)) + parameters.receivers = [parameters.receivers]; + parameters.receivers.forEach(id => !validateAddress(id) ? invalids.push(id) : null); + if (invalids.length) + throw "Invalid receivers:" + invalids; + if (parameters.change_address && !validateAddress(parameters.change_address)) + throw "Invalid change_address:" + parameters.change_address; + //fee and amounts + if ((typeof parameters.fee !== "number" || parameters.fee <= 0) && parameters.fee !== null) //fee = null (auto calc) + throw "Invalid fee:" + parameters.fee; + if (!Array.isArray(parameters.amounts)) + parameters.amounts = [parameters.amounts]; + if (parameters.receivers.length != parameters.amounts.length) + throw "Array length for receivers and amounts should be equal"; + parameters.amounts.forEach(a => typeof a !== "number" || a <= 0 ? invalids.push(a) : null); + if (invalids.length) + throw "Invalid amounts:" + invalids; + //return + return parameters; + } + + function createTransaction(senders, redeemScripts, receivers, amounts, fee, change_address, fee_from_receiver) { + return new Promise((resolve, reject) => { + let total_amount = parseFloat(amounts.reduce((t, a) => t + a, 0).toFixed(8)); + const tx = coinjs.transaction(); + let output_size = addOutputs(tx, receivers, amounts, change_address); + addInputs(tx, senders, redeemScripts, total_amount, fee, output_size, fee_from_receiver).then(result => { + if (result.change_amount > 0 && result.change_amount > result.fee) //add change amount if any (ignore dust change) + tx.outs[tx.outs.length - 1].value = util.BTC_to_Sat(result.change_amount); //values are in satoshi + if (fee_from_receiver) { //deduce fee from receivers if fee_from_receiver + let fee_remaining = util.BTC_to_Sat(result.fee); + for (let i = 0; i < tx.outs.length - 1 && fee_remaining > 0; i++) { + if (fee_remaining < tx.outs[i].value) { + tx.outs[i].value -= fee_remaining; + fee_remaining = 0; + } else { + fee_remaining -= tx.outs[i].value; + tx.outs[i].value = 0; + } + } + if (fee_remaining > 0) + return reject("Send amount is less than fee"); + + } + //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; + result.transaction = tx; + resolve(result); + }).catch(error => reject(error)) + }) + } + + function addInputs(tx, senders, redeemScripts, total_amount, fee, output_size, fee_from_receiver) { + return new Promise((resolve, reject) => { + if (fee !== null) { + addUTXOs(tx, senders, redeemScripts, fee_from_receiver ? total_amount : total_amount + fee, false).then(result => { + result.fee = fee; + resolve(result); + }).catch(error => reject(error)) + } else { + get_fee_rate().then(fee_rate => { + let net_fee = BASE_TX_SIZE * fee_rate; + net_fee += (output_size * fee_rate); + (fee_from_receiver ? + addUTXOs(tx, senders, redeemScripts, total_amount, false) : + addUTXOs(tx, senders, redeemScripts, total_amount + net_fee, fee_rate) + ).then(result => { + result.fee = parseFloat((net_fee + (result.input_size * fee_rate)).toFixed(8)); + result.fee_rate = fee_rate; + resolve(result); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + } + }) + } + + function addUTXOs(tx, senders, redeemScripts, required_amount, fee_rate, rec_args = {}) { + return new Promise((resolve, reject) => { + required_amount = parseFloat(required_amount.toFixed(8)); + if (typeof rec_args.n === "undefined") { + rec_args.n = 0; + rec_args.input_size = 0; + rec_args.input_amount = 0; + } + if (required_amount <= 0) + return resolve({ + input_size: rec_args.input_size, + input_amount: rec_args.input_amount, + change_amount: required_amount * -1 //required_amount will be -ve of change_amount + }); + else if (rec_args.n >= senders.length) + return reject("Insufficient Balance"); + let addr = senders[rec_args.n], + rs = redeemScripts[rec_args.n]; + let addr_type = coinjs.addressDecode(addr).type; + 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); + for (let i = 0; i < utxos.length && required_amount > 0; i++) { + if (!utxos[i].confirmations) //ignore unconfirmed utxo + continue; + var script; + if (!rs || !rs.length) //legacy script + script = utxos[i].script; + else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi)) || addr_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(utxos[i].value.toFixed(0), 8)); + script = Crypto.util.bytesToHex(s.buffer); + } else //redeemScript for multisig (segwit) + script = rs; + tx.addinput(utxos[i].tx_hash_big_endian, utxos[i].tx_output_n, script, 0xfffffffd /*sequence*/); //0xfffffffd for Replace-by-fee + //update track values + rec_args.input_size += size_per_input; + rec_args.input_amount += util.Sat_to_BTC(utxos[i].value); + required_amount -= util.Sat_to_BTC(utxos[i].value); + if (fee_rate) //automatic fee calculation (dynamic) + required_amount += size_per_input * fee_rate; + } + rec_args.n += 1; + addUTXOs(tx, senders, redeemScripts, required_amount, fee_rate, rec_args) + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + function addOutputs(tx, receivers, amounts, change_address) { + let size = 0; + for (let i in receivers) { + tx.addoutput(receivers[i], amounts[i]); + size += _sizePerOutput(receivers[i]); + } + tx.addoutput(change_address, 0); + size += _sizePerOutput(change_address); + return size; + } + + /* + function autoFeeCalc(tx) { + return new Promise((resolve, reject) => { + get_fee_rate().then(fee_rate => { + let tx_size = tx.size(); + for (var i = 0; i < this.ins.length; i++) + switch (tx.extractScriptKey(i).type) { + case 'scriptpubkey': + tx_size += SIGN_SIZE; + break; + case 'segwit': + case 'multisig': + tx_size += SIGN_SIZE * 0.25; + break; + default: + console.warn('Unknown script-type'); + tx_size += SIGN_SIZE; + } + resolve(tx_size * fee_rate); + }).catch(error => reject(error)) + }) + } + + function editFee(tx, current_fee, target_fee, index = -1) { + //values are in satoshi + index = parseInt(index >= 0 ? index : tx.outs.length - index); + if (index < 0 || index >= tx.outs.length) + throw "Invalid index"; + let edit_value = parseInt(current_fee - target_fee), //rip of any decimal places + current_value = tx.outs[index].value; //could be BigInterger + if (edit_value < 0 && edit_value > current_value) + throw "Insufficient value at vout"; + tx.outs[index].value = current_value instanceof BigInteger ? + current_value.add(new BigInteger('' + edit_value)) : parseInt(current_value + edit_value); + } + */ + + 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 => { + debugger; + broadcastTx(result.transaction.serialize()) + .then(txid => resolve(txid)) + .catch(error => reject(error)); + }).catch(error => reject(error)) + }) + } + + const createSignedTx = btcOperator.createSignedTx = function (senders, privkeys, receivers, amounts, fee = null, options = {}) { + return new Promise((resolve, reject) => { + try { + ({ + senders, + privkeys, + receivers, + amounts + } = validateTxParameters({ + senders, + privkeys, + receivers, + amounts, + fee, + change_address: options.change_address + })); + } catch (e) { + return reject(e) + } + let redeemScripts = [], + wif_keys = []; + for (let i in senders) { + let rs = _redeemScript(senders[i], privkeys[i]); //get redeem-script (segwit/bech32) + redeemScripts.push(rs); + rs === false ? wif_keys.unshift(privkeys[i]) : wif_keys.push(privkeys[i]); //sorting private-keys (wif) + } + if (redeemScripts.includes(null)) //TODO: segwit + return reject("Unable to get redeem-script"); + //create transaction + 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 => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF + console.debug("Signed:", tx.serialize()); + resolve(result); + }).catch(error => reject(error)); + }) + } + + btcOperator.createTx = function (senders, receivers, amounts, fee = null, options = {}) { + return new Promise((resolve, reject) => { + try { + ({ + senders, + receivers, + amounts + } = validateTxParameters({ + senders, + receivers, + amounts, + fee, + change_address: options.change_address + })); + } catch (e) { + return reject(e) + } + let redeemScripts = senders.map(id => _redeemScript(id)); + if (redeemScripts.includes(null)) //TODO: segwit + return reject("Unable to get redeem-script"); + //create transaction + createTransaction(senders, redeemScripts, receivers, amounts, fee, options.change_address || senders[0], options.fee_from_receiver).then(result => { + result.tx_hex = result.transaction.serialize(); + delete result.transaction; + resolve(result); + }).catch(error => reject(error)) + }) + } + + btcOperator.createMultiSigTx = function (sender, redeemScript, receivers, amounts, fee = null, options = {}) { + return new Promise((resolve, reject) => { + //validate tx parameters + let addr_type = validateAddress(sender); + if (!(["multisig", "multisigBech32"].includes(addr_type))) + return reject("Invalid sender (multisig):" + sender); + else { + let script = coinjs.script(); + let decode = (addr_type == "multisig") ? + script.decodeRedeemScript(redeemScript) : + script.decodeRedeemScriptBech32(redeemScript); + if (!decode || decode.address !== sender) + return reject("Invalid redeem-script"); + } + try { + ({ + receivers, + amounts + } = validateTxParameters({ + receivers, + amounts, + fee, + change_address: options.change_address + })); + } catch (e) { + return reject(e) + } + //create transaction + createTransaction([sender], [redeemScript], receivers, amounts, fee, options.change_address || sender, options.fee_from_receiver).then(result => { + result.tx_hex = result.transaction.serialize(); + delete result.transaction; + resolve(result); + }).catch(error => reject(error)) + + }) + } + + function deserializeTx(tx) { + if (typeof tx === 'string' || Array.isArray(tx)) { + try { + tx = coinjs.transaction().deserialize(tx); + } catch { + throw "Invalid transaction hex"; + } + } else if (typeof tx !== 'object' || typeof tx.sign !== 'function') + throw "Invalid transaction object"; + return tx; + } + + btcOperator.signTx = function (tx, privkeys, sighashtype = 1) { + tx = deserializeTx(tx); + if (!Array.isArray(privkeys)) + privkeys = [privkeys]; + for (let i in privkeys) + if (privkeys[i].length === 64) + privkeys[i] = coinjs.privkey2wif(privkeys[i]); + new Set(privkeys).forEach(key => tx.sign(key, sighashtype)); //Sign the tx using private key WIF + return tx.serialize(); + } + + const checkSigned = btcOperator.checkSigned = function (tx, bool = true) { + tx = deserializeTx(tx); + let n = []; + for (let i in tx.ins) { + var s = tx.extractScriptKey(i); + if (s['type'] !== 'multisig' && s['type'] !== 'multisig_bech32') + n.push(s.signed == 'true' || (tx.witness[i] && tx.witness[i].length == 2)) + else { + var rs = coinjs.script().decodeRedeemScript(s.script); //will work for bech32 too, as only address is diff + let x = { + s: s['signatures'], + r: rs['signaturesRequired'], + t: rs['pubkeys'].length + }; + if (x.r > x.t) + throw "signaturesRequired is more than publicKeys"; + else if (x.s < x.r) + n.push(x); + else + n.push(true); + } + } + return bool ? !(n.filter(x => x !== true).length) : n; + } + + btcOperator.checkIfSameTx = function (tx1, tx2) { + tx1 = deserializeTx(tx1); + tx2 = deserializeTx(tx2); + //compare input and output length + if (tx1.ins.length !== tx2.ins.length || tx1.outs.length !== tx2.outs.length) + return false; + //compare inputs + for (let i = 0; i < tx1.ins.length; i++) + if (tx1.ins[i].outpoint.hash !== tx2.ins[i].outpoint.hash || tx1.ins[i].outpoint.index !== tx2.ins[i].outpoint.index) + return false; + //compare outputs + for (let i = 0; i < tx1.outs.length; i++) + if (tx1.outs[i].value !== tx2.outs[i].value || Crypto.util.bytesToHex(tx1.outs[i].script.buffer) !== Crypto.util.bytesToHex(tx2.outs[i].script.buffer)) + return false; + return true; + } + + const getTxOutput = (txid, i) => new Promise((resolve, reject) => { + fetch_api(`rawtx/${txid}`) + .then(result => resolve(result.out[i])) + .catch(error => reject(error)) + }); + + const parseTransaction = btcOperator.parseTransaction = function (tx) { + return new Promise((resolve, reject) => { + tx = deserializeTx(tx); + let result = {}; + let promises = []; + //Parse Inputs + for (let i = 0; i < tx.ins.length; i++) + promises.push(getTxOutput(tx.ins[i].outpoint.hash, tx.ins[i].outpoint.index)); + Promise.all(promises).then(inputs => { + result.inputs = inputs.map(inp => Object({ + address: inp.addr, + value: util.Sat_to_BTC(inp.value) + })); + let signed = checkSigned(tx, false); + result.inputs.forEach((inp, i) => inp.signed = signed[i]); + //Parse Outputs + result.outputs = tx.outs.map(out => { + var address; + switch (out.script.chunks[0]) { + case 0: //bech32, multisig-bech32 + address = util.encodeBech32(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.bech32.version, coinjs.bech32.hrp); + break; + case 169: //segwit, multisig-segwit + address = util.encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.multisig); + break; + case 118: //legacy + address = util.encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[2]), coinjs.pub); + } + return { + address, + value: util.Sat_to_BTC(out.value) + } + }); + //Parse Totals + result.total_input = parseFloat(result.inputs.reduce((a, inp) => a += inp.value, 0).toFixed(8)); + result.total_output = parseFloat(result.outputs.reduce((a, out) => a += out.value, 0).toFixed(8)); + result.fee = parseFloat((result.total_input - result.total_output).toFixed(8)); + resolve(result); + }).catch(error => reject(error)) + }) + } + + btcOperator.transactionID = function (tx) { + tx = deserializeTx(tx); + let clone = coinjs.clone(tx); + clone.witness = null; + let raw_bytes = Crypto.util.hexToBytes(clone.serialize()); + let txid = Crypto.SHA256(Crypto.SHA256(raw_bytes, { asBytes: true }), { asBytes: true }).reverse(); + return Crypto.util.bytesToHex(txid); + } + + const getLatestBlock = btcOperator.getLatestBlock = () => new Promise((resolve, reject) => { + fetch_api(`q/getblockcount`) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + + const getTx = btcOperator.getTx = txid => new Promise((resolve, reject) => { + fetch_api(`rawtx/${txid}`).then(result => { + getLatestBlock().then(latest_block => resolve({ + block: result.block_height, + txid: result.hash, + time: result.time * 1000, + confirmations: result.block_height === null ? 0 : latest_block - result.block_height, //calculate confirmations using latest block number as api doesnt relay it + size: result.size, + fee: util.Sat_to_BTC(result.fee), + inputs: result.inputs.map(i => Object({ address: i.prev_out.addr, value: util.Sat_to_BTC(i.prev_out.value) })), + total_input_value: util.Sat_to_BTC(result.inputs.reduce((a, i) => a + i.prev_out.value, 0)), + outputs: result.out.map(o => Object({ address: o.addr, value: util.Sat_to_BTC(o.value) })), + total_output_value: util.Sat_to_BTC(result.out.reduce((a, o) => a += o.value, 0)), + })) + }).catch(error => reject(error)) + }); + + getTx.hex = txid => new Promise((resolve, reject) => { + fetch_api(`rawtx/${txid}?format=hex`, false) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + + btcOperator.getAddressData = address => new Promise((resolve, reject) => { + fetch_api(`rawaddr/${address}`).then(data => { + let details = {}; + details.balance = util.Sat_to_BTC(data.final_balance); + details.address = data.address; + details.txs = data.txs.map(tx => { + let d = { + txid: tx.hash, + time: tx.time * 1000, //s to ms + block: tx.block_height, + } + //sender list + d.tx_senders = {}; + tx.inputs.forEach(i => { + if (i.prev_out.addr in d.tx_senders) + d.tx_senders[i.prev_out.addr] += i.prev_out.value; + else d.tx_senders[i.prev_out.addr] = i.prev_out.value; + }); + d.tx_input_value = 0; + for (let s in d.tx_senders) { + let val = d.tx_senders[s]; + d.tx_senders[s] = util.Sat_to_BTC(val); + d.tx_input_value += val; + } + d.tx_input_value = util.Sat_to_BTC(d.tx_input_value); + //receiver list + d.tx_receivers = {}; + tx.out.forEach(o => { + if (o.addr in d.tx_receivers) + d.tx_receivers[o.addr] += o.value; + else d.tx_receivers[o.addr] = o.value; + }); + d.tx_output_value = 0; + for (let r in d.tx_receivers) { + let val = d.tx_receivers[r]; + d.tx_receivers[r] = util.Sat_to_BTC(val); + d.tx_output_value += val; + } + d.tx_output_value = util.Sat_to_BTC(d.tx_output_value); + d.tx_fee = util.Sat_to_BTC(tx.fee); + //tx type + if (tx.result > 0) { //net > 0, balance inc => type=in + d.type = "in"; + d.amount = util.Sat_to_BTC(tx.result); + d.sender = Object.keys(d.tx_senders).filter(s => s !== address); + } else if (Object.keys(d.tx_receivers).some(r => r !== address)) { //net < 0, balance dec & receiver present => type=out + d.type = "out"; + d.amount = util.Sat_to_BTC(tx.result * -1); + d.receiver = Object.keys(d.tx_receivers).filter(r => r !== address); + d.fee = d.tx_fee; + } else { //net < 0 (fee) & no other id in receiver list => type=self + d.type = "self"; + d.amount = d.tx_receivers[address]; + d.address = address + } + return d; + }) + resolve(details); + }).catch(error => reject(error)) + }); + + btcOperator.getBlock = block => new Promise((resolve, reject) => { + fetch_api(`rawblock/${block}`).then(result => resolve({ + height: result.height, + hash: result.hash, + merkle_root: result.mrkl_root, + prev_block: result.prev_block, + next_block: result.next_block[0], + size: result.size, + time: result.time * 1000, //s to ms + txs: result.tx.map(t => Object({ + fee: t.fee, + size: t.size, + inputs: t.inputs.map(i => Object({ address: i.prev_out.addr, value: util.Sat_to_BTC(i.prev_out.value) })), + total_input_value: util.Sat_to_BTC(t.inputs.reduce((a, i) => a + i.prev_out.value, 0)), + outputs: t.out.map(o => Object({ address: o.addr, value: util.Sat_to_BTC(o.value) })), + total_output_value: util.Sat_to_BTC(t.out.reduce((a, o) => a += o.value, 0)), + })) + + })).catch(error => reject(error)) + }); + +})('object' === typeof module ? module.exports : window.btcOperator = {}); diff --git a/compactIDB.js b/compactIDB.js new file mode 100644 index 0000000..ba843ec --- /dev/null +++ b/compactIDB.js @@ -0,0 +1,257 @@ +(function (EXPORTS) { //compactIDB v2.1.2 + /* Compact IndexedDB operations */ + 'use strict'; + const compactIDB = EXPORTS; + + var defaultDB; + + const indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; + const IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction; + const IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange; + + if (!indexedDB) { + console.error("Your browser doesn't support a stable version of IndexedDB."); + return; + } + + compactIDB.setDefaultDB = dbName => defaultDB = dbName; + + Object.defineProperty(compactIDB, 'default', { + get: () => defaultDB, + set: dbName => defaultDB = dbName + }); + + function getDBversion(dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + resolve(db.version) + db.close() + }).catch(error => reject(error)) + }) + } + + function upgradeDB(dbName, createList = null, deleteList = null) { + return new Promise((resolve, reject) => { + getDBversion(dbName).then(version => { + var idb = indexedDB.open(dbName, version + 1); + idb.onerror = (event) => reject("Error in opening IndexedDB"); + idb.onupgradeneeded = (event) => { + let db = event.target.result; + if (createList instanceof Object) { + if (Array.isArray(createList)) { + let tmp = {} + createList.forEach(o => tmp[o] = {}) + createList = tmp + } + for (let o in createList) { + let obs = db.createObjectStore(o, createList[o].options || {}); + if (createList[o].indexes instanceof Object) + for (let i in createList[o].indexes) + obs.createIndex(i, i, createList[o].indexes || {}); + } + } + if (Array.isArray(deleteList)) + deleteList.forEach(o => db.deleteObjectStore(o)); + resolve('Database upgraded') + } + idb.onsuccess = (event) => event.target.result.close(); + }).catch(error => reject(error)) + }) + } + + compactIDB.initDB = function (dbName, objectStores = {}) { + return new Promise((resolve, reject) => { + if (!(objectStores instanceof Object)) + return reject('ObjectStores must be an object or array') + defaultDB = defaultDB || dbName; + var idb = indexedDB.open(dbName); + idb.onerror = (event) => reject("Error in opening IndexedDB"); + idb.onsuccess = (event) => { + var db = event.target.result; + let cList = Object.values(db.objectStoreNames); + var obs = {}, + a_obs = {}, + d_obs = []; + if (!Array.isArray(objectStores)) + var obs = objectStores + else + objectStores.forEach(o => obs[o] = {}) + let nList = Object.keys(obs) + for (let o of nList) + if (!cList.includes(o)) + a_obs[o] = obs[o] + for (let o of cList) + if (!nList.includes(o)) + d_obs.push(o) + if (!Object.keys(a_obs).length && !d_obs.length) + resolve("Initiated IndexedDB"); + else + upgradeDB(dbName, a_obs, d_obs) + .then(result => resolve(result)) + .catch(error => reject(error)) + db.close(); + } + }); + } + + 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"); + idb.onupgradeneeded = (event) => { + event.target.result.close(); + deleteDB(dbName).then(_ => null).catch(_ => null).finally(_ => reject("Datebase not found")) + } + idb.onsuccess = (event) => resolve(event.target.result); + }); + } + + const deleteDB = compactIDB.deleteDB = function (dbName = defaultDB) { + return new Promise((resolve, reject) => { + var deleteReq = indexedDB.deleteDatabase(dbName);; + deleteReq.onerror = (event) => reject("Error deleting database!"); + deleteReq.onsuccess = (event) => resolve("Database deleted successfully"); + }); + } + + 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); + let writeReq = (key ? obs.put(data, key) : obs.put(data)); + writeReq.onsuccess = (evt) => resolve(`Write data Successful`); + writeReq.onerror = (evt) => reject( + `Write data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + 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); + let addReq = (key ? obs.add(data, key) : obs.add(data)); + addReq.onsuccess = (evt) => resolve(`Add data successful`); + addReq.onerror = (evt) => reject( + `Add data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.removeData = function (obsName, key, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readwrite").objectStore(obsName); + let delReq = obs.delete(key); + delReq.onsuccess = (evt) => resolve(`Removed Data ${key}`); + delReq.onerror = (evt) => reject( + `Remove data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.clearData = function (obsName, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readwrite").objectStore(obsName); + let clearReq = obs.clear(); + clearReq.onsuccess = (evt) => resolve(`Clear data Successful`); + clearReq.onerror = (evt) => reject(`Clear data Unsuccessful`); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.readData = function (obsName, key, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + let getReq = obs.get(key); + getReq.onsuccess = (evt) => resolve(evt.target.result); + getReq.onerror = (evt) => reject( + `Read data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + compactIDB.readAllData = function (obsName, dbName = defaultDB) { + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + var tmpResult = {} + let curReq = obs.openCursor(); + curReq.onsuccess = (evt) => { + var cursor = evt.target.result; + if (cursor) { + tmpResult[cursor.primaryKey] = cursor.value; + cursor.continue(); + } else + resolve(tmpResult); + } + curReq.onerror = (evt) => reject( + `Read-All data unsuccessful [${evt.target.error.name}] ${evt.target.error.message}` + ); + db.close(); + }).catch(error => reject(error)); + }); + } + + /* compactIDB.searchData = function (obsName, options = {}, dbName = defaultDB) { + + return new Promise((resolve, reject) => { + openDB(dbName).then(db => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + var filteredResult = {} + let keyRange; + if(options.lowerKey!==null && options.upperKey!==null) + keyRange = IDBKeyRange.bound(options.lowerKey, options.upperKey); + else if(options.lowerKey!==null) + keyRange = IDBKeyRange.lowerBound(options.lowerKey); + else if (options.upperKey!==null) + keyRange = IDBKeyRange.upperBound(options.upperBound); + else if (options.atKey) + let curReq = obs.openCursor(keyRange, ) + }).catch(error => reject(error)) + }) + }*/ + + 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) => 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 => { + var obs = db.transaction(obsName, "readonly").objectStore(obsName); + var filteredResult = {} + let curReq = obs.openCursor( + options.upperKey ? IDBKeyRange.bound(options.lowerKey, options.upperKey) : IDBKeyRange.lowerBound(options.lowerKey), + options.lastOnly || options.reverse ? "prev" : "next"); + curReq.onsuccess = (evt) => { + var cursor = evt.target.result; + 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 + cursor.continue(); + } + curReq.onerror = (evt) => reject(`Search unsuccessful [${evt.target.error.name}] ${evt.target.error.message}`); + db.close(); + }).catch(error => reject(error)); + }); + } + + +})(window.compactIDB = {}); \ No newline at end of file diff --git a/components.js b/components.js new file mode 100644 index 0000000..16fc832 --- /dev/null +++ b/components.js @@ -0,0 +1,15 @@ +/*jshint esversion: 6 */ +// Components downloaded: button,chips,copy,form,input,notifications,popup,switch,select,spinner,tags-input,textarea,theme-toggle +const smButton = document.createElement("template"); smButton.innerHTML = "
", customElements.define("sm-button", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smButton.content.cloneNode(!0)) } static get observedAttributes() { return ["disabled"] } get disabled() { return this.hasAttribute("disabled") } set disabled(t) { t ? this.setAttribute("disabled", "") : this.removeAttribute("disabled") } focusIn() { this.focus() } handleKeyDown(t) { this.hasAttribute("disabled") || "Enter" !== t.key && " " !== t.key || (t.preventDefault(), this.click()) } connectedCallback() { this.hasAttribute("disabled") || this.setAttribute("tabindex", "0"), this.setAttribute("role", "button"), this.addEventListener("keydown", this.handleKeyDown) } attributeChangedCallback(t) { "disabled" === t && (this.hasAttribute("disabled") ? this.removeAttribute("tabindex") : this.setAttribute("tabindex", "0"), this.setAttribute("aria-disabled", this.hasAttribute("disabled"))) } }); +const smChips = document.createElement("template"); smChips.innerHTML = '
', customElements.define("sm-chips", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smChips.content.cloneNode(!0)), this.chipsWrapper = this.shadowRoot.querySelector(".sm-chips"), this.coverLeft = this.shadowRoot.querySelector(".cover--left"), this.coverRight = this.shadowRoot.querySelector(".cover--right"), this.navButtonLeft = this.shadowRoot.querySelector(".nav-button--left"), this.navButtonRight = this.shadowRoot.querySelector(".nav-button--right"), this.slottedOptions = void 0, this._value = void 0, this.scrollDistance = 0, this.assignedElements = [], this.scrollLeft = this.scrollLeft.bind(this), this.scrollRight = this.scrollRight.bind(this), this.fireEvent = this.fireEvent.bind(this), this.setSelectedOption = this.setSelectedOption.bind(this) } get value() { return this._value } set value(t) { this.setSelectedOption(t) } scrollLeft() { this.chipsWrapper.scrollBy({ left: -this.scrollDistance, behavior: "smooth" }) } scrollRight() { this.chipsWrapper.scrollBy({ left: this.scrollDistance, behavior: "smooth" }) } setSelectedOption(t) { this._value !== t && (this._value = t, this.assignedElements.forEach(e => { e.value == t ? (e.setAttribute("selected", ""), e.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" })) : e.removeAttribute("selected") })) } fireEvent() { this.dispatchEvent(new CustomEvent("change", { bubbles: !0, composed: !0, detail: { value: this._value } })) } connectedCallback() { this.setAttribute("role", "listbox"); const t = this.shadowRoot.querySelector("slot"); t.addEventListener("slotchange", e => { n.disconnect(), i.disconnect(), this.observeSelf.disconnect(), clearTimeout(this.slotChangeTimeout), this.slotChangeTimeout = setTimeout(() => { this.assignedElements = t.assignedElements(), this.assignedElements.forEach(t => { t.hasAttribute("selected") && (this._value = t.value) }), this.observeSelf.observe(this) }, 0) }); const e = new ResizeObserver(t => { t.forEach(t => { if (t.contentBoxSize) { const e = Array.isArray(t.contentBoxSize) ? t.contentBoxSize[0] : t.contentBoxSize; this.scrollDistance = .6 * e.inlineSize } else this.scrollDistance = .6 * t.contentRect.width }) }); e.observe(this), this.observeSelf = new IntersectionObserver((t, e) => { t.forEach(t => { t.isIntersecting && !this.hasAttribute("multiline") && this.assignedElements.length > 0 && (n.observe(this.assignedElements[0]), i.observe(this.assignedElements[this.assignedElements.length - 1]), e.unobserve(this)) }) }, { threshold: 1 }), this.chipsWrapper.addEventListener("option-clicked", t => { this._value !== t.target.value && (this.setSelectedOption(t.target.value), this.fireEvent()) }); const n = new IntersectionObserver(t => { t.forEach(t => { t.isIntersecting ? (this.navButtonLeft.classList.add("hide"), this.coverLeft.classList.add("hide")) : (this.navButtonLeft.classList.remove("hide"), this.coverLeft.classList.remove("hide")) }) }, { threshold: 1, root: this }), i = new IntersectionObserver(t => { t.forEach(t => { t.isIntersecting ? (this.navButtonRight.classList.add("hide"), this.coverRight.classList.add("hide")) : (this.navButtonRight.classList.remove("hide"), this.coverRight.classList.remove("hide")) }) }, { threshold: 1, root: this }); this.navButtonLeft.addEventListener("click", this.scrollLeft), this.navButtonRight.addEventListener("click", this.scrollRight) } disconnectedCallback() { this.navButtonLeft.removeEventListener("click", this.scrollLeft), this.navButtonRight.removeEventListener("click", this.scrollRight) } }); const smChip = document.createElement("template"); smChip.innerHTML = ' ', customElements.define("sm-chip", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smChip.content.cloneNode(!0)), this._value = void 0, this.radioButton = this.shadowRoot.querySelector("input"), this.fireEvent = this.fireEvent.bind(this), this.handleKeyDown = this.handleKeyDown.bind(this) } get value() { return this._value } fireEvent() { this.dispatchEvent(new CustomEvent("option-clicked", { bubbles: !0, composed: !0, detail: { value: this._value } })) } handleKeyDown(t) { "Enter" !== t.key && "Space" !== t.key || this.fireEvent() } connectedCallback() { this.setAttribute("role", "option"), this.setAttribute("tabindex", "0"), this._value = this.getAttribute("value"), this.addEventListener("click", this.fireEvent), this.addEventListener("keydown", this.handleKeyDown) } disconnectedCallback() { this.removeEventListener("click", this.fireEvent), this.removeEventListener("keydown", this.handleKeyDown) } }); +const smCopy = document.createElement("template"); smCopy.innerHTML = '

', customElements.define("sm-copy", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smCopy.content.cloneNode(!0)), this.copyContent = this.shadowRoot.querySelector(".copy-content"), this.copyButton = this.shadowRoot.querySelector(".copy-button"), this.copy = this.copy.bind(this) } static get observedAttributes() { return ["value"] } set value(t) { this.setAttribute("value", t) } get value() { return this.getAttribute("value") } fireEvent() { this.dispatchEvent(new CustomEvent("copy", { composed: !0, bubbles: !0, cancelable: !0 })) } copy() { navigator.clipboard.writeText(this.getAttribute("value")).then(t => this.fireEvent()).catch(t => console.error(t)) } connectedCallback() { this.copyButton.addEventListener("click", this.copy) } attributeChangedCallback(t, n, o) { if ("value" === t) { const t = this.copyContent.querySelector("slot"); if (!t) return; const n = t.assignedNodes(); n && n.length || (this.copyContent.textContent = o) } } disconnectedCallback() { this.copyButton.removeEventListener("click", this.copy) } }); +const smForm = document.createElement("template"); smForm.innerHTML = '
', customElements.define("sm-form", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smForm.content.cloneNode(!0)), this.form = this.shadowRoot.querySelector("form"), this.invalidFieldsCount, this.skipSubmit = !1, this.isFormValid = void 0, this.supportedElements = "input, sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio", this.formElements = [], this._requiredElements = [], this.debounce = this.debounce.bind(this), this._checkValidity = this._checkValidity.bind(this), this.handleKeydown = this.handleKeydown.bind(this), this.reset = this.reset.bind(this), this.elementsChanged = this.elementsChanged.bind(this) } static get observedAttributes() { return ["skip-submit"] } get validity() { return this.isFormValid } debounce(e, t) { let i = null; return (...s) => { window.clearTimeout(i), i = window.setTimeout((() => { e.apply(null, s) }), t) } } _checkValidity() { this.submitButton && 0 !== this._requiredElements.length && (this.invalidFieldsCount = 0, this._requiredElements.forEach((([e, t]) => { (!e.disabled && t && !e.isValid || !t && !e.checkValidity()) && this.invalidFieldsCount++ })), this.isFormValid !== (0 === this.invalidFieldsCount) && (this.isFormValid = 0 === this.invalidFieldsCount, this.dispatchEvent(new CustomEvent(this.isFormValid ? "valid" : "invalid", { bubbles: !0, composed: !0 })), this.skipSubmit || (this.submitButton.disabled = !this.isFormValid))) } handleKeydown(e) { if ("Enter" === e.key && e.target.tagName.includes("INPUT")) if (0 === this.invalidFieldsCount) this.submitButton && this.submitButton.click(), this.dispatchEvent(new CustomEvent("submit", { bubbles: !0, composed: !0 })); else for (const [e, t] of this._requiredElements) { if (t ? !e.isValid : !e.checkValidity()) { (e?.shadowRoot?.lastElementChild || e).animate([{ transform: "translateX(-1rem)" }, { transform: "translateX(1rem)" }, { transform: "translateX(-0.5rem)" }, { transform: "translateX(0.5rem)" }, { transform: "translateX(0)" }], { duration: 300, easing: "ease" }), t ? e.focusIn() : e.focus(); break } } } reset() { this.formElements.forEach((([e, t]) => { if (t) e.reset(); else switch (e.type) { case "checkbox": case "radio": e.checked = !1; break; default: e.value = "" } })), this._checkValidity() } elementsChanged() { this.formElements = [...this.querySelectorAll(this.supportedElements)].map((e => [e, e.tagName.includes("-")])), this._requiredElements = this.formElements.filter((([e]) => e.hasAttribute("required"))), this.submitButton = this.querySelector('[variant="primary"], [type="submit"]'), this.resetButton = this.querySelector('[type="reset"]'), this.resetButton && this.resetButton.addEventListener("click", this.reset), this._checkValidity() } connectedCallback() { const e = this.debounce(this.elementsChanged, 100); this.addEventListener("input", this.debounce(this._checkValidity, 100)), this.addEventListener("keydown", this.debounce(this.handleKeydown, 100)), this.shadowRoot.querySelector("slot").addEventListener("slotchange", e), this.mutationObserver = new MutationObserver((t => { t.forEach((t => { ("childList" === t.type && [...t.addedNodes].some((e => 1 === e.nodeType && e.querySelector(this.supportedElements))) || [...t.removedNodes].some((e => 1 === e.nodeType && e.querySelector(this.supportedElements)))) && e() })) })), this.mutationObserver.observe(this, { childList: !0, subtree: !0 }) } attributeChangedCallback(e, t, i) { "skip-submit" === e && (this.skipSubmit = null !== i) } disconnectedCallback() { this.removeEventListener("input", this.debounce(this._checkValidity, 100)), this.removeEventListener("keydown", this.debounce(this.handleKeydown, 100)), this.mutationObserver.disconnect() } }); +const smInput = document.createElement("template"); smInput.innerHTML = '
', customElements.define("sm-input", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smInput.content.cloneNode(!0)), this.inputParent = this.shadowRoot.querySelector(".input"), this.input = this.shadowRoot.querySelector("input"), this.clearBtn = this.shadowRoot.querySelector(".clear"), this.label = this.shadowRoot.querySelector(".label"), this.feedbackText = this.shadowRoot.querySelector(".feedback-text"), this.outerContainer = this.shadowRoot.querySelector(".outer-container"), this.optionList = this.shadowRoot.querySelector(".datalist"), this._helperText = "", this._errorText = "", this.isRequired = !1, this.datalist = [], this.validationFunction = void 0, this.reflectedAttributes = ["value", "required", "disabled", "type", "inputmode", "readonly", "min", "max", "pattern", "minlength", "maxlength", "step", "list", "autocomplete"], this.reset = this.reset.bind(this), this.clear = this.clear.bind(this), this.focusIn = this.focusIn.bind(this), this.focusOut = this.focusOut.bind(this), this.fireEvent = this.fireEvent.bind(this), this.checkInput = this.checkInput.bind(this), this.showError = this.showError.bind(this), this.allowOnlyNum = this.allowOnlyNum.bind(this), this.handleOptionClick = this.handleOptionClick.bind(this), this.handleInputNavigation = this.handleInputNavigation.bind(this), this.handleDatalistNavigation = this.handleDatalistNavigation.bind(this), this.handleFocus = this.handleFocus.bind(this), this.handleBlur = this.handleBlur.bind(this) } static get observedAttributes() { return ["value", "placeholder", "required", "disabled", "type", "inputmode", "readonly", "min", "max", "pattern", "minlength", "maxlength", "step", "helper-text", "error-text", "list"] } get value() { return this.input.value } set value(t) { t !== this.input.value && (this.input.value = t, this.checkInput()) } get placeholder() { return this.getAttribute("placeholder") } set placeholder(t) { this.setAttribute("placeholder", t) } get type() { return this.getAttribute("type") } set type(t) { this.setAttribute("type", t) } get validity() { return this.input.validity } get disabled() { return this.hasAttribute("disabled") } set disabled(t) { t ? (this.inputParent.classList.add("disabled"), this.setAttribute("disabled", "")) : (this.inputParent.classList.remove("disabled"), this.removeAttribute("disabled")) } get readOnly() { return this.hasAttribute("readonly") } set readOnly(t) { t ? this.setAttribute("readonly", "") : this.removeAttribute("readonly") } set customValidation(t) { this.validationFunction = t } set errorText(t) { this._errorText = t } showError() { this.feedbackText.className = "feedback-text error", this.feedbackText.innerHTML = ` ${this._errorText}` } set helperText(t) { this._helperText = t } get isValid() { if ("" !== this.input.value) { const t = this.input.checkValidity(); let e = !0; return this.validationFunction && (e = Boolean(this.validationFunction(this.input.value))), t && e ? (this.feedbackText.className = "feedback-text success", this.feedbackText.textContent = "") : this._errorText && this.showError(), t && e } } reset() { this.value = "" } clear() { this.value = "", this.input.focus(), this.fireEvent() } focusIn() { this.input.focus() } focusOut() { this.input.blur() } fireEvent() { let t = new Event("input", { bubbles: !0, cancelable: !0, composed: !0 }); this.dispatchEvent(t) } searchDatalist(t) { const e = this.datalist.filter(e => e.toLowerCase().includes(t.toLowerCase())); if (e.sort((e, n) => { const i = e.toLowerCase().indexOf(t.toLowerCase()), s = n.toLowerCase().indexOf(t.toLowerCase()); return i - s }), e.length) { if (this.optionList.children.length > e.length) { const t = this.optionList.children.length - e.length; for (let e = 0; e < t; e++)this.optionList.removeChild(this.optionList.lastChild) } e.forEach((t, e) => { if (this.optionList.children[e]) this.optionList.children[e].textContent = t; else { const e = document.createElement("li"); e.textContent = t, e.classList.add("datalist-item"), e.setAttribute("tabindex", "0"), this.optionList.appendChild(e) } }), this.optionList.classList.remove("hidden") } else this.optionList.classList.add("hidden") } checkInput(t) { this.hasAttribute("readonly") || ("" !== this.input.value ? this.clearBtn.classList.remove("hidden") : this.clearBtn.classList.add("hidden")), this.hasAttribute("placeholder") && "" !== this.getAttribute("placeholder").trim() && ("" !== this.input.value ? (this.animate ? this.inputParent.classList.add("animate-placeholder") : this.label.classList.add("hidden"), this.datalist.length && (this.searchTimeout && clearTimeout(this.searchTimeout), this.searchTimeout = setTimeout(() => { this.searchDatalist(this.input.value.trim()) }, 100))) : (this.animate ? this.inputParent.classList.remove("animate-placeholder") : this.label.classList.remove("hidden"), this.feedbackText.textContent = "", this.datalist.length && (this.optionList.innerHTML = "", this.optionList.classList.add("hidden")))) } allowOnlyNum(t) { 1 === t.key.length && (("." !== t.key || !t.target.value.includes(".") && 0 !== t.target.value.length) && ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."].includes(t.key) || t.preventDefault()) } handleOptionClick(t) { this.input.value = t.target.textContent, this.optionList.classList.add("hidden"), this.input.focus() } handleInputNavigation(t) { "ArrowDown" === t.key ? (t.preventDefault(), this.optionList.children.length && this.optionList.children[0].focus()) : "ArrowUp" === t.key && (t.preventDefault(), this.optionList.children.length && this.optionList.children[this.optionList.children.length - 1].focus()) } handleDatalistNavigation(t) { "ArrowUp" === t.key ? (t.preventDefault(), this.shadowRoot.activeElement.previousElementSibling ? this.shadowRoot.activeElement.previousElementSibling.focus() : this.input.focus()) : "ArrowDown" === t.key ? (t.preventDefault(), this.shadowRoot.activeElement.nextElementSibling ? this.shadowRoot.activeElement.nextElementSibling.focus() : this.input.focus()) : "Enter" !== t.key && " " !== t.key || (t.preventDefault(), this.input.value = t.target.textContent, this.optionList.classList.add("hidden"), this.input.focus()) } handleFocus(t) { this.datalist.length && this.searchDatalist(this.input.value.trim()) } handleBlur(t) { this.datalist.length && this.optionList.classList.add("hidden") } connectedCallback() { this.animate = this.hasAttribute("animate"), this.setAttribute("role", "textbox"), this.input.addEventListener("input", this.checkInput), this.clearBtn.addEventListener("click", this.clear), this.datalist.length && (this.optionList.addEventListener("click", this.handleOptionClick), this.input.addEventListener("keydown", this.handleInputNavigation), this.optionList.addEventListener("keydown", this.handleDatalistNavigation)), this.input.addEventListener("focusin", this.handleFocus), this.addEventListener("focusout", this.handleBlur) } attributeChangedCallback(t, e, n) { if (e !== n) switch (this.reflectedAttributes.includes(t) && (this.hasAttribute(t) ? this.input.setAttribute(t, this.getAttribute(t) ? this.getAttribute(t) : "") : this.input.removeAttribute(t)), t) { case "placeholder": this.label.textContent = n, this.setAttribute("aria-label", n); break; case "value": this.checkInput(); break; case "type": this.hasAttribute("type") && "number" === this.getAttribute("type") ? (this.input.setAttribute("inputmode", "decimal"), this.input.addEventListener("keydown", this.allowOnlyNum)) : this.input.removeEventListener("keydown", this.allowOnlyNum); break; case "helper-text": this._helperText = n; break; case "error-text": this._errorText = n; break; case "required": this.isRequired = this.hasAttribute("required"), this.isRequired ? this.setAttribute("aria-required", "true") : this.setAttribute("aria-required", "false"); break; case "readonly": this.hasAttribute("readonly") ? this.inputParent.classList.add("readonly") : this.inputParent.classList.remove("readonly"); break; case "disabled": this.hasAttribute("disabled") ? this.inputParent.classList.add("disabled") : this.inputParent.classList.remove("disabled"); break; case "list": this.hasAttribute("list") && "" !== this.getAttribute("list").trim() && (this.datalist = this.getAttribute("list").split(",")) } } disconnectedCallback() { this.input.removeEventListener("input", this.checkInput), this.clearBtn.removeEventListener("click", this.clear), this.input.removeEventListener("keydown", this.allowOnlyNum), this.optionList.removeEventListener("click", this.handleOptionClick), this.input.removeEventListener("keydown", this.handleInputNavigation), this.optionList.removeEventListener("keydown", this.handleDatalistNavigation), this.input.removeEventListener("focusin", this.handleFocus), this.removeEventListener("focusout", this.handleBlur) } }); +const smNotifications = document.createElement("template"); smNotifications.innerHTML = '
', customElements.define("sm-notifications", class extends HTMLElement { constructor() { super(), this.shadow = this.attachShadow({ mode: "open" }).append(smNotifications.content.cloneNode(!0)), this.notificationPanel = this.shadowRoot.querySelector(".notification-panel"), this.animationOptions = { duration: 300, fill: "forwards", easing: "cubic-bezier(0.175, 0.885, 0.32, 1.275)" }, this.push = this.push.bind(this), this.createNotification = this.createNotification.bind(this), this.removeNotification = this.removeNotification.bind(this), this.clearAll = this.clearAll.bind(this), this.remove = this.remove.bind(this), this.handlePointerMove = this.handlePointerMove.bind(this), this.startX = 0, this.currentX = 0, this.endX = 0, this.swipeDistance = 0, this.swipeDirection = "", this.swipeThreshold = 0, this.startTime = 0, this.swipeTime = 0, this.swipeTimeThreshold = 200, this.currentTarget = null, this.mediaQuery = window.matchMedia("(min-width: 640px)"), this.handleOrientationChange = this.handleOrientationChange.bind(this), this.isLandscape = !1 } randString(n) { let t = ""; const e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; for (let i = 0; i < n; i++)t += e.charAt(Math.floor(Math.random() * e.length)); return t } createNotification(n, t = {}) { const { pinned: e = !1, icon: i = "", action: o } = t, r = document.createElement("div"); r.id = this.randString(8), r.className = `notification ${e ? "pinned" : ""}`; const a = document.createElement("div"); a.className = "icon-container", a.innerHTML = i; const s = document.createElement("output"); if (s.textContent = n, r.append(a, s), o) { const n = document.createElement("button"); n.className = "action", n.innerText = o.label, n.addEventListener("click", o.callback) } if (e) { const n = document.createElement("button"); n.className = "close", n.innerHTML = ' ', n.addEventListener("click", () => { this.remove(r.id) }), r.append(n) } return r } push(n, t = {}) { const e = this.createNotification(n, t); return this.isLandscape ? this.notificationPanel.append(e) : this.notificationPanel.prepend(e), this.notificationPanel.animate([{ transform: `translateY(${this.isLandscape ? "" : "-"}${e.clientHeight}px)` }, { transform: "none" }], this.animationOptions), e.animate([{ transform: "translateY(-1rem)", opacity: "0" }, { transform: "none", opacity: "1" }], this.animationOptions).onfinish = (n => { n.target.commitStyles(), n.target.cancel() }), e.querySelector(".action") && e.querySelector(".action").addEventListener("click", t.action.callback), e.id } removeNotification(n, t = "left") { if (!n) return; const e = "left" === t ? "-" : "+"; n.animate([{ transform: this.currentX ? `translateX(${this.currentX}px)` : "none", opacity: "1" }, { transform: `translateX(calc(${e}${Math.abs(this.currentX)}px ${e} 1rem))`, opacity: "0" }], this.animationOptions).onfinish = (() => { n.remove() }) } remove(n) { const t = this.notificationPanel.querySelector(`#${n}`); t && this.removeNotification(t) } clearAll() { Array.from(this.notificationPanel.children).forEach(n => { this.removeNotification(n) }) } handlePointerMove(n) { this.currentX = n.clientX - this.startX, this.currentTarget.style.transform = `translateX(${this.currentX}px)` } handleOrientationChange(n) { this.isLandscape = n.matches, n.matches } connectedCallback() { this.handleOrientationChange(this.mediaQuery), this.mediaQuery.addEventListener("change", this.handleOrientationChange), this.notificationPanel.addEventListener("pointerdown", n => { n.target.closest(".close") ? this.removeNotification(n.target.closest(".notification")) : n.target.closest(".notification") && (this.swipeThreshold = n.target.closest(".notification").getBoundingClientRect().width / 2, this.currentTarget = n.target.closest(".notification"), this.currentTarget.setPointerCapture(n.pointerId), this.startTime = Date.now(), this.startX = n.clientX, this.startY = n.clientY, this.notificationPanel.addEventListener("pointermove", this.handlePointerMove)) }), this.notificationPanel.addEventListener("pointerup", n => { this.endX = n.clientX, this.endY = n.clientY, this.swipeDistance = Math.abs(this.endX - this.startX), this.swipeTime = Date.now() - this.startTime, this.endX > this.startX ? this.swipeDirection = "right" : this.swipeDirection = "left", this.swipeTime < this.swipeTimeThreshold ? this.swipeDistance > 50 && this.removeNotification(this.currentTarget, this.swipeDirection) : this.swipeDistance > this.swipeThreshold ? this.removeNotification(this.currentTarget, this.swipeDirection) : this.currentTarget.animate([{ transform: `translateX(${this.currentX}px)` }, { transform: "none" }], this.animationOptions).onfinish = (n => { n.target.commitStyles(), n.target.cancel() }), this.notificationPanel.removeEventListener("pointermove", this.handlePointerMove), this.notificationPanel.releasePointerCapture(n.pointerId), this.currentX = 0 }); const n = new MutationObserver(n => { n.forEach(n => { "childList" === n.type && n.addedNodes.length && !n.addedNodes[0].classList.contains("pinned") && setTimeout(() => { this.removeNotification(n.addedNodes[0]) }, 5e3) }) }); n.observe(this.notificationPanel, { childList: !0 }) } disconnectedCallback() { mediaQueryList.removeEventListener("change", handleOrientationChange) } }); +class Stack { constructor() { this.items = [] } push(t) { this.items.push(t) } pop() { return 0 == this.items.length ? "Underflow" : this.items.pop() } peek() { return this.items[this.items.length - 1] } } const popupStack = new Stack, smPopup = document.createElement("template"); smPopup.innerHTML = ` `, customElements.define("sm-popup", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smPopup.content.cloneNode(!0)), this.allowClosing = !1, this.isOpen = !1, this.offset = 0, this.touchStartY = 0, this.touchEndY = 0, this.touchStartTime = 0, this.touchEndTime = 0, this.touchEndAnimation = void 0, this.focusable, this.autoFocus, this.mutationObserver, this.popupContainer = this.shadowRoot.querySelector(".popup-container"), this.backdrop = this.shadowRoot.querySelector(".backdrop"), this.dialogBox = this.shadowRoot.querySelector(".popup"), this.popupBodySlot = this.shadowRoot.querySelector(".popup-body slot"), this.popupHeader = this.shadowRoot.querySelector(".popup-top"), this.resumeScrolling = this.resumeScrolling.bind(this), this.setStateOpen = this.setStateOpen.bind(this), this.show = this.show.bind(this), this.hide = this.hide.bind(this), this.handleTouchStart = this.handleTouchStart.bind(this), this.handleTouchMove = this.handleTouchMove.bind(this), this.handleTouchEnd = this.handleTouchEnd.bind(this), this.detectFocus = this.detectFocus.bind(this), this.handleSoftDismiss = this.handleSoftDismiss.bind(this), this.debounce = this.debounce.bind(this) } static get observedAttributes() { return ["open"] } get open() { return this.isOpen } animateTo(t, e, i) { let s = t.animate(e, { ...i, fill: "both" }); return s.finished.then(() => { s.commitStyles(), s.cancel() }), s } resumeScrolling() { let t = document.body.style.top; window.scrollTo(0, -1 * parseInt(t || "0")), document.body.style.overflow = "", document.body.style.top = "initial" } setStateOpen() { if (!this.isOpen || this.offset) { let t = window.innerWidth > 640 ? "scale(1.1)" : `translateY(${this.offset ? `${this.offset}px` : "100%"})`; this.animateTo(this.dialogBox, [{ opacity: this.offset ? 1 : 0, transform: t }, { opacity: 1, transform: "none" },], { duration: 300, easing: "ease" }) } } show(t = {}) { let { pinned: e = !1, payload: i } = t; if (this.isOpen) return; let s = { duration: 300, easing: "ease" }; return this.payload = i, popupStack.push({ popup: this, permission: e }), popupStack.items.length > 1 && this.animateTo(popupStack.items[popupStack.items.length - 2].popup.shadowRoot.querySelector(".popup"), [{ transform: "none" }, { transform: window.innerWidth > 640 ? "scale(0.95)" : "translateY(-1.5rem)" },], s), this.popupContainer.classList.remove("hide"), this.offset || (this.backdrop.animate([{ opacity: 0 }, { opacity: 1 },], s).onfinish = () => { this.resolveOpen(this.payload) }, this.dispatchEvent(new CustomEvent("popupopened", { bubbles: !0, composed: !0, detail: { payload: this.payload } })), document.body.style.overflow = "hidden", document.body.style.top = `-${window.scrollY}px`), this.setStateOpen(), this.pinned = e, this.isOpen = !0, setTimeout(() => { let t = this.autoFocus || this.focusable?.[0] || this.dialogBox; t && (t.tagName.includes("-") ? t.focusIn() : t.focus()) }, 0), this.hasAttribute("open") || (this.setAttribute("open", ""), this.addEventListener("keydown", this.detectFocus), this.resizeObserver.observe(this), this.mutationObserver.observe(this, { attributes: !0, childList: !0, subtree: !0 }), this.popupHeader.addEventListener("touchstart", this.handleTouchStart, { passive: !0 }), this.backdrop.addEventListener("mousedown", this.handleSoftDismiss)), { opened: new Promise(t => { this.resolveOpen = t }), closed: new Promise(t => { this.resolveClose = t }) } } hide(t = {}) { let { payload: e } = t, i = { duration: 150, easing: "ease" }; this.backdrop.animate([{ opacity: 1 }, { opacity: 0 }], i), this.animateTo(this.dialogBox, [{ opacity: 1, transform: window.innerWidth > 640 ? "none" : `translateY(${this.offset ? `${this.offset}px` : "0"})` }, { opacity: 0, transform: window.innerWidth > 640 ? "scale(1.1)" : "translateY(100%)" },], i).finished.finally(() => { this.popupContainer.classList.add("hide"), this.dialogBox.style = "", this.removeAttribute("open"), this.forms.length && this.forms.forEach(t => t.reset()), this.dispatchEvent(new CustomEvent("popupclosed", { bubbles: !0, composed: !0, detail: { payload: e || this.payload } })), this.resolveClose(e || this.payload), this.isOpen = !1 }), popupStack.pop(), popupStack.items.length ? this.animateTo(popupStack.items[popupStack.items.length - 1].popup.shadowRoot.querySelector(".popup"), [{ transform: window.innerWidth > 640 ? "scale(0.95)" : "translateY(-1.5rem)" }, { transform: "none" },], i) : this.resumeScrolling(), this.resizeObserver.disconnect(), this.mutationObserver.disconnect(), this.removeEventListener("keydown", this.detectFocus), this.popupHeader.removeEventListener("touchstart", this.handleTouchStart, { passive: !0 }), this.backdrop.removeEventListener("mousedown", this.handleSoftDismiss) } handleTouchStart(t) { this.offset = 0, this.popupHeader.addEventListener("touchmove", this.handleTouchMove, { passive: !0 }), this.popupHeader.addEventListener("touchend", this.handleTouchEnd, { passive: !0 }), this.touchStartY = t.changedTouches[0].clientY, this.touchStartTime = t.timeStamp } handleTouchMove(t) { this.touchStartY < t.changedTouches[0].clientY && (this.offset = t.changedTouches[0].clientY - this.touchStartY, this.touchEndAnimation = window.requestAnimationFrame(() => { this.dialogBox.style.transform = `translateY(${this.offset}px)` })) } handleTouchEnd(t) { if (this.touchEndTime = t.timeStamp, cancelAnimationFrame(this.touchEndAnimation), this.touchEndY = t.changedTouches[0].clientY, this.threshold = .3 * this.dialogBox.getBoundingClientRect().height, this.touchEndTime - this.touchStartTime > 200) { if (this.touchEndY - this.touchStartY > this.threshold) { if (this.pinned) { this.setStateOpen(); return } this.hide() } else this.setStateOpen() } else if (this.touchEndY > this.touchStartY) { if (this.pinned) { this.setStateOpen(); return } this.hide() } this.popupHeader.removeEventListener("touchmove", this.handleTouchMove, { passive: !0 }), this.popupHeader.removeEventListener("touchend", this.handleTouchEnd, { passive: !0 }) } detectFocus(t) { if ("Tab" === t.key && this.focusable.length) { if (!this.firstFocusable) { for (let e = 0; e < this.focusable.length; e++)if (!this.focusable[e].disabled) { this.firstFocusable = this.focusable[e]; break } } if (!this.lastFocusable) { for (let i = this.focusable.length - 1; i >= 0; i--)if (!this.focusable[i].disabled) { this.lastFocusable = this.focusable[i]; break } } t.shiftKey && document.activeElement === this.firstFocusable ? (t.preventDefault(), this.lastFocusable.tagName.includes("SM-") ? this.lastFocusable.focusIn() : this.lastFocusable.focus()) : t.shiftKey || document.activeElement !== this.lastFocusable || (t.preventDefault(), this.firstFocusable.tagName.includes("SM-") ? this.firstFocusable.focusIn() : this.firstFocusable.focus()) } } updateFocusableList() { this.focusable = this.querySelectorAll('sm-button:not([disabled]), button:not([disabled]), [href], sm-input, input:not([readonly]), sm-select, select, sm-checkbox, sm-textarea, textarea, [tabindex]:not([tabindex="-1"])'), this.autoFocus = this.querySelector("[autofocus]"), this.firstFocusable = null, this.lastFocusable = null } handleSoftDismiss() { this.pinned ? this.dialogBox.animate([{ transform: "translateX(-1rem)" }, { transform: "translateX(1rem)" }, { transform: "translateX(-0.5rem)" }, { transform: "translateX(0.5rem)" }, { transform: "translateX(0)" },], { duration: 300, easing: "ease" }) : this.hide() } debounce(t, e) { let i = null; return (...s) => { window.clearTimeout(i), i = window.setTimeout(() => { t.apply(null, s) }, e) } } connectedCallback() { this.popupBodySlot.addEventListener("slotchange", this.debounce(() => { this.forms = this.querySelectorAll("sm-form"), this.updateFocusableList() }, 0)), this.resizeObserver = new ResizeObserver(t => { t.forEach(t => { if (t.contentBoxSize) { let e = Array.isArray(t.contentBoxSize) ? t.contentBoxSize[0] : t.contentBoxSize; this.threshold = .3 * e.blockSize.height } else this.threshold = .3 * t.contentRect.height }) }), this.mutationObserver = new MutationObserver(t => { this.updateFocusableList() }) } disconnectedCallback() { this.resizeObserver.disconnect(), this.mutationObserver.disconnect(), this.removeEventListener("keydown", this.detectFocus), this.popupHeader.removeEventListener("touchstart", this.handleTouchStart, { passive: !0 }), this.backdrop.removeEventListener("mousedown", this.handleSoftDismiss) } attributeChangedCallback(t) { "open" === t && this.hasAttribute("open") && this.show() } }); +const smSwitch = document.createElement("template"); smSwitch.innerHTML = '\t', customElements.define("sm-switch", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smSwitch.content.cloneNode(!0)), this.switch = this.shadowRoot.querySelector(".switch"), this.input = this.shadowRoot.querySelector("input"), this.isChecked = !1, this.isDisabled = !1, this.dispatch = this.dispatch.bind(this) } static get observedAttributes() { return ["disabled", "checked"] } get disabled() { return this.isDisabled } set disabled(e) { e ? this.setAttribute("disabled", "") : this.removeAttribute("disabled") } get checked() { return this.isChecked } set checked(e) { e ? this.setAttribute("checked", "") : this.removeAttribute("checked") } get value() { return this.isChecked } reset() { } dispatch() { this.dispatchEvent(new CustomEvent("change", { bubbles: !0, composed: !0, detail: { value: this.isChecked } })) } connectedCallback() { this.addEventListener("keydown", e => { " " !== e.key || this.isDisabled || (e.preventDefault(), this.input.click()) }), this.input.addEventListener("click", e => { this.input.checked ? this.checked = !0 : this.checked = !1, this.dispatch() }) } attributeChangedCallback(e, t, n) { t !== n && ("disabled" === e ? this.hasAttribute("disabled") ? this.disabled = !0 : this.disabled = !1 : "checked" === e && (this.hasAttribute("checked") ? (this.isChecked = !0, this.input.checked = !0) : (this.isChecked = !1, this.input.checked = !1))) } }); +const smSelect = document.createElement("template"); smSelect.innerHTML = '
', customElements.define("sm-select", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smSelect.content.cloneNode(!0)), this.focusIn = this.focusIn.bind(this), this.reset = this.reset.bind(this), this.open = this.open.bind(this), this.collapse = this.collapse.bind(this), this.toggle = this.toggle.bind(this), this.handleOptionsNavigation = this.handleOptionsNavigation.bind(this), this.handleOptionSelection = this.handleOptionSelection.bind(this), this.handleKeydown = this.handleKeydown.bind(this), this.handleClickOutside = this.handleClickOutside.bind(this), this.selectOption = this.selectOption.bind(this), this.debounce = this.debounce.bind(this), this.elementsChanged = this.elementsChanged.bind(this), this.availableOptions = [], this.previousOption, this.isOpen = !1, this.label = "", this.defaultSelected = "", this.isUnderViewport = !1, this.animationOptions = { duration: 300, fill: "forwards", easing: "ease" }, this.optionList = this.shadowRoot.querySelector(".options"), this.selection = this.shadowRoot.querySelector(".selection"), this.selectedOptionText = this.shadowRoot.querySelector(".selected-option-text") } static get observedAttributes() { return ["disabled", "label", "readonly"] } get value() { return this.getAttribute("value") } set value(t) { const e = this.availableOptions.find(e => e.getAttribute("value") === t); e ? (this.setAttribute("value", t), this.selectOption(e)) : console.warn(`There is no option with ${t} as value`) } debounce(t, e) { let n = null; return (...i) => { window.clearTimeout(n), n = window.setTimeout(() => { t.apply(null, i) }, e) } } reset(t = !0) { if (this.availableOptions[0] && this.previousOption !== this.availableOptions[0]) { const e = this.availableOptions.find(t => t.hasAttribute("selected")) || this.availableOptions[0]; this.value = e.getAttribute("value"), t && this.fireEvent() } } selectOption(t) { this.previousOption !== t && (this.querySelectorAll("[selected]").forEach(t => t.removeAttribute("selected")), this.selectedOptionText.textContent = `${this.label}${t.textContent}`, t.setAttribute("selected", ""), this.previousOption = t) } focusIn() { this.selection.focus() } open() { this.availableOptions.forEach(t => t.setAttribute("tabindex", 0)), this.optionList.classList.remove("hidden"), this.isUnderViewport = this.getBoundingClientRect().bottom + this.optionList.getBoundingClientRect().height > window.innerHeight, this.isUnderViewport ? this.setAttribute("isUnder", "") : this.removeAttribute("isUnder"), this.optionList.animate([{ transform: `translateY(${this.isUnderViewport ? "" : "-"}0.5rem)`, opacity: 0 }, { transform: "translateY(0)", opacity: 1 }], this.animationOptions), this.setAttribute("open", ""), this.style.zIndex = 1e3, (this.availableOptions.find(t => t.hasAttribute("selected")) || this.availableOptions[0]).focus(), document.addEventListener("mousedown", this.handleClickOutside), this.isOpen = !0 } collapse() { this.removeAttribute("open"), this.optionList.animate([{ transform: "translateY(0)", opacity: 1 }, { transform: `translateY(${this.isUnderViewport ? "" : "-"}0.5rem)`, opacity: 0 }], this.animationOptions).onfinish = (() => { this.availableOptions.forEach(t => t.removeAttribute("tabindex")), document.removeEventListener("mousedown", this.handleClickOutside), this.optionList.classList.add("hidden"), this.isOpen = !1, this.style.zIndex = "auto" }) } toggle() { this.isOpen || this.hasAttribute("disabled") ? this.collapse() : this.open() } fireEvent() { this.dispatchEvent(new CustomEvent("change", { bubbles: !0, composed: !0, detail: { value: this.value } })) } handleOptionsNavigation(t) { "ArrowUp" === t.key ? (t.preventDefault(), document.activeElement.previousElementSibling ? document.activeElement.previousElementSibling.focus() : this.availableOptions[this.availableOptions.length - 1].focus()) : "ArrowDown" === t.key && (t.preventDefault(), document.activeElement.nextElementSibling ? document.activeElement.nextElementSibling.focus() : this.availableOptions[0].focus()) } handleOptionSelection(t) { this.previousOption !== document.activeElement && (this.value = document.activeElement.getAttribute("value"), this.fireEvent()) } handleClick(t) { t.target === this ? this.toggle() : (this.handleOptionSelection(), this.collapse()) } handleKeydown(t) { t.target === this ? this.isOpen && "ArrowDown" === t.key ? (t.preventDefault(), (this.availableOptions.find(t => t.hasAttribute("selected")) || this.availableOptions[0]).focus(), this.handleOptionSelection(t)) : " " === t.key && (t.preventDefault(), this.toggle()) : (this.handleOptionsNavigation(t), this.handleOptionSelection(t), ["Enter", " ", "Escape", "Tab"].includes(t.key) && (t.preventDefault(), this.collapse(), this.focusIn())) } handleClickOutside(t) { this.isOpen && !this.contains(t.target) && this.collapse() } elementsChanged() { this.availableOptions = [...this.querySelectorAll("sm-option")], this.reset(!1), this.defaultSelected = this.value } connectedCallback() { this.setAttribute("role", "listbox"), this.hasAttribute("disabled") || this.hasAttribute("readonly") || (this.selection.setAttribute("tabindex", "0"), this.addEventListener("click", this.handleClick), this.addEventListener("keydown", this.handleKeydown)); const t = this.debounce(this.elementsChanged, 100); this.shadowRoot.querySelector("slot").addEventListener("slotchange", t), this.mutationObserver = new MutationObserver(e => { let n = !1; if (e.forEach(e => { switch (e.type) { case "childList": t(); break; case "attributes": n = !0 } }), n) { const t = this.availableOptions.find(t => t.hasAttribute("selected")) || this.availableOptions[0]; this.selectedOptionText.textContent = `${this.label}${t.textContent}`, this.setAttribute("value", t.getAttribute("value")) } }), this.mutationObserver.observe(this, { subtree: !0, childList: !0, attributeFilter: ["selected"] }), new IntersectionObserver((t, e) => { t.forEach(t => { if (t.isIntersecting) { const t = this.selection.getBoundingClientRect().left; t < window.innerWidth / 2 ? this.setAttribute("align-select", "left") : this.setAttribute("align-select", "right") } }) }).observe(this) } disconnectedCallback() { this.removeEventListener("click", this.handleClick), this.removeEventListener("keydown", this.handleKeydown) } attributeChangedCallback(t) { "disabled" === t || "readonly" === t ? this.hasAttribute("disabled") || this.hasAttribute("readonly") ? (this.selection.removeAttribute("tabindex"), this.removeEventListener("click", this.handleClick), this.removeEventListener("keydown", this.handleKeydown)) : (this.selection.setAttribute("tabindex", "0"), this.addEventListener("click", this.handleClick), this.addEventListener("keydown", this.handleKeydown)) : "label" === t && (this.label = this.hasAttribute("label") ? `${this.getAttribute("label")} ` : "") } }); const smOption = document.createElement("template"); smOption.innerHTML = '
', customElements.define("sm-option", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smOption.content.cloneNode(!0)) } connectedCallback() { this.setAttribute("role", "option") } }); +const spinner = document.createElement("template"); spinner.innerHTML = ''; class SpinnerLoader extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(spinner.content.cloneNode(!0)) } } window.customElements.define("sm-spinner", SpinnerLoader); +const tagsInput = document.createElement("template"); tagsInput.innerHTML = '

', customElements.define("tags-input", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(tagsInput.content.cloneNode(!0)), this.input = this.shadowRoot.querySelector("input"), this.tagsWrapper = this.shadowRoot.querySelector(".tags-wrapper"), this.placeholder = this.shadowRoot.querySelector(".placeholder"), this.reflectedAttributes = ["placeholder", "limit"], this.limit = void 0, this.tags = new Set, this.reset = this.reset.bind(this), this.handleInput = this.handleInput.bind(this), this.addTag = this.addTag.bind(this), this.handleKeydown = this.handleKeydown.bind(this), this.handleClick = this.handleClick.bind(this), this.removeTag = this.removeTag.bind(this) } static get observedAttributes() { return ["placeholder", "limit"] } get value() { return [...this.tags].filter(t => void 0 !== t) } set value(t) { this.reset(), [...new Set(t.filter(t => void 0 !== t))].forEach(t => this.addTag(t)) } get isValid() { return this.tags.size } focusIn() { this.input.focus() } reset() { for (this.input.value = "", this.tags.clear(); this.input.previousElementSibling;)this.input.previousElementSibling.remove() } addTag(t) { const e = document.createElement("span"); e.dataset.value = t, e.className = "tag", e.innerHTML = ` ${t} `, this.input.before(e), this.tags.add(t) } handleInput(t) { const e = t.target.value.trim().length; t.target.setAttribute("size", e || "3"), e ? this.placeholder.classList.add("hide") : e || this.tags.size || this.placeholder.classList.remove("hide") } handleKeydown(t) { if ("," !== t.key && "/" !== t.key || t.preventDefault(), "" !== t.target.value.trim()) { if ("Enter" === t.key || "," === t.key || "/" === t.key) { const e = t.target.value.trim(); if (this.tags.has(e) ? this.tagsWrapper.querySelector(`[data-value="${e}"]`).animate([{ backgroundColor: "initial" }, { backgroundColor: "var(--accent-color,teal)" }, { backgroundColor: "initial" }], { duration: 300, easing: "ease" }) : this.addTag(e), t.target.value = "", t.target.setAttribute("size", "3"), this.limit && this.limit < this.tags.size + 1) return void (this.input.readOnly = !0) } } else "Backspace" === t.key && this.input.previousElementSibling && this.removeTag(this.input.previousElementSibling), this.limit && this.limit > this.tags.size && (this.input.readOnly = !1) } handleClick(t) { t.target.closest(".tag") ? this.removeTag(t.target.closest(".tag")) : this.input.focus() } removeTag(t) { this.tags.delete(t.dataset.value), t.remove(), this.tags.size || this.placeholder.classList.remove("hide") } connectedCallback() { this.input.addEventListener("input", this.handleInput), this.input.addEventListener("keydown", this.handleKeydown), this.tagsWrapper.addEventListener("click", this.handleClick) } attributeChangedCallback(t, e, n) { "placeholder" === t && (this.placeholder.textContent = n), "limit" === t && (this.limit = parseInt(n)) } disconnectedCallback() { this.input.removeEventListener("input", this.handleInput), this.input.removeEventListener("keydown", this.handleKeydown), this.tagsWrapper.removeEventListener("click", this.handleClick) } }); +const smTextarea = document.createElement("template"); smTextarea.innerHTML = ' ', customElements.define("sm-textarea", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smTextarea.content.cloneNode(!0)), this.textarea = this.shadowRoot.querySelector("textarea"), this.textareaBox = this.shadowRoot.querySelector(".textarea"), this.placeholder = this.shadowRoot.querySelector(".placeholder"), this.reflectedAttributes = ["disabled", "required", "readonly", "rows", "minlength", "maxlength"], this.reset = this.reset.bind(this), this.focusIn = this.focusIn.bind(this), this.fireEvent = this.fireEvent.bind(this), this.checkInput = this.checkInput.bind(this) } static get observedAttributes() { return ["disabled", "value", "placeholder", "required", "readonly", "rows", "minlength", "maxlength"] } get value() { return this.textarea.value } set value(e) { this.setAttribute("value", e), this.fireEvent() } get disabled() { return this.hasAttribute("disabled") } set disabled(e) { e ? this.setAttribute("disabled", "") : this.removeAttribute("disabled") } get isValid() { return this.textarea.checkValidity() } reset() { this.setAttribute("value", "") } focusIn() { this.textarea.focus() } fireEvent() { let e = new Event("input", { bubbles: !0, cancelable: !0, composed: !0 }); this.dispatchEvent(e) } checkInput() { this.hasAttribute("placeholder") && "" !== this.getAttribute("placeholder") && ("" !== this.textarea.value ? this.placeholder.classList.add("hide") : this.placeholder.classList.remove("hide")) } connectedCallback() { this.textarea.addEventListener("input", e => { this.textareaBox.dataset.value = this.textarea.value, this.checkInput() }) } attributeChangedCallback(e, t, n) { this.reflectedAttributes.includes(e) ? this.hasAttribute(e) ? this.textarea.setAttribute(e, this.getAttribute(e) ? this.getAttribute(e) : "") : this.textContent.removeAttribute(e) : "placeholder" === e ? this.placeholder.textContent = this.getAttribute("placeholder") : "value" === e && (this.textarea.value = n, this.textareaBox.dataset.value = n, this.checkInput()) } }); +const themeToggle = document.createElement("template"); themeToggle.innerHTML = ' '; class ThemeToggle extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(themeToggle.content.cloneNode(!0)), this.isChecked = !1, this.hasTheme = "light", this.toggleState = this.toggleState.bind(this), this.fireEvent = this.fireEvent.bind(this), this.handleThemeChange = this.handleThemeChange.bind(this) } static get observedAttributes() { return ["checked"] } daylight() { this.hasTheme = "light", document.body.dataset.theme = "light", this.setAttribute("aria-checked", "false") } nightlight() { this.hasTheme = "dark", document.body.dataset.theme = "dark", this.setAttribute("aria-checked", "true") } toggleState() { this.toggleAttribute("checked"), this.fireEvent() } handleKeyDown(e) { " " === e.key && this.toggleState() } handleThemeChange(e) { e.detail.theme !== this.hasTheme && ("dark" === e.detail.theme ? this.setAttribute("checked", "") : this.removeAttribute("checked")) } fireEvent() { this.dispatchEvent(new CustomEvent("themechange", { bubbles: !0, composed: !0, detail: { theme: this.hasTheme } })) } connectedCallback() { this.setAttribute("role", "switch"), this.setAttribute("aria-label", "theme toggle"), "dark" === localStorage.getItem(`${window.location.hostname}-theme`) ? (this.nightlight(), this.setAttribute("checked", "")) : "light" === localStorage.getItem(`${window.location.hostname}-theme`) ? (this.daylight(), this.removeAttribute("checked")) : window.matchMedia("(prefers-color-scheme: dark)").matches ? (this.nightlight(), this.setAttribute("checked", "")) : (this.daylight(), this.removeAttribute("checked")), this.addEventListener("click", this.toggleState), this.addEventListener("keydown", this.handleKeyDown), document.addEventListener("themechange", this.handleThemeChange) } disconnectedCallback() { this.removeEventListener("click", this.toggleState), this.removeEventListener("keydown", this.handleKeyDown), document.removeEventListener("themechange", this.handleThemeChange) } attributeChangedCallback(e, t, n) { "checked" === e && (this.hasAttribute("checked") ? (this.nightlight(), localStorage.setItem(`${window.location.hostname}-theme`, "dark")) : (this.daylight(), localStorage.setItem(`${window.location.hostname}-theme`, "light"))) } } window.customElements.define("theme-toggle", ThemeToggle); \ No newline at end of file diff --git a/floBlockchainAPI.js b/floBlockchainAPI.js new file mode 100644 index 0000000..9dedb90 --- /dev/null +++ b/floBlockchainAPI.js @@ -0,0 +1,1044 @@ +(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://blockbook.ranchimall.net/'], + FLO_TEST: [] + }, + sendAmt: 0.0003, + fee: 0.0002, + minChangeAmt: 0.0002, + receiverID: floGlobals.adminID + }; + + const SATOSHI_IN_BTC = 1e8; + const isUndefined = val => typeof val === 'undefined'; + + const util = floBlockchainAPI.util = {}; + + util.Sat_to_FLO = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8)); + util.FLO_to_Sat = value => parseInt(value * SATOSHI_IN_BTC); + util.toFixed = value => parseFloat((value).toFixed(8)); + + Object.defineProperties(floBlockchainAPI, { + sendAmt: { + get: () => DEFAULT.sendAmt, + set: amt => !isNaN(amt) ? DEFAULT.sendAmt = amt : null + }, + fee: { + get: () => DEFAULT.fee, + set: fee => !isNaN(fee) ? DEFAULT.fee = fee : null + }, + defaultReceiver: { + get: () => DEFAULT.receiverID, + set: floID => DEFAULT.receiverID = floID + }, + blockchain: { + get: () => DEFAULT.blockchain + } + }); + + if (floGlobals.sendAmt) floBlockchainAPI.sendAmt = floGlobals.sendAmt; + if (floGlobals.fee) floBlockchainAPI.fee = floGlobals.fee; + + Object.defineProperties(floGlobals, { + sendAmt: { + get: () => DEFAULT.sendAmt, + set: amt => !isNaN(amt) ? DEFAULT.sendAmt = amt : null + }, + fee: { + get: () => DEFAULT.fee, + set: fee => !isNaN(fee) ? DEFAULT.fee = fee : null + } + }); + + const allServerList = new Set(floGlobals.apiURL && floGlobals.apiURL[DEFAULT.blockchain] ? floGlobals.apiURL[DEFAULT.blockchain] : DEFAULT.apiURL[DEFAULT.blockchain]); + + var serverList = Array.from(allServerList); + var curPos = floCrypto.randInt(0, serverList.length - 1); + + function fetch_retry(apicall, rm_node) { + return new Promise((resolve, reject) => { + let i = serverList.indexOf(rm_node) + if (i != -1) serverList.splice(i, 1); + curPos = floCrypto.randInt(0, serverList.length - 1); + fetch_api(apicall, false) + .then(result => resolve(result)) + .catch(error => reject(error)); + }) + } + + function fetch_api(apicall, ic = true) { + return new Promise((resolve, reject) => { + if (serverList.length === 0) { + if (ic) { + serverList = Array.from(allServerList); + curPos = floCrypto.randInt(0, serverList.length - 1); + fetch_api(apicall, false) + .then(result => resolve(result)) + .catch(error => reject(error)); + } else + reject("No FLO blockbook server working"); + } else { + let serverURL = serverList[curPos]; + fetch(serverURL + apicall).then(response => { + if (response.ok) + response.json().then(data => resolve(data)); + else { + fetch_retry(apicall, serverURL) + .then(result => resolve(result)) + .catch(error => reject(error)); + } + }).catch(error => { + fetch_retry(apicall, serverURL) + .then(result => resolve(result)) + .catch(error => reject(error)); + }) + } + }) + } + + Object.defineProperties(floBlockchainAPI, { + serverList: { + get: () => Array.from(serverList) + }, + current_server: { + get: () => serverList[curPos] + } + }); + + //Promised function to get data from API + const promisedAPI = floBlockchainAPI.promisedAPI = floBlockchainAPI.fetch = function (apicall, query_params = undefined) { + return new Promise((resolve, reject) => { + 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)); + }); + } + + //Get balance for the given Address + const getBalance = floBlockchainAPI.getBalance = function (addr) { + return new Promise((resolve, reject) => { + let api = `api/address/${addr}`; + promisedAPI(api, { details: "basic" }) + .then(result => resolve(result["balance"])) + .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 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)) + }) + + //create a transaction with single sender + const createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + else if (!floCrypto.validateFloID(senderAddr, true)) + return reject(`Invalid address : ${senderAddr}`); + else if (!floCrypto.validateFloID(receiverAddr)) + return reject(`Invalid address : ${receiverAddr}`); + else if (typeof sendAmt !== 'number' || sendAmt <= 0) + return reject(`Invalid sendAmt : ${sendAmt}`); + + getBalance(senderAddr).then(balance => { + var fee = DEFAULT.fee; + if (balance < sendAmt + fee) + return reject("Insufficient FLO balance!"); + 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)) + }) + } + + floBlockchainAPI.createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + createTx(senderAddr, receiverAddr, sendAmt, floData, strict_utxo) + .then(trx => resolve(trx.serialize())) + .catch(error => reject(error)) + }) + } + + //Send Tx to blockchain + const sendTx = floBlockchainAPI.sendTx = function (senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + if (!floCrypto.validateFloID(senderAddr, true)) + return reject(`Invalid address : ${senderAddr}`); + else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr)) + return reject("Invalid Private key!"); + createTx(senderAddr, receiverAddr, sendAmt, floData, strict_utxo).then(trx => { + var signedTxHash = trx.sign(privKey, 1); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }); + } + + //Write Data into blockchain + floBlockchainAPI.writeData = function (senderAddr, data, privKey, receiverAddr = DEFAULT.receiverID, options = {}) { + let strict_utxo = options.strict_utxo === false ? false : true, + sendAmt = isNaN(options.sendAmt) ? DEFAULT.sendAmt : options.sendAmt; + return new Promise((resolve, reject) => { + if (typeof data != "string") + data = JSON.stringify(data); + sendTx(senderAddr, receiverAddr, sendAmt, privKey, data, strict_utxo) + .then(txid => resolve(txid)) + .catch(error => reject(error)); + }); + } + + //merge all UTXOs of a given floID into a single UTXO + floBlockchainAPI.mergeUTXOs = function (floID, privKey, 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 trx = bitjs.transaction(); + var utxoAmt = 0.0; + var fee = DEFAULT.fee; + getUTXOs(floID).then(utxos => { + for (var i = utxos.length - 1; i >= 0; i--) + if (utxos[i].confirmations) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + } + trx.addoutput(floID, utxoAmt - fee); + 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)) + }) + } + + //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 + * @param {Array} receivers List of receivers + * @param {boolean} preserveRatio (optional) preserve ratio or equal contribution + * @return {Promise} + */ + floBlockchainAPI.writeDataMultiple = function (senderPrivKeys, data, receivers = [DEFAULT.receiverID], options = {}) { + return new Promise((resolve, reject) => { + if (!Array.isArray(senderPrivKeys)) + return reject("Invalid senderPrivKeys: SenderPrivKeys must be Array"); + if (options.preserveRatio === false) { + let tmp = {}; + let amount = (DEFAULT.sendAmt * receivers.length) / senderPrivKeys.length; + senderPrivKeys.forEach(key => tmp[key] = amount); + senderPrivKeys = tmp; + } + if (!Array.isArray(receivers)) + return reject("Invalid receivers: Receivers must be Array"); + else { + let tmp = {}; + let amount = options.sendAmt || DEFAULT.sendAmt; + receivers.forEach(floID => tmp[floID] = amount); + receivers = tmp + } + if (typeof data != "string") + data = JSON.stringify(data); + sendTxMultiple(senderPrivKeys, receivers, data) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }) + } + + /**Send Tx from (and/or) to multiple floID + * @param {Array or Object} senderPrivKeys List of sender private-key (optional: with coins to be sent) + * @param {Object} receivers List of receivers with respective amount to be sent + * @param {string} floData FLO data of the txn + * @return {Promise} + */ + const sendTxMultiple = floBlockchainAPI.sendTxMultiple = function (senderPrivKeys, receivers, floData = '') { + return new Promise((resolve, reject) => { + if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + let senders = {}, + preserveRatio; + //check for argument validations + try { + let invalids = { + InvalidSenderPrivKeys: [], + InvalidSenderAmountFor: [], + InvalidReceiverIDs: [], + InvalidReceiveAmountFor: [] + } + let inputVal = 0, + outputVal = 0; + //Validate sender privatekeys (and send amount if passed) + //conversion when only privateKeys are passed (preserveRatio mode) + if (Array.isArray(senderPrivKeys)) { + senderPrivKeys.forEach(key => { + try { + if (!key) + invalids.InvalidSenderPrivKeys.push(key); + else { + let floID = floCrypto.getFloID(key); + senders[floID] = { + wif: key + } + } + } catch (error) { + invalids.InvalidSenderPrivKeys.push(key) + } + }) + preserveRatio = true; + } + //conversion when privatekeys are passed with send amount + else { + for (let key in senderPrivKeys) { + try { + if (!key) + invalids.InvalidSenderPrivKeys.push(key); + else { + if (typeof senderPrivKeys[key] !== 'number' || senderPrivKeys[key] <= 0) + invalids.InvalidSenderAmountFor.push(key); + else + inputVal += senderPrivKeys[key]; + let floID = floCrypto.getFloID(key); + senders[floID] = { + wif: key, + coins: senderPrivKeys[key] + } + } + } catch (error) { + invalids.InvalidSenderPrivKeys.push(key) + } + } + preserveRatio = false; + } + //Validate the receiver IDs and receive amount + for (let floID in receivers) { + if (!floCrypto.validateFloID(floID)) + invalids.InvalidReceiverIDs.push(floID); + if (typeof receivers[floID] !== 'number' || receivers[floID] <= 0) + invalids.InvalidReceiveAmountFor.push(floID); + else + outputVal += receivers[floID]; + } + //Reject if any invalids are found + for (let i in invalids) + if (!invalids[i].length) + delete invalids[i]; + if (Object.keys(invalids).length) + return reject(invalids); + //Reject if given inputVal and outputVal are not equal + if (!preserveRatio && inputVal != outputVal) + return reject(`Input Amount (${inputVal}) not equal to Output Amount (${outputVal})`); + } catch (error) { + return reject(error) + } + //Get balance of senders + let promises = []; + for (let floID in senders) + promises.push(getBalance(floID)); + Promise.all(promises).then(results => { + let totalBalance = 0, + totalFee = DEFAULT.fee, + balance = {}; + //Divide fee among sender if not for preserveRatio + if (!preserveRatio) + var dividedFee = totalFee / Object.keys(senders).length; + //Check if balance of each sender is sufficient enough + let insufficient = []; + for (let floID in senders) { + balance[floID] = parseFloat(results.shift()); + if (isNaN(balance[floID]) || (preserveRatio && balance[floID] <= totalFee) || + (!preserveRatio && balance[floID] < senders[floID].coins + dividedFee)) + insufficient.push(floID); + totalBalance += balance[floID]; + } + if (insufficient.length) + return reject({ + InsufficientBalance: insufficient + }) + //Calculate totalSentAmount and check if totalBalance is sufficient + let totalSendAmt = totalFee; + for (let floID in receivers) + totalSendAmt += receivers[floID]; + if (totalBalance < totalSendAmt) + return reject("Insufficient total Balance"); + //Get the UTXOs of the senders + let promises = []; + for (let floID in senders) + promises.push(getUTXOs(floID)); + Promise.all(promises).then(results => { + var trx = bitjs.transaction(); + for (let floID in senders) { + let utxos = results.shift(); + let sendAmt; + if (preserveRatio) { + let ratio = (balance[floID] / totalBalance); + sendAmt = totalSendAmt * ratio; + } else + sendAmt = senders[floID].coins + dividedFee; + let utxoAmt = 0.0; + for (let i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt); i--) { + if (utxos[i].confirmations) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + } + } + if (utxoAmt < sendAmt) + return reject("Insufficient balance:" + floID); + let change = (utxoAmt - sendAmt); + if (change > 0) + trx.addoutput(floID, change); + } + for (let floID in receivers) + trx.addoutput(floID, receivers[floID]); + trx.addflodata(floData.replace(/\n/g, ' ')); + for (let floID in senders) + trx.sign(senders[floID].wif, 1); + var signedTxHash = trx.serialize(); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //Create a multisig transaction + const createMultisigTx = function (redeemScript, receivers, amounts, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + var multisig = floCrypto.decodeRedeemScript(redeemScript); + + //validate multisig script and flodata + if (!multisig) + return reject(`Invalid redeemScript`); + var senderAddr = multisig.address; + if (!floCrypto.validateFloID(senderAddr)) + return reject(`Invalid multisig : ${senderAddr}`); + else if (!floCrypto.validateASCII(floData)) + return reject("Invalid FLO_Data: only printable ASCII characters are allowed"); + //validate receiver addresses + if (!Array.isArray(receivers)) + receivers = [receivers]; + for (let r of receivers) + if (!floCrypto.validateFloID(r)) + return reject(`Invalid address : ${r}`); + //validate amounts + if (!Array.isArray(amounts)) + amounts = [amounts]; + if (amounts.length != receivers.length) + return reject("Receivers and amounts have different length"); + var sendAmt = 0; + for (let a of amounts) { + if (typeof a !== 'number' || a <= 0) + return reject(`Invalid amount : ${a}`); + sendAmt += a; + } + + getBalance(senderAddr).then(balance => { + var fee = DEFAULT.fee; + if (balance < sendAmt + fee) + return reject("Insufficient FLO balance!"); + 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)) + }); + } + + //Same as above, but explict call should return serialized tx-hex + floBlockchainAPI.createMultisigTx = function (redeemScript, receivers, amounts, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + createMultisigTx(redeemScript, receivers, amounts, floData, strict_utxo) + .then(trx => resolve(trx.serialize())) + .catch(error => reject(error)) + }) + } + + //Create and send multisig transaction + const sendMultisigTx = floBlockchainAPI.sendMultisigTx = function (redeemScript, privateKeys, receivers, amounts, floData = '', strict_utxo = true) { + return new Promise((resolve, reject) => { + var multisig = floCrypto.decodeRedeemScript(redeemScript); + if (!multisig) + return reject(`Invalid redeemScript`); + if (privateKeys.length < multisig.required) + return reject(`Insufficient privateKeys (required ${multisig.required})`); + for (let pk of privateKeys) { + var flag = false; + for (let pub of multisig.pubkeys) + if (floCrypto.verifyPrivKey(pk, pub, false)) + flag = true; + if (!flag) + return reject(`Invalid Private key`); + } + createMultisigTx(redeemScript, receivers, amounts, floData, strict_utxo).then(trx => { + for (let pk of privateKeys) + trx.sign(pk, 1); + var signedTxHash = trx.serialize(); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + floBlockchainAPI.writeMultisigData = function (redeemScript, data, privatekeys, receiverAddr = DEFAULT.receiverID, options = {}) { + let strict_utxo = options.strict_utxo === false ? false : true, + sendAmt = isNaN(options.sendAmt) ? DEFAULT.sendAmt : options.sendAmt; + return new Promise((resolve, reject) => { + if (!floCrypto.validateFloID(receiverAddr)) + return reject(`Invalid receiver: ${receiverAddr}`); + sendMultisigTx(redeemScript, privatekeys, receiverAddr, sendAmt, data, strict_utxo) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }) + } + + function deserializeTx(tx) { + if (typeof tx === 'string' || Array.isArray(tx)) { + try { + tx = bitjs.transaction(tx); + } catch { + throw "Invalid transaction hex"; + } + } else if (typeof tx !== 'object' || typeof tx.sign !== 'function') + throw "Invalid transaction object"; + return tx; + } + + floBlockchainAPI.signTx = function (tx, privateKey, sighashtype = 1) { + if (!floCrypto.getFloID(privateKey)) + throw "Invalid Private key"; + //deserialize if needed + tx = deserializeTx(tx); + var signedTxHex = tx.sign(privateKey, sighashtype); + return signedTxHex; + } + + const checkSigned = floBlockchainAPI.checkSigned = function (tx, bool = true) { + tx = deserializeTx(tx); + let n = []; + for (let i = 0; i < tx.inputs.length; i++) { + var s = tx.scriptDecode(i); + if (s['type'] === 'scriptpubkey') + n.push(s.signed); + else if (s['type'] === 'multisig') { + var rs = tx.decodeRedeemScript(s['rs']); + let x = { + s: 0, + r: rs['required'], + t: rs['pubkeys'].length + }; + //check input script for signatures + var script = Array.from(tx.inputs[i].script); + if (script[0] == 0) { //script with signatures + script = tx.parseScript(script); + for (var k = 0; k < script.length; k++) + if (Array.isArray(script[k]) && script[k][0] == 48) //0x30 DERSequence + x.s++; + } + //validate counts + if (x.r > x.t) + throw "signaturesRequired is more than publicKeys"; + else if (x.s < x.r) + n.push(x); + else + n.push(true); + } + } + return bool ? !(n.filter(x => x !== true).length) : n; + } + + floBlockchainAPI.checkIfSameTx = function (tx1, tx2) { + tx1 = deserializeTx(tx1); + tx2 = deserializeTx(tx2); + //compare input and output length + if (tx1.inputs.length !== tx2.inputs.length || tx1.outputs.length !== tx2.outputs.length) + return false; + //compare flodata + if (tx1.floData !== tx2.floData) + return false + //compare inputs + for (let i = 0; i < tx1.inputs.length; i++) + if (tx1.inputs[i].outpoint.hash !== tx2.inputs[i].outpoint.hash || tx1.inputs[i].outpoint.index !== tx2.inputs[i].outpoint.index) + return false; + //compare outputs + for (let i = 0; i < tx1.outputs.length; i++) + if (tx1.outputs[i].value !== tx2.outputs[i].value || Crypto.util.bytesToHex(tx1.outputs[i].script) !== Crypto.util.bytesToHex(tx2.outputs[i].script)) + return false; + return true; + } + + floBlockchainAPI.transactionID = function (tx) { + tx = deserializeTx(tx); + let clone = bitjs.clone(tx); + let raw_bytes = Crypto.util.hexToBytes(clone.serialize()); + let txid = Crypto.SHA256(Crypto.SHA256(raw_bytes, { asBytes: true }), { asBytes: true }).reverse(); + return Crypto.util.bytesToHex(txid); + } + + const getTxOutput = (txid, i) => new Promise((resolve, reject) => { + promisedAPI(`api/tx/${txid}`) + .then(result => resolve(result.vout[i])) + .catch(error => reject(error)) + }); + + function getOutputAddress(outscript) { + var bytes, version; + switch (outscript[0]) { + case 118: //legacy + bytes = outscript.slice(3, outscript.length - 2); + version = bitjs.pub; + break + case 169: //multisig + bytes = outscript.slice(2, outscript.length - 1); + version = bitjs.multisig; + break; + default: return; //unknown + } + bytes.unshift(version); + var hash = Crypto.SHA256(Crypto.SHA256(bytes, { asBytes: true }), { asBytes: true }); + var checksum = hash.slice(0, 4); + return bitjs.Base58.encode(bytes.concat(checksum)); + } + + floBlockchainAPI.parseTransaction = function (tx) { + return new Promise((resolve, reject) => { + tx = deserializeTx(tx); + let result = {}; + let promises = []; + //Parse Inputs + for (let i = 0; i < tx.inputs.length; i++) + promises.push(getTxOutput(tx.inputs[i].outpoint.hash, tx.inputs[i].outpoint.index)); + Promise.all(promises).then(inputs => { + result.inputs = inputs.map(inp => Object({ + address: inp.scriptPubKey.addresses[0], + value: parseFloat(inp.value) + })); + let signed = checkSigned(tx, false); + result.inputs.forEach((inp, i) => inp.signed = signed[i]); + //Parse Outputs + result.outputs = tx.outputs.map(out => Object({ + address: getOutputAddress(out.script), + value: util.Sat_to_FLO(out.value) + })) + //Parse Totals + result.total_input = parseFloat(result.inputs.reduce((a, inp) => a += inp.value, 0).toFixed(8)); + result.total_output = parseFloat(result.outputs.reduce((a, out) => a += out.value, 0).toFixed(8)); + result.fee = parseFloat((result.total_input - result.total_output).toFixed(8)); + result.floData = tx.floData; + resolve(result); + }).catch(error => reject(error)) + }) + } + + //Broadcast signed Tx in blockchain using API + const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) { + return new Promise((resolve, reject) => { + if (signedTxHash.length < 1) + return reject("Empty Transaction Data"); + + promisedAPI('/api/sendtx/' + signedTxHash) + .then(response => resolve(response["result"])) + .catch(error => reject(error)) + }) + } + + const getTx = floBlockchainAPI.getTx = function (txid) { + return new Promise((resolve, reject) => { + promisedAPI(`api/tx/${txid}`) + .then(response => resolve(response)) + .catch(error => reject(error)) + }) + } + + /**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 + const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) { + return new Promise((resolve, reject) => { + //API options + 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)) + }); + } + + //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 => { + 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({ + 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 + 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 + pattern : filters data that with JSON pattern + filter : custom filter funtion for floData (eg . filter: d => {return d[0] == '$'}) + 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 + */ + floBlockchainAPI.readData = function (addr, options = {}) { + return new Promise((resolve, reject) => { + + //fetch options + let query_options = {}; + query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: ignore unconfirmed tx + + 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]; + + //filter the txs based on options + const filteredData = response.items.filter(tx => { + + if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query + return false; + + if (options.sentOnly && !tx.vin.some(vin => vin.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; + + if (options.pattern) { + try { + let jsonContent = JSON.parse(tx.floData); + if (!Object.keys(jsonContent).includes(options.pattern)) + return false; + } catch { + return false; + } + } + + if (options.filter && !options.filter(tx.floData)) + return false; + + return true; + }).map(tx => options.tx ? { + txid: tx.txid, + time: tx.time, + blockheight: tx.blockheight, + senders: new Set(tx.vin.map(v => v.addresses[0])), + receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])), + data: tx.floData + } : tx.floData); + + const result = { lastItem: response.lastItem }; + if (options.tx) + result.items = filteredData; + else + result.data = filteredData + resolve(result); + + }).catch(error => reject(error)) + }) + } + + /*Get the latest flo Data that match the caseFn from txs of given Address + caseFn: (function) flodata => return bool value + options can be used to filter data + after : query after the given txid + 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/floCloudAPI.js b/floCloudAPI.js new file mode 100644 index 0000000..db50d11 --- /dev/null +++ b/floCloudAPI.js @@ -0,0 +1,1106 @@ +(function (EXPORTS) { //floCloudAPI v2.4.5 + /* FLO Cloud operations to send/request application data*/ + 'use strict'; + const floCloudAPI = EXPORTS; + + const DEFAULT = { + blockchainPrefix: 0x23, //Prefix version for FLO blockchain + SNStorageID: floGlobals.SNStorageID || "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk", + adminID: floGlobals.adminID, + application: floGlobals.application, + SNStorageName: "SuperNodeStorage", + callback: (d, e) => console.debug(d, e) + }; + + var user_id, user_public, user_private, aes_key; + + function user(id, priv) { + if (!priv || !id) + return user.clear(); + let pub = floCrypto.getPubKeyHex(priv); + if (!pub || !floCrypto.verifyPubKey(pub, id)) + return user.clear(); + let n = floCrypto.randInt(12, 20); + aes_key = floCrypto.randString(n); + user_private = Crypto.AES.encrypt(priv, aes_key); + user_public = pub; + user_id = id; + return user_id; + } + + Object.defineProperties(user, { + id: { + get: () => { + if (!user_id) + throw "User not set"; + return user_id; + } + }, + public: { + get: () => { + if (!user_public) + throw "User not set"; + return user_public; + } + }, + sign: { + value: msg => { + if (!user_private) + throw "User not set"; + return floCrypto.signData(msg, Crypto.AES.decrypt(user_private, aes_key)); + } + }, + clear: { + value: () => user_id = user_public = user_private = aes_key = undefined + } + }) + + Object.defineProperties(floCloudAPI, { + SNStorageID: { + get: () => DEFAULT.SNStorageID + }, + SNStorageName: { + get: () => DEFAULT.SNStorageName + }, + adminID: { + get: () => DEFAULT.adminID + }, + application: { + get: () => DEFAULT.application + }, + user: { + get: () => user + } + }); + + var appObjects, generalData, lastVC; + Object.defineProperties(floGlobals, { + appObjects: { + get: () => appObjects, + set: obj => appObjects = obj + }, + generalData: { + get: () => generalData, + set: data => generalData = data + }, + generalDataset: { + value: (type, options = {}) => generalData[filterKey(type, options)] + }, + lastVC: { + get: () => lastVC, + set: vc => lastVC = vc + } + }); + + var supernodes = {}; //each supnernode must be stored as floID : {uri:,pubKey:} + Object.defineProperty(floCloudAPI, 'nodes', { + get: () => JSON.parse(JSON.stringify(supernodes)) + }); + + var kBucket; + const K_Bucket = floCloudAPI.K_Bucket = function (masterID, nodeList) { + + const decodeID = floID => { + let k = bitjs.Base58.decode(floID); + k.shift(); + k.splice(-4, 4); + let decodedId = Crypto.util.bytesToHex(k); + let nodeIdBigInt = new BigInteger(decodedId, 16); + let nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned(); + let nodeIdNewInt8Array = new Uint8Array(nodeIdBytes); + return nodeIdNewInt8Array; + }; + + const _KB = new BuildKBucket({ + localNodeId: decodeID(masterID) + }); + nodeList.forEach(id => _KB.add({ + id: decodeID(id), + floID: id + })); + + const _CO = nodeList.map(id => [_KB.distance(_KB.localNodeId, decodeID(id)), id]) + .sort((a, b) => a[0] - b[0]) + .map(a => a[1]); + + const self = this; + Object.defineProperty(self, 'tree', { + get: () => _KB + }); + Object.defineProperty(self, 'list', { + get: () => Array.from(_CO) + }); + + self.isNode = floID => _CO.includes(floID); + self.innerNodes = function (id1, id2) { + if (!_CO.includes(id1) || !_CO.includes(id2)) + throw Error('Given nodes are not supernode'); + let iNodes = [] + for (let i = _CO.indexOf(id1) + 1; _CO[i] != id2; i++) { + if (i < _CO.length) + iNodes.push(_CO[i]) + else i = -1 + } + return iNodes + } + self.outterNodes = function (id1, id2) { + if (!_CO.includes(id1) || !_CO.includes(id2)) + throw Error('Given nodes are not supernode'); + let oNodes = [] + for (let i = _CO.indexOf(id2) + 1; _CO[i] != id1; i++) { + if (i < _CO.length) + oNodes.push(_CO[i]) + else i = -1 + } + return oNodes + } + self.prevNode = function (id, N = 1) { + let n = N || _CO.length; + if (!_CO.includes(id)) + throw Error('Given node is not supernode'); + let pNodes = [] + for (let i = 0, j = _CO.indexOf(id) - 1; i < n; j--) { + if (j == _CO.indexOf(id)) + break; + else if (j > -1) + pNodes[i++] = _CO[j] + else j = _CO.length + } + return (N == 1 ? pNodes[0] : pNodes) + } + self.nextNode = function (id, N = 1) { + let n = N || _CO.length; + if (!_CO.includes(id)) + throw Error('Given node is not supernode'); + if (!n) n = _CO.length; + let nNodes = [] + for (let i = 0, j = _CO.indexOf(id) + 1; i < n; j++) { + if (j == _CO.indexOf(id)) + break; + else if (j < _CO.length) + nNodes[i++] = _CO[j] + else j = -1 + } + return (N == 1 ? nNodes[0] : nNodes) + } + self.closestNode = function (id, N = 1) { + let decodedId = decodeID(id); + let n = N || _CO.length; + let cNodes = _KB.closest(decodedId, n) + .map(k => k.floID) + return (N == 1 ? cNodes[0] : cNodes) + } + } + + floCloudAPI.init = function startCloudProcess(nodes) { + return new Promise((resolve, reject) => { + try { + supernodes = nodes; + kBucket = new K_Bucket(DEFAULT.SNStorageID, Object.keys(supernodes)); + resolve('Cloud init successful'); + } catch (error) { + reject(error); + } + }) + } + + Object.defineProperty(floCloudAPI, 'kBucket', { + get: () => kBucket + }); + + const _inactive = new Set(); + + function ws_connect(snID) { + return new Promise((resolve, reject) => { + if (!(snID in supernodes)) + return reject(`${snID} is not a supernode`) + if (_inactive.has(snID)) + return reject(`${snID} is not active`) + var wsConn = new WebSocket("wss://" + supernodes[snID].uri + "/"); + wsConn.onopen = evt => resolve(wsConn); + wsConn.onerror = evt => { + _inactive.add(snID) + reject(`${snID} is unavailable`) + } + }) + } + + function ws_activeConnect(snID, reverse = false) { + return new Promise((resolve, reject) => { + if (_inactive.size === kBucket.list.length) + return reject('Cloud offline'); + if (!(snID in supernodes)) + snID = kBucket.closestNode(proxyID(snID)); + ws_connect(snID) + .then(node => resolve(node)) + .catch(error => { + if (reverse) + var nxtNode = kBucket.prevNode(snID); + else + var nxtNode = kBucket.nextNode(snID); + ws_activeConnect(nxtNode, reverse) + .then(node => resolve(node)) + .catch(error => reject(error)) + }) + }) + } + + function fetch_API(snID, data) { + return new Promise((resolve, reject) => { + if (_inactive.has(snID)) + return reject(`${snID} is not active`); + let fetcher, sn_url = "https://" + supernodes[snID].uri; + if (typeof data === "string") + fetcher = fetch(sn_url + "?" + data); + else if (typeof data === "object" && data.method === "POST") + fetcher = fetch(sn_url, data); + fetcher.then(response => { + if (response.ok || response.status === 400 || response.status === 500) + resolve(response); + else + reject(response); + }).catch(error => reject(error)) + }) + } + + function fetch_ActiveAPI(snID, data, reverse = false) { + return new Promise((resolve, reject) => { + if (_inactive.size === kBucket.list.length) + return reject('Cloud offline'); + if (!(snID in supernodes)) + snID = kBucket.closestNode(proxyID(snID)); + fetch_API(snID, data) + .then(result => resolve(result)) + .catch(error => { + _inactive.add(snID) + if (reverse) + var nxtNode = kBucket.prevNode(snID); + else + var nxtNode = kBucket.nextNode(snID); + fetch_ActiveAPI(nxtNode, data, reverse) + .then(result => resolve(result)) + .catch(error => reject(error)); + }) + }) + } + + function singleRequest(floID, data_obj, method = "POST") { + return new Promise((resolve, reject) => { + let data; + if (method === "POST") + data = { + method: "POST", + body: JSON.stringify(data_obj) + }; + else + data = new URLSearchParams(JSON.parse(JSON.stringify(data_obj))).toString(); + fetch_ActiveAPI(floID, data).then(response => { + if (response.ok) + response.json() + .then(result => resolve(result)) + .catch(error => reject(error)) + else response.text() + .then(result => reject(response.status + ": " + result)) //Error Message from Node + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + const _liveRequest = {}; + + function liveRequest(floID, request, callback) { + const filterData = typeof request.status !== 'undefined' ? + data => { + if (request.status) + return data; + else { + let filtered = {}; + for (let i in data) + if (request.trackList.includes(i)) + filtered[i] = data[i]; + return filtered; + } + } : + data => { + data = objectifier(data); + let filtered = {}, + proxy = proxyID(request.receiverID), + r = request; + for (let v in data) { + let d = data[v]; + if ((!r.atVectorClock || r.atVectorClock == v) && + (r.atVectorClock || !r.lowerVectorClock || r.lowerVectorClock <= v) && + (r.atVectorClock || !r.upperVectorClock || r.upperVectorClock >= v) && + (!r.afterTime || r.afterTime < d.log_time) && + r.application == d.application && + (proxy == d.receiverID || proxy == d.proxyID) && + (!r.comment || r.comment == d.comment) && + (!r.type || r.type == d.type) && + (!r.senderID || r.senderID.includes(d.senderID))) + filtered[v] = data[v]; + } + return filtered; + }; + + return new Promise((resolve, reject) => { + ws_activeConnect(floID).then(node => { + let randID = floCrypto.randString(5); + node.send(JSON.stringify(request)); + node.onmessage = (evt) => { + let d = null, + e = null; + try { + d = filterData(JSON.parse(evt.data)); + } catch (error) { + e = evt.data + } finally { + callback(d, e) + } + } + _liveRequest[randID] = node; + _liveRequest[randID].request = request; + resolve(randID); + }).catch(error => reject(error)); + }); + } + + Object.defineProperty(floCloudAPI, 'liveRequest', { + get: () => _liveRequest + }); + + Object.defineProperty(floCloudAPI, 'inactive', { + get: () => _inactive + }); + + const util = floCloudAPI.util = {}; + + const encodeMessage = util.encodeMessage = function (message) { + return btoa(unescape(encodeURIComponent(JSON.stringify(message)))) + } + + const decodeMessage = util.decodeMessage = function (message) { + return JSON.parse(decodeURIComponent(escape(atob(message)))) + } + + const filterKey = util.filterKey = function (type, options = {}) { + return type + (options.comment ? ':' + options.comment : '') + + '|' + (options.group || options.receiverID || DEFAULT.adminID) + + '|' + (options.application || DEFAULT.application); + } + + const proxyID = util.proxyID = function (address) { + if (!address) + return; + var bytes; + if (address.length == 33 || address.length == 34) { //legacy encoding + let decode = bitjs.Base58.decode(address); + bytes = decode.slice(0, decode.length - 4); + let checksum = decode.slice(decode.length - 4), + hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3] ? + bytes = undefined : bytes.shift(); + } else if (address.length == 42 || address.length == 62) { //bech encoding + if (typeof coinjs !== 'function') + throw "library missing (lib_btc.js)"; + let decode = coinjs.bech32_decode(address); + if (decode) { + bytes = decode.data; + bytes.shift(); + bytes = coinjs.bech32_convert(bytes, 5, 8, false); + if (address.length == 62) //for long bech, aggregate once more to get 160 bit + bytes = coinjs.bech32_convert(bytes, 5, 8, false); + } + } else if (address.length == 66) { //public key hex + bytes = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(address), { + asBytes: true + })); + } + if (!bytes) + throw "Invalid address: " + address; + else { + bytes.unshift(DEFAULT.blockchainPrefix); + let hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + return bitjs.Base58.encode(bytes.concat(hash.slice(0, 4))); + } + } + + const lastCommit = {}; + Object.defineProperty(lastCommit, 'get', { + value: objName => JSON.parse(lastCommit[objName]) + }); + Object.defineProperty(lastCommit, 'set', { + value: objName => lastCommit[objName] = JSON.stringify(appObjects[objName]) + }); + + function updateObject(objectName, dataSet) { + try { + console.log(dataSet) + let vcList = Object.keys(dataSet).sort(); + for (let vc of vcList) { + if (vc < lastVC[objectName] || dataSet[vc].type !== objectName) + continue; + switch (dataSet[vc].comment) { + case "RESET": + if (dataSet[vc].message.reset) + appObjects[objectName] = dataSet[vc].message.reset; + break; + case "UPDATE": + if (dataSet[vc].message.diff) + appObjects[objectName] = diff.merge(appObjects[objectName], dataSet[vc].message.diff); + } + lastVC[objectName] = vc; + } + lastCommit.set(objectName); + compactIDB.writeData("appObjects", appObjects[objectName], objectName); + compactIDB.writeData("lastVC", lastVC[objectName], objectName); + } catch (error) { + console.error(error) + } + } + + function storeGeneral(fk, dataSet) { + try { + console.log(dataSet) + if (typeof generalData[fk] !== "object") + generalData[fk] = {} + for (let vc in dataSet) { + generalData[fk][vc] = dataSet[vc]; + if (dataSet[vc].log_time > lastVC[fk]) + lastVC[fk] = dataSet[vc].log_time; + } + compactIDB.writeData("lastVC", lastVC[fk], fk) + compactIDB.writeData("generalData", generalData[fk], fk) + } catch (error) { + console.error(error) + } + } + + function objectifier(data) { + if (!Array.isArray(data)) + data = [data]; + return Object.fromEntries(data.map(d => { + d.message = decodeMessage(d.message); + return [d.vectorClock, d]; + })); + } + + //set status as online for user_id + floCloudAPI.setStatus = function (options = {}) { + return new Promise((resolve, reject) => { + let callback = options.callback instanceof Function ? options.callback : DEFAULT.callback; + var request = { + floID: user.id, + application: options.application || DEFAULT.application, + time: Date.now(), + status: true, + pubKey: user.public + } + let hashcontent = ["time", "application", "floID"].map(d => request[d]).join("|"); + request.sign = user.sign(hashcontent); + liveRequest(options.refID || DEFAULT.adminID, request, callback) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //request status of floID(s) in trackList + floCloudAPI.requestStatus = function (trackList, options = {}) { + return new Promise((resolve, reject) => { + if (!Array.isArray(trackList)) + trackList = [trackList]; + let callback = options.callback instanceof Function ? options.callback : DEFAULT.callback; + let request = { + status: false, + application: options.application || DEFAULT.application, + trackList: trackList + } + liveRequest(options.refID || DEFAULT.adminID, request, callback) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //send any message to supernode cloud storage + const sendApplicationData = floCloudAPI.sendApplicationData = function (message, type, options = {}) { + return new Promise((resolve, reject) => { + var data = { + senderID: user.id, + receiverID: options.receiverID || DEFAULT.adminID, + pubKey: user.public, + message: encodeMessage(message), + time: Date.now(), + application: options.application || DEFAULT.application, + type: type, + comment: options.comment || "" + } + let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"] + .map(d => data[d]).join("|") + data.sign = user.sign(hashcontent); + singleRequest(data.receiverID, data) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //request any data from supernode cloud + const requestApplicationData = floCloudAPI.requestApplicationData = function (type, options = {}) { + return new Promise((resolve, reject) => { + var request = { + receiverID: options.receiverID || DEFAULT.adminID, + senderID: options.senderID || undefined, + application: options.application || DEFAULT.application, + type: type, + comment: options.comment || undefined, + lowerVectorClock: options.lowerVectorClock || undefined, + upperVectorClock: options.upperVectorClock || undefined, + atVectorClock: options.atVectorClock || undefined, + afterTime: options.afterTime || undefined, + mostRecent: options.mostRecent || undefined, + } + + if (options.callback instanceof Function) { + liveRequest(request.receiverID, request, options.callback) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + if (options.method === "POST") + request = { + time: Date.now(), + request + }; + singleRequest(request.receiverID, request, options.method || "GET") + .then(data => resolve(data)).catch(error => reject(error)) + } + }) + } + + /*(NEEDS UPDATE) + //delete data from supernode cloud (received only) + floCloudAPI.deleteApplicationData = function(vectorClocks, options = {}) { + return new Promise((resolve, reject) => { + var delreq = { + requestorID: user.id, + pubKey: user.public, + time: Date.now(), + delete: (Array.isArray(vectorClocks) ? vectorClocks : [vectorClocks]), + application: options.application || DEFAULT.application + } + let hashcontent = ["time", "application", "delete"] + .map(d => delreq[d]).join("|") + delreq.sign = user.sign(hashcontent) + singleRequest(delreq.requestorID, delreq).then(result => { + let success = [], + failed = []; + result.forEach(r => r.status === 'fulfilled' ? + success.push(r.value) : failed.push(r.reason)); + resolve({ + success, + failed + }) + }).catch(error => reject(error)) + }) + } + */ + //edit comment of data in supernode cloud (sender only) + floCloudAPI.editApplicationData = function (vectorClock, comment_edit, options = {}) { + return new Promise((resolve, reject) => { + //request the data from cloud for resigning + let req_options = Object.assign({}, options); + req_options.atVectorClock = vectorClock; + requestApplicationData(undefined, req_options).then(result => { + if (!result.length) + return reject("Data not found"); + let data = result[0]; + if (data.senderID !== user.id) + return reject("Only sender can edit comment"); + data.comment = comment_edit; + let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"] + .map(d => data[d]).join("|"); + let re_sign = user.sign(hashcontent); + var request = { + receiverID: options.receiverID || DEFAULT.adminID, + requestorID: user.id, + pubKey: user.public, + time: Date.now(), + vectorClock: vectorClock, + edit: comment_edit, + re_sign: re_sign + } + let request_hash = ["time", "vectorClock", "edit", "re_sign"].map(d => request[d]).join("|"); + request.sign = user.sign(request_hash); + singleRequest(request.receiverID, request) + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //tag data in supernode cloud (subAdmin access only) + floCloudAPI.tagApplicationData = function (vectorClock, tag, options = {}) { + return new Promise((resolve, reject) => { + if (!floGlobals.subAdmins.includes(user.id)) + return reject("Only subAdmins can tag data") + var request = { + receiverID: options.receiverID || DEFAULT.adminID, + requestorID: user.id, + pubKey: user.public, + time: Date.now(), + vectorClock: vectorClock, + tag: tag, + } + let hashcontent = ["time", "vectorClock", 'tag'].map(d => request[d]).join("|"); + request.sign = user.sign(hashcontent); + singleRequest(request.receiverID, request) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //note data in supernode cloud (receiver only or subAdmin allowed if receiver is adminID) + floCloudAPI.noteApplicationData = function (vectorClock, note, options = {}) { + return new Promise((resolve, reject) => { + var request = { + receiverID: options.receiverID || DEFAULT.adminID, + requestorID: user.id, + pubKey: user.public, + time: Date.now(), + vectorClock: vectorClock, + note: note, + } + let hashcontent = ["time", "vectorClock", 'note'].map(d => request[d]).join("|"); + request.sign = user.sign(hashcontent); + singleRequest(request.receiverID, request) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //send general data + floCloudAPI.sendGeneralData = function (message, type, options = {}) { + return new Promise((resolve, reject) => { + if (options.encrypt) { + let encryptionKey = options.encrypt === true ? + floGlobals.settings.encryptionKey : options.encrypt + message = floCrypto.encryptData(JSON.stringify(message), encryptionKey) + } + sendApplicationData(message, type, options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + //request general data + floCloudAPI.requestGeneralData = function (type, options = {}) { + return new Promise((resolve, reject) => { + var fk = filterKey(type, options) + lastVC[fk] = parseInt(lastVC[fk]) || 0; + options.afterTime = options.afterTime || lastVC[fk]; + if (options.callback instanceof Function) { + let new_options = Object.create(options) + new_options.callback = (d, e) => { + storeGeneral(fk, d); + options.callback(d, e) + } + requestApplicationData(type, new_options) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + requestApplicationData(type, options).then(dataSet => { + storeGeneral(fk, objectifier(dataSet)) + resolve(dataSet) + }).catch(error => reject(error)) + } + }) + } + + //request an object data from supernode cloud + floCloudAPI.requestObjectData = function (objectName, options = {}) { + return new Promise((resolve, reject) => { + options.lowerVectorClock = options.lowerVectorClock || lastVC[objectName] + 1; + options.senderID = [false, null].includes(options.senderID) ? null : + options.senderID || floGlobals.subAdmins; + options.mostRecent = true; + options.comment = 'RESET'; + let callback = null; + if (options.callback instanceof Function) { + let old_callback = options.callback; + callback = (d, e) => { + updateObject(objectName, d); + old_callback(d, e); + } + delete options.callback; + } + requestApplicationData(objectName, options).then(dataSet => { + updateObject(objectName, objectifier(dataSet)); + delete options.comment; + options.lowerVectorClock = lastVC[objectName] + 1; + delete options.mostRecent; + if (callback) { + let new_options = Object.create(options); + new_options.callback = callback; + requestApplicationData(objectName, new_options) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + requestApplicationData(objectName, options).then(dataSet => { + updateObject(objectName, objectifier(dataSet)) + resolve(appObjects[objectName]) + }).catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + } + + floCloudAPI.closeRequest = function (requestID) { + return new Promise((resolve, reject) => { + let conn = _liveRequest[requestID] + if (!conn) + return reject('Request not found') + conn.onclose = evt => { + delete _liveRequest[requestID]; + resolve('Request connection closed') + } + conn.close() + }) + } + + //reset or initialize an object and send it to cloud + floCloudAPI.resetObjectData = function (objectName, options = {}) { + return new Promise((resolve, reject) => { + let message = { + reset: appObjects[objectName] + } + options.comment = 'RESET'; + sendApplicationData(message, objectName, options).then(result => { + lastCommit.set(objectName); + resolve(result) + }).catch(error => reject(error)) + }) + } + + //update the diff and send it to cloud + floCloudAPI.updateObjectData = function (objectName, options = {}) { + return new Promise((resolve, reject) => { + let message = { + diff: diff.find(lastCommit.get(objectName), appObjects[ + objectName]) + } + options.comment = 'UPDATE'; + sendApplicationData(message, objectName, options).then(result => { + lastCommit.set(objectName); + resolve(result) + }).catch(error => reject(error)) + }) + } + + //upload file + floCloudAPI.uploadFile = function (fileBlob, type, options = {}) { + return new Promise((resolve, reject) => { + if (!(fileBlob instanceof File) && !(fileBlob instanceof Blob)) + return reject("file must be instance of File/Blob"); + fileBlob.arrayBuffer().then(arraybuf => { + let file_data = { type: fileBlob.type, name: fileBlob.name }; + file_data.content = Crypto.util.bytesToBase64(new Uint8Array(arraybuf)); + if (options.encrypt) { + let encryptionKey = options.encrypt === true ? + floGlobals.settings.encryptionKey : options.encrypt + file_data = floCrypto.encryptData(JSON.stringify(file_data), encryptionKey) + } + sendApplicationData(file_data, type, options) + .then(({ vectorClock, receiverID, type, application }) => resolve({ vectorClock, receiverID, type, application })) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + //download file + floCloudAPI.downloadFile = function (vectorClock, options = {}) { + return new Promise((resolve, reject) => { + options.atVectorClock = vectorClock; + requestApplicationData(options.type, options).then(result => { + if (!result.length) + return reject("File not found"); + result = result[0]; + try { + let file_data = decodeMessage(result.message); + //file is encrypted: decryption required + if (file_data instanceof Object && "secret" in file_data) { + if (!options.decrypt) + return reject("Data is encrypted"); + let decryptionKey = (options.decrypt === true) ? Crypto.AES.decrypt(user_private, aes_key) : options.decrypt; + if (!Array.isArray(decryptionKey)) + decryptionKey = [decryptionKey]; + let flag = false; + for (let key of decryptionKey) { + try { + let tmp = floCrypto.decryptData(file_data, key); + file_data = JSON.parse(tmp); + flag = true; + break; + } catch (error) { } + } + if (!flag) + return reject("Unable to decrypt file: Invalid private key"); + } + //reconstruct the file + let arraybuf = new Uint8Array(Crypto.util.base64ToBytes(file_data.content)) + result.file = new File([arraybuf], file_data.name, { type: file_data.type }); + resolve(result) + } catch (error) { + console.error(error); + reject("Data is not a file"); + } + }).catch(error => reject(error)) + }) + } + + /* + Functions: + findDiff(original, updatedObj) returns an object with the added, deleted and updated differences + mergeDiff(original, allDiff) returns a new object from original object merged with all differences (allDiff is returned object of findDiff) + */ + var diff = (function () { + const isDate = d => d instanceof Date; + const isEmpty = o => Object.keys(o).length === 0; + const isObject = o => o != null && typeof o === 'object'; + const properObject = o => isObject(o) && !o.hasOwnProperty ? { + ...o + } : o; + const getLargerArray = (l, r) => l.length > r.length ? l : r; + + const preserve = (diff, left, right) => { + if (!isObject(diff)) return diff; + return Object.keys(diff).reduce((acc, key) => { + const leftArray = left[key]; + const rightArray = right[key]; + if (Array.isArray(leftArray) && Array.isArray(rightArray)) { + const array = [...getLargerArray(leftArray, rightArray)]; + return { + ...acc, + [key]: array.reduce((acc2, item, index) => { + if (diff[key].hasOwnProperty(index)) { + acc2[index] = preserve(diff[key][index], leftArray[index], rightArray[index]); // diff recurse and check for nested arrays + return acc2; + } + delete acc2[index]; // no diff aka empty + return acc2; + }, array) + }; + } + return { + ...acc, + [key]: diff[key] + }; + }, {}); + }; + + const updatedDiff = (lhs, rhs) => { + if (lhs === rhs) return {}; + if (!isObject(lhs) || !isObject(rhs)) return rhs; + const l = properObject(lhs); + const r = properObject(rhs); + if (isDate(l) || isDate(r)) { + if (l.valueOf() == r.valueOf()) return {}; + return r; + } + return Object.keys(r).reduce((acc, key) => { + if (l.hasOwnProperty(key)) { + const difference = updatedDiff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; + return { + ...acc, + [key]: difference + }; + } + return acc; + }, {}); + }; + + + const diff = (lhs, rhs) => { + if (lhs === rhs) return {}; // equal return no diff + if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs + const l = properObject(lhs); + const r = properObject(rhs); + const deletedValues = Object.keys(l).reduce((acc, key) => { + return r.hasOwnProperty(key) ? acc : { + ...acc, + [key]: null + }; + }, {}); + if (isDate(l) || isDate(r)) { + if (l.valueOf() == r.valueOf()) return {}; + return r; + } + return Object.keys(r).reduce((acc, key) => { + if (!l.hasOwnProperty(key)) return { + ...acc, + [key]: r[key] + }; // return added r key + const difference = diff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff + return { + ...acc, + [key]: difference + }; // return updated key + }, deletedValues); + }; + + const addedDiff = (lhs, rhs) => { + if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {}; + const l = properObject(lhs); + const r = properObject(rhs); + return Object.keys(r).reduce((acc, key) => { + if (l.hasOwnProperty(key)) { + const difference = addedDiff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference)) return acc; + return { + ...acc, + [key]: difference + }; + } + return { + ...acc, + [key]: r[key] + }; + }, {}); + }; + + const arrayDiff = (lhs, rhs) => { + if (lhs === rhs) return {}; // equal return no diff + if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs + const l = properObject(lhs); + const r = properObject(rhs); + const deletedValues = Object.keys(l).reduce((acc, key) => { + return r.hasOwnProperty(key) ? acc : { + ...acc, + [key]: null + }; + }, {}); + if (isDate(l) || isDate(r)) { + if (l.valueOf() == r.valueOf()) return {}; + return r; + } + if (Array.isArray(r) && Array.isArray(l)) { + const deletedValues = l.reduce((acc, item, index) => { + return r.hasOwnProperty(index) ? acc.concat(item) : acc.concat(null); + }, []); + return r.reduce((acc, rightItem, index) => { + if (!deletedValues.hasOwnProperty(index)) { + return acc.concat(rightItem); + } + const leftItem = l[index]; + const difference = diff(rightItem, leftItem); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) { + delete acc[index]; + return acc; // return no diff + } + return acc.slice(0, index).concat(rightItem).concat(acc.slice(index + 1)); // return updated key + }, deletedValues); + } + + return Object.keys(r).reduce((acc, key) => { + if (!l.hasOwnProperty(key)) return { + ...acc, + [key]: r[key] + }; // return added r key + const difference = diff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff + return { + ...acc, + [key]: difference + }; // return updated key + }, deletedValues); + }; + + const deletedDiff = (lhs, rhs) => { + if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {}; + const l = properObject(lhs); + const r = properObject(rhs); + return Object.keys(l).reduce((acc, key) => { + if (r.hasOwnProperty(key)) { + const difference = deletedDiff(l[key], r[key]); + if (isObject(difference) && isEmpty(difference)) return acc; + return { + ...acc, + [key]: difference + }; + } + return { + ...acc, + [key]: null + }; + }, {}); + }; + + const mergeRecursive = (obj1, obj2, deleteMode = false) => { + for (var p in obj2) { + try { + if (obj2[p].constructor == Object) + obj1[p] = mergeRecursive(obj1[p], obj2[p], deleteMode); + // Property in destination object set; update its value. + else if (Array.isArray(obj2[p])) { + // obj1[p] = []; + if (obj2[p].length < 1) + obj1[p] = obj2[p]; + else + obj1[p] = mergeRecursive(obj1[p], obj2[p], deleteMode); + } else + obj1[p] = deleteMode && obj2[p] === null ? undefined : obj2[p]; + } catch (e) { + // Property in destination object not set; create it and set its value. + obj1[p] = deleteMode && obj2[p] === null ? undefined : obj2[p]; + } + } + return obj1; + } + + const cleanse = (obj) => { + Object.keys(obj).forEach(key => { + var value = obj[key]; + if (typeof value === "object" && value !== null) + obj[key] = cleanse(value); + else if (typeof value === 'undefined') + delete obj[key]; // undefined, remove it + }); + if (Array.isArray(obj)) + obj = obj.filter(v => typeof v !== 'undefined'); + return obj; + } + + + const findDiff = (lhs, rhs) => ({ + added: addedDiff(lhs, rhs), + deleted: deletedDiff(lhs, rhs), + updated: updatedDiff(lhs, rhs), + }); + + /*obj is original object or array, diff is the output of findDiff */ + const mergeDiff = (obj, diff) => { + if (Object.keys(diff.updated).length !== 0) + obj = mergeRecursive(obj, diff.updated) + if (Object.keys(diff.deleted).length !== 0) { + obj = mergeRecursive(obj, diff.deleted, true) + obj = cleanse(obj) + } + if (Object.keys(diff.added).length !== 0) + obj = mergeRecursive(obj, diff.added) + return obj + } + + return { + find: findDiff, + merge: mergeDiff + } + })(); + + +})('object' === typeof module ? module.exports : window.floCloudAPI = {}); \ No newline at end of file diff --git a/floCrypto.js b/floCrypto.js new file mode 100644 index 0000000..f1e14d3 --- /dev/null +++ b/floCrypto.js @@ -0,0 +1,530 @@ +(function (EXPORTS) { //floCrypto v2.3.6a + /* FLO Crypto Operators */ + 'use strict'; + const floCrypto = EXPORTS; + + const p = BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16); + const ecparams = EllipticCurve.getSECCurveByName("secp256k1"); + const ascii_alternatives = `‘ '\n’ '\n“ "\n” "\n– --\n— ---\n≥ >=\n≤ <=\n≠ !=\n× *\n÷ /\n← <-\n→ ->\n↔ <->\n⇒ =>\n⇐ <=\n⇔ <=>`; + const exponent1 = () => p.add(BigInteger.ONE).divide(BigInteger("4")); + coinjs.compressed = true; //defaulting coinjs compressed to true; + + function calculateY(x) { + let exp = exponent1(); + // x is x value of public key in BigInteger format without 02 or 03 or 04 prefix + return x.modPow(BigInteger("3"), p).add(BigInteger("7")).mod(p).modPow(exp, p) + } + + function getUncompressedPublicKey(compressedPublicKey) { + // Fetch x from compressedPublicKey + let pubKeyBytes = Crypto.util.hexToBytes(compressedPublicKey); + const prefix = pubKeyBytes.shift() // remove prefix + let prefix_modulus = prefix % 2; + pubKeyBytes.unshift(0) // add prefix 0 + let x = new BigInteger(pubKeyBytes) + let xDecimalValue = x.toString() + // Fetch y + let y = calculateY(x); + let yDecimalValue = y.toString(); + // verify y value + let resultBigInt = y.mod(BigInteger("2")); + let check = resultBigInt.toString() % 2; + if (prefix_modulus !== check) + yDecimalValue = y.negate().mod(p).toString(); + return { + x: xDecimalValue, + y: yDecimalValue + }; + } + + function getSenderPublicKeyString() { + let privateKey = ellipticCurveEncryption.senderRandom(); + var senderPublicKeyString = ellipticCurveEncryption.senderPublicString(privateKey); + return { + privateKey: privateKey, + senderPublicKeyString: senderPublicKeyString + } + } + + function deriveSharedKeySender(receiverPublicKeyHex, senderPrivateKey) { + let receiverPublicKeyString = getUncompressedPublicKey(receiverPublicKeyHex); + var senderDerivedKey = ellipticCurveEncryption.senderSharedKeyDerivation( + receiverPublicKeyString.x, receiverPublicKeyString.y, senderPrivateKey); + return senderDerivedKey; + } + + function deriveSharedKeyReceiver(senderPublicKeyString, receiverPrivateKey) { + return ellipticCurveEncryption.receiverSharedKeyDerivation( + senderPublicKeyString.XValuePublicString, senderPublicKeyString.YValuePublicString, receiverPrivateKey); + } + + function getReceiverPublicKeyString(privateKey) { + return ellipticCurveEncryption.receiverPublicString(privateKey); + } + + function wifToDecimal(pk_wif, isPubKeyCompressed = false) { + let pk = Bitcoin.Base58.decode(pk_wif) + pk.shift() + pk.splice(-4, 4) + //If the private key corresponded to a compressed public key, also drop the last byte (it should be 0x01). + if (isPubKeyCompressed == true) pk.pop() + pk.unshift(0) + let privateKeyDecimal = BigInteger(pk).toString() + let privateKeyHex = Crypto.util.bytesToHex(pk) + return { + privateKeyDecimal: privateKeyDecimal, + privateKeyHex: privateKeyHex + } + } + + //generate a random Interger within range + floCrypto.randInt = function (min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(securedMathRandom() * (max - min + 1)) + min; + } + + //generate a random String within length (options : alphaNumeric chars only) + floCrypto.randString = function (length, alphaNumeric = true) { + var result = ''; + var characters = alphaNumeric ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' : + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+-./*?@#&$<>=[]{}():'; + for (var i = 0; i < length; i++) + result += characters.charAt(Math.floor(securedMathRandom() * characters.length)); + return result; + } + + //Encrypt Data using public-key + floCrypto.encryptData = function (data, receiverPublicKeyHex) { + var senderECKeyData = getSenderPublicKeyString(); + var senderDerivedKey = deriveSharedKeySender(receiverPublicKeyHex, senderECKeyData.privateKey); + let senderKey = senderDerivedKey.XValue + senderDerivedKey.YValue; + let secret = Crypto.AES.encrypt(data, senderKey); + return { + secret: secret, + senderPublicKeyString: senderECKeyData.senderPublicKeyString + }; + } + + //Decrypt Data using private-key + floCrypto.decryptData = function (data, privateKeyHex) { + var receiverECKeyData = {}; + if (typeof privateKeyHex !== "string") throw new Error("No private key found."); + let privateKey = wifToDecimal(privateKeyHex, true); + if (typeof privateKey.privateKeyDecimal !== "string") throw new Error("Failed to detremine your private key."); + receiverECKeyData.privateKey = privateKey.privateKeyDecimal; + var receiverDerivedKey = deriveSharedKeyReceiver(data.senderPublicKeyString, receiverECKeyData.privateKey); + let receiverKey = receiverDerivedKey.XValue + receiverDerivedKey.YValue; + let decryptMsg = Crypto.AES.decrypt(data.secret, receiverKey); + return decryptMsg; + } + + //Sign data using private-key + floCrypto.signData = function (data, privateKeyHex) { + var key = new Bitcoin.ECKey(privateKeyHex); + var messageHash = Crypto.SHA256(data); + var messageSign = Bitcoin.ECDSA.sign(messageHash, key.priv); + var sighex = Crypto.util.bytesToHex(messageSign); + return sighex; + } + + //Verify signatue of the data using public-key + floCrypto.verifySign = function (data, signatureHex, publicKeyHex) { + var msgHash = Crypto.SHA256(data); + var sigBytes = Crypto.util.hexToBytes(signatureHex); + var publicKeyPoint = ecparams.getCurve().decodePointHex(publicKeyHex); + var verify = Bitcoin.ECDSA.verify(msgHash, sigBytes, publicKeyPoint); + return verify; + } + + //Generates a new flo ID and returns private-key, public-key and floID + const generateNewID = floCrypto.generateNewID = function () { + var key = new Bitcoin.ECKey(false); + key.setCompressed(true); + return { + floID: key.getBitcoinAddress(), + pubKey: key.getPubKeyHex(), + privKey: key.getBitcoinWalletImportFormat() + } + } + + Object.defineProperties(floCrypto, { + newID: { + get: () => generateNewID() + }, + hashID: { + value: (str) => { + let bytes = ripemd160(Crypto.SHA256(str, { asBytes: true }), { asBytes: true }); + bytes.unshift(bitjs.pub); + var hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + return bitjs.Base58.encode(bytes.concat(checksum)); + } + }, + tmpID: { + get: () => { + let bytes = Crypto.util.randomBytes(20); + bytes.unshift(bitjs.pub); + var hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + var checksum = hash.slice(0, 4); + return bitjs.Base58.encode(bytes.concat(checksum)); + } + } + }); + + //Returns public-key from private-key + floCrypto.getPubKeyHex = function (privateKeyHex) { + if (!privateKeyHex) + return null; + var key = new Bitcoin.ECKey(privateKeyHex); + if (key.priv == null) + return null; + key.setCompressed(true); + return key.getPubKeyHex(); + } + + //Returns flo-ID from public-key or private-key + floCrypto.getFloID = function (keyHex) { + if (!keyHex) + return null; + try { + var key = new Bitcoin.ECKey(keyHex); + if (key.priv == null) + key.setPub(keyHex); + return key.getBitcoinAddress(); + } catch { + return null; + } + } + + floCrypto.getAddress = function (privateKeyHex, strict = false) { + if (!privateKeyHex) + return; + var key = new Bitcoin.ECKey(privateKeyHex); + if (key.priv == null) + return null; + key.setCompressed(true); + let pubKey = key.getPubKeyHex(), + version = bitjs.Base58.decode(privateKeyHex)[0]; + switch (version) { + case coinjs.priv: //BTC + return coinjs.bech32Address(pubKey).address; + case bitjs.priv: //FLO + return bitjs.pubkey2address(pubKey); + default: + return strict ? false : bitjs.pubkey2address(pubKey); //default to FLO address (if strict=false) + } + } + + //Verify the private-key for the given public-key or flo-ID + floCrypto.verifyPrivKey = function (privateKeyHex, pubKey_floID, isfloID = true) { + if (!privateKeyHex || !pubKey_floID) + return false; + try { + var key = new Bitcoin.ECKey(privateKeyHex); + if (key.priv == null) + return false; + key.setCompressed(true); + if (isfloID && pubKey_floID == key.getBitcoinAddress()) + return true; + else if (!isfloID && pubKey_floID.toUpperCase() == key.getPubKeyHex().toUpperCase()) + return true; + else + return false; + } catch { + return null; + } + } + + floCrypto.getMultisigAddress = function (publicKeyList, requiredSignatures) { + if (!Array.isArray(publicKeyList) || !publicKeyList.length) + return null; + if (!Number.isInteger(requiredSignatures) || requiredSignatures < 1 || requiredSignatures > publicKeyList.length) + return null; + try { + var multisig = bitjs.pubkeys2multisig(publicKeyList, requiredSignatures); + return multisig; + } catch { + return null; + } + } + + floCrypto.decodeRedeemScript = function (redeemScript) { + try { + var decoded = bitjs.transaction().decodeRedeemScript(redeemScript); + return decoded; + } catch { + return null; + } + } + + //Check if the given flo-id is valid or not + floCrypto.validateFloID = function (floID, regularOnly = false) { + if (!floID) + return false; + try { + let addr = new Bitcoin.Address(floID); + if (regularOnly && addr.version != Bitcoin.Address.standardVersion) + return false; + return true; + } catch { + return false; + } + } + + //Check if the given address (any blockchain) is valid or not + floCrypto.validateAddr = function (address, std = true, bech = true) { + let raw = decodeAddress(address); + if (!raw) + return false; + if (typeof raw.version !== 'undefined') { //legacy or segwit + if (std == false) + return false; + else if (std === true || (!Array.isArray(std) && std === raw.version) || (Array.isArray(std) && std.includes(raw.version))) + return true; + else + return false; + } else if (typeof raw.bech_version !== 'undefined') { //bech32 + if (bech === false) + return false; + else if (bech === true || (!Array.isArray(bech) && bech === raw.bech_version) || (Array.isArray(bech) && bech.includes(raw.bech_version))) + return true; + else + return false; + } else //unknown + return false; + } + + //Check the public-key (or redeem-script) for the address (any blockchain) + floCrypto.verifyPubKey = function (pubKeyHex, address) { + let raw = decodeAddress(address); + if (!raw) + return; + let pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), { asBytes: true }))); + if (typeof raw.bech_version !== 'undefined' && raw.bytes.length == 32) //bech32-multisig + raw.hex = Crypto.util.bytesToHex(ripemd160(raw.bytes, { asBytes: true })); + return pub_hash === raw.hex; + } + + //Convert the given address (any blockchain) to equivalent floID + floCrypto.toFloID = function (address, options = null) { + if (!address) + return; + let raw = decodeAddress(address); + if (!raw) + return; + else if (options) { //if (optional) version check is passed + if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version))) + return; + if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version))) + return; + } + raw.bytes.unshift(bitjs.pub); + let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, { + asBytes: true + }), { + asBytes: true + }); + return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4))); + } + + //Convert raw address bytes to floID + floCrypto.rawToFloID = function (raw_bytes) { + if (typeof raw_bytes === 'string') + raw_bytes = Crypto.util.hexToBytes(raw_bytes); + if (raw_bytes.length != 20) + return null; + raw_bytes.unshift(bitjs.pub); + let hash = Crypto.SHA256(Crypto.SHA256(raw_bytes, { + asBytes: true + }), { + asBytes: true + }); + return bitjs.Base58.encode(raw_bytes.concat(hash.slice(0, 4))); + } + + //Convert the given multisig address (any blockchain) to equivalent multisig floID + floCrypto.toMultisigFloID = function (address, options = null) { + if (!address) + return; + let raw = decodeAddress(address); + if (!raw) + return; + else if (options) { //if (optional) version check is passed + if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version))) + return; + if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version))) + return; + } + if (typeof raw.bech_version !== 'undefined') { + if (raw.bytes.length != 32) return; //multisig bech address have 32 bytes + //multisig-bech:hash=SHA256 whereas multisig:hash=r160(SHA265), thus ripemd160 the bytes from multisig-bech + raw.bytes = ripemd160(raw.bytes, { + asBytes: true + }); + } + raw.bytes.unshift(bitjs.multisig); + let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, { + asBytes: true + }), { + asBytes: true + }); + return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4))); + } + + //Checks if the given addresses (any blockchain) are same (w.r.t keys) + floCrypto.isSameAddr = function (addr1, addr2) { + if (!addr1 || !addr2) + return; + let raw1 = decodeAddress(addr1), + raw2 = decodeAddress(addr2); + if (!raw1 || !raw2) + return false; + else { + if (typeof raw1.bech_version !== 'undefined' && raw1.bytes.length == 32) //bech32-multisig + raw1.hex = Crypto.util.bytesToHex(ripemd160(raw1.bytes, { asBytes: true })); + if (typeof raw2.bech_version !== 'undefined' && raw2.bytes.length == 32) //bech32-multisig + raw2.hex = Crypto.util.bytesToHex(ripemd160(raw2.bytes, { asBytes: true })); + return raw1.hex === raw2.hex; + } + } + + const decodeAddress = floCrypto.decodeAddr = function (address) { + if (!address) + return; + else if (address.length == 33 || address.length == 34) { //legacy encoding + let decode = bitjs.Base58.decode(address); + let bytes = decode.slice(0, decode.length - 4); + let checksum = decode.slice(decode.length - 4), + hash = Crypto.SHA256(Crypto.SHA256(bytes, { + asBytes: true + }), { + asBytes: true + }); + return (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3]) ? null : { + version: bytes.shift(), + hex: Crypto.util.bytesToHex(bytes), + bytes + } + } else if (address.length == 42 || address.length == 62) { //bech encoding + let decode = coinjs.bech32_decode(address); + if (decode) { + let bytes = decode.data; + let bech_version = bytes.shift(); + bytes = coinjs.bech32_convert(bytes, 5, 8, false); + return { + bech_version, + hrp: decode.hrp, + hex: Crypto.util.bytesToHex(bytes), + bytes + } + } else + return null; + } + } + + //Split the str using shamir's Secret and Returns the shares + floCrypto.createShamirsSecretShares = function (str, total_shares, threshold_limit) { + try { + if (str.length > 0) { + var strHex = shamirSecretShare.str2hex(str); + var shares = shamirSecretShare.share(strHex, total_shares, threshold_limit); + return shares; + } + return false; + } catch { + return false + } + } + + //Returns the retrived secret by combining the shamirs shares + const retrieveShamirSecret = floCrypto.retrieveShamirSecret = function (sharesArray) { + try { + if (sharesArray.length > 0) { + var comb = shamirSecretShare.combine(sharesArray.slice(0, sharesArray.length)); + comb = shamirSecretShare.hex2str(comb); + return comb; + } + return false; + } catch { + return false; + } + } + + //Verifies the shares and str + floCrypto.verifyShamirsSecret = function (sharesArray, str) { + if (!str) + return null; + else if (retrieveShamirSecret(sharesArray) === str) + return true; + else + return false; + } + + const validateASCII = floCrypto.validateASCII = function (string, bool = true) { + if (typeof string !== "string") + return null; + if (bool) { + let x; + for (let i = 0; i < string.length; i++) { + x = string.charCodeAt(i); + if (x < 32 || x > 127) + return false; + } + return true; + } else { + let x, invalids = {}; + for (let i = 0; i < string.length; i++) { + x = string.charCodeAt(i); + if (x < 32 || x > 127) + if (x in invalids) + invalids[string[i]].push(i) + else + invalids[string[i]] = [i]; + } + if (Object.keys(invalids).length) + return invalids; + else + return true; + } + } + + floCrypto.convertToASCII = function (string, mode = 'soft-remove') { + let chars = validateASCII(string, false); + if (chars === true) + return string; + else if (chars === null) + return null; + let convertor, result = string, + refAlt = {}; + ascii_alternatives.split('\n').forEach(a => refAlt[a[0]] = a.slice(2)); + mode = mode.toLowerCase(); + if (mode === "hard-unicode") + convertor = (c) => `\\u${('000' + c.charCodeAt().toString(16)).slice(-4)}`; + else if (mode === "soft-unicode") + convertor = (c) => refAlt[c] || `\\u${('000' + c.charCodeAt().toString(16)).slice(-4)}`; + else if (mode === "hard-remove") + convertor = c => ""; + else if (mode === "soft-remove") + convertor = c => refAlt[c] || ""; + else + return null; + for (let c in chars) + result = result.replaceAll(c, convertor(c)); + return result; + } + + floCrypto.revertUnicode = function (string) { + return string.replace(/\\u[\dA-F]{4}/gi, + m => String.fromCharCode(parseInt(m.replace(/\\u/g, ''), 16))); + } + +})('object' === typeof module ? module.exports : window.floCrypto = {}); \ No newline at end of file diff --git a/floDapps.js b/floDapps.js new file mode 100644 index 0000000..480e4e4 --- /dev/null +++ b/floDapps.js @@ -0,0 +1,843 @@ +(function (EXPORTS) { //floDapps v2.4.1 + /* General functions for FLO Dapps*/ + 'use strict'; + const floDapps = EXPORTS; + + const DEFAULT = { + root: "floDapps", + application: floGlobals.application, + adminID: floGlobals.adminID + }; + + Object.defineProperties(floDapps, { + application: { + get: () => DEFAULT.application + }, + adminID: { + get: () => DEFAULT.adminID + }, + root: { + get: () => DEFAULT.root + } + }); + + var user_priv_raw, aes_key, user_priv_wrap; //private variable inside capsule + const raw_user = { + get private() { + if (!user_priv_raw) + throw "User not logged in"; + return Crypto.AES.decrypt(user_priv_raw, aes_key); + } + } + + var user_id, user_public, user_private; + const user = floDapps.user = { + get id() { + if (!user_id) + throw "User not logged in"; + return user_id; + }, + get public() { + if (!user_public) + throw "User not logged in"; + return user_public; + }, + get private() { + if (!user_private) + throw "User not logged in"; + else if (user_private instanceof Function) + return user_private(); + else + return Crypto.AES.decrypt(user_private, aes_key); + }, + sign(message) { + return floCrypto.signData(message, raw_user.private); + }, + decrypt(data) { + return floCrypto.decryptData(data, raw_user.private); + }, + encipher(message) { + return Crypto.AES.encrypt(message, raw_user.private); + }, + decipher(data) { + return Crypto.AES.decrypt(data, raw_user.private); + }, + get db_name() { + return "floDapps#" + floCrypto.toFloID(user.id); + }, + lock() { + user_private = user_priv_wrap; + }, + async unlock() { + if (await user.private === raw_user.private) + user_private = user_priv_raw; + }, + get_contact(id) { + if (!user.contacts) + throw "Contacts not available"; + else if (user.contacts[id]) + return user.contacts[id]; + else { + let id_raw = floCrypto.decodeAddr(id).hex; + for (let i in user.contacts) + if (floCrypto.decodeAddr(i).hex == id_raw) + return user.contacts[i]; + } + }, + get_pubKey(id) { + if (!user.pubKeys) + throw "Contacts not available"; + else if (user.pubKeys[id]) + return user.pubKeys[id]; + else { + let id_raw = floCrypto.decodeAddr(id).hex; + for (let i in user.pubKeys) + if (floCrypto.decodeAddr(i).hex == id_raw) + return user.pubKeys[i]; + } + }, + clear() { + user_id = user_public = user_private = undefined; + user_priv_raw = aes_key = undefined; + delete user.contacts; + delete user.pubKeys; + delete user.messages; + } + }; + + Object.defineProperties(window, { + myFloID: { + get: () => { + try { + return user.id; + } catch { + return; + } + } + }, + myUserID: { + get: () => { + try { + return user.id; + } catch { + return; + } + } + }, + myPubKey: { + get: () => { + try { + return user.public; + } catch { + return; + } + } + }, + myPrivKey: { + get: () => { + try { + return user.private; + } catch { + return; + } + } + } + }); + + var subAdmins, trustedIDs, settings; + Object.defineProperties(floGlobals, { + subAdmins: { + get: () => subAdmins + }, + trustedIDs: { + get: () => trustedIDs + }, + settings: { + get: () => settings + }, + contacts: { + get: () => user.contacts + }, + pubKeys: { + get: () => user.pubKeys + }, + messages: { + get: () => user.messages + } + }) + + function initIndexedDB() { + return new Promise((resolve, reject) => { + var obs_g = { + //general + lastTx: {}, + //supernode (cloud list) + supernodes: {} + } + var obs_a = { + //login credentials + credentials: {}, + //for Dapps + subAdmins: {}, + trustedIDs: {}, + settings: {}, + appObjects: {}, + generalData: {}, + lastVC: {} + } + //add other given objectStores + initIndexedDB.appObs = initIndexedDB.appObs || {} + for (let o in initIndexedDB.appObs) + if (!(o in obs_a)) + obs_a[o] = initIndexedDB.appObs[o] + Promise.all([ + compactIDB.initDB(DEFAULT.application, obs_a), + compactIDB.initDB(DEFAULT.root, obs_g) + ]).then(result => { + compactIDB.setDefaultDB(DEFAULT.application) + resolve("IndexedDB App Storage Initated Successfully") + }).catch(error => reject(error)); + }) + } + + function initUserDB() { + return new Promise((resolve, reject) => { + var obs = { + contacts: {}, + pubKeys: {}, + messages: {} + } + compactIDB.initDB(user.db_name, obs).then(result => { + resolve("UserDB Initated Successfully") + }).catch(error => reject('Init userDB failed')); + }) + } + + function loadUserDB() { + return new Promise((resolve, reject) => { + var loadData = ["contacts", "pubKeys", "messages"] + var promises = [] + for (var i = 0; i < loadData.length; i++) + promises[i] = compactIDB.readAllData(loadData[i], user.db_name) + Promise.all(promises).then(results => { + for (var i = 0; i < loadData.length; i++) + user[loadData[i]] = results[i] + resolve("Loaded Data from userDB") + }).catch(error => reject('Load userDB failed')) + }) + } + + const startUpOptions = { + cloud: true, + app_config: true, + } + + floDapps.startUpOptions = { + set app_config(val) { + if (val === true || val === false) + startUpOptions.app_config = val; + }, + get app_config() { return startUpOptions.app_config }, + + set cloud(val) { + if (val === true || val === false) + startUpOptions.cloud = val; + }, + get cloud() { return startUpOptions.cloud }, + } + + const startUpFunctions = []; + + startUpFunctions.push(function readSupernodeListFromAPI() { + return new Promise((resolve, reject) => { + if (!startUpOptions.cloud) + return resolve("No cloud for this app"); + const CLOUD_KEY = "floCloudAPI#" + floCloudAPI.SNStorageID; + compactIDB.readData("lastTx", CLOUD_KEY, DEFAULT.root).then(lastTx => { + 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 => { + compactIDB.readData("supernodes", CLOUD_KEY, DEFAULT.root).then(nodes => { + nodes = nodes || {}; + for (var i = result.data.length - 1; i >= 0; i--) { + var content = JSON.parse(result.data[i])[floCloudAPI.SNStorageName]; + for (let sn in content.removeNodes) + delete nodes[sn]; + for (let sn in content.newNodes) + nodes[sn] = content.newNodes[sn]; + for (let sn in content.updateNodes) + if (sn in nodes) //check if node is listed + nodes[sn].uri = content.updateNodes[sn]; + } + Promise.all([ + compactIDB.writeData("lastTx", result.lastItem, CLOUD_KEY, DEFAULT.root), + compactIDB.writeData("supernodes", nodes, CLOUD_KEY, DEFAULT.root) + ]).then(_ => { + floCloudAPI.init(nodes) + .then(result => resolve("Loaded Supernode list\n" + result)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + }).catch(error => reject(error)) + }) + }); + + startUpFunctions.push(function readAppConfigFromAPI() { + return new Promise((resolve, reject) => { + if (!startUpOptions.app_config) + return resolve("No configs for this app"); + compactIDB.readData("lastTx", `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root).then(lastTx => { + 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") + continue; + if (Array.isArray(content.removeSubAdmin)) + for (var j = 0; j < content.removeSubAdmin.length; j++) + compactIDB.removeData("subAdmins", content.removeSubAdmin[j]); + if (Array.isArray(content.addSubAdmin)) + for (var k = 0; k < content.addSubAdmin.length; k++) + compactIDB.writeData("subAdmins", true, content.addSubAdmin[k]); + if (Array.isArray(content.removeTrustedID)) + for (var j = 0; j < content.removeTrustedID.length; j++) + compactIDB.removeData("trustedIDs", content.removeTrustedID[j]); + if (Array.isArray(content.addTrustedID)) + for (var k = 0; k < content.addTrustedID.length; k++) + compactIDB.writeData("trustedIDs", true, content.addTrustedID[k]); + if (content.settings) + for (let l in content.settings) + compactIDB.writeData("settings", content.settings[l], l) + } + compactIDB.writeData("lastTx", result.lastItem, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root); + compactIDB.readAllData("subAdmins").then(result => { + subAdmins = Object.keys(result); + compactIDB.readAllData("trustedIDs").then(result => { + trustedIDs = Object.keys(result); + compactIDB.readAllData("settings").then(result => { + settings = result; + resolve("Read app configuration from blockchain"); + }) + }) + }) + }) + }).catch(error => reject(error)) + }) + }); + + startUpFunctions.push(function loadDataFromAppIDB() { + return new Promise((resolve, reject) => { + if (!startUpOptions.cloud) + return resolve("No cloud for this app"); + var loadData = ["appObjects", "generalData", "lastVC"] + var promises = [] + for (var i = 0; i < loadData.length; i++) + promises[i] = compactIDB.readAllData(loadData[i]) + Promise.all(promises).then(results => { + for (var i = 0; i < loadData.length; i++) + floGlobals[loadData[i]] = results[i] + resolve("Loaded Data from app IDB") + }).catch(error => reject(error)) + }) + }); + + var keyInput = type => new Promise((resolve, reject) => { + let inputVal = prompt(`Enter ${type}: `) + if (inputVal === null) + reject(null) + else + resolve(inputVal) + }); + + function getCredentials() { + + const readSharesFromIDB = indexArr => new Promise((resolve, reject) => { + var promises = [] + for (var i = 0; i < indexArr.length; i++) + promises.push(compactIDB.readData('credentials', indexArr[i])) + Promise.all(promises).then(shares => { + var secret = floCrypto.retrieveShamirSecret(shares) + if (secret) + resolve(secret) + else + reject("Shares are insufficient or incorrect") + }).catch(error => { + clearCredentials(); + location.reload(); + }) + }); + + const writeSharesToIDB = (shares, i = 0, resultIndexes = []) => new Promise(resolve => { + if (i >= shares.length) + return resolve(resultIndexes) + var n = floCrypto.randInt(0, 100000) + compactIDB.addData("credentials", shares[i], n).then(res => { + resultIndexes.push(n) + writeSharesToIDB(shares, i + 1, resultIndexes) + .then(result => resolve(result)) + }).catch(error => { + writeSharesToIDB(shares, i, resultIndexes) + .then(result => resolve(result)) + }) + }); + + const getPrivateKeyCredentials = () => new Promise((resolve, reject) => { + var indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`) + if (indexArr) { + readSharesFromIDB(JSON.parse(indexArr)) + .then(result => resolve(result)) + .catch(error => reject(error)) + } else { + var privKey; + keyInput("PRIVATE_KEY").then(result => { + if (!result) + return reject("Empty Private Key") + var floID = floCrypto.getFloID(result) + if (!floID || !floCrypto.validateFloID(floID)) + return reject("Invalid Private Key") + privKey = result; + }).catch(error => { + console.log(error, "Generating Random Keys") + privKey = floCrypto.generateNewID().privKey + }).finally(_ => { + if (!privKey) + return; + var threshold = floCrypto.randInt(10, 20) + var shares = floCrypto.createShamirsSecretShares(privKey, threshold, threshold) + writeSharesToIDB(shares).then(resultIndexes => { + //store index keys in localStorage + localStorage.setItem(`${DEFAULT.application}#privKey`, JSON.stringify(resultIndexes)) + //also add a dummy privatekey to the IDB + var randomPrivKey = floCrypto.generateNewID().privKey + var randomThreshold = floCrypto.randInt(10, 20) + var randomShares = floCrypto.createShamirsSecretShares(randomPrivKey, randomThreshold, randomThreshold) + writeSharesToIDB(randomShares) + //resolve private Key + resolve(privKey) + }) + }) + } + }); + + const checkIfPinRequired = key => new Promise((resolve, reject) => { + if (key.length == 52) + resolve(key) + else { + keyInput("PIN/Password").then(pwd => { + try { + let privKey = Crypto.AES.decrypt(key, pwd); + resolve(privKey) + } catch (error) { + reject("Access Denied: Incorrect PIN/Password") + } + }).catch(error => reject("Access Denied: PIN/Password required")) + } + }); + + return new Promise((resolve, reject) => { + getPrivateKeyCredentials().then(key => { + checkIfPinRequired(key).then(privKey => { + try { + user_public = floCrypto.getPubKeyHex(privKey); + user_id = floCrypto.getAddress(privKey); + if (startUpOptions.cloud) + floCloudAPI.user(user_id, privKey); //Set user for floCloudAPI + user_priv_wrap = () => checkIfPinRequired(key); + let n = floCrypto.randInt(12, 20); + aes_key = floCrypto.randString(n); + user_priv_raw = Crypto.AES.encrypt(privKey, aes_key); + user_private = user_priv_wrap; + resolve('Login Credentials loaded successful') + } catch (error) { + console.log(error) + reject("Corrupted Private Key") + } + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + var startUpLog = (status, log) => status ? console.log(log) : console.error(log); + + const callStartUpFunction = i => new Promise((resolve, reject) => { + startUpFunctions[i]().then(result => { + callStartUpFunction.completed += 1; + startUpLog(true, `${result}\nCompleted ${callStartUpFunction.completed}/${callStartUpFunction.total} Startup functions`) + resolve(true) + }).catch(error => { + callStartUpFunction.failed += 1; + startUpLog(false, `${error}\nFailed ${callStartUpFunction.failed}/${callStartUpFunction.total} Startup functions`) + reject(false) + }) + }); + + var _midFunction; + const midStartUp = () => new Promise((res, rej) => { + if (_midFunction instanceof Function) { + _midFunction() + .then(r => res("Mid startup function completed")) + .catch(e => rej("Mid startup function failed")) + } else + res("No mid startup function") + }); + + const callAndLog = p => new Promise((res, rej) => { + p.then(r => { + startUpLog(true, r) + res(r) + }).catch(e => { + startUpLog(false, e) + rej(e) + }) + }); + + floDapps.launchStartUp = function () { + return new Promise((resolve, reject) => { + initIndexedDB().then(log => { + console.log(log) + callStartUpFunction.total = startUpFunctions.length; + callStartUpFunction.completed = 0; + callStartUpFunction.failed = 0; + let p1 = new Promise((res, rej) => { + Promise.all(startUpFunctions.map((f, i) => callStartUpFunction(i))).then(r => { + callAndLog(midStartUp()) + .then(r => res(true)) + .catch(e => rej(false)) + }) + }); + let p2 = new Promise((res, rej) => { + callAndLog(getCredentials()).then(r => { + callAndLog(initUserDB()).then(r => { + callAndLog(loadUserDB()) + .then(r => res(true)) + .catch(e => rej(false)) + }).catch(e => rej(false)) + }).catch(e => rej(false)) + }) + Promise.all([p1, p2]) + .then(r => resolve('App Startup finished successful')) + .catch(e => reject('App Startup failed')) + }).catch(error => { + startUpLog(false, error); + reject("App database initiation failed") + }) + }) + } + + floDapps.addStartUpFunction = fn => fn instanceof Function && !startUpFunctions.includes(fn) ? startUpFunctions.push(fn) : false; + + floDapps.setMidStartup = fn => fn instanceof Function ? _midFunction = fn : false; + + floDapps.setCustomStartupLogger = fn => fn instanceof Function ? startUpLog = fn : false; + + floDapps.setCustomPrivKeyInput = fn => fn instanceof Function ? keyInput = fn : false; + + floDapps.setAppObjectStores = appObs => initIndexedDB.appObs = appObs; + + floDapps.storeContact = function (floID, name) { + return new Promise((resolve, reject) => { + if (!floCrypto.validateAddr(floID)) + return reject("Invalid floID!") + compactIDB.writeData("contacts", name, floID, user.db_name).then(result => { + user.contacts[floID] = name; + resolve("Contact stored") + }).catch(error => reject(error)) + }); + } + + floDapps.storePubKey = function (floID, pubKey) { + return new Promise((resolve, reject) => { + if (floID in user.pubKeys) + return resolve("pubKey already stored") + if (!floCrypto.validateAddr(floID)) + return reject("Invalid floID!") + if (!floCrypto.verifyPubKey(pubKey, floID)) + return reject("Incorrect pubKey") + compactIDB.writeData("pubKeys", pubKey, floID, user.db_name).then(result => { + user.pubKeys[floID] = pubKey; + resolve("pubKey stored") + }).catch(error => reject(error)) + }); + } + + floDapps.sendMessage = function (floID, message) { + return new Promise((resolve, reject) => { + let options = { + receiverID: floID, + application: DEFAULT.root, + comment: DEFAULT.application + } + if (floID in user.pubKeys) + message = floCrypto.encryptData(JSON.stringify(message), user.pubKeys[floID]) + floCloudAPI.sendApplicationData(message, "Message", options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + floDapps.requestInbox = function (callback) { + return new Promise((resolve, reject) => { + let lastVC = Object.keys(user.messages).sort().pop() + let options = { + receiverID: user.id, + application: DEFAULT.root, + lowerVectorClock: lastVC + 1 + } + let privKey = raw_user.private; + options.callback = (d, e) => { + for (let v in d) { + try { + if (d[v].message instanceof Object && "secret" in d[v].message) + d[v].message = floCrypto.decryptData(d[v].message, privKey) + } catch (error) { } + compactIDB.writeData("messages", d[v], v, user.db_name) + user.messages[v] = d[v] + } + if (callback instanceof Function) + callback(d, e) + } + floCloudAPI.requestApplicationData("Message", options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + floDapps.manageAppConfig = function (adminPrivKey, addList, rmList, settings) { + return new Promise((resolve, reject) => { + if (!startUpOptions.app_config) + return reject("No configs for this app"); + if (!Array.isArray(addList) || !addList.length) addList = undefined; + if (!Array.isArray(rmList) || !rmList.length) rmList = undefined; + if (!settings || typeof settings !== "object" || !Object.keys(settings).length) settings = undefined; + if (!addList && !rmList && !settings) + return reject("No configuration change") + var floData = { + [DEFAULT.application]: { + addSubAdmin: addList, + removeSubAdmin: rmList, + settings: settings + } + } + var floID = floCrypto.getFloID(adminPrivKey) + if (floID != DEFAULT.adminID) + reject('Access Denied for Admin privilege') + else + floBlockchainAPI.writeData(floID, JSON.stringify(floData), adminPrivKey) + .then(result => resolve(['Updated App Configuration', result])) + .catch(error => reject(error)) + }) + } + + floDapps.manageAppTrustedIDs = function (adminPrivKey, addList, rmList) { + return new Promise((resolve, reject) => { + if (!startUpOptions.app_config) + return reject("No configs for this app"); + if (!Array.isArray(addList) || !addList.length) addList = undefined; + if (!Array.isArray(rmList) || !rmList.length) rmList = undefined; + if (!addList && !rmList) + return reject("No change in list") + var floData = { + [DEFAULT.application]: { + addTrustedID: addList, + removeTrustedID: rmList + } + } + var floID = floCrypto.getFloID(adminPrivKey) + if (floID != DEFAULT.adminID) + reject('Access Denied for Admin privilege') + else + floBlockchainAPI.writeData(floID, JSON.stringify(floData), adminPrivKey) + .then(result => resolve(['Updated App Configuration', result])) + .catch(error => reject(error)) + }) + } + + const clearCredentials = floDapps.clearCredentials = function () { + return new Promise((resolve, reject) => { + compactIDB.clearData('credentials', DEFAULT.application).then(result => { + localStorage.removeItem(`${DEFAULT.application}#privKey`); + user.clear(); + resolve("privKey credentials deleted!") + }).catch(error => reject(error)) + }) + } + + floDapps.deleteUserData = function (credentials = false) { + return new Promise((resolve, reject) => { + let p = [] + p.push(compactIDB.deleteDB(user.db_name)) + if (credentials) + p.push(clearCredentials()) + Promise.all(p) + .then(result => resolve('User database(local) deleted')) + .catch(error => reject(error)) + }) + } + + floDapps.deleteAppData = function () { + return new Promise((resolve, reject) => { + compactIDB.deleteDB(DEFAULT.application).then(result => { + localStorage.removeItem(`${DEFAULT.application}#privKey`) + user.clear(); + compactIDB.removeData('lastTx', `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root) + .then(result => resolve("App database(local) deleted")) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + floDapps.securePrivKey = function (pwd) { + return new Promise(async (resolve, reject) => { + let indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`) + if (!indexArr) + return reject("PrivKey not found"); + indexArr = JSON.parse(indexArr) + let encryptedKey = Crypto.AES.encrypt(await user.private, pwd); + let threshold = indexArr.length; + let shares = floCrypto.createShamirsSecretShares(encryptedKey, threshold, threshold) + let promises = []; + let overwriteFn = (share, index) => + compactIDB.writeData("credentials", share, index, DEFAULT.application); + for (var i = 0; i < threshold; i++) + promises.push(overwriteFn(shares[i], indexArr[i])); + Promise.all(promises) + .then(results => resolve("Private Key Secured")) + .catch(error => reject(error)) + }) + } + + floDapps.verifyPin = function (pin = null) { + const readSharesFromIDB = function (indexArr) { + return new Promise((resolve, reject) => { + var promises = [] + for (var i = 0; i < indexArr.length; i++) + promises.push(compactIDB.readData('credentials', indexArr[i])) + Promise.all(promises).then(shares => { + var secret = floCrypto.retrieveShamirSecret(shares) + console.info(shares, secret) + if (secret) + resolve(secret) + else + reject("Shares are insufficient or incorrect") + }).catch(error => { + clearCredentials(); + location.reload(); + }) + }) + } + return new Promise((resolve, reject) => { + var indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`) + console.info(indexArr) + if (!indexArr) + reject('No login credentials found') + readSharesFromIDB(JSON.parse(indexArr)).then(key => { + if (key.length == 52) { + if (pin === null) + resolve("Private key not secured") + else + reject("Private key not secured") + } else { + if (pin === null) + return reject("PIN/Password required") + try { + let privKey = Crypto.AES.decrypt(key, pin); + resolve("PIN/Password verified") + } catch (error) { + reject("Incorrect PIN/Password") + } + } + }).catch(error => reject(error)) + }) + } + + const getNextGeneralData = floDapps.getNextGeneralData = function (type, vectorClock = null, options = {}) { + var fk = floCloudAPI.util.filterKey(type, options) + vectorClock = vectorClock || getNextGeneralData[fk] || '0'; + var filteredResult = {} + if (floGlobals.generalData[fk]) { + for (let d in floGlobals.generalData[fk]) + if (d > vectorClock) + filteredResult[d] = JSON.parse(JSON.stringify(floGlobals.generalData[fk][d])) + } else if (options.comment) { + let comment = options.comment; + delete options.comment; + let fk = floCloudAPI.util.filterKey(type, options); + for (let d in floGlobals.generalData[fk]) + if (d > vectorClock && floGlobals.generalData[fk][d].comment == comment) + filteredResult[d] = JSON.parse(JSON.stringify(floGlobals.generalData[fk][d])) + } + if (options.decrypt) { + let decryptionKey = (options.decrypt === true) ? raw_user.private : options.decrypt; + if (!Array.isArray(decryptionKey)) + decryptionKey = [decryptionKey]; + for (let f in filteredResult) { + let data = filteredResult[f] + try { + if (data.message instanceof Object && "secret" in data.message) { + for (let key of decryptionKey) { + try { + let tmp = floCrypto.decryptData(data.message, key) + data.message = JSON.parse(tmp) + break; + } catch (error) { } + } + } + } catch (error) { } + } + } + getNextGeneralData[fk] = Object.keys(filteredResult).sort().pop(); + return filteredResult; + } + + const syncData = floDapps.syncData = {}; + + syncData.oldDevice = () => new Promise((resolve, reject) => { + let sync = { + contacts: user.contacts, + pubKeys: user.pubKeys, + messages: user.messages + } + let message = Crypto.AES.encrypt(JSON.stringify(sync), raw_user.private) + let options = { + receiverID: user.id, + application: DEFAULT.root + } + floCloudAPI.sendApplicationData(message, "syncData", options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }); + + syncData.newDevice = () => new Promise((resolve, reject) => { + var options = { + receiverID: user.id, + senderID: user.id, + application: DEFAULT.root, + mostRecent: true, + } + floCloudAPI.requestApplicationData("syncData", options).then(response => { + let vc = Object.keys(response).sort().pop() + let sync = JSON.parse(Crypto.AES.decrypt(response[vc].message, raw_user.private)) + let promises = [] + let store = (key, val, obs) => promises.push(compactIDB.writeData(obs, val, key, user.db_name)); + ["contacts", "pubKeys", "messages"].forEach(c => { + for (let i in sync[c]) { + store(i, sync[c][i], c) + user[c][i] = sync[c][i] + } + }) + Promise.all(promises) + .then(results => resolve("Sync data successful")) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }); +})('object' === typeof module ? module.exports : window.floDapps = {}); \ No newline at end of file diff --git a/floTokenAPI.js b/floTokenAPI.js new file mode 100644 index 0000000..2456b88 --- /dev/null +++ b/floTokenAPI.js @@ -0,0 +1,166 @@ +(function (EXPORTS) { //floTokenAPI v1.0.4a + /* Token Operator to send/receive tokens via blockchain using API calls*/ + 'use strict'; + const tokenAPI = EXPORTS; + + const DEFAULT = { + apiURL: floGlobals.tokenURL || "https://ranchimallflo.duckdns.org/", + currency: floGlobals.currency || "rupee" + } + + Object.defineProperties(tokenAPI, { + URL: { + get: () => DEFAULT.apiURL + }, + currency: { + get: () => DEFAULT.currency, + set: currency => DEFAULT.currency = currency + } + }); + + if (floGlobals.currency) tokenAPI.currency = floGlobals.currency; + + Object.defineProperties(floGlobals, { + currency: { + get: () => DEFAULT.currency, + set: currency => DEFAULT.currency = currency + } + }); + + const fetch_api = tokenAPI.fetch = function (apicall) { + return new Promise((resolve, reject) => { + console.debug(DEFAULT.apiURL + apicall); + fetch(DEFAULT.apiURL + apicall).then(response => { + if (response.ok) + response.json().then(data => resolve(data)); + else + reject(response) + }).catch(error => reject(error)) + }) + } + + const getBalance = tokenAPI.getBalance = function (floID, token = DEFAULT.currency) { + return new Promise((resolve, reject) => { + fetch_api(`api/v1.0/getFloAddressBalance?token=${token}&floAddress=${floID}`) + .then(result => resolve(result.balance || 0)) + .catch(error => reject(error)) + }) + } + + tokenAPI.getTx = function (txID) { + return new Promise((resolve, reject) => { + fetch_api(`api/v1.0/getTransactionDetails/${txID}`).then(res => { + if (res.result === "error") + reject(res.description); + else if (!res.parsedFloData) + reject("Data piece (parsedFloData) missing"); + else if (!res.transactionDetails) + reject("Data piece (transactionDetails) missing"); + else + resolve(res); + }).catch(error => reject(error)) + }) + } + + tokenAPI.sendToken = function (privKey, amount, receiverID, message = "", token = DEFAULT.currency, options = {}) { + return new Promise((resolve, reject) => { + let senderID = floCrypto.getFloID(privKey); + if (typeof amount !== "number" || isNaN(amount) || amount <= 0) + return reject("Invalid amount"); + getBalance(senderID, token).then(bal => { + if (amount > bal) + return reject(`Insufficient ${token}# balance`); + floBlockchainAPI.writeData(senderID, `send ${amount} ${token}# ${message}`, privKey, receiverID, options) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }); + } + + 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}`) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } + + const util = tokenAPI.util = {}; + + util.parseTxData = function (txData) { + let parsedData = {}; + for (let p in txData.parsedFloData) + parsedData[p] = txData.parsedFloData[p]; + parsedData.sender = txData.transactionDetails.vin[0].addr; + for (let vout of txData.transactionDetails.vout) + if (vout.scriptPubKey.addresses[0] !== parsedData.sender) + parsedData.receiver = vout.scriptPubKey.addresses[0]; + parsedData.time = txData.transactionDetails.time; + return parsedData; + } + +})('object' === typeof module ? module.exports : window.floTokenAPI = {}); \ No newline at end of file diff --git a/index.html b/index.html index 153a807..ecfbd7b 100644 --- a/index.html +++ b/index.html @@ -7,42 +7,18 @@ RanchiMall Times - - - - @@ -274,12 +250,12 @@
- - Analytics - Requests - Writers - CC - + + Analytics + Requests + Writers + CC +
    @@ -730,13 +706,17 @@ - - - - - - - + + + + + + + + + + +