diff --git a/app/api/electrumAddressApi.js b/app/api/electrumAddressApi.js index fd9c9818f..cb3042e22 100644 --- a/app/api/electrumAddressApi.js +++ b/app/api/electrumAddressApi.js @@ -353,6 +353,34 @@ function lookupTxBlockHash(txid) { }); } +// Lookup the spending transaction and height of a given transaction output. Only works with Electrum 1.5 protocol, ElectRS 0.9.0 does not implement subscriptions +// but can return the transaction spending the given outpoint + +function lookupOutpointTx(txid, vout) { + if (electrumClients.length == 0) { + return Promise.reject({ error: "Not supported by Electrum 1.4", userText: noConnectionsErrorText }); + } + + return runOnAllServers(function(electrumClient) { + return electrumClient.request('blockchain.outpoint.subscribe', [txid, vout]); + }).then(function(results) { + var spend_infos = results[0].result; + runOnAllServers(function(electrumClient) { + return electrumClient.request('blockchain.outpoint.unsubscribe', [txid, vout]) //will fail until subscriptions for outpoint are implemented in ElectRS + }); + if (results.slice(1).every(({ result }) => result == spend_infos)) { + if (spend_infos.length < 2) { + return false; + } else { + return spend_infos; + } + } else { + return Promise.reject({conflictedResults:results}); + } + }); +} + + function logStats(cmd, dt, success) { if (!global.electrumStats.rpc[cmd]) { global.electrumStats.rpc[cmd] = {count:0, time:0, successes:0, failures:0}; @@ -380,5 +408,6 @@ module.exports = { connectToServers: connectToServers, getAddressDetails: getAddressDetails, lookupTxBlockHash: lookupTxBlockHash, + lookupOutpointTx: lookupOutpointTx, }; diff --git a/routes/baseRouter.js b/routes/baseRouter.js index a64038e1e..9be7fbaae 100644 --- a/routes/baseRouter.js +++ b/routes/baseRouter.js @@ -26,6 +26,7 @@ const coins = require("./../app/coins.js"); const config = require("./../app/config.js"); const coreApi = require("./../app/api/coreApi.js"); const addressApi = require("./../app/api/addressApi.js"); +const electrumAddressApi = require("./../app/api/electrumAddressApi.js"); const rpcApi = require("./../app/api/rpcApi.js"); const btcQuotes = require("./../app/coins/btcQuotes.js"); @@ -1423,6 +1424,25 @@ router.get("/tx/:transactionId", asyncHandler(async (req, res, next) => { await Promise.all(promises); + // Electrs 0.9.0 support spending transaction lookup for an outpoint + if ((config.addressApi == "electrum" || config.addressApi == "electrumx") && config.electrumTxIndex) { + let spending_promises = []; + for (const vout in tx.vout) { + spending_promises.push(new Promise(async (resolve, reject) => { + if (res.locals.utxos[vout] == null) { + const spent = await electrumAddressApi.lookupOutpointTx(txid, parseInt(vout)); + resolve(spent); + } else { + resolve(false); + } + })); + } + + await Promise.all(spending_promises).then((outpoint_results) => { + res.locals.spendings = outpoint_results; + }); + } + if (global.specialTransactions && global.specialTransactions[txid]) { let funInfo = global.specialTransactions[txid]; diff --git a/views/includes/transaction-io-details.pug b/views/includes/transaction-io-details.pug index 07552cd56..3e3174d43 100644 --- a/views/includes/transaction-io-details.pug +++ b/views/includes/transaction-io-details.pug @@ -46,7 +46,7 @@ mixin outputValueDisplay(vout, voutIndex) if (utxos[voutIndex]) span(title="This output remains unspent (a valid UTXO)." data-bs-toggle="tooltip") i.far.fa-check-circle.fa-sm.text-success.ms-2 - + else if (utxos[voutIndex] == null) span(title="This output has been spent (destroyed)." data-bs-toggle="tooltip") i.far.fa-times-circle.fa-sm.text-danger.ms-2 @@ -290,10 +290,22 @@ mixin outputValueDisplay(vout, voutIndex) +darkBadge span(title=`Output Type: ${utils.outputTypeName(vout.scriptPubKey.type)}`, data-bs-toggle="tooltip") #{utils.outputTypeAbbreviation(vout.scriptPubKey.type)} + if (spendings) + - var spending_txid = spendings[voutIndex].spender_txhash; + if (spending_txid) + span.mt-1 spent by + a(href=`./tx/${spending_txid}`) #{utils.ellipsizeMiddle(spendings[voutIndex].spender_txhash,9)} + if (spendings[voutIndex].spender_height > 0 && spendings[voutIndex].spender_height > spendings[voutIndex].height) + span #{spendings[voutIndex].spender_height - spendings[voutIndex].height} blocks later + else if (spendings[voutIndex].spender_height <= 0) + span in the mempool + else if (spendings[voutIndex].spender_height == spendings[voutIndex].height) + span in the same block span.ms-2.float-end +outputValueDisplay(vout, voutIndex) + if (vout && vout.scriptPubKey.asm && !vout.scriptPubKey.asm.startsWith("OP_RETURN ")) if (voutAddresses.length == 0) .my-2.d-none.d-sm-block