Add an "Extracted By" field for block and tx data

-An optional "Extracted By" column can now be added to the homepage tx data, the block page, tx page and/or in the /ext/getlasttxs api
-Added 4 new settings to allow displaying the "Extracted By" data on the homepage, block page, transaction page and/or in the /ext/getlasttxs api
-Fixed an issue with the get_txs function where it wasn't properly searching by txid
-The rl_labels.pug file has been updated to consolidate similar code without being duplicated
-Updated the README with new verbiage for the extracted by data
This commit is contained in:
Joe Uhren
2025-03-02 16:37:52 -07:00
parent f736792c51
commit c239f129cf
9 changed files with 271 additions and 63 deletions
+2 -2
View File
@@ -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*
+120 -29
View File
@@ -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));
if (settings.block_page.multi_algorithm.show_algo == true)
data_entry.algo = (txs[i].algo == null ? '' : txs[i].algo);
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
};
data.push(data_entry);
}
}
if (settings.block_page.multi_algorithm.show_algo == true)
data_entry.algo = (txs[i].algo == null ? '' : txs[i].algo);
return cb(data, count);
if (show_extracted_by == true)
data_entry.extracted_by = extracted_by_addresses;
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
};
+16 -4
View File
@@ -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
+55
View File
@@ -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();
});
});
+16 -4
View File
@@ -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
+8
View File
@@ -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)
+5 -12
View File
@@ -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;')
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;')
+41 -12
View File
@@ -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] + '.<span class="decimal">' + amountParts[1] + '</span>';
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] + '.<span class="decimal">' + amountParts[1] + '</span>';
const timestamp = data[5];
let offset = 0;
$("td:eq(0)", row).html('<a href="/tx/' + txhash + '"><span class="fa-regular fa-eye" data-bs-toggle="tooltip" data-bs-placement="top" title="#{settings.localization.view_tx}"></span></a>').addClass('text-center d-table-cell d-md-none');
$("td:eq(1)", row).html('<a href="/block/' + blockhash + '">' + blockindex + '</a>');
$("td:eq(2)", row).html('<a href="/tx/' + txhash + '">' + txhash + '</a>').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 += '<div style="overflow: auto;">';
extracted_contents += `<a href='/address/${extracted_by[i].a_id}'>${(extracted_by[i].claimname == '' ? extracted_by[i].a_id : extracted_by[i].claimname)}</a>`;
if (labels[extracted_by[i].a_id] != null && labels[extracted_by[i].a_id].enabled == true) {
extracted_contents += `<label class="badge bg-${(labels[extracted_by[i].a_id].type == null || labels[extracted_by[i].a_id].type == '' ? 'default' : labels[extracted_by[i].a_id].type)} float-end d-none d-${active == 'richlist' ? 'md' : (active == 'tx' ? 'lg' : 'sm')}-block" style="margin-left:15px;margin-bottom:0;">${labels[extracted_by[i].a_id].label}`;
if (labels[extracted_by[i].a_id].url)
extracted_contents += `<a href="${labels[extracted_by[i].a_id].url}" target="_blank" alt="Visit site" title="Visit site" data-bs-toggle="tooltip" data-bs-placement="top"><span class="fa-solid fa-circle-question" style="margin-left:5px;" /></a>`;
extracted_contents += `</label>`;
}
extracted_contents += '</div>';
}
$("td:eq(" + (5 + offset) + ")", row).html(extracted_contents).addClass("text-center");
offset += 1;
}
$("td:eq(" + (5 + offset) + ")", row).html('<span' + (#{settings.shared_pages.date_time.enable_alt_timezone_tooltips} == true ? ' data-bs-toggle="tooltip" data-bs-placement="auto" title="' + format_unixtime(timestamp, true) + '"' : '') + '>' + format_unixtime(timestamp) + '</span>').addClass('text-center');
@@ -180,5 +207,7 @@ block content
span.small.fw-normal (#{settings.coin.symbol})
if settings.block_page.multi_algorithm.show_algo == true
th.text-center="Algorithm"
if settings.index_page.show_extracted_by == true
th.text-center="Extracted By"
th.text-center #{settings.localization.timestamp}
tbody.text-center
+8
View File
@@ -50,6 +50,8 @@ block content
tr(class=theadClasses)
th.text-center.d-table-cell.d-md-none
th.d-none.d-md-table-cell #{settings.localization.tx_block_hash}
if settings.transaction_page.show_extracted_by == true && extracted_by_addresses != null && extracted_by_addresses.length > 0
th.text-center='Extracted By'
if settings.transaction_page.show_op_return == true
th='OP_RETURN'
th.text-center #{settings.localization.confirmations}
@@ -62,6 +64,12 @@ block content
span.fa-regular.fa-eye(data-bs-toggle='tooltip', data-bs-placement='top', title=settings.localization.view_block)
td.d-none.d-md-table-cell
a.breakWord(href='/block/' + tx.blockhash) #{tx.blockhash}
if settings.transaction_page.show_extracted_by == true && extracted_by_addresses != null && extracted_by_addresses.length > 0
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 settings.transaction_page.show_op_return == true
td.breakWord #{tx.op_return}
if confirms >= confirmations