From 066ce8d84d676c763dc35b13533a199062d8de31 Mon Sep 17 00:00:00 2001 From: Philippe McLean Date: Thu, 30 Oct 2025 15:33:19 -0700 Subject: [PATCH] check for and display simplicity transaction inputs - simplicity program displayed in base64 (not hex) --- client/src/lib/elements.js | 79 ++++++++++++++++++++++++++++++++++ client/src/views/asset-list.js | 2 +- client/src/views/tx-vin.js | 32 +++++++++++++- client/src/views/util.js | 30 +++---------- 4 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 client/src/lib/elements.js diff --git a/client/src/lib/elements.js b/client/src/lib/elements.js new file mode 100644 index 00000000..5a8585d5 --- /dev/null +++ b/client/src/lib/elements.js @@ -0,0 +1,79 @@ +import {formatAssetAmount} from "../views/util"; + +export const getSupply = (asset, t) => { + let { chain_stats = {}, mempool_stats = {} } = asset + let has_blinded_issuances = + chain_stats.has_blinded_issuances || mempool_stats.has_blinded_issuances + let is_native_asset = !asset.issuance_txin + let circulating = is_native_asset + ? chain_stats.peg_in_amount + + mempool_stats.peg_in_amount - + chain_stats.peg_out_amount - + mempool_stats.peg_out_amount - + chain_stats.burned_amount - + mempool_stats.burned_amount + : has_blinded_issuances + ? null + : chain_stats.issued_amount + + mempool_stats.issued_amount - + chain_stats.burned_amount - + mempool_stats.burned_amount; + + let totalSupply = circulating == null ? t`Confidential` + : formatAssetAmount(circulating, asset.precision, t) + return totalSupply +} + + +// Simplicity helpers (Elements only) + +// Helper to check if witness has an annex (per BIP 341) +const hasAnnex = (witness) => witness && witness.length >= 2 && witness[witness.length - 1].startsWith('50') + +// Get the control block element from a witness stack (accounting for optional annex) +const getControlBlock = witness => { + if (!witness || witness.length < 3) return null + const hasAnnexBlock = hasAnnex(witness) + if (hasAnnexBlock) { + return witness[witness.length - 2] + } + return witness[witness.length - 1] +} + +export const isSimplicityTapleafVersion = (controlBlock) => { + return controlBlock && (controlBlock.startsWith('be') || controlBlock.startsWith('bf')) +} + +// Check if a vin is a P2TR Simplicity spend +// A P2TR Simplicity spend has: +// - 4 witness elements (or 5 with optional annex) +// - is a simplicity Tapleaf +export const isSimplicitySpend = (vin) => { + if (!process.env.IS_ELEMENTS || !vin.witness) return false + + const witnessLen = vin.witness.length + const hasAnnexBlock = hasAnnex(vin.witness) + + // Must be 4 elements without annex, or 5 elements with annex + if (witnessLen !== 4 && !(witnessLen === 5 && hasAnnexBlock)) return false + + const controlBlock = getControlBlock(vin.witness) + return isSimplicityTapleafVersion(controlBlock) +} + +// Extract Simplicity witness components from a vin +// Returns: { witnessData, program, cmr, controlBlock, annex } +export const getSimplicityWitness = (vin) => { + if (!isSimplicitySpend(vin)) return null + + const witness = vin.witness + const hasAnnexBlock = hasAnnex(witness) + + return { + witnessData: witness[0], + program: witness[1], + cmr: witness[2], + controlBlock: witness[3], + annex: hasAnnexBlock && witness.length === 5 ? witness[4] : null + } +} diff --git a/client/src/views/asset-list.js b/client/src/views/asset-list.js index c8124bda..0921af12 100644 --- a/client/src/views/asset-list.js +++ b/client/src/views/asset-list.js @@ -1,5 +1,5 @@ import Snabbdom from 'snabbdom-pragma' -import { getSupply } from './util' +import { getSupply } from '../lib/elements.js' import layout from './layout' import loader from '../components/loading' diff --git a/client/src/views/tx-vin.js b/client/src/views/tx-vin.js index 7b2d5060..c1e4aa32 100644 --- a/client/src/views/tx-vin.js +++ b/client/src/views/tx-vin.js @@ -1,5 +1,6 @@ import Snabbdom from 'snabbdom-pragma' -import { linkToParentOut, formatOutAmount, formatAssetAmount, formatHex, linkToAddr, formatNumber } from './util' +import { linkToParentOut, formatOutAmount, formatAssetAmount, formatHex, linkToAddr, formatNumber, hexToBase64 } from './util' +import { isSimplicitySpend, getSimplicityWitness } from '../lib/elements.js' const layout = (vin, desc, body, { t, ...S }) =>
@@ -103,11 +104,38 @@ const standard = (vin, { isOpen, t, ...S }, assetMeta=getAssetMeta(vin, S)) => l
] } - { vin.witness &&
+ { vin.witness && !isSimplicitySpend(vin) &&
{t`Witness`}
{vin.witness.map(wit => [ ' ', wit.length ? wit : <empty> ])}
} + { isSimplicitySpend(vin) && ((sw = getSimplicityWitness(vin)) => [ +
+
{t`Simplicity witness data`}
+
{sw.witnessData}
+
+ + ,
+
{t`Simplicity program`}

{t`(base64)`}
+
{hexToBase64(sw.program)}
+
+ + ,
+
{t`Simplicity CMR`}
+
{sw.cmr}
+
+ + ,
+
{t`Taproot control block`}
+
{sw.controlBlock}
+
+ + , sw.annex &&
+
{t`Taproot Annex`}
+
{sw.annex}
+
+ ])() } + { vin.inner_redeemscript_asm &&
{t`P2SH redeem script`}
{vin.inner_redeemscript_asm}
diff --git a/client/src/views/util.js b/client/src/views/util.js index f2176cd6..e8c84542 100644 --- a/client/src/views/util.js +++ b/client/src/views/util.js @@ -97,28 +97,10 @@ export const formatVMB = bytes => : '< 0.01 vMB' -export const getSupply = (asset, t) => { - let { chain_stats = {}, mempool_stats = {} } = asset - let has_blinded_issuances = - chain_stats.has_blinded_issuances || mempool_stats.has_blinded_issuances - let is_native_asset = !asset.issuance_txin - let circulating = is_native_asset - ? chain_stats.peg_in_amount + - mempool_stats.peg_in_amount - - chain_stats.peg_out_amount - - mempool_stats.peg_out_amount - - chain_stats.burned_amount - - mempool_stats.burned_amount - : has_blinded_issuances - ? null - : chain_stats.issued_amount + - mempool_stats.issued_amount - - chain_stats.burned_amount - - mempool_stats.burned_amount; - - let totalSupply = circulating == null ? t`Confidential` - : formatAssetAmount(circulating, asset.precision, t) - return totalSupply -} +export const strTruncate = (str) => str.substr(0, 10) + '...' + str.substr(str.length-4, str.length); + -export const strTruncate = (str) => str.substr(0, 10) + '...' + str.substr(str.length-4, str.length); \ No newline at end of file +// Convert hex string to base64 +export const hexToBase64 = (hex) => { + return Buffer.from(hex, 'hex').toString('base64') +}