From 92303cbc47ed065d3e7999bb036860258e05ccc5 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Sat, 13 May 2023 18:18:23 +0530 Subject: [PATCH 01/13] floBlockchainAPI v2.5.6b: bug fix --- floBlockchainAPI.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/floBlockchainAPI.js b/floBlockchainAPI.js index 460775b..6fec5b8 100644 --- a/floBlockchainAPI.js +++ b/floBlockchainAPI.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floBlockchainAPI v2.5.6a +(function (EXPORTS) { //floBlockchainAPI v2.5.6b /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/ 'use strict'; const floBlockchainAPI = EXPORTS; @@ -843,7 +843,7 @@ query_params.to = options.to; } if (!isUndefined(options.latest)) - query_params.latest = latest; + query_params.latest = options.latest; if (!isUndefined(options.mempool)) query_params.mempool = options.mempool; promisedAPI(api, query_params) From 34c30aea05c6df6c2f0af9aed1d80fa7328614e9 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Thu, 1 Jun 2023 20:03:42 +0530 Subject: [PATCH 02/13] btcOperator v1.1.3: increase fee - editFee: increase the fee for the given transaction. can pass either tx hex directly to edit it, or pass txid of unconfirmed tx to edit it for rebroadcasting - transaction outputs will be removed if the value is less than DUST amount (546 satoshi) --- btcOperator.js | 114 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 7 deletions(-) diff --git a/btcOperator.js b/btcOperator.js index ff86fb9..f61f605 100644 --- a/btcOperator.js +++ b/btcOperator.js @@ -1,10 +1,12 @@ -(function (EXPORTS) { //btcOperator v1.1.2a +(function (EXPORTS) { //btcOperator v1.1.3 /* BTC Crypto and API Operator */ const btcOperator = EXPORTS; //This library uses API provided by chain.so (https://chain.so/) const URL = "https://blockchain.info/"; + const DUST_AMT = 546; + const fetch_api = btcOperator.fetch = function (api, json_res = true) { return new Promise((resolve, reject) => { console.debug(URL + api); @@ -400,7 +402,7 @@ return reject("Send amount is less than fee"); } - tx.outs = tx.outs.filter(o => o.value != 0); //remove all output with value 0 + tx.outs = tx.outs.filter(o => o.value <= DUST_AMT); //remove all output with value less than DUST amount result.output_size = output_size; result.output_amount = total_amount - (fee_from_receiver ? result.fee : 0); result.total_size = BASE_TX_SIZE + output_size + result.input_size; @@ -456,7 +458,7 @@ let size_per_input = _sizePerInput(addr, rs); fetch_api(`unspent?active=${addr}`).then(result => { let utxos = result.unspent_outputs; - console.debug("add-utxo", addr, rs, required_amount, utxos); + //console.debug("add-utxo", addr, rs, required_amount, utxos); for (let i = 0; i < utxos.length && required_amount > 0; i++) { if (!utxos[i].confirmations) //ignore unconfirmed utxo continue; @@ -536,6 +538,104 @@ } */ + function tx_fetch_for_editing(tx) { + return new Promise((resolve, reject) => { + if (typeof tx == 'string' && /^[0-9a-f]{64}$/i.test(tx)) { //tx is txid + getTx.hex(tx) + .then(txhex => resolve(deserializeTx(txhex))) + .catch(error => reject(error)) + } else resolve(deserializeTx(tx)); + }) + } + + + btcOperator.editFee = function (tx_hex, new_fee, private_keys, change_only = true) { + return new Promise((resolve, reject) => { + if (!Array.isArray(private_keys)) + private_keys = [private_keys]; + tx_fetch_for_editing(tx_hex).then(tx => { + parseTransaction(tx).then(tx_parsed => { + if (tx_parsed.fee >= new_fee) + return reject("Fees can only be increased"); + + //editable addresses in output values (for fee increase) + var edit_output_address = new Set(); + if (change_only === true) //allow only change values (ie, sender address) to be edited to inc fee + tx_parsed.inputs.forEach(inp => edit_output_address.add(inp.address)); + else if (change_only === false) //allow all output values to be edited + tx_parsed.outputs.forEach(out => edit_output_address.add(out.address)); + else if (typeof change_only == 'string') // allow only given receiver id output to be edited + edit_output_address.add(change_only); + else if (Array.isArray(change_only)) //allow only given set of receiver id outputs to be edited + change_only.forEach(id => edit_output_address.add(id)); + + //edit output values to increase fee + let inc_fee = util.BTC_to_Sat(new_fee - tx_parsed.fee); + for (let i = tx.outs.length - 1; i >= 0 && inc_fee > 0; i--) //reduce in reverse order + if (edit_output_address.has(tx_parsed.outputs[i].address)) { + let current_value = tx.outs[i].value; + if (current_value instanceof BigInteger) //convert BigInteger class to inv value + current_value = current_value.intValue(); + //edit the value as required + if (current_value > inc_fee) { + tx.out[i].value = current_value - inc_fee; + inc_fee = 0; + } else { + inc_fee -= current_value; + tx.out[i].value = 0; + } + } + if (inc_fee > 0) { + let max_possible_fee = util.BTC_to_Sat(new_fee) - inc_fee; //in satoshi + return reject(`Insufficient output values to increase fee. Maximum fee possible: ${util.Sat_to_BTC(max_possible_fee)}`); + } + tx.outs = tx.outs.filter(o => o.value <= DUST_AMT); //remove all output with value less than DUST amount + + //remove existing signatures and reset the scripts + let wif_keys = []; + for (let i in tx.ins) { + var addr = tx_parsed.inputs[i].address, + value = util.BTC_to_Sat(tx_parsed.inputs[i].value); + let addr_decode = coinjs.addressDecode(addr); + //find the correct key for addr + var privKey = private_keys.find(pk => verifyKey(addr, pk)); + if (!privKey) + return reject(`Private key missing for ${addr}`); + //find redeemScript (if any) + const rs = _redeemScript(addr, privKey); + rs === false ? wif_keys.unshift(privKey) : wif_keys.push(privKey); //sorting private-keys (wif) + //reset the script for re-signing + var script; + if (!rs || !rs.length) { + //legacy script (derive from address) + let s = coinjs.script(); + s.writeOp(118); //OP_DUP + s.writeOp(169); //OP_HASH160 + s.writeBytes(addr_decode.bytes); + s.writeOp(136); //OP_EQUALVERIFY + s.writeOp(172); //OP_CHECKSIG + script = Crypto.util.bytesToHex(s.buffer); + } else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi)) || addr_decode.type === 'multisigBech32') { + //redeemScript for segwit/bech32 and multisig (bech32) + let s = coinjs.script(); + s.writeBytes(Crypto.util.hexToBytes(rs)); + s.writeOp(0); + s.writeBytes(coinjs.numToBytes(value.toFixed(0), 8)); + script = Crypto.util.bytesToHex(s.buffer); + } else //redeemScript for multisig (segwit) + script = rs; + tx.ins[i].script = coinjs.script(script); + } + tx.witness = false; //remove all witness signatures + console.debug("Unsigned:", tx.serialize()); + //re-sign the transaction + new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF + resolve(tx.serialize()); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + btcOperator.sendTx = function (senders, privkeys, receivers, amounts, fee = null, options = {}) { return new Promise((resolve, reject) => { createSignedTx(senders, privkeys, receivers, amounts, fee, options).then(result => { @@ -579,7 +679,7 @@ createTransaction(senders, redeemScripts, receivers, amounts, fee, options.change_address || senders[0], options.fee_from_receiver).then(result => { let tx = result.transaction; console.debug("Unsigned:", tx.serialize()); - new Set(wif_keys).forEach(key => console.debug("Signing key:", key, tx.sign(key, 1 /*sighashtype*/))); //Sign the tx using private key WIF + new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF console.debug("Signed:", tx.serialize()); resolve(result); }).catch(error => reject(error)); @@ -723,7 +823,7 @@ .catch(error => reject(error)) }); - btcOperator.parseTransaction = function (tx) { + const parseTransaction = btcOperator.parseTransaction = function (tx) { return new Promise((resolve, reject) => { tx = deserializeTx(tx); let result = {}; @@ -780,7 +880,7 @@ .catch(error => reject(error)) }) - btcOperator.getTx = txid => new Promise((resolve, reject) => { + const getTx = btcOperator.getTx = txid => new Promise((resolve, reject) => { fetch_api(`rawtx/${txid}`).then(result => { getLatestBlock().then(latest_block => resolve({ block: result.block_height, @@ -797,7 +897,7 @@ }).catch(error => reject(error)) }); - btcOperator.getTx.hex = txid => new Promise((resolve, reject) => { + getTx.hex = txid => new Promise((resolve, reject) => { fetch_api(`rawtx/${txid}?format=hex`, false) .then(result => resolve(result)) .catch(error => reject(error)) From 365d3d450324958bbe654f83c6d4d44a34aaa882 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Thu, 1 Jun 2023 21:06:04 +0530 Subject: [PATCH 03/13] btcOperator v1.1.3a: bug fix - Fixed: minor syntax errors and typos - Fixed: createTransaction not giving correct fee and output value when dust is ignored --- btcOperator.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/btcOperator.js b/btcOperator.js index f61f605..6d45a62 100644 --- a/btcOperator.js +++ b/btcOperator.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //btcOperator v1.1.3 +(function (EXPORTS) { //btcOperator v1.1.3a /* BTC Crypto and API Operator */ const btcOperator = EXPORTS; @@ -402,7 +402,12 @@ return reject("Send amount is less than fee"); } - tx.outs = tx.outs.filter(o => o.value <= DUST_AMT); //remove all output with value less than DUST amount + //remove all output with value less than DUST amount + let filtered_outputs = [], dust_value = 0; + tx.outs.forEach(o => o.value >= DUST_AMT ? filtered_outputs.push(o) : dust_value += o.value); + tx.outs = filtered_outputs; + //update result values + result.fee += util.Sat_to_BTC(dust_value); result.output_size = output_size; result.output_amount = total_amount - (fee_from_receiver ? result.fee : 0); result.total_size = BASE_TX_SIZE + output_size + result.input_size; @@ -578,18 +583,18 @@ current_value = current_value.intValue(); //edit the value as required if (current_value > inc_fee) { - tx.out[i].value = current_value - inc_fee; + tx.outs[i].value = current_value - inc_fee; inc_fee = 0; } else { inc_fee -= current_value; - tx.out[i].value = 0; + tx.outs[i].value = 0; } } if (inc_fee > 0) { let max_possible_fee = util.BTC_to_Sat(new_fee) - inc_fee; //in satoshi return reject(`Insufficient output values to increase fee. Maximum fee possible: ${util.Sat_to_BTC(max_possible_fee)}`); } - tx.outs = tx.outs.filter(o => o.value <= DUST_AMT); //remove all output with value less than DUST amount + tx.outs = tx.outs.filter(o => o.value >= DUST_AMT); //remove all output with value less than DUST amount //remove existing signatures and reset the scripts let wif_keys = []; From 49d5c2411c28a8781f2abc1f80974d8bade34d35 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Thu, 1 Jun 2023 21:09:50 +0530 Subject: [PATCH 04/13] btcOperator v1.1.3b: fix - Fixed: fee increment minimum value Minimum fee increment should be atleast MIN_FEE_UPDATE (219 satoshi) --- btcOperator.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/btcOperator.js b/btcOperator.js index 6d45a62..2abb80f 100644 --- a/btcOperator.js +++ b/btcOperator.js @@ -1,11 +1,12 @@ -(function (EXPORTS) { //btcOperator v1.1.3a +(function (EXPORTS) { //btcOperator v1.1.3b /* BTC Crypto and API Operator */ const btcOperator = EXPORTS; //This library uses API provided by chain.so (https://chain.so/) const URL = "https://blockchain.info/"; - const DUST_AMT = 546; + const DUST_AMT = 546, + MIN_FEE_UPDATE = 219; const fetch_api = btcOperator.fetch = function (api, json_res = true) { return new Promise((resolve, reject) => { @@ -576,6 +577,8 @@ //edit output values to increase fee let inc_fee = util.BTC_to_Sat(new_fee - tx_parsed.fee); + if (inc_fee < MIN_FEE_UPDATE) + return reject(`Insufficient additional fee. Minimum increment: ${MIN_FEE_UPDATE}`); for (let i = tx.outs.length - 1; i >= 0 && inc_fee > 0; i--) //reduce in reverse order if (edit_output_address.has(tx_parsed.outputs[i].address)) { let current_value = tx.outs[i].value; From d15af057647472d6e4a592ee6c0b1ebb8412aae3 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Tue, 4 Jul 2023 00:20:24 +0530 Subject: [PATCH 05/13] floBlockchainAPI v3.0.0: BlockBook API - Converting API calls to FLO blockbook API - Changes and fixes required for the same --- floBlockchainAPI.js | 271 +++++++++++++++++++------------------------- 1 file changed, 117 insertions(+), 154 deletions(-) diff --git a/floBlockchainAPI.js b/floBlockchainAPI.js index 6fec5b8..589279f 100644 --- a/floBlockchainAPI.js +++ b/floBlockchainAPI.js @@ -1,13 +1,13 @@ -(function (EXPORTS) { //floBlockchainAPI v2.5.6b - /* FLO Blockchain Operator to send/receive data from blockchain using API calls*/ +(function (EXPORTS) { //floBlockchainAPI v3.0.0 + /* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/ 'use strict'; const floBlockchainAPI = EXPORTS; const DEFAULT = { blockchain: floGlobals.blockchain, apiURL: { - FLO: ['https://flosight.ranchimall.net/'], - FLO_TEST: ['https://flosight-testnet.ranchimall.net/'] + FLO: ['https://blockbook.ranchimall.net/'], + FLO_TEST: [] }, sendAmt: 0.0003, fee: 0.0002, @@ -61,9 +61,9 @@ var serverList = Array.from(allServerList); var curPos = floCrypto.randInt(0, serverList.length - 1); - function fetch_retry(apicall, rm_flosight) { + function fetch_retry(apicall, rm_node) { return new Promise((resolve, reject) => { - let i = serverList.indexOf(rm_flosight) + let i = serverList.indexOf(rm_node) if (i != -1) serverList.splice(i, 1); curPos = floCrypto.randInt(0, serverList.length - 1); fetch_api(apicall, false) @@ -82,19 +82,19 @@ .then(result => resolve(result)) .catch(error => reject(error)); } else - reject("No floSight server working"); + reject("No FLO blockbook server working"); } else { - let flosight = serverList[curPos]; - fetch(flosight + apicall).then(response => { + let serverURL = serverList[curPos]; + fetch(serverURL + apicall).then(response => { if (response.ok) response.json().then(data => resolve(data)); else { - fetch_retry(apicall, flosight) + fetch_retry(apicall, serverURL) .then(result => resolve(result)) .catch(error => reject(error)); } }).catch(error => { - fetch_retry(apicall, flosight) + fetch_retry(apicall, serverURL) .then(result => resolve(result)) .catch(error => reject(error)); }) @@ -124,43 +124,27 @@ } //Get balance for the given Address - const getBalance = floBlockchainAPI.getBalance = function (addr, after = null) { + const getBalance = floBlockchainAPI.getBalance = function (addr) { return new Promise((resolve, reject) => { - let api = `api/addr/${addr}/balance`, query_params = {}; - if (after) { - if (typeof after === 'string' && /^[0-9a-z]{64}$/i.test(after)) - query_params.after = after; - else return reject("Invalid 'after' parameter"); - } - promisedAPI(api, query_params).then(result => { - if (typeof result === 'object' && result.lastItem) { - getBalance(addr, result.lastItem) - .then(r => resolve(util.toFixed(r + result.data))) - .catch(error => reject(error)) - } else resolve(result); - }).catch(error => reject(error)) + let api = `api/address/${addr}`; + promisedAPI(api, { details: "basic" }) + .then(result => resolve(result["balance"])) + .catch(error => reject(error)) }); } - const getUTXOs = address => new Promise((resolve, reject) => { - promisedAPI(`api/addr/${address}/utxo`) - .then(utxo => resolve(utxo)) - .catch(error => reject(error)) - }) + function getScriptPubKey(address) { + var tx = bitjs.transaction(); + tx.addoutput(address, 0); + let outputBuffer = tx.outputs.pop().script; + return Crypto.util.bytesToHex(outputBuffer) + } - const getUnconfirmedSpent = address => new Promise((resolve, reject) => { - readTxs(address, { mempool: "only" }).then(result => { - let unconfirmedSpent = {}; - for (let tx of result.items) - if (tx.confirmations == 0) - for (let vin of tx.vin) - if (vin.addr === address) { - if (Array.isArray(unconfirmedSpent[vin.txid])) - unconfirmedSpent[vin.txid].push(vin.vout); - else - unconfirmedSpent[vin.txid] = [vin.vout]; - } - resolve(unconfirmedSpent); + const getUTXOs = address => new Promise((resolve, reject) => { + promisedAPI(`api/utxo/${address}`, { confirmed: true }).then(utxos => { + let scriptPubKey = getScriptPubKey(address); + utxos.forEach(u => u.scriptPubKey = scriptPubKey); + resolve(utxos); }).catch(error => reject(error)) }) @@ -180,32 +164,28 @@ var fee = DEFAULT.fee; if (balance < sendAmt + fee) return reject("Insufficient FLO balance!"); - getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => { - getUTXOs(senderAddr).then(utxos => { - //form/construct the transaction data - var trx = bitjs.transaction(); - var utxoAmt = 0.0; - for (var i = utxos.length - 1; - (i >= 0) && (utxoAmt < sendAmt + fee); i--) { - //use only utxos with confirmations (strict_utxo mode) - if (utxos[i].confirmations || !strict_utxo) { - if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) - continue; //A transaction has already used the utxo, but is unconfirmed. - trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); - utxoAmt += utxos[i].amount; - }; - } - if (utxoAmt < sendAmt + fee) - reject("Insufficient FLO: Some UTXOs are unconfirmed"); - else { - trx.addoutput(receiverAddr, sendAmt); - var change = utxoAmt - sendAmt - fee; - if (change > DEFAULT.minChangeAmt) - trx.addoutput(senderAddr, change); - trx.addflodata(floData.replace(/\n/g, ' ')); - resolve(trx); - } - }).catch(error => reject(error)) + getUTXOs(senderAddr).then(utxos => { + //form/construct the transaction data + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (var i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < sendAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + trx.addoutput(receiverAddr, sendAmt); + var change = utxoAmt - sendAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(senderAddr, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + resolve(trx); + } }).catch(error => reject(error)) }).catch(error => reject(error)) }) @@ -293,34 +273,30 @@ if (balance < totalAmt + fee) return reject("Insufficient FLO balance!"); //get unconfirmed tx list - getUnconfirmedSpent(floID).then(unconfirmedSpent => { - getUTXOs(floID).then(utxos => { - var trx = bitjs.transaction(); - var utxoAmt = 0.0; - for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + fee); i--) { - //use only utxos with confirmations (strict_utxo mode) - if (utxos[i].confirmations || !strict_utxo) { - if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) - continue; //A transaction has already used the utxo, but is unconfirmed. - trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); - utxoAmt += utxos[i].amount; - }; - } - if (utxoAmt < totalAmt + fee) - reject("Insufficient FLO: Some UTXOs are unconfirmed"); - else { - for (let i = 0; i < count; i++) - trx.addoutput(floID, splitAmt); - var change = utxoAmt - totalAmt - fee; - if (change > DEFAULT.minChangeAmt) - trx.addoutput(floID, change); - trx.addflodata(floData.replace(/\n/g, ' ')); - var signedTxHash = trx.sign(privKey, 1); - broadcastTx(signedTxHash) - .then(txid => resolve(txid)) - .catch(error => reject(error)) - } - }).catch(error => reject(error)) + getUTXOs(floID).then(utxos => { + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey); + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < totalAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + for (let i = 0; i < count; i++) + trx.addoutput(floID, splitAmt); + var change = utxoAmt - totalAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(floID, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + var signedTxHash = trx.sign(privKey, 1); + broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + } }).catch(error => reject(error)) }).catch(error => reject(error)) }) @@ -551,33 +527,29 @@ var fee = DEFAULT.fee; if (balance < sendAmt + fee) return reject("Insufficient FLO balance!"); - getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => { - getUTXOs(senderAddr).then(utxos => { - //form/construct the transaction data - var trx = bitjs.transaction(); - var utxoAmt = 0.0; - for (var i = utxos.length - 1; - (i >= 0) && (utxoAmt < sendAmt + fee); i--) { - //use only utxos with confirmations (strict_utxo mode) - if (utxos[i].confirmations || !strict_utxo) { - if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout)) - continue; //A transaction has already used the utxo, but is unconfirmed. - trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript - utxoAmt += utxos[i].amount; - }; - } - if (utxoAmt < sendAmt + fee) - reject("Insufficient FLO: Some UTXOs are unconfirmed"); - else { - for (let i in receivers) - trx.addoutput(receivers[i], amounts[i]); - var change = utxoAmt - sendAmt - fee; - if (change > DEFAULT.minChangeAmt) - trx.addoutput(senderAddr, change); - trx.addflodata(floData.replace(/\n/g, ' ')); - resolve(trx); - } - }).catch(error => reject(error)) + getUTXOs(senderAddr).then(utxos => { + //form/construct the transaction data + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + for (var i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt + fee); i--) { + //use only utxos with confirmations (strict_utxo mode) + if (utxos[i].confirmations || !strict_utxo) { + trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript + utxoAmt += utxos[i].amount; + }; + } + if (utxoAmt < sendAmt + fee) + reject("Insufficient FLO: Some UTXOs are unconfirmed"); + else { + for (let i in receivers) + trx.addoutput(receivers[i], amounts[i]); + var change = utxoAmt - sendAmt - fee; + if (change > DEFAULT.minChangeAmt) + trx.addoutput(senderAddr, change); + trx.addflodata(floData.replace(/\n/g, ' ')); + resolve(trx); + } }).catch(error => reject(error)) }).catch(error => reject(error)) }); @@ -773,20 +745,11 @@ const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) { return new Promise((resolve, reject) => { if (signedTxHash.length < 1) - return reject("Empty Signature"); - var url = serverList[curPos] + 'api/tx/send'; - fetch(url, { - method: "POST", - headers: { - 'Content-Type': 'application/json' - }, - body: `{"rawtx":"${signedTxHash}"}` - }).then(response => { - if (response.ok) - response.json().then(data => resolve(data.txid.result)); - else - response.text().then(data => resolve(data)); - }).catch(error => reject(error)); + return reject("Empty Transaction Data"); + + promisedAPI('/api/sendtx/' + signedTxHash) + .then(response => resolve(response["result"])) + .catch(error => reject(error)) }) } @@ -825,27 +788,27 @@ }) } - //Read Txs of Address between from and to + //Read Txs of Address const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) { return new Promise((resolve, reject) => { let api = `api/addrs/${addr}/txs`; //API options let query_params = {}; - if (!isUndefined(options.after) || !isUndefined(options.before)) { - if (!isUndefined(options.after)) - query_params.after = options.after; - if (!isUndefined(options.before)) - query_params.before = options.before; - } else { - if (!isUndefined(options.from)) - query_params.from = options.from; - if (!isUndefined(options.to)) - query_params.to = options.to; - } - if (!isUndefined(options.latest)) - query_params.latest = options.latest; - if (!isUndefined(options.mempool)) - query_params.mempool = options.mempool; + //page options + if (!isUndefined(options.page) && Number.isInteger(options.page)) + query_params.page = options.page; + if (!isUndefined(options.pageSize) && Number.isInteger(options.pageSize)) + query_params.pageSize = options.pageSize; + //only confirmed tx + if (options.confirmed) //Default is false in server, so only add confirmed filter if confirmed has a true value + query_params.confirmed = true; + //from block number + if (!isUndefined(options.fromBlock) && Number.isInteger(options.fromBlock)) + query_params.from = options.fromBlock; + //to block number + if (!isUndefined(options.toBlock) && Number.isInteger(options.toBlock)) + query_params.to = options.toBlock; + promisedAPI(api, query_params) .then(response => resolve(response)) .catch(error => reject(error)) From 5398fcbbbc87de06fea95779bdee3304c7220c28 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Wed, 5 Jul 2023 05:14:44 +0530 Subject: [PATCH 06/13] floBlockchainAPI v3.0.1 - Fixed readTxs: not working - Removed fromBlock and toBlock options from readTxs - Fixed: chain querying to blockbook format for readAllTxs, readData, getLatestData - Changed `mempool` option to `confirmed` option (for blockbook API) - Removed `before` options from respective fns --- floBlockchainAPI.js | 179 +++++++++++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 69 deletions(-) diff --git a/floBlockchainAPI.js b/floBlockchainAPI.js index 589279f..c47fb2f 100644 --- a/floBlockchainAPI.js +++ b/floBlockchainAPI.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floBlockchainAPI v3.0.0 +(function (EXPORTS) { //floBlockchainAPI v3.0.1 /* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/ 'use strict'; const floBlockchainAPI = EXPORTS; @@ -791,9 +791,8 @@ //Read Txs of Address const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) { return new Promise((resolve, reject) => { - let api = `api/addrs/${addr}/txs`; //API options - let query_params = {}; + let query_params = { details: 'txs' }; //page options if (!isUndefined(options.page) && Number.isInteger(options.page)) query_params.page = options.page; @@ -802,47 +801,81 @@ //only confirmed tx if (options.confirmed) //Default is false in server, so only add confirmed filter if confirmed has a true value query_params.confirmed = true; - //from block number - if (!isUndefined(options.fromBlock) && Number.isInteger(options.fromBlock)) - query_params.from = options.fromBlock; - //to block number - if (!isUndefined(options.toBlock) && Number.isInteger(options.toBlock)) - query_params.to = options.toBlock; - promisedAPI(api, query_params) + promisedAPI(`api/address/${addr}`, query_params) .then(response => resolve(response)) .catch(error => reject(error)) }); } + //backward support (floBlockchainAPI < v2.5.6) + function readAllTxs_oldSupport(addr, options, ignoreOld = 0, cacheTotal = 0) { + return new Promise((resolve, reject) => { + readTxs(addr, options).then(response => { + cacheTotal += response.txs.length; + let n_remaining = response.txApperances - cacheTotal + if (n_remaining < ignoreOld) { // must remove tx that would have been fetch during prev call + let n_remove = ignoreOld - n_remaining; + resolve(response.txs.slice(0, -n_remove)); + } else if (response.page == response.totalPages) //last page reached + resolve(response.txs); + else { + options.page = response.page + 1; + readAllTxs_oldSupport(addr, options, ignoreOld, cacheTotal) + .then(result => resolve(response.txs.concat(result))) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + } + + function readAllTxs_new(addr, options, lastItem) { + return new Promise((resolve, reject) => { + readTxs(addr, options).then(response => { + let i = response.txs.findIndex(t => t.txid === lastItem); + if (i != -1) //found lastItem + resolve(response.txs.slice(0, i)) + else if (response.page == response.totalPages) //last page reached + resolve(response.txs); + else { + options.page = response.page + 1; + readAllTxs_new(addr, options, lastItem) + .then(result => resolve(response.txs.concat(result))) + .catch(error => reject(error)) + } + }).catch(error => reject(error)) + }) + } + //Read All Txs of Address (newest first) const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options = {}) { return new Promise((resolve, reject) => { - readTxs(addr, options).then(response => { - if (response.incomplete) { - let next_options = Object.assign({}, options); - if (options.latest) - next_options.before = response.initItem; //update before for chain query (latest 1st) - else - next_options.after = response.lastItem; //update after for chain query (oldest 1st) - readAllTxs(addr, next_options).then(r => { - r.items = r.items.concat(response.items); //latest tx are 1st in array - resolve(r); - }).catch(error => reject(error)) - } else + if (Number.isInteger(options.ignoreOld)) //backward support: data from floBlockchainAPI < v2.5.6 + readAllTxs_oldSupport(addr, options, options.ignoreOld).then(txs => { + let last_tx = txs.find(t => t.confirmations > 0); + let new_lastItem = last_tx ? last_tx.txid : options.ignoreOld; resolve({ - lastItem: response.lastItem || options.after, - items: response.items - }); - }) - }); + lastItem: new_lastItem, + items: txs + }) + + }).catch(error => reject(error)) + else //New format for floBlockchainAPI >= v2.5.6 + readAllTxs_new(addr, options, options.after).then(txs => { + let last_tx = txs.find(t => t.confirmations > 0); + let new_lastItem = last_tx ? last_tx.txid : options.after; + resolve({ + lastItem: new_lastItem, + items: txs + }) + }).catch(error => reject(error)) + }) } /*Read flo Data from txs of given Address options can be used to filter data after : query after the given txid - before : query before the given txid - mempool : query mempool tx or not (options same as readAllTx, DEFAULT=false: ignore unconfirmed tx) + confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx) ignoreOld : ignore old txs (deprecated: support for backward compatibility only, cannot be used with 'after') sentOnly : filters only sent data receivedOnly: filters only received data @@ -857,18 +890,14 @@ //fetch options let query_options = {}; - query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx - if (!isUndefined(options.after) || !isUndefined(options.before)) { - if (!isUndefined(options.ignoreOld)) //Backward support - return reject("Invalid options: cannot use after/before and ignoreOld in same query"); - //use passed after and/or before options (options remain undefined if not passed) - query_options.after = options.after; - query_options.before = options.before; - } - readAllTxs(addr, query_options).then(response => { + query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: ignore unconfirmed tx - if (Number.isInteger(options.ignoreOld)) //backward support, cannot be used with options.after or options.before - response.items.splice(-options.ignoreOld); //negative to count from end of the array + if (!isUndefined(options.after)) + query_options.after = options.after; + else if (!isUndefined(options.ignoreOld)) + query_options.ignoreOld = options.ignoreOld; + + readAllTxs(addr, query_options).then(response => { if (typeof options.senders === "string") options.senders = [options.senders]; if (typeof options.receivers === "string") options.receivers = [options.receivers]; @@ -879,9 +908,9 @@ if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query return false; - if (options.sentOnly && !tx.vin.some(vin => vin.addr === addr)) + if (options.sentOnly && !tx.vin.some(vin => vin.scriptSig.addresses[0] === addr)) return false; - else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addr))) + else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.scriptSig.addresses[0]))) return false; if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) @@ -907,7 +936,7 @@ txid: tx.txid, time: tx.time, blockheight: tx.blockheight, - senders: new Set(tx.vin.map(v => v.addr)), + senders: new Set(tx.vin.map(v => v.scriptSig.addresses[0])), receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])), data: tx.floData } : tx.floData); @@ -927,8 +956,7 @@ caseFn: (function) flodata => return bool value options can be used to filter data after : query after the given txid - before : query before the given txid - mempool : query mempool tx or not (options same as readAllTx, DEFAULT=false: ignore unconfirmed tx) + confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx) sentOnly : filters only sent data receivedOnly: filters only received data tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details) @@ -938,23 +966,37 @@ const getLatestData = floBlockchainAPI.getLatestData = function (addr, caseFn, options = {}) { return new Promise((resolve, reject) => { //fetch options - let query_options = { latest: true }; - query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx - if (!isUndefined(options.after)) query_options.after = options.after; - if (!isUndefined(options.before)) query_options.before = options.before; + let query_options = {}; + query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: confirmed tx only + if (!isUndefined(options.page)) + query_options.page = options.page; + //if (!isUndefined(options.after)) query_options.after = options.after; + let new_lastItem; readTxs(addr, query_options).then(response => { + //lastItem confirmed tx checked + if (!new_lastItem) { + let last_tx = response.items.find(t => t.confirmations > 0); + if (last_tx) + new_lastItem = last_tx.txid; + } + if (typeof options.senders === "string") options.senders = [options.senders]; if (typeof options.receivers === "string") options.receivers = [options.receivers]; + //check if `after` txid is in the response + let i_after = response.txs.findIndex(t => t.txid === options.after); + if (i_after != -1) //found lastItem, hence remove it and all txs before that + response.items.splice(i_after); + var item = response.items.find(tx => { if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query return false; - if (options.sentOnly && !tx.vin.some(vin => vin.addr === addr)) + if (options.sentOnly && !tx.vin.some(vin => vin.scriptSig.addresses[0] === addr)) return false; - else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addr))) + else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.scriptSig.addresses[0]))) return false; if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) @@ -967,32 +1009,31 @@ //if item found, then resolve the result if (!isUndefined(item)) { - const result = { lastItem: response.lastItem }; + const result = { lastItem: new_lastItem || item.txid }; if (options.tx) { result.item = { - txid: tx.txid, - time: tx.time, - blockheight: tx.blockheight, - senders: new Set(tx.vin.map(v => v.addr)), - receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])), - data: tx.floData + txid: item.txid, + time: item.time, + blockheight: item.blockheight, + senders: new Set(item.vin.map(v => v.scriptSig.addresses[0])), + receivers: new Set(item.vout.map(v => v.scriptPubKey.addresses[0])), + data: item.floData } } else - result.data = tx.floData; + result.data = item.floData; return resolve(result); } + + if (response.page == response.totalPages || i_after != -1) //reached last page to check + resolve({ lastItem: new_lastItem || options.after }); //no data match the caseFn, resolve just the lastItem + //else if address needs chain query - else if (response.incomplete) { - let next_options = Object.assign({}, options); - options.before = response.initItem; //this fn uses latest option, so using before to chain query - getLatestData(addr, caseFn, next_options).then(r => { - r.lastItem = response.lastItem; //update last key as it should be the newest tx - resolve(r); - }).catch(error => reject(error)) + else { + options.page = response.page + 1; + getLatestData(addr, caseFn, options) + .then(result => resolve(result)) + .catch(error => reject(error)) } - //no data match the caseFn, resolve just the lastItem - else - resolve({ lastItem: response.lastItem }); }).catch(error => reject(error)) }) From e120b657daf2633bd25ec9c04cac50c7b26c454f Mon Sep 17 00:00:00 2001 From: sairajzero Date: Wed, 5 Jul 2023 13:35:07 +0530 Subject: [PATCH 07/13] floBlockchainAPI v3.0.1a - Fixed: readTx and all its dependencies not having txs property when address (floID) doesnt have any tx --- floBlockchainAPI.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/floBlockchainAPI.js b/floBlockchainAPI.js index c47fb2f..ba517fc 100644 --- a/floBlockchainAPI.js +++ b/floBlockchainAPI.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floBlockchainAPI v3.0.1 +(function (EXPORTS) { //floBlockchainAPI v3.0.1a /* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/ 'use strict'; const floBlockchainAPI = EXPORTS; @@ -802,9 +802,11 @@ if (options.confirmed) //Default is false in server, so only add confirmed filter if confirmed has a true value query_params.confirmed = true; - promisedAPI(`api/address/${addr}`, query_params) - .then(response => resolve(response)) - .catch(error => reject(error)) + promisedAPI(`api/address/${addr}`, query_params).then(response => { + if (!Array.isArray(response.txs)) //set empty array if address doesnt have any tx + response.txs = []; + resolve(response) + }).catch(error => reject(error)) }); } From 904deaa63c1421b6ecdebbb312ebc686ffd5ae77 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Wed, 5 Jul 2023 13:48:38 +0530 Subject: [PATCH 08/13] floBlockchainAPI v3.0.1b - bug fix - Fixed: vin address at .addresses[0] not .scriptSig.addresses[0] --- floBlockchainAPI.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/floBlockchainAPI.js b/floBlockchainAPI.js index ba517fc..9dedb90 100644 --- a/floBlockchainAPI.js +++ b/floBlockchainAPI.js @@ -1,4 +1,4 @@ -(function (EXPORTS) { //floBlockchainAPI v3.0.1a +(function (EXPORTS) { //floBlockchainAPI v3.0.1b /* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/ 'use strict'; const floBlockchainAPI = EXPORTS; @@ -910,9 +910,9 @@ if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query return false; - if (options.sentOnly && !tx.vin.some(vin => vin.scriptSig.addresses[0] === addr)) + if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr)) return false; - else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.scriptSig.addresses[0]))) + else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0]))) return false; if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) @@ -938,7 +938,7 @@ txid: tx.txid, time: tx.time, blockheight: tx.blockheight, - senders: new Set(tx.vin.map(v => v.scriptSig.addresses[0])), + senders: new Set(tx.vin.map(v => v.addresses[0])), receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])), data: tx.floData } : tx.floData); @@ -996,9 +996,9 @@ if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query return false; - if (options.sentOnly && !tx.vin.some(vin => vin.scriptSig.addresses[0] === addr)) + if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr)) return false; - else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.scriptSig.addresses[0]))) + else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0]))) return false; if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr)) @@ -1017,7 +1017,7 @@ txid: item.txid, time: item.time, blockheight: item.blockheight, - senders: new Set(item.vin.map(v => v.scriptSig.addresses[0])), + senders: new Set(item.vin.map(v => v.addresses[0])), receivers: new Set(item.vout.map(v => v.scriptPubKey.addresses[0])), data: item.floData } From a4ca904d282fb7f06661169ccc02bd8f2d428b67 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Thu, 6 Jul 2023 05:55:12 +0530 Subject: [PATCH 09/13] remove old unused files --- FLO_webWallet_minified.html | 18 - old.html | 6766 --------------------- testnet.html | 11088 ---------------------------------- 3 files changed, 17872 deletions(-) delete mode 100644 FLO_webWallet_minified.html delete mode 100644 old.html delete mode 100644 testnet.html diff --git a/FLO_webWallet_minified.html b/FLO_webWallet_minified.html deleted file mode 100644 index d4c981b..0000000 --- a/FLO_webWallet_minified.html +++ /dev/null @@ -1,18 +0,0 @@ - - - -FLO web wallet - - - - - - -
There seems to be a problem connecting to the internet.
Monitor
Monitor FLO data
Send
Send FLO data
Generate
Generate address
Settings
Settings
Settings
Copied

Add new address to monitoring list

Edit display card

Copy FLO address

Add new address to monitor
Go back to monitoring page

Refresh transactions
Available balance
0
To send FLO data, make sure you have enough balance.
Go back to monitoring page

Send

Transaction successful



Dark mode

Automatic
Dark mode active : 6pm - 6am
Manual
Dark mode

Clear all local data

This will delete all local Web Wallet data like added addresses and locally stored transactions.After clearing local data you may experience slow loading of newly added address, please proceed cautiously!

About

Version 2.7.2
Powered by
- - - diff --git a/old.html b/old.html deleted file mode 100644 index 9e35a94..0000000 --- a/old.html +++ /dev/null @@ -1,6766 +0,0 @@ - - - - - FLO web wallet - - - - - - - -
-
- There seems to be a problem connecting to the internet. -
-
- -
-
- -
- - - - - - - -
Generate
- Generate address -
-
- - - - - - -
Send
- Send FLO data -
-
- - - -
Monitor
- Monitor FLO data -
-
- - Settings - - - - - -
Settings
- Settings -
-
-
-
- Copied -
-
-
-
-

Add new address to monitoring list

-
- - -
-
- - -
- - -
-
-
-
-

Edit display card

- - Copy FLO address - - -
- - - - - - - -

-
-
- - -
- - - -
-
-
-
- - Add new address to monitor - - -
-
-
- - Go back to monitoring page - - - -

-
-
- - Refresh transactions - - -
- - - - - - -
- -
-
-
- - - - - - -
Available balance
- 0 - FLO(s) -
To send FLO data, make sure you have enough balance.
-
-
- - - -
-
-
-
- - -
-
- - -
-
- - -
1040/1040
-
- -
-
-
-
- - Go back to monitoring page - - - -

Send

-
-
-
-
- - -
- -
-
-
- - - - - - -

Transaction successful

-

- -
-
- -

-
-
-
-
-
-

Dark mode

-
- - Automatic
- Dark mode active : 6pm - 6am -
- -
-
- Manual
- Dark mode -
- -
-
-
-

Clear all local data

-
This will delete all local Web Wallet data like added addresses and locally stored - transactions.After clearing local data you may experience slow loading of newly added address, - please proceed cautiously!
- -
-
-

About

-
Version 2.7.4
- -
Powered by
- - - - - - - - - - - - - -
-
-
-
-
-
- -
- - - - - - - - - \ No newline at end of file diff --git a/testnet.html b/testnet.html deleted file mode 100644 index c3eeff7..0000000 --- a/testnet.html +++ /dev/null @@ -1,11088 +0,0 @@ - - - - - FLO web wallet - - - - - - - - -
-
- There seems to be a problem connecting to the internet. -
-
- -
-
- -
- - - - - - - -
Generate
- Generate address -
-
- - - - - - -
Send
- Send FLO data -
-
- - - -
Monitor
- Monitor FLO data -
-
- - Settings - - - - - -
Settings
- Settings -
-
-
-
- Copied -
-
-
-
-

Add new address to monitoring list

-
- - -
-
- - -
- - -
-
-
-
-

Edit display card

- - Copy FLO address - - -
- - - - - - - -

-
-
- - -
- - - -
-
-
-
- - Add new address to monitor - - -
-
-
- - Go back to monitoring page - - - -

-
-
- - Refresh transactions - - -
- - - - -
- -
-
-
- - - - - - -
Available balance
- 0 - FLO(s) -
To send FLO data, make sure you have enough balance.
-
-
- - - -
-
-
-
- - -
-
- - -
-
- - -
1040/1040
-
-

- - -
-
-
-
- - Go back to monitoring page - - - -

Send

-
-
-
-
- - -
- -
-
-
- - - - - - -

Transaction successful

-

- -
-
- -

-
-
-
-
-
-

Dark mode

-
- - Automatic
- Dark mode active : 6pm - 6am -
- -
-
- Manual
- Dark mode -
- -
-
-
-

Clear all local data

-
This will delete all local Web Wallet data like added addresses and locally stored - transactions.After clearing local data you may experience slow loading of newly added address, - please proceed cautiously!
- -
-
-
- -

Developer options

- - - - -
-
-
-

Allow unconfirmed UTXOs

- - This is a developer option only and not recommended for normal use. - -
- -
-
-
-
-

About

-
Version 2.7.4
- -
Powered by
- - - - - - - - - - - - - -
-
-
-
-
-
- -
- - - - - - - - - \ No newline at end of file From 2a78454cae84f50378b22c171adefa50cd4ba81f Mon Sep 17 00:00:00 2001 From: sairajzero Date: Thu, 6 Jul 2023 06:59:05 +0530 Subject: [PATCH 10/13] Fixes and improvements - Fixes for blockbook API (floBlockchainAPI >=3.0.0) - Improvements to listTransactions - formatted tx details have mining details separately. Added senders and receivers details containing full list of senders and receivers. Added other tx details like confirmation, fee, block - listTransactions fn now uses options: page, pageSize --- scripts/flo-webwallet.js | 104 +++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 37 deletions(-) diff --git a/scripts/flo-webwallet.js b/scripts/flo-webwallet.js index 5d6538a..6ecf0b8 100644 --- a/scripts/flo-webwallet.js +++ b/scripts/flo-webwallet.js @@ -48,46 +48,76 @@ }) } - function listTransactions_raw(address, options = {}) { + function formatTx(address, tx) { + let result = { + time: tx.time, + block: tx.blockheight, + blockhash: tx.blockhash, + txid: tx.txid, + floData: tx.floData, + confirmations: tx.confirmations + } + + //format receivers + let receivers = {}; + for (let vout of tx.vout) { + if (vout.scriptPubKey.isAddress) { + let id = vout.scriptPubKey.addresses[0]; + if (id in receivers) + receivers[id] += vout.value; + else receivers[id] = vout.value; + } + } + result.receivers = receivers; + //format senders (or mined) + if (!tx.vin[0].isAddress) { //mined (ie, coinbase) + let coinbase = tx.vin[0].coinbase; + result.mine = coinbase; + result.mined = { [coinbase]: tx.valueOut } + } else { + result.sender = tx.vin[0].addresses[0]; + result.receiver = tx.vout[0].scriptPubKey.addresses[0]; + result.fees = tx.fees; + let senders = {}; + for (let vin of tx.vin) { + if (vin.isAddress) { + let id = vin.addresses[0]; + if (id in senders) + senders[id] += vin.value; + else senders[id] = vin.value; + } + } + result.senders = senders; + + //remove change amounts + for (let id in senders) { + if (id in receivers) { + if (senders[id] > receivers[id]) { + senders[id] -= receivers[id]; + delete receivers[id]; + } else if (senders[id] < receivers[id]) { //&& id != address + receivers[id] -= senders[id]; + delete senders[id]; + } + } + } + } + + return result; + } + + floWebWallet.listTransactions = function (address, page_options = {}) { return new Promise((resolve, reject) => { - options.latest = true; + let options = {}; + if (Number.isInteger(page_options.page)) + options.page = page_options.page; + if (Number.isInteger(page_options.pageSize)) + options.pageSize = page_options.pageSize; floBlockchainAPI.readTxs(address, options).then(response => { const result = {} - result.items = response.items.map(({ time, txid, floData, isCoinBase, vin, vout }) => ({ - time, txid, floData, isCoinBase, - sender: isCoinBase ? `(mined)${vin[0].coinbase}` : vin[0].addr, - receiver: isCoinBase ? address : vout[0].scriptPubKey.addresses[0] - })); - result.lastItem = response.lastItem; - result.initItem = response.initItem; - resolve(result); - }).catch(error => reject(error)) - }) - } - - - floWebWallet.listTransactions = function (address) { - return new Promise((resolve, reject) => { - listTransactions_raw(address) - .then(result => resolve(result)) - .catch(error => reject(error)) - }) - } - - - floWebWallet.listTransactions.syncNew = function (address, lastItem) { - return new Promise((resolve, reject) => { - listTransactions_raw(address, { after: lastItem }).then(result => { - delete result.initItem; - resolve(result); - }).catch(error => reject(error)) - }) - } - - floWebWallet.listTransactions.syncOld = function (address, initItem) { - return new Promise((resolve, reject) => { - listTransactions_raw(address, { before: initItem }).then(result => { - delete result.lastItem; + result.items = response.txs.map(tx => formatTx(address, tx)); + result.page = response.page; + result.totalPages = response.totalPages; resolve(result); }).catch(error => reject(error)) }) From 9ce06f5cbbd7faf568c4c7c9228ab9d74657c24c Mon Sep 17 00:00:00 2001 From: sairaj mote Date: Thu, 6 Jul 2023 18:17:31 +0530 Subject: [PATCH 11/13] Added UI for new transaction format --- css/main.css | 26 ++++++++- css/main.min.css | 2 +- css/main.scss | 44 ++++++++++++--- index.html | 121 ++++++++++++++++++++++++------------------ scripts/components.js | 2 +- 5 files changed, 134 insertions(+), 61 deletions(-) diff --git a/css/main.css b/css/main.css index 2339754..a7ebe05 100644 --- a/css/main.css +++ b/css/main.css @@ -776,7 +776,7 @@ h3 { } .transaction { - gap: 1rem; + gap: 1.2rem; padding: 1rem; background-color: rgba(var(--text-color), 0.03); border-radius: 0.3rem; @@ -792,7 +792,29 @@ h3 { } .transaction__receiver { margin-left: 0.5rem; - color: rgba(var(--text-color), 0.8); + color: rgba(var(--text-color), 0.9); + font-weight: 500; +} +.transaction__amount { + font-weight: 700; +} +.transaction.mined .transaction__icon .icon, .transaction.received .transaction__icon .icon, .transaction.self .transaction__icon .icon { + fill: var(--green); +} +.transaction.mined .transaction__amount, .transaction.received .transaction__amount, .transaction.self .transaction__amount { + color: var(--green); +} +.transaction.mined .transaction__amount::before, .transaction.received .transaction__amount::before, .transaction.self .transaction__amount::before { + content: "+ "; +} +.transaction.sent .transaction__icon .icon { + fill: var(--danger-color); +} +.transaction.sent .transaction__amount { + color: var(--danger-color); +} +.transaction.sent .transaction__amount::before { + content: "- "; } .transaction p { font-size: 0.9rem; diff --git a/css/main.min.css b/css/main.min.css index aa28dc4..5d40581 100644 --- a/css/main.min.css +++ b/css/main.min.css @@ -1 +1 @@ -*{padding:0;margin:0;box-sizing:border-box;font-family:"Roboto",sans-serif}:root{font-size:clamp(1rem,1.2vmax,1.2rem)}html,body{height:100%}body{--accent-color: #256eff;--text-color: 20, 20, 20;--background-color: 240, 240, 240;--foreground-color: 250, 250, 250;--danger-color: rgb(255, 75, 75);--green: #1cad59;scrollbar-width:thin;scrollbar-gutter:stable;color:rgba(var(--text-color), 1);background-color:rgba(var(--background-color), 1);transition:background-color .3s;--dark-red: #d40e1e;--red: #f50000;--kinda-pink: #e40273;--purple: #462191;--shady-blue: #324de6;--nice-blue: #256eff;--maybe-cyan: #00b0ff;--teal: #00bcd4;--mint-green: #16c79a;--yellowish-green: #66bb6a;--greenish-yellow: #8bc34a;--dark-teal: #11698e;--tangerine: #ff6f00;--orange: #ff9100;--redish-orange: #ff3d00}body[data-theme=dark]{--accent-color: #86afff;--text-color: 220, 220, 220;--background-color: 10, 10, 10;--foreground-color: 24, 24, 24;--danger-color: rgb(255, 106, 106);--green: #00e676;--dark-red: #ff5e7e;--red: #ff6098;--kinda-pink: #c44ae6;--purple: #9565f7;--shady-blue: #7084f5;--nice-blue: #86afff;--maybe-cyan: #66cfff;--teal: #6aeeff;--mint-green: #4dffd2;--yellowish-green: #9effa2;--greenish-yellow: #c7fc8b;--dark-teal: #51cbff;--tangerine: #ffac6d;--orange: #ffbe68;--redish-orange: #ff8560}body[data-theme=dark] sm-popup::part(popup){background-color:rgba(var(--foreground-color), 1)}body[data-theme=dark] ::-webkit-calendar-picker-indicator{filter:invert(1)}p,strong{font-size:.9rem;max-width:65ch;line-height:1.7;color:rgba(var(--text-color), 0.9)}p:not(:last-of-type),strong:not(:last-of-type){margin-bottom:1.5rem}a{text-decoration:none;color:var(--accent-color)}a:focus-visible{box-shadow:0 0 0 .1rem rgba(var(--text-color), 1) inset}button,.button{-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative;display:inline-flex;border:none;background-color:rgba(0,0,0,0);overflow:hidden;color:inherit;-webkit-tap-highlight-color:rgba(0,0,0,0);align-items:center;font-size:inherit;font-weight:500;white-space:nowrap;padding:.9rem;border-radius:.3rem;justify-content:center}button:focus-visible,.button:focus-visible{outline:var(--accent-color) solid medium}button:not(:disabled),.button:not(:disabled){cursor:pointer}.button{background-color:rgba(var(--text-color), 0.02);border:solid thin rgba(var(--text-color), 0.06)}.button--primary{color:rgba(var(--background-color), 1);background-color:var(--accent-color)}.button--primary .icon{fill:rgba(var(--background-color), 1)}.button--colored{color:var(--accent-color)}.button--colored .icon{fill:var(--accent-color)}.button--danger{background-color:rgba(255,115,115,.062745098);color:var(--danger-color)}.button--danger .icon{fill:var(--danger-color)}.button--small{padding:.4rem .6rem}.button--outlined{border:solid rgba(var(--text-color), 0.3) .1rem;background-color:rgba(var(--foreground-color), 1)}.button--transparent{background-color:rgba(0,0,0,0)}button:disabled{opacity:.4;cursor:not-allowed;filter:saturate(0)}.cta{text-transform:uppercase;font-size:.9rem;font-weight:700;letter-spacing:.05em;padding:.8rem 1rem}.icon{width:1.2rem;height:1.2rem;fill:rgba(var(--text-color), 0.8);flex-shrink:0}.icon-only{padding:.5rem;border-radius:.3rem}.icon--big{width:3rem;height:3rem}a:-webkit-any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}a:-moz-any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}a:any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}details{padding:1rem 0}details summary{display:flex;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer;align-items:center;justify-content:space-between;color:var(--accent-color)}details[open] summary{margin-bottom:1rem}details[open]>summary .down-arrow{transform:rotate(180deg)}sm-input,sm-textarea{font-size:.9rem;--border-radius: 0.5rem;--background-color: rgba(var(--foreground-color), 1)}sm-input button .icon,sm-textarea button .icon{fill:var(--accent-color)}sm-textarea{--max-height: auto}sm-spinner{--size: 1rem;--stroke-width: 0.1rem}sm-form{--gap: 1rem}sm-chips{--gap: 0.3rem}sm-chip{position:relative;font-size:.9rem;--border-radius: 0.5rem;--padding: 0.5rem 0.8rem;--background: rgba(var(--text-color), 0.06);-webkit-user-select:none;-moz-user-select:none;user-select:none}sm-chip[selected]{--background: var(--accent-color);color:rgba(var(--background-color), 1)}sm-select::part(options){max-height:40vh}sm-option{flex-shrink:0}sm-option::part(option){grid-template-columns:none}ul{list-style:none}.overflow-ellipsis{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.breakable{overflow-wrap:break-word;word-wrap:break-word;word-break:break-word}.full-bleed{grid-column:1/-1}.h1{font-size:1.5rem}.h2{font-size:1.2rem}h3{font-size:1.2rem;line-height:1.3}.h4{font-size:.9rem}.h5{font-size:.75rem}.uppercase{text-transform:uppercase}.capitalize::first-letter{text-transform:uppercase}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.flex{display:flex}.flex-1{flex:1}.flex-wrap{flex-wrap:wrap}.grid{display:grid}.flow-column{grid-auto-flow:column}.gap-0-3{gap:.3rem}.gap-0-5{gap:.5rem}.gap-1{gap:1rem}.gap-1-5{gap:1.5rem}.gap-2{gap:2rem}.gap-3{gap:3rem}.text-align-right{text-align:right}.align-start{align-content:flex-start}.align-center{align-items:center}.align-end{align-items:flex-end}.align-content-center{align-content:center}.text-center{text-align:center}.justify-start{justify-content:start}.justify-center{justify-content:center}.justify-right{margin-left:auto}.justify-items-center{justify-items:center}.align-self-center{align-self:center}.justify-self-center{justify-self:center}.justify-self-start{justify-self:start}.justify-self-end{justify-self:end}.direction-column{flex-direction:column}.space-between{justify-content:space-between}.w-100{width:100%}.h-100{height:100%}.margin-left-0-5{margin-left:.5rem}.margin-left-auto{margin-left:auto}.margin-right-0-5{margin-right:.5rem}.margin-right-auto{margin-right:auto}.ripple{height:8rem;width:8rem;position:absolute;border-radius:50%;transform:scale(0);background:radial-gradient(circle, rgba(var(--text-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%);pointer-events:none}.button--primary .ripple,.button--danger .ripple{background:radial-gradient(circle, rgba(var(--background-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%)}.interact{position:relative;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:rgba(0,0,0,0)}.empty-state{display:grid;width:100%;padding:1.5rem 0}.observe-empty-state:empty{display:none !important}.observe-empty-state:not(:empty)+.empty-state{display:none}.bullet-point{display:flex;align-items:center;justify-content:center;margin:0 .8ch}.bullet-point::after{content:"";height:.4ch;width:.4ch;border-radius:.5em;background-color:var(--accent-color)}#confirmation_popup,#prompt_popup{flex-direction:column}#confirmation_popup h4,#prompt_popup h4{margin-bottom:.5rem}#confirmation_popup .flex,#prompt_popup .flex{margin-top:1rem}.popup__header{position:relative;display:grid;gap:.5rem;width:100%;padding:0 1.5rem;align-items:center}.popup__header>*{grid-row:1}.popup__header h3,.popup__header h4{grid-column:1/-1;justify-self:center;align-self:center}.popup__header__close{grid-column:1;margin-left:-1rem;justify-self:flex-start}#loader{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(var(--foreground-color), 1);z-index:100;display:grid;place-content:center;place-items:center;gap:1rem;text-align:center}#show_character_count{font-size:.8rem;margin-left:auto}#flo_data_status:not(:empty){color:red;padding:.5rem 0}#saved_ids_popup{--height: 80vh}#main_header{padding:.6rem 1rem}.app-brand{display:flex;gap:.3rem;align-items:center}.app-brand .icon{height:1.7rem;width:1.7rem}.app-name__company{font-size:.8rem;font-weight:500;color:rgba(var(--text-color), 0.8)}#main_card{display:flex;flex-direction:column;height:100%;width:100%;background-color:rgba(var(--foreground-color), 1);transition:background-color .3s}#pages_container{flex:1;overflow-y:auto}#main_navbar{display:flex;background:rgba(var(--text-color), 0.03)}#main_navbar.hide-away{position:absolute}#main_navbar ul{display:flex;height:100%;width:100%}#main_navbar ul li{width:100%}.nav-item{position:relative;display:flex;flex:1;width:100%;height:100%;flex-direction:column;align-items:center;justify-content:center;padding:.5rem .3rem;color:var(--text-color);font-size:.7rem;border-radius:.3rem;text-align:center}.nav-item .icon{transition:transform .2s}.nav-item__title{transition:opacity .2s,transform .2s}.nav-item--active{color:var(--accent-color)}.nav-item--active .icon{fill:var(--accent-color)}.nav-item__indicator{position:absolute;bottom:0;width:2rem;height:.3rem;background:var(--accent-color);border-radius:1rem 1rem 0 0;z-index:1}#fix_invalid_button{margin-bottom:1rem !important}.password-field label{display:flex}.password-field label input:checked~.visible{display:none}.password-field label input:not(:checked)~.invisible{display:none}.multi-state-button{display:grid;text-align:center;align-items:center;justify-items:center}.multi-state-button>*{grid-area:1/1/2/2}.multi-state-button button{z-index:1;width:100%}.clip{-webkit-clip-path:circle(0);clip-path:circle(0)}#flo_id_warning{padding-bottom:1.5rem;border-bottom:thin solid rgba(var(--text-color), 0.3)}#flo_id_warning .icon{height:4rem;width:4rem;padding:1rem;background-color:#ffc107;border-radius:3rem;fill:rgba(0,0,0,.8);margin-bottom:1.5rem}.generated-id-card{display:grid;gap:1rem}.generated-id-card h5{margin-bottom:.3rem}#contacts>:first-child{overflow-y:auto;align-content:flex-start;padding-bottom:4rem}#primary_actions_wrapper{display:grid;gap:.5rem}.primary-action{display:flex;padding:.8rem 1rem;gap:.5rem;white-space:normal;font-size:.85rem;border-radius:.5rem;background-color:rgba(0,0,0,0);border:thin solid rgba(var(--text-color), 0.3);text-align:left}.primary-action .icon{fill:var(--accent-color)}#search{position:relative;height:100%}#queried_flo_address h4{font-size:1.1rem}#queried_flo_address>sm-copy{font-size:.8rem}#token_list{display:flex;flex-wrap:wrap;gap:.5rem}.token-item{font-size:.9rem;padding:.5rem 1rem;background-color:rgba(var(--text-color), 0.06);border-radius:.3rem}.transaction{gap:1rem;padding:1rem;background-color:rgba(var(--text-color), 0.03);border-radius:.3rem}.transaction:not(:last-of-type){margin-bottom:1rem}.transaction .icon{fill:var(--accent-color)}.transaction__time,.transaction__link,.transaction__receiver{font-size:.8rem}.transaction__receiver{margin-left:.5rem;color:rgba(var(--text-color), 0.8)}.transaction p{font-size:.9rem;max-width:unset}.transaction__time{justify-self:flex-end;color:rgba(var(--text-color), 0.8)}#search_wrapper{width:min(100%,36rem)}#search_query_input{justify-self:center}#saved_ids_list{position:relative;align-content:flex-start;padding-bottom:1.5rem;margin-top:1rem;gap:1rem;grid-template-columns:repeat(auto-fill, minmax(16rem, 1fr))}.saved-id{grid-template-columns:auto 1fr;gap:0 .8rem;border-radius:.3rem;padding:.5rem;background-color:rgba(var(--text-color), 0.03);-webkit-user-select:none;-moz-user-select:none;user-select:none}.saved-id.highlight{box-shadow:0 0 .1rem .1rem var(--accent-color) inset}.saved-id .edit-saved{grid-area:1/1/3/2;padding:.3rem;position:relative}.saved-id .edit-saved .icon{position:absolute;height:1.2rem;width:1.2rem;right:0;bottom:0;border-radius:.5rem;padding:.2rem;background-color:rgba(var(--background-color), 1)}.saved-id__initial{display:flex;align-items:center;justify-content:center;height:2.4rem;width:2.4rem;font-size:1.2rem;text-transform:uppercase;color:rgba(var(--background-color), 1);font-weight:700;line-height:1;background-color:var(--accent-color);justify-self:flex-start;border-radius:2rem}.saved-id__label{align-self:flex-end}.saved-id__flo-id{font-size:.8rem}.page{position:relative;display:flex;flex-direction:column;overflow-y:auto;align-content:flex-start;padding:0 1rem;padding-bottom:3rem}.fab{position:absolute;right:0;bottom:0;margin:1.5rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.2);z-index:2}#add_address_button{border-radius:.5rem;color:rgba(var(--background-color), 1);background-color:var(--accent-color)}#add_address_button .icon{fill:rgba(var(--background-color), 1)}#balance_card{display:flex;flex-direction:column;gap:2rem;padding:max(1rem,2vw);background-color:rgba(var(--text-color), 0.06);aspect-ratio:4/2;justify-content:flex-end;border-radius:.5rem}#balance_card form{margin-top:1rem}#balance_card fieldset{border:none}.token-balance{display:flex;align-items:center;cursor:pointer;gap:.5rem;background-color:rgba(var(--text-color), 0.06);padding:.8rem;border-radius:.3rem;font-size:.9rem}.token-balance span:first-of-type::first-letter{text-transform:capitalize}.token-balance input{height:1rem;width:1rem;accent-color:var(--accent-color)}.token-receiver-combo{border:solid thin rgba(var(--text-color), 0.2);padding:.5rem;border-radius:.8rem}.token-receiver-combo--removable{grid-template-columns:1fr auto;grid-template-areas:"receiver receiver" "amount remove"}.token-receiver-combo--removable .token-receiver{grid-area:receiver}.token-receiver-combo--removable .token-amount{grid-area:amount}.token-receiver-combo--removable .remove-token-receiver{grid-area:remove}#transaction_result{display:grid;gap:.5rem;height:max(40vh,24rem);align-items:center;justify-content:center;text-align:center;align-content:center}#transaction_result:empty{display:none}#transaction_result h3{text-align:center;width:100%}#transaction_result .icon{justify-self:center;height:4rem;width:4rem;border-radius:5rem;margin-bottom:1rem;-webkit-animation:popup 1s;animation:popup 1s}#transaction_result .icon--success{fill:rgba(var(--background-color), 1);padding:1rem;background-color:#0bbe56}#transaction_result .icon--failed{background-color:rgba(var(--text-color), 0.03);fill:var(--danger-color)}#transaction_result sm-copy{font-size:.8rem}@-webkit-keyframes popup{0%{opacity:0;transform:scale(0.2) translateY(600%)}10%{transform:scale(0.2) translateY(5rem);opacity:1}40%{transform:scale(0.2) translateY(0)}80%{transform:scale(1.1) translateY(0)}100%{transform:scale(1) translateY(0)}}@keyframes popup{0%{opacity:0;transform:scale(0.2) translateY(600%)}10%{transform:scale(0.2) translateY(5rem);opacity:1}40%{transform:scale(0.2) translateY(0)}80%{transform:scale(1.1) translateY(0)}100%{transform:scale(1) translateY(0)}}#queried_address_transactions{display:flex;flex-direction:column;padding-bottom:4rem}#pagination_wrapper{position:fixed;bottom:0;right:0;margin:0 auto;padding:.5rem;background-color:rgba(var(--foreground-color), 1);z-index:5;border-radius:.7rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.2);margin:.5rem}.pagination__item{display:flex;padding:.2rem .5rem;border-radius:.3rem}.pagination__item--active{background-color:var(--accent-color);color:rgba(var(--background-color), 1)}legend,.label{font-size:.8rem;color:rgba(var(--text-color), 0.8)}#smartcontracts{display:grid;min-width:0;height:100%}#smartcontracts>*{grid-area:1/1}#smartcontracts fieldset{padding:.5rem;border-radius:.5rem;border:solid 1px rgba(var(--text-color), 0.3)}#smartcontracts fieldset legend{padding:0 .5rem}#smartcontracts label{padding:.3rem .5rem}#smartcontracts label:has(input:not(:disabled):not(:checked)){cursor:pointer}#smartcontracts input[type=radio]{height:1.1em;width:1.1em;margin-right:.5rem;accent-color:var(--accent-color)}#smartcontracts input[type=datetime-local]{width:100%;padding:.8rem .6rem;border:none;border-radius:.5rem;font-weight:500;font-family:inherit;font-size:inherit;color:inherit;background-color:rgba(var(--text-color), 0.06)}#smartcontracts input[type=datetime-local]:focus{outline:none;box-shadow:0 0 0 .1rem var(--accent-color)}#smartcontracts sm-input:not([placeholder]){--min-height: 3rem}.smart-contract-action{flex-direction:column;white-space:normal;gap:.5rem;color:rgba(var(--text-color), 0.8);width:5rem;font-size:.9rem;aspect-ratio:1/1;border-radius:.5rem;border:solid 1px rgba(var(--text-color), 0.3);text-align:center}.smart-contract-action .icon{fill:var(--accent-color)}#smart_contract_creation_templates{display:grid;gap:.5rem}#smart_contract_creation_templates li{display:flex;flex:1}.smart-contract-template{display:grid;grid-template-areas:"heading arrow" "description arrow";grid-template-columns:1fr auto;padding:max(1rem,2vw);border:solid 1px rgba(var(--text-color), 0.3);border-radius:.5rem;gap:.5rem;text-align:start;white-space:normal;width:100%;justify-content:flex-start}.smart-contract-template h4{grid-area:heading}.smart-contract-template p{grid-area:description;font-weight:400;line-height:1.3;font-size:.9rem;color:rgba(var(--text-color), 0.8);text-transform:none}.smart-contract-template .icon{margin-top:auto;grid-area:arrow;border:solid 1px rgba(var(--text-color), 0.3);border-radius:3rem;padding:.4rem;height:2rem;width:2rem;fill:var(--accent-color)}.payee-address-wrapper{display:grid;grid-template-columns:1fr auto;gap:.5rem}.payee-address-wrapper:first-of-type{grid-template-areas:"address" "share"}.payee-address-wrapper:not(:first-of-type){grid-template-areas:"address address" "share button"}.payee-address-wrapper .payee-address{grid-area:address}.payee-address-wrapper .payee-share{grid-area:share}.payee-address-wrapper .icon-only{grid-area:button}.payee-address-wrapper .icon-only{height:3.18rem}.choice-wrapper{display:grid;grid-template-columns:1fr auto;gap:.5rem}.choice-wrapper .icon-only{aspect-ratio:1/1;height:3.18rem}@media screen and (max-width: 40rem){#main_navbar.hide-away{bottom:0;left:0;right:0}#primary_actions_wrapper{grid-template-columns:1fr 1fr}.nav-item__title{margin-top:.3rem}.nav-item--active .icon{transform:translateY(50%)}.nav-item--active .nav-item__title{transform:translateY(100%);opacity:0}#pagination_wrapper{margin:0;margin-bottom:4.8rem;left:50%;right:auto;transform:translateX(-50%);width:calc(100vw - 2rem);flex-wrap:wrap}}@media screen and (min-width: 40rem){sm-popup{--width: 24rem}.popup__header{padding:1rem 1.5rem 0 1.5rem}body{display:flex;align-items:center;justify-content:center}#main_card{display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr;grid-template-areas:"header header" "nav main";position:relative}#main_header{grid-area:header}#pages_container{grid-area:main}.page{padding:0 1.5rem}#main_navbar{grid-area:nav;border-top:none;flex-direction:column;height:100%}#main_navbar ul{flex-direction:column}.nav-item{flex-direction:row;text-align:left;justify-content:flex-start;gap:.5rem;padding:1rem;font-weight:500;font-size:.8rem;border-radius:0;min-width:10rem}.nav-item__indicator{width:.25rem;height:50%;left:0;border-radius:0 1rem 1rem 0;bottom:auto}#primary_actions_wrapper{display:flex;flex-wrap:wrap}#create_flo_id_popup,#retrieve_flo_id_popup{--width: 26rem}#send{padding:0 6vw}#send sm-form{width:min(56rem,100%);margin:auto}#smart_contract_creation_templates{grid-template-columns:repeat(auto-fill, minmax(20rem, 1fr))}#smart_contract_deposit_form,#smart_contract_participate_form,#smart_contract_update_form,#smart_contract_trigger_form{width:min(36rem,100%);margin:auto;--gap: 1.5rem}#smart_contract_creation_form::part(form){gap:1.5rem;margin:auto;width:min(36rem,100%)}#smart_contract_creation_form::part(form).split-layout{grid-template-columns:1fr 1.5fr;align-items:flex-start;width:min(50rem,100%)}.payee-address-wrapper{display:grid;grid-template-columns:1fr 8rem 3rem}.payee-address-wrapper:first-of-type{grid-template-areas:"address share share"}.payee-address-wrapper:not(:first-of-type){grid-template-areas:"address share button"}}@media screen and (min-width: 56rem){#send sm-form::part(form){align-items:flex-start;grid-template-columns:1fr 1.5fr}}@media screen and (min-width: 64rem){#address_details_wrapper{grid-template-columns:auto 1fr;align-items:flex-start}#transactions_hero_section{position:-webkit-sticky;position:sticky;top:0}}@media(any-hover: hover){::-webkit-scrollbar{width:.5rem;height:.5rem}::-webkit-scrollbar-thumb{background:rgba(var(--text-color), 0.3);border-radius:1rem}::-webkit-scrollbar-thumb:hover{background:rgba(var(--text-color), 0.5)}.interact:not([disabled]){transition:background-color .3s}.interact:not([disabled]):hover{background-color:rgba(var(--text-color), 0.06)}.button:not([disabled]){transition:background-color .3s,filter .3s}.button:not([disabled]):hover{filter:contrast(2)}}@supports(overflow: overlay){body{overflow:overlay}}.hidden{display:none !important} \ No newline at end of file +*{padding:0;margin:0;box-sizing:border-box;font-family:"Roboto",sans-serif}:root{font-size:clamp(1rem,1.2vmax,1.2rem)}html,body{height:100%}body{--accent-color: #256eff;--text-color: 20, 20, 20;--background-color: 240, 240, 240;--foreground-color: 250, 250, 250;--danger-color: rgb(255, 75, 75);--green: #1cad59;scrollbar-width:thin;scrollbar-gutter:stable;color:rgba(var(--text-color), 1);background-color:rgba(var(--background-color), 1);transition:background-color .3s;--dark-red: #d40e1e;--red: #f50000;--kinda-pink: #e40273;--purple: #462191;--shady-blue: #324de6;--nice-blue: #256eff;--maybe-cyan: #00b0ff;--teal: #00bcd4;--mint-green: #16c79a;--yellowish-green: #66bb6a;--greenish-yellow: #8bc34a;--dark-teal: #11698e;--tangerine: #ff6f00;--orange: #ff9100;--redish-orange: #ff3d00}body[data-theme=dark]{--accent-color: #86afff;--text-color: 220, 220, 220;--background-color: 10, 10, 10;--foreground-color: 24, 24, 24;--danger-color: rgb(255, 106, 106);--green: #00e676;--dark-red: #ff5e7e;--red: #ff6098;--kinda-pink: #c44ae6;--purple: #9565f7;--shady-blue: #7084f5;--nice-blue: #86afff;--maybe-cyan: #66cfff;--teal: #6aeeff;--mint-green: #4dffd2;--yellowish-green: #9effa2;--greenish-yellow: #c7fc8b;--dark-teal: #51cbff;--tangerine: #ffac6d;--orange: #ffbe68;--redish-orange: #ff8560}body[data-theme=dark] sm-popup::part(popup){background-color:rgba(var(--foreground-color), 1)}body[data-theme=dark] ::-webkit-calendar-picker-indicator{filter:invert(1)}p,strong{font-size:.9rem;max-width:65ch;line-height:1.7;color:rgba(var(--text-color), 0.9)}p:not(:last-of-type),strong:not(:last-of-type){margin-bottom:1.5rem}a{text-decoration:none;color:var(--accent-color)}a:focus-visible{box-shadow:0 0 0 .1rem rgba(var(--text-color), 1) inset}button,.button{-webkit-user-select:none;-moz-user-select:none;user-select:none;position:relative;display:inline-flex;border:none;background-color:rgba(0,0,0,0);overflow:hidden;color:inherit;-webkit-tap-highlight-color:rgba(0,0,0,0);align-items:center;font-size:inherit;font-weight:500;white-space:nowrap;padding:.9rem;border-radius:.3rem;justify-content:center}button:focus-visible,.button:focus-visible{outline:var(--accent-color) solid medium}button:not(:disabled),.button:not(:disabled){cursor:pointer}.button{background-color:rgba(var(--text-color), 0.02);border:solid thin rgba(var(--text-color), 0.06)}.button--primary{color:rgba(var(--background-color), 1);background-color:var(--accent-color)}.button--primary .icon{fill:rgba(var(--background-color), 1)}.button--colored{color:var(--accent-color)}.button--colored .icon{fill:var(--accent-color)}.button--danger{background-color:rgba(255,115,115,.062745098);color:var(--danger-color)}.button--danger .icon{fill:var(--danger-color)}.button--small{padding:.4rem .6rem}.button--outlined{border:solid rgba(var(--text-color), 0.3) .1rem;background-color:rgba(var(--foreground-color), 1)}.button--transparent{background-color:rgba(0,0,0,0)}button:disabled{opacity:.4;cursor:not-allowed;filter:saturate(0)}.cta{text-transform:uppercase;font-size:.9rem;font-weight:700;letter-spacing:.05em;padding:.8rem 1rem}.icon{width:1.2rem;height:1.2rem;fill:rgba(var(--text-color), 0.8);flex-shrink:0}.icon-only{padding:.5rem;border-radius:.3rem}.icon--big{width:3rem;height:3rem}a:-webkit-any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}a:-moz-any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}a:any-link:focus-visible{outline:rgba(var(--text-color), 1) .1rem solid}details{padding:1rem 0}details summary{display:flex;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer;align-items:center;justify-content:space-between;color:var(--accent-color)}details[open] summary{margin-bottom:1rem}details[open]>summary .down-arrow{transform:rotate(180deg)}sm-input,sm-textarea{font-size:.9rem;--border-radius: 0.5rem;--background-color: rgba(var(--foreground-color), 1)}sm-input button .icon,sm-textarea button .icon{fill:var(--accent-color)}sm-textarea{--max-height: auto}sm-spinner{--size: 1rem;--stroke-width: 0.1rem}sm-form{--gap: 1rem}sm-chips{--gap: 0.3rem}sm-chip{position:relative;font-size:.9rem;--border-radius: 0.5rem;--padding: 0.5rem 0.8rem;--background: rgba(var(--text-color), 0.06);-webkit-user-select:none;-moz-user-select:none;user-select:none}sm-chip[selected]{--background: var(--accent-color);color:rgba(var(--background-color), 1)}sm-select::part(options){max-height:40vh}sm-option{flex-shrink:0}sm-option::part(option){grid-template-columns:none}ul{list-style:none}.overflow-ellipsis{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.breakable{overflow-wrap:break-word;word-wrap:break-word;word-break:break-word}.full-bleed{grid-column:1/-1}.h1{font-size:1.5rem}.h2{font-size:1.2rem}h3{font-size:1.2rem;line-height:1.3}.h4{font-size:.9rem}.h5{font-size:.75rem}.uppercase{text-transform:uppercase}.capitalize::first-letter{text-transform:uppercase}.sticky{position:-webkit-sticky;position:sticky}.top-0{top:0}.flex{display:flex}.flex-1{flex:1}.flex-wrap{flex-wrap:wrap}.grid{display:grid}.flow-column{grid-auto-flow:column}.gap-0-3{gap:.3rem}.gap-0-5{gap:.5rem}.gap-1{gap:1rem}.gap-1-5{gap:1.5rem}.gap-2{gap:2rem}.gap-3{gap:3rem}.text-align-right{text-align:right}.align-start{align-content:flex-start}.align-center{align-items:center}.align-end{align-items:flex-end}.align-content-center{align-content:center}.text-center{text-align:center}.justify-start{justify-content:start}.justify-center{justify-content:center}.justify-right{margin-left:auto}.justify-items-center{justify-items:center}.align-self-center{align-self:center}.justify-self-center{justify-self:center}.justify-self-start{justify-self:start}.justify-self-end{justify-self:end}.direction-column{flex-direction:column}.space-between{justify-content:space-between}.w-100{width:100%}.h-100{height:100%}.margin-left-0-5{margin-left:.5rem}.margin-left-auto{margin-left:auto}.margin-right-0-5{margin-right:.5rem}.margin-right-auto{margin-right:auto}.ripple{height:8rem;width:8rem;position:absolute;border-radius:50%;transform:scale(0);background:radial-gradient(circle, rgba(var(--text-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%);pointer-events:none}.button--primary .ripple,.button--danger .ripple{background:radial-gradient(circle, rgba(var(--background-color), 0.3) 0%, rgba(0, 0, 0, 0) 50%)}.interact{position:relative;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:rgba(0,0,0,0)}.empty-state{display:grid;width:100%;padding:1.5rem 0}.observe-empty-state:empty{display:none !important}.observe-empty-state:not(:empty)+.empty-state{display:none}.bullet-point{display:flex;align-items:center;justify-content:center;margin:0 .8ch}.bullet-point::after{content:"";height:.4ch;width:.4ch;border-radius:.5em;background-color:var(--accent-color)}#confirmation_popup,#prompt_popup{flex-direction:column}#confirmation_popup h4,#prompt_popup h4{margin-bottom:.5rem}#confirmation_popup .flex,#prompt_popup .flex{margin-top:1rem}.popup__header{position:relative;display:grid;gap:.5rem;width:100%;padding:0 1.5rem;align-items:center}.popup__header>*{grid-row:1}.popup__header h3,.popup__header h4{grid-column:1/-1;justify-self:center;align-self:center}.popup__header__close{grid-column:1;margin-left:-1rem;justify-self:flex-start}#loader{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(var(--foreground-color), 1);z-index:100;display:grid;place-content:center;place-items:center;gap:1rem;text-align:center}#show_character_count{font-size:.8rem;margin-left:auto}#flo_data_status:not(:empty){color:red;padding:.5rem 0}#saved_ids_popup{--height: 80vh}#main_header{padding:.6rem 1rem}.app-brand{display:flex;gap:.3rem;align-items:center}.app-brand .icon{height:1.7rem;width:1.7rem}.app-name__company{font-size:.8rem;font-weight:500;color:rgba(var(--text-color), 0.8)}#main_card{display:flex;flex-direction:column;height:100%;width:100%;background-color:rgba(var(--foreground-color), 1);transition:background-color .3s}#pages_container{flex:1;overflow-y:auto}#main_navbar{display:flex;background:rgba(var(--text-color), 0.03)}#main_navbar.hide-away{position:absolute}#main_navbar ul{display:flex;height:100%;width:100%}#main_navbar ul li{width:100%}.nav-item{position:relative;display:flex;flex:1;width:100%;height:100%;flex-direction:column;align-items:center;justify-content:center;padding:.5rem .3rem;color:var(--text-color);font-size:.7rem;border-radius:.3rem;text-align:center}.nav-item .icon{transition:transform .2s}.nav-item__title{transition:opacity .2s,transform .2s}.nav-item--active{color:var(--accent-color)}.nav-item--active .icon{fill:var(--accent-color)}.nav-item__indicator{position:absolute;bottom:0;width:2rem;height:.3rem;background:var(--accent-color);border-radius:1rem 1rem 0 0;z-index:1}#fix_invalid_button{margin-bottom:1rem !important}.password-field label{display:flex}.password-field label input:checked~.visible{display:none}.password-field label input:not(:checked)~.invisible{display:none}.multi-state-button{display:grid;text-align:center;align-items:center;justify-items:center}.multi-state-button>*{grid-area:1/1/2/2}.multi-state-button button{z-index:1;width:100%}.clip{-webkit-clip-path:circle(0);clip-path:circle(0)}#flo_id_warning{padding-bottom:1.5rem;border-bottom:thin solid rgba(var(--text-color), 0.3)}#flo_id_warning .icon{height:4rem;width:4rem;padding:1rem;background-color:#ffc107;border-radius:3rem;fill:rgba(0,0,0,.8);margin-bottom:1.5rem}.generated-id-card{display:grid;gap:1rem}.generated-id-card h5{margin-bottom:.3rem}#contacts>:first-child{overflow-y:auto;align-content:flex-start;padding-bottom:4rem}#primary_actions_wrapper{display:grid;gap:.5rem}.primary-action{display:flex;padding:.8rem 1rem;gap:.5rem;white-space:normal;font-size:.85rem;border-radius:.5rem;background-color:rgba(0,0,0,0);border:thin solid rgba(var(--text-color), 0.3);text-align:left}.primary-action .icon{fill:var(--accent-color)}#search{position:relative;height:100%}#queried_flo_address h4{font-size:1.1rem}#queried_flo_address>sm-copy{font-size:.8rem}#token_list{display:flex;flex-wrap:wrap;gap:.5rem}.token-item{font-size:.9rem;padding:.5rem 1rem;background-color:rgba(var(--text-color), 0.06);border-radius:.3rem}.transaction{gap:1.2rem;padding:1rem;background-color:rgba(var(--text-color), 0.03);border-radius:.3rem}.transaction:not(:last-of-type){margin-bottom:1rem}.transaction .icon{fill:var(--accent-color)}.transaction__time,.transaction__link,.transaction__receiver{font-size:.8rem}.transaction__receiver{margin-left:.5rem;color:rgba(var(--text-color), 0.9);font-weight:500}.transaction__amount{font-weight:700}.transaction.mined .transaction__icon .icon,.transaction.received .transaction__icon .icon,.transaction.self .transaction__icon .icon{fill:var(--green)}.transaction.mined .transaction__amount,.transaction.received .transaction__amount,.transaction.self .transaction__amount{color:var(--green)}.transaction.mined .transaction__amount::before,.transaction.received .transaction__amount::before,.transaction.self .transaction__amount::before{content:"+ "}.transaction.sent .transaction__icon .icon{fill:var(--danger-color)}.transaction.sent .transaction__amount{color:var(--danger-color)}.transaction.sent .transaction__amount::before{content:"- "}.transaction p{font-size:.9rem;max-width:unset}.transaction__time{justify-self:flex-end;color:rgba(var(--text-color), 0.8)}#search_wrapper{width:min(100%,36rem)}#search_query_input{justify-self:center}#saved_ids_list{position:relative;align-content:flex-start;padding-bottom:1.5rem;margin-top:1rem;gap:1rem;grid-template-columns:repeat(auto-fill, minmax(16rem, 1fr))}.saved-id{grid-template-columns:auto 1fr;gap:0 .8rem;border-radius:.3rem;padding:.5rem;background-color:rgba(var(--text-color), 0.03);-webkit-user-select:none;-moz-user-select:none;user-select:none}.saved-id.highlight{box-shadow:0 0 .1rem .1rem var(--accent-color) inset}.saved-id .edit-saved{grid-area:1/1/3/2;padding:.3rem;position:relative}.saved-id .edit-saved .icon{position:absolute;height:1.2rem;width:1.2rem;right:0;bottom:0;border-radius:.5rem;padding:.2rem;background-color:rgba(var(--background-color), 1)}.saved-id__initial{display:flex;align-items:center;justify-content:center;height:2.4rem;width:2.4rem;font-size:1.2rem;text-transform:uppercase;color:rgba(var(--background-color), 1);font-weight:700;line-height:1;background-color:var(--accent-color);justify-self:flex-start;border-radius:2rem}.saved-id__label{align-self:flex-end}.saved-id__flo-id{font-size:.8rem}.page{position:relative;display:flex;flex-direction:column;overflow-y:auto;align-content:flex-start;padding:0 1rem;padding-bottom:3rem}.fab{position:absolute;right:0;bottom:0;margin:1.5rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.2);z-index:2}#add_address_button{border-radius:.5rem;color:rgba(var(--background-color), 1);background-color:var(--accent-color)}#add_address_button .icon{fill:rgba(var(--background-color), 1)}#balance_card{display:flex;flex-direction:column;gap:2rem;padding:max(1rem,2vw);background-color:rgba(var(--text-color), 0.06);aspect-ratio:4/2;justify-content:flex-end;border-radius:.5rem}#balance_card form{margin-top:1rem}#balance_card fieldset{border:none}.token-balance{display:flex;align-items:center;cursor:pointer;gap:.5rem;background-color:rgba(var(--text-color), 0.06);padding:.8rem;border-radius:.3rem;font-size:.9rem}.token-balance span:first-of-type::first-letter{text-transform:capitalize}.token-balance input{height:1rem;width:1rem;accent-color:var(--accent-color)}.token-receiver-combo{border:solid thin rgba(var(--text-color), 0.2);padding:.5rem;border-radius:.8rem}.token-receiver-combo--removable{grid-template-columns:1fr auto;grid-template-areas:"receiver receiver" "amount remove"}.token-receiver-combo--removable .token-receiver{grid-area:receiver}.token-receiver-combo--removable .token-amount{grid-area:amount}.token-receiver-combo--removable .remove-token-receiver{grid-area:remove}#transaction_result{display:grid;gap:.5rem;height:max(40vh,24rem);align-items:center;justify-content:center;text-align:center;align-content:center}#transaction_result:empty{display:none}#transaction_result h3{text-align:center;width:100%}#transaction_result .icon{justify-self:center;height:4rem;width:4rem;border-radius:5rem;margin-bottom:1rem;-webkit-animation:popup 1s;animation:popup 1s}#transaction_result .icon--success{fill:rgba(var(--background-color), 1);padding:1rem;background-color:#0bbe56}#transaction_result .icon--failed{background-color:rgba(var(--text-color), 0.03);fill:var(--danger-color)}#transaction_result sm-copy{font-size:.8rem}@-webkit-keyframes popup{0%{opacity:0;transform:scale(0.2) translateY(600%)}10%{transform:scale(0.2) translateY(5rem);opacity:1}40%{transform:scale(0.2) translateY(0)}80%{transform:scale(1.1) translateY(0)}100%{transform:scale(1) translateY(0)}}@keyframes popup{0%{opacity:0;transform:scale(0.2) translateY(600%)}10%{transform:scale(0.2) translateY(5rem);opacity:1}40%{transform:scale(0.2) translateY(0)}80%{transform:scale(1.1) translateY(0)}100%{transform:scale(1) translateY(0)}}#queried_address_transactions{display:flex;flex-direction:column;padding-bottom:4rem}#pagination_wrapper{position:fixed;bottom:0;right:0;margin:0 auto;padding:.5rem;background-color:rgba(var(--foreground-color), 1);z-index:5;border-radius:.7rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.2);margin:.5rem}.pagination__item{display:flex;padding:.2rem .5rem;border-radius:.3rem}.pagination__item--active{background-color:var(--accent-color);color:rgba(var(--background-color), 1)}legend,.label{font-size:.8rem;color:rgba(var(--text-color), 0.8)}#smartcontracts{display:grid;min-width:0;height:100%}#smartcontracts>*{grid-area:1/1}#smartcontracts fieldset{padding:.5rem;border-radius:.5rem;border:solid 1px rgba(var(--text-color), 0.3)}#smartcontracts fieldset legend{padding:0 .5rem}#smartcontracts label{padding:.3rem .5rem}#smartcontracts label:has(input:not(:disabled):not(:checked)){cursor:pointer}#smartcontracts input[type=radio]{height:1.1em;width:1.1em;margin-right:.5rem;accent-color:var(--accent-color)}#smartcontracts input[type=datetime-local]{width:100%;padding:.8rem .6rem;border:none;border-radius:.5rem;font-weight:500;font-family:inherit;font-size:inherit;color:inherit;background-color:rgba(var(--text-color), 0.06)}#smartcontracts input[type=datetime-local]:focus{outline:none;box-shadow:0 0 0 .1rem var(--accent-color)}#smartcontracts sm-input:not([placeholder]){--min-height: 3rem}.smart-contract-action{flex-direction:column;white-space:normal;gap:.5rem;color:rgba(var(--text-color), 0.8);width:5rem;font-size:.9rem;aspect-ratio:1/1;border-radius:.5rem;border:solid 1px rgba(var(--text-color), 0.3);text-align:center}.smart-contract-action .icon{fill:var(--accent-color)}#smart_contract_creation_templates{display:grid;gap:.5rem}#smart_contract_creation_templates li{display:flex;flex:1}.smart-contract-template{display:grid;grid-template-areas:"heading arrow" "description arrow";grid-template-columns:1fr auto;padding:max(1rem,2vw);border:solid 1px rgba(var(--text-color), 0.3);border-radius:.5rem;gap:.5rem;text-align:start;white-space:normal;width:100%;justify-content:flex-start}.smart-contract-template h4{grid-area:heading}.smart-contract-template p{grid-area:description;font-weight:400;line-height:1.3;font-size:.9rem;color:rgba(var(--text-color), 0.8);text-transform:none}.smart-contract-template .icon{margin-top:auto;grid-area:arrow;border:solid 1px rgba(var(--text-color), 0.3);border-radius:3rem;padding:.4rem;height:2rem;width:2rem;fill:var(--accent-color)}.payee-address-wrapper{display:grid;grid-template-columns:1fr auto;gap:.5rem}.payee-address-wrapper:first-of-type{grid-template-areas:"address" "share"}.payee-address-wrapper:not(:first-of-type){grid-template-areas:"address address" "share button"}.payee-address-wrapper .payee-address{grid-area:address}.payee-address-wrapper .payee-share{grid-area:share}.payee-address-wrapper .icon-only{grid-area:button}.payee-address-wrapper .icon-only{height:3.18rem}.choice-wrapper{display:grid;grid-template-columns:1fr auto;gap:.5rem}.choice-wrapper .icon-only{aspect-ratio:1/1;height:3.18rem}@media screen and (max-width: 40rem){#main_navbar.hide-away{bottom:0;left:0;right:0}#primary_actions_wrapper{grid-template-columns:1fr 1fr}.nav-item__title{margin-top:.3rem}.nav-item--active .icon{transform:translateY(50%)}.nav-item--active .nav-item__title{transform:translateY(100%);opacity:0}#pagination_wrapper{margin:0;margin-bottom:4.8rem;left:50%;right:auto;transform:translateX(-50%);width:calc(100vw - 2rem);flex-wrap:wrap}}@media screen and (min-width: 40rem){sm-popup{--width: 24rem}.popup__header{padding:1rem 1.5rem 0 1.5rem}body{display:flex;align-items:center;justify-content:center}#main_card{display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr;grid-template-areas:"header header" "nav main";position:relative}#main_header{grid-area:header}#pages_container{grid-area:main}.page{padding:0 1.5rem}#main_navbar{grid-area:nav;border-top:none;flex-direction:column;height:100%}#main_navbar ul{flex-direction:column}.nav-item{flex-direction:row;text-align:left;justify-content:flex-start;gap:.5rem;padding:1rem;font-weight:500;font-size:.8rem;border-radius:0;min-width:10rem}.nav-item__indicator{width:.25rem;height:50%;left:0;border-radius:0 1rem 1rem 0;bottom:auto}#primary_actions_wrapper{display:flex;flex-wrap:wrap}#create_flo_id_popup,#retrieve_flo_id_popup{--width: 26rem}#send{padding:0 6vw}#send sm-form{width:min(56rem,100%);margin:auto}#smart_contract_creation_templates{grid-template-columns:repeat(auto-fill, minmax(20rem, 1fr))}#smart_contract_deposit_form,#smart_contract_participate_form,#smart_contract_update_form,#smart_contract_trigger_form{width:min(36rem,100%);margin:auto;--gap: 1.5rem}#smart_contract_creation_form::part(form){gap:1.5rem;margin:auto;width:min(36rem,100%)}#smart_contract_creation_form::part(form).split-layout{grid-template-columns:1fr 1.5fr;align-items:flex-start;width:min(50rem,100%)}.payee-address-wrapper{display:grid;grid-template-columns:1fr 8rem 3rem}.payee-address-wrapper:first-of-type{grid-template-areas:"address share share"}.payee-address-wrapper:not(:first-of-type){grid-template-areas:"address share button"}}@media screen and (min-width: 56rem){#send sm-form::part(form){align-items:flex-start;grid-template-columns:1fr 1.5fr}}@media screen and (min-width: 64rem){#address_details_wrapper{grid-template-columns:auto 1fr;align-items:flex-start}#transactions_hero_section{position:-webkit-sticky;position:sticky;top:0}}@media(any-hover: hover){::-webkit-scrollbar{width:.5rem;height:.5rem}::-webkit-scrollbar-thumb{background:rgba(var(--text-color), 0.3);border-radius:1rem}::-webkit-scrollbar-thumb:hover{background:rgba(var(--text-color), 0.5)}.interact:not([disabled]){transition:background-color .3s}.interact:not([disabled]):hover{background-color:rgba(var(--text-color), 0.06)}.button:not([disabled]){transition:background-color .3s,filter .3s}.button:not([disabled]):hover{filter:contrast(2)}}@supports(overflow: overlay){body{overflow:overlay}}.hidden{display:none !important} \ No newline at end of file diff --git a/css/main.scss b/css/main.scss index cf75982..fa866b4 100644 --- a/css/main.scss +++ b/css/main.scss @@ -558,17 +558,17 @@ h3 { #main_header { padding: 0.6rem 1rem; } -.app-brand{ +.app-brand { display: flex; gap: 0.3rem; align-items: center; - .icon{ + .icon { height: 1.7rem; width: 1.7rem; } } -.app-name{ - &__company{ +.app-name { + &__company { font-size: 0.8rem; font-weight: 500; color: rgba(var(--text-color), 0.8); @@ -741,7 +741,7 @@ h3 { border-radius: 0.3rem; } .transaction { - gap: 1rem; + gap: 1.2rem; padding: 1rem; background-color: rgba(var(--text-color), 0.03); border-radius: 0.3rem; @@ -758,7 +758,39 @@ h3 { } &__receiver { margin-left: 0.5rem; - color: rgba(var(--text-color), 0.8); + color: rgba(var(--text-color), 0.9); + font-weight: 500; + } + &__amount { + font-weight: 700; + } + &.mined, + &.received, + &.self { + .transaction__icon { + .icon { + fill: var(--green); + } + } + .transaction__amount { + color: var(--green); + &::before { + content: "+ "; + } + } + } + &.sent { + .transaction__icon { + .icon { + fill: var(--danger-color); + } + } + .transaction__amount { + color: var(--danger-color); + &::before { + content: "- "; + } + } } p { font-size: 0.9rem; diff --git a/index.html b/index.html index 7a48fa2..a729315 100644 --- a/index.html +++ b/index.html @@ -187,13 +187,14 @@
-
+

Transactions

    @@ -830,7 +831,14 @@
    -

    +
    +
    Amount
    +
    +
    +
    +
    FLO Data
    +

    +
    { - filterFetchedTransactions() - render.paginatedTransactions(page) - }) - } else if (page % Math.ceil(1000 / txsPerPage) === 0 && floGlobals.query.transactions.length <= page * txsPerPage) { + }/* else if (floGlobals.query.totalPages <= page * txsPerPage) { loadMoreTransactions() } else { render.paginatedTransactions(page) - } + }*/ + fetchTransactions(query, page).then(() => { + filterFetchedTransactions() + render.paginatedTransactions(page) + }) } catch (err) { notify(err, 'error') } @@ -1877,45 +1885,55 @@ }) }, transactionCard(details) { - const { sender, receiver, floData, time, txid } = details + const { sender, receiver, floData, time, txid, netValue, mine } = details const { query: queriedFloId } = pagesData.params const clone = getRef('transaction_template').content.cloneNode(true).firstElementChild; - if (sender === receiver) { + if (mine) { + clone.classList.add('mined') + clone.querySelector('.transaction__icon').innerHTML = ` `; + clone.querySelector('.transaction__receiver').textContent = 'Coinbase' + } else if (sender === receiver) { + clone.classList.add('self') clone.querySelector('.transaction__icon').innerHTML = `sender and receiver is same`; + clone.querySelector('.transaction__receiver').textContent = receiver } else if (queriedFloId === sender) { - clone.querySelector('.transaction__icon').innerHTML = ``; + clone.classList.add('sent') + clone.querySelector('.transaction__icon').innerHTML = ``; + clone.querySelector('.transaction__receiver').textContent = receiver } else { - clone.querySelector('.transaction__icon').innerHTML = ``; + clone.classList.add('received') + clone.querySelector('.transaction__icon').innerHTML = ``; + clone.querySelector('.transaction__receiver').textContent = sender + } + if (netValue) { + clone.querySelector('.transaction__amount').textContent = `${netValue} FLO` + } else { + clone.querySelector('.transaction__amount').parentNode.remove() + } + if (floData) { + clone.querySelector('.transaction__flo-data').textContent = floData + } else { + clone.querySelector('.transaction__flo-data').parentNode.remove() } - clone.querySelector('.transaction__receiver').textContent = queriedFloId === sender ? receiver : sender - clone.querySelector('.transaction__flo-data').textContent = floData clone.querySelector('.transaction__link').href = `${floBlockchainAPI.current_server}tx/${txid}` clone.querySelector('.transaction__time').textContent = getFormattedTime(time * 1000) return clone }, paginatedTransactions(page = parseInt(pagesData.params.page) || 1) { - const { transactions, string: address, filteredTransactions } = floGlobals.query - let startingIndex = ((page - 1) * txsPerPage) - if ((filteredTransactions?.length || transactions.length) < startingIndex) { - startingIndex = 0; - window.history.replaceState({}, '', `#/search?type=address&query=${address}&page=1`) - pagesData.params.page = page = 1; - } - const endingIndex = startingIndex + txsPerPage + const { transactions, string: address, filteredTransactions, totalPages } = floGlobals.query const renderedTransactions = (filteredTransactions || transactions) - .slice(startingIndex, endingIndex) .map(transaction => render.transactionCard(transaction)) renderElem(getRef('queried_address_transactions'), html`${renderedTransactions}`) getRef('transactions_hero_section').scrollIntoView({ behavior: 'smooth', block: 'start' }) - if (floGlobals.query.transactions.length) { + if (transactions.length) { getRef('filter_selector').classList.remove('hidden') } else { getRef('filter_selector').classList.add('hidden') } - const paginationSegments = (filteredTransactions || transactions) ? Math.ceil((filteredTransactions || transactions).length / txsPerPage) : 0; + const paginationSegments = totalPages; let pagination = [] let startingPage = page - 2; let showTill = page + 2; @@ -1956,11 +1974,11 @@ } else { getRef('pagination_wrapper').classList.add('hidden') } - if (filteredTransactions && paginationSegments === page && filteredTransactions.length % txsPerPage !== 0 && transactions.length % txsPerPage === 0) { + /* if (filteredTransactions && paginationSegments === page && filteredTransactions.length % txsPerPage !== 0 && transactions.length % txsPerPage === 0) { document.getElementById('load_more_transactions').classList.remove('hidden') } else { document.getElementById('load_more_transactions').classList.add('hidden') - } + } */ }, availableAssetOptions() { return (floGlobals.tokens || []).map(token => html` ${token} `) @@ -2524,7 +2542,7 @@ getRef('token_list_wrapper').classList.remove('hidden') } // retrieve FLO balance - getRef('flo_balance').textContent = `${parseFloat(floBalance.toFixed(3))} FLO`; + getRef('flo_balance').textContent = `${parseFloat(floBalance.toFixed(8))} FLO`; } catch (e) { console.error(e) } @@ -2551,8 +2569,8 @@ string: '', filteredTransactions: null } - const txsPerPage = 25; - async function fetchTransactions(address, loadOlder = false) { + const txsPerPage = 100; + async function fetchTransactions(address, page = 1) { try { document.getElementById('load_more_transactions').classList.add('hidden') renderElem(getRef('pagination_wrapper'), html``) @@ -2562,23 +2580,12 @@ Loading transactions...
    `) - if (loadOlder) { - const { items, initItem } = await floWebWallet.listTransactions.syncOld(address, floGlobals.query.initItem) - floGlobals.query = { - transactions: [...items, ...floGlobals.query.transactions], - string: address, - initItem, - filteredTransactions: null - } - } else { - const { items, lastItem, initItem } = await floWebWallet.listTransactions(address) - floGlobals.query = { - transactions: items, - string: address, - lastItem, - initItem, - filteredTransactions: null - } + const { items, totalPages } = await floWebWallet.listTransactions(address, { pageSize: txsPerPage, page }) + floGlobals.query = { + transactions: items, + string: address, + filteredTransactions: null, + totalPages } } catch (err) { renderElem(getRef('queried_address_transactions'), html` Failed to load transactions `) @@ -2588,7 +2595,19 @@ function filterFetchedTransactions() { const filter = getRef('filter_selector').value; if (filter !== 'all') { - floGlobals.query.filteredTransactions = floGlobals.query.transactions.filter(t => filter === 'sent' ? t.sender === floGlobals.query.string : t.receiver === floGlobals.query.string) + floGlobals.query.filteredTransactions = floGlobals.query.transactions.filter(t => { + switch (filter) { + case 'sent': + return t.sender === floGlobals.query.string + break + case 'received': + return t.receiver === floGlobals.query.string + break + case 'mined': + return t.mined + break + } + }) } else { floGlobals.query.filteredTransactions = null } diff --git a/scripts/components.js b/scripts/components.js index 75d3898..1f1e510 100644 --- a/scripts/components.js +++ b/scripts/components.js @@ -1,7 +1,7 @@ /*jshint esversion: 6 */ // Components downloaded: chips,copy,form,input,notifications,popup,select,spinner,textarea,theme-toggle const smChips = document.createElement("template"); smChips.innerHTML = '
    ', customElements.define("sm-chips", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smChips.content.cloneNode(!0)), this.chipsWrapper = this.shadowRoot.querySelector(".sm-chips"), this.coverLeft = this.shadowRoot.querySelector(".cover--left"), this.coverRight = this.shadowRoot.querySelector(".cover--right"), this.navButtonLeft = this.shadowRoot.querySelector(".nav-button--left"), this.navButtonRight = this.shadowRoot.querySelector(".nav-button--right"), this.slottedOptions = void 0, this._value = void 0, this.scrollDistance = 0, this.assignedElements = [], this.scrollLeft = this.scrollLeft.bind(this), this.scrollRight = this.scrollRight.bind(this), this.fireEvent = this.fireEvent.bind(this), this.setSelectedOption = this.setSelectedOption.bind(this) } get value() { return this._value } set value(t) { this.setSelectedOption(t) } scrollLeft() { this.chipsWrapper.scrollBy({ left: -this.scrollDistance, behavior: "smooth" }) } scrollRight() { this.chipsWrapper.scrollBy({ left: this.scrollDistance, behavior: "smooth" }) } setSelectedOption(t) { this._value !== t && (this._value = t, this.assignedElements.forEach(e => { e.value == t ? (e.setAttribute("selected", ""), e.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" })) : e.removeAttribute("selected") })) } fireEvent() { this.dispatchEvent(new CustomEvent("change", { bubbles: !0, composed: !0, detail: { value: this._value } })) } connectedCallback() { this.setAttribute("role", "listbox"); const t = this.shadowRoot.querySelector("slot"); t.addEventListener("slotchange", e => { n.disconnect(), i.disconnect(), this.observeSelf.disconnect(), clearTimeout(this.slotChangeTimeout), this.slotChangeTimeout = setTimeout(() => { this.assignedElements = t.assignedElements(), this.assignedElements.forEach(t => { t.hasAttribute("selected") && (this._value = t.value) }), this.observeSelf.observe(this) }, 0) }); const e = new ResizeObserver(t => { t.forEach(t => { if (t.contentBoxSize) { const e = Array.isArray(t.contentBoxSize) ? t.contentBoxSize[0] : t.contentBoxSize; this.scrollDistance = .6 * e.inlineSize } else this.scrollDistance = .6 * t.contentRect.width }) }); e.observe(this), this.observeSelf = new IntersectionObserver((t, e) => { t.forEach(t => { t.isIntersecting && !this.hasAttribute("multiline") && this.assignedElements.length > 0 && (n.observe(this.assignedElements[0]), i.observe(this.assignedElements[this.assignedElements.length - 1]), e.unobserve(this)) }) }, { threshold: 1 }), this.chipsWrapper.addEventListener("option-clicked", t => { this._value !== t.target.value && (this.setSelectedOption(t.target.value), this.fireEvent()) }); const n = new IntersectionObserver(t => { t.forEach(t => { t.isIntersecting ? (this.navButtonLeft.classList.add("hide"), this.coverLeft.classList.add("hide")) : (this.navButtonLeft.classList.remove("hide"), this.coverLeft.classList.remove("hide")) }) }, { threshold: 1, root: this }), i = new IntersectionObserver(t => { t.forEach(t => { t.isIntersecting ? (this.navButtonRight.classList.add("hide"), this.coverRight.classList.add("hide")) : (this.navButtonRight.classList.remove("hide"), this.coverRight.classList.remove("hide")) }) }, { threshold: 1, root: this }); this.navButtonLeft.addEventListener("click", this.scrollLeft), this.navButtonRight.addEventListener("click", this.scrollRight) } disconnectedCallback() { this.navButtonLeft.removeEventListener("click", this.scrollLeft), this.navButtonRight.removeEventListener("click", this.scrollRight) } }); const smChip = document.createElement("template"); smChip.innerHTML = ' ', customElements.define("sm-chip", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smChip.content.cloneNode(!0)), this._value = void 0, this.radioButton = this.shadowRoot.querySelector("input"), this.fireEvent = this.fireEvent.bind(this), this.handleKeyDown = this.handleKeyDown.bind(this) } get value() { return this._value } fireEvent() { this.dispatchEvent(new CustomEvent("option-clicked", { bubbles: !0, composed: !0, detail: { value: this._value } })) } handleKeyDown(t) { "Enter" !== t.key && "Space" !== t.key || this.fireEvent() } connectedCallback() { this.setAttribute("role", "option"), this.setAttribute("tabindex", "0"), this._value = this.getAttribute("value"), this.addEventListener("click", this.fireEvent), this.addEventListener("keydown", this.handleKeyDown) } disconnectedCallback() { this.removeEventListener("click", this.fireEvent), this.removeEventListener("keydown", this.handleKeyDown) } }); -const smCopy = document.createElement("template"); smCopy.innerHTML = '

    ', customElements.define("sm-copy", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smCopy.content.cloneNode(!0)), this.copyContent = this.shadowRoot.querySelector(".copy-content"), this.copyButton = this.shadowRoot.querySelector(".copy-button"), this.copy = this.copy.bind(this) } static get observedAttributes() { return ["value"] } set value(t) { this.setAttribute("value", t) } get value() { return this.getAttribute("value") } fireEvent() { this.dispatchEvent(new CustomEvent("copy", { composed: !0, bubbles: !0, cancelable: !0 })) } copy() { navigator.clipboard.writeText(this.getAttribute("value")).then(t => this.fireEvent()).catch(t => console.error(t)) } connectedCallback() { this.copyButton.addEventListener("click", this.copy) } attributeChangedCallback(t, n, o) { "value" === t && this.copyContent.querySelector("slot").assignedNodes() && 0 === this.copyContent.querySelector("slot").assignedNodes().length && (this.copyContent.textContent = o) } disconnectedCallback() { this.copyButton.removeEventListener("click", this.copy) } }); +const smCopy = document.createElement("template"); smCopy.innerHTML = '

    ', customElements.define("sm-copy", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smCopy.content.cloneNode(!0)), this.copyContent = this.shadowRoot.querySelector(".copy-content"), this.copyButton = this.shadowRoot.querySelector(".copy-button"), this.copy = this.copy.bind(this) } static get observedAttributes() { return ["value"] } set value(t) { this.setAttribute("value", t) } get value() { return this.getAttribute("value") } fireEvent() { this.dispatchEvent(new CustomEvent("copy", { composed: !0, bubbles: !0, cancelable: !0 })) } copy() { navigator.clipboard.writeText(this.getAttribute("value")).then(t => this.fireEvent()).catch(t => console.error(t)) } connectedCallback() { this.copyButton.addEventListener("click", this.copy) } attributeChangedCallback(t, n, o) { if ("value" === t) { const t = this.copyContent.querySelector("slot"); if (!t) return; const n = t.assignedNodes(); n && n.length || (this.copyContent.textContent = o) } } disconnectedCallback() { this.copyButton.removeEventListener("click", this.copy) } }); const smForm = document.createElement("template"); smForm.innerHTML = '
    ', customElements.define("sm-form", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smForm.content.cloneNode(!0)), this.form = this.shadowRoot.querySelector("form"), this.invalidFieldsCount, this.skipSubmit = !1, this.isFormValid = void 0, this.supportedElements = "input, sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio", this.formElements = [], this._requiredElements = [], this.debounce = this.debounce.bind(this), this._checkValidity = this._checkValidity.bind(this), this.handleKeydown = this.handleKeydown.bind(this), this.reset = this.reset.bind(this), this.elementsChanged = this.elementsChanged.bind(this) } static get observedAttributes() { return ["skip-submit"] } get validity() { return this.isFormValid } debounce(e, t) { let i = null; return (...s) => { window.clearTimeout(i), i = window.setTimeout((() => { e.apply(null, s) }), t) } } _checkValidity() { this.submitButton && 0 !== this._requiredElements.length && (this.invalidFieldsCount = 0, this._requiredElements.forEach((([e, t]) => { (!e.disabled && t && !e.isValid || !t && !e.checkValidity()) && this.invalidFieldsCount++ })), this.isFormValid !== (0 === this.invalidFieldsCount) && (this.isFormValid = 0 === this.invalidFieldsCount, this.dispatchEvent(new CustomEvent(this.isFormValid ? "valid" : "invalid", { bubbles: !0, composed: !0 })), this.skipSubmit || (this.submitButton.disabled = !this.isFormValid))) } handleKeydown(e) { if ("Enter" === e.key && e.target.tagName.includes("INPUT")) if (0 === this.invalidFieldsCount) this.submitButton && this.submitButton.click(), this.dispatchEvent(new CustomEvent("submit", { bubbles: !0, composed: !0 })); else for (const [e, t] of this._requiredElements) { if (t ? !e.isValid : !e.checkValidity()) { (e?.shadowRoot?.lastElementChild || e).animate([{ transform: "translateX(-1rem)" }, { transform: "translateX(1rem)" }, { transform: "translateX(-0.5rem)" }, { transform: "translateX(0.5rem)" }, { transform: "translateX(0)" }], { duration: 300, easing: "ease" }), t ? e.focusIn() : e.focus(); break } } } reset() { this.formElements.forEach((([e, t]) => { if (t) e.reset(); else switch (e.type) { case "checkbox": case "radio": e.checked = !1; break; default: e.value = "" } })), this._checkValidity() } elementsChanged() { this.formElements = [...this.querySelectorAll(this.supportedElements)].map((e => [e, e.tagName.includes("-")])), this._requiredElements = this.formElements.filter((([e]) => e.hasAttribute("required"))), this.submitButton = this.querySelector('[variant="primary"], [type="submit"]'), this.resetButton = this.querySelector('[type="reset"]'), this.resetButton && this.resetButton.addEventListener("click", this.reset), this._checkValidity() } connectedCallback() { const e = this.debounce(this.elementsChanged, 100); this.addEventListener("input", this.debounce(this._checkValidity, 100)), this.addEventListener("keydown", this.debounce(this.handleKeydown, 100)), this.shadowRoot.querySelector("slot").addEventListener("slotchange", e), this.mutationObserver = new MutationObserver((t => { t.forEach((t => { ("childList" === t.type && [...t.addedNodes].some((e => 1 === e.nodeType && e.querySelector(this.supportedElements))) || [...t.removedNodes].some((e => 1 === e.nodeType && e.querySelector(this.supportedElements)))) && e() })) })), this.mutationObserver.observe(this, { childList: !0, subtree: !0 }) } attributeChangedCallback(e, t, i) { "skip-submit" === e && (this.skipSubmit = null !== i) } disconnectedCallback() { this.removeEventListener("input", this.debounce(this._checkValidity, 100)), this.removeEventListener("keydown", this.debounce(this.handleKeydown, 100)), this.mutationObserver.disconnect() } }); const smInput = document.createElement("template"); smInput.innerHTML = '
    ', customElements.define("sm-input", class extends HTMLElement { constructor() { super(), this.attachShadow({ mode: "open" }).append(smInput.content.cloneNode(!0)), this.inputParent = this.shadowRoot.querySelector(".input"), this.input = this.shadowRoot.querySelector("input"), this.clearBtn = this.shadowRoot.querySelector(".clear"), this.label = this.shadowRoot.querySelector(".label"), this.feedbackText = this.shadowRoot.querySelector(".feedback-text"), this.outerContainer = this.shadowRoot.querySelector(".outer-container"), this.optionList = this.shadowRoot.querySelector(".datalist"), this._helperText = "", this._errorText = "", this.isRequired = !1, this.datalist = [], this.validationFunction = void 0, this.reflectedAttributes = ["value", "required", "disabled", "type", "inputmode", "readonly", "min", "max", "pattern", "minlength", "maxlength", "step", "list", "autocomplete"], this.reset = this.reset.bind(this), this.clear = this.clear.bind(this), this.focusIn = this.focusIn.bind(this), this.focusOut = this.focusOut.bind(this), this.fireEvent = this.fireEvent.bind(this), this.checkInput = this.checkInput.bind(this), this.showError = this.showError.bind(this), this.allowOnlyNum = this.allowOnlyNum.bind(this), this.handleOptionClick = this.handleOptionClick.bind(this), this.handleInputNavigation = this.handleInputNavigation.bind(this), this.handleDatalistNavigation = this.handleDatalistNavigation.bind(this), this.handleFocus = this.handleFocus.bind(this), this.handleBlur = this.handleBlur.bind(this) } static get observedAttributes() { return ["value", "placeholder", "required", "disabled", "type", "inputmode", "readonly", "min", "max", "pattern", "minlength", "maxlength", "step", "helper-text", "error-text", "list"] } get value() { return this.input.value } set value(t) { t !== this.input.value && (this.input.value = t, this.checkInput()) } get placeholder() { return this.getAttribute("placeholder") } set placeholder(t) { this.setAttribute("placeholder", t) } get type() { return this.getAttribute("type") } set type(t) { this.setAttribute("type", t) } get validity() { return this.input.validity } get disabled() { return this.hasAttribute("disabled") } set disabled(t) { t ? (this.inputParent.classList.add("disabled"), this.setAttribute("disabled", "")) : (this.inputParent.classList.remove("disabled"), this.removeAttribute("disabled")) } get readOnly() { return this.hasAttribute("readonly") } set readOnly(t) { t ? this.setAttribute("readonly", "") : this.removeAttribute("readonly") } set customValidation(t) { this.validationFunction = t } set errorText(t) { this._errorText = t } showError() { this.feedbackText.className = "feedback-text error", this.feedbackText.innerHTML = ` ${this._errorText}` } set helperText(t) { this._helperText = t } get isValid() { if ("" !== this.input.value) { const t = this.input.checkValidity(); let e = !0; return this.validationFunction && (e = Boolean(this.validationFunction(this.input.value))), t && e ? (this.feedbackText.className = "feedback-text success", this.feedbackText.textContent = "") : this._errorText && this.showError(), t && e } } reset() { this.value = "" } clear() { this.value = "", this.input.focus(), this.fireEvent() } focusIn() { this.input.focus() } focusOut() { this.input.blur() } fireEvent() { let t = new Event("input", { bubbles: !0, cancelable: !0, composed: !0 }); this.dispatchEvent(t) } searchDatalist(t) { const e = this.datalist.filter(e => e.toLowerCase().includes(t.toLowerCase())); if (e.sort((e, n) => { const i = e.toLowerCase().indexOf(t.toLowerCase()), s = n.toLowerCase().indexOf(t.toLowerCase()); return i - s }), e.length) { if (this.optionList.children.length > e.length) { const t = this.optionList.children.length - e.length; for (let e = 0; e < t; e++)this.optionList.removeChild(this.optionList.lastChild) } e.forEach((t, e) => { if (this.optionList.children[e]) this.optionList.children[e].textContent = t; else { const e = document.createElement("li"); e.textContent = t, e.classList.add("datalist-item"), e.setAttribute("tabindex", "0"), this.optionList.appendChild(e) } }), this.optionList.classList.remove("hidden") } else this.optionList.classList.add("hidden") } checkInput(t) { this.hasAttribute("readonly") || ("" !== this.input.value ? this.clearBtn.classList.remove("hidden") : this.clearBtn.classList.add("hidden")), this.hasAttribute("placeholder") && "" !== this.getAttribute("placeholder").trim() && ("" !== this.input.value ? (this.animate ? this.inputParent.classList.add("animate-placeholder") : this.label.classList.add("hidden"), this.datalist.length && (this.searchTimeout && clearTimeout(this.searchTimeout), this.searchTimeout = setTimeout(() => { this.searchDatalist(this.input.value.trim()) }, 100))) : (this.animate ? this.inputParent.classList.remove("animate-placeholder") : this.label.classList.remove("hidden"), this.feedbackText.textContent = "", this.datalist.length && (this.optionList.innerHTML = "", this.optionList.classList.add("hidden")))) } allowOnlyNum(t) { 1 === t.key.length && (("." !== t.key || !t.target.value.includes(".") && 0 !== t.target.value.length) && ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."].includes(t.key) || t.preventDefault()) } handleOptionClick(t) { this.input.value = t.target.textContent, this.optionList.classList.add("hidden"), this.input.focus() } handleInputNavigation(t) { "ArrowDown" === t.key ? (t.preventDefault(), this.optionList.children.length && this.optionList.children[0].focus()) : "ArrowUp" === t.key && (t.preventDefault(), this.optionList.children.length && this.optionList.children[this.optionList.children.length - 1].focus()) } handleDatalistNavigation(t) { "ArrowUp" === t.key ? (t.preventDefault(), this.shadowRoot.activeElement.previousElementSibling ? this.shadowRoot.activeElement.previousElementSibling.focus() : this.input.focus()) : "ArrowDown" === t.key ? (t.preventDefault(), this.shadowRoot.activeElement.nextElementSibling ? this.shadowRoot.activeElement.nextElementSibling.focus() : this.input.focus()) : "Enter" !== t.key && " " !== t.key || (t.preventDefault(), this.input.value = t.target.textContent, this.optionList.classList.add("hidden"), this.input.focus()) } handleFocus(t) { this.datalist.length && this.searchDatalist(this.input.value.trim()) } handleBlur(t) { this.datalist.length && this.optionList.classList.add("hidden") } connectedCallback() { this.animate = this.hasAttribute("animate"), this.setAttribute("role", "textbox"), this.input.addEventListener("input", this.checkInput), this.clearBtn.addEventListener("click", this.clear), this.datalist.length && (this.optionList.addEventListener("click", this.handleOptionClick), this.input.addEventListener("keydown", this.handleInputNavigation), this.optionList.addEventListener("keydown", this.handleDatalistNavigation)), this.input.addEventListener("focusin", this.handleFocus), this.addEventListener("focusout", this.handleBlur) } attributeChangedCallback(t, e, n) { if (e !== n) switch (this.reflectedAttributes.includes(t) && (this.hasAttribute(t) ? this.input.setAttribute(t, this.getAttribute(t) ? this.getAttribute(t) : "") : this.input.removeAttribute(t)), t) { case "placeholder": this.label.textContent = n, this.setAttribute("aria-label", n); break; case "value": this.checkInput(); break; case "type": this.hasAttribute("type") && "number" === this.getAttribute("type") ? (this.input.setAttribute("inputmode", "decimal"), this.input.addEventListener("keydown", this.allowOnlyNum)) : this.input.removeEventListener("keydown", this.allowOnlyNum); break; case "helper-text": this._helperText = n; break; case "error-text": this._errorText = n; break; case "required": this.isRequired = this.hasAttribute("required"), this.isRequired ? this.setAttribute("aria-required", "true") : this.setAttribute("aria-required", "false"); break; case "readonly": this.hasAttribute("readonly") ? this.inputParent.classList.add("readonly") : this.inputParent.classList.remove("readonly"); break; case "disabled": this.hasAttribute("disabled") ? this.inputParent.classList.add("disabled") : this.inputParent.classList.remove("disabled"); break; case "list": this.hasAttribute("list") && "" !== this.getAttribute("list").trim() && (this.datalist = this.getAttribute("list").split(",")) } } disconnectedCallback() { this.input.removeEventListener("input", this.checkInput), this.clearBtn.removeEventListener("click", this.clear), this.input.removeEventListener("keydown", this.allowOnlyNum), this.optionList.removeEventListener("click", this.handleOptionClick), this.input.removeEventListener("keydown", this.handleInputNavigation), this.optionList.removeEventListener("keydown", this.handleDatalistNavigation), this.input.removeEventListener("focusin", this.handleFocus), this.removeEventListener("focusout", this.handleBlur) } }); const smNotifications = document.createElement("template"); smNotifications.innerHTML = '
    ', customElements.define("sm-notifications", class extends HTMLElement { constructor() { super(), this.shadow = this.attachShadow({ mode: "open" }).append(smNotifications.content.cloneNode(!0)), this.notificationPanel = this.shadowRoot.querySelector(".notification-panel"), this.animationOptions = { duration: 300, fill: "forwards", easing: "cubic-bezier(0.175, 0.885, 0.32, 1.275)" }, this.push = this.push.bind(this), this.createNotification = this.createNotification.bind(this), this.removeNotification = this.removeNotification.bind(this), this.clearAll = this.clearAll.bind(this), this.remove = this.remove.bind(this), this.handlePointerMove = this.handlePointerMove.bind(this), this.startX = 0, this.currentX = 0, this.endX = 0, this.swipeDistance = 0, this.swipeDirection = "", this.swipeThreshold = 0, this.startTime = 0, this.swipeTime = 0, this.swipeTimeThreshold = 200, this.currentTarget = null, this.mediaQuery = window.matchMedia("(min-width: 640px)"), this.handleOrientationChange = this.handleOrientationChange.bind(this), this.isLandscape = !1 } randString(n) { let t = ""; const e = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; for (let i = 0; i < n; i++)t += e.charAt(Math.floor(Math.random() * e.length)); return t } createNotification(n, t = {}) { const { pinned: e = !1, icon: i = "", action: o } = t, r = document.createElement("div"); r.id = this.randString(8), r.className = `notification ${e ? "pinned" : ""}`; const a = document.createElement("div"); a.className = "icon-container", a.innerHTML = i; const s = document.createElement("output"); if (s.textContent = n, r.append(a, s), o) { const n = document.createElement("button"); n.className = "action", n.innerText = o.label, n.addEventListener("click", o.callback) } if (e) { const n = document.createElement("button"); n.className = "close", n.innerHTML = ' ', n.addEventListener("click", () => { this.remove(r.id) }), r.append(n) } return r } push(n, t = {}) { const e = this.createNotification(n, t); return this.isLandscape ? this.notificationPanel.append(e) : this.notificationPanel.prepend(e), this.notificationPanel.animate([{ transform: `translateY(${this.isLandscape ? "" : "-"}${e.clientHeight}px)` }, { transform: "none" }], this.animationOptions), e.animate([{ transform: "translateY(-1rem)", opacity: "0" }, { transform: "none", opacity: "1" }], this.animationOptions).onfinish = (n => { n.target.commitStyles(), n.target.cancel() }), e.querySelector(".action") && e.querySelector(".action").addEventListener("click", t.action.callback), e.id } removeNotification(n, t = "left") { if (!n) return; const e = "left" === t ? "-" : "+"; n.animate([{ transform: this.currentX ? `translateX(${this.currentX}px)` : "none", opacity: "1" }, { transform: `translateX(calc(${e}${Math.abs(this.currentX)}px ${e} 1rem))`, opacity: "0" }], this.animationOptions).onfinish = (() => { n.remove() }) } remove(n) { const t = this.notificationPanel.querySelector(`#${n}`); t && this.removeNotification(t) } clearAll() { Array.from(this.notificationPanel.children).forEach(n => { this.removeNotification(n) }) } handlePointerMove(n) { this.currentX = n.clientX - this.startX, this.currentTarget.style.transform = `translateX(${this.currentX}px)` } handleOrientationChange(n) { this.isLandscape = n.matches, n.matches } connectedCallback() { this.handleOrientationChange(this.mediaQuery), this.mediaQuery.addEventListener("change", this.handleOrientationChange), this.notificationPanel.addEventListener("pointerdown", n => { n.target.closest(".close") ? this.removeNotification(n.target.closest(".notification")) : n.target.closest(".notification") && (this.swipeThreshold = n.target.closest(".notification").getBoundingClientRect().width / 2, this.currentTarget = n.target.closest(".notification"), this.currentTarget.setPointerCapture(n.pointerId), this.startTime = Date.now(), this.startX = n.clientX, this.startY = n.clientY, this.notificationPanel.addEventListener("pointermove", this.handlePointerMove)) }), this.notificationPanel.addEventListener("pointerup", n => { this.endX = n.clientX, this.endY = n.clientY, this.swipeDistance = Math.abs(this.endX - this.startX), this.swipeTime = Date.now() - this.startTime, this.endX > this.startX ? this.swipeDirection = "right" : this.swipeDirection = "left", this.swipeTime < this.swipeTimeThreshold ? this.swipeDistance > 50 && this.removeNotification(this.currentTarget, this.swipeDirection) : this.swipeDistance > this.swipeThreshold ? this.removeNotification(this.currentTarget, this.swipeDirection) : this.currentTarget.animate([{ transform: `translateX(${this.currentX}px)` }, { transform: "none" }], this.animationOptions).onfinish = (n => { n.target.commitStyles(), n.target.cancel() }), this.notificationPanel.removeEventListener("pointermove", this.handlePointerMove), this.notificationPanel.releasePointerCapture(n.pointerId), this.currentX = 0 }); const n = new MutationObserver(n => { n.forEach(n => { "childList" === n.type && n.addedNodes.length && !n.addedNodes[0].classList.contains("pinned") && setTimeout(() => { this.removeNotification(n.addedNodes[0]) }, 5e3) }) }); n.observe(this.notificationPanel, { childList: !0 }) } disconnectedCallback() { mediaQueryList.removeEventListener("change", handleOrientationChange) } }); From ffa2726a9e8c9dff558bdc2e40657c57b3db8180 Mon Sep 17 00:00:00 2001 From: sairaj mote Date: Thu, 6 Jul 2023 18:30:01 +0530 Subject: [PATCH 12/13] Minor bug fix --- index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index a729315..7d4182f 100644 --- a/index.html +++ b/index.html @@ -2514,12 +2514,12 @@ getRef('address_details_wrapper').classList.remove('hidden') floWebWallet.getLabels().then(allLabels => { if (allLabels[queriedFloId]) { - renderElem(getRef('queried_flo_address'), html`

    ${allLabels[queriedFloId]}

    `) + getRef('queried_flo_address').innerHTML = html`

    ${allLabels[queriedFloId]}

    `; } else { - renderElem(getRef('queried_flo_address'), html` + getRef('queried_flo_address').innerHTML = html`

    FLO Address

    - `) + `; } }) const queriedFloId = address || getRef('search_query_input').value.trim() From eb443096414ce0b36077d848076b68ef723cb63b Mon Sep 17 00:00:00 2001 From: sairaj mote Date: Thu, 6 Jul 2023 18:30:49 +0530 Subject: [PATCH 13/13] bug fix --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 7d4182f..5280c2a 100644 --- a/index.html +++ b/index.html @@ -2514,9 +2514,9 @@ getRef('address_details_wrapper').classList.remove('hidden') floWebWallet.getLabels().then(allLabels => { if (allLabels[queriedFloId]) { - getRef('queried_flo_address').innerHTML = html`

    ${allLabels[queriedFloId]}

    `; + getRef('queried_flo_address').innerHTML = `

    ${allLabels[queriedFloId]}

    `; } else { - getRef('queried_flo_address').innerHTML = html` + getRef('queried_flo_address').innerHTML = `

    FLO Address

    `;