From 96ba7d0656ef70ca66d96d358d58b87e8b4585c0 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 25 Aug 2023 16:40:57 +0900 Subject: [PATCH] Add draft sip tx page --- unfurler/src/routes.ts | 68 +++++++++++++++++----- unfurler/views/head.ejs | 3 + unfurler/views/tx.ejs | 126 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 unfurler/views/tx.ejs 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) { %> + + + + + <% } else { %> + + + + + <% } %> + + + + + + + + + + + + + <% if (data.cpfp && data.cpfp.effectiveFeePerVsize && data.cpfp.effectiveFeePerVsize !== (data.transaction.fee / (data.transaction.weight / 4))) { %> + + + + + <% } %> +
Timestamp<%= (new Date(data.transaction.status.block_time * 1000)).toISOString() %>
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
Effective fee rate<%= data.cpfp.effectiveFeePerVsize.toFixed(2) %> sat/vB
+ +

Inputs & Outputs

+
+
+

Inputs

+ + <% data.transaction.vin.forEach((vin, i) => { %> + + <% if (vin.is_coinbase) { %> + + <% } else { %> + + <% } %> + + + <% }) %> +
Coinbase <%= data.hex2ascii(vin.scriptsig) %><%= 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

+ + + + + + + + + + + + + + + + + + + + + + <% if (data.cpfp && data.cpfp.adjustedVsize && data.cpfp.adjustedVsize > (data.transaction.weight / 4)) { %> + + + + + + + + + <% } %> +
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 %>
Sigops<%= data.cpfp.sigops %>
Adjusted vsize<%= data.cpfp.adjustedVsize %> vB
+
+ <%- include('footer'); %> + + \ No newline at end of file