diff --git a/unfurler/src/routes.ts b/unfurler/src/routes.ts
index 54dc5c555..80dd54f24 100644
--- a/unfurler/src/routes.ts
+++ b/unfurler/src/routes.ts
@@ -102,6 +102,61 @@ const routes = {
}
}
},
+ tx: {
+ render: true,
+ params: 1,
+ getTitle(path) {
+ return `Transaction: ${path[0]}`;
+ },
+ sip: {
+ template: 'tx',
+ async getData (params: string[]) {
+ if (params?.length) {
+ let txid = params[0];
+ const [transaction, times, cpfp, rbf, outspends]: any[] = await Promise.all([
+ sipFetchJSON(config.API.ESPLORA + `/tx/${txid}`),
+ sipFetchJSON(config.API.MEMPOOL + `/transaction-times?txId[]=${txid}`),
+ sipFetchJSON(config.API.MEMPOOL + `/cpfp/${txid}`),
+ sipFetchJSON(config.API.MEMPOOL + `/tx/${txid}/rbf`),
+ sipFetchJSON(config.API.MEMPOOL + `/outspends?txId[]=${txid}`),
+ ])
+ const features = transaction ? {
+ segwit: transaction.vin.some((v) => v.prevout && ['v0_p2wsh', 'v0_p2wpkh'].includes(v.prevout.scriptpubkey_type)),
+ taproot: transaction.vin.some((v) => v.prevout && v.prevout.scriptpubkey_type === 'v1_p2tr'),
+ rbf: transaction.vin.some((v) => v.sequence < 0xfffffffe),
+ } : {};
+ return {
+ transaction,
+ times,
+ cpfp,
+ rbf,
+ outspends,
+ features,
+ hex2ascii: function(hex) {
+ const opPush = hex.split(' ').filter((_, i, a) => i > 0 && /^OP_PUSH/.test(a[i - 1]));
+ if (opPush[0]) {
+ hex = opPush[0];
+ }
+ if (!hex) {
+ return '';
+ }
+ const bytes: number[] = [];
+ for (let i = 0; i < hex.length; i += 2) {
+ bytes.push(parseInt(hex.substr(i, 2), 16));
+ }
+ return new TextDecoder('utf8').decode(Uint8Array.from(bytes)).replace(/\uFFFD/g, '').replace(/\\0/g, '');
+ },
+ }
+ }
+ }
+ },
+ routes: {
+ push: {
+ title: "Push Transaction",
+ fallbackImg: '/resources/previews/tx-push.jpg',
+ }
+ }
+ },
enterprise: {
title: "Mempool Enterprise",
fallbackImg: '/resources/previews/enterprise.jpg',
@@ -177,19 +232,6 @@ const routes = {
title: "Trademark Policy",
fallbackImg: '/resources/previews/trademark-policy.jpg',
},
- tx: {
- render: true,
- params: 1,
- getTitle(path) {
- return `Transaction: ${path[0]}`;
- },
- routes: {
- push: {
- title: "Push Transaction",
- fallbackImg: '/resources/previews/tx-push.jpg',
- }
- }
- }
};
const networks = {
diff --git a/unfurler/views/head.ejs b/unfurler/views/head.ejs
index bc36e54f1..3581b622d 100644
--- a/unfurler/views/head.ejs
+++ b/unfurler/views/head.ejs
@@ -23,5 +23,8 @@
\ No newline at end of file
diff --git a/unfurler/views/tx.ejs b/unfurler/views/tx.ejs
new file mode 100644
index 000000000..399ca94db
--- /dev/null
+++ b/unfurler/views/tx.ejs
@@ -0,0 +1,126 @@
+
+
+ <%- include('head'); %>
+
+ <%- include('header'); %>
+
+
Transaction <%- data.transaction.txid %>
+ <% if (data.transaction.status.confirmed) { %>
+
confirmed in block <%= data.transaction.status.block_height %>
+ <% } %>
+
Summary
+
+ <% if (data.transaction.status.confirmed) { %>
+
+ Timestamp |
+ <%= (new Date(data.transaction.status.block_time * 1000)).toISOString() %> |
+
+ <% } else { %>
+
+ First seen |
+ <%= (new Date(data.times[0] * 1000)).toISOString() %> |
+
+ <% } %>
+
+ Features |
+
+
+ - SegWit
- "><%= data.features.segwit ? "yes" : "no" %>
+ - Taproot
- "><%= data.features.taproot ? "yes" : "no" %>
+ - RBF
- "><%= data.features.rbf ? "yes" : "no" %>
+
+ |
+
+
+ Fee |
+ <%= data.transaction.fee %> sat |
+
+
+ Fee rate |
+ <%= (data.transaction.fee / (data.transaction.weight / 4)).toFixed(2) %> sat/vB |
+
+ <% if (data.cpfp && data.cpfp.effectiveFeePerVsize && data.cpfp.effectiveFeePerVsize !== (data.transaction.fee / (data.transaction.weight / 4))) { %>
+
+ Effective fee rate |
+ <%= data.cpfp.effectiveFeePerVsize.toFixed(2) %> sat/vB |
+
+ <% } %>
+
+
+
Inputs & Outputs
+
+
+
Inputs
+
+ <% data.transaction.vin.forEach((vin, i) => { %>
+
+ <% if (vin.is_coinbase) { %>
+ Coinbase <%= data.hex2ascii(vin.scriptsig) %> |
+ <% } else { %>
+ <%= vin.prevout.scriptpubkey_address %> |
+ <% } %>
+ <%= ((vin.prevout ? vin.prevout.value : 0) / 100_000_000).toFixed(8) %> BTC |
+
+ <% }) %>
+
+
+
+
Outputs
+
+ <% data.transaction.vout.forEach((vout, i) => { %>
+
+
+ <% if (vout.scriptpubkey_type === 'op_return') { %>
+ OP_RETURN <%= data.hex2ascii(vout.scriptpubkey_asm) %>
+ <% } else if (vout.scriptpubkey_type === 'p2pk') { %>
+ P2PK <%= vout.scriptpubkey.slice(2, -2) %>
+ <% } else if (!['p2pkh', 'p2sh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(vout.scriptpubkey_type)) { %>
+ <%= vout.scriptpubkey_type.toUpperCase() %>
+ <% } else { %>
+ <%= vout.scriptpubkey_address %>
+ <% } %>
+ |
+ <%= (vout.value / 100_000_000).toFixed(8) %> BTC |
+
+ <% }) %>
+
+
+
+
+
Details
+
+
+ Size |
+ <%= data.transaction.size %> B |
+
+
+ Virtual size |
+ <%= data.transaction.weight / 4 %> vB |
+
+
+ Weight |
+ <%= data.transaction.weight %> WU |
+
+
+ Version |
+ <%= data.transaction.version %> |
+
+
+ Locktime |
+ <%= data.transaction.locktime %> |
+
+ <% if (data.cpfp && data.cpfp.adjustedVsize && data.cpfp.adjustedVsize > (data.transaction.weight / 4)) { %>
+
+ Sigops |
+ <%= data.cpfp.sigops %> |
+
+
+ Adjusted vsize |
+ <%= data.cpfp.adjustedVsize %> vB |
+
+ <% } %>
+
+
+ <%- include('footer'); %>
+
+
\ No newline at end of file