diff --git a/README.md b/README.md index bfd001a..08537a0 100644 --- a/README.md +++ b/README.md @@ -143,8 +143,8 @@ Table of Contents - **getmasternoderewardstotal:** Returns the total number of coins earned in masternode rewards for a specific address that arrived after a specific block height *\*only applicable to masternode coins* - **Claim Address:** Allows anyone to set custom display names for wallet addresses that they own using the **Sign Message** feature from their local wallet. Includes *bad word* filter support. - **Orphaned Blocks:** Displays a list of orphaned blocks with links to the next and previous "good" blocks - - **Block Info:** Displays block summary and list of transactions for a specific block height along with optional hash algorithm for multi-algo coins - - **Transaction Info:** Displays transaction summary, optional OP_RETURN value, list of input addresses and output addresses for a specific transaction + - **Block Info:** Displays block summary and list of transactions for a specific block height along with optional hash algorithm for multi-algo coins and optional list of wallet addresses that extracted/mined the block + - **Transaction Info:** Displays transaction summary, optional OP_RETURN value, optional list of wallet addresses that extracted/mined the coinbase transaction, list of input addresses and output addresses for a specific transaction - **Address Info:** Displays wallet address summary (balance, total sent, total received, QR code) and a list of latest transactions for a specific wallet address - Choose from 26 built-in themes with tweakable settings such as light and dark options to customize the look and feel of the explorer: - **Exor** *\*default theme made especially for eIquidus* diff --git a/lib/database.js b/lib/database.js index 1f2b593..ec08c24 100644 --- a/lib/database.js +++ b/lib/database.js @@ -433,6 +433,46 @@ function after_update_claim_name(hash, claim_name, cb) { }); } +function get_extracted_by_addresses(show_extracted_by, internal, tx, cb) { + // check if the extracted by addresses should be found + if (show_extracted_by == true) { + // check if this is a coinbase tx + if ( + tx != null && + tx.vout != null && + ( + tx.vin == null || + tx.vin.length === 0 || + ( + tx.vin.length === 1 && + tx.vin[0].addresses === 'coinbase' && + tx.vin[0].amount != 0 + ) + ) + ) { + // get a list of all the block reward addresses + const extracted_by_addresses = tx.vout.map(v => v.addresses); + + // check if this is an internal call which requires an additional lookup on claim names + if (internal) { + // add claim name data to the array + module.exports.get_extracted_by_claim_names(extracted_by_addresses, function(updated_extracted_by_addresses) { + return cb(updated_extracted_by_addresses); + }); + } else { + // return the extracted by addresses without looking up claim name data + return cb(extracted_by_addresses); + } + } else { + // no extracted by addresses for this tx + return cb([]); + } + } else { + // extracted by addresses are not enabled + return cb([]); + } +} + module.exports = { // initialize DB connect: function(database, cb) { @@ -680,6 +720,15 @@ module.exports = { }); }, + get_claim_names: function(hash_array, cb) { + ClaimAddress.find({ a_id: { $in: hash_array } }).exec().then((claim_records) => { + return cb(claim_records); + }).catch((err) => { + console.log(err); + return cb([]); + }); + }, + get_richlist: function(coin, cb) { find_richlist(coin, function(richlist) { return cb(richlist); @@ -861,7 +910,7 @@ module.exports = { let txs = []; async.eachSeries(block.tx, function(current_tx, loop) { - find_tx(current_tx[i], function(tx) { + find_tx(current_tx, function(tx) { if (tx) { // add tx to the list txs.push(tx); @@ -877,41 +926,53 @@ module.exports = { get_last_txs: function(start, length, min, internal, cb) { this.get_last_txs_ajax(start, length, min, function(txs, count) { - var data = []; + const show_extracted_by = (internal ? settings.index_page.show_extracted_by : settings.api_page.public_apis.ext.getlasttxs.show_extracted_by); + let data = []; - for (i = 0; i < txs.length; i++) { - if (internal) { - var row = []; + async.timesSeries(txs.length, function(i, loop) { + // get the extracted by address data for this tx + get_extracted_by_addresses(show_extracted_by, internal, txs[i], function(extracted_by_addresses) { + if (internal) { + let row = []; - row.push(txs[i].blockindex); - row.push(txs[i].blockhash); - row.push(txs[i].txid); - row.push(txs[i].vout.length); - row.push((txs[i].total / 100000000)); - row.push(txs[i].timestamp); + row.push(txs[i].blockindex); + row.push(txs[i].blockhash); + row.push(txs[i].txid); + row.push(txs[i].vout.length); + row.push((txs[i].total / 100000000)); + row.push(txs[i].timestamp); - if (settings.block_page.multi_algorithm.show_algo == true) - row.push('algo:' + (txs[i].algo == null ? '' : txs[i].algo)); + if (settings.block_page.multi_algorithm.show_algo == true) + row.push('algo:' + (txs[i].algo == null ? '' : txs[i].algo)); - data.push(row); - } else { - let data_entry = { - blockindex: txs[i].blockindex, - blockhash: txs[i].blockhash, - txid: txs[i].txid, - recipients: txs[i].vout.length, - amount: (txs[i].total / 100000000), - timestamp: txs[i].timestamp - }; + if (show_extracted_by == true) + row.push('extracted_by:' + JSON.stringify(extracted_by_addresses)); + + data.push(row); + loop(); + } else { + let data_entry = { + blockindex: txs[i].blockindex, + blockhash: txs[i].blockhash, + txid: txs[i].txid, + recipients: txs[i].vout.length, + amount: (txs[i].total / 100000000), + timestamp: txs[i].timestamp + }; - if (settings.block_page.multi_algorithm.show_algo == true) - data_entry.algo = (txs[i].algo == null ? '' : txs[i].algo); + if (settings.block_page.multi_algorithm.show_algo == true) + data_entry.algo = (txs[i].algo == null ? '' : txs[i].algo); - data.push(data_entry); - } - } + if (show_extracted_by == true) + data_entry.extracted_by = extracted_by_addresses; - return cb(data, count); + data.push(data_entry); + loop(); + } + }); + }, function() { + return cb(data, count); + }); }); }, @@ -1988,5 +2049,35 @@ module.exports = { }); }, + get_extracted_by_claim_names: function(extracted_by_addresses, cb) { + // check if custom claim names are enabled + if (settings.claim_address_page.enabled == true) { + // lookup the claim names for the extracted addresses + module.exports.get_claim_names(extracted_by_addresses, function(claim_names) { + // combine the addresses from the original array with the claim names to create an object array + extracted_by_addresses = extracted_by_addresses.map(address => { + const match = claim_names.find(doc => doc.a_id === address); + + return { + a_id: address, + claimname: match ? match.claim_name : '' + }; + }); + + return cb(extracted_by_addresses); + }); + } else { + // create an object array of the extracted addresses + extracted_by_addresses = extracted_by_addresses.map(address => { + return { + a_id: address, + claimname: '' + }; + }); + + return cb(extracted_by_addresses); + } + }, + fs: fs }; \ No newline at end of file diff --git a/lib/settings.js b/lib/settings.js index 62aaddb..8328121 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -615,7 +615,10 @@ exports.index_page = { // reload_table_seconds: The time in seconds to automatically reload the table data from the server // Set to 0 to disable automatic reloading of table data "reload_table_seconds": 60 - } + }, + // show_extracted_by: Determine whether to show a new column with the address(es) that mined the block + // If enabled, a new column will be displayed on the index page that displays the address(es) that mined the block for the coinbase transaction only + "show_extracted_by": false }; // block_page: a collection of settings that pertain to the block page @@ -649,7 +652,10 @@ exports.block_page = { // key_name: The name of the key or identifier in the raw block data that determines which hash algorithm was used to mine a particular block // NOTE: Changing this option will require a full reindex of the blockchain data before previously synced blocks can display the algorithm "key_name": "pow_algo" - } + }, + // show_extracted_by: Determine whether to show a new column with the address(es) that mined the block + // If enabled, a new column will be displayed on the block page that displays the address(es) that mined the block + "show_extracted_by": false }; // transaction_page: a collection of settings that pertain to the transaction/tx page @@ -675,7 +681,10 @@ exports.transaction_page = { "genesis_tx": "dd1d332ad2d8d8f49195056d482ae3c96fd2d16e9d166413b27ca7f19775644c", // show_op_return: Determine whether to decode and show OP_RETURN values that may have been embeddeded in a transaction // NOTE: Enabling this option will require a full reindex of the blockchain data before previously synced transactions can display the OP_RETURN value - "show_op_return": false + "show_op_return": false, + // show_extracted_by: Determine whether to show a new column with the address(es) that mined the block + // If enabled, a new column will be displayed on the transaction page that displays the address(es) that mined the block for the coinbase transaction only + "show_extracted_by": false }; // address_page: a collection of settings that pertain to the address page @@ -1263,7 +1272,10 @@ exports.api_page = { // If set to false, the /ext/getlasttxs api will be completely disabled for public use (no definition on the api page and a disabled error msg if you try to call the endpoint directly) but the api will still be available internally to the explorer "enabled": true, // max_items_per_query: The maximum # of transactions that can be returned from the /ext/getlasttxs api endpoint in a single call - "max_items_per_query": 100 + "max_items_per_query": 100, + // show_extracted_by: Determine whether to return a new field with the address(es) that mined the block + // If enabled, a new field will be added to the /ext/getlasttxs api endpoint that displays the address(es) that mined the block for the coinbase transaction only + "show_extracted_by": false }, // getcurrentprice: a collection of settings that pertain to the /ext/getcurrentprice api endpoint // Returns last known exchange price diff --git a/routes/index.js b/routes/index.js index 25baba5..ca5b106 100644 --- a/routes/index.js +++ b/routes/index.js @@ -6,6 +6,25 @@ const lib = require('../lib/explorer'); const async = require('async'); function send_block_data(res, block, txs, title_text, orphan) { + let extracted_by_addresses = []; + + // check if the extracted by addresses should be found + if (settings.block_page.show_extracted_by == true && txs != null && txs.length > 0) { + // find the block reward tx + const block_reward_tx = txs.find(tx => tx.vin != null && (tx.vin.length === 0 || (tx.vin.length === 1 && tx.vin[0].addresses === 'coinbase' && tx.vin[0].amount != 0))); + + // get a list of all the block reward addresses + extracted_by_addresses = (block_reward_tx ? block_reward_tx.vout.map(v => v.addresses) : []); + + // add claim name data to the array + db.get_extracted_by_claim_names(extracted_by_addresses, function(updated_extracted_by_addresses) { + finalize_send_block_data(res, block, txs, title_text, orphan, updated_extracted_by_addresses); + }); + } else + finalize_send_block_data(res, block, txs, title_text, orphan, extracted_by_addresses); +} + +function finalize_send_block_data(res, block, txs, title_text, orphan, extracted_by_addresses) { res.render( 'block', { @@ -14,6 +33,7 @@ function send_block_data(res, block, txs, title_text, orphan) { orphan: orphan, confirmations: settings.shared_pages.confirmations, txs: txs, + extracted_by_addresses: extracted_by_addresses, showSync: db.check_show_sync_message(), customHash: get_custom_hash(), styleHash: get_style_hash(), @@ -24,6 +44,35 @@ function send_block_data(res, block, txs, title_text, orphan) { } function send_tx_data(res, tx, blockcount, orphan) { + let extracted_by_addresses = []; + + // check if the extracted by addresses should be found + if ( + settings.transaction_page.show_extracted_by == true && + tx != null && + tx.vout != null && + ( + tx.vin == null || + tx.vin.length === 0 || + ( + tx.vin.length === 1 && + tx.vin[0].addresses === 'coinbase' && + tx.vin[0].amount != 0 + ) + ) + ) { + // get a list of all the block reward addresses + extracted_by_addresses = tx.vout.map(v => v.addresses); + + // add claim name data to the array + db.get_extracted_by_claim_names(extracted_by_addresses, function(updated_extracted_by_addresses) { + finalize_send_tx_data(res, tx, blockcount, orphan, updated_extracted_by_addresses); + }); + } else + finalize_send_tx_data(res, tx, blockcount, orphan, extracted_by_addresses); +} + +function finalize_send_tx_data(res, tx, blockcount, orphan, extracted_by_addresses) { res.render( 'tx', { @@ -32,6 +81,7 @@ function send_tx_data(res, tx, blockcount, orphan) { orphan: orphan, confirmations: settings.shared_pages.confirmations, blockcount: blockcount, + extracted_by_addresses: extracted_by_addresses, showSync: db.check_show_sync_message(), customHash: get_custom_hash(), styleHash: get_style_hash(), @@ -109,6 +159,11 @@ function get_block_data_from_wallet(block, res, orphan) { total: total.toFixed(8) }); + if (settings.block_page.show_extracted_by == true) { + // add the vin object to the tx data + ntxs[ntxs.length - 1].vin = (vin == null || vin.length == 0 ? [] : nvin); + } + loop(); }); }); diff --git a/settings.json.template b/settings.json.template index db690bf..d88a5d2 100644 --- a/settings.json.template +++ b/settings.json.template @@ -702,7 +702,10 @@ // reload_table_seconds: The time in seconds to automatically reload the table data from the server // Set to 0 to disable automatic reloading of table data "reload_table_seconds": 60 - } + }, + // show_extracted_by: Determine whether to show a new column with the address(es) that mined the block + // If enabled, a new column will be displayed on the index page that displays the address(es) that mined the block for the coinbase transaction only + "show_extracted_by": false }, // block_page: a collection of settings that pertain to the block page @@ -736,7 +739,10 @@ // key_name: The name of the key or identifier in the raw block data that determines which hash algorithm was used to mine a particular block // NOTE: Changing this option will require a full reindex of the blockchain data before previously synced blocks can display the algorithm "key_name": "pow_algo" - } + }, + // show_extracted_by: Determine whether to show a new column with the address(es) that mined the block + // If enabled, a new column will be displayed on the block page that displays the address(es) that mined the block + "show_extracted_by": false }, // transaction_page: a collection of settings that pertain to the transaction/tx page @@ -762,7 +768,10 @@ "genesis_tx": "dd1d332ad2d8d8f49195056d482ae3c96fd2d16e9d166413b27ca7f19775644c", // show_op_return: Determine whether to decode and show OP_RETURN values that may have been embeddeded in a transaction // NOTE: Enabling this option will require a full reindex of the blockchain data before previously synced transactions can display the OP_RETURN value - "show_op_return": false + "show_op_return": false, + // show_extracted_by: Determine whether to show a new column with the address(es) that mined the block + // If enabled, a new column will be displayed on the transaction page that displays the address(es) that mined the block for the coinbase transaction only + "show_extracted_by": false }, // address_page: a collection of settings that pertain to the address page @@ -1350,7 +1359,10 @@ // If set to false, the /ext/getlasttxs api will be completely disabled for public use (no definition on the api page and a disabled error msg if you try to call the endpoint directly) but the api will still be available internally to the explorer "enabled": true, // max_items_per_query: The maximum # of transactions that can be returned from the /ext/getlasttxs api endpoint in a single call - "max_items_per_query": 100 + "max_items_per_query": 100, + // show_extracted_by: Determine whether to return a new field with the address(es) that mined the block + // If enabled, a new field will be added to the /ext/getlasttxs api endpoint that displays the address(es) that mined the block for the coinbase transaction only + "show_extracted_by": false }, // getcurrentprice: a collection of settings that pertain to the /ext/getcurrentprice api endpoint // Returns last known exchange price diff --git a/views/block.pug b/views/block.pug index a04ea6b..0eea675 100644 --- a/views/block.pug +++ b/views/block.pug @@ -76,6 +76,8 @@ block content th.text-center #{settings.localization.difficulty} if settings.block_page.multi_algorithm.show_algo == true th.text-center='Algorithm' + if settings.block_page.show_extracted_by == true + th.text-center='Extracted By' th.text-center #{settings.localization.confirmations} if settings.blockchain_specific.heavycoin.enabled == true th.text-center Vote @@ -94,6 +96,12 @@ block content span.decimal #{splitDifficulty[1]} if settings.block_page.multi_algorithm.show_algo == true td.text-center=block[settings.block_page.multi_algorithm.key_name] + if settings.block_page.show_extracted_by == true + td.text-center + each address in extracted_by_addresses + div(style='overflow: auto;') + a(href=`/address/${address.a_id}`)=(address.claimname == '' ? address.a_id : address.claimname) + include ./includes/rl_labels.pug if block.confirmations >= confirmations td.text-center.table-success=block.confirmations else if block.confirmations < (confirmations / 2) diff --git a/views/includes/rl_labels.pug b/views/includes/rl_labels.pug index 2146586..72188ff 100644 --- a/views/includes/rl_labels.pug +++ b/views/includes/rl_labels.pug @@ -1,15 +1,8 @@ if address.a_id == null - address.a_id = address.addresses; if settings.labels[address.a_id] != null && settings.labels[address.a_id].enabled == true - if settings.labels[address.a_id].type - label(class='badge bg-' + settings.labels[address.a_id].type + ' float-end d-none d-' + (active == 'richlist' ? 'md' : (active == 'tx' ? 'lg' : 'sm')) + '-block', style='margin-left:15px;margin-bottom:0;') - =settings.labels[address.a_id].label - if settings.labels[address.a_id].url - a(href=settings.labels[address.a_id].url, target='_blank', alt='Visit site', title='Visit site', data-bs-toggle='tooltip', data-bs-placement='top') - span.fa-solid.fa-circle-question(style='margin-left:5px;') - else - label.badge.bg-default.float-end.d-none(class='d-' + (active == 'richlist' ? 'md' : (active == 'tx' ? 'lg' : 'sm')) + '-block', style='margin-left:15px;margin-bottom:0;') - =settings.labels[address.a_id].label - if settings.labels[address.a_id].url - a(href=settings.labels[address.a_id].url, target='_blank', alt='Visit site', title='Visit site', data-bs-toggle='tooltip', data-bs-placement='top') - span.fa-solid.fa-circle-question(style='margin-left:5px;') \ No newline at end of file + label(class='badge bg-' + (settings.labels[address.a_id].type == null || settings.labels[address.a_id].type == '' ? 'default' : settings.labels[address.a_id].type) + ' float-end d-none d-' + (active == 'richlist' ? 'md' : (active == 'tx' ? 'lg' : 'sm')) + '-block', style='margin-left:15px;margin-bottom:0;') + =settings.labels[address.a_id].label + if settings.labels[address.a_id].url + a(href=settings.labels[address.a_id].url, target='_blank', alt='Visit site', title='Visit site', data-bs-toggle='tooltip', data-bs-placement='top') + span.fa-solid.fa-circle-question(style='margin-left:5px;') \ No newline at end of file diff --git a/views/index.pug b/views/index.pug index 11c3dab..86f92dc 100644 --- a/views/index.pug +++ b/views/index.pug @@ -61,26 +61,53 @@ block content }, rowCallback: function(row, data, index) { // variables for better readability - var blockindex = data[0]; - var blockhash = data[1]; - var txhash = data[2]; - var outputs = data[3]; - var amount = Number(data[4]).toLocaleString('en',{'minimumFractionDigits':2,'maximumFractionDigits':8,'useGrouping':true}); - var amountParts = amount.split('.'); - var amount = amountParts[0] + '.' + amountParts[1] + ''; - var timestamp = data[5]; - var offset = 0; + const blockindex = data[0]; + const blockhash = data[1]; + const txhash = data[2]; + const outputs = data[3]; + const amount = Number(data[4]).toLocaleString('en',{'minimumFractionDigits':2,'maximumFractionDigits':8,'useGrouping':true}); + const amountParts = amount.split('.'); + const updatedAmount = amountParts[0] + '.' + amountParts[1] + ''; + const timestamp = data[5]; + let offset = 0; $("td:eq(0)", row).html('').addClass('text-center d-table-cell d-md-none'); $("td:eq(1)", row).html('' + blockindex + ''); $("td:eq(2)", row).html('' + txhash + '').addClass("text-center breakWord d-none d-md-table-cell"); $("td:eq(3)", row).html(outputs).addClass("text-center d-none d-sm-table-cell"); - $("td:eq(4)", row).html(amount); + $("td:eq(4)", row).html(updatedAmount); if (#{settings.block_page.multi_algorithm.show_algo} == true) { - var algo = (data.length > 6 && data[6].indexOf('algo:') == 0 ? data[6].substring('algo:'.length) : ''); + const algo = (data.length > 6 && data[6].indexOf('algo:') == 0 ? data[6].substring('algo:'.length) : ''); $("td:eq(5)", row).html(algo); - offset = 1; + offset += 1; + } + + if (#{settings.index_page.show_extracted_by} == true) { + const extracted_by = JSON.parse(data.length > 6 && data[6 + offset].indexOf('extracted_by:') == 0 ? data[6 + offset].substring('extracted_by:'.length) : ''); + const labels = !{JSON.stringify(settings.labels)}; + const active = '#{active}'; + + let extracted_contents = ''; + + for (i = 0; i < extracted_by.length; i++) { + extracted_contents += '