`
+ document.querySelectorAll('.messenger-illustration').forEach(elem => {
+ elem.innerHTML = messengerIllustration
+ })
diff --git a/scripts/btcOperator.js b/scripts/btcOperator.js
new file mode 100644
index 0000000..cf3d316
--- /dev/null
+++ b/scripts/btcOperator.js
@@ -0,0 +1,554 @@
+(function (EXPORTS) { //btcOperator v1.0.8
+ /* BTC Crypto and API Operator */
+ const btcOperator = EXPORTS;
+
+ //This library uses API provided by chain.so (https://chain.so/)
+ const URL = "https://chain.so/api/v2/";
+
+ const fetch_api = btcOperator.fetch = function (api) {
+ return new Promise((resolve, reject) => {
+ console.debug(URL + api);
+ fetch(URL + api).then(response => {
+ response.json()
+ .then(result => result.status === "success" ? resolve(result) : reject(result))
+ .catch(error => reject(error))
+ }).catch(error => reject(error))
+ })
+ };
+
+ const SIGN_SIZE = 73;
+
+ 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(result.regular))
+ .catch(error => reject(error));
+ else
+ reject(response);
+ }).catch(error => reject(error))
+ })
+ }
+
+ const broadcast = btcOperator.broadcast = rawtx => new Promise((resolve, reject) => {
+ $.ajax({
+ type: "POST",
+ url: URL + "send_tx/BTC/",
+ data: {
+ "tx_hex": rawtx
+ },
+ dataType: "json",
+ error: e => reject(e.responseJSON),
+ success: r => r.status === "success" ? resolve(r.data) : reject(r)
+ })
+ });
+
+ 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"].includes(type))
+ return type;
+ else
+ return false;
+ }
+
+ btcOperator.multiSigAddress = function (pubKeys, minRequired) {
+ 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";
+ return coinjs.pubkeys2MultisigAddress(pubKeys, minRequired);
+ }
+
+ //convert from one blockchain to another blockchain (target version)
+ btcOperator.convert = {};
+
+ btcOperator.convert.wif = function (source_wif, target_version = coinjs.priv) {
+ let keyHex = decodeLegacy(source_wif).hex;
+ if (!keyHex || keyHex.length < 66 || !/01$/.test(keyHex))
+ return null;
+ else
+ return encodeLegacy(keyHex, target_version);
+ }
+
+ btcOperator.convert.legacy2legacy = function (source_addr, target_version = coinjs.pub) {
+ let rawHex = decodeLegacy(source_addr).hex;
+ if (!rawHex)
+ return null;
+ else
+ return encodeLegacy(rawHex, target_version);
+ }
+
+ btcOperator.convert.legacy2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) {
+ let rawHex = decodeLegacy(source_addr).hex;
+ if (!rawHex)
+ return null;
+ else
+ return encodeBech32(rawHex, target_version, target_hrp);
+ }
+
+ btcOperator.convert.bech2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) {
+ let rawHex = decodeBech32(source_addr).hex;
+ if (!rawHex)
+ return null;
+ else
+ return encodeBech32(rawHex, target_version, target_hrp);
+ }
+
+ btcOperator.convert.bech2legacy = function (source_addr, target_version = coinjs.pub) {
+ let rawHex = decodeBech32(source_addr).hex;
+ if (!rawHex)
+ return null;
+ else
+ return encodeLegacy(rawHex, target_version);
+ }
+
+ function decodeLegacy(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 null;
+ let version = raw.shift();
+ return {
+ version: version,
+ hex: Crypto.util.bytesToHex(raw)
+ }
+ }
+
+ function encodeLegacy(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));
+ }
+
+ function decodeBech32(source) {
+ let decode = coinjs.bech32_decode(source);
+ if (!decode)
+ return null;
+ 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)
+ }
+ }
+
+ function encodeBech32(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(`get_address_balance/BTC/${addr}`)
+ .then(result => resolve(parseFloat(result.data.confirmed_balance)))
+ .catch(error => reject(error))
+ });
+
+ 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 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 keys:" + 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_addr && !validateAddress(parameters.change_addr))
+ throw "Invalid change_address:" + parameters.change_addr;
+ //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;
+ }
+
+ const TMP_FEE = 0.00001;
+
+ function createTransaction(senders, redeemScripts, receivers, amounts, fee, change_addr) {
+ return new Promise((resolve, reject) => {
+ let auto_fee = false,
+ total_amount = parseFloat(amounts.reduce((t, a) => t + a, 0).toFixed(8));
+ if (fee === null) {
+ auto_fee = true;
+ fee = TMP_FEE;
+ }
+ const tx = coinjs.transaction();
+ addUTXOs(tx, senders, redeemScripts, total_amount + fee).then(result => {
+ if (result > 0)
+ return reject("Insufficient Balance");
+ let change = addOutputs(tx, receivers, amounts, Math.abs(result), change_addr);
+ if (!auto_fee)
+ return resolve(tx);
+ autoFeeCalc(tx).then(fee_calc => {
+ fee = Math.round((fee * 1) * 1e8); //satoshi convertion
+ if (!change)
+ tx.addoutput(change_addr, 0);
+ editFee(tx, fee, fee_calc);
+ resolve(tx);
+ }).catch(error => reject(error))
+ })
+ })
+ }
+
+ function addUTXOs(tx, senders, redeemScripts, required_amount, n = 0) {
+ return new Promise((resolve, reject) => {
+ required_amount = parseFloat(required_amount.toFixed(8));
+ if (required_amount <= 0 || n >= senders.length)
+ return resolve(required_amount);
+ let addr = senders[n],
+ rs = redeemScripts[n];
+ fetch_api(`get_tx_unspent/BTC/${addr}`).then(result => {
+ let utxos = result.data.txs;
+ 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;
+ required_amount -= parseFloat(utxos[i].value);
+ var script;
+ if (!rs || !rs.length) //legacy script
+ script = utxos[i].script_hex;
+ else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi))) {
+ //redeemScript for segwit/bech32
+ let s = coinjs.script();
+ s.writeBytes(Crypto.util.hexToBytes(rs));
+ s.writeOp(0);
+ s.writeBytes(coinjs.numToBytes((utxos[i].value * 100000000).toFixed(0), 8));
+ script = Crypto.util.bytesToHex(s.buffer);
+ } else //redeemScript for multisig
+ script = rs;
+ tx.addinput(utxos[i].txid, utxos[i].output_no, script, 0xfffffffd /*sequence*/); //0xfffffffd for Replace-by-fee
+ }
+ addUTXOs(tx, senders, redeemScripts, required_amount, n + 1)
+ .then(result => resolve(result))
+ .catch(error => reject(error))
+ }).catch(error => reject(error))
+ })
+ }
+
+ function addOutputs(tx, receivers, amounts, change, change_addr) {
+ for (let i in receivers)
+ tx.addoutput(receivers[i], amounts[i]);
+ if (parseFloat(change.toFixed(8)) > 0) {
+ tx.addoutput(change_addr, change);
+ return true;
+ } else
+ return false;
+ }
+
+ 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.out.length - index);
+ if (index < 0 || index >= tx.out.length)
+ throw "Invalid index";
+ let edit_value = parseInt(current_fee - target_fee), //rip of any decimal places
+ current_value = tx.out[index].value; //could be BigInterger
+ if (edit_value < 0 && edit_value > current_value)
+ throw "Insufficient value at vout";
+ tx.out[index].value = current_value instanceof BigInteger ?
+ current_value.add(new BigInteger('' + edit_value)) : parseInt(current_value + edit_value);
+ }
+
+ btcOperator.sendTx = function (senders, privkeys, receivers, amounts, fee, change_addr = null) {
+ return new Promise((resolve, reject) => {
+ try {
+ ({
+ senders,
+ privkeys,
+ receivers,
+ amounts
+ } = validateTxParameters({
+ senders,
+ privkeys,
+ receivers,
+ amounts,
+ fee,
+ change_addr
+ }));
+ } 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, change_addr || senders[0]).then(tx => {
+ console.debug("Unsigned:", tx.serialize());
+ new Set(wif_keys).forEach(key => console.debug("Signing key:", key, tx.sign(key, 1 /*sighashtype*/))); //Sign the tx using private key WIF
+ console.debug("Signed:", tx.serialize());
+ debugger;
+ broadcast(tx.serialize())
+ .then(result => resolve(result))
+ .catch(error => reject(error));
+ }).catch(error => reject(error));
+ })
+ }
+
+ btcOperator.createTx = function (senders, receivers, amounts, fee = null, change_addr = null) {
+ return new Promise((resolve, reject) => {
+ try {
+ ({
+ senders,
+ receivers,
+ amounts
+ } = validateTxParameters({
+ senders,
+ receivers,
+ amounts,
+ fee,
+ change_addr
+ }));
+ } 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, change_addr || senders[0])
+ .then(tx => resolve(tx.serialize()))
+ .catch(error => reject(error))
+ })
+ }
+
+ btcOperator.createMultiSigTx = function (sender, redeemScript, receivers, amounts, fee) {
+ return new Promise((resolve, reject) => {
+ //validate tx parameters
+ if (validateAddress(sender) !== "multisig")
+ return reject("Invalid sender (multisig):" + sender);
+ else {
+ let script = coinjs.script();
+ let decode = script.decodeRedeemScript(redeemScript);
+ if (!decode || decode.address !== sender)
+ return reject("Invalid redeem-script");
+ }
+ try {
+ ({
+ receivers,
+ amounts
+ } = validateTxParameters({
+ receivers,
+ amounts,
+ fee
+ }));
+ } catch (e) {
+ return reject(e)
+ }
+ //create transaction
+ createTransaction([sender], [redeemScript], receivers, amounts, fee, sender)
+ .then(tx => resolve(tx.serialize()))
+ .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();
+ }
+
+ 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')
+ n.push(s.signed == 'true' || (tx.witness[i] && tx.witness[i].length == 2))
+ else {
+ var rs = coinjs.script().decodeRedeemScript(s.script);
+ 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);
+ if (tx1.ins.length !== tx2.ins.length || tx1.outs.length !== tx2.outs.length)
+ return false;
+ 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;
+ for (let i = 0; i < tx2.ins.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;
+ }
+
+ btcOperator.getTx = txid => new Promise((resolve, reject) => {
+ fetch_api(`get_tx/BTC/${txid}`)
+ .then(result => resolve(result.data))
+ .catch(error => reject(error))
+ });
+
+ btcOperator.getAddressData = addr => new Promise((resolve, reject) => {
+ fetch_api(`address/BTC/${addr}`)
+ .then(result => resolve(result.data))
+ .catch(error => reject(error))
+ });
+
+ btcOperator.getBlock = block => new Promise((resolve, reject) => {
+ fetch_api(`get_block/BTC/${block}`)
+ .then(result => resolve(result.data))
+ .catch(error => reject(error))
+ });
+
+})('object' === typeof module ? module.exports : window.btcOperator = {});
\ No newline at end of file
diff --git a/scripts/components.js b/scripts/components.js
index 056bfc5..2b13864 100644
--- a/scripts/components.js
+++ b/scripts/components.js
@@ -1,2907 +1,18 @@
/*jshint esversion: 6 */
-//Button
-const smButton = document.createElement('template')
-smButton.innerHTML = `
-
-
-
-
`;
-customElements.define('sm-button',
- class extends HTMLElement {
- constructor() {
- super();
- this.attachShadow({
- mode: 'open'
- }).append(smButton.content.cloneNode(true));
- }
- static get observedAttributes() {
- return ['disabled'];
- }
-
- get disabled() {
- return this.hasAttribute('disabled');
- }
-
- set disabled(value) {
- if (value) {
- this.setAttribute('disabled', '');
- } else {
- this.removeAttribute('disabled');
- }
- }
- focusIn() {
- this.focus();
- }
-
- handleKeyDown(e) {
- if (!this.hasAttribute('disabled') && (e.key === 'Enter' || e.key === ' ')) {
- e.preventDefault();
- this.click();
- }
- }
-
- connectedCallback() {
- if (!this.hasAttribute('disabled')) {
- this.setAttribute('tabindex', '0');
- }
- this.setAttribute('role', 'button');
- this.addEventListener('keydown', this.handleKeyDown);
- }
- attributeChangedCallback(name) {
- if (name === 'disabled') {
- if (this.hasAttribute('disabled')) {
- this.removeAttribute('tabindex');
- } else {
- this.setAttribute('tabindex', '0');
- }
- this.setAttribute('aria-disabled', this.hasAttribute('disabled'));
- }
- }
- })
-
-//Input
-const smInput = document.createElement('template')
-smInput.innerHTML = `
-
-
-`;
-customElements.define('sm-input',
- class extends HTMLElement {
-
- constructor() {
- super();
- this.attachShadow({
- mode: 'open'
- }).append(smInput.content.cloneNode(true));
-
- 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._helperText = '';
- this._errorText = '';
- this.isRequired = false;
- this.validationFunction = undefined;
- this.reflectedAttributes = ['value', 'required', 'disabled', 'type', 'inputmode', 'readonly', 'min', 'max', 'pattern', 'minlength', 'maxlength', 'step'];
-
- 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.vibrate = this.vibrate.bind(this);
- }
-
- static get observedAttributes() {
- return ['value', 'placeholder', 'required', 'disabled', 'type', 'inputmode', 'readonly', 'min', 'max', 'pattern', 'minlength', 'maxlength', 'step', 'helper-text', 'error-text', 'hiderequired'];
- }
-
- get value() {
- return this.input.value;
- }
-
- set value(val) {
- this.input.value = val;
- this.checkInput();
- this.fireEvent();
- }
-
- get placeholder() {
- return this.getAttribute('placeholder');
- }
-
- set placeholder(val) {
- this.setAttribute('placeholder', val);
- }
-
- get type() {
- return this.getAttribute('type');
- }
-
- set type(val) {
- this.setAttribute('type', val);
- }
-
- get validity() {
- return this.input.validity;
- }
-
- get disabled() {
- return this.hasAttribute('disabled');
- }
- set disabled(value) {
- if (value)
- this.inputParent.classList.add('disabled');
- else
- this.inputParent.classList.remove('disabled');
- }
- get readOnly() {
- return this.hasAttribute('readonly');
- }
- set readOnly(value) {
- if (value) {
- this.setAttribute('readonly', '');
- } else {
- this.removeAttribute('readonly');
- }
- }
- set customValidation(val) {
- this.validationFunction = val;
- }
- set errorText(val) {
- this._errorText = val;
- }
- set helperText(val) {
- this._helperText = val;
- }
- get isValid() {
- if (this.input.value !== '') {
- const _isValid = this.input.checkValidity();
- let _customValid = true;
- if (this.validationFunction) {
- _customValid = Boolean(this.validationFunction(this.input.value));
- }
- if (_isValid && _customValid) {
- this.feedbackText.classList.remove('error');
- this.feedbackText.classList.add('success');
- this.feedbackText.textContent = '';
- } else {
- if (this._errorText) {
- this.feedbackText.classList.add('error');
- this.feedbackText.classList.remove('success');
- this.feedbackText.innerHTML = `
-
- ${this._errorText}
- `;
- }
- }
- return (_isValid && _customValid);
- }
- }
- reset() {
- this.value = '';
- }
- clear() {
- this.value = '';
- this.input.focus();
- }
-
- focusIn() {
- this.input.focus();
- }
-
- focusOut() {
- this.input.blur();
- }
-
- fireEvent() {
- let event = new Event('input', {
- bubbles: true,
- cancelable: true,
- composed: true
- });
- this.dispatchEvent(event);
- }
-
- checkInput(e) {
- if (!this.hasAttribute('readonly')) {
- if (this.input.value.trim() !== '') {
- this.clearBtn.classList.remove('hide');
- } else {
- this.clearBtn.classList.add('hide');
- }
- }
- if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder').trim() === '') return;
- if (this.input.value !== '') {
- if (this.animate)
- this.inputParent.classList.add('animate-placeholder');
- else
- this.label.classList.add('hide');
- } else {
- if (this.animate)
- this.inputParent.classList.remove('animate-placeholder');
- else
- this.label.classList.remove('hide');
- this.feedbackText.textContent = '';
- }
- }
- vibrate() {
- this.outerContainer.animate([
- { transform: 'translateX(-1rem)' },
- { transform: 'translateX(1rem)' },
- { transform: 'translateX(-0.5rem)' },
- { transform: 'translateX(0.5rem)' },
- { transform: 'translateX(0)' },
- ], {
- duration: 300,
- easing: 'ease'
- });
- }
-
-
- connectedCallback() {
- this.animate = this.hasAttribute('animate');
- this.setAttribute('role', 'textbox');
- this.input.addEventListener('input', this.checkInput);
- this.clearBtn.addEventListener('click', this.clear);
- }
-
- attributeChangedCallback(name, oldValue, newValue) {
- if (oldValue !== newValue) {
- if (this.reflectedAttributes.includes(name)) {
- if (this.hasAttribute(name)) {
- this.input.setAttribute(name, this.getAttribute(name) ? this.getAttribute(name) : '');
- }
- else {
- this.input.removeAttribute(name);
- }
- }
- if (name === 'placeholder') {
- this.label.textContent = newValue;
- this.setAttribute('aria-label', newValue);
- }
- else if (this.hasAttribute('value')) {
- this.checkInput();
- }
- else if (name === 'type') {
- if (this.hasAttribute('type') && this.getAttribute('type') === 'number') {
- this.input.setAttribute('inputmode', 'decimal');
- }
- }
- else if (name === 'helper-text') {
- this._helperText = this.getAttribute('helper-text');
- }
- else if (name === 'error-text') {
- this._errorText = this.getAttribute('error-text');
- }
- else if (name === 'required') {
- this.isRequired = this.hasAttribute('required');
- if (this.isRequired) {
- this.setAttribute('aria-required', 'true');
- }
- else {
- this.setAttribute('aria-required', 'false');
- }
- }
- else if (name === 'readonly') {
- if (this.hasAttribute('readonly')) {
- this.inputParent.classList.add('readonly');
- } else {
- this.inputParent.classList.remove('readonly');
- }
- }
- else if (name === 'disabled') {
- if (this.hasAttribute('disabled')) {
- this.inputParent.classList.add('disabled');
- }
- else {
- this.inputParent.classList.remove('disabled');
- }
- }
- }
- }
- disconnectedCallback() {
- this.input.removeEventListener('input', this.checkInput);
- this.clearBtn.removeEventListener('click', this.clear);
- }
- })
-
-//textarea
-const smTextarea = document.createElement('template')
-smTextarea.innerHTML = `
-
-
- `;
-customElements.define('sm-textarea',
- class extends HTMLElement {
- constructor() {
- super()
- this.attachShadow({
- mode: 'open'
- }).append(smTextarea.content.cloneNode(true))
-
- 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(val) {
- this.setAttribute('value', val)
- this.fireEvent()
- }
- get disabled() {
- return this.hasAttribute('disabled')
- }
- set disabled(val) {
- if (val) {
- this.setAttribute('disabled', '')
- } else {
- this.removeAttribute('disabled')
- }
- }
- get isValid() {
- return this.textarea.checkValidity()
- }
- reset() {
- this.setAttribute('value', '')
- }
- focusIn() {
- this.textarea.focus()
- }
- fireEvent() {
- let event = new Event('input', {
- bubbles: true,
- cancelable: true,
- composed: true
- });
- this.dispatchEvent(event);
- }
- checkInput() {
- if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder') === '')
- return;
- if (this.textarea.value !== '') {
- this.placeholder.classList.add('hide')
- } else {
- this.placeholder.classList.remove('hide')
- }
- }
- connectedCallback() {
- this.textarea.addEventListener('input', e => {
- this.textareaBox.dataset.value = this.textarea.value
- this.checkInput()
- })
- }
- attributeChangedCallback(name, oldValue, newValue) {
- if (this.reflectedAttributes.includes(name)) {
- if (this.hasAttribute(name)) {
- this.textarea.setAttribute(name, this.getAttribute(name) ? this.getAttribute(name) : '')
- }
- else {
- this.textContent.removeAttribute(name)
- }
- }
- else if (name === 'placeholder') {
- this.placeholder.textContent = this.getAttribute('placeholder')
- }
- else if (name === 'value') {
- this.textarea.value = newValue;
- this.textareaBox.dataset.value = newValue
- this.checkInput()
- }
- }
- })
-const smForm = document.createElement('template');
-smForm.innerHTML = `
-
-
- `;
-
-customElements.define('sm-form', class extends HTMLElement {
- constructor() {
- super()
- this.attachShadow({
- mode: 'open'
- }).append(smForm.content.cloneNode(true))
-
- this.form = this.shadowRoot.querySelector('form');
- this.formElements
- this.requiredElements
- this.submitButton
- this.resetButton
- this.allRequiredValid = false;
-
- 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)
- }
- debounce(callback, wait) {
- let timeoutId = null;
- return (...args) => {
- window.clearTimeout(timeoutId);
- timeoutId = window.setTimeout(() => {
- callback.apply(null, args);
- }, wait);
- };
- }
- _checkValidity() {
- this.allRequiredValid = this.requiredElements.every(elem => elem.isValid)
- if (!this.submitButton) return;
- if (this.allRequiredValid) {
- this.submitButton.disabled = false;
- }
- else {
- this.submitButton.disabled = true;
- }
- }
- handleKeydown(e) {
- if (e.key === 'Enter' && !e.target.tagName.includes('TEXTAREA')) {
- if (this.allRequiredValid) {
- if (this.submitButton) {
- this.submitButton.click()
- }
- this.dispatchEvent(new CustomEvent('submit', {
- bubbles: true,
- composed: true,
- }))
- }
- else {
- this.requiredElements.find(elem => !elem.isValid).vibrate()
- }
- }
- }
- reset() {
- this.formElements.forEach(elem => elem.reset())
- }
- elementsChanged() {
- this.formElements = [...this.querySelectorAll('sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio')]
- this.requiredElements = this.formElements.filter(elem => elem.hasAttribute('required'));
- this.submitButton = this.querySelector('[variant="primary"], [type="submit"]');
- this.resetButton = this.querySelector('[type="reset"]');
- if (this.resetButton) {
- this.resetButton.addEventListener('click', this.reset);
- }
- this._checkValidity()
- }
- connectedCallback() {
- const slot = this.shadowRoot.querySelector('slot')
- slot.addEventListener('slotchange', this.elementsChanged)
- this.addEventListener('input', this.debounce(this._checkValidity, 100));
- this.addEventListener('keydown', this.debounce(this.handleKeydown, 100));
- }
- disconnectedCallback() {
- this.removeEventListener('input', this.debounce(this._checkValidity, 100));
- this.removeEventListener('keydown', this.debounce(this.handleKeydown, 100));
- }
-})
-//switch
-
-const smSwitch = document.createElement('template')
-smSwitch.innerHTML = `
-
-
`
-
-customElements.define('sm-switch', class extends HTMLElement {
- constructor() {
- super()
- this.attachShadow({
- mode: 'open'
- }).append(smSwitch.content.cloneNode(true))
- this.switch = this.shadowRoot.querySelector('.switch');
- this.input = this.shadowRoot.querySelector('input')
- this.isChecked = false
- this.isDisabled = false
-
- this.dispatch = this.dispatch.bind(this)
- }
-
- static get observedAttributes() {
- return ['disabled', 'checked']
- }
-
- get disabled() {
- return this.isDisabled
- }
-
- set disabled(val) {
- if (val) {
- this.setAttribute('disabled', '')
- } else {
- this.removeAttribute('disabled')
- }
- }
-
- get checked() {
- return this.isChecked
- }
-
- set checked(value) {
- if (value) {
- this.setAttribute('checked', '')
- } else {
- this.removeAttribute('checked')
- }
- }
- get value() {
- return this.isChecked
- }
-
- reset() {
-
- }
-
- dispatch() {
- this.dispatchEvent(new CustomEvent('change', {
- bubbles: true,
- composed: true,
- detail: {
- value: this.isChecked
- }
- }))
- }
-
- connectedCallback() {
- this.addEventListener('keydown', e => {
- if (e.key === ' ' && !this.isDisabled) {
- e.preventDefault()
- this.input.click()
- }
- })
- this.input.addEventListener('click', e => {
- if (this.input.checked)
- this.checked = true
- else
- this.checked = false
- this.dispatch()
- })
- }
- attributeChangedCallback(name, oldValue, newValue) {
- if (oldValue !== newValue) {
- if (name === 'disabled') {
- if (this.hasAttribute('disabled')) {
- this.disabled = true
- }
- else {
- this.disabled = false
- }
- }
- else if (name === 'checked') {
- if (this.hasAttribute('checked')) {
- this.isChecked = true
- this.input.checked = true
- }
- else {
- this.isChecked = false
- this.input.checked = false
- }
- }
- }
- }
-
-})
-
-// select
-const smSelect = document.createElement('template')
-smSelect.innerHTML = `
-
-
`;
-customElements.define('sm-select', class extends HTMLElement {
- constructor() {
- super()
- this.attachShadow({
- mode: 'open'
- }).append(smSelect.content.cloneNode(true))
-
- 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.availableOptions
- this.previousOption
- this.isOpen = false;
- this.label = ''
- this.slideDown = [{
- transform: `translateY(-0.5rem)`,
- opacity: 0
- },
- {
- transform: `translateY(0)`,
- opacity: 1
- }
- ]
- this.slideUp = [{
- transform: `translateY(0)`,
- opacity: 1
- },
- {
- transform: `translateY(-0.5rem)`,
- opacity: 0
- }
- ]
- this.animationOptions = {
- duration: 300,
- fill: "forwards",
- easing: 'ease'
- }
-
- this.optionList = this.shadowRoot.querySelector('.options')
- this.chevron = this.shadowRoot.querySelector('.toggle')
- this.selection = this.shadowRoot.querySelector('.selection')
- this.selectedOptionText = this.shadowRoot.querySelector('.selected-option-text')
- }
- static get observedAttributes() {
- return ['disabled', 'label']
- }
- get value() {
- return this.getAttribute('value')
- }
- set value(val) {
- const selectedOption = this.availableOptions.find(option => option.getAttribute('value') === val)
- if (selectedOption) {
- this.setAttribute('value', val)
- this.selectedOptionText.textContent = `${this.label}${selectedOption.textContent}`;
- if (this.previousOption) {
- this.previousOption.classList.remove('check-selected')
- }
- selectedOption.classList.add('check-selected')
- this.previousOption = selectedOption
- } else {
- console.warn(`There is no option with ${val} as value`)
- }
- }
-
- reset(fire = true) {
- if (this.availableOptions[0] && this.previousOption !== this.availableOptions[0]) {
- const firstElement = this.availableOptions[0];
- if (this.previousOption) {
- this.previousOption.classList.remove('check-selected')
- }
- firstElement.classList.add('check-selected')
- this.value = firstElement.getAttribute('value')
- this.selectedOptionText.textContent = `${this.label}${firstElement.textContent}`
- this.previousOption = firstElement;
- if (fire) {
- this.fireEvent()
- }
- }
- }
-
- focusIn() {
- this.selection.focus()
- }
-
- open() {
- this.optionList.classList.remove('hide')
- this.optionList.animate(this.slideDown, this.animationOptions)
- this.chevron.classList.add('rotate')
- this.isOpen = true
- }
- collapse() {
- this.chevron.classList.remove('rotate')
- this.optionList.animate(this.slideUp, this.animationOptions)
- .onfinish = () => {
- this.optionList.classList.add('hide')
- this.isOpen = false
- }
- }
- toggle() {
- if (!this.isOpen && !this.hasAttribute('disabled')) {
- this.open()
- } else {
- this.collapse()
- }
- }
-
- fireEvent() {
- this.dispatchEvent(new CustomEvent('change', {
- bubbles: true,
- composed: true,
- detail: {
- value: this.value
- }
- }))
- }
-
- handleOptionsNavigation(e) {
- if (e.key === 'ArrowUp') {
- e.preventDefault()
- if (document.activeElement.previousElementSibling) {
- document.activeElement.previousElementSibling.focus()
- } else {
- this.availableOptions[this.availableOptions.length - 1].focus()
- }
- }
- else if (e.key === 'ArrowDown') {
- e.preventDefault()
- if (document.activeElement.nextElementSibling) {
- document.activeElement.nextElementSibling.focus()
- } else {
- this.availableOptions[0].focus()
- }
- }
- }
- handleOptionSelection(e) {
- if (this.previousOption !== document.activeElement) {
- this.value = document.activeElement.getAttribute('value')
- this.fireEvent()
- }
- }
- handleClick(e) {
- if (e.target === this) {
- this.toggle()
- }
- else {
- this.handleOptionSelection()
- this.collapse()
- }
- }
- handleKeydown(e) {
- if (e.target === this) {
- if (this.isOpen && e.key === 'ArrowDown') {
- e.preventDefault()
- this.availableOptions[0].focus()
- this.handleOptionSelection(e)
- }
- else if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault()
- this.toggle()
- }
- }
- else {
- this.handleOptionsNavigation(e)
- this.handleOptionSelection(e)
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault()
- this.collapse()
- }
- }
- }
- handleClickOutside(e) {
- if (this.isOpen && !this.contains(e.target)) {
- this.collapse()
- }
- }
- connectedCallback() {
- this.setAttribute('role', 'listbox')
- if (!this.hasAttribute('disabled')) {
- this.selection.setAttribute('tabindex', '0')
- }
- let slot = this.shadowRoot.querySelector('slot')
- slot.addEventListener('slotchange', e => {
- this.availableOptions = slot.assignedElements()
- this.reset(false)
- });
- this.addEventListener('click', this.handleClick)
- this.addEventListener('keydown', this.handleKeydown)
- document.addEventListener('mousedown', this.handleClickOutside)
- }
- disconnectedCallback() {
- this.removeEventListener('click', this.toggle)
- this.removeEventListener('keydown', this.handleKeydown)
- document.removeEventListener('mousedown', this.handleClickOutside)
- }
- attributeChangedCallback(name) {
- if (name === "disabled") {
- if (this.hasAttribute('disabled')) {
- this.selection.removeAttribute('tabindex')
- } else {
- this.selection.setAttribute('tabindex', '0')
- }
- } else if (name === 'label') {
- this.label = this.hasAttribute('label') ? `${this.getAttribute('label')} ` : ''
- }
- }
-})
-
-// option
-const smOption = document.createElement('template')
-smOption.innerHTML = `
-
-
`;
-customElements.define('sm-option', class extends HTMLElement {
- constructor() {
- super()
- this.attachShadow({
- mode: 'open'
- }).append(smOption.content.cloneNode(true))
- }
-
- connectedCallback() {
- this.setAttribute('role', 'option')
- this.setAttribute('tabindex', '0')
- }
-})
-
-//popup
-class Stack {
- constructor() {
- this.items = [];
- }
- push(element) {
- this.items.push(element);
- }
- pop() {
- if (this.items.length == 0)
- return "Underflow";
- return this.items.pop();
- }
- peek() {
- return this.items[this.items.length - 1];
- }
-}
-const popupStack = new Stack();
-
-const smPopup = document.createElement('template');
-smPopup.innerHTML = `
-
-
-`;
-customElements.define('sm-popup', class extends HTMLElement {
- constructor() {
- super();
- this.attachShadow({
- mode: 'open'
- }).append(smPopup.content.cloneNode(true));
-
- this.allowClosing = false;
- this.isOpen = false;
- this.pinned = false;
- this.offset = 0;
- this.touchStartY = 0;
- this.touchEndY = 0;
- this.touchStartTime = 0;
- this.touchEndTime = 0;
- this.touchEndAnimation = undefined;
- this.focusable
- this.autoFocus
- this.mutationObserver
-
- this.popupContainer = this.shadowRoot.querySelector('.popup-container');
- this.backdrop = this.shadowRoot.querySelector('.background');
- 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);
- }
-
- static get observedAttributes() {
- return ['open'];
- }
-
- get open() {
- return this.isOpen;
- }
-
- animateTo(element, keyframes, options) {
- const anime = element.animate(keyframes, { ...options, fill: 'both' })
- anime.finished.then(() => {
- anime.commitStyles()
- anime.cancel()
- })
- return anime
- }
-
- resumeScrolling() {
- const scrollY = document.body.style.top;
- window.scrollTo(0, parseInt(scrollY || '0') * -1);
- document.body.style.overflow = '';
- document.body.style.top = 'initial';
- }
-
- setStateOpen() {
- if (!this.isOpen || this.offset) {
- const animOptions = {
- duration: 300,
- easing: 'ease'
- }
- const initialAnimation = (window.innerWidth > 640) ? 'scale(1.1)' : `translateY(${this.offset ? `${this.offset}px` : '100%'})`
- this.animateTo(this.dialogBox, [
- {
- opacity: this.offset ? 1 : 0,
- transform: initialAnimation
- },
- {
- opacity: 1,
- transform: 'none'
- },
- ], animOptions)
-
- }
- }
-
- show(options = {}) {
- const { pinned = false } = options;
- if (!this.isOpen) {
- const animOptions = {
- duration: 300,
- easing: 'ease'
- }
- popupStack.push({
- popup: this,
- permission: pinned
- });
- if (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)' },
- ], animOptions)
- }
- this.popupContainer.classList.remove('hide');
- if (!this.offset)
- this.backdrop.animate([
- { opacity: 0 },
- { opacity: 1 },
- ], animOptions)
- this.setStateOpen()
- this.dispatchEvent(
- new CustomEvent("popupopened", {
- bubbles: true,
- detail: {
- popup: this,
- }
- })
- );
- this.pinned = pinned;
- this.isOpen = true;
- document.body.style.overflow = 'hidden';
- document.body.style.top = `-${window.scrollY}px`;
- const elementToFocus = this.autoFocus || this.focusable[0];
- elementToFocus.tagName.includes('SM-') ? elementToFocus.focusIn() : elementToFocus.focus();
- if (!this.hasAttribute('open'))
- this.setAttribute('open', '');
- }
- }
- hide() {
- const animOptions = {
- duration: 150,
- easing: 'ease'
- }
- this.backdrop.animate([
- { opacity: 1 },
- { opacity: 0 }
- ], animOptions)
- 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%)'
- },
- ], animOptions).finished
- .finally(() => {
- this.popupContainer.classList.add('hide');
- this.dialogBox.style = ''
- this.removeAttribute('open');
-
- if (this.forms.length) {
- this.forms.forEach(form => form.reset());
- }
- this.dispatchEvent(
- new CustomEvent("popupclosed", {
- bubbles: true,
- detail: {
- popup: this,
- }
- })
- );
- this.isOpen = false;
- })
- popupStack.pop();
- if (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' },
- ], animOptions)
-
- } else {
- this.resumeScrolling();
- }
- }
-
- handleTouchStart(e) {
- this.offset = 0
- this.popupHeader.addEventListener('touchmove', this.handleTouchMove, { passive: true });
- this.popupHeader.addEventListener('touchend', this.handleTouchEnd, { passive: true });
- this.touchStartY = e.changedTouches[0].clientY;
- this.touchStartTime = e.timeStamp;
- }
-
- handleTouchMove(e) {
- if (this.touchStartY < e.changedTouches[0].clientY) {
- this.offset = e.changedTouches[0].clientY - this.touchStartY;
- this.touchEndAnimation = window.requestAnimationFrame(() => {
- this.dialogBox.style.transform = `translateY(${this.offset}px)`;
- });
- }
- }
-
- handleTouchEnd(e) {
- this.touchEndTime = e.timeStamp;
- cancelAnimationFrame(this.touchEndAnimation);
- this.touchEndY = e.changedTouches[0].clientY;
- this.threshold = this.dialogBox.getBoundingClientRect().height * 0.3;
- if (this.touchEndTime - this.touchStartTime > 200) {
- if (this.touchEndY - this.touchStartY > this.threshold) {
- if (this.pinned) {
- this.setStateOpen();
- return;
- } else
- this.hide();
- } else {
- this.setStateOpen();
- }
- } else {
- if (this.touchEndY > this.touchStartY)
- if (this.pinned) {
- this.setStateOpen();
- return;
- }
- else
- this.hide();
- }
- this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, { passive: true });
- this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, { passive: true });
- }
-
-
- detectFocus(e) {
- if (e.key === 'Tab') {
- const lastElement = this.focusable[this.focusable.length - 1];
- const firstElement = this.focusable[0];
- if (e.shiftKey && document.activeElement === firstElement) {
- e.preventDefault();
- lastElement.tagName.includes('SM-') ? lastElement.focusIn() : lastElement.focus();
- } else if (!e.shiftKey && document.activeElement === lastElement) {
- e.preventDefault();
- firstElement.tagName.includes('SM-') ? firstElement.focusIn() : firstElement.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]')
- }
-
- connectedCallback() {
- this.popupBodySlot.addEventListener('slotchange', () => {
- this.forms = this.querySelectorAll('sm-form');
- this.updateFocusableList()
- });
- this.popupContainer.addEventListener('mousedown', e => {
- if (e.target === this.popupContainer && !this.pinned) {
- if (this.pinned) {
- this.setStateOpen();
- } else
- this.hide();
- }
- });
-
- const resizeObserver = new ResizeObserver(entries => {
- for (let entry of entries) {
- if (entry.contentBoxSize) {
- // Firefox implements `contentBoxSize` as a single content rect, rather than an array
- const contentBoxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize;
- this.threshold = contentBoxSize.blockSize.height * 0.3;
- } else {
- this.threshold = entry.contentRect.height * 0.3;
- }
- }
- });
- resizeObserver.observe(this);
-
- this.mutationObserver = new MutationObserver(entries => {
- this.updateFocusableList()
- })
- this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true })
-
- this.addEventListener('keydown', this.detectFocus);
- this.popupHeader.addEventListener('touchstart', this.handleTouchStart, { passive: true });
- }
- disconnectedCallback() {
- this.removeEventListener('keydown', this.detectFocus);
- resizeObserver.unobserve();
- this.mutationObserver.disconnect()
- this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
- }
- attributeChangedCallback(name) {
- if (name === 'open') {
- if (this.hasAttribute('open')) {
- this.show();
- }
- }
- }
-});
-
-//notifications
-
-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(true))
-
- 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.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 = false
- }
-
- randString(length) {
- let result = '';
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- for (let i = 0; i < length; i++)
- result += characters.charAt(Math.floor(Math.random() * characters.length));
- return result;
- }
-
- createNotification(message, options = {}) {
- const { pinned = false, icon = '', action } = options;
- const notification = document.createElement('output')
- notification.id = this.randString(8)
- notification.classList.add('notification');
- let composition = ``;
- composition += `
-
${icon}
-
${message}
- `;
- if (action) {
- composition += `
-
- `
- }
- if (pinned) {
- notification.classList.add('pinned');
- composition += `
-
- `;
- }
- notification.innerHTML = composition;
- return notification;
- }
-
- push(message, options = {}) {
- const notification = this.createNotification(message, options);
- if (this.isLandscape)
- this.notificationPanel.append(notification);
- else
- this.notificationPanel.prepend(notification);
- this.notificationPanel.animate(
- [
- {
- transform: `translateY(${this.isLandscape ? '' : '-'}${notification.clientHeight}px)`,
- },
- {
- transform: `none`,
- },
- ], this.animationOptions
- )
- notification.animate([
- {
- transform: `translateY(-1rem)`,
- opacity: '0'
- },
- {
- transform: `none`,
- opacity: '1'
- },
- ], this.animationOptions).onfinish = (e) => {
- e.target.commitStyles()
- e.target.cancel()
- }
- if (notification.querySelector('.action'))
- notification.querySelector('.action').addEventListener('click', options.action.callback)
- return notification.id;
- }
-
- removeNotification(notification, direction = 'left') {
- const sign = direction === 'left' ? '-' : '+';
- notification.animate([
- {
- transform: this.currentX ? `translateX(${this.currentX}px)` : `none`,
- opacity: '1'
- },
- {
- transform: `translateX(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`,
- opacity: '0'
- }
- ], this.animationOptions).onfinish = () => {
- notification.remove();
- };
- }
-
- clearAll() {
- Array.from(this.notificationPanel.children).forEach(child => {
- this.removeNotification(child);
- });
- }
-
- handlePointerMove(e) {
- this.currentX = e.clientX - this.startX;
- this.currentTarget.style.transform = `translateX(${this.currentX}px)`;
- }
-
- handleOrientationChange(e) {
- this.isLandscape = e.matches
- if (e.matches) {
- // landscape
-
- } else {
- // portrait
- }
- }
- connectedCallback() {
-
- this.handleOrientationChange(this.mediaQuery);
-
- this.mediaQuery.addEventListener('change', this.handleOrientationChange);
- this.notificationPanel.addEventListener('pointerdown', e => {
- if (e.target.closest('.notification')) {
- this.swipeThreshold = this.clientWidth / 2;
- this.currentTarget = e.target.closest('.notification');
- this.currentTarget.setPointerCapture(e.pointerId);
- this.startTime = Date.now();
- this.startX = e.clientX;
- this.startY = e.clientY;
- this.notificationPanel.addEventListener('pointermove', this.handlePointerMove);
- }
- });
- this.notificationPanel.addEventListener('pointerup', e => {
- this.endX = e.clientX;
- this.endY = e.clientY;
- this.swipeDistance = Math.abs(this.endX - this.startX);
- this.swipeTime = Date.now() - this.startTime;
- if (this.endX > this.startX) {
- this.swipeDirection = 'right';
- } else {
- this.swipeDirection = 'left';
- }
- if (this.swipeTime < this.swipeTimeThreshold) {
- if (this.swipeDistance > 50)
- this.removeNotification(this.currentTarget, this.swipeDirection);
- } else {
- if (this.swipeDistance > this.swipeThreshold) {
- this.removeNotification(this.currentTarget, this.swipeDirection);
- } else {
- this.currentTarget.animate([
- {
- transform: `translateX(${this.currentX}px)`,
- },
- {
- transform: `none`,
- },
- ], this.animationOptions).onfinish = (e) => {
- e.target.commitStyles()
- e.target.cancel()
- }
- }
- }
- this.notificationPanel.removeEventListener('pointermove', this.handlePointerMove)
- this.notificationPanel.releasePointerCapture(e.pointerId);
- this.currentX = 0;
- });
- this.notificationPanel.addEventListener('click', e => {
- if (e.target.closest('.close')) {
- this.removeNotification(e.target.closest('.notification'));
- }
- });
-
- const observer = new MutationObserver(mutationList => {
- mutationList.forEach(mutation => {
- if (mutation.type === 'childList') {
- if (mutation.addedNodes.length && !mutation.addedNodes[0].classList.contains('pinned')) {
- setTimeout(() => {
- this.removeNotification(mutation.addedNodes[0]);
- }, 5000);
- }
- }
- });
- });
- observer.observe(this.notificationPanel, {
- childList: true,
- });
- }
- disconnectedCallback() {
- mediaQueryList.removeEventListener('change', handleOrientationChange);
- }
-});
-
-// sm-menu
-const smMenu = document.createElement('template')
-smMenu.innerHTML = `
-
-
`;
-customElements.define('sm-menu', class extends HTMLElement {
- constructor() {
- super()
- this.attachShadow({
- mode: 'open'
- }).append(smMenu.content.cloneNode(true))
- this.availableOptions
- this.containerDimensions
- this.optionList = this.shadowRoot.querySelector('.options')
- this.menu = this.shadowRoot.querySelector('.menu')
- this.open = false;
- }
- static get observedAttributes() {
- return ['value']
- }
- get value() {
- return this.getAttribute('value')
- }
- set value(val) {
- this.setAttribute('value', val)
- }
- expand = () => {
- if (!this.open) {
- this.optionList.classList.remove('hide')
- this.optionList.classList.add('no-transformations')
- this.menu.classList.add('focused')
- this.open = true
- this.availableOptions.forEach(option => {
- option.setAttribute('tabindex', '0')
- })
- }
- }
- collapse() {
- if (this.open) {
- this.open = false
- this.optionList.classList.add('hide')
- this.optionList.classList.remove('no-transformations')
- this.menu.classList.remove('focused')
- this.availableOptions.forEach(option => {
- option.removeAttribute('tabindex')
- })
- }
- }
- connectedCallback() {
- let slot = this.shadowRoot.querySelector('.options slot')
- this.menu.addEventListener('click', e => {
- if (!this.open) {
- this.expand()
- } else {
- this.collapse()
- }
- })
- this.menu.addEventListener('keydown', e => {
- if (e.code === 'ArrowDown' || e.code === 'ArrowRight') {
- e.preventDefault()
- this.availableOptions[0].focus()
- }
- if (e.code === 'Enter' || e.code === 'Space') {
- e.preventDefault()
- if (!this.open) {
- this.expand()
- } else {
- this.collapse()
- }
- }
- })
- this.optionList.addEventListener('keydown', e => {
- if (e.code === 'ArrowUp' || e.code === 'ArrowRight') {
- e.preventDefault()
- if (document.activeElement.previousElementSibling) {
- document.activeElement.previousElementSibling.focus()
- }
- }
- if (e.code === 'ArrowDown' || e.code === 'ArrowLeft') {
- e.preventDefault()
- if (document.activeElement.nextElementSibling)
- document.activeElement.nextElementSibling.focus()
- }
- })
- this.optionList.addEventListener('click', e => {
- this.collapse()
- })
- slot.addEventListener('slotchange', e => {
- this.availableOptions = slot.assignedElements()
- this.containerDimensions = this.optionList.getBoundingClientRect()
- });
- window.addEventListener('mousedown', e => {
- if (!this.contains(e.target) && e.button !== 2) {
- this.collapse()
- }
- })
- }
-})
-
-// option
-const smMenuOption = document.createElement('template')
-smMenuOption.innerHTML = `
-
-
-
-
`;
-customElements.define('sm-menu-option', class extends HTMLElement {
- constructor() {
- super()
- this.attachShadow({
- mode: 'open'
- }).append(smMenuOption.content.cloneNode(true))
- }
-
- connectedCallback() {
- this.addEventListener('keyup', e => {
- if (e.code === 'Enter' || e.code === 'Space') {
- e.preventDefault()
- this.click()
- }
- })
- }
-})
-
-const textField = document.createElement('template')
-textField.innerHTML = `
-
-
-
-
-
-
-
-
-`
-
-customElements.define('text-field', class extends HTMLElement {
- constructor() {
- super()
- this.attachShadow({
- mode: 'open'
- }).append(textField.content.cloneNode(true))
-
- this.textField = this.shadowRoot.querySelector('.text-field')
- this.textContainer = this.textField.children[0]
- this.iconsContainer = this.textField.children[1]
- this.editButton = this.textField.querySelector('.edit-button')
- this.saveButton = this.textField.querySelector('.save-button')
- this.isTextEditable = false
- this.isDisabled = false
- }
-
- static get observedAttributes() {
- return ['disable']
- }
-
- get value() {
- return this.text
- }
- set value(val) {
- this.text = val
- this.textContainer.textContent = val
- this.setAttribute('value', val)
- }
- set disabled(val) {
- this.isDisabled = val
- if (this.isDisabled)
- this.setAttribute('disable', '')
- else
- this.removeAttribute('disable')
- }
- fireEvent = (value) => {
- let event = new CustomEvent('contentchanged', {
- bubbles: true,
- cancelable: true,
- composed: true,
- detail: {
- value
- }
- });
- this.dispatchEvent(event);
- }
-
- setEditable = () => {
- if (this.isTextEditable) return
- this.textContainer.contentEditable = true
- this.textContainer.classList.add('editable')
- this.textContainer.focus()
- document.execCommand('selectAll', false, null);
- this.editButton.animate(this.rotateOut, this.animOptions).onfinish = () => {
- this.editButton.classList.add('hide')
- }
- setTimeout(() => {
- this.saveButton.classList.remove('hide')
- this.saveButton.animate(this.rotateIn, this.animOptions)
- }, 100);
- this.isTextEditable = true
- }
- setNonEditable = () => {
- if (!this.isTextEditable) return
- this.textContainer.contentEditable = false
- this.textContainer.classList.remove('editable')
-
- if (this.text !== this.textContainer.textContent.trim()) {
- this.setAttribute('value', this.textContainer.textContent)
- this.text = this.textContainer.textContent.trim()
- this.fireEvent(this.text)
- }
- this.saveButton.animate(this.rotateOut, this.animOptions).onfinish = () => {
- this.saveButton.classList.add('hide')
- }
- setTimeout(() => {
- this.editButton.classList.remove('hide')
- this.editButton.animate(this.rotateIn, this.animOptions)
- }, 100);
- this.isTextEditable = false
- }
-
- revert = () => {
- if (this.textContainer.isContentEditable) {
- this.value = this.text
- this.setNonEditable()
- }
- }
-
- connectedCallback() {
- this.text
- if (this.hasAttribute('value')) {
- this.text = this.getAttribute('value')
- this.textContainer.textContent = this.text
- }
- if (this.hasAttribute('disable'))
- this.isDisabled = true
- else
- this.isDisabled = false
-
- this.rotateOut = [
- {
- transform: 'rotate(0)',
- opacity: 1
- },
- {
- transform: 'rotate(90deg)',
- opacity: 0
- },
- ]
- this.rotateIn = [
- {
- transform: 'rotate(-90deg)',
- opacity: 0
- },
- {
- transform: 'rotate(0)',
- opacity: 1
- },
- ]
- this.animOptions = {
- duration: 300,
- easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
- fill: 'forwards'
- }
- if (!this.isDisabled) {
- this.iconsContainer.classList.remove('hide')
- this.textContainer.addEventListener('dblclick', this.setEditable)
- this.editButton.addEventListener('click', this.setEditable)
- this.saveButton.addEventListener('click', this.setNonEditable)
- }
- }
- attributeChangedCallback(name) {
- if (name === 'disable') {
- if (this.hasAttribute('disable')) {
- this.iconsContainer.classList.add('hide')
- this.textContainer.removeEventListener('dblclick', this.setEditable)
- this.editButton.removeEventListener('click', this.setEditable)
- this.saveButton.removeEventListener('click', this.setNonEditable)
- this.revert()
- }
- else {
- this.iconsContainer.classList.remove('hide')
- this.textContainer.addEventListener('dblclick', this.setEditable)
- this.editButton.addEventListener('click', this.setEditable)
- this.saveButton.addEventListener('click', this.setNonEditable)
- }
- }
- }
- disconnectedCallback() {
- this.textContainer.removeEventListener('dblclick', this.setEditable)
- this.editButton.removeEventListener('click', this.setEditable)
- this.saveButton.removeEventListener('click', this.setNonEditable)
- }
-})
-
+const smButton = document.createElement("template"); smButton.innerHTML = "\n\n
\n \n
", 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 smCopy = document.createElement("template"); smCopy.innerHTML = '\n\n
\n', 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(n) { this.setAttribute("value", n) } get value() { return this.getAttribute("value") } fireEvent() { this.dispatchEvent(new CustomEvent("copy", { composed: !0, bubbles: !0, cancelable: !0 })) } copy() { navigator.clipboard.writeText(this.copyContent.textContent).then(n => this.fireEvent()).catch(n => console.error(n)) } connectedCallback() { this.copyButton.addEventListener("click", this.copy) } attributeChangedCallback(n, t, o) { "value" === n && (this.copyContent.textContent = o) } disconnectedCallback() { this.copyButton.removeEventListener("click", this.copy) } });
+const smForm = document.createElement("template"); smForm.innerHTML = '\n \n
\n ', 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.formElements, this.requiredElements, this.submitButton, this.resetButton, this.invalidFields = !1, this.mutationObserver, 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) } debounce(t, e) { let i = null; return (...s) => { window.clearTimeout(i), i = window.setTimeout(() => { t.apply(null, s) }, e) } } _checkValidity() { this.submitButton && (this.invalidFields = this.requiredElements.filter(t => !t.isValid), this.submitButton.disabled = this.invalidFields.length) } handleKeydown(t) { "Enter" === t.key && t.target.tagName.includes("SM-INPUT") && (this.invalidFields.length ? this.requiredElements.forEach(t => { t.isValid || t.vibrate() }) : (this.submitButton && this.submitButton.click(), this.dispatchEvent(new CustomEvent("submit", { bubbles: !0, composed: !0 })))) } reset() { this.formElements.forEach(t => t.reset()) } elementsChanged() { this.formElements = [...this.querySelectorAll("sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio")], this.requiredElements = this.formElements.filter(t => t.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() { this.shadowRoot.querySelector("slot").addEventListener("slotchange", this.elementsChanged), this.addEventListener("input", this.debounce(this._checkValidity, 100)), this.addEventListener("keydown", this.debounce(this.handleKeydown, 100)), this.mutationObserver = new MutationObserver(t => { t.forEach(t => { "childList" === t.type && this.elementsChanged() }) }), this.mutationObserver.observe(this, { childList: !0, subtree: !0 }) } 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 = '\n \n
\n ', 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._helperText = "", this._errorText = "", this.isRequired = !1, 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.handleKeydown = this.handleKeydown.bind(this), this.vibrate = this.vibrate.bind(this) } static get observedAttributes() { return ["value", "placeholder", "required", "disabled", "type", "inputmode", "readonly", "min", "max", "pattern", "minlength", "maxlength", "step", "helper-text", "error-text"] } 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.inputParent.classList.remove("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 } 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.classList.remove("error"), this.feedbackText.classList.add("success"), this.feedbackText.textContent = "") : this._errorText && (this.feedbackText.classList.add("error"), this.feedbackText.classList.remove("success"), this.feedbackText.innerHTML = `\n
\n ${this._errorText}\n `), 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) } checkInput(t) { this.hasAttribute("readonly") || (this.clearBtn.style.visibility = "" !== this.input.value ? "visible" : "hidden"), this.hasAttribute("placeholder") && "" !== this.getAttribute("placeholder").trim() && ("" !== this.input.value ? this.animate ? this.inputParent.classList.add("animate-placeholder") : this.label.classList.add("hide") : (this.animate ? this.inputParent.classList.remove("animate-placeholder") : this.label.classList.remove("hide"), this.feedbackText.textContent = "")) } handleKeydown(t) { 1 === t.key.length && (["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."].includes(t.key) ? "." === t.key && t.target.value.includes(".") && t.preventDefault() : t.preventDefault()) } vibrate() { this.outerContainer.animate([{ transform: "translateX(-1rem)" }, { transform: "translateX(1rem)" }, { transform: "translateX(-0.5rem)" }, { transform: "translateX(0.5rem)" }, { transform: "translateX(0)" }], { duration: 300, easing: "ease" }) } connectedCallback() { this.animate = this.hasAttribute("animate"), this.setAttribute("role", "textbox"), this.input.addEventListener("input", this.checkInput), this.clearBtn.addEventListener("click", this.clear) } attributeChangedCallback(t, e, n) { e !== n && (this.reflectedAttributes.includes(t) && (this.hasAttribute(t) ? this.input.setAttribute(t, this.getAttribute(t) ? this.getAttribute(t) : "") : this.input.removeAttribute(t)), "placeholder" === t ? (this.label.textContent = n, this.setAttribute("aria-label", n)) : this.hasAttribute("value") ? this.checkInput() : "type" === t ? this.hasAttribute("type") && "number" === this.getAttribute("type") ? (this.input.setAttribute("inputmode", "decimal"), this.input.addEventListener("keydown", this.handleKeydown)) : this.input.removeEventListener("keydown", this.handleKeydown) : "helper-text" === t ? this._helperText = this.getAttribute("helper-text") : "error-text" === t ? this._errorText = this.getAttribute("error-text") : "required" === t ? (this.isRequired = this.hasAttribute("required"), this.isRequired ? this.setAttribute("aria-required", "true") : this.setAttribute("aria-required", "false")) : "readonly" === t ? this.hasAttribute("readonly") ? this.inputParent.classList.add("readonly") : this.inputParent.classList.remove("readonly") : "disabled" === t && (this.hasAttribute("disabled") ? this.inputParent.classList.add("disabled") : this.inputParent.classList.remove("disabled"))) } disconnectedCallback() { this.input.removeEventListener("input", this.checkInput), this.clearBtn.removeEventListener("click", this.clear), this.input.removeEventListener("keydown", this.handleKeydown) } });
+const smMenu = document.createElement("template"); smMenu.innerHTML = '\n\n
', customElements.define("sm-menu", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smMenu.content.cloneNode(!0)), this.isOpen = !1, this.availableOptions, this.containerDimensions, this.animOptions = { duration: 200, easing: "ease" }, this.optionList = this.shadowRoot.querySelector(".options"), this.menu = this.shadowRoot.querySelector(".menu"), this.icon = this.shadowRoot.querySelector(".icon"), this.expand = this.expand.bind(this), this.collapse = this.collapse.bind(this), this.toggle = this.toggle.bind(this), this.handleKeyDown = this.handleKeyDown.bind(this), this.handleClickOutside = this.handleClickOutside.bind(this) } static get observedAttributes() { return ["value"] } get value() { return this.getAttribute("value") } set value(n) { this.setAttribute("value", n) } expand() { this.isOpen || (this.optionList.classList.remove("hide"), this.optionList.animate([{ transform: window.innerWidth < 640 ? "translateY(1.5rem)" : "translateY(-1rem)", opacity: "0" }, { transform: "none", opacity: "1" }], this.animOptions).onfinish = (() => { this.isOpen = !0, this.icon.classList.add("focused") })) } collapse() { this.isOpen && (this.optionList.animate([{ transform: "none", opacity: "1" }, { transform: window.innerWidth < 640 ? "translateY(1.5rem)" : "translateY(-1rem)", opacity: "0" }], this.animOptions).onfinish = (() => { this.isOpen = !1, this.icon.classList.remove("focused"), this.optionList.classList.add("hide") })) } toggle() { this.isOpen ? this.collapse() : this.expand() } handleKeyDown(n) { n.target === this ? "ArrowDown" === n.key ? (n.preventDefault(), this.availableOptions[0].focus()) : "Enter" !== n.key && " " !== n.key || (n.preventDefault(), this.toggle()) : "ArrowUp" === n.key ? (n.preventDefault(), document.activeElement.previousElementSibling ? document.activeElement.previousElementSibling.focus() : this.availableOptions[this.availableOptions.length - 1].focus()) : "ArrowDown" === n.key ? (n.preventDefault(), document.activeElement.nextElementSibling ? document.activeElement.nextElementSibling.focus() : this.availableOptions[0].focus()) : "Enter" !== n.key && " " !== n.key || (n.preventDefault(), n.target.click()) } handleClickOutside(n) { this.contains(n.target) || 2 === n.button || this.collapse() } connectedCallback() { this.setAttribute("role", "listbox"), this.setAttribute("aria-label", "dropdown menu"); const n = this.shadowRoot.querySelector(".options slot"); n.addEventListener("slotchange", n => { this.availableOptions = n.target.assignedElements(), this.containerDimensions = this.optionList.getBoundingClientRect() }), this.addEventListener("click", this.toggle), this.addEventListener("keydown", this.handleKeyDown), document.addEventListener("mousedown", this.handleClickOutside) } disconnectedCallback() { this.removeEventListener("click", this.toggle), this.removeEventListener("keydown", this.handleKeyDown), document.removeEventListener("mousedown", this.handleClickOutside) } }); const menuOption = document.createElement("template"); menuOption.innerHTML = '\n\n
\n \n
', customElements.define("menu-option", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(menuOption.content.cloneNode(!0)) } connectedCallback() { this.setAttribute("role", "option"), this.setAttribute("tabindex", "0"), this.addEventListener("keyup", n => { "Enter" !== n.key && " " !== n.key || (n.preventDefault(), this.click()) }) } });
+const smNotifications = document.createElement("template"); smNotifications.innerHTML = '\n \n
\n ', 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.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 i = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (let e = 0; e < n; e++)t += i.charAt(Math.floor(Math.random() * i.length)); return t } createNotification(n, t = {}) { const { pinned: i = !1, icon: e = "", action: o } = t, a = document.createElement("div"); a.id = this.randString(8), a.classList.add("notification"); let r = ""; return r += `\n
${e}
\n
\n `, o && (r += `\n
\n `), i && (a.classList.add("pinned"), r += '\n
\n '), a.innerHTML = r, a } push(n, t = {}) { const i = this.createNotification(n, t); return this.isLandscape ? this.notificationPanel.append(i) : this.notificationPanel.prepend(i), this.notificationPanel.animate([{ transform: `translateY(${this.isLandscape ? "" : "-"}${i.clientHeight}px)` }, { transform: "none" }], this.animationOptions), i.animate([{ transform: "translateY(-1rem)", opacity: "0" }, { transform: "none", opacity: "1" }], this.animationOptions).onfinish = (n => { n.target.commitStyles(), n.target.cancel() }), i.querySelector(".action") && i.querySelector(".action").addEventListener("click", t.action.callback), i.id } removeNotification(n, t = "left") { if (!n) return; const i = "left" === t ? "-" : "+"; n.animate([{ transform: this.currentX ? `translateX(${this.currentX}px)` : "none", opacity: "1" }, { transform: `translateX(calc(${i}${Math.abs(this.currentX)}px ${i} 1rem))`, opacity: "0" }], this.animationOptions).onfinish = (() => { n.remove() }) } 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 = '\n\n\n', 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.pinned = !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(".background"), 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) } static get observedAttributes() { return ["open"] } get open() { return this.isOpen } animateTo(t, e, n) { const i = t.animate(e, { ...n, fill: "both" }); return i.finished.then(() => { i.commitStyles(), i.cancel() }), i } resumeScrolling() { const 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) { const t = { duration: 300, easing: "ease" }, e = window.innerWidth > 640 ? "scale(1.1)" : `translateY(${this.offset ? `${this.offset}px` : "100%"})`; this.animateTo(this.dialogBox, [{ opacity: this.offset ? 1 : 0, transform: e }, { opacity: 1, transform: "none" }], t) } } show(t = {}) { const { pinned: e = !1 } = t; if (!this.isOpen) { const t = { duration: 300, easing: "ease" }; 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)" }], t), this.popupContainer.classList.remove("hide"), this.offset || this.backdrop.animate([{ opacity: 0 }, { opacity: 1 }], t), this.setStateOpen(), this.dispatchEvent(new CustomEvent("popupopened", { bubbles: !0, detail: { popup: this } })), this.pinned = e, this.isOpen = !0, document.body.style.overflow = "hidden", document.body.style.top = `-${window.scrollY}px`; const n = this.autoFocus || this.focusable[0]; n.tagName.includes("SM-") ? n.focusIn() : n.focus(), this.hasAttribute("open") || this.setAttribute("open", "") } } hide() { const t = { duration: 150, easing: "ease" }; this.backdrop.animate([{ opacity: 1 }, { opacity: 0 }], t), 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%)" }], t).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, detail: { popup: this } })), 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" }], t) : this.resumeScrolling() } 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) return void this.setStateOpen(); this.hide() } else this.setStateOpen(); else if (this.touchEndY > this.touchStartY) { if (this.pinned) return void this.setStateOpen(); this.hide() } this.popupHeader.removeEventListener("touchmove", this.handleTouchMove, { passive: !0 }), this.popupHeader.removeEventListener("touchend", this.handleTouchEnd, { passive: !0 }) } detectFocus(t) { if ("Tab" === t.key) { const e = this.focusable[this.focusable.length - 1], n = this.focusable[0]; t.shiftKey && document.activeElement === n ? (t.preventDefault(), e.tagName.includes("SM-") ? e.focusIn() : e.focus()) : t.shiftKey || document.activeElement !== e || (t.preventDefault(), n.tagName.includes("SM-") ? n.focusIn() : n.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]") } connectedCallback() { this.popupBodySlot.addEventListener("slotchange", () => { this.forms = this.querySelectorAll("sm-form"), this.updateFocusableList() }), this.popupContainer.addEventListener("mousedown", t => { t.target !== this.popupContainer || this.pinned || (this.pinned ? this.setStateOpen() : this.hide()) }); const t = new ResizeObserver(t => { for (let e of t) if (e.contentBoxSize) { const t = Array.isArray(e.contentBoxSize) ? e.contentBoxSize[0] : e.contentBoxSize; this.threshold = .3 * t.blockSize.height } else this.threshold = .3 * e.contentRect.height }); t.observe(this), this.mutationObserver = new MutationObserver(t => { this.updateFocusableList() }), this.mutationObserver.observe(this, { attributes: !0, childList: !0, subtree: !0 }), this.addEventListener("keydown", this.detectFocus), this.popupHeader.addEventListener("touchstart", this.handleTouchStart, { passive: !0 }) } disconnectedCallback() { this.removeEventListener("keydown", this.detectFocus), resizeObserver.unobserve(), this.mutationObserver.disconnect(), this.popupHeader.removeEventListener("touchstart", this.handleTouchStart, { passive: !0 }) } attributeChangedCallback(t) { "open" === t && this.hasAttribute("open") && this.show() } });
+const smSwitch = document.createElement("template"); smSwitch.innerHTML = '\t\n\n
', 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 = '\n\n
', 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.availableOptions = [], this.previousOption, this.isOpen = !1, this.label = "", this.slideDown = [{ transform: "translateY(-0.5rem)", opacity: 0 }, { transform: "translateY(0)", opacity: 1 }], this.slideUp = [{ transform: "translateY(0)", opacity: 1 }, { transform: "translateY(-0.5rem)", opacity: 0 }], 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"] } get value() { return this.getAttribute("value") } set value(t) { const e = this.shadowRoot.querySelector("slot").assignedElements().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("hide"), this.optionList.animate(this.slideDown, this.animationOptions), this.setAttribute("open", ""), (this.availableOptions.find(t => t.hasAttribute("selected")) || this.availableOptions[0]).focus(), this.isOpen = !0 } collapse() { this.removeAttribute("open"), this.optionList.animate(this.slideUp, this.animationOptions).onfinish = (() => { this.availableOptions.forEach(t => t.removeAttribute("tabindex")), this.optionList.classList.add("hide"), this.isOpen = !1 }) } 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() } connectedCallback() { this.setAttribute("role", "listbox"), this.hasAttribute("disabled") || this.selection.setAttribute("tabindex", "0"); let t = this.shadowRoot.querySelector("slot"); t.addEventListener("slotchange", this.debounce(e => { this.availableOptions = t.assignedElements(), this.reset(!1) }, 100)), 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), this.addEventListener("click", this.handleClick), this.addEventListener("keydown", this.handleKeydown), document.addEventListener("mousedown", this.handleClickOutside) } disconnectedCallback() { this.removeEventListener("click", this.handleClick), this.removeEventListener("click", this.toggle), this.removeEventListener("keydown", this.handleKeydown), document.removeEventListener("mousedown", this.handleClickOutside) } attributeChangedCallback(t) { "disabled" === t ? this.hasAttribute("disabled") ? this.selection.removeAttribute("tabindex") : this.selection.setAttribute("tabindex", "0") : "label" === t && (this.label = this.hasAttribute("label") ? `${this.getAttribute("label")} ` : "") } }); const smOption = document.createElement("template"); smOption.innerHTML = "\n\n
\n \n
", 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 = '\n\n
\n\n'; class SpinnerLoader extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(spinner.content.cloneNode(!0)) } } window.customElements.define("sm-spinner", SpinnerLoader);
+const stripSelect = document.createElement("template"); stripSelect.innerHTML = '\n\n
\n\n', customElements.define("strip-select", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(stripSelect.content.cloneNode(!0)), this.stripSelect = this.shadowRoot.querySelector(".strip-select"), 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.stripSelect.scrollBy({ left: -this.scrollDistance, behavior: "smooth" }) } scrollRight() { this.stripSelect.scrollBy({ left: this.scrollDistance, behavior: "smooth" }) } setSelectedOption(t) { this._value !== t && (this._value = t, this.assignedElements.forEach(e => { e.value === t ? (e.setAttribute("active", ""), e.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" })) : e.removeAttribute("active") })) } 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"), e = this.shadowRoot.querySelector(".cover--left"), n = this.shadowRoot.querySelector(".cover--right"), i = this.shadowRoot.querySelector(".nav-button--left"), s = this.shadowRoot.querySelector(".nav-button--right"); t.addEventListener("slotchange", o => { this.assignedElements = t.assignedElements(), this.assignedElements.forEach(t => { t.hasAttribute("selected") && (t.setAttribute("active", ""), this._value = t.value) }), this.hasAttribute("multiline") || (this.assignedElements.length > 0 ? (r.observe(this.assignedElements[0]), a.observe(this.assignedElements[this.assignedElements.length - 1])) : (i.classList.add("hide"), s.classList.add("hide"), e.classList.add("hide"), n.classList.add("hide"), r.disconnect(), a.disconnect())) }); const o = 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 }) }); o.observe(this), this.stripSelect.addEventListener("option-clicked", t => { this._value !== t.target.value && (this.setSelectedOption(t.target.value), this.fireEvent()) }); const r = new IntersectionObserver(t => { t.forEach(t => { t.isIntersecting ? (i.classList.add("hide"), e.classList.add("hide")) : (i.classList.remove("hide"), e.classList.remove("hide")) }) }, { threshold: .9, root: this }), a = new IntersectionObserver(t => { t.forEach(t => { t.isIntersecting ? (s.classList.add("hide"), n.classList.add("hide")) : (s.classList.remove("hide"), n.classList.remove("hide")) }) }, { threshold: .9, root: this }); i.addEventListener("click", this.scrollLeft), s.addEventListener("click", this.scrollRight) } disconnectedCallback() { navButtonLeft.removeEventListener("click", this.scrollLeft), navButtonRight.removeEventListener("click", this.scrollRight) } }); const stripOption = document.createElement("template"); stripOption.innerHTML = '\n\n
\n', customElements.define("strip-option", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(stripOption.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 smTextarea = document.createElement("template"); smTextarea.innerHTML = '\n \n
\n ', 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 textField = document.createElement("template"); textField.innerHTML = '\n\n
\n', customElements.define("text-field", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(textField.content.cloneNode(!0)), this.textField = this.shadowRoot.querySelector(".text-field"), this.textContainer = this.textField.children[0], this.iconsContainer = this.textField.children[1], this.editButton = this.textField.querySelector(".edit-button"), this.isTextEditable = !1, this.isDisabled = !1, this.fireEvent = this.fireEvent.bind(this), this.setEditable = this.setEditable.bind(this), this.setNonEditable = this.setNonEditable.bind(this), this.toggleEditable = this.toggleEditable.bind(this), this.revert = this.revert.bind(this) } static get observedAttributes() { return ["disabled", "value"] } get value() { return this.text } set value(t) { this.setAttribute("value", t) } set disabled(t) { this.isDisabled = t, this.isDisabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled") } fireEvent(t) { let e = new CustomEvent("change", { bubbles: !0, cancelable: !0, composed: !0, detail: { value: t } }); this.dispatchEvent(e) } setEditable() { this.isTextEditable || (this.textContainer.contentEditable = !0, this.textContainer.classList.add("editable"), this.textContainer.focus(), document.execCommand("selectAll", !1, null), this.editButton.children[0].animate(this.rotateOut, this.animOptions).onfinish = (() => { this.editButton.children[0].classList.add("hide") }), setTimeout(() => { this.editButton.children[1].classList.remove("hide"), this.editButton.children[1].animate(this.rotateIn, this.animOptions) }, 100), this.isTextEditable = !0) } setNonEditable() { if (!this.isTextEditable) return; this.textContainer.contentEditable = !1, this.textContainer.classList.remove("editable"); const t = this.textContainer.textContent.trim(); this.text !== t && "" !== t ? (this.setAttribute("value", this.textContainer.textContent), this.text = this.textContainer.textContent.trim(), this.fireEvent(this.text)) : this.value = this.text, this.editButton.children[1].animate(this.rotateOut, this.animOptions).onfinish = (() => { this.editButton.children[1].classList.add("hide") }), setTimeout(() => { this.editButton.children[0].classList.remove("hide"), this.editButton.children[0].animate(this.rotateIn, this.animOptions) }, 100), this.isTextEditable = !1 } toggleEditable() { this.isTextEditable ? this.setNonEditable() : this.setEditable() } revert() { this.textContainer.isContentEditable && (this.value = this.text, this.setNonEditable()) } connectedCallback() { this.text, this.hasAttribute("value") && (this.text = this.getAttribute("value"), this.textContainer.textContent = this.text), this.hasAttribute("disabled") ? this.isDisabled = !0 : this.isDisabled = !1, this.rotateOut = [{ transform: "rotate(0)", opacity: 1 }, { transform: "rotate(90deg)", opacity: 0 }], this.rotateIn = [{ transform: "rotate(-90deg)", opacity: 0 }, { transform: "rotate(0)", opacity: 1 }], this.animOptions = { duration: 300, easing: "cubic-bezier(0.175, 0.885, 0.32, 1.275)", fill: "forwards" }, this.isDisabled || (this.iconsContainer.classList.remove("hide"), this.textContainer.addEventListener("dblclick", this.setEditable), this.editButton.addEventListener("click", this.toggleEditable)) } attributeChangedCallback(t, e, i) { "disabled" === t ? this.hasAttribute("disabled") ? (this.textContainer.removeEventListener("dblclick", this.setEditable), this.editButton.removeEventListener("click", this.toggleEditable), this.revert()) : (this.textContainer.addEventListener("dblclick", this.setEditable), this.editButton.addEventListener("click", this.toggleEditable)) : "value" === t && (this.text = i, this.textContainer.textContent = i) } disconnectedCallback() { this.textContainer.removeEventListener("dblclick", this.setEditable), this.editButton.removeEventListener("click", this.toggleEditable) } });
+const themeToggle = document.createElement("template"); themeToggle.innerHTML = '\n \n
\n'; 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);
//Color Grid
const colorGrid = document.createElement('template');
colorGrid.innerHTML = `
@@ -3051,945 +162,4 @@ customElements.define('color-grid',
disconnectedCallback() {
this.container.removeEventListener('change', this.handleChange)
}
- })
-
-const pinInput = document.createElement('template');
-pinInput.innerHTML = `
-
-
-
-`;
-
-customElements.define('pin-input',
-
- class extends HTMLElement {
- constructor() {
- super()
- this.attachShadow({
- mode: 'open'
- }).append(pinInput.content.cloneNode(true))
-
- this.pinDigits = 4
-
- this.arrayOfInput = [];
- this.container = this.shadowRoot.querySelector('.pin-container');
- this.toggleButton = this.shadowRoot.querySelector('button')
- }
-
- set value(val) {
- this.arrayOfInput.forEach((input, index) => input.value = val[index] ? val[index] : '')
- }
-
- get value() {
- return this.getValue()
- }
-
- set pinLength(val) {
- this.pinDigits = val
- this.setAttribute('pin-length', val)
- this.style.setProperty('--pin-length', val)
- this.render()
- }
-
- get isValid() {
- return this.arrayOfInput.every(input => input.value.trim().length)
- }
-
- clear = () => {
- this.value = ''
- }
-
- focusIn = () => {
- this.arrayOfInput[0].focus();
- }
-
- getValue = () => {
- return this.arrayOfInput.reduce((acc, val) => {
- return acc += val.value
- }, '')
- }
-
- render = () => {
- this.container.innerHTML = ''
- const frag = document.createDocumentFragment();
-
- for (let i = 0; i < this.pinDigits; i++) {
- const inputBox = document.createElement('input')
- inputBox.setAttribute('type', 'password')
- inputBox.setAttribute('inputmode', 'numeric')
- inputBox.setAttribute('maxlength', '1')
- inputBox.setAttribute('required', '')
- this.arrayOfInput.push(inputBox);
- frag.append(inputBox);
- }
- this.container.append(frag);
- }
-
- handleKeydown = (e) => {
- const activeInput = e.target.closest('input')
- if (/[0-9]/.test(e.key)) {
- if (activeInput.value.trim().length > 2) {
- e.preventDefault();
- }
- else {
- if (activeInput.value.trim().length === 1) {
- activeInput.value = e.key
- }
- if (activeInput.nextElementSibling) {
- setTimeout(() => {
- activeInput.nextElementSibling.focus();
- }, 0)
- }
- }
- }
- else if (e.key === "Backspace") {
- if (activeInput.previousElementSibling)
- setTimeout(() => {
- activeInput.previousElementSibling.focus();
- }, 0)
- }
- else if (e.key.length === 1 && !/[0-9]/.test(e.key)) {
- e.preventDefault();
- }
- }
-
- handleInput = () => {
- if (this.isValid) {
- this.fireEvent(this.getValue())
- }
- }
-
- fireEvent = (value) => {
- let event = new CustomEvent('pincomplete', {
- bubbles: true,
- cancelable: true,
- composed: true,
- detail: {
- value
- }
- });
- this.dispatchEvent(event);
- }
-
- toggleVisiblity = () => {
- if (this.arrayOfInput[0].getAttribute('type') === 'password') {
- this.toggleButton.innerHTML = `
-
- Hide
- `
- this.arrayOfInput.forEach(input => input.setAttribute('type', 'text'))
- }
- else {
- this.toggleButton.innerHTML = `
-
- Show
- `
- this.arrayOfInput.forEach(input => input.setAttribute('type', 'password'))
-
- }
- }
-
- connectedCallback() {
- if (this.hasAttribute('pin-length')) {
- const pinLength = parseInt(this.getAttribute('pin-length'))
- this.pinDigits = pinLength
- this.style.setProperty('--pin-length', pinLength)
- }
-
- this.render()
-
- this.toggleButton.addEventListener('click', this.toggleVisiblity)
-
- this.container.addEventListener('input', this.handleInput);
- this.container.addEventListener('keydown', this.handleKeydown);
- }
- disconnectedCallback() {
- this.toggleButton.removeEventListener('click', this.toggleVisiblity)
-
- this.container.removeEventListener('input', this.handleInput);
- this.container.removeEventListener('keydown', this.handleKeydown);
- }
- })
-
-const themeToggle = document.createElement('template');
-themeToggle.innerHTML = `
-
-
-`;
-
-class ThemeToggle extends HTMLElement {
- constructor() {
- super();
-
- this.attachShadow({
- mode: 'open'
- }).append(themeToggle.content.cloneNode(true));
-
- this.isChecked = false;
- 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) {
- if (e.key === ' ') {
- this.toggleState();
- }
- }
- handleThemeChange(e) {
- if (e.detail.theme !== this.hasTheme) {
- if (e.detail.theme === 'dark') {
- this.setAttribute('checked', '');
- }
- else {
- this.removeAttribute('checked');
- }
- }
- }
-
- fireEvent() {
- this.dispatchEvent(
- new CustomEvent('themechange', {
- bubbles: true,
- composed: true,
- detail: {
- theme: this.hasTheme
- }
- })
- );
- }
-
- connectedCallback() {
- this.setAttribute('role', 'switch');
- this.setAttribute('aria-label', 'theme toggle');
- if (localStorage.getItem(`${window.location.hostname}-theme`) === "dark") {
- this.nightlight();
- this.setAttribute('checked', '');
- } else if (localStorage.getItem(`${window.location.hostname}-theme`) === "light") {
- this.daylight();
- this.removeAttribute('checked');
- }
- else {
- if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) {
- this.nightlight();
- this.setAttribute('checked', '');
- } else {
- 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(name, oldVal, newVal) {
- if (name === 'checked') {
- if (this.hasAttribute('checked')) {
- this.nightlight();
- localStorage.setItem(`${window.location.hostname}-theme`, "dark");
- } else {
- this.daylight();
- localStorage.setItem(`${window.location.hostname}-theme`, "light");
- }
- }
- }
-}
-
-window.customElements.define('theme-toggle', ThemeToggle);
-
-const smCopy = document.createElement('template');
-smCopy.innerHTML = `
-
-
-`;
-customElements.define('sm-copy',
- class extends HTMLElement {
- constructor() {
- super();
- this.attachShadow({
- mode: 'open'
- }).append(smCopy.content.cloneNode(true));
-
- 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(val) {
- this.setAttribute('value', val);
- }
- get value() {
- return this.getAttribute('value');
- }
- fireEvent() {
- this.dispatchEvent(
- new CustomEvent('copy', {
- composed: true,
- bubbles: true,
- cancelable: true,
- })
- );
- }
- copy() {
- navigator.clipboard.writeText(this.copyContent.textContent)
- .then(res => this.fireEvent())
- .catch(err => console.error(err));
- }
- connectedCallback() {
- this.copyButton.addEventListener('click', this.copy);
- }
- attributeChangedCallback(name, oldValue, newValue) {
- if (name === 'value') {
- this.copyContent.textContent = newValue;
- }
- }
- disconnectedCallback() {
- this.copyButton.removeEventListener('click', this.copy);
- }
- });
-const spinner = document.createElement('template');
-spinner.innerHTML = `
-
-
-
-`;
-class SpinnerLoader extends HTMLElement {
- constructor() {
- super();
- this.attachShadow({
- mode: 'open'
- }).append(spinner.content.cloneNode(true));
- }
-}
-window.customElements.define('sm-spinner', SpinnerLoader);
-
-const stripSelect = document.createElement('template');
-stripSelect.innerHTML = `
-
-
-
-`;
-customElements.define('strip-select', class extends HTMLElement {
- constructor() {
- super();
- this.attachShadow({
- mode: 'open'
- }).append(stripSelect.content.cloneNode(true));
- this.stripSelect = this.shadowRoot.querySelector('.strip-select');
- this.slottedOptions = undefined;
- this._value = undefined;
- 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(val) {
- this.setSelectedOption(val);
- }
- scrollLeft() {
- this.stripSelect.scrollBy({
- left: -this.scrollDistance,
- behavior: 'smooth'
- });
- }
-
- scrollRight() {
- this.stripSelect.scrollBy({
- left: this.scrollDistance,
- behavior: 'smooth'
- });
- }
- setSelectedOption(value) {
- if (this._value === value) return
- this._value = value;
- this.assignedElements.forEach(elem => {
- if (elem.value === value) {
- elem.setAttribute('active', '');
- elem.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
- }
- else
- elem.removeAttribute('active')
- });
- }
-
- fireEvent() {
- this.dispatchEvent(
- new CustomEvent("change", {
- bubbles: true,
- composed: true,
- detail: {
- value: this._value
- }
- })
- );
- }
- connectedCallback() {
- this.setAttribute('role', 'listbox');
-
- const slot = this.shadowRoot.querySelector('slot');
- const coverLeft = this.shadowRoot.querySelector('.cover--left');
- const coverRight = this.shadowRoot.querySelector('.cover--right');
- const navButtonLeft = this.shadowRoot.querySelector('.nav-button--left');
- const navButtonRight = this.shadowRoot.querySelector('.nav-button--right');
- slot.addEventListener('slotchange', e => {
- this.assignedElements = slot.assignedElements();
- this.assignedElements.forEach(elem => {
- if (elem.hasAttribute('selected')) {
- elem.setAttribute('active', '');
- this._value = elem.value;
- }
- });
- if (!this.hasAttribute('multiline')) {
- if (this.assignedElements.length > 0) {
- firstOptionObserver.observe(this.assignedElements[0]);
- lastOptionObserver.observe(this.assignedElements[this.assignedElements.length - 1]);
- }
- else {
- navButtonLeft.classList.add('hide');
- navButtonRight.classList.add('hide');
- coverLeft.classList.add('hide');
- coverRight.classList.add('hide');
- firstOptionObserver.disconnect();
- lastOptionObserver.disconnect();
- }
- }
- });
- const resObs = new ResizeObserver(entries => {
- entries.forEach(entry => {
- if (entry.contentBoxSize) {
- // Firefox implements `contentBoxSize` as a single content rect, rather than an array
- const contentBoxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize;
-
- this.scrollDistance = contentBoxSize.inlineSize * 0.6;
- } else {
- this.scrollDistance = entry.contentRect.width * 0.6;
- }
- });
- });
- resObs.observe(this);
- this.stripSelect.addEventListener('option-clicked', e => {
- if (this._value !== e.target.value) {
- this.setSelectedOption(e.target.value);
- this.fireEvent();
- }
- });
- const firstOptionObserver = new IntersectionObserver(entries => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- navButtonLeft.classList.add('hide');
- coverLeft.classList.add('hide');
- }
- else {
- navButtonLeft.classList.remove('hide');
- coverLeft.classList.remove('hide');
- }
- });
- },
- {
- threshold: 0.9,
- root: this
- });
- const lastOptionObserver = new IntersectionObserver(entries => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- navButtonRight.classList.add('hide');
- coverRight.classList.add('hide');
- }
- else {
- navButtonRight.classList.remove('hide');
- coverRight.classList.remove('hide');
- }
- });
- },
- {
- threshold: 0.9,
- root: this
- });
- navButtonLeft.addEventListener('click', this.scrollLeft);
- navButtonRight.addEventListener('click', this.scrollRight);
- }
- disconnectedCallback() {
- navButtonLeft.removeEventListener('click', this.scrollLeft);
- navButtonRight.removeEventListener('click', this.scrollRight);
- }
-});
-
-//Strip option
-const stripOption = document.createElement('template');
-stripOption.innerHTML = `
-
-
-`;
-customElements.define('strip-option', class extends HTMLElement {
- constructor() {
- super();
- this.attachShadow({
- mode: 'open'
- }).append(stripOption.content.cloneNode(true));
- this._value = undefined;
- 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: true,
- composed: true,
- detail: {
- value: this._value
- }
- })
- );
- }
- handleKeyDown(e) {
- if (e.key === 'Enter' || e.key === 'Space') {
- 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);
- }
-});
\ No newline at end of file
+ })
\ No newline at end of file
diff --git a/scripts/floBlockchainAPI.js b/scripts/floBlockchainAPI.js
index fcc73b9..d250f86 100644
--- a/scripts/floBlockchainAPI.js
+++ b/scripts/floBlockchainAPI.js
@@ -1,4 +1,4 @@
-(function(EXPORTS) { //floBlockchainAPI v2.3.3
+(function(EXPORTS) { //floBlockchainAPI v2.3.3b
/* FLO Blockchain Operator to send/receive data from blockchain using API calls*/
'use strict';
const floBlockchainAPI = EXPORTS;
@@ -6,7 +6,7 @@
const DEFAULT = {
blockchain: floGlobals.blockchain,
apiURL: {
- FLO: ['https://livenet.flocha.in/', 'https://flosight.duckdns.org/'],
+ FLO: ['https://flosight.duckdns.org/'],
FLO_TEST: ['https://testnet-flosight.duckdns.org', 'https://testnet.flocha.in/']
},
sendAmt: 0.001,
@@ -49,7 +49,7 @@
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 - 1);
+ var curPos = floCrypto.randInt(0, serverList.length - 1);
function fetch_retry(apicall, rm_flosight) {
return new Promise((resolve, reject) => {
@@ -125,9 +125,9 @@
return new Promise((resolve, reject) => {
if (!floCrypto.validateASCII(floData))
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
- else if (!floCrypto.validateAddr(senderAddr))
+ else if (!floCrypto.validateFloID(senderAddr))
return reject(`Invalid address : ${senderAddr}`);
- else if (!floCrypto.validateAddr(receiverAddr))
+ else if (!floCrypto.validateFloID(receiverAddr))
return reject(`Invalid address : ${receiverAddr}`);
else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr))
return reject("Invalid Private key!");
@@ -202,7 +202,7 @@
//merge all UTXOs of a given floID into a single UTXO
floBlockchainAPI.mergeUTXOs = function(floID, privKey, floData = '') {
return new Promise((resolve, reject) => {
- if (!floCrypto.validateAddr(floID))
+ if (!floCrypto.validateFloID(floID))
return reject(`Invalid floID`);
if (!floCrypto.verifyPrivKey(privKey, floID))
return reject("Invalid Private Key");
@@ -326,7 +326,7 @@
}
//Validate the receiver IDs and receive amount
for (let floID in receivers) {
- if (!floCrypto.validateAddr(floID))
+ if (!floCrypto.validateFloID(floID))
invalids.InvalidReceiverIDs.push(floID);
if (typeof receivers[floID] !== 'number' || receivers[floID] <= 0)
invalids.InvalidReceiveAmountFor.push(floID);
@@ -371,18 +371,18 @@
})
//Calculate totalSentAmount and check if totalBalance is sufficient
let totalSendAmt = totalFee;
- for (floID in receivers)
+ 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 (floID in senders)
+ for (let floID in senders)
promises.push(promisedAPI(`api/addr/${floID}/utxo`));
Promise.all(promises).then(results => {
let wifSeq = [];
var trx = bitjs.transaction();
- for (floID in senders) {
+ for (let floID in senders) {
let utxos = results.shift();
let sendAmt;
if (preserveRatio) {
@@ -406,7 +406,7 @@
if (change > 0)
trx.addoutput(floID, change);
}
- for (floID in receivers)
+ for (let floID in receivers)
trx.addoutput(floID, receivers[floID]);
trx.addflodata(floData.replace(/\n/g, ' '));
for (let i = 0; i < wifSeq.length; i++)
diff --git a/scripts/floCloudAPI.js b/scripts/floCloudAPI.js
index ca31378..1fafb51 100644
--- a/scripts/floCloudAPI.js
+++ b/scripts/floCloudAPI.js
@@ -1,14 +1,59 @@
-(function(EXPORTS) { //floCloudAPI v2.3.0
+(function(EXPORTS) { //floCloudAPI v2.4.2d
/* 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
+ application: floGlobals.application,
+ 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
@@ -18,6 +63,9 @@
},
application: {
get: () => DEFAULT.application
+ },
+ user: {
+ get: () => user
}
});
@@ -175,7 +223,7 @@
if (_inactive.size === kBucket.list.length)
return reject('Cloud offline');
if (!(snID in supernodes))
- snID = kBucket.closestNode(snID);
+ snID = kBucket.closestNode(proxyID(snID));
ws_connect(snID)
.then(node => resolve(node))
.catch(error => {
@@ -213,7 +261,7 @@
if (_inactive.size === kBucket.list.length)
return reject('Cloud offline');
if (!(snID in supernodes))
- snID = kBucket.closestNode(snID);
+ snID = kBucket.closestNode(proxyID(snID));
fetch_API(snID, data)
.then(result => resolve(result))
.catch(error => {
@@ -269,6 +317,7 @@
data => {
data = objectifier(data);
let filtered = {},
+ proxy = proxyID(request.receiverID),
r = request;
for (let v in data) {
let d = data[v];
@@ -277,7 +326,7 @@
(r.atVectorClock || !r.upperVectorClock || r.upperVectorClock >= v) &&
(!r.afterTime || r.afterTime < d.log_time) &&
r.application == d.application &&
- r.receiverID == d.receiverID &&
+ (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)))
@@ -332,6 +381,50 @@
'|' + (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])
@@ -392,19 +485,19 @@
}));
}
- //set status as online for myFloID
+ //set status as online for user_id
floCloudAPI.setStatus = function(options = {}) {
return new Promise((resolve, reject) => {
- let callback = options.callback instanceof Function ? options.callback : (d, e) => console.debug(d, e);
+ let callback = options.callback instanceof Function ? options.callback : DEFAULT.callback;
var request = {
- floID: myFloID,
+ floID: user.id,
application: options.application || DEFAULT.application,
time: Date.now(),
status: true,
- pubKey: myPubKey
+ pubKey: user.public
}
let hashcontent = ["time", "application", "floID"].map(d => request[d]).join("|");
- request.sign = floCrypto.signData(hashcontent, myPrivKey);
+ request.sign = user.sign(hashcontent);
liveRequest(options.refID || DEFAULT.adminID, request, callback)
.then(result => resolve(result))
.catch(error => reject(error))
@@ -416,7 +509,7 @@
return new Promise((resolve, reject) => {
if (!Array.isArray(trackList))
trackList = [trackList];
- let callback = options.callback instanceof Function ? options.callback : (d, e) => console.debug(d, e);
+ let callback = options.callback instanceof Function ? options.callback : DEFAULT.callback;
let request = {
status: false,
application: options.application || DEFAULT.application,
@@ -432,9 +525,9 @@
const sendApplicationData = floCloudAPI.sendApplicationData = function(message, type, options = {}) {
return new Promise((resolve, reject) => {
var data = {
- senderID: myFloID,
+ senderID: user.id,
receiverID: options.receiverID || DEFAULT.adminID,
- pubKey: myPubKey,
+ pubKey: user.public,
message: encodeMessage(message),
time: Date.now(),
application: options.application || DEFAULT.application,
@@ -443,7 +536,7 @@
}
let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"]
.map(d => data[d]).join("|")
- data.sign = floCrypto.signData(hashcontent, myPrivKey);
+ data.sign = user.sign(hashcontent);
singleRequest(data.receiverID, data)
.then(result => resolve(result))
.catch(error => reject(error))
@@ -482,19 +575,20 @@
})
}
- //(NEEDS UPDATE) delete data from supernode cloud (received only)
+ /*(NEEDS UPDATE)
+ //delete data from supernode cloud (received only)
floCloudAPI.deleteApplicationData = function(vectorClocks, options = {}) {
return new Promise((resolve, reject) => {
var delreq = {
- requestorID: myFloID,
- pubKey: myPubKey,
+ 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 = floCrypto.signData(hashcontent, myPrivKey)
+ delreq.sign = user.sign(hashcontent)
singleRequest(delreq.requestorID, delreq).then(result => {
let success = [],
failed = [];
@@ -507,8 +601,9 @@
}).catch(error => reject(error))
})
}
-
- //(NEEDS UPDATE) edit comment of data in supernode cloud (mutable comments only)
+ */
+ /*(NEEDS UPDATE)
+ //edit comment of data in supernode cloud (mutable comments only)
floCloudAPI.editApplicationData = function(vectorClock, newComment, oldData, options = {}) {
return new Promise((resolve, reject) => {
let p0
@@ -523,12 +618,12 @@
}
})
p0.then(d => {
- if (d.senderID != myFloID)
+ if (d.senderID != user.id)
return reject("Invalid requestorID")
else if (!d.comment.startsWith("EDIT:"))
return reject("Data immutable")
let data = {
- requestorID: myFloID,
+ requestorID: user.id,
receiverID: d.receiverID,
time: Date.now(),
application: d.application,
@@ -542,29 +637,30 @@
"comment"
]
.map(x => d[x]).join("|")
- data.edit.sign = floCrypto.signData(hashcontent, myPrivKey)
+ data.edit.sign = user.sign(hashcontent)
singleRequest(data.receiverID, data)
.then(result => resolve("Data comment updated"))
.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(myFloID))
+ if (!floGlobals.subAdmins.includes(user.id))
return reject("Only subAdmins can tag data")
var request = {
receiverID: options.receiverID || DEFAULT.adminID,
- requestorID: myFloID,
- pubKey: myPubKey,
+ 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 = floCrypto.signData(hashcontent, myPrivKey);
+ request.sign = user.sign(hashcontent);
singleRequest(request.receiverID, request)
.then(result => resolve(result))
.catch(error => reject(error))
@@ -576,14 +672,14 @@
return new Promise((resolve, reject) => {
var request = {
receiverID: options.receiverID || DEFAULT.adminID,
- requestorID: myFloID,
- pubKey: myPubKey,
+ 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 = floCrypto.signData(hashcontent, myPrivKey);
+ request.sign = user.sign(hashcontent);
singleRequest(request.receiverID, request)
.then(result => resolve(result))
.catch(error => reject(error))
diff --git a/scripts/floCrypto.js b/scripts/floCrypto.js
index 4ab4d5c..6671ffa 100644
--- a/scripts/floCrypto.js
+++ b/scripts/floCrypto.js
@@ -1,4 +1,4 @@
-(function(EXPORTS) { //floCrypto v2.3.0a
+(function(EXPORTS) { //floCrypto v2.3.3d
/* FLO Crypto Operators */
'use strict';
const floCrypto = EXPORTS;
@@ -7,6 +7,7 @@
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();
@@ -80,7 +81,7 @@
floCrypto.randInt = function(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
- return Math.floor(Math.random() * (max - min + 1)) + min;
+ return Math.floor(securedMathRandom() * (max - min + 1)) + min;
}
//generate a random String within length (options : alphaNumeric chars only)
@@ -89,7 +90,7 @@
var characters = alphaNumeric ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' :
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+-./*?@#&$<>=[]{}():';
for (var i = 0; i < length; i++)
- result += characters.charAt(Math.floor(Math.random() * characters.length));
+ result += characters.charAt(Math.floor(securedMathRandom() * characters.length));
return result;
}
@@ -121,12 +122,8 @@
//Sign data using private-key
floCrypto.signData = function(data, privateKeyHex) {
var key = new Bitcoin.ECKey(privateKeyHex);
- key.setCompressed(true);
- var privateKeyArr = key.getBitcoinPrivateKeyByteArray();
- var privateKey = BigInteger.fromByteArrayUnsigned(privateKeyArr);
var messageHash = Crypto.SHA256(data);
- var messageHashBigInteger = new BigInteger(messageHash);
- var messageSign = Bitcoin.ECDSA.sign(messageHashBigInteger, key.priv);
+ var messageSign = Bitcoin.ECDSA.sign(messageHash, key.priv);
var sighex = Crypto.util.bytesToHex(messageSign);
return sighex;
}
@@ -134,11 +131,9 @@
//Verify signatue of the data using public-key
floCrypto.verifySign = function(data, signatureHex, publicKeyHex) {
var msgHash = Crypto.SHA256(data);
- var messageHashBigInteger = new BigInteger(msgHash);
var sigBytes = Crypto.util.hexToBytes(signatureHex);
- var signature = Bitcoin.ECDSA.parseSig(sigBytes);
var publicKeyPoint = ecparams.getCurve().decodePointHex(publicKeyHex);
- var verify = Bitcoin.ECDSA.verifyRaw(messageHashBigInteger, signature.r, signature.s, publicKeyPoint);
+ var verify = Bitcoin.ECDSA.verify(msgHash, sigBytes, publicKeyPoint);
return verify;
}
@@ -153,8 +148,23 @@
}
}
- Object.defineProperty(floCrypto, 'newID', {
- get: () => generateNewID()
+ Object.defineProperties(floCrypto, {
+ newID: {
+ get: () => generateNewID()
+ },
+ 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
@@ -182,6 +192,25 @@
}
}
+ 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)
@@ -202,18 +231,112 @@
}
}
- //Check if the given Address is valid or not
- floCrypto.validateFloID = floCrypto.validateAddr = function(inpAddr) {
- if (!inpAddr)
+ //Check if the given flo-id is valid or not
+ floCrypto.validateFloID = function(floID) {
+ if (!floID)
return false;
try {
- let addr = new Bitcoin.Address(inpAddr);
+ let addr = new Bitcoin.Address(floID);
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 for the address (any blockchain)
+ floCrypto.verifyPubKey = function(pubKeyHex, address) {
+ let raw = decodeAddress(address),
+ pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), {
+ asBytes: true
+ })));
+ return raw ? pub_hash === raw.hex : false;
+ }
+
+ //Convert the given address (any blockchain) to equivalent floID
+ floCrypto.toFloID = function(address) {
+ if (!address)
+ return;
+ let raw = decodeAddress(address);
+ if (!raw)
+ 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)));
+ }
+
+ //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
+ 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) { //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 {
diff --git a/scripts/floDapps.js b/scripts/floDapps.js
index 0e92708..38ee0b4 100644
--- a/scripts/floDapps.js
+++ b/scripts/floDapps.js
@@ -1,8 +1,168 @@
-(function(EXPORTS) { //floDapps v2.2.1
+(function(EXPORTS) { //floDapps v2.3.2d
/* General functions for FLO Dapps*/
- //'use strict';
+ '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, settings
+ Object.defineProperties(floGlobals, {
+ subAdmins: {
+ get: () => subAdmins
+ },
+ 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 = {
@@ -28,41 +188,41 @@
}
//add other given objectStores
initIndexedDB.appObs = initIndexedDB.appObs || {}
- for (o in initIndexedDB.appObs)
+ for (let o in initIndexedDB.appObs)
if (!(o in obs_a))
obs_a[o] = initIndexedDB.appObs[o]
Promise.all([
- compactIDB.initDB(floGlobals.application, obs_a),
- compactIDB.initDB("floDapps", obs_g)
+ compactIDB.initDB(DEFAULT.application, obs_a),
+ compactIDB.initDB(DEFAULT.root, obs_g)
]).then(result => {
- compactIDB.setDefaultDB(floGlobals.application)
+ compactIDB.setDefaultDB(DEFAULT.application)
resolve("IndexedDB App Storage Initated Successfully")
}).catch(error => reject(error));
})
}
- function initUserDB(floID) {
+ function initUserDB() {
return new Promise((resolve, reject) => {
var obs = {
contacts: {},
pubKeys: {},
messages: {}
}
- compactIDB.initDB(`floDapps#${floID}`, obs).then(result => {
+ compactIDB.initDB(user.db_name, obs).then(result => {
resolve("UserDB Initated Successfully")
}).catch(error => reject('Init userDB failed'));
})
}
- function loadUserDB(floID) {
+ 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], `floDapps#${floID}`)
+ promises[i] = compactIDB.readAllData(loadData[i], user.db_name)
Promise.all(promises).then(results => {
for (var i = 0; i < loadData.length; i++)
- floGlobals[loadData[i]] = results[i]
+ user[loadData[i]] = results[i]
resolve("Loaded Data from userDB")
}).catch(error => reject('Load userDB failed'))
})
@@ -72,7 +232,7 @@
startUpFunctions.push(function readSupernodeListFromAPI() {
return new Promise((resolve, reject) => {
- compactIDB.readData("lastTx", floCloudAPI.SNStorageID, "floDapps").then(lastTx => {
+ compactIDB.readData("lastTx", floCloudAPI.SNStorageID, DEFAULT.root).then(lastTx => {
floBlockchainAPI.readData(floCloudAPI.SNStorageID, {
ignoreOld: lastTx,
sentOnly: true,
@@ -80,14 +240,14 @@
}).then(result => {
for (var i = result.data.length - 1; i >= 0; i--) {
var content = JSON.parse(result.data[i]).SuperNodeStorage;
- for (sn in content.removeNodes)
- compactIDB.removeData("supernodes", sn, "floDapps");
- for (sn in content.newNodes)
- compactIDB.writeData("supernodes", content.newNodes[sn], sn, "floDapps");
+ for (let sn in content.removeNodes)
+ compactIDB.removeData("supernodes", sn, DEFAULT.root);
+ for (let sn in content.newNodes)
+ compactIDB.writeData("supernodes", content.newNodes[sn], sn, DEFAULT.root);
}
- compactIDB.writeData("lastTx", result.totalTxs, floCloudAPI.SNStorageID, "floDapps");
- compactIDB.readAllData("supernodes", "floDapps").then(result => {
- floCloudAPI.init(result)
+ compactIDB.writeData("lastTx", result.totalTxs, floCloudAPI.SNStorageID, DEFAULT.root);
+ compactIDB.readAllData("supernodes", DEFAULT.root).then(nodes => {
+ floCloudAPI.init(nodes)
.then(result => resolve("Loaded Supernode list\n" + result))
.catch(error => reject(error))
})
@@ -98,14 +258,14 @@
startUpFunctions.push(function readAppConfigFromAPI() {
return new Promise((resolve, reject) => {
- compactIDB.readData("lastTx", `${floGlobals.application}|${floGlobals.adminID}`, "floDapps").then(lastTx => {
- floBlockchainAPI.readData(floGlobals.adminID, {
+ compactIDB.readData("lastTx", `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root).then(lastTx => {
+ floBlockchainAPI.readData(DEFAULT.adminID, {
ignoreOld: lastTx,
sentOnly: true,
- pattern: floGlobals.application
+ pattern: DEFAULT.application
}).then(result => {
for (var i = result.data.length - 1; i >= 0; i--) {
- var content = JSON.parse(result.data[i])[floGlobals.application];
+ var content = JSON.parse(result.data[i])[DEFAULT.application];
if (!content || typeof content !== "object")
continue;
if (Array.isArray(content.removeSubAdmin))
@@ -118,11 +278,11 @@
for (let l in content.settings)
compactIDB.writeData("settings", content.settings[l], l)
}
- compactIDB.writeData("lastTx", result.totalTxs, `${floGlobals.application}|${floGlobals.adminID}`, "floDapps");
+ compactIDB.writeData("lastTx", result.totalTxs, `${DEFAULT.application}|${DEFAULT.adminID}`, DEFAULT.root);
compactIDB.readAllData("subAdmins").then(result => {
- floGlobals.subAdmins = Object.keys(result);
+ subAdmins = Object.keys(result);
compactIDB.readAllData("settings").then(result => {
- floGlobals.settings = result;
+ settings = result;
resolve("Read app configuration from blockchain");
})
})
@@ -186,7 +346,7 @@
});
const getPrivateKeyCredentials = () => new Promise((resolve, reject) => {
- var indexArr = localStorage.getItem(`${floGlobals.application}#privKey`)
+ var indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`)
if (indexArr) {
readSharesFromIDB(JSON.parse(indexArr))
.then(result => resolve(result))
@@ -197,7 +357,7 @@
if (!result)
return reject("Empty Private Key")
var floID = floCrypto.getFloID(result)
- if (!floID || !floCrypto.validateAddr(floID))
+ if (!floID || !floCrypto.validateFloID(floID))
return reject("Invalid Private Key")
privKey = result;
}).catch(error => {
@@ -210,7 +370,7 @@
var shares = floCrypto.createShamirsSecretShares(privKey, threshold, threshold)
writeSharesToIDB(shares).then(resultIndexes => {
//store index keys in localStorage
- localStorage.setItem(`${floGlobals.application}#privKey`, JSON.stringify(resultIndexes))
+ 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)
@@ -242,9 +402,14 @@
getPrivateKeyCredentials().then(key => {
checkIfPinRequired(key).then(privKey => {
try {
- myPrivKey = privKey
- myPubKey = floCrypto.getPubKeyHex(myPrivKey)
- myFloID = floCrypto.getFloID(myPubKey)
+ user_public = floCrypto.getPubKeyHex(privKey);
+ user_id = floCrypto.getAddress(privKey);
+ 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)
@@ -305,8 +470,8 @@
});
let p2 = new Promise((res, rej) => {
callAndLog(getCredentials()).then(r => {
- callAndLog(initUserDB(myFloID)).then(r => {
- callAndLog(loadUserDB(myFloID))
+ callAndLog(initUserDB()).then(r => {
+ callAndLog(loadUserDB())
.then(r => res(true))
.catch(e => rej(false))
}).catch(e => rej(false))
@@ -315,7 +480,10 @@
Promise.all([p1, p2])
.then(r => resolve('App Startup finished successful'))
.catch(e => reject('App Startup failed'))
- }).catch(error => reject("App database initiation failed"))
+ }).catch(error => {
+ startUpLog(false, error);
+ reject("App database initiation failed")
+ })
})
}
@@ -333,8 +501,8 @@
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(floID))
return reject("Invalid floID!")
- compactIDB.writeData("contacts", name, floID, `floDapps#${myFloID}`).then(result => {
- floGlobals.contacts[floID] = name;
+ compactIDB.writeData("contacts", name, floID, user.db_name).then(result => {
+ user.contacts[floID] = name;
resolve("Contact stored")
}).catch(error => reject(error))
});
@@ -342,14 +510,14 @@
floDapps.storePubKey = function(floID, pubKey) {
return new Promise((resolve, reject) => {
- if (floID in floGlobals.pubKeys)
+ if (floID in user.pubKeys)
return resolve("pubKey already stored")
if (!floCrypto.validateAddr(floID))
return reject("Invalid floID!")
- if (floCrypto.getFloID(pubKey) != floID)
+ if (!floCrypto.verifyPubKey(pubKey, floID))
return reject("Incorrect pubKey")
- compactIDB.writeData("pubKeys", pubKey, floID, `floDapps#${myFloID}`).then(result => {
- floGlobals.pubKeys[floID] = pubKey;
+ compactIDB.writeData("pubKeys", pubKey, floID, user.db_name).then(result => {
+ user.pubKeys[floID] = pubKey;
resolve("pubKey stored")
}).catch(error => reject(error))
});
@@ -359,11 +527,11 @@
return new Promise((resolve, reject) => {
let options = {
receiverID: floID,
- application: "floDapps",
- comment: floGlobals.application
+ application: DEFAULT.root,
+ comment: DEFAULT.application
}
- if (floID in floGlobals.pubKeys)
- message = floCrypto.encryptData(JSON.stringify(message), floGlobals.pubKeys[floID])
+ 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))
@@ -372,20 +540,21 @@
floDapps.requestInbox = function(callback) {
return new Promise((resolve, reject) => {
- let lastVC = Object.keys(floGlobals.messages).sort().pop()
+ let lastVC = Object.keys(user.messages).sort().pop()
let options = {
- receiverID: myFloID,
- application: "floDapps",
+ 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, myPrivKey)
+ d[v].message = floCrypto.decryptData(d[v].message, privKey)
} catch (error) {}
- compactIDB.writeData("messages", d[v], v, `floDapps#${myFloID}`)
- floGlobals.messages[v] = d[v]
+ compactIDB.writeData("messages", d[v], v, user.db_name)
+ user.messages[v] = d[v]
}
if (callback instanceof Function)
callback(d, e)
@@ -404,14 +573,14 @@
if (!addList && !rmList && !settings)
return reject("No configuration change")
var floData = {
- [floGlobals.application]: {
+ [DEFAULT.application]: {
addSubAdmin: addList,
removeSubAdmin: rmList,
settings: settings
}
}
var floID = floCrypto.getFloID(adminPrivKey)
- if (floID != floGlobals.adminID)
+ if (floID != DEFAULT.adminID)
reject('Access Denied for Admin privilege')
else
floBlockchainAPI.writeData(floID, JSON.stringify(floData), adminPrivKey)
@@ -422,9 +591,9 @@
const clearCredentials = floDapps.clearCredentials = function() {
return new Promise((resolve, reject) => {
- compactIDB.clearData('credentials', floGlobals.application).then(result => {
- localStorage.removeItem(`${floGlobals.application}#privKey`)
- myPrivKey = myPubKey = myFloID = undefined;
+ compactIDB.clearData('credentials', DEFAULT.application).then(result => {
+ localStorage.removeItem(`${DEFAULT.application}#privKey`);
+ user.clear();
resolve("privKey credentials deleted!")
}).catch(error => reject(error))
})
@@ -433,7 +602,7 @@
floDapps.deleteUserData = function(credentials = false) {
return new Promise((resolve, reject) => {
let p = []
- p.push(compactIDB.deleteDB(`floDapps#${myFloID}`))
+ p.push(compactIDB.deleteDB(user.db_name))
if (credentials)
p.push(clearCredentials())
Promise.all(p)
@@ -444,10 +613,10 @@
floDapps.deleteAppData = function() {
return new Promise((resolve, reject) => {
- compactIDB.deleteDB(floGlobals.application).then(result => {
- localStorage.removeItem(`${floGlobals.application}#privKey`)
- myPrivKey = myPubKey = myFloID = undefined;
- compactIDB.removeData('lastTx', `${floGlobals.application}|${floGlobals.adminID}`, 'floDapps')
+ 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))
@@ -455,17 +624,17 @@
}
floDapps.securePrivKey = function(pwd) {
- return new Promise((resolve, reject) => {
- let indexArr = localStorage.getItem(`${floGlobals.application}#privKey`)
+ 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(myPrivKey, pwd);
+ 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, floGlobals.application);
+ compactIDB.writeData("credentials", share, index, DEFAULT.application);
for (var i = 0; i < threshold; i++)
promises.push(overwriteFn(shares[i], indexArr[i]));
Promise.all(promises)
@@ -494,7 +663,7 @@
})
}
return new Promise((resolve, reject) => {
- var indexArr = localStorage.getItem(`${floGlobals.application}#privKey`)
+ var indexArr = localStorage.getItem(`${DEFAULT.application}#privKey`)
console.info(indexArr)
if (!indexArr)
reject('No login credentials found')
@@ -535,7 +704,7 @@
filteredResult[d] = JSON.parse(JSON.stringify(floGlobals.generalData[fk][d]))
}
if (options.decrypt) {
- let decryptionKey = (options.decrypt === true) ? myPrivKey : options.decrypt;
+ let decryptionKey = (options.decrypt === true) ? raw_user.private : options.decrypt;
if (!Array.isArray(decryptionKey))
decryptionKey = [decryptionKey];
for (let f in filteredResult) {
@@ -561,14 +730,14 @@
syncData.oldDevice = () => new Promise((resolve, reject) => {
let sync = {
- contacts: floGlobals.contacts,
- pubKeys: floGlobals.pubKeys,
- messages: floGlobals.messages
+ contacts: user.contacts,
+ pubKeys: user.pubKeys,
+ messages: user.messages
}
- let message = Crypto.AES.encrypt(JSON.stringify(sync), myPrivKey)
+ let message = Crypto.AES.encrypt(JSON.stringify(sync), raw_user.private)
let options = {
- receiverID: myFloID,
- application: "floDapps"
+ receiverID: user.id,
+ application: DEFAULT.root
}
floCloudAPI.sendApplicationData(message, "syncData", options)
.then(result => resolve(result))
@@ -577,20 +746,20 @@
syncData.newDevice = () => new Promise((resolve, reject) => {
var options = {
- receiverID: myFloID,
- senderID: myFloID,
- application: "floDapps",
+ 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, myPrivKey))
+ 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, `floDapps#${floID}`));
+ 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)
- floGlobals[c][i] = sync[c][i]
+ user[c][i] = sync[c][i]
}
})
Promise.all(promises)
diff --git a/scripts/lib.js b/scripts/lib.js
index 80afa89..b435194 100644
--- a/scripts/lib.js
+++ b/scripts/lib.js
@@ -1,4 +1,4 @@
-(function(GLOBAL) { //lib v1.2.2a
+(function(GLOBAL) { //lib v1.3.1
'use strict';
/* Utility Libraries required for Standard operations
* All credits for these codes belong to their respective creators, moderators and owners.
@@ -6,6 +6,37 @@
*/
GLOBAL.cryptocoin = (typeof floGlobals === 'undefined' ? null : floGlobals.blockchain) || 'FLO';
+ const getRandomBytes = (function() {
+ if (typeof require === 'function') {
+ const crypto = require('crypto');
+ return function(buf) {
+ var bytes = crypto.randomBytes(buf.length);
+ buf.set(bytes);
+ return buf;
+ }
+ } else if (GLOBAL.crypto && GLOBAL.crypto.getRandomValues) {
+ return function(buf) {
+ return GLOBAL.crypto.getRandomValues(buf);
+ }
+ } else
+ throw Error('Unable to define getRandomBytes');
+ })();
+
+
+ GLOBAL.securedMathRandom = (function() {
+ if (typeof require === 'function') {
+ const crypto = require('crypto');
+ return function() {
+ return crypto.randomBytes(4).readUInt32LE() / 0xffffffff;
+ }
+ } else if (GLOBAL.crypto && GLOBAL.crypto.getRandomValues) {
+ return function() {
+ return (GLOBAL.crypto.getRandomValues(new Uint32Array(1))[0] / 0xffffffff);
+ }
+ } else
+ throw Error('Unable to define securedMathRandom');
+ })();
+
//Crypto.js
(function() {
// Global Crypto object
@@ -52,7 +83,7 @@
// Generate an array of any length of random bytes
randomBytes: function(n) {
for (var bytes = []; n > 0; n--)
- bytes.push(Math.floor(Math.random() * 256));
+ bytes.push(Math.floor(securedMathRandom() * 256));
return bytes;
},
@@ -405,16 +436,6 @@
//SecureRandom.js
(function() {
- const getRandomValues = function(buf) {
- if (typeof require === 'function') {
- var bytes = require('crypto').randomBytes(buf.length);
- buf.set(bytes)
- return buf;
- } else if (GLOBAL.crypto && GLOBAL.crypto.getRandomValues)
- return GLOBAL.crypto.getRandomValues(buf);
- else
- return null;
- }
/*!
* Random number generator with ArcFour PRNG
@@ -446,10 +467,10 @@
// ba: byte array
sr.prototype.nextBytes = function(ba) {
var i;
- if (getRandomValues && GLOBAL.Uint8Array) {
+ if (getRandomBytes && GLOBAL.Uint8Array) {
try {
var rvBytes = new Uint8Array(ba.length);
- getRandomValues(rvBytes);
+ getRandomBytes(rvBytes);
for (i = 0; i < ba.length; ++i)
ba[i] = sr.getByte() ^ rvBytes[i];
return;
@@ -549,23 +570,23 @@
sr.pool = new Array();
sr.pptr = 0;
var t;
- if (getRandomValues && GLOBAL.Uint8Array) {
+ if (getRandomBytes && GLOBAL.Uint8Array) {
try {
// Use webcrypto if available
var ua = new Uint8Array(sr.poolSize);
- getRandomValues(ua);
+ getRandomBytes(ua);
for (t = 0; t < sr.poolSize; ++t)
sr.pool[sr.pptr++] = ua[t];
} catch (e) {
alert(e);
}
}
- while (sr.pptr < sr.poolSize) { // extract some randomness from Math.random()
- t = Math.floor(65536 * Math.random());
+ while (sr.pptr < sr.poolSize) { // extract some randomness from securedMathRandom()
+ t = Math.floor(65536 * securedMathRandom());
sr.pool[sr.pptr++] = t >>> 8;
sr.pool[sr.pptr++] = t & 255;
}
- sr.pptr = Math.floor(sr.poolSize * Math.random());
+ sr.pptr = Math.floor(sr.poolSize * securedMathRandom());
sr.seedTime();
// entropy
var entropyStr = "";
@@ -1654,7 +1675,7 @@
var a = nbi();
for (var i = 0; i < t; ++i) {
//Pick bases at random, instead of starting at 2
- a.fromInt(lowprimes[Math.floor(Math.random() * lowprimes.length)]);
+ a.fromInt(lowprimes[Math.floor(securedMathRandom() * lowprimes.length)]);
var y = a.modPow(r, this);
if (y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) {
var j = 1;
@@ -2612,7 +2633,7 @@
pad: function(cipher, message) {
var reqd = _requiredPadding(cipher, message);
for (var i = 1; i < reqd; i++) {
- message.push(Math.floor(Math.random() * 256));
+ message.push(Math.floor(securedMathRandom() * 256));
}
message.push(reqd);
},
@@ -4782,8 +4803,8 @@
} else if (floDataCount < 253) {
floDataCountString = floDataCount.toString(16);
} else if (floDataCount <= 1040) {
- floDataCountAdjusted = (floDataCount - 253) + parseInt("0xfd00fd");
- floDataCountStringAdjusted = floDataCountAdjusted.toString(16);
+ let floDataCountAdjusted = (floDataCount - 253) + parseInt("0xfd00fd");
+ let floDataCountStringAdjusted = floDataCountAdjusted.toString(16);
floDataCountString = floDataCountStringAdjusted.substr(0, 2) + floDataCountStringAdjusted.substr(4, 2) + floDataCountStringAdjusted.substr(2, 2);
} else {
floDataCountString = "Character Limit Exceeded";
@@ -5016,12 +5037,12 @@
*
* Returns the address as a base58-encoded string in the standardized format.
*/
- Bitcoin.Address.prototype.toString = function() {
+ Bitcoin.Address.prototype.toString = function(version = null) {
// Get a copy of the hash
var hash = this.hash.slice(0);
// Version
- hash.unshift(this.version);
+ hash.unshift(version !== null ? version : this.version);
var checksum = Crypto.SHA256(Crypto.SHA256(hash, {
asBytes: true
}), {
@@ -5130,7 +5151,7 @@
}
var Q;
- if (pubkey instanceof ec.PointFp) {
+ if (pubkey instanceof EllipticCurve.PointFp) {
Q = pubkey;
} else if (Bitcoin.Util.isArray(pubkey)) {
Q = EllipticCurve.PointFp.decodeFrom(ecparams.getCurve(), pubkey);
@@ -5444,9 +5465,8 @@
var bytes = null;
try {
- // This part is edited for FLO. FLO WIF are always compressed WIF. FLO WIF (private key) starts with R for mainnet and c for testnet.
- if (((GLOBAL.cryptocoin == "FLO") && /^R[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{51}$/.test(input)) ||
- ((GLOBAL.cryptocoin == "FLO_TEST") && /^c[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{51}$/.test(input))) {
+ // This part is edited for FLO. FLO WIF are always compressed WIF (length of 52).
+ if ((/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{52}$/.test(input))) {
bytes = ECKey.decodeCompressedWalletImportFormat(input);
this.compressed = true;
} else if (ECKey.isHexFormat(input)) {
@@ -5673,9 +5693,11 @@
}
var version = hash.shift();
+ /*
if (version != ECKey.privateKeyPrefix) {
throw "Version " + version + " not supported!";
}
+ */
return hash;
};
@@ -5697,9 +5719,11 @@
throw "Checksum validation failed!";
}
var version = hash.shift();
+ /*
if (version != ECKey.privateKeyPrefix) {
throw "Version " + version + " not supported!";
}
+ */
hash.pop();
return hash;
};
@@ -6014,6 +6038,2562 @@
})("secp256k1");
})();
+ //sha512.js
+ (function() {
+ /*
+ A JavaScript implementation of the SHA family of hashes, as defined in FIPS
+ PUB 180-2 as well as the corresponding HMAC implementation as defined in
+ FIPS PUB 198a
+
+ Copyright Brian Turek 2008-2012
+ Distributed under the BSD License
+ See http://caligatio.github.com/jsSHA/ for more information
+
+ Several functions taken from Paul Johnson
+ */
+ function n(a) {
+ throw a;
+ }
+ var q = null;
+
+ function s(a, b) {
+ this.a = a;
+ this.b = b
+ }
+
+ function u(a, b) {
+ var d = [],
+ h = (1 << b) - 1,
+ f = a.length * b,
+ g;
+ for (g = 0; g < f; g += b) d[g >>> 5] |= (a.charCodeAt(g / b) & h) << 32 - b - g % 32;
+ return {
+ value: d,
+ binLen: f
+ }
+ }
+
+ function x(a) {
+ var b = [],
+ d = a.length,
+ h, f;
+ 0 !== d % 2 && n("String of HEX type must be in byte increments");
+ for (h = 0; h < d; h += 2) f = parseInt(a.substr(h, 2), 16), isNaN(f) && n("String of HEX type contains invalid characters"), b[h >>> 3] |= f << 24 - 4 * (h % 8);
+ return {
+ value: b,
+ binLen: 4 * d
+ }
+ }
+
+ function B(a) {
+ var b = [],
+ d = 0,
+ h, f, g, k, m; - 1 === a.search(/^[a-zA-Z0-9=+\/]+$/) && n("Invalid character in base-64 string");
+ h = a.indexOf("=");
+ a = a.replace(/\=/g, ""); - 1 !== h && h < a.length && n("Invalid '=' found in base-64 string");
+ for (f = 0; f < a.length; f += 4) {
+ m = a.substr(f, 4);
+ for (g = k = 0; g < m.length; g += 1) h = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(m[g]), k |= h << 18 - 6 * g;
+ for (g = 0; g < m.length - 1; g += 1) b[d >> 2] |= (k >>> 16 - 8 * g & 255) << 24 - 8 * (d % 4), d += 1
+ }
+ return {
+ value: b,
+ binLen: 8 * d
+ }
+ }
+
+ function E(a, b) {
+ var d = "",
+ h = 4 * a.length,
+ f, g;
+ for (f = 0; f < h; f += 1) g = a[f >>> 2] >>> 8 * (3 - f % 4), d += "0123456789abcdef".charAt(g >>> 4 & 15) + "0123456789abcdef".charAt(g & 15);
+ return b.outputUpper ? d.toUpperCase() : d
+ }
+
+ function F(a, b) {
+ var d = "",
+ h = 4 * a.length,
+ f, g, k;
+ for (f = 0; f < h; f += 3) {
+ k = (a[f >>> 2] >>> 8 * (3 - f % 4) & 255) << 16 | (a[f + 1 >>> 2] >>> 8 * (3 - (f + 1) % 4) & 255) << 8 | a[f + 2 >>> 2] >>> 8 * (3 - (f + 2) % 4) & 255;
+ for (g = 0; 4 > g; g += 1) d = 8 * f + 6 * g <= 32 * a.length ? d + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(k >>> 6 * (3 - g) & 63) : d + b.b64Pad
+ }
+ return d
+ }
+
+ function G(a) {
+ var b = {
+ outputUpper: !1,
+ b64Pad: "="
+ };
+ try {
+ a.hasOwnProperty("outputUpper") && (b.outputUpper = a.outputUpper), a.hasOwnProperty("b64Pad") && (b.b64Pad = a.b64Pad)
+ } catch (d) {}
+ "boolean" !== typeof b.outputUpper && n("Invalid outputUpper formatting option");
+ "string" !== typeof b.b64Pad && n("Invalid b64Pad formatting option");
+ return b
+ }
+
+ function H(a, b) {
+ var d = q,
+ d = new s(a.a, a.b);
+ return d = 32 >= b ? new s(d.a >>> b | d.b << 32 - b & 4294967295, d.b >>> b | d.a << 32 - b & 4294967295) : new s(d.b >>> b - 32 | d.a << 64 - b & 4294967295, d.a >>> b - 32 | d.b << 64 - b & 4294967295)
+ }
+
+ function I(a, b) {
+ var d = q;
+ return d = 32 >= b ? new s(a.a >>> b, a.b >>> b | a.a << 32 - b & 4294967295) : new s(0, a.a >>> b - 32)
+ }
+
+ function J(a, b, d) {
+ return new s(a.a & b.a ^ ~a.a & d.a, a.b & b.b ^ ~a.b & d.b)
+ }
+
+ function U(a, b, d) {
+ return new s(a.a & b.a ^ a.a & d.a ^ b.a & d.a, a.b & b.b ^ a.b & d.b ^ b.b & d.b)
+ }
+
+ function V(a) {
+ var b = H(a, 28),
+ d = H(a, 34);
+ a = H(a, 39);
+ return new s(b.a ^ d.a ^ a.a, b.b ^ d.b ^ a.b)
+ }
+
+ function W(a) {
+ var b = H(a, 14),
+ d = H(a, 18);
+ a = H(a, 41);
+ return new s(b.a ^ d.a ^ a.a, b.b ^ d.b ^ a.b)
+ }
+
+ function X(a) {
+ var b = H(a, 1),
+ d = H(a, 8);
+ a = I(a, 7);
+ return new s(b.a ^ d.a ^ a.a, b.b ^ d.b ^ a.b)
+ }
+
+ function Y(a) {
+ var b = H(a, 19),
+ d = H(a, 61);
+ a = I(a, 6);
+ return new s(b.a ^ d.a ^ a.a, b.b ^ d.b ^ a.b)
+ }
+
+ function Z(a, b) {
+ var d, h, f;
+ d = (a.b & 65535) + (b.b & 65535);
+ h = (a.b >>> 16) + (b.b >>> 16) + (d >>> 16);
+ f = (h & 65535) << 16 | d & 65535;
+ d = (a.a & 65535) + (b.a & 65535) + (h >>> 16);
+ h = (a.a >>> 16) + (b.a >>> 16) + (d >>> 16);
+ return new s((h & 65535) << 16 | d & 65535, f)
+ }
+
+ function aa(a, b, d, h) {
+ var f, g, k;
+ f = (a.b & 65535) + (b.b & 65535) + (d.b & 65535) + (h.b & 65535);
+ g = (a.b >>> 16) + (b.b >>> 16) + (d.b >>> 16) + (h.b >>> 16) + (f >>> 16);
+ k = (g & 65535) << 16 | f & 65535;
+ f = (a.a & 65535) + (b.a & 65535) + (d.a & 65535) + (h.a & 65535) + (g >>> 16);
+ g = (a.a >>> 16) + (b.a >>> 16) + (d.a >>> 16) + (h.a >>> 16) + (f >>> 16);
+ return new s((g & 65535) << 16 | f & 65535, k)
+ }
+
+ function ba(a, b, d, h, f) {
+ var g, k, m;
+ g = (a.b & 65535) + (b.b & 65535) + (d.b & 65535) + (h.b & 65535) + (f.b & 65535);
+ k = (a.b >>> 16) + (b.b >>> 16) + (d.b >>> 16) + (h.b >>> 16) + (f.b >>> 16) + (g >>> 16);
+ m = (k & 65535) << 16 | g & 65535;
+ g = (a.a & 65535) + (b.a & 65535) + (d.a & 65535) + (h.a & 65535) + (f.a & 65535) + (k >>> 16);
+ k = (a.a >>> 16) + (b.a >>> 16) + (d.a >>> 16) + (h.a >>> 16) + (f.a >>> 16) + (g >>> 16);
+ return new s((k & 65535) << 16 | g & 65535, m)
+ }
+
+ function $(a, b, d) {
+ var h, f, g, k, m, j, A, C, K, e, L, v, l, M, t, p, y, z, r, N, O, P, Q, R, c, S, w = [],
+ T, D;
+ "SHA-384" === d || "SHA-512" === d ? (L = 80, h = (b + 128 >>> 10 << 5) + 31, M = 32, t = 2, c = s, p = Z, y = aa, z = ba, r = X, N = Y, O = V, P = W, R = U, Q = J, S = [new c(1116352408, 3609767458), new c(1899447441, 602891725), new c(3049323471, 3964484399), new c(3921009573, 2173295548), new c(961987163, 4081628472), new c(1508970993, 3053834265), new c(2453635748, 2937671579), new c(2870763221, 3664609560), new c(3624381080, 2734883394), new c(310598401, 1164996542), new c(607225278, 1323610764),
+ new c(1426881987, 3590304994), new c(1925078388, 4068182383), new c(2162078206, 991336113), new c(2614888103, 633803317), new c(3248222580, 3479774868), new c(3835390401, 2666613458), new c(4022224774, 944711139), new c(264347078, 2341262773), new c(604807628, 2007800933), new c(770255983, 1495990901), new c(1249150122, 1856431235), new c(1555081692, 3175218132), new c(1996064986, 2198950837), new c(2554220882, 3999719339), new c(2821834349, 766784016), new c(2952996808, 2566594879), new c(3210313671, 3203337956), new c(3336571891,
+ 1034457026), new c(3584528711, 2466948901), new c(113926993, 3758326383), new c(338241895, 168717936), new c(666307205, 1188179964), new c(773529912, 1546045734), new c(1294757372, 1522805485), new c(1396182291, 2643833823), new c(1695183700, 2343527390), new c(1986661051, 1014477480), new c(2177026350, 1206759142), new c(2456956037, 344077627), new c(2730485921, 1290863460), new c(2820302411, 3158454273), new c(3259730800, 3505952657), new c(3345764771, 106217008), new c(3516065817, 3606008344), new c(3600352804, 1432725776), new c(4094571909,
+ 1467031594), new c(275423344, 851169720), new c(430227734, 3100823752), new c(506948616, 1363258195), new c(659060556, 3750685593), new c(883997877, 3785050280), new c(958139571, 3318307427), new c(1322822218, 3812723403), new c(1537002063, 2003034995), new c(1747873779, 3602036899), new c(1955562222, 1575990012), new c(2024104815, 1125592928), new c(2227730452, 2716904306), new c(2361852424, 442776044), new c(2428436474, 593698344), new c(2756734187, 3733110249), new c(3204031479, 2999351573), new c(3329325298, 3815920427), new c(3391569614,
+ 3928383900), new c(3515267271, 566280711), new c(3940187606, 3454069534), new c(4118630271, 4000239992), new c(116418474, 1914138554), new c(174292421, 2731055270), new c(289380356, 3203993006), new c(460393269, 320620315), new c(685471733, 587496836), new c(852142971, 1086792851), new c(1017036298, 365543100), new c(1126000580, 2618297676), new c(1288033470, 3409855158), new c(1501505948, 4234509866), new c(1607167915, 987167468), new c(1816402316, 1246189591)
+ ], e = "SHA-384" === d ? [new c(3418070365, 3238371032), new c(1654270250, 914150663),
+ new c(2438529370, 812702999), new c(355462360, 4144912697), new c(1731405415, 4290775857), new c(41048885895, 1750603025), new c(3675008525, 1694076839), new c(1203062813, 3204075428)
+ ] : [new c(1779033703, 4089235720), new c(3144134277, 2227873595), new c(1013904242, 4271175723), new c(2773480762, 1595750129), new c(1359893119, 2917565137), new c(2600822924, 725511199), new c(528734635, 4215389547), new c(1541459225, 327033209)]) : n("Unexpected error in SHA-2 implementation");
+ a[b >>> 5] |= 128 << 24 - b % 32;
+ a[h] = b;
+ T = a.length;
+ for (v = 0; v <
+ T; v += M) {
+ b = e[0];
+ h = e[1];
+ f = e[2];
+ g = e[3];
+ k = e[4];
+ m = e[5];
+ j = e[6];
+ A = e[7];
+ for (l = 0; l < L; l += 1) w[l] = 16 > l ? new c(a[l * t + v], a[l * t + v + 1]) : y(N(w[l - 2]), w[l - 7], r(w[l - 15]), w[l - 16]), C = z(A, P(k), Q(k, m, j), S[l], w[l]), K = p(O(b), R(b, h, f)), A = j, j = m, m = k, k = p(g, C), g = f, f = h, h = b, b = p(C, K);
+ e[0] = p(b, e[0]);
+ e[1] = p(h, e[1]);
+ e[2] = p(f, e[2]);
+ e[3] = p(g, e[3]);
+ e[4] = p(k, e[4]);
+ e[5] = p(m, e[5]);
+ e[6] = p(j, e[6]);
+ e[7] = p(A, e[7])
+ }
+ "SHA-384" === d ? D = [e[0].a, e[0].b, e[1].a, e[1].b, e[2].a, e[2].b, e[3].a, e[3].b, e[4].a, e[4].b, e[5].a, e[5].b] : "SHA-512" === d ? D = [e[0].a, e[0].b,
+ e[1].a, e[1].b, e[2].a, e[2].b, e[3].a, e[3].b, e[4].a, e[4].b, e[5].a, e[5].b, e[6].a, e[6].b, e[7].a, e[7].b
+ ] : n("Unexpected error in SHA-2 implementation");
+ return D
+ }
+ GLOBAL.jsSHA = function(a, b, d) {
+ var h = q,
+ f = q,
+ g = 0,
+ k = [0],
+ m = 0,
+ j = q,
+ m = "undefined" !== typeof d ? d : 8;
+ 8 === m || 16 === m || n("charSize must be 8 or 16");
+ "HEX" === b ? (0 !== a.length % 2 && n("srcString of HEX type must be in byte increments"), j = x(a), g = j.binLen, k = j.value) : "ASCII" === b || "TEXT" === b ? (j = u(a, m), g = j.binLen, k = j.value) : "B64" === b ? (j = B(a), g = j.binLen, k = j.value) : n("inputFormat must be HEX, TEXT, ASCII, or B64");
+ this.getHash = function(a, b, d) {
+ var e = q,
+ m = k.slice(),
+ j = "";
+ switch (b) {
+ case "HEX":
+ e = E;
+ break;
+ case "B64":
+ e = F;
+ break;
+ default:
+ n("format must be HEX or B64")
+ }
+ "SHA-384" ===
+ a ? (q === h && (h = $(m, g, a)), j = e(h, G(d))) : "SHA-512" === a ? (q === f && (f = $(m, g, a)), j = e(f, G(d))) : n("Chosen SHA variant is not supported");
+ return j
+ };
+ this.getHMAC = function(a, b, d, e, f) {
+ var h, l, j, t, p, y = [],
+ z = [],
+ r = q;
+ switch (e) {
+ case "HEX":
+ h = E;
+ break;
+ case "B64":
+ h = F;
+ break;
+ default:
+ n("outputFormat must be HEX or B64")
+ }
+ "SHA-384" === d ? (j = 128, p = 384) : "SHA-512" === d ? (j = 128, p = 512) : n("Chosen SHA variant is not supported");
+ "HEX" === b ? (r = x(a), t = r.binLen, l = r.value) : "ASCII" === b || "TEXT" === b ? (r = u(a, m), t = r.binLen, l = r.value) : "B64" === b ? (r = B(a),
+ t = r.binLen, l = r.value) : n("inputFormat must be HEX, TEXT, ASCII, or B64");
+ a = 8 * j;
+ b = j / 4 - 1;
+ j < t / 8 ? (l = $(l, t, d), l[b] &= 4294967040) : j > t / 8 && (l[b] &= 4294967040);
+ for (j = 0; j <= b; j += 1) y[j] = l[j] ^ 909522486, z[j] = l[j] ^ 1549556828;
+ d = $(z.concat($(y.concat(k), a + g, d)), a + p, d);
+ return h(d, G(f))
+ }
+ };
+ })();
+
+ //coin.js
+ (function() {
+ /*
+ Coinjs 0.01 beta by OutCast3k{at}gmail.com
+ A bitcoin framework.
+ http://github.com/OutCast3k/coinjs or http://coinb.in/coinjs
+ */
+ var coinjs = GLOBAL.coinjs = function() {};
+
+ /* public vars */
+ coinjs.pub = 0x00;
+ coinjs.priv = 0x80;
+ coinjs.multisig = 0x05;
+ coinjs.hdkey = {
+ 'prv': 0x0488ade4,
+ 'pub': 0x0488b21e
+ };
+ coinjs.bech32 = {
+ 'charset': 'qpzry9x8gf2tvdw0s3jn54khce6mua7l',
+ 'version': 0,
+ 'hrp': 'bc'
+ };
+
+ coinjs.compressed = false;
+
+ /* other vars */
+ coinjs.developer = '33tht1bKDgZVxb39MnZsWa8oxHXHvUYE4G'; //bitcoin
+
+ /* bit(coinb.in) api vars
+ coinjs.hostname = ((document.location.hostname.split(".")[(document.location.hostname.split(".")).length - 1]) == 'onion') ? 'coinbin3ravkwb24f7rmxx6w3snkjw45jhs5lxbh3yfeg3vpt6janwqd.onion' : 'coinb.in';
+ coinjs.host = ('https:' == document.location.protocol ? 'https://' : 'http://') + coinjs.hostname + '/api/';
+ coinjs.uid = '1';
+ coinjs.key = '12345678901234567890123456789012';
+ */
+
+ /* start of address functions */
+
+ /* generate a private and public keypair, with address and WIF address */
+ coinjs.newKeys = function(input) {
+ var privkey = (input) ? Crypto.SHA256(input) : this.newPrivkey();
+ var pubkey = this.newPubkey(privkey);
+ return {
+ 'privkey': privkey,
+ 'pubkey': pubkey,
+ 'address': this.pubkey2address(pubkey),
+ 'wif': this.privkey2wif(privkey),
+ 'compressed': this.compressed
+ };
+ }
+
+ /* generate a new random private key */
+ coinjs.newPrivkey = function() {
+ var x = GLOBAL.location;
+ x += (GLOBAL.screen.height * GLOBAL.screen.width * GLOBAL.screen.colorDepth);
+ x += coinjs.random(64);
+ x += (GLOBAL.screen.availHeight * GLOBAL.screen.availWidth * GLOBAL.screen.pixelDepth);
+ x += navigator.language;
+ x += GLOBAL.history.length;
+ x += coinjs.random(64);
+ x += navigator.userAgent;
+ x += 'coinb.in';
+ x += (Crypto.util.randomBytes(64)).join("");
+ x += x.length;
+ var dateObj = new Date();
+ x += dateObj.getTimezoneOffset();
+ x += coinjs.random(64);
+ x += (document.getElementById("entropybucket")) ? document.getElementById("entropybucket").innerHTML : '';
+ x += x + '' + x;
+ var r = x;
+ for (let i = 0; i < (x).length / 25; i++) {
+ r = Crypto.SHA256(r.concat(x));
+ }
+ var checkrBigInt = new BigInteger(r);
+ var orderBigInt = new BigInteger("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
+ while (checkrBigInt.compareTo(orderBigInt) >= 0 || checkrBigInt.equals(BigInteger.ZERO) || checkrBigInt.equals(BigInteger.ONE)) {
+ r = Crypto.SHA256(r.concat(x));
+ checkrBigInt = new BigInteger(r);
+ }
+ return r;
+ }
+
+ /* generate a public key from a private key */
+ coinjs.newPubkey = function(hash) {
+ var privateKeyBigInt = BigInteger.fromByteArrayUnsigned(Crypto.util.hexToBytes(hash));
+ var curve = EllipticCurve.getSECCurveByName("secp256k1");
+
+ var curvePt = curve.getG().multiply(privateKeyBigInt);
+ var x = curvePt.getX().toBigInteger();
+ var y = curvePt.getY().toBigInteger();
+
+ var publicKeyBytes = EllipticCurve.integerToBytes(x, 32);
+ publicKeyBytes = publicKeyBytes.concat(EllipticCurve.integerToBytes(y, 32));
+ publicKeyBytes.unshift(0x04);
+
+ if (coinjs.compressed == true) {
+ var publicKeyBytesCompressed = EllipticCurve.integerToBytes(x, 32)
+ if (y.isEven()) {
+ publicKeyBytesCompressed.unshift(0x02)
+ } else {
+ publicKeyBytesCompressed.unshift(0x03)
+ }
+ return Crypto.util.bytesToHex(publicKeyBytesCompressed);
+ } else {
+ return Crypto.util.bytesToHex(publicKeyBytes);
+ }
+ }
+
+ /* provide a public key and return address */
+ coinjs.pubkey2address = function(h, byte) {
+ var r = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(h), {
+ asBytes: true
+ }));
+ r.unshift(byte || coinjs.pub);
+ var hash = Crypto.SHA256(Crypto.SHA256(r, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ var checksum = hash.slice(0, 4);
+ return coinjs.base58encode(r.concat(checksum));
+ }
+
+ /* provide a scripthash and return address */
+ coinjs.scripthash2address = function(h) {
+ var x = Crypto.util.hexToBytes(h);
+ x.unshift(coinjs.pub);
+ var r = x;
+ r = Crypto.SHA256(Crypto.SHA256(r, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ var checksum = r.slice(0, 4);
+ return coinjs.base58encode(x.concat(checksum));
+ }
+
+ /* new multisig address, provide the pubkeys AND required signatures to release the funds */
+ coinjs.pubkeys2MultisigAddress = function(pubkeys, required) {
+ var s = coinjs.script();
+ s.writeOp(81 + (required * 1) - 1); //OP_1
+ for (var i = 0; i < pubkeys.length; ++i) {
+ s.writeBytes(Crypto.util.hexToBytes(pubkeys[i]));
+ }
+ s.writeOp(81 + pubkeys.length - 1); //OP_1
+ s.writeOp(174); //OP_CHECKMULTISIG
+ var x = ripemd160(Crypto.SHA256(s.buffer, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ x.unshift(coinjs.multisig);
+ var r = x;
+ r = Crypto.SHA256(Crypto.SHA256(r, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ var checksum = r.slice(0, 4);
+ var redeemScript = Crypto.util.bytesToHex(s.buffer);
+ var address = coinjs.base58encode(x.concat(checksum));
+
+ if (s.buffer.length > 520) { // too large
+ address = 'invalid';
+ redeemScript = 'invalid';
+ }
+
+ return {
+ 'address': address,
+ 'redeemScript': redeemScript,
+ 'size': s.buffer.length
+ };
+ }
+
+ /* new time locked address, provide the pubkey and time necessary to unlock the funds.
+ when time is greater than 500000000, it should be a unix timestamp (seconds since epoch),
+ otherwise it should be the block height required before this transaction can be released.
+
+ may throw a string on failure!
+ */
+ coinjs.simpleHodlAddress = function(pubkey, checklocktimeverify) {
+
+ if (checklocktimeverify < 0) {
+ throw "Parameter for OP_CHECKLOCKTIMEVERIFY is negative.";
+ }
+
+ var s = coinjs.script();
+ if (checklocktimeverify <= 16 && checklocktimeverify >= 1) {
+ s.writeOp(0x50 + checklocktimeverify); //OP_1 to OP_16 for minimal encoding
+ } else {
+ s.writeBytes(coinjs.numToScriptNumBytes(checklocktimeverify));
+ }
+ s.writeOp(177); //OP_CHECKLOCKTIMEVERIFY
+ s.writeOp(117); //OP_DROP
+ s.writeBytes(Crypto.util.hexToBytes(pubkey));
+ s.writeOp(172); //OP_CHECKSIG
+
+ var x = ripemd160(Crypto.SHA256(s.buffer, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ x.unshift(coinjs.multisig);
+ var r = x;
+ r = Crypto.SHA256(Crypto.SHA256(r, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ var checksum = r.slice(0, 4);
+ var redeemScript = Crypto.util.bytesToHex(s.buffer);
+ var address = coinjs.base58encode(x.concat(checksum));
+
+ return {
+ 'address': address,
+ 'redeemScript': redeemScript
+ };
+ }
+
+ /* create a new segwit address */
+ coinjs.segwitAddress = function(pubkey) {
+ var keyhash = [0x00, 0x14].concat(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubkey), {
+ asBytes: true
+ }), {
+ asBytes: true
+ }));
+ var x = ripemd160(Crypto.SHA256(keyhash, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ x.unshift(coinjs.multisig);
+ var r = x;
+ r = Crypto.SHA256(Crypto.SHA256(r, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ var checksum = r.slice(0, 4);
+ var address = coinjs.base58encode(x.concat(checksum));
+
+ return {
+ 'address': address,
+ 'type': 'segwit',
+ 'redeemscript': Crypto.util.bytesToHex(keyhash)
+ };
+ }
+
+ /* create a new segwit bech32 encoded address */
+ coinjs.bech32Address = function(pubkey) {
+ var program = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubkey), {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ var address = coinjs.bech32_encode(coinjs.bech32.hrp, [coinjs.bech32.version].concat(coinjs.bech32_convert(program, 8, 5, true)));
+ return {
+ 'address': address,
+ 'type': 'bech32',
+ 'redeemscript': Crypto.util.bytesToHex(program)
+ };
+ }
+
+ /* extract the redeemscript from a bech32 address */
+ coinjs.bech32redeemscript = function(address) {
+ var r = false;
+ var decode = coinjs.bech32_decode(address);
+ if (decode) {
+ decode.data.shift();
+ return Crypto.util.bytesToHex(coinjs.bech32_convert(decode.data, 5, 8, false));
+ }
+ return r;
+ }
+
+ /* provide a privkey and return an WIF */
+ coinjs.privkey2wif = function(h) {
+ var r = Crypto.util.hexToBytes(h);
+
+ if (coinjs.compressed == true) {
+ r.push(0x01);
+ }
+
+ r.unshift(coinjs.priv);
+ var hash = Crypto.SHA256(Crypto.SHA256(r, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ var checksum = hash.slice(0, 4);
+
+ return coinjs.base58encode(r.concat(checksum));
+ }
+
+ /* convert a wif key back to a private key */
+ coinjs.wif2privkey = function(wif) {
+ var compressed = false;
+ var decode = coinjs.base58decode(wif);
+ var key = decode.slice(0, decode.length - 4);
+ key = key.slice(1, key.length);
+ if (key.length >= 33 && key[key.length - 1] == 0x01) {
+ key = key.slice(0, key.length - 1);
+ compressed = true;
+ }
+ return {
+ 'privkey': Crypto.util.bytesToHex(key),
+ 'compressed': compressed
+ };
+ }
+
+ /* convert a wif to a pubkey */
+ coinjs.wif2pubkey = function(wif) {
+ var compressed = coinjs.compressed;
+ var r = coinjs.wif2privkey(wif);
+ coinjs.compressed = r['compressed'];
+ var pubkey = coinjs.newPubkey(r['privkey']);
+ coinjs.compressed = compressed;
+ return {
+ 'pubkey': pubkey,
+ 'compressed': r['compressed']
+ };
+ }
+
+ /* convert a wif to a address */
+ coinjs.wif2address = function(wif) {
+ var r = coinjs.wif2pubkey(wif);
+ return {
+ 'address': coinjs.pubkey2address(r['pubkey']),
+ 'compressed': r['compressed']
+ };
+ }
+
+ /* decode or validate an address and return the hash */
+ coinjs.addressDecode = function(addr) {
+ try {
+ var bytes = coinjs.base58decode(addr);
+ var front = bytes.slice(0, bytes.length - 4);
+ var back = bytes.slice(bytes.length - 4);
+ var checksum = Crypto.SHA256(Crypto.SHA256(front, {
+ asBytes: true
+ }), {
+ asBytes: true
+ }).slice(0, 4);
+ if (checksum + "" == back + "") {
+
+ var o = {};
+ o.bytes = front.slice(1);
+ o.version = front[0];
+
+ if (o.version == coinjs.pub) { // standard address
+ o.type = 'standard';
+
+ } else if (o.version == coinjs.multisig) { // multisig address
+ o.type = 'multisig';
+
+ } else if (o.version == coinjs.priv) { // wifkey
+ o.type = 'wifkey';
+
+ } else if (o.version == 42) { // stealth address
+ o.type = 'stealth';
+
+ o.option = front[1];
+ if (o.option != 0) {
+ alert("Stealth Address option other than 0 is currently not supported!");
+ return false;
+ };
+
+ o.scankey = Crypto.util.bytesToHex(front.slice(2, 35));
+ o.n = front[35];
+
+ if (o.n > 1) {
+ alert("Stealth Multisig is currently not supported!");
+ return false;
+ };
+
+ o.spendkey = Crypto.util.bytesToHex(front.slice(36, 69));
+ o.m = front[69];
+ o.prefixlen = front[70];
+
+ if (o.prefixlen > 0) {
+ alert("Stealth Address Prefixes are currently not supported!");
+ return false;
+ };
+ o.prefix = front.slice(71);
+
+ } else { // everything else
+ o.type = 'other'; // address is still valid but unknown version
+ }
+
+ return o;
+ } else {
+ throw "Invalid checksum";
+ }
+ } catch (e) {
+ let bech32rs = coinjs.bech32redeemscript(addr);
+ if (bech32rs) {
+ return {
+ 'type': 'bech32',
+ 'redeemscript': bech32rs
+ };
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /* retreive the balance from a given address */
+ coinjs.addressBalance = function(address, callback) {
+ coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=addresses&request=bal&address=' + address + '&r=' + securedMathRandom(), callback, "GET");
+ }
+
+ /* decompress an compressed public key */
+ coinjs.pubkeydecompress = function(pubkey) {
+ if ((typeof(pubkey) == 'string') && pubkey.match(/^[a-f0-9]+$/i)) {
+ var curve = EllipticCurve.getSECCurveByName("secp256k1");
+ try {
+ var pt = curve.curve.decodePointHex(pubkey);
+ var x = pt.getX().toBigInteger();
+ var y = pt.getY().toBigInteger();
+
+ var publicKeyBytes = EllipticCurve.integerToBytes(x, 32);
+ publicKeyBytes = publicKeyBytes.concat(EllipticCurve.integerToBytes(y, 32));
+ publicKeyBytes.unshift(0x04);
+ return Crypto.util.bytesToHex(publicKeyBytes);
+ } catch (e) {
+ // console.log(e);
+ return false;
+ }
+ }
+ return false;
+ }
+
+ coinjs.bech32_polymod = function(values) {
+ var chk = 1;
+ var BECH32_GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
+ for (var p = 0; p < values.length; ++p) {
+ var top = chk >> 25;
+ chk = (chk & 0x1ffffff) << 5 ^ values[p];
+ for (var i = 0; i < 5; ++i) {
+ if ((top >> i) & 1) {
+ chk ^= BECH32_GENERATOR[i];
+ }
+ }
+ }
+ return chk;
+ }
+
+ coinjs.bech32_hrpExpand = function(hrp) {
+ var ret = [];
+ var p;
+ for (p = 0; p < hrp.length; ++p) {
+ ret.push(hrp.charCodeAt(p) >> 5);
+ }
+ ret.push(0);
+ for (p = 0; p < hrp.length; ++p) {
+ ret.push(hrp.charCodeAt(p) & 31);
+ }
+ return ret;
+ }
+
+ coinjs.bech32_verifyChecksum = function(hrp, data) {
+ return coinjs.bech32_polymod(coinjs.bech32_hrpExpand(hrp).concat(data)) === 1;
+ }
+
+ coinjs.bech32_createChecksum = function(hrp, data) {
+ var values = coinjs.bech32_hrpExpand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]);
+ var mod = coinjs.bech32_polymod(values) ^ 1;
+ var ret = [];
+ for (var p = 0; p < 6; ++p) {
+ ret.push((mod >> 5 * (5 - p)) & 31);
+ }
+ return ret;
+ }
+
+ coinjs.bech32_encode = function(hrp, data) {
+ var combined = data.concat(coinjs.bech32_createChecksum(hrp, data));
+ var ret = hrp + '1';
+ for (var p = 0; p < combined.length; ++p) {
+ ret += coinjs.bech32.charset.charAt(combined[p]);
+ }
+ return ret;
+ }
+
+ coinjs.bech32_decode = function(bechString) {
+ var p;
+ var has_lower = false;
+ var has_upper = false;
+ for (p = 0; p < bechString.length; ++p) {
+ if (bechString.charCodeAt(p) < 33 || bechString.charCodeAt(p) > 126) {
+ return null;
+ }
+ if (bechString.charCodeAt(p) >= 97 && bechString.charCodeAt(p) <= 122) {
+ has_lower = true;
+ }
+ if (bechString.charCodeAt(p) >= 65 && bechString.charCodeAt(p) <= 90) {
+ has_upper = true;
+ }
+ }
+ if (has_lower && has_upper) {
+ return null;
+ }
+ bechString = bechString.toLowerCase();
+ var pos = bechString.lastIndexOf('1');
+ if (pos < 1 || pos + 7 > bechString.length || bechString.length > 90) {
+ return null;
+ }
+ var hrp = bechString.substring(0, pos);
+ var data = [];
+ for (p = pos + 1; p < bechString.length; ++p) {
+ var d = coinjs.bech32.charset.indexOf(bechString.charAt(p));
+ if (d === -1) {
+ return null;
+ }
+ data.push(d);
+ }
+ if (!coinjs.bech32_verifyChecksum(hrp, data)) {
+ return null;
+ }
+ return {
+ hrp: hrp,
+ data: data.slice(0, data.length - 6)
+ };
+ }
+
+ coinjs.bech32_convert = function(data, inBits, outBits, pad) {
+ var value = 0;
+ var bits = 0;
+ var maxV = (1 << outBits) - 1;
+
+ var result = [];
+ for (var i = 0; i < data.length; ++i) {
+ value = (value << inBits) | data[i];
+ bits += inBits;
+
+ while (bits >= outBits) {
+ bits -= outBits;
+ result.push((value >> bits) & maxV);
+ }
+ }
+
+ if (pad) {
+ if (bits > 0) {
+ result.push((value << (outBits - bits)) & maxV);
+ }
+ } else {
+ if (bits >= inBits) throw new Error('Excess padding');
+ if ((value << (outBits - bits)) & maxV) throw new Error('Non-zero padding');
+ }
+
+ return result;
+ }
+
+ coinjs.testdeterministicK = function() {
+ // https://github.com/bitpay/bitcore/blob/9a5193d8e94b0bd5b8e7f00038e7c0b935405a03/test/crypto/ecdsa.js
+ // Line 21 and 22 specify digest hash and privkey for the first 2 test vectors.
+ // Line 96-117 tells expected result.
+
+ var tx = coinjs.transaction();
+
+ var test_vectors = [{
+ 'message': 'test data',
+ 'privkey': 'fee0a1f7afebf9d2a5a80c0c98a31c709681cce195cbcd06342b517970c0be1e',
+ 'k_bad00': 'fcce1de7a9bcd6b2d3defade6afa1913fb9229e3b7ddf4749b55c4848b2a196e',
+ 'k_bad01': '727fbcb59eb48b1d7d46f95a04991fc512eb9dbf9105628e3aec87428df28fd8',
+ 'k_bad15': '398f0e2c9f79728f7b3d84d447ac3a86d8b2083c8f234a0ffa9c4043d68bd258'
+ },
+ {
+ 'message': 'Everything should be made as simple as possible, but not simpler.',
+ 'privkey': '0000000000000000000000000000000000000000000000000000000000000001',
+ 'k_bad00': 'ec633bd56a5774a0940cb97e27a9e4e51dc94af737596a0c5cbb3d30332d92a5',
+ 'k_bad01': 'df55b6d1b5c48184622b0ead41a0e02bfa5ac3ebdb4c34701454e80aabf36f56',
+ 'k_bad15': 'def007a9a3c2f7c769c75da9d47f2af84075af95cadd1407393dc1e26086ef87'
+ },
+ {
+ 'message': 'Satoshi Nakamoto',
+ 'privkey': '0000000000000000000000000000000000000000000000000000000000000002',
+ 'k_bad00': 'd3edc1b8224e953f6ee05c8bbf7ae228f461030e47caf97cde91430b4607405e',
+ 'k_bad01': 'f86d8e43c09a6a83953f0ab6d0af59fb7446b4660119902e9967067596b58374',
+ 'k_bad15': '241d1f57d6cfd2f73b1ada7907b199951f95ef5ad362b13aed84009656e0254a'
+ },
+ {
+ 'message': 'Diffie Hellman',
+ 'privkey': '7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f',
+ 'k_bad00': 'c378a41cb17dce12340788dd3503635f54f894c306d52f6e9bc4b8f18d27afcc',
+ 'k_bad01': '90756c96fef41152ac9abe08819c4e95f16da2af472880192c69a2b7bac29114',
+ 'k_bad15': '7b3f53300ab0ccd0f698f4d67db87c44cf3e9e513d9df61137256652b2e94e7c'
+ },
+ {
+ 'message': 'Japan',
+ 'privkey': '8080808080808080808080808080808080808080808080808080808080808080',
+ 'k_bad00': 'f471e61b51d2d8db78f3dae19d973616f57cdc54caaa81c269394b8c34edcf59',
+ 'k_bad01': '6819d85b9730acc876fdf59e162bf309e9f63dd35550edf20869d23c2f3e6d17',
+ 'k_bad15': 'd8e8bae3ee330a198d1f5e00ad7c5f9ed7c24c357c0a004322abca5d9cd17847'
+ },
+ {
+ 'message': 'Bitcoin',
+ 'privkey': 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140',
+ 'k_bad00': '36c848ffb2cbecc5422c33a994955b807665317c1ce2a0f59c689321aaa631cc',
+ 'k_bad01': '4ed8de1ec952a4f5b3bd79d1ff96446bcd45cabb00fc6ca127183e14671bcb85',
+ 'k_bad15': '56b6f47babc1662c011d3b1f93aa51a6e9b5f6512e9f2e16821a238d450a31f8'
+ },
+ {
+ 'message': 'i2FLPP8WEus5WPjpoHwheXOMSobUJVaZM1JPMQZq',
+ 'privkey': 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140',
+ 'k_bad00': '6e9b434fcc6bbb081a0463c094356b47d62d7efae7da9c518ed7bac23f4e2ed6',
+ 'k_bad01': 'ae5323ae338d6117ce8520a43b92eacd2ea1312ae514d53d8e34010154c593bb',
+ 'k_bad15': '3eaa1b61d1b8ab2f1ca71219c399f2b8b3defa624719f1e96fe3957628c2c4ea'
+ },
+ {
+ 'message': 'lEE55EJNP7aLrMtjkeJKKux4Yg0E8E1SAJnWTCEh',
+ 'privkey': '3881e5286abc580bb6139fe8e83d7c8271c6fe5e5c2d640c1f0ed0e1ee37edc9',
+ 'k_bad00': '5b606665a16da29cc1c5411d744ab554640479dd8abd3c04ff23bd6b302e7034',
+ 'k_bad01': 'f8b25263152c042807c992eacd2ac2cc5790d1e9957c394f77ea368e3d9923bd',
+ 'k_bad15': 'ea624578f7e7964ac1d84adb5b5087dd14f0ee78b49072aa19051cc15dab6f33'
+ },
+ {
+ 'message': '2SaVPvhxkAPrayIVKcsoQO5DKA8Uv5X/esZFlf+y',
+ 'privkey': '7259dff07922de7f9c4c5720d68c9745e230b32508c497dd24cb95ef18856631',
+ 'k_bad00': '3ab6c19ab5d3aea6aa0c6da37516b1d6e28e3985019b3adb388714e8f536686b',
+ 'k_bad01': '19af21b05004b0ce9cdca82458a371a9d2cf0dc35a813108c557b551c08eb52e',
+ 'k_bad15': '117a32665fca1b7137a91c4739ac5719fec0cf2e146f40f8e7c21b45a07ebc6a'
+ },
+ {
+ 'message': '00A0OwO2THi7j5Z/jp0FmN6nn7N/DQd6eBnCS+/b',
+ 'privkey': '0d6ea45d62b334777d6995052965c795a4f8506044b4fd7dc59c15656a28f7aa',
+ 'k_bad00': '79487de0c8799158294d94c0eb92ee4b567e4dc7ca18addc86e49d31ce1d2db6',
+ 'k_bad01': '9561d2401164a48a8f600882753b3105ebdd35e2358f4f808c4f549c91490009',
+ 'k_bad15': 'b0d273634129ff4dbdf0df317d4062a1dbc58818f88878ffdb4ec511c77976c0'
+ }
+ ];
+
+ var result_txt = '\n----------------------\nResults\n----------------------\n\n';
+
+ for (i = 0; i < test_vectors.length; i++) {
+ var hash = Crypto.SHA256(test_vectors[i]['message'].split('').map(function(c) {
+ return c.charCodeAt(0);
+ }), {
+ asBytes: true
+ });
+ var wif = coinjs.privkey2wif(test_vectors[i]['privkey']);
+
+ var KBigInt = tx.deterministicK(wif, hash);
+ var KBigInt0 = tx.deterministicK(wif, hash, 0);
+ var KBigInt1 = tx.deterministicK(wif, hash, 1);
+ var KBigInt15 = tx.deterministicK(wif, hash, 15);
+
+ var K = Crypto.util.bytesToHex(KBigInt.toByteArrayUnsigned());
+ var K0 = Crypto.util.bytesToHex(KBigInt0.toByteArrayUnsigned());
+ var K1 = Crypto.util.bytesToHex(KBigInt1.toByteArrayUnsigned());
+ var K15 = Crypto.util.bytesToHex(KBigInt15.toByteArrayUnsigned());
+
+ if (K != test_vectors[i]['k_bad00']) {
+ result_txt += 'Failed Test #' + (i + 1) + '\n K = ' + K + '\nExpected = ' + test_vectors[i]['k_bad00'] + '\n\n';
+ } else if (K0 != test_vectors[i]['k_bad00']) {
+ result_txt += 'Failed Test #' + (i + 1) + '\n K0 = ' + K0 + '\nExpected = ' + test_vectors[i]['k_bad00'] + '\n\n';
+ } else if (K1 != test_vectors[i]['k_bad01']) {
+ result_txt += 'Failed Test #' + (i + 1) + '\n K1 = ' + K1 + '\nExpected = ' + test_vectors[i]['k_bad01'] + '\n\n';
+ } else if (K15 != test_vectors[i]['k_bad15']) {
+ result_txt += 'Failed Test #' + (i + 1) + '\n K15 = ' + K15 + '\nExpected = ' + test_vectors[i]['k_bad15'] + '\n\n';
+ };
+ };
+
+ if (result_txt.length < 60) {
+ result_txt = 'All Tests OK!';
+ };
+
+ return result_txt;
+ };
+
+ /* start of hd functions, thanks bip32.org */
+ coinjs.hd = function(data) {
+
+ var r = {};
+
+ /* some hd value parsing */
+ r.parse = function() {
+
+ var bytes = [];
+
+ // some quick validation
+ if (typeof(data) == 'string') {
+ var decoded = coinjs.base58decode(data);
+ if (decoded.length == 82) {
+ var checksum = decoded.slice(78, 82);
+ var hash = Crypto.SHA256(Crypto.SHA256(decoded.slice(0, 78), {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ if (checksum[0] == hash[0] && checksum[1] == hash[1] && checksum[2] == hash[2] && checksum[3] == hash[3]) {
+ bytes = decoded.slice(0, 78);
+ }
+ }
+ }
+
+ // actual parsing code
+ if (bytes && bytes.length > 0) {
+ r.version = coinjs.uint(bytes.slice(0, 4), 4);
+ r.depth = coinjs.uint(bytes.slice(4, 5), 1);
+ r.parent_fingerprint = bytes.slice(5, 9);
+ r.child_index = coinjs.uint(bytes.slice(9, 13), 4);
+ r.chain_code = bytes.slice(13, 45);
+ r.key_bytes = bytes.slice(45, 78);
+
+ var c = coinjs.compressed; // get current default
+ coinjs.compressed = true;
+
+ if (r.key_bytes[0] == 0x00) {
+ r.type = 'private';
+ var privkey = (r.key_bytes).slice(1, 33);
+ var privkeyHex = Crypto.util.bytesToHex(privkey);
+ var pubkey = coinjs.newPubkey(privkeyHex);
+
+ r.keys = {
+ 'privkey': privkeyHex,
+ 'pubkey': pubkey,
+ 'address': coinjs.pubkey2address(pubkey),
+ 'wif': coinjs.privkey2wif(privkeyHex)
+ };
+
+ } else if (r.key_bytes[0] == 0x02 || r.key_bytes[0] == 0x03) {
+ r.type = 'public';
+ var pubkeyHex = Crypto.util.bytesToHex(r.key_bytes);
+
+ r.keys = {
+ 'pubkey': pubkeyHex,
+ 'address': coinjs.pubkey2address(pubkeyHex)
+ };
+ } else {
+ r.type = 'invalid';
+ }
+
+ r.keys_extended = r.extend();
+
+ coinjs.compressed = c; // reset to default
+ }
+
+ return r;
+ }
+
+ // extend prv/pub key
+ r.extend = function() {
+ var hd = coinjs.hd();
+ return hd.make({
+ 'depth': (this.depth * 1) + 1,
+ 'parent_fingerprint': this.parent_fingerprint,
+ 'child_index': this.child_index,
+ 'chain_code': this.chain_code,
+ 'privkey': this.keys.privkey,
+ 'pubkey': this.keys.pubkey
+ });
+ }
+
+ // derive from path
+ r.derive_path = function(path) {
+
+ if (path == 'm' || path == 'M' || path == 'm\'' || path == 'M\'') return this;
+
+ var p = path.split('/');
+ var hdp = coinjs.clone(this); // clone hd path
+
+ for (var i in p) {
+
+ if (((i == 0) && c != 'm') || i == 'remove') {
+ continue;
+ }
+
+ var c = p[i];
+
+ var use_private = (c.length > 1) && (c[c.length - 1] == '\'');
+ var child_index = parseInt(use_private ? c.slice(0, c.length - 1) : c) & 0x7fffffff;
+ if (use_private)
+ child_index += 0x80000000;
+
+ hdp = hdp.derive(child_index);
+ var key = ((hdp.keys_extended.privkey) && hdp.keys_extended.privkey != '') ? hdp.keys_extended.privkey : hdp.keys_extended.pubkey;
+ hdp = coinjs.hd(key);
+ }
+ return hdp;
+ }
+
+ // derive key from index
+ r.derive = function(i) {
+
+ i = (i) ? i : 0;
+ var blob = (Crypto.util.hexToBytes(this.keys.pubkey)).concat(coinjs.numToBytes(i, 4).reverse());
+
+ var j = new jsSHA(Crypto.util.bytesToHex(blob), 'HEX');
+ var hash = j.getHMAC(Crypto.util.bytesToHex(r.chain_code), "HEX", "SHA-512", "HEX");
+
+ var il = new BigInteger(hash.slice(0, 64), 16);
+ var ir = Crypto.util.hexToBytes(hash.slice(64, 128));
+
+ var ecparams = EllipticCurve.getSECCurveByName("secp256k1");
+ var curve = ecparams.getCurve();
+
+ var k, key, pubkey, o;
+
+ o = coinjs.clone(this);
+ o.chain_code = ir;
+ o.child_index = i;
+
+ if (this.type == 'private') {
+ // derive key pair from from a xprv key
+ k = il.add(new BigInteger([0].concat(Crypto.util.hexToBytes(this.keys.privkey)))).mod(ecparams.getN());
+ key = Crypto.util.bytesToHex(k.toByteArrayUnsigned());
+
+ pubkey = coinjs.newPubkey(key);
+
+ o.keys = {
+ 'privkey': key,
+ 'pubkey': pubkey,
+ 'wif': coinjs.privkey2wif(key),
+ 'address': coinjs.pubkey2address(pubkey)
+ };
+
+ } else if (this.type == 'public') {
+ // derive xpub key from an xpub key
+ q = ecparams.curve.decodePointHex(this.keys.pubkey);
+ var curvePt = ecparams.getG().multiply(il).add(q);
+
+ var x = curvePt.getX().toBigInteger();
+ var y = curvePt.getY().toBigInteger();
+
+ var publicKeyBytesCompressed = EllipticCurve.integerToBytes(x, 32)
+ if (y.isEven()) {
+ publicKeyBytesCompressed.unshift(0x02)
+ } else {
+ publicKeyBytesCompressed.unshift(0x03)
+ }
+ pubkey = Crypto.util.bytesToHex(publicKeyBytesCompressed);
+
+ o.keys = {
+ 'pubkey': pubkey,
+ 'address': coinjs.pubkey2address(pubkey)
+ }
+ } else {
+ // fail
+ }
+
+ o.parent_fingerprint = (ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(r.keys.pubkey), {
+ asBytes: true
+ }), {
+ asBytes: true
+ })).slice(0, 4);
+ o.keys_extended = o.extend();
+ return o;
+ }
+
+ // make a master hd xprv/xpub
+ r.master = function(pass) {
+ var seed = (pass) ? Crypto.SHA256(pass) : coinjs.newPrivkey();
+ var hasher = new jsSHA(seed, 'HEX');
+ var I = hasher.getHMAC("Bitcoin seed", "TEXT", "SHA-512", "HEX");
+
+ var privkey = Crypto.util.hexToBytes(I.slice(0, 64));
+ var chain = Crypto.util.hexToBytes(I.slice(64, 128));
+
+ var hd = coinjs.hd();
+ return hd.make({
+ 'depth': 0,
+ 'parent_fingerprint': [0, 0, 0, 0],
+ 'child_index': 0,
+ 'chain_code': chain,
+ 'privkey': I.slice(0, 64),
+ 'pubkey': coinjs.newPubkey(I.slice(0, 64))
+ });
+ }
+
+ // encode data to a base58 string
+ r.make = function(data) { // { (int) depth, (array) parent_fingerprint, (int) child_index, (byte array) chain_code, (hex str) privkey, (hex str) pubkey}
+ var k = [];
+
+ //depth
+ k.push(data.depth * 1);
+
+ //parent fingerprint
+ k = k.concat(data.parent_fingerprint);
+
+ //child index
+ k = k.concat((coinjs.numToBytes(data.child_index, 4)).reverse());
+
+ //Chain code
+ k = k.concat(data.chain_code);
+
+ var o = {}; // results
+
+ //encode xprv key
+ if (data.privkey) {
+ var prv = (coinjs.numToBytes(coinjs.hdkey.prv, 4)).reverse();
+ prv = prv.concat(k);
+ prv.push(0x00);
+ prv = prv.concat(Crypto.util.hexToBytes(data.privkey));
+ var hash = Crypto.SHA256(Crypto.SHA256(prv, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ var checksum = hash.slice(0, 4);
+ var ret = prv.concat(checksum);
+ o.privkey = coinjs.base58encode(ret);
+ }
+
+ //encode xpub key
+ if (data.pubkey) {
+ var pub = (coinjs.numToBytes(coinjs.hdkey.pub, 4)).reverse();
+ pub = pub.concat(k);
+ pub = pub.concat(Crypto.util.hexToBytes(data.pubkey));
+ var hash = Crypto.SHA256(Crypto.SHA256(pub, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ var checksum = hash.slice(0, 4);
+ var ret = pub.concat(checksum);
+ o.pubkey = coinjs.base58encode(ret);
+ }
+ return o;
+ }
+
+ return r.parse();
+ }
+
+
+ /* start of script functions */
+ coinjs.script = function(data) {
+ var r = {};
+
+ if (!data) {
+ r.buffer = [];
+ } else if ("string" == typeof data) {
+ r.buffer = Crypto.util.hexToBytes(data);
+ } else if (coinjs.isArray(data)) {
+ r.buffer = data;
+ } else if (data instanceof coinjs.script) {
+ r.buffer = data.buffer;
+ } else {
+ r.buffer = data;
+ }
+
+ /* parse buffer array */
+ r.parse = function() {
+
+ var self = this;
+ r.chunks = [];
+ var i = 0;
+
+ function readChunk(n) {
+ self.chunks.push(self.buffer.slice(i, i + n));
+ i += n;
+ };
+
+ while (i < this.buffer.length) {
+ var opcode = this.buffer[i++];
+ if (opcode >= 0xF0) {
+ opcode = (opcode << 8) | this.buffer[i++];
+ }
+
+ var len;
+ if (opcode > 0 && opcode < 76) { //OP_PUSHDATA1
+ readChunk(opcode);
+ } else if (opcode == 76) { //OP_PUSHDATA1
+ len = this.buffer[i++];
+ readChunk(len);
+ } else if (opcode == 77) { //OP_PUSHDATA2
+ len = (this.buffer[i++] << 8) | this.buffer[i++];
+ readChunk(len);
+ } else if (opcode == 78) { //OP_PUSHDATA4
+ len = (this.buffer[i++] << 24) | (this.buffer[i++] << 16) | (this.buffer[i++] << 8) | this.buffer[i++];
+ readChunk(len);
+ } else {
+ this.chunks.push(opcode);
+ }
+
+ if (i < 0x00) {
+ break;
+ }
+ }
+
+ return true;
+ };
+
+ /* decode the redeemscript of a multisignature transaction */
+ r.decodeRedeemScript = function(script) {
+ var r = false;
+ try {
+ var s = coinjs.script(Crypto.util.hexToBytes(script));
+ if ((s.chunks.length >= 3) && s.chunks[s.chunks.length - 1] == 174) { //OP_CHECKMULTISIG
+ r = {};
+ r.signaturesRequired = s.chunks[0] - 80;
+ var pubkeys = [];
+ for (var i = 1; i < s.chunks.length - 2; i++) {
+ pubkeys.push(Crypto.util.bytesToHex(s.chunks[i]));
+ }
+ r.pubkeys = pubkeys;
+ var multi = coinjs.pubkeys2MultisigAddress(pubkeys, r.signaturesRequired);
+ r.address = multi['address'];
+ r.type = 'multisig__'; // using __ for now to differentiat from the other object .type == "multisig"
+ var rs = Crypto.util.bytesToHex(s.buffer);
+ r.redeemscript = rs;
+
+ } else if ((s.chunks.length == 2) && (s.buffer[0] == 0 && s.buffer[1] == 20)) { // SEGWIT
+ r = {};
+ r.type = "segwit__";
+ var rs = Crypto.util.bytesToHex(s.buffer);
+ r.address = coinjs.pubkey2address(rs, coinjs.multisig);
+ r.redeemscript = rs;
+
+ } else if (s.chunks.length == 5 && s.chunks[1] == 177 && s.chunks[2] == 117 && s.chunks[4] == 172) {
+ // ^
OP_CHECKLOCKTIMEVERIFY OP_DROP OP_CHECKSIG ^
+ r = {}
+ r.pubkey = Crypto.util.bytesToHex(s.chunks[3]);
+ r.checklocktimeverify = coinjs.bytesToNum(s.chunks[0].slice());
+ r.address = coinjs.simpleHodlAddress(r.pubkey, r.checklocktimeverify).address;
+ var rs = Crypto.util.bytesToHex(s.buffer);
+ r.redeemscript = rs;
+ r.type = "hodl__";
+ }
+ } catch (e) {
+ // console.log(e);
+ r = false;
+ }
+ return r;
+ }
+
+ /* create output script to spend */
+ r.spendToScript = function(address) {
+ var addr = coinjs.addressDecode(address);
+ var s = coinjs.script();
+ if (addr.type == "bech32") {
+ s.writeOp(0);
+ s.writeBytes(Crypto.util.hexToBytes(addr.redeemscript));
+ } else if (addr.version == coinjs.multisig) { // multisig address
+ s.writeOp(169); //OP_HASH160
+ s.writeBytes(addr.bytes);
+ s.writeOp(135); //OP_EQUAL
+ } else { // regular address
+ s.writeOp(118); //OP_DUP
+ s.writeOp(169); //OP_HASH160
+ s.writeBytes(addr.bytes);
+ s.writeOp(136); //OP_EQUALVERIFY
+ s.writeOp(172); //OP_CHECKSIG
+ }
+ return s;
+ }
+
+ /* geneate a (script) pubkey hash of the address - used for when signing */
+ r.pubkeyHash = function(address) {
+ var addr = coinjs.addressDecode(address);
+ var s = coinjs.script();
+ s.writeOp(118); //OP_DUP
+ s.writeOp(169); //OP_HASH160
+ s.writeBytes(addr.bytes);
+ s.writeOp(136); //OP_EQUALVERIFY
+ s.writeOp(172); //OP_CHECKSIG
+ return s;
+ }
+
+ /* write to buffer */
+ r.writeOp = function(op) {
+ this.buffer.push(op);
+ this.chunks.push(op);
+ return true;
+ }
+
+ /* write bytes to buffer */
+ r.writeBytes = function(data) {
+ if (data.length < 76) { //OP_PUSHDATA1
+ this.buffer.push(data.length);
+ } else if (data.length <= 0xff) {
+ this.buffer.push(76); //OP_PUSHDATA1
+ this.buffer.push(data.length);
+ } else if (data.length <= 0xffff) {
+ this.buffer.push(77); //OP_PUSHDATA2
+ this.buffer.push(data.length & 0xff);
+ this.buffer.push((data.length >>> 8) & 0xff);
+ } else {
+ this.buffer.push(78); //OP_PUSHDATA4
+ this.buffer.push(data.length & 0xff);
+ this.buffer.push((data.length >>> 8) & 0xff);
+ this.buffer.push((data.length >>> 16) & 0xff);
+ this.buffer.push((data.length >>> 24) & 0xff);
+ }
+ this.buffer = this.buffer.concat(data);
+ this.chunks.push(data);
+ return true;
+ }
+
+ r.parse();
+ return r;
+ }
+
+ /* start of transaction functions */
+
+ /* create a new transaction object */
+ coinjs.transaction = function() {
+
+ var r = {};
+ r.version = 1;
+ r.lock_time = 0;
+ r.ins = [];
+ r.outs = [];
+ r.witness = false;
+ r.timestamp = null;
+ r.block = null;
+
+ /* add an input to a transaction */
+ r.addinput = function(txid, index, script, sequence) {
+ var o = {};
+ o.outpoint = {
+ 'hash': txid,
+ 'index': index
+ };
+ o.script = coinjs.script(script || []);
+ o.sequence = sequence || ((r.lock_time == 0) ? 4294967295 : 0);
+ return this.ins.push(o);
+ }
+
+ /* add an output to a transaction */
+ r.addoutput = function(address, value) {
+ var o = {};
+ o.value = new BigInteger('' + Math.round((value * 1) * 1e8), 10);
+ var s = coinjs.script();
+ o.script = s.spendToScript(address);
+
+ return this.outs.push(o);
+ }
+
+ /* add two outputs for stealth addresses to a transaction */
+ r.addstealth = function(stealth, value) {
+ var ephemeralKeyBigInt = BigInteger.fromByteArrayUnsigned(Crypto.util.hexToBytes(coinjs.newPrivkey()));
+ var curve = EllipticCurve.getSECCurveByName("secp256k1");
+
+ var p = EllipticCurve.fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F");
+ var a = BigInteger.ZERO;
+ var b = EllipticCurve.fromHex("7");
+ var calccurve = new EllipticCurve.CurveFp(p, a, b);
+
+ var ephemeralPt = curve.getG().multiply(ephemeralKeyBigInt);
+ var scanPt = calccurve.decodePointHex(stealth.scankey);
+ var sharedPt = scanPt.multiply(ephemeralKeyBigInt);
+ var stealthindexKeyBigInt = BigInteger.fromByteArrayUnsigned(Crypto.SHA256(sharedPt.getEncoded(true), {
+ asBytes: true
+ }));
+
+ var stealthindexPt = curve.getG().multiply(stealthindexKeyBigInt);
+ var spendPt = calccurve.decodePointHex(stealth.spendkey);
+ var addressPt = spendPt.add(stealthindexPt);
+
+ var sendaddress = coinjs.pubkey2address(Crypto.util.bytesToHex(addressPt.getEncoded(true)));
+
+
+ var OPRETBytes = [6].concat(Crypto.util.randomBytes(4)).concat(ephemeralPt.getEncoded(true)); // ephemkey data
+ var q = coinjs.script();
+ q.writeOp(106); // OP_RETURN
+ q.writeBytes(OPRETBytes);
+ v = {};
+ v.value = 0;
+ v.script = q;
+
+ this.outs.push(v);
+
+ var o = {};
+ o.value = new BigInteger('' + Math.round((value * 1) * 1e8), 10);
+ var s = coinjs.script();
+ o.script = s.spendToScript(sendaddress);
+
+ return this.outs.push(o);
+ }
+
+ /* add data to a transaction */
+ r.adddata = function(data) {
+ var r = false;
+ if (((data.match(/^[a-f0-9]+$/gi)) && data.length < 160) && (data.length % 2) == 0) {
+ var s = coinjs.script();
+ s.writeOp(106); // OP_RETURN
+ s.writeBytes(Crypto.util.hexToBytes(data));
+ o = {};
+ o.value = 0;
+ o.script = s;
+ return this.outs.push(o);
+ }
+ return r;
+ }
+
+ /* list unspent transactions */
+ r.listUnspent = function(address, callback) {
+ coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=addresses&request=unspent&address=' + address + '&r=' + securedMathRandom(), callback, "GET");
+ }
+
+ /* list transaction data */
+ r.getTransaction = function(txid, callback) {
+ coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=bitcoin&request=gettransaction&txid=' + txid + '&r=' + securedMathRandom(), callback, "GET");
+ }
+
+ /* add unspent to transaction */
+ r.addUnspent = function(address, callback, script, segwit, sequence) {
+ var self = this;
+ this.listUnspent(address, function(data) {
+ var s = coinjs.script();
+ var value = 0;
+ var total = 0;
+ var x = {};
+
+ if (GLOBAL.DOMParser) {
+ parser = new DOMParser();
+ xmlDoc = parser.parseFromString(data, "text/xml");
+ } else {
+ xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
+ xmlDoc.async = false;
+ xmlDoc.loadXML(data);
+ }
+
+ var unspent = xmlDoc.getElementsByTagName("unspent")[0];
+
+ if (unspent) {
+ for (i = 1; i <= unspent.childElementCount; i++) {
+ var u = xmlDoc.getElementsByTagName("unspent_" + i)[0]
+ var txhash = (u.getElementsByTagName("tx_hash")[0].childNodes[0].nodeValue).match(/.{1,2}/g).reverse().join("") + '';
+ var n = u.getElementsByTagName("tx_output_n")[0].childNodes[0].nodeValue;
+ var scr = script || u.getElementsByTagName("script")[0].childNodes[0].nodeValue;
+
+ if (segwit) {
+ /* this is a small hack to include the value with the redeemscript to make the signing procedure smoother.
+ It is not standard and removed during the signing procedure. */
+
+ s = coinjs.script();
+ s.writeBytes(Crypto.util.hexToBytes(script));
+ s.writeOp(0);
+ s.writeBytes(coinjs.numToBytes(u.getElementsByTagName("value")[0].childNodes[0].nodeValue * 1, 8));
+ scr = Crypto.util.bytesToHex(s.buffer);
+ }
+
+ var seq = sequence || false;
+ self.addinput(txhash, n, scr, seq);
+ value += u.getElementsByTagName("value")[0].childNodes[0].nodeValue * 1;
+ total++;
+ }
+ }
+
+ x.result = xmlDoc.getElementsByTagName("result")[0].childNodes[0].nodeValue;
+ x.unspent = unspent;
+ x.value = value;
+ x.total = total;
+ x.response = xmlDoc.getElementsByTagName("response")[0].childNodes[0].nodeValue;
+
+ return callback(x);
+ });
+ }
+
+ /* add unspent and sign */
+ r.addUnspentAndSign = function(wif, callback) {
+ var self = this;
+ var address = coinjs.wif2address(wif);
+ self.addUnspent(address['address'], function(data) {
+ self.sign(wif);
+ return callback(data);
+ });
+ }
+
+ /* broadcast a transaction */
+ r.broadcast = function(callback, txhex) {
+ var tx = txhex || this.serialize();
+ coinjs.ajax(coinjs.host + '?uid=' + coinjs.uid + '&key=' + coinjs.key + '&setmodule=bitcoin&request=sendrawtransaction', callback, "POST", ["rawtx=" + tx]);
+ }
+
+ /* generate the transaction hash to sign from a transaction input */
+ r.transactionHash = function(index, sigHashType) {
+
+ var clone = coinjs.clone(this);
+ var shType = sigHashType || 1;
+
+ /* black out all other ins, except this one */
+ for (var i = 0; i < clone.ins.length; i++) {
+ if (index != i) {
+ clone.ins[i].script = coinjs.script();
+ }
+ }
+
+ var extract = this.extractScriptKey(index);
+ clone.ins[index].script = coinjs.script(extract['script']);
+
+ if ((clone.ins) && clone.ins[index]) {
+
+ /* SIGHASH : For more info on sig hashs see https://en.bitcoin.it/wiki/OP_CHECKSIG
+ and https://bitcoin.org/en/developer-guide#signature-hash-type */
+
+ if (shType == 1) {
+ //SIGHASH_ALL 0x01
+
+ } else if (shType == 2) {
+ //SIGHASH_NONE 0x02
+ clone.outs = [];
+ for (var i = 0; i < clone.ins.length; i++) {
+ if (index != i) {
+ clone.ins[i].sequence = 0;
+ }
+ }
+
+ } else if (shType == 3) {
+
+ //SIGHASH_SINGLE 0x03
+ clone.outs.length = index + 1;
+
+ for (var i = 0; i < index; i++) {
+ clone.outs[i].value = -1;
+ clone.outs[i].script.buffer = [];
+ }
+
+ for (var i = 0; i < clone.ins.length; i++) {
+ if (index != i) {
+ clone.ins[i].sequence = 0;
+ }
+ }
+
+ } else if (shType >= 128) {
+ //SIGHASH_ANYONECANPAY 0x80
+ clone.ins = [clone.ins[index]];
+
+ if (shType == 129) {
+ // SIGHASH_ALL + SIGHASH_ANYONECANPAY
+
+ } else if (shType == 130) {
+ // SIGHASH_NONE + SIGHASH_ANYONECANPAY
+ clone.outs = [];
+
+ } else if (shType == 131) {
+ // SIGHASH_SINGLE + SIGHASH_ANYONECANPAY
+ clone.outs.length = index + 1;
+ for (var i = 0; i < index; i++) {
+ clone.outs[i].value = -1;
+ clone.outs[i].script.buffer = [];
+ }
+ }
+ }
+
+ var buffer = Crypto.util.hexToBytes(clone.serialize());
+ buffer = buffer.concat(coinjs.numToBytes(parseInt(shType), 4));
+ var hash = Crypto.SHA256(buffer, {
+ asBytes: true
+ });
+ var r = Crypto.util.bytesToHex(Crypto.SHA256(hash, {
+ asBytes: true
+ }));
+ return r;
+ } else {
+ return false;
+ }
+ }
+
+ /* generate a segwit transaction hash to sign from a transaction input */
+ r.transactionHashSegWitV0 = function(index, sigHashType) {
+ /*
+ Notice: coinb.in by default, deals with segwit transactions in a non-standard way.
+ Segwit transactions require that input values are included in the transaction hash.
+ To save wasting resources and potentially slowing down this service, we include the amount with the
+ redeem script to generate the transaction hash and remove it after its signed.
+ */
+
+ // start redeem script check
+ var extract = this.extractScriptKey(index);
+ if (extract['type'] != 'segwit') {
+ return {
+ 'result': 0,
+ 'fail': 'redeemscript',
+ 'response': 'redeemscript missing or not valid for segwit'
+ };
+ }
+
+ if (extract['value'] == -1) {
+ return {
+ 'result': 0,
+ 'fail': 'value',
+ 'response': 'unable to generate a valid segwit hash without a value'
+ };
+ }
+
+ var scriptcode = Crypto.util.hexToBytes(extract['script']);
+
+ // end of redeem script check
+
+ /* P2WPKH */
+ if (scriptcode.length == 20) {
+ scriptcode = [0x00, 0x14].concat(scriptcode);
+ }
+
+ if (scriptcode.length == 22) {
+ scriptcode = scriptcode.slice(1);
+ scriptcode.unshift(25, 118, 169);
+ scriptcode.push(136, 172);
+ }
+
+ var value = coinjs.numToBytes(extract['value'], 8);
+
+ // start
+
+ var zero = coinjs.numToBytes(0, 32);
+ var version = coinjs.numToBytes(parseInt(this.version), 4);
+
+ var bufferTmp = [];
+ if (!(sigHashType >= 80)) { // not sighash anyonecanpay
+ for (var i = 0; i < this.ins.length; i++) {
+ bufferTmp = bufferTmp.concat(Crypto.util.hexToBytes(this.ins[i].outpoint.hash).reverse());
+ bufferTmp = bufferTmp.concat(coinjs.numToBytes(this.ins[i].outpoint.index, 4));
+ }
+ }
+ var hashPrevouts = bufferTmp.length >= 1 ? Crypto.SHA256(Crypto.SHA256(bufferTmp, {
+ asBytes: true
+ }), {
+ asBytes: true
+ }) : zero;
+
+ var bufferTmp = [];
+ if (!(sigHashType >= 80) && sigHashType != 2 && sigHashType != 3) { // not sighash anyonecanpay & single & none
+ for (var i = 0; i < this.ins.length; i++) {
+ bufferTmp = bufferTmp.concat(coinjs.numToBytes(this.ins[i].sequence, 4));
+ }
+ }
+ var hashSequence = bufferTmp.length >= 1 ? Crypto.SHA256(Crypto.SHA256(bufferTmp, {
+ asBytes: true
+ }), {
+ asBytes: true
+ }) : zero;
+
+ var outpoint = Crypto.util.hexToBytes(this.ins[index].outpoint.hash).reverse();
+ outpoint = outpoint.concat(coinjs.numToBytes(this.ins[index].outpoint.index, 4));
+
+ var nsequence = coinjs.numToBytes(this.ins[index].sequence, 4);
+ var hashOutputs = zero;
+ var bufferTmp = [];
+ if (sigHashType != 2 && sigHashType != 3) { // not sighash single & none
+ for (var i = 0; i < this.outs.length; i++) {
+ bufferTmp = bufferTmp.concat(coinjs.numToBytes(this.outs[i].value, 8));
+ bufferTmp = bufferTmp.concat(coinjs.numToVarInt(this.outs[i].script.buffer.length));
+ bufferTmp = bufferTmp.concat(this.outs[i].script.buffer);
+ }
+ hashOutputs = Crypto.SHA256(Crypto.SHA256(bufferTmp, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+
+ } else if ((sigHashType == 2) && index < this.outs.length) { // is sighash single
+ bufferTmp = bufferTmp.concat(coinjs.numToBytes(this.outs[index].value, 8));
+ bufferTmp = bufferTmp.concat(coinjs.numToVarInt(this.outs[i].script.buffer.length));
+ bufferTmp = bufferTmp.concat(this.outs[index].script.buffer);
+ hashOutputs = Crypto.SHA256(Crypto.SHA256(bufferTmp, {
+ asBytes: true
+ }), {
+ asBytes: true
+ });
+ }
+
+ var locktime = coinjs.numToBytes(this.lock_time, 4);
+ var sighash = coinjs.numToBytes(sigHashType, 4);
+
+ var buffer = [];
+ buffer = buffer.concat(version);
+ buffer = buffer.concat(hashPrevouts);
+ buffer = buffer.concat(hashSequence);
+ buffer = buffer.concat(outpoint);
+ buffer = buffer.concat(scriptcode);
+ buffer = buffer.concat(value);
+ buffer = buffer.concat(nsequence);
+ buffer = buffer.concat(hashOutputs);
+ buffer = buffer.concat(locktime);
+ buffer = buffer.concat(sighash);
+
+ var hash = Crypto.SHA256(buffer, {
+ asBytes: true
+ });
+ return {
+ 'result': 1,
+ 'hash': Crypto.util.bytesToHex(Crypto.SHA256(hash, {
+ asBytes: true
+ })),
+ 'response': 'hash generated'
+ };
+ }
+
+ /* extract the scriptSig, used in the transactionHash() function */
+ r.extractScriptKey = function(index) {
+ if (this.ins[index]) {
+ if ((this.ins[index].script.chunks.length == 5) && this.ins[index].script.chunks[4] == 172 && coinjs.isArray(this.ins[index].script.chunks[2])) { //OP_CHECKSIG
+ // regular scriptPubkey (not signed)
+ return {
+ 'type': 'scriptpubkey',
+ 'signed': 'false',
+ 'signatures': 0,
+ 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer)
+ };
+ } else if ((this.ins[index].script.chunks.length == 2) && this.ins[index].script.chunks[0][0] == 48 && this.ins[index].script.chunks[1].length == 5 && this.ins[index].script.chunks[1][1] == 177) { //OP_CHECKLOCKTIMEVERIFY
+ // hodl script (signed)
+ return {
+ 'type': 'hodl',
+ 'signed': 'true',
+ 'signatures': 1,
+ 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer)
+ };
+ } else if ((this.ins[index].script.chunks.length == 2) && this.ins[index].script.chunks[0][0] == 48) {
+ // regular scriptPubkey (probably signed)
+ return {
+ 'type': 'scriptpubkey',
+ 'signed': 'true',
+ 'signatures': 1,
+ 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer)
+ };
+ } else if (this.ins[index].script.chunks.length == 5 && this.ins[index].script.chunks[1] == 177) { //OP_CHECKLOCKTIMEVERIFY
+ // hodl script (not signed)
+ return {
+ 'type': 'hodl',
+ 'signed': 'false',
+ 'signatures': 0,
+ 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer)
+ };
+ } else if ((this.ins[index].script.chunks.length <= 3 && this.ins[index].script.chunks.length > 0) && ((this.ins[index].script.chunks[0].length == 22 && this.ins[index].script.chunks[0][0] == 0) || (this.ins[index].script.chunks[0].length == 20 && this.ins[index].script.chunks[1] == 0))) {
+ var signed = ((this.witness[index]) && this.witness[index].length == 2) ? 'true' : 'false';
+ var sigs = (signed == 'true') ? 1 : 0;
+ var value = -1; // no value found
+ if ((this.ins[index].script.chunks[2]) && this.ins[index].script.chunks[2].length == 8) {
+ value = coinjs.bytesToNum(this.ins[index].script.chunks[2]); // value found encoded in transaction (THIS IS NON STANDARD)
+ }
+ return {
+ 'type': 'segwit',
+ 'signed': signed,
+ 'signatures': sigs,
+ 'script': Crypto.util.bytesToHex(this.ins[index].script.chunks[0]),
+ 'value': value
+ };
+ } else if (this.ins[index].script.chunks[0] == 0 && this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1][this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1].length - 1] == 174) { // OP_CHECKMULTISIG
+ // multisig script, with signature(s) included
+ var sigcount = 0;
+ for (let i = 1; i < this.ins[index].script.chunks.length - 1; i++) {
+ if (this.ins[index].script.chunks[i] != 0) {
+ sigcount++;
+ }
+ }
+
+ return {
+ 'type': 'multisig',
+ 'signed': 'true',
+ 'signatures': sigcount,
+ 'script': Crypto.util.bytesToHex(this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1])
+ };
+ } else if (this.ins[index].script.chunks[0] >= 80 && this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1] == 174) { // OP_CHECKMULTISIG
+ // multisig script, without signature!
+ return {
+ 'type': 'multisig',
+ 'signed': 'false',
+ 'signatures': 0,
+ 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer)
+ };
+ } else if (this.ins[index].script.chunks.length == 0) {
+ // empty
+ //bech32 witness check
+ var signed = ((this.witness[index]) && this.witness[index].length == 2) ? 'true' : 'false';
+ var sigs = (signed == 'true') ? 1 : 0;
+ return {
+ 'type': 'empty',
+ 'signed': signed,
+ 'signatures': sigs,
+ 'script': ''
+ };
+ } else {
+ // something else
+ return {
+ 'type': 'unknown',
+ 'signed': 'false',
+ 'signatures': 0,
+ 'script': Crypto.util.bytesToHex(this.ins[index].script.buffer)
+ };
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /* generate a signature from a transaction hash */
+ r.transactionSig = function(index, wif, sigHashType, txhash) {
+
+ function serializeSig(r, s) {
+ var rBa = r.toByteArraySigned();
+ var sBa = s.toByteArraySigned();
+
+ var sequence = [];
+ sequence.push(0x02); // INTEGER
+ sequence.push(rBa.length);
+ sequence = sequence.concat(rBa);
+
+ sequence.push(0x02); // INTEGER
+ sequence.push(sBa.length);
+ sequence = sequence.concat(sBa);
+
+ sequence.unshift(sequence.length);
+ sequence.unshift(0x30); // SEQUENCE
+
+ return sequence;
+ }
+
+ var shType = sigHashType || 1;
+ var hash = txhash || Crypto.util.hexToBytes(this.transactionHash(index, shType));
+
+ if (hash) {
+ var curve = EllipticCurve.getSECCurveByName("secp256k1");
+ var key = coinjs.wif2privkey(wif);
+ var priv = BigInteger.fromByteArrayUnsigned(Crypto.util.hexToBytes(key['privkey']));
+ var n = curve.getN();
+ var e = BigInteger.fromByteArrayUnsigned(hash);
+ var badrs = 0
+ do {
+ var k = this.deterministicK(wif, hash, badrs);
+ var G = curve.getG();
+ var Q = G.multiply(k);
+ var r = Q.getX().toBigInteger().mod(n);
+ var s = k.modInverse(n).multiply(e.add(priv.multiply(r))).mod(n);
+ badrs++
+ } while (r.compareTo(BigInteger.ZERO) <= 0 || s.compareTo(BigInteger.ZERO) <= 0);
+
+ // Force lower s values per BIP62
+ var halfn = n.shiftRight(1);
+ if (s.compareTo(halfn) > 0) {
+ s = n.subtract(s);
+ };
+
+ var sig = serializeSig(r, s);
+ sig.push(parseInt(shType, 10));
+
+ return Crypto.util.bytesToHex(sig);
+ } else {
+ return false;
+ }
+ }
+
+ // https://tools.ietf.org/html/rfc6979#section-3.2
+ r.deterministicK = function(wif, hash, badrs) {
+ // if r or s were invalid when this function was used in signing,
+ // we do not want to actually compute r, s here for efficiency, so,
+ // we can increment badrs. explained at end of RFC 6979 section 3.2
+
+ // wif is b58check encoded wif privkey.
+ // hash is byte array of transaction digest.
+ // badrs is used only if the k resulted in bad r or s.
+
+ // some necessary things out of the way for clarity.
+ badrs = badrs || 0;
+ var key = coinjs.wif2privkey(wif);
+ var x = Crypto.util.hexToBytes(key['privkey'])
+ var curve = EllipticCurve.getSECCurveByName("secp256k1");
+ var N = curve.getN();
+
+ // Step: a
+ // hash is a byteArray of the message digest. so h1 == hash in our case
+
+ // Step: b
+ var v = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
+
+ // Step: c
+ var k = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+
+ // Step: d
+ k = Crypto.HMAC(Crypto.SHA256, v.concat([0]).concat(x).concat(hash), k, {
+ asBytes: true
+ });
+
+ // Step: e
+ v = Crypto.HMAC(Crypto.SHA256, v, k, {
+ asBytes: true
+ });
+
+ // Step: f
+ k = Crypto.HMAC(Crypto.SHA256, v.concat([1]).concat(x).concat(hash), k, {
+ asBytes: true
+ });
+
+ // Step: g
+ v = Crypto.HMAC(Crypto.SHA256, v, k, {
+ asBytes: true
+ });
+
+ // Step: h1
+ var T = [];
+
+ // Step: h2 (since we know tlen = qlen, just copy v to T.)
+ v = Crypto.HMAC(Crypto.SHA256, v, k, {
+ asBytes: true
+ });
+ T = v;
+
+ // Step: h3
+ var KBigInt = BigInteger.fromByteArrayUnsigned(T);
+
+ // loop if KBigInt is not in the range of [1, N-1] or if badrs needs incrementing.
+ var i = 0
+ while (KBigInt.compareTo(N) >= 0 || KBigInt.compareTo(BigInteger.ZERO) <= 0 || i < badrs) {
+ k = Crypto.HMAC(Crypto.SHA256, v.concat([0]), k, {
+ asBytes: true
+ });
+ v = Crypto.HMAC(Crypto.SHA256, v, k, {
+ asBytes: true
+ });
+ v = Crypto.HMAC(Crypto.SHA256, v, k, {
+ asBytes: true
+ });
+ T = v;
+ KBigInt = BigInteger.fromByteArrayUnsigned(T);
+ i++
+ };
+
+ return KBigInt;
+ };
+
+ /* sign a "standard" input */
+ r.signinput = function(index, wif, sigHashType) {
+ var key = coinjs.wif2pubkey(wif);
+ var shType = sigHashType || 1;
+ var signature = this.transactionSig(index, wif, shType);
+ var s = coinjs.script();
+ s.writeBytes(Crypto.util.hexToBytes(signature));
+ s.writeBytes(Crypto.util.hexToBytes(key['pubkey']));
+ this.ins[index].script = s;
+ return true;
+ }
+
+ /* signs a time locked / hodl input */
+ r.signhodl = function(index, wif, sigHashType) {
+ var shType = sigHashType || 1;
+ var signature = this.transactionSig(index, wif, shType);
+ var redeemScript = this.ins[index].script.buffer
+ var s = coinjs.script();
+ s.writeBytes(Crypto.util.hexToBytes(signature));
+ s.writeBytes(redeemScript);
+ this.ins[index].script = s;
+ return true;
+ }
+
+ /* sign a multisig input */
+ r.signmultisig = function(index, wif, sigHashType) {
+
+ function scriptListPubkey(redeemScript) {
+ var r = {};
+ for (var i = 1; i < redeemScript.chunks.length - 2; i++) {
+ r[i] = Crypto.util.hexToBytes(coinjs.pubkeydecompress(Crypto.util.bytesToHex(redeemScript.chunks[i])));
+ }
+ return r;
+ }
+
+ function scriptListSigs(scriptSig) {
+ var r = {};
+ var c = 0;
+ if (scriptSig.chunks[0] == 0 && scriptSig.chunks[scriptSig.chunks.length - 1][scriptSig.chunks[scriptSig.chunks.length - 1].length - 1] == 174) {
+ for (var i = 1; i < scriptSig.chunks.length - 1; i++) {
+ if (scriptSig.chunks[i] != 0) {
+ c++;
+ r[c] = scriptSig.chunks[i];
+ }
+ }
+ }
+ return r;
+ }
+
+ var redeemScript = (this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1] == 174) ? this.ins[index].script.buffer : this.ins[index].script.chunks[this.ins[index].script.chunks.length - 1];
+
+ var pubkeyList = scriptListPubkey(coinjs.script(redeemScript));
+ var sigsList = scriptListSigs(this.ins[index].script);
+
+ var shType = sigHashType || 1;
+ var sighash = Crypto.util.hexToBytes(this.transactionHash(index, shType));
+ var signature = Crypto.util.hexToBytes(this.transactionSig(index, wif, shType));
+
+ sigsList[coinjs.countObject(sigsList) + 1] = signature;
+
+ var s = coinjs.script();
+
+ s.writeOp(0);
+
+ for (let x in pubkeyList) {
+ for (let y in sigsList) {
+ this.ins[index].script.buffer = redeemScript;
+ sighash = Crypto.util.hexToBytes(this.transactionHash(index, sigsList[y].slice(-1)[0] * 1));
+ if (coinjs.verifySignature(sighash, sigsList[y], pubkeyList[x])) {
+ s.writeBytes(sigsList[y]);
+ }
+ }
+ }
+
+ s.writeBytes(redeemScript);
+ this.ins[index].script = s;
+ return true;
+ }
+
+ /* sign segwit input */
+ r.signsegwit = function(index, wif, sigHashType) {
+ var shType = sigHashType || 1;
+
+ var wif2 = coinjs.wif2pubkey(wif);
+ var segwit = coinjs.segwitAddress(wif2['pubkey']);
+ var bech32 = coinjs.bech32Address(wif2['pubkey']);
+
+ if ((segwit['redeemscript'] == Crypto.util.bytesToHex(this.ins[index].script.chunks[0])) || (bech32['redeemscript'] == Crypto.util.bytesToHex(this.ins[index].script.chunks[0]))) {
+ var txhash = this.transactionHashSegWitV0(index, shType);
+
+ if (txhash.result == 1) {
+
+ var segwitHash = Crypto.util.hexToBytes(txhash.hash);
+ var signature = this.transactionSig(index, wif, shType, segwitHash);
+
+ // remove any non standard data we store, i.e. input value
+ var script = coinjs.script();
+ script.writeBytes(this.ins[index].script.chunks[0]);
+ this.ins[index].script = script;
+
+ if (!coinjs.isArray(this.witness)) {
+ this.witness = new Array(this.ins.length);
+ this.witness.fill([]);
+ }
+
+ this.witness[index] = ([signature, wif2['pubkey']]);
+
+ // bech32, empty redeemscript
+ if (bech32['redeemscript'] == Crypto.util.bytesToHex(this.ins[index].script.chunks[0])) {
+ this.ins[index].script = coinjs.script();
+ }
+
+ /* attempt to reorder witness data as best as we can.
+ data can't be easily validated at this stage as
+ we dont have access to the inputs value and
+ making a web call will be too slow. */
+
+ /*
+ var witness_order = [];
+ var witness_used = [];
+ for (var i = 0; i < this.ins.length; i++) {
+ for (var y = 0; y < this.witness.length; y++) {
+ if (!witness_used.includes(y)) {
+ var sw = coinjs.segwitAddress(this.witness[y][1]);
+ var b32 = coinjs.bech32Address(this.witness[y][1]);
+ var rs = '';
+
+ if (this.ins[i].script.chunks.length >= 1) {
+ rs = Crypto.util.bytesToHex(this.ins[i].script.chunks[0]);
+ } else if (this.ins[i].script.chunks.length == 0) {
+ rs = b32['redeemscript'];
+ }
+
+ if ((sw['redeemscript'] == rs) || (b32['redeemscript'] == rs)) {
+ witness_order.push(this.witness[y]);
+ witness_used.push(y);
+
+ // bech32, empty redeemscript
+ if (b32['redeemscript'] == rs) {
+ this.ins[index].script = coinjs.script();
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ this.witness = witness_order;
+ */
+ }
+ }
+ return true;
+ }
+
+ /* sign inputs */
+ r.sign = function(wif, sigHashType) {
+ var shType = sigHashType || 1;
+ for (var i = 0; i < this.ins.length; i++) {
+ var d = this.extractScriptKey(i);
+
+ var w2a = coinjs.wif2address(wif);
+ var script = coinjs.script();
+ var pubkeyHash = script.pubkeyHash(w2a['address']);
+
+ if (((d['type'] == 'scriptpubkey' && d['script'] == Crypto.util.bytesToHex(pubkeyHash.buffer)) || d['type'] == 'empty') && d['signed'] == "false") {
+ this.signinput(i, wif, shType);
+
+ } else if (d['type'] == 'hodl' && d['signed'] == "false") {
+ this.signhodl(i, wif, shType);
+
+ } else if (d['type'] == 'multisig') {
+ this.signmultisig(i, wif, shType);
+
+ } else if (d['type'] == 'segwit') {
+ this.signsegwit(i, wif, shType);
+
+ } else {
+ // could not sign
+ }
+ }
+ return this.serialize();
+ }
+
+ /* serialize a transaction */
+ r.serialize = function() {
+ var buffer = [];
+ buffer = buffer.concat(coinjs.numToBytes(parseInt(this.version), 4));
+
+ if (coinjs.isArray(this.witness)) {
+ buffer = buffer.concat([0x00, 0x01]);
+ }
+
+ buffer = buffer.concat(coinjs.numToVarInt(this.ins.length));
+ for (var i = 0; i < this.ins.length; i++) {
+ var txin = this.ins[i];
+ buffer = buffer.concat(Crypto.util.hexToBytes(txin.outpoint.hash).reverse());
+ buffer = buffer.concat(coinjs.numToBytes(parseInt(txin.outpoint.index), 4));
+ var scriptBytes = txin.script.buffer;
+ buffer = buffer.concat(coinjs.numToVarInt(scriptBytes.length));
+ buffer = buffer.concat(scriptBytes);
+ buffer = buffer.concat(coinjs.numToBytes(parseInt(txin.sequence), 4));
+ }
+ buffer = buffer.concat(coinjs.numToVarInt(this.outs.length));
+
+ for (var i = 0; i < this.outs.length; i++) {
+ var txout = this.outs[i];
+ buffer = buffer.concat(coinjs.numToBytes(txout.value, 8));
+ var scriptBytes = txout.script.buffer;
+ buffer = buffer.concat(coinjs.numToVarInt(scriptBytes.length));
+ buffer = buffer.concat(scriptBytes);
+ }
+
+ if ((coinjs.isArray(this.witness)) && this.witness.length >= 1) {
+ for (var i = 0; i < this.witness.length; i++) {
+ buffer = buffer.concat(coinjs.numToVarInt(this.witness[i].length));
+ for (var x = 0; x < this.witness[i].length; x++) {
+ buffer = buffer.concat(coinjs.numToVarInt(Crypto.util.hexToBytes(this.witness[i][x]).length));
+ buffer = buffer.concat(Crypto.util.hexToBytes(this.witness[i][x]));
+ }
+ }
+ }
+
+ buffer = buffer.concat(coinjs.numToBytes(parseInt(this.lock_time), 4));
+ return Crypto.util.bytesToHex(buffer);
+ }
+
+ /* deserialize a transaction */
+ r.deserialize = function(buffer) {
+ if (typeof buffer == "string") {
+ buffer = Crypto.util.hexToBytes(buffer)
+ }
+
+ var pos = 0;
+ var witness = false;
+
+ var readAsInt = function(bytes) {
+ if (bytes == 0) return 0;
+ pos++;
+ return buffer[pos - 1] + readAsInt(bytes - 1) * 256;
+ }
+
+ var readVarInt = function() {
+ pos++;
+ if (buffer[pos - 1] < 253) {
+ return buffer[pos - 1];
+ }
+ return readAsInt(buffer[pos - 1] - 251);
+ }
+
+ var readBytes = function(bytes) {
+ pos += bytes;
+ return buffer.slice(pos - bytes, pos);
+ }
+
+ var readVarString = function() {
+ var size = readVarInt();
+ return readBytes(size);
+ }
+
+ var obj = new coinjs.transaction();
+ obj.version = readAsInt(4);
+
+ if (buffer[pos] == 0x00 && buffer[pos + 1] == 0x01) {
+ // segwit transaction
+ witness = true;
+ obj.witness = [];
+ pos += 2;
+ }
+
+ var ins = readVarInt();
+ for (var i = 0; i < ins; i++) {
+ obj.ins.push({
+ outpoint: {
+ hash: Crypto.util.bytesToHex(readBytes(32).reverse()),
+ index: readAsInt(4)
+ },
+ script: coinjs.script(readVarString()),
+ sequence: readAsInt(4)
+ });
+ }
+
+ var outs = readVarInt();
+ for (var i = 0; i < outs; i++) {
+ obj.outs.push({
+ value: coinjs.bytesToNum(readBytes(8)),
+ script: coinjs.script(readVarString())
+ });
+ }
+
+ if (witness == true) {
+ for (i = 0; i < ins; ++i) {
+ var count = readVarInt();
+ var vector = [];
+ if (!coinjs.isArray(obj.witness[i])) {
+ obj.witness[i] = [];
+ }
+ for (var y = 0; y < count; y++) {
+ var slice = readVarInt();
+ pos += slice;
+ obj.witness[i].push(Crypto.util.bytesToHex(buffer.slice(pos - slice, pos)));
+ }
+ }
+ }
+
+ obj.lock_time = readAsInt(4);
+ return obj;
+ }
+
+ r.size = function() {
+ return ((this.serialize()).length / 2).toFixed(0);
+ }
+
+ return r;
+ }
+
+ /* start of signature vertification functions */
+
+ coinjs.verifySignature = function(hash, sig, pubkey) {
+
+ function parseSig(sig) {
+ var cursor;
+ if (sig[0] != 0x30)
+ throw new Error("Signature not a valid DERSequence");
+
+ cursor = 2;
+ if (sig[cursor] != 0x02)
+ throw new Error("First element in signature must be a DERInteger");;
+
+ var rBa = sig.slice(cursor + 2, cursor + 2 + sig[cursor + 1]);
+
+ cursor += 2 + sig[cursor + 1];
+ if (sig[cursor] != 0x02)
+ throw new Error("Second element in signature must be a DERInteger");
+
+ var sBa = sig.slice(cursor + 2, cursor + 2 + sig[cursor + 1]);
+
+ cursor += 2 + sig[cursor + 1];
+
+ var r = BigInteger.fromByteArrayUnsigned(rBa);
+ var s = BigInteger.fromByteArrayUnsigned(sBa);
+
+ return {
+ r: r,
+ s: s
+ };
+ }
+
+ var r, s;
+
+ if (coinjs.isArray(sig)) {
+ var obj = parseSig(sig);
+ r = obj.r;
+ s = obj.s;
+ } else if ("object" === typeof sig && sig.r && sig.s) {
+ r = sig.r;
+ s = sig.s;
+ } else {
+ throw "Invalid value for signature";
+ }
+
+ var Q;
+ if (coinjs.isArray(pubkey)) {
+ var ecparams = EllipticCurve.getSECCurveByName("secp256k1");
+ Q = EllipticCurve.PointFp.decodeFrom(ecparams.getCurve(), pubkey);
+ } else {
+ throw "Invalid format for pubkey value, must be byte array";
+ }
+ var e = BigInteger.fromByteArrayUnsigned(hash);
+
+ return coinjs.verifySignatureRaw(e, r, s, Q);
+ }
+
+ coinjs.verifySignatureRaw = function(e, r, s, Q) {
+ var ecparams = EllipticCurve.getSECCurveByName("secp256k1");
+ var n = ecparams.getN();
+ var G = ecparams.getG();
+
+ if (r.compareTo(BigInteger.ONE) < 0 || r.compareTo(n) >= 0)
+ return false;
+
+ if (s.compareTo(BigInteger.ONE) < 0 || s.compareTo(n) >= 0)
+ return false;
+
+ var c = s.modInverse(n);
+
+ var u1 = e.multiply(c).mod(n);
+ var u2 = r.multiply(c).mod(n);
+
+ var point = G.multiply(u1).add(Q.multiply(u2));
+
+ var v = point.getX().toBigInteger().mod(n);
+
+ return v.equals(r);
+ }
+
+ /* start of privates functions */
+
+ /* base58 encode function */
+ coinjs.base58encode = function(buffer) {
+ var alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+ var base = BigInteger.valueOf(58);
+
+ var bi = BigInteger.fromByteArrayUnsigned(buffer);
+ var chars = [];
+
+ while (bi.compareTo(base) >= 0) {
+ var mod = bi.mod(base);
+ chars.unshift(alphabet[mod.intValue()]);
+ bi = bi.subtract(mod).divide(base);
+ }
+
+ chars.unshift(alphabet[bi.intValue()]);
+ for (var i = 0; i < buffer.length; i++) {
+ if (buffer[i] == 0x00) {
+ chars.unshift(alphabet[0]);
+ } else break;
+ }
+ return chars.join('');
+ }
+
+ /* base58 decode function */
+ coinjs.base58decode = function(buffer) {
+ var alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+ var base = BigInteger.valueOf(58);
+ var validRegex = /^[1-9A-HJ-NP-Za-km-z]+$/;
+
+ var bi = BigInteger.valueOf(0);
+ var leadingZerosNum = 0;
+ for (var i = buffer.length - 1; i >= 0; i--) {
+ var alphaIndex = alphabet.indexOf(buffer[i]);
+ if (alphaIndex < 0) {
+ throw "Invalid character";
+ }
+ bi = bi.add(BigInteger.valueOf(alphaIndex).multiply(base.pow(buffer.length - 1 - i)));
+
+ if (buffer[i] == "1") leadingZerosNum++;
+ else leadingZerosNum = 0;
+ }
+
+ var bytes = bi.toByteArrayUnsigned();
+ while (leadingZerosNum-- > 0) bytes.unshift(0);
+ return bytes;
+ }
+
+ /* raw ajax function to avoid needing bigger frame works like jquery, mootools etc */
+ coinjs.ajax = function(u, f, m, a) {
+ var x = false;
+ try {
+ x = new ActiveXObject('Msxml2.XMLHTTP')
+ } catch (e) {
+ try {
+ x = new ActiveXObject('Microsoft.XMLHTTP')
+ } catch (e) {
+ x = new XMLHttpRequest()
+ }
+ }
+
+ if (x == false) {
+ return false;
+ }
+
+ x.open(m, u, true);
+ x.onreadystatechange = function() {
+ if ((x.readyState == 4) && f)
+ f(x.responseText);
+ };
+
+ if (m == 'POST') {
+ x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+ }
+
+ x.send(a);
+ }
+
+ /* clone an object */
+ coinjs.clone = function(obj) {
+ if (obj == null || typeof(obj) != 'object') return obj;
+ var temp = new obj.constructor();
+
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ temp[key] = coinjs.clone(obj[key]);
+ }
+ }
+ return temp;
+ }
+
+ coinjs.numToBytes = function(num, bytes) {
+ if (typeof bytes === "undefined") bytes = 8;
+ if (bytes == 0) {
+ return [];
+ } else if (num == -1) {
+ return Crypto.util.hexToBytes("ffffffffffffffff");
+ } else {
+ return [num % 256].concat(coinjs.numToBytes(Math.floor(num / 256), bytes - 1));
+ }
+ }
+
+ function scriptNumSize(i) {
+ return i > 0x7fffffff ? 5 :
+ i > 0x7fffff ? 4 :
+ i > 0x7fff ? 3 :
+ i > 0x7f ? 2 :
+ i > 0x00 ? 1 :
+ 0;
+ }
+
+ coinjs.numToScriptNumBytes = function(_number) {
+ var value = Math.abs(_number);
+ var size = scriptNumSize(value);
+ var result = [];
+ for (var i = 0; i < size; ++i) {
+ result.push(0);
+ }
+ var negative = _number < 0;
+ for (i = 0; i < size; ++i) {
+ result[i] = value & 0xff;
+ value = Math.floor(value / 256);
+ }
+ if (negative) {
+ result[size - 1] |= 0x80;
+ }
+ return result;
+ }
+
+ coinjs.numToVarInt = function(num) {
+ if (num < 253) {
+ return [num];
+ } else if (num < 65536) {
+ return [253].concat(coinjs.numToBytes(num, 2));
+ } else if (num < 4294967296) {
+ return [254].concat(coinjs.numToBytes(num, 4));
+ } else {
+ return [255].concat(coinjs.numToBytes(num, 8));
+ }
+ }
+
+ coinjs.bytesToNum = function(bytes) {
+ if (bytes.length == 0) return 0;
+ else return bytes[0] + 256 * coinjs.bytesToNum(bytes.slice(1));
+ }
+
+ coinjs.uint = function(f, size) {
+ if (f.length < size)
+ throw new Error("not enough data");
+ var n = 0;
+ for (var i = 0; i < size; i++) {
+ n *= 256;
+ n += f[i];
+ }
+ return n;
+ }
+
+ coinjs.isArray = function(o) {
+ return Object.prototype.toString.call(o) === '[object Array]';
+ }
+
+ coinjs.countObject = function(obj) {
+ var count = 0;
+ var i;
+ for (i in obj) {
+ if (obj.hasOwnProperty(i)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ coinjs.random = function(length) {
+ var r = "";
+ var l = length || 25;
+ var chars = "!$%^&*()_+{}:@~?><|\./;'#][=-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
+ for (let x = 0; x < l; x++) {
+ r += chars.charAt(Math.floor(securedMathRandom() * 62));
+ }
+ return r;
+ }
+
+ })();
+
//secrets.js
(function() {
//Shamir Secret Share by Alexander Stetsyuk - released under MIT License
@@ -6036,7 +8616,7 @@
],
// warning for insecure PRNG
- warning: 'WARNING:\nA secure random number generator was not found.\nUsing Math.random(), which is NOT cryptographically strong!'
+ warning: 'WARNING:\nA secure random number generator was not found.\nUsing securedMathRandom(), which is NOT cryptographically strong!'
};
// Protected settings object
@@ -6156,7 +8736,7 @@
str = null;
while (str === null) {
for (var i = 0; i < elems; i++) {
- arr[i] = Math.floor(Math.random() * max + 1);
+ arr[i] = Math.floor(securedMathRandom() * max + 1);
}
str = construct(bits, arr, 10, bitsPerNum);
}
@@ -6165,7 +8745,7 @@
};
// Warn about using insecure rng.
- // Called when Math.random() is being used.
+ // Called when securedMathRandom() is being used.
function warn() {
GLOBAL['console']['warn'](defaults.warning);
if (typeof GLOBAL['alert'] === 'function' && config.alert) {
@@ -6589,22 +9169,12 @@
//kbucket.js
(function() {
- const getRandomValues = function(buf) {
- if (typeof require === 'function') {
- var bytes = require('crypto').randomBytes(buf.length);
- buf.set(bytes)
- return buf;
- } else if (GLOBAL.crypto && GLOBAL.crypto.getRandomValues)
- return GLOBAL.crypto.getRandomValues(buf);
- else
- return null;
- }
// Kademlia DHT K-bucket implementation as a binary tree.
// by 'Tristan Slominski' under 'MIT License'
GLOBAL.BuildKBucket = function KBucket(options = {}) {
if (!(this instanceof KBucket))
return new KBucket(options);
- this.localNodeId = options.localNodeId || getRandomValues(new Uint8Array(20))
+ this.localNodeId = options.localNodeId || getRandomBytes(new Uint8Array(20))
this.numberOfNodesPerKBucket = options.numberOfNodesPerKBucket || 20
this.numberOfNodesToPing = options.numberOfNodesToPing || 3
this.distance = options.distance || this.distance
@@ -6783,5 +9353,4 @@
}
})();
-
})(typeof global !== "undefined" ? global : window);
\ No newline at end of file
diff --git a/scripts/messenger.js b/scripts/messenger.js
index 71bffde..777b714 100644
--- a/scripts/messenger.js
+++ b/scripts/messenger.js
@@ -1,10 +1,20 @@
-(function () {
+(function() {
const messenger = window.messenger = {};
+ const user = {
+ get id() {
+ return floDapps.user.id
+ },
+ get public() {
+ return floDapps.user.public
+ }
+ }
+
const expiredKeys = {};
const UI = {
group: (d, e) => console.log(d, e),
+ pipeline: (d, e) => console.log(d, e),
direct: (d, e) => console.log(d, e),
chats: (c) => console.log(c),
mails: (m) => console.log(m),
@@ -21,6 +31,9 @@
groupChat: {
set: ui_fn => UI.group = ui_fn
},
+ pipeline: {
+ set: ui_fn => UI.pipeline = ui_fn
+ },
mails: {
set: ui_fn => UI.mails = ui_fn
},
@@ -37,6 +50,9 @@
groups: {
get: () => _loaded.groups
},
+ pipeline: {
+ get: () => _loaded.pipeline
+ },
blocked: {
get: () => _loaded.blocked
},
@@ -45,7 +61,8 @@
}
});
- var directConnID, groupConnID = {};
+ var directConnID, groupConnID = {},
+ pipeConnID = {};
messenger.conn = {};
Object.defineProperties(messenger.conn, {
direct: {
@@ -60,12 +77,15 @@
function sendRaw(message, recipient, type, encrypt = null, comment = undefined) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(recipient))
- return reject("Invalid Recipient floID");
+ return reject("Invalid Recipient");
- if ([true, null].includes(encrypt) && recipient in floGlobals.pubKeys)
- message = floCrypto.encryptData(message, floGlobals.pubKeys[recipient])
- else if (encrypt === true)
- return reject("recipient's pubKey not found")
+ if ([true, null].includes(encrypt)) {
+ let r_pubKey = floDapps.user.get_pubKey(recipient);
+ if (r_pubKey)
+ message = floCrypto.encryptData(message, r_pubKey);
+ else if (encrypt === true)
+ return reject("recipient's pubKey not found")
+ }
let options = {
receiverID: recipient,
}
@@ -116,102 +136,7 @@
})
}
- function requestGroupInbox(groupID) {
- if (groupConnID[groupID]) { //close existing request connection (if any)
- floCloudAPI.closeRequest(groupConnID[groupID]);
- delete groupConnID[groupID];
- }
- let callbackFn = function (dataSet, error) {
- if (error)
- return console.error(error)
- console.info(dataSet)
- let newInbox = {
- messages: {}
- }
- let infoChange = false;
- for (let vc in dataSet) {
- if (groupID !== dataSet[vc].receiverID ||
- !_loaded.groups[groupID].members.includes(dataSet[vc].senderID))
- continue;
- try {
- let data = {
- time: dataSet[vc].time,
- sender: dataSet[vc].senderID,
- groupID: dataSet[vc].receiverID
- }
- let k = _loaded.groups[groupID].eKey;
- if (expiredKeys[groupID]) {
- var ex = Object.keys(expiredKeys[groupID]).sort()
- while (ex.length && vc > ex[0]) ex.shift()
- if (ex.length)
- k = expiredKeys[groupID][ex.shift()]
- }
- dataSet[vc].message = decrypt(dataSet[vc].message, k)
- //store the pubKey if not stored already
- floDapps.storePubKey(dataSet[vc].senderID, dataSet[vc].pubKey)
- if (dataSet[vc].type === "GROUP_MSG")
- data.message = encrypt(dataSet[vc].message);
- else if (data.sender === _loaded.groups[groupID].admin) {
- let groupInfo = _loaded.groups[groupID]
- data.admin = true;
- if (dataSet[vc].type === "ADD_MEMBERS") {
- data.newMembers = dataSet[vc].message.split("|")
- data.note = dataSet[vc].comment
- groupInfo.members = [...new Set(groupInfo.members.concat(data.newMembers))]
- } else if (dataSet[vc].type === "RM_MEMBERS") {
- data.rmMembers = dataSet[vc].message.split("|")
- data.note = dataSet[vc].comment
- groupInfo.members = groupInfo.members.filter(m => !data.rmMembers.includes(m))
- if (data.rmMembers.includes(myFloID)) {
- disableGroup(groupID);
- return;
- }
- } else if (dataSet[vc].type === "UP_DESCRIPTION") {
- data.description = dataSet[vc].message
- groupInfo.description = data.description
- } else if (dataSet[vc].type === "UP_NAME") {
- data.name = dataSet[vc].message
- groupInfo.name = data.name
- }
- infoChange = true;
- }
- compactIDB.addData("messages", {
- ...data
- }, `${groupID}|${vc}`)
- if (data.message)
- data.message = decrypt(data.message);
- newInbox.messages[vc] = data;
- if (data.sender !== myFloID)
- addMark(data.groupID, "unread")
- if (!_loaded.appendix[`lastReceived_${groupID}`] ||
- _loaded.appendix[`lastReceived_${groupID}`] < vc)
- _loaded.appendix[`lastReceived_${groupID}`] = vc;
- } catch (error) {
- console.log(error)
- }
- }
- compactIDB.writeData("appendix", _loaded.appendix[`lastReceived_${groupID}`],
- `lastReceived_${groupID}`);
- if (infoChange) {
- let newInfo = {
- ..._loaded.groups[groupID]
- }
- newInfo.eKey = encrypt(newInfo.eKey)
- compactIDB.writeData("groups", newInfo, groupID)
- }
- console.debug(newInbox);
- UI.group(newInbox);
- }
- floCloudAPI.requestApplicationData(null, {
- receiverID: groupID,
- lowerVectorClock: _loaded.appendix[`lastReceived_${groupID}`] + 1,
- callback: callbackFn
- }).then(conn_id => groupConnID[groupID] = conn_id)
- .catch(error => console.error(`request-group(${groupID}):`, error))
-
- }
-
- const initUserDB = function () {
+ const initUserDB = function() {
return new Promise((resolve, reject) => {
var obj = {
messages: {},
@@ -221,18 +146,25 @@
groups: {},
gkeys: {},
blocked: {},
+ pipeline: {},
+ request_sent: {},
+ request_received: {},
+ response_sent: {},
+ response_received: {},
+ flodata: {},
appendix: {},
userSettings: {}
}
- compactIDB.initDB(`${floGlobals.application}_${myFloID}`, obj).then(result => {
+ let user_db = `${floGlobals.application}_${floCrypto.toFloID(user.id)}`;
+ compactIDB.initDB(user_db, obj).then(result => {
console.info(result)
- compactIDB.setDefaultDB(`${floGlobals.application}_${myFloID}`);
+ compactIDB.setDefaultDB(user_db);
resolve("Messenger UserDB Initated Successfully")
}).catch(error => reject(error));
})
}
- messenger.blockUser = function (floID) {
+ messenger.blockUser = function(floID) {
return new Promise((resolve, reject) => {
if (_loaded.blocked.has(floID))
return resolve("User is already blocked");
@@ -243,7 +175,7 @@
})
}
- messenger.unblockUser = function (floID) {
+ messenger.unblockUser = function(floID) {
return new Promise((resolve, reject) => {
if (!_loaded.blocked.has(floID))
return resolve("User is not blocked");
@@ -254,7 +186,7 @@
})
}
- messenger.sendMessage = function (message, receiver) {
+ messenger.sendMessage = function(message, receiver) {
return new Promise((resolve, reject) => {
sendRaw(message, receiver, "MESSAGE").then(result => {
let vc = result.vectorClock;
@@ -266,9 +198,7 @@
}
_loaded.chats[receiver] = parseInt(vc)
compactIDB.writeData("chats", parseInt(vc), receiver)
- compactIDB.addData("messages", {
- ...data
- }, `${receiver}|${vc}`)
+ compactIDB.addData("messages", Object.assign({}, data), `${receiver}|${vc}`)
data.message = message;
resolve({
[vc]: data
@@ -277,7 +207,7 @@
})
}
- messenger.sendMail = function (subject, content, recipients, prev = null) {
+ messenger.sendMail = function(subject, content, recipients, prev = null) {
return new Promise((resolve, reject) => {
if (!Array.isArray(recipients))
recipients = [recipients]
@@ -290,7 +220,7 @@
let promises = recipients.map(r => sendRaw(JSON.stringify(mail), r, "MAIL"))
Promise.allSettled(promises).then(results => {
mail.time = Date.now();
- mail.from = myFloID
+ mail.from = user.id
mail.to = []
results.forEach(r => {
if (r.status === "fulfilled")
@@ -299,9 +229,7 @@
if (mail.to.length === 0)
return reject(results)
mail.content = encrypt(content)
- compactIDB.addData("mails", {
- ...mail
- }, mail.ref)
+ compactIDB.addData("mails", Object.assign({}, mail), mail.ref)
mail.content = content
resolve({
[mail.ref]: mail
@@ -310,95 +238,216 @@
})
}
- const requestDirectInbox = function () {
+ function listRequests(obs, options = null) {
+ return new Promise((resolve, reject) => {
+ compactIDB.readAllData(obs).then(result => {
+ if (!options || typeof options !== 'object')
+ return resolve(result);
+ let filtered = {};
+ for (let k in result) {
+ let val = result[k];
+ if (options.type && options.type == val.type) continue;
+ else if (options.floID && options.floID == val.floID) continue;
+ else if (typeof options.completed !== 'undefined' && options.completed == !(val.completed))
+ continue;
+ filtered[k] = val;
+ }
+ resolve(filtered);
+ }).catch(error => reject(error))
+ })
+ }
+
+ messenger.list_request_sent = (options = null) => listRequests('request_sent', options);
+ messenger.list_request_received = (options = null) => listRequests('request_received', options);
+ messenger.list_response_sent = (options = null) => listRequests('response_sent', options);
+ messenger.list_response_received = (options = null) => listRequests('response_received', options);
+
+ function sendRequest(receiver, type, message, encrypt = null) {
+ return new Promise((resolve, reject) => {
+ sendRaw(message, receiver, "REQUEST", encrypt, type).then(result => {
+ let vc = result.vectorClock;
+ let data = {
+ floID: receiver,
+ time: result.time,
+ message: message,
+ type: type
+ }
+ compactIDB.addData("request_sent", data, vc);
+ resolve({
+ [vc]: data
+ });
+ }).catch(error => reject(error))
+ })
+ }
+
+ messenger.request_pubKey = (receiver, message = '') => sendRequest(receiver, "PUBLIC_KEY", message, false);
+
+ function sendResponse(req_id, message, encrypt = null) {
+ return new Promise((resolve, reject) => {
+ compactIDB.readData("request_received", req_id).then(request => {
+ let _message = JSON.stringify({
+ value: message,
+ reqID: req_id
+ });
+ sendRaw(_message, request.floID, "RESPONSE", encrypt, request.type).then(result => {
+ let vc = result.vectorClock;
+ let data = {
+ floID: request.floID,
+ time: result.time,
+ message: message,
+ type: request.type,
+ reqID: req_id
+ }
+ compactIDB.addData("response_sent", data, vc);
+ request.completed = vc;
+ compactIDB.writeData("request_received", request, req_id);
+ resolve({
+ [vc]: data
+ });
+ }).catch(error => reject(error))
+ }).catch(error => reject(error))
+ })
+ }
+
+ messenger.respond_pubKey = (req_id, message = '') => sendResponse(req_id, message, false);
+
+ const processData = {};
+ processData.direct = function() {
+ return (unparsed, newInbox) => {
+ //store the pubKey if not stored already
+ floDapps.storePubKey(unparsed.senderID, unparsed.pubKey);
+ if (_loaded.blocked.has(unparsed.senderID) && unparsed.type !== "REVOKE_KEY")
+ throw "blocked-user";
+ if (unparsed.message instanceof Object && "secret" in unparsed.message)
+ unparsed.message = floDapps.user.decrypt(unparsed.message);
+ let vc = unparsed.vectorClock;
+ switch (unparsed.type) {
+ case "MESSAGE": { //process as message
+ let dm = {
+ time: unparsed.time,
+ floID: unparsed.senderID,
+ category: "received",
+ message: encrypt(unparsed.message)
+ }
+ compactIDB.addData("messages", Object.assign({}, dm), `${dm.floID}|${vc}`)
+ _loaded.chats[dm.floID] = parseInt(vc)
+ compactIDB.writeData("chats", parseInt(vc), dm.floID)
+ dm.message = unparsed.message;
+ newInbox.messages[vc] = dm;
+ addMark(dm.floID, "unread");
+ break;
+ }
+ case "REQUEST": {
+ let req = {
+ floID: unparsed.senderID,
+ time: unparsed.time,
+ message: unparsed.message,
+ type: unparsed.comment
+ }
+ compactIDB.addData("request_received", req, vc);
+ newInbox.requests[vc] = req;
+ break;
+ }
+ case "RESPONSE": {
+ let data = JSON.parse(unparsed.message);
+ let res = {
+ floID: unparsed.senderID,
+ time: unparsed.time,
+ message: data.value,
+ type: unparsed.comment,
+ reqID: data.reqID
+ }
+ compactIDB.addData("response_received", res, vc);
+ compactIDB.readData("request_sent", data.reqID).then(req => {
+ req.completed = vc;
+ compactIDB.writeData("request_sent", req, data.reqID)
+ });
+ newInbox.responses[vc] = res;
+ break;
+ }
+ case "MAIL": { //process as mail
+ let data = JSON.parse(unparsed.message);
+ let mail = {
+ time: unparsed.time,
+ from: unparsed.senderID,
+ to: [unparsed.receiverID],
+ subject: data.subject,
+ content: encrypt(data.content),
+ ref: data.ref,
+ prev: data.prev
+ }
+ compactIDB.addData("mails", Object.assign({}, mail), mail.ref);
+ mail.content = data.content;
+ newInbox.mails[mail.ref] = mail;
+ addMark(mail.ref, "unread");
+ break;
+ }
+ case "CREATE_GROUP": { //process create group
+ let groupInfo = JSON.parse(unparsed.message);
+ let h = ["groupID", "created", "admin"].map(x => groupInfo[x]).join('|')
+ if (groupInfo.admin === unparsed.senderID &&
+ floCrypto.verifySign(h, groupInfo.hash, groupInfo.pubKey) &&
+ floCrypto.getFloID(groupInfo.pubKey) === groupInfo.groupID) {
+ let eKey = groupInfo.eKey
+ groupInfo.eKey = encrypt(eKey)
+ compactIDB.writeData("groups", Object.assign({}, groupInfo), groupInfo.groupID)
+ groupInfo.eKey = eKey
+ _loaded.groups[groupInfo.groupID] = groupInfo
+ requestGroupInbox(groupInfo.groupID)
+ newInbox.newgroups.push(groupInfo.groupID)
+ }
+ break;
+ }
+ case "REVOKE_KEY": { //revoke group key
+ let r = JSON.parse(unparsed.message);
+ let groupInfo = _loaded.groups[r.groupID]
+ if (unparsed.senderID === groupInfo.admin) {
+ if (typeof expiredKeys[r.groupID] !== "object")
+ expiredKeys[r.groupID] = {}
+ expiredKeys[r.groupID][vc] = groupInfo.eKey
+ let eKey = r.newKey
+ groupInfo.eKey = encrypt(eKey);
+ compactIDB.writeData("groups", Object.assign({}, groupInfo), groupInfo.groupID)
+ groupInfo.eKey = eKey
+ newInbox.keyrevoke.push(groupInfo.groupID)
+ }
+ break;
+ }
+ case "CREATE_PIPELINE": { //add pipeline
+ let pipelineInfo = JSON.parse(unparsed.message);
+ let eKey = pipelineInfo.eKey;
+ pipelineInfo.eKey = encrypt(eKey)
+ compactIDB.addData("pipeline", Object.assign({}, pipelineInfo), pipelineInfo.id);
+ pipelineInfo.eKey = eKey;
+ _loaded.pipeline[pipelineInfo.id] = pipelineInfo
+ requestPipelineInbox(pipelineInfo.id, pipelineInfo.model);
+ newInbox.pipeline[pipelineInfo.id] = pipelineInfo.model;
+ }
+ }
+ }
+ }
+
+ function requestDirectInbox() {
if (directConnID) { //close existing request connection (if any)
floCloudAPI.closeRequest(directConnID);
directConnID = undefined;
}
- let callbackFn = function (dataSet, error) {
+ const parseData = processData.direct();
+ let callbackFn = function(dataSet, error) {
if (error)
return console.error(error)
let newInbox = {
messages: {},
+ requests: {},
+ responses: {},
mails: {},
newgroups: [],
- keyrevoke: []
+ keyrevoke: [],
+ pipeline: {}
}
for (let vc in dataSet) {
try {
- //store the pubKey if not stored already
- floDapps.storePubKey(dataSet[vc].senderID, dataSet[vc].pubKey);
- if (_loaded.blocked.has(dataSet[vc].senderID) && dataSet[vc].type !== "REVOKE_KEY")
- throw "blocked-user";
- if (dataSet[vc].message instanceof Object && "secret" in dataSet[vc].message)
- dataSet[vc].message = floCrypto.decryptData(dataSet[vc].message, myPrivKey)
- if (dataSet[vc].type === "MESSAGE") {
- //process as message
- let dm = {
- time: dataSet[vc].time,
- floID: dataSet[vc].senderID,
- category: "received",
- message: encrypt(dataSet[vc].message)
- }
- compactIDB.addData("messages", {
- ...dm
- }, `${dm.floID}|${vc}`)
- _loaded.chats[dm.floID] = parseInt(vc)
- compactIDB.writeData("chats", parseInt(vc), dm.floID)
- dm.message = dataSet[vc].message;
- newInbox.messages[vc] = dm;
- addMark(dm.floID, "unread")
- } else if (dataSet[vc].type === "MAIL") {
- //process as mail
- let data = JSON.parse(dataSet[vc].message);
- let mail = {
- time: dataSet[vc].time,
- from: dataSet[vc].senderID,
- to: [myFloID],
- subject: data.subject,
- content: encrypt(data.content),
- ref: data.ref,
- prev: data.prev
- }
- compactIDB.addData("mails", {
- ...mail
- }, mail.ref);
- mail.content = data.content;
- newInbox.mails[mail.ref] = mail;
- addMark(mail.ref, "unread")
- } else if (dataSet[vc].type === "CREATE_GROUP") {
- //process create group
- let groupInfo = JSON.parse(dataSet[vc].message);
- let h = ["groupID", "created", "admin"].map(x => groupInfo[x]).join('|')
- if (groupInfo.admin === dataSet[vc].senderID &&
- floCrypto.verifySign(h, groupInfo.hash, groupInfo.pubKey) &&
- floCrypto.getFloID(groupInfo.pubKey) === groupInfo.groupID) {
- let eKey = groupInfo.eKey
- groupInfo.eKey = encrypt(eKey)
- compactIDB.writeData("groups", {
- ...groupInfo
- }, groupInfo.groupID)
- groupInfo.eKey = eKey
- _loaded.groups[groupInfo.groupID] = groupInfo
- requestGroupInbox(groupInfo.groupID)
- newInbox.newgroups.push(groupInfo.groupID)
- }
- } else if (dataSet[vc].type === "REVOKE_KEY") {
- let r = JSON.parse(dataSet[vc].message);
- let groupInfo = _loaded.groups[r.groupID]
- if (dataSet[vc].senderID === groupInfo.admin) {
- if (typeof expiredKeys[r.groupID] !== "object")
- expiredKeys[r.groupID] = {}
- expiredKeys[r.groupID][vc] = groupInfo.eKey
- let eKey = r.newKey
- groupInfo.eKey = encrypt(eKey);
- compactIDB.writeData("groups", {
- ...groupInfo
- }, groupInfo.groupID)
- groupInfo.eKey = eKey
- newInbox.keyrevoke.push(groupInfo.groupID)
- }
- }
+ parseData(dataSet[vc], newInbox);
} catch (error) {
//if (error !== "blocked-user")
console.log(error);
@@ -412,14 +461,14 @@
UI.direct(newInbox)
}
floCloudAPI.requestApplicationData(null, {
- receiverID: myFloID,
- lowerVectorClock: _loaded.appendix.lastReceived + 1,
- callback: callbackFn
- }).then(conn_id => directConnID = conn_id)
+ receiverID: user.id,
+ lowerVectorClock: _loaded.appendix.lastReceived + 1,
+ callback: callbackFn
+ }).then(conn_id => directConnID = conn_id)
.catch(error => console.error("request-direct:", error));
}
- messenger.getMail = function (mailRef) {
+ messenger.getMail = function(mailRef) {
return new Promise((resolve, reject) => {
compactIDB.readData("mails", mailRef).then(mail => {
mail.content = decrypt(mail.content)
@@ -428,7 +477,7 @@
});
}
- const getChatOrder = messenger.getChatOrder = function (separate = false) {
+ const getChatOrder = messenger.getChatOrder = function(separate = false) {
let result;
if (separate) {
result = {};
@@ -436,24 +485,27 @@
.sort((a, b) => b[0] - a[0]).map(a => a[1]);
result.group = Object.keys(_loaded.groups).map(a => [parseInt(_loaded.appendix[`lastReceived_${a}`]), a])
.sort((a, b) => b[0] - a[0]).map(a => a[1]);
+ result.pipeline = Object.keys(_loaded.pipeline).map(a => [parseInt(_loaded.appendix[`lastReceived_${a}`]), a])
+ .sort((a, b) => b[0] - a[0]).map(a => a[1]);
} else {
result = Object.keys(_loaded.chats).map(a => [_loaded.chats[a], a])
.concat(Object.keys(_loaded.groups).map(a => [parseInt(_loaded.appendix[`lastReceived_${a}`]), a]))
+ .concat(Object.keys(_loaded.pipeline).map(a => [parseInt(_loaded.appendix[`lastReceived_${a}`]), a]))
.sort((a, b) => b[0] - a[0]).map(a => a[1])
}
return result;
}
- messenger.storeContact = function (floID, name) {
+ messenger.storeContact = function(floID, name) {
return floDapps.storeContact(floID, name)
}
- const loadDataFromIDB = function (defaultList = true) {
+ const loadDataFromIDB = function(defaultList = true) {
return new Promise((resolve, reject) => {
if (defaultList)
- dataList = ["mails", "marked", "groups", "chats", "blocked", "appendix"]
+ dataList = ["mails", "marked", "groups", "pipeline", "chats", "blocked", "appendix"]
else
- dataList = ["messages", "mails", "marked", "chats", "groups", "gkeys", "blocked", "appendix"]
+ dataList = ["messages", "mails", "marked", "chats", "groups", "gkeys", "pipeline", "blocked", "appendix"]
let promises = []
for (var i = 0; i < dataList.length; i++)
promises[i] = compactIDB.readAllData(dataList[i])
@@ -464,7 +516,7 @@
data.appendix.lastReceived = data.appendix.lastReceived || '0';
if (data.appendix.AESKey) {
try {
- let AESKey = floCrypto.decryptData(data.appendix.AESKey, myPrivKey);
+ let AESKey = floDapps.user.decrypt(data.appendix.AESKey);
data.appendix.AESKey = AESKey;
if (dataList.includes("messages"))
for (let m in data.messages)
@@ -479,15 +531,19 @@
if (dataList.includes("gkeys"))
for (let k in data.gkeys)
data.gkeys[k] = decrypt(data.gkeys[k], AESKey);
+ if (dataList.includes("pipeline"))
+ for (let p in data.pipeline)
+ data.pipeline[p].eKey = decrypt(data.pipeline[p].eKey, AESKey);
resolve(data)
} catch (error) {
+ console.error(error)
reject("Corrupted AES Key");
}
} else {
if (Object.keys(data.mails).length)
return reject("AES Key not Found")
let AESKey = floCrypto.randString(32, false);
- let encryptedKey = floCrypto.encryptData(AESKey, myPubKey);
+ let encryptedKey = floCrypto.encryptData(AESKey, user.public);
compactIDB.addData("appendix", encryptedKey, "AESKey").then(result => {
data.appendix.AESKey = AESKey;
resolve(data);
@@ -497,19 +553,19 @@
})
}
- messenger.addMark = function (key, mark) {
+ messenger.addMark = function(key, mark) {
if (_loaded.marked.hasOwnProperty(key) && !_loaded.marked[key].includes(mark))
_loaded.marked[key].push(mark)
return addMark(key, mark)
}
- messenger.removeMark = function (key, mark) {
+ messenger.removeMark = function(key, mark) {
if (_loaded.marked.hasOwnProperty(key))
_loaded.marked[key] = _loaded.marked[key].filter(v => v !== mark)
return removeMark(key, mark)
}
- messenger.addChat = function (chatID) {
+ messenger.addChat = function(chatID) {
return new Promise((resolve, reject) => {
compactIDB.addData("chats", 0, chatID)
.then(result => resolve("Added chat"))
@@ -517,7 +573,7 @@
})
}
- messenger.rmChat = function (chatID) {
+ messenger.rmChat = function(chatID) {
return new Promise((resolve, reject) => {
compactIDB.removeData("chats", chatID)
.then(result => resolve("Chat removed"))
@@ -525,7 +581,7 @@
})
}
- messenger.clearChat = function (chatID) {
+ messenger.clearChat = function(chatID) {
return new Promise((resolve, reject) => {
let options = {
lowerKey: `${chatID}|`,
@@ -542,7 +598,7 @@
})
}
- messenger.getChat = function (chatID) {
+ const getChat = messenger.getChat = function(chatID) {
return new Promise((resolve, reject) => {
let options = {
lowerKey: `${chatID}|`,
@@ -557,7 +613,7 @@
})
}
- messenger.backupData = function () {
+ messenger.backupData = function() {
return new Promise((resolve, reject) => {
loadDataFromIDB(false).then(data => {
delete data.appendix.AESKey;
@@ -565,11 +621,11 @@
data.pubKeys = floGlobals.pubKeys;
data = btoa(unescape(encodeURIComponent(JSON.stringify(data))))
let blobData = {
- floID: myFloID,
- pubKey: myPubKey,
- data: encrypt(data, myPrivKey),
+ floID: user.id,
+ pubKey: user.public,
+ data: floDapps.user.encipher(data),
}
- blobData.sign = floCrypto.signData(blobData.data, myPrivKey);
+ blobData.sign = floDapps.sign(blobData.data);
resolve(new Blob([JSON.stringify(blobData)], {
type: 'application/json'
}));
@@ -577,7 +633,7 @@
})
}
- const parseBackup = messenger.parseBackup = function (blob) {
+ const parseBackup = messenger.parseBackup = function(blob) {
return new Promise((resolve, reject) => {
if (blob instanceof Blob || blob instanceof File) {
let reader = new FileReader();
@@ -585,11 +641,11 @@
var blobData = JSON.parse(evt.target.result);
if (!floCrypto.verifySign(blobData.data, blobData.sign, blobData.pubKey))
reject("Corrupted Backup file: Signature verification failed");
- else if (myFloID !== blobData.floID || myPubKey !== blobData.pubKey)
+ else if (user.id !== blobData.floID || user.public !== blobData.pubKey)
reject("Invalid Backup file: Incorrect floID");
else {
try {
- let data = decrypt(blobData.data, myPrivKey)
+ let data = floDapps.user.decipher(blobData.data);
try {
data = JSON.parse(decodeURIComponent(escape(atob(data))));
resolve(data)
@@ -607,7 +663,7 @@
})
}
- messenger.restoreData = function (arg) {
+ messenger.restoreData = function(arg) {
return new Promise((resolve, reject) => {
if (arg instanceof Blob || arg instanceof File)
var parseData = parseBackup
@@ -623,6 +679,8 @@
data.gkeys[k] = encrypt(data.gkeys[k])
for (let g in data.groups)
data.groups[g].eKey = encrypt(data.groups[g].eKey)
+ for (let p in data.pipeline)
+ data.pipeline[p].eKey = encrypt(data.pipeline[p].eKey)
for (let c in data.chats)
if (data.chats[c] <= _loaded.chats[c])
delete data.chats[c]
@@ -660,7 +718,7 @@
})
}
- messenger.clearUserData = function () {
+ messenger.clearUserData = function() {
return new Promise((resolve, reject) => {
let promises = [
compactIDB.deleteDB(),
@@ -674,18 +732,18 @@
//group feature
- messenger.createGroup = function (groupname, description = '') {
+ messenger.createGroup = function(groupname, description = '') {
return new Promise((resolve, reject) => {
if (!groupname) return reject("Invalid Group Name")
let id = floCrypto.generateNewID();
let groupInfo = {
groupID: id.floID,
pubKey: id.pubKey,
- admin: myFloID,
+ admin: user.id,
name: groupname,
description: description,
created: Date.now(),
- members: [myFloID]
+ members: [user.id]
}
let h = ["groupID", "created", "admin"].map(x => groupInfo[x]).join('|')
groupInfo.hash = floCrypto.signData(h, id.privKey)
@@ -702,10 +760,10 @@
})
}
- messenger.changeGroupName = function (groupID, name) {
+ messenger.changeGroupName = function(groupID, name) {
return new Promise((resolve, reject) => {
let groupInfo = _loaded.groups[groupID]
- if (myFloID !== groupInfo.admin)
+ if (user.id !== groupInfo.admin)
return reject("Access denied: Admin only!")
let message = encrypt(name, groupInfo.eKey)
sendRaw(message, groupID, "UP_NAME", false)
@@ -714,10 +772,10 @@
})
}
- messenger.changeGroupDescription = function (groupID, description) {
+ messenger.changeGroupDescription = function(groupID, description) {
return new Promise((resolve, reject) => {
let groupInfo = _loaded.groups[groupID]
- if (myFloID !== groupInfo.admin)
+ if (user.id !== groupInfo.admin)
return reject("Access denied: Admin only!")
let message = encrypt(description, groupInfo.eKey)
sendRaw(message, groupID, "UP_DESCRIPTION", false)
@@ -726,7 +784,7 @@
})
}
- messenger.addGroupMembers = function (groupID, newMem, note = undefined) {
+ messenger.addGroupMembers = function(groupID, newMem, note = undefined) {
return new Promise((resolve, reject) => {
if (!Array.isArray(newMem) && typeof newMem === "string")
newMem = [newMem]
@@ -735,7 +793,7 @@
imem2 = []
newMem.forEach(m =>
!floCrypto.validateAddr(m) ? imem1.push(m) :
- m in floGlobals.pubKeys ? null : imem2.push(m)
+ m in floGlobals.pubKeys ? null : imem2.push(m)
);
if (imem1.length)
return reject(`Invalid Members(floIDs): ${imem1}`)
@@ -743,7 +801,7 @@
return reject(`Invalid Members (pubKey not available): ${imem2}`)
//send new newMem list to existing members
let groupInfo = _loaded.groups[groupID]
- if (myFloID !== groupInfo.admin)
+ if (user.id !== groupInfo.admin)
return reject("Access denied: Admin only!")
let k = groupInfo.eKey;
//send groupInfo to new newMem
@@ -755,8 +813,8 @@
for (let i in results)
if (results[i].status === "fulfilled")
success.push(newMem[i])
- else if (results[i].status === "rejected")
- failed.push(newMem[i])
+ else if (results[i].status === "rejected")
+ failed.push(newMem[i])
let message = encrypt(success.join("|"), k)
sendRaw(message, groupID, "ADD_MEMBERS", false, note)
.then(r => resolve(`Members added: ${success}`))
@@ -765,7 +823,7 @@
})
}
- messenger.rmGroupMembers = function (groupID, rmMem, note = undefined) {
+ messenger.rmGroupMembers = function(groupID, rmMem, note = undefined) {
return new Promise((resolve, reject) => {
if (!Array.isArray(rmMem) && typeof rmMem === "string")
rmMem = [rmMem]
@@ -773,7 +831,7 @@
let imem = rmMem.filter(m => !groupInfo.members.includes(m))
if (imem.length)
return reject(`Invalid members: ${imem}`)
- if (myFloID !== groupInfo.admin)
+ if (user.id !== groupInfo.admin)
return reject("Access denied: Admin only!")
let message = encrypt(rmMem.join("|"), groupInfo.eKey)
let p1 = sendRaw(message, groupID, "RM_MEMBERS", false, note)
@@ -785,10 +843,10 @@
})
}
- const revokeKey = messenger.revokeKey = function (groupID) {
+ const revokeKey = messenger.revokeKey = function(groupID) {
return new Promise((resolve, reject) => {
let groupInfo = _loaded.groups[groupID]
- if (myFloID !== groupInfo.admin)
+ if (user.id !== groupInfo.admin)
return reject("Access denied: Admin only!")
let newKey = floCrypto.randString(16, false);
Promise.all(groupInfo.members.map(m => sendRaw(JSON.stringify({
@@ -800,7 +858,7 @@
})
}
- messenger.sendGroupMessage = function (message, groupID) {
+ messenger.sendGroupMessage = function(message, groupID) {
return new Promise((resolve, reject) => {
let k = _loaded.groups[groupID].eKey
message = encrypt(message, k)
@@ -810,13 +868,11 @@
})
}
- const disableGroup = messenger.disableGroup = function (groupID) {
+ const disableGroup = messenger.disableGroup = function(groupID) {
return new Promise((resolve, reject) => {
if (!_loaded.groups[groupID])
return reject("Group not found");
- let groupInfo = {
- ..._loaded.groups[groupID]
- };
+ let groupInfo = Object.assign({}, _loaded.groups[groupID]);
if (groupInfo.disabled)
return resolve("Group already diabled");
groupInfo.disabled = true;
@@ -829,7 +885,119 @@
})
}
- messenger.init = function () {
+ processData.group = function(groupID) {
+ return (unparsed, newInbox) => {
+ if (!_loaded.groups[groupID].members.includes(unparsed.senderID))
+ return;
+ //store the pubKey if not stored already
+ floDapps.storePubKey(unparsed.senderID, unparsed.pubKey)
+ let data = {
+ time: unparsed.time,
+ sender: unparsed.senderID,
+ groupID: unparsed.receiverID
+ }
+ let vc = unparsed.vectorClock,
+ k = _loaded.groups[groupID].eKey;
+ if (expiredKeys[groupID]) {
+ var ex = Object.keys(expiredKeys[groupID]).sort()
+ while (ex.length && vc > ex[0]) ex.shift()
+ if (ex.length)
+ k = expiredKeys[groupID][ex.shift()]
+ }
+ unparsed.message = decrypt(unparsed.message, k);
+ var infoChange = false;
+ if (unparsed.type === "GROUP_MSG")
+ data.message = encrypt(unparsed.message);
+ else if (data.sender === _loaded.groups[groupID].admin) {
+ let groupInfo = _loaded.groups[groupID]
+ data.admin = true;
+ switch (unparsed.type) {
+ case "ADD_MEMBERS": {
+ data.newMembers = unparsed.message.split("|")
+ data.note = unparsed.comment
+ groupInfo.members = Array.from(new Set(groupInfo.members.concat(data.newMembers)))
+ break;
+ }
+ case "UP_DESCRIPTION": {
+ data.description = unparsed.message;
+ groupInfo.description = data.description;
+ break;
+ }
+ case "RM_MEMBERS": {
+ data.rmMembers = unparsed.message.split("|")
+ data.note = unparsed.comment
+ groupInfo.members = groupInfo.members.filter(m => !data.rmMembers.includes(m))
+ if (data.rmMembers.includes(user.id)) {
+ disableGroup(groupID);
+ return;
+ }
+ break;
+ }
+ case "UP_NAME": {
+ data.name = unparsed.message
+ groupInfo.name = data.name;
+ break;
+ }
+ }
+ infoChange = true;
+ }
+ compactIDB.addData("messages", Object.assign({}, data), `${groupID}|${vc}`)
+ if (data.message)
+ data.message = decrypt(data.message);
+ newInbox.messages[vc] = data;
+ if (data.sender !== user.id)
+ addMark(data.groupID, "unread");
+ return infoChange;
+ }
+ }
+
+ function requestGroupInbox(groupID) {
+ if (groupConnID[groupID]) { //close existing request connection (if any)
+ floCloudAPI.closeRequest(groupConnID[groupID]);
+ delete groupConnID[groupID];
+ }
+
+ const parseData = processData.group(groupID);
+ let callbackFn = function(dataSet, error) {
+ if (error)
+ return console.error(error)
+ console.info(dataSet)
+ let newInbox = {
+ messages: {}
+ }
+ let infoChange = false;
+ for (let vc in dataSet) {
+ if (groupID !== dataSet[vc].receiverID)
+ continue;
+ try {
+ infoChange = parseData(dataSet[vc], newInbox) || infoChange;
+ if (!_loaded.appendix[`lastReceived_${groupID}`] ||
+ _loaded.appendix[`lastReceived_${groupID}`] < vc)
+ _loaded.appendix[`lastReceived_${groupID}`] = vc;
+ } catch (error) {
+ console.log(error)
+ }
+ }
+ compactIDB.writeData("appendix", _loaded.appendix[`lastReceived_${groupID}`], `lastReceived_${groupID}`);
+ if (infoChange) {
+ let newInfo = Object.assign({}, _loaded.groups[groupID]);
+ newInfo.eKey = encrypt(newInfo.eKey)
+ compactIDB.writeData("groups", newInfo, groupID)
+ }
+ console.debug(newInbox);
+ UI.group(newInbox);
+ }
+ floCloudAPI.requestApplicationData(null, {
+ receiverID: groupID,
+ lowerVectorClock: _loaded.appendix[`lastReceived_${groupID}`] + 1,
+ callback: callbackFn
+ }).then(conn_id => groupConnID[groupID] = conn_id)
+ .catch(error => console.error(`request-group(${groupID}):`, error))
+
+ }
+
+ //messenger startups
+ messenger.init = function() {
return new Promise((resolve, reject) => {
initUserDB().then(result => {
console.debug(result);
@@ -838,6 +1006,7 @@
//load data to memory
_loaded.appendix = data.appendix;
_loaded.groups = data.groups;
+ _loaded.pipeline = data.pipeline;
_loaded.chats = data.chats;
_loaded.marked = data.marked;
_loaded.blocked = new Set(Object.keys(data.blocked));
@@ -850,9 +1019,319 @@
for (let g in data.groups)
if (data.groups[g].disabled !== true)
requestGroupInbox(g);
- resolve("Messenger initiated");
+ for (let p in data.pipeline)
+ if (data.pipeline[p].disabled !== true)
+ requestPipelineInbox(p, data.pipeline[p].model);
+ loadDataFromBlockchain()
+ .then(result => resolve("Messenger initiated"))
+ .catch(error => reject(error))
}).catch(error => reject(error));
})
})
}
+
+ const loadDataFromBlockchain = messenger.loadDataFromBlockchain = function() {
+ return new Promise((resolve, reject) => {
+ let user_floID = floCrypto.toFloID(user.id);
+ if (!user_floID)
+ return reject("Not an valid address");
+ let last_key = `${floGlobals.application}|${user_floID}`;
+ compactIDB.readData("lastTx", last_key, floDapps.root).then(lastTx => {
+ floBlockchainAPI.readData(user_floID, {
+ ignoreOld: lastTx,
+ pattern: floGlobals.application,
+ tx: true //need tx-time and txid for key construction
+ }).then(result => {
+ for (var i = result.data.length - 1; i >= 0; i--) {
+ let tx = result.data[i],
+ content = JSON.parse(tx.data)[floGlobals.application];
+ if (!(content instanceof Object))
+ continue;
+ let key = (content.type ? content.type + "|" : "") + tx.txid.substr(0, 16);
+ compactIDB.writeData("flodata", {
+ time: tx.time,
+ txid: tx.txid,
+ data: content
+ }, key);
+ }
+ compactIDB.writeData("lastTx", result.totalTxs, last_key, floDapps.root);
+ resolve(true);
+ }).catch(error => reject(error))
+ }).catch(error => reject(error))
+ })
+ }
+
+ //BTC multisig application
+ const MultiSig = messenger.multisig = {}
+ const TYPE_BTC_MULTISIG = "btc_multisig";
+ MultiSig.createAddress = function(pubKeys, minRequired) {
+ return new Promise(async (resolve, reject) => {
+ let co_owners = pubKeys.map(p => floCrypto.getFloID(p));
+ if (co_owners.includes(null))
+ return reject("Invalid public key: " + pubKeys[co_owners.indexOf(null)]);
+ let privateKey = await floDapps.user.private;
+ let multisig = btcOperator.multiSigAddress(pubKeys, minRequired) //TODO: change to correct function
+ if (typeof multisig !== 'object')
+ return reject("Unable to create multisig address");
+ let content = {
+ type: TYPE_BTC_MULTISIG,
+ address: multisig.address, //TODO: maybe encrypt the address
+ redeemScript: multisig.redeemScript
+ };
+ console.debug(content.address, content.redeemScript);
+ debugger;
+ floBlockchainAPI.writeDataMultiple([privateKey], JSON.stringify({
+ [floGlobals.application]: content
+ }), co_owners).then(txid => {
+ console.info(txid);
+ let key = TYPE_BTC_MULTISIG + "|" + txid.substr(0, 16);
+ compactIDB.writeData("flodata", {
+ time: null, //time will be overwritten when confirmed on blockchain
+ txid: txid,
+ data: content
+ }, key);
+ resolve(multisig.address);
+ }).catch(error => reject(error))
+ })
+ }
+
+ MultiSig.listAddress = function() {
+ return new Promise((resolve, reject) => {
+ let options = {
+ lowerKey: `${TYPE_BTC_MULTISIG}|`,
+ upperKey: `${TYPE_BTC_MULTISIG}||`
+ }
+ compactIDB.searchData("flodata", options).then(result => {
+ let multsigs = {};
+ for (let i in result) {
+ let addr = result[i].data.address;
+ let decode = coinjs.script().decodeRedeemScript(result[i].data.redeemScript);
+ if (!decode || decode.address !== addr)
+ console.warn("Invalid redeem-script:", addr);
+ else if (decode.type !== "multisig__")
+ console.warn("Redeem-script is not of a multisig:", addr);
+ else if (!decode.pubkeys.includes(user.public.toLowerCase()) && !decode.pubkeys.includes(user.public.toUpperCase()))
+ console.warn("User is not a part of this multisig:", addr);
+ else if (decode.pubkeys.length < decode.signaturesRequired)
+ console.warn("Invalid multisig (required is greater than users):", addr);
+ else
+ multsigs[addr] = {
+ redeemScript: decode.redeemscript,
+ pubKeys: decode.pubkeys,
+ minRequired: decode.signaturesRequired
+ }
+ }
+ resolve(multsigs);
+ }).catch(error => reject(error))
+ })
+ }
+
+ MultiSig.createTx = function(address, redeemScript, receivers, amounts, fee = null) {
+ return new Promise(async (resolve, reject) => {
+ let decode = coinjs.script().decodeRedeemScript(redeemScript);
+ if (!decode || decode.address !== address || decode.type !== "multisig__")
+ return reject("Invalid redeem-script");
+ else if (!decode.pubkeys.includes(user.public.toLowerCase()) && !decode.pubkeys.includes(user.public.toUpperCase()))
+ return reject("User is not a part of this multisig");
+ else if (decode.pubkeys.length < decode.signaturesRequired)
+ return reject("Invalid multisig (required is greater than users)");
+ let co_owners = decode.pubkeys.map(p => floCrypto.getFloID(p));
+ let privateKey = await floDapps.user.private;
+ btcOperator.createMultiSigTx(address, redeemScript, receivers, amounts, fee).then(tx => {
+ tx = btcOperator.signTx(tx, privateKey);
+ createPipeline(TYPE_BTC_MULTISIG, co_owners, 32).then(pipeline => {
+ let message = encrypt(tx, pipeline.eKey);
+ sendRaw(message, pipeline.id, "TRANSACTION", false)
+ .then(result => resolve(pipeline.id))
+ .catch(error => reject(error))
+ }).catch(error => reject(error))
+ }).catch(error => reject(error))
+ })
+ }
+
+ MultiSig.signTx = function(pipeID) {
+ return new Promise((resolve, reject) => {
+ if (_loaded.pipeline[pipeID].disabled)
+ return reject("Pipeline is already closed");
+ getChat(pipeID).then(async result => {
+ let pipeline = _loaded.pipeline[pipeID],
+ tx_hex_latest = Object.keys(result).sort().map(i => result[i].tx_hex).filter(x => x).pop();
+ let privateKey = await floDapps.user.private;
+ let tx_hex_signed = btcOperator.signTx(tx_hex_latest, privateKey);
+ let message = encrypt(tx_hex_signed, pipeline.eKey);
+ sendRaw(message, pipeline.id, "TRANSACTION", false).then(result => {
+ if (!btcOperator.checkSigned(tx_hex_signed))
+ return resolve({
+ tx_hex: tx_hex_signed
+ });
+ debugger;
+ btcOperator.broadcast(tx_hex_signed).then(result => {
+ let txid = result.txid;
+ console.debug(txid);
+ sendRaw(encrypt(txid, pipeline.eKey), pipeline.id, "BROADCAST", false)
+ .then(result => resolve({
+ tx_hex: tx_hex_signed,
+ txid: txid
+ })).catch(error => reject(error))
+ }).catch(error => reject(error))
+ }).catch(error => reject(error))
+ }).catch(error => console.error(error))
+ })
+ }
+
+ //Pipelines
+ const createPipeline = function(model, members, ekeySize = 16) {
+ return new Promise((resolve, reject) => {
+ //validate members
+ let imem1 = [],
+ imem2 = []
+ members.forEach(m =>
+ !floCrypto.validateAddr(m) ? imem1.push(m) :
+ m in floGlobals.pubKeys ? null :
+ m != user.id ? imem2.push(m) : null
+ );
+ if (imem1.length)
+ return reject(`Invalid Members(floIDs): ${imem1}`);
+ else if (imem2.length)
+ return reject(`Invalid Members (pubKey not available): ${imem2}`);
+ //create pipeline info
+ const id = floCrypto.tmpID;
+ let pipeline = {
+ id,
+ model,
+ members
+ }
+ if (ekeySize)
+ pipeline.eKey = floCrypto.randString(ekeySize);
+ //send pipeline info to members
+ let pipelineInfo = JSON.stringify(pipeline);
+ let promises = members.filter(m => m != user.id).map(m => sendRaw(pipelineInfo, m, "CREATE_PIPELINE", true));
+ Promise.allSettled(promises).then(results => {
+ console.debug(results.filter(r => r.status === "rejected").map(r => r.reason));
+ _loaded.pipeline[pipeline.id] = Object.assign({}, pipeline);
+ if (pipeline.eKey)
+ pipeline.eKey = encrypt(pipeline.eKey);
+ compactIDB.addData("pipeline", pipeline, pipeline.id).then(result => {
+ requestPipelineInbox(pipeline.id, pipeline.model);
+ resolve(_loaded.pipeline[pipeline.id])
+ }).catch(error => reject(error))
+ })
+ })
+ }
+
+ function requestPipelineInbox(pipeID, model) {
+ if (pipeConnID[pipeID]) { //close existing request connection (if any)
+ floCloudAPI.closeRequest(pipeConnID[pipeID]);
+ delete pipeConnID[pipeID];
+ }
+
+ let parseData = processData.pipeline[model](pipeID);
+ let callbackFn = function(dataSet, error) {
+ if (error)
+ return console.error(error);
+ console.info(dataSet)
+ let newInbox = {
+ messages: {}
+ }
+ for (let vc in dataSet) {
+ if (pipeID !== dataSet[vc].receiverID)
+ continue;
+ try {
+ parseData(dataSet[vc], newInbox);
+ if (dataSet[vc].senderID !== user.id)
+ addMark(pipeID, "unread")
+ if (!_loaded.appendix[`lastReceived_${pipeID}`] ||
+ _loaded.appendix[`lastReceived_${pipeID}`] < vc)
+ _loaded.appendix[`lastReceived_${pipeID}`] = vc;
+ } catch (error) {
+ console.log(error)
+ }
+ }
+ compactIDB.writeData("appendix", _loaded.appendix[`lastReceived_${pipeID}`], `lastReceived_${pipeID}`);
+ console.debug(newInbox);
+ UI.pipeline(model, newInbox);
+ }
+
+ floCloudAPI.requestApplicationData(null, {
+ receiverID: pipeID,
+ lowerVectorClock: _loaded.appendix[`lastReceived_${pipeID}`] + 1,
+ callback: callbackFn
+ }).then(conn_id => pipeConnID[pipeID] = conn_id)
+ .catch(error => console.error(`request-pipeline(${pipeID}):`, error))
+ }
+
+ const disablePipeline = messenger.disablePipeline = function(pipeID) {
+ console.debug(JSON.stringify(pipeConnID), pipeConnID[pipeID])
+ return new Promise((resolve, reject) => {
+ if (!_loaded.pipeline[pipeID])
+ return reject("Pipeline not found");
+ if (_loaded.pipeline[pipeID].disabled)
+ return resolve("Pipeline already diabled");
+ _loaded.pipeline[pipeID].disabled = true;
+ let pipelineInfo = Object.assign({}, _loaded.pipeline[pipeID]);
+ pipelineInfo.eKey = encrypt(pipelineInfo.eKey)
+ compactIDB.writeData("pipeline", pipelineInfo, pipeID).then(result => {
+ floCloudAPI.closeRequest(pipeConnID[pipeID]);
+ delete pipeConnID[pipeID];
+ resolve("Pipeline diabled");
+ }).catch(error => reject(error))
+ })
+ }
+
+ messenger.sendPipelineMessage = function(message, pipeID) {
+ return new Promise((resolve, reject) => {
+ let k = _loaded.pipeline[pipeID].eKey;
+ if (k) message = encrypt(message, k);
+ sendRaw(message, pipeID, "MESSAGE", false)
+ .then(result => resolve(`${pipeID}: ${message}`))
+ .catch(error => reject(error))
+ })
+ }
+
+ processData.pipeline = {};
+ processData.pipeline[TYPE_BTC_MULTISIG] = function(pipeID) {
+ return (unparsed, newInbox) => {
+ if (!_loaded.pipeline[pipeID].members.includes(floCrypto.toFloID(unparsed.senderID)))
+ return;
+ let data = {
+ time: unparsed.time,
+ sender: unparsed.senderID,
+ pipeID: unparsed.receiverID
+ }
+ let vc = unparsed.vectorClock,
+ k = _loaded.pipeline[pipeID].eKey;
+ unparsed.message = decrypt(unparsed.message, k)
+ //store the pubKey if not stored already
+ floDapps.storePubKey(unparsed.senderID, unparsed.pubKey);
+ data.type = unparsed.type;
+ switch (unparsed.type) {
+ case "TRANSACTION": {
+ data.tx_hex = unparsed.message;
+ break;
+ }
+ case "BROADCAST": {
+ data.txid = unparsed.message;
+ //the following check is done on parallel (in background) instead of sync
+ btcOperator.getTx(data.txid).then(tx => {
+ let tx_hex_final = tx.tx_hex;
+ getChat(pipeID).then(result => {
+ let tx_hex_inital = Object.keys(result).sort().map(i => result[i].message).filter(x => x).shift();
+ if (btcOperator.checkIfSameTx(tx_hex_inital, tx_hex_final))
+ disablePipeline(pipeID);
+ }).catch(error => console.error(error))
+ }).catch(error => console.error(error))
+ break;
+ }
+ case "MESSAGE": {
+ data.message = encrypt(unparsed.message);
+ break;
+ }
+ }
+ compactIDB.addData("messages", Object.assign({}, data), `${pipeID}|${vc}`);
+ if (data.message)
+ data.message = decrypt(data.message);
+ newInbox.messages[vc] = data;
+ }
+ }
+
})();
\ No newline at end of file
diff --git a/scripts/script.js b/scripts/script.js
deleted file mode 100644
index df72c34..0000000
--- a/scripts/script.js
+++ /dev/null
@@ -1,194 +0,0 @@
-/* settings component */
-const settingsMenu = document.createElement('template');
-settingsMenu.innerHTML = `
-
-
-
-`;
-
-customElements.define('settings-menu', class extends HTMLElement {
- constructor() {
- super()
- this.attachShadow({
- mode: 'open'
- }).append(settingsMenu.content.cloneNode(true))
-
- this.settingsContainer = this.shadowRoot.querySelector('.settingsContainer');
- this.settingsIcon = this.shadowRoot.querySelector('.settingsIcon'); //svg icon
- this.menu = this.shadowRoot.querySelector('.menu'); // for the main menu
- this.menuContainer = this.shadowRoot.querySelector('.menuContainer');
- this.menuSlot = this.shadowRoot.querySelector('.menuSlot');
- this.closeMenu; // for close icons
- this.count = 0; // a counter
- this.menuIndex;
- }
-
-
-
- connectedCallback() {
-
- // to rotate the svg icon and display the main menu
- this.settingsIcon.addEventListener('click', e => {
-
- if(this.menuIndex){
- this.shadowRoot.querySelector('.submenu'+(this.menuIndex)).style.visibility = "hidden";
- }
-
-
- if(this.count%2==0){
-
- this.settingsIcon.classList.add('svgClicked');
- this.menu.style.visibility = "visible";
-
- }else{
-
- this.settingsIcon.classList.remove('svgClicked');
- this.menu.style.visibility = "hidden";
- /* I know instead of this I should have added and removed a class but it was not working */
-
- }
-
- this.count++;
-
- });
-
- // creating slots for sub menu
- const frag = document.createDocumentFragment();
-
- this.menuSlot.assignedElements().forEach( (menuOption , index) =>{
-
- let submenu = document.createElement('div');
- let submenuSlot = document.createElement('slot');
- let submenuTitle = document.createElement('div');
-
- // adding a heading and a close icon to a sub menu
- submenuTitle.innerHTML = " " + menuOption.innerHTML;
-
- menuOption.setAttribute("index", (index+1));
- submenuSlot.setAttribute('name', 'menu' + (index+1));
- submenu.classList.add('submenu');
- submenu.classList.add('submenu' + (index+1));
-
- submenu.append(submenuTitle);
- submenu.append(submenuSlot);
- frag.append(submenu);
-
- });
-
-
- this.menuContainer.append(frag);
- this.closeMenu = this.shadowRoot.querySelectorAll('.closeMenu'); // get a list of closeIcons
-
- // adding click event to main menu to open respective sub menu
-
- this.menu.addEventListener( 'click', e=> {
-
- let menuOption = e.target.closest('div');
-
- this.menuIndex = menuOption.getAttribute('index');
-
- let submenu = this.shadowRoot.querySelector('.submenu'+(this.menuIndex));
-
- submenu.style.visibility="visible";
- this.menu.style.visibility = "hidden";
-
- });
-
- // adding click event to close Icons and adding code to hide the respective sub menu
-
- this.closeMenu.forEach((closeButton) =>{
- closeButton.addEventListener('click', e=>{
- let submenuIndex = closeButton.getAttribute('index');
-
- this.shadowRoot.querySelector('.submenu'+(submenuIndex)).style.visibility = "hidden";
-
- this.menu.style.visibility = "visible";
-
- });
- });
-
-
- }
-
-});
\ No newline at end of file