From c49574b6cb04b8780065a0d036964b5b2b3f1c25 Mon Sep 17 00:00:00 2001 From: waclaw-claw Date: Sat, 28 Mar 2026 04:29:58 -0400 Subject: [PATCH] feat: add channel ID and inscription search - Enhanced search to match channel_id and inscription fields - Add Channel/Inscription column to transaction table - Display channel ID preview (blue) and inscription preview (green) - Updated search placeholder to mention channel/inscription search - Support hex and decoded text inscription matching Backend: - TransactionRepository.search() now extracts and searches channel_id - Searches inscription hex and decoded UTF-8 text - Matches from ChannelInscribe, ChannelBlob, ChannelSetKeys operations Frontend: - Added getChannelAndInscriptionPreview() helper - Added Channel/Inscription column with colored previews - Updated table columns and headers - Updated search placeholder text --- src/db/transaction.py | 46 +++++++++++++++-- static/components/TransactionsTable.js | 69 ++++++++++++++++++++++++-- 2 files changed, 107 insertions(+), 8 deletions(-) diff --git a/src/db/transaction.py b/src/db/transaction.py index 204a719..7e43328 100644 --- a/src/db/transaction.py +++ b/src/db/transaction.py @@ -151,13 +151,13 @@ class TransactionRepository: page_size: int = 50, ) -> tuple[List[Transaction], int]: """ - Search transactions by hash (partial match). + Search transactions by hash, channel_id, or inscription. Returns (transactions, total_count). """ offset = page * page_size chain = chain_block_ids_cte(fork=fork) - # Build search condition: match hash (case-insensitive, partial) + # Build search condition: match hash, channel_id, or inscription (case-insensitive, partial) search_term = query.lower() with self.client.session() as session: @@ -171,12 +171,48 @@ class TransactionRepository: ) all_transactions = session.exec(all_statement).all() - # Filter in Python for hash matching + # Filter in Python for hash, channel, and inscription matching filtered = [] for tx in all_transactions: - # Convert hash bytes to hex string and check if search term is in it + # Check hash hex_hash = tx.hash.hex().lower() if hasattr(tx.hash, 'hex') else bytes(tx.hash).hex().lower() - if search_term in hex_hash: + + # Check channel_id and inscription in operations + channel_matches = [] + inscription_matches = [] + + if hasattr(tx, 'operations') and tx.operations: + for op in tx.operations: + if hasattr(op, 'content') and op.content: + content = op.content + + # Check channel_id (from ChannelInscribe, ChannelBlob, ChannelSetKeys) + channel_id = content.get('channel_id') or content.get('channel') + if channel_id and isinstance(channel_id, str): + channel_id_lower = channel_id.lower() + if search_term in channel_id_lower: + channel_matches.append(channel_id) + + # Check inscription (from ChannelInscribe) + inscription = content.get('inscription') + if inscription and isinstance(inscription, str): + # Check hex inscription + if search_term in inscription.lower(): + inscription_matches.append(inscription) + # Also check decoded text + try: + if len(inscription) % 2 == 0: + bytes_data = bytes.fromhex(inscription) + decoded = bytes_data.decode('utf-8') + if search_term in decoded.lower(): + inscription_matches.append(decoded) + except (ValueError, UnicodeDecodeError): + pass + + # Add transaction if any search criteria match + if (search_term in hex_hash or + len(channel_matches) > 0 or + len(inscription_matches) > 0): filtered.append(tx) # Apply pagination diff --git a/static/components/TransactionsTable.js b/static/components/TransactionsTable.js index 0944dd9..2c34823 100644 --- a/static/components/TransactionsTable.js +++ b/static/components/TransactionsTable.js @@ -33,13 +33,41 @@ function tryDecodeUtf8Hex(hex) { bytes[i / 2] = b; } const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); - if (/[\x20-\x7e]/.test(text)) return text; + if (/[ \x20-\x7e]/.test(text)) return text; return null; } catch { return null; } } +// Extract channel and inscription preview from normalized transaction +function getChannelAndInscriptionPreview(tx) { + const channelIds = tx.channelIds || []; + const inscriptions = tx.inscriptions || []; + + let channelPreview = ''; + let inscriptionPreview = ''; + + // Get channel preview + if (channelIds.length > 0) { + const shortId = channelIds[0].slice(0, 8); + channelPreview = channelIds.length > 1 ? `${shortId}… (${channelIds.length})` : shortId; + } + + // Get inscription preview + if (inscriptions.length > 0) { + const hexInscription = inscriptions[0]; + const decoded = tryDecodeUtf8Hex(hexInscription); + if (decoded != null) { + inscriptionPreview = decoded.length > 20 ? decoded.slice(0, 20) + '…' : decoded; + } else { + inscriptionPreview = hexInscription.slice(0, 8); + } + } + + return { channelPreview, inscriptionPreview }; +} + function opPreview(op) { const content = op?.content ?? op; const type = content?.type ?? (typeof op === 'string' ? op : 'op'); @@ -89,6 +117,29 @@ function normalize(raw) { const totalOutputValue = outputs.reduce((sum, note) => sum + toNumber(note?.value), 0); const block = raw?.block ?? {}; + // Extract channel and inscription info from operations + let channelIds = []; + let inscriptions = []; + + if (ops && Array.isArray(ops)) { + for (const op of ops) { + if (op?.content) { + const content = op.content; + + // Extract channel_id from various channel operations + const channelId = content.channel_id || content.channel; + if (channelId) { + channelIds.push(channelId); + } + + // Extract inscription from ChannelInscribe + if (content.type === 'ChannelInscribe' && content.inscription) { + inscriptions.push(content.inscription); + } + } + } + } + return { id: raw?.id ?? '', hash: raw?.hash ?? '', @@ -99,6 +150,8 @@ function normalize(raw) { totalOutputValue, blockHeight: block?.height ?? 0, blockSlot: block?.slot ?? 0, + channelIds: channelIds, + inscriptions: inscriptions, }; } @@ -277,6 +330,9 @@ export default function TransactionsTable({ live, onDisableLive }) { const opsPreview = formatOperationsPreview(tx.operations); const fullPreview = Array.isArray(tx.operations) ? tx.operations.map(opPreview).join(', ') : ''; const outputsText = `${tx.numberOfOutputs} / ${tx.totalOutputValue.toLocaleString(undefined, { maximumFractionDigits: 8 })}`; + + // Get channel and inscription preview + const { channelPreview, inscriptionPreview } = getChannelAndInscriptionPreview(tx); return h( 'tr', @@ -301,6 +357,11 @@ export default function TransactionsTable({ live, onDisableLive }) { ), // Block Height h('td', { class: 'mono' }, String(tx.blockHeight)), + // Channel/Inscription (NEW) + h('td', { style: 'white-space:normal; line-height:1.4;' }, + channelPreview ? h('span', { class: 'mono', style: 'color: #4a90d9;' }, channelPreview) : '—', + inscriptionPreview ? h('span', { style: 'margin-left: 8px; color: #7bbd5a;' }, inscriptionPreview) : '' + ), // Operations h('td', { style: 'white-space:normal; line-height:1.4;' }, h('span', { title: fullPreview }, opsPreview)), // Block Slot @@ -354,7 +415,7 @@ export default function TransactionsTable({ live, onDisableLive }) { }, h('input', { type: 'text', - placeholder: 'Search by hash or block height...', + placeholder: 'Search by hash, channel ID, inscription, or block height...', value: searchQuery, onInput: (e) => setSearchQuery(e.target.value), style: @@ -379,8 +440,9 @@ export default function TransactionsTable({ live, onDisableLive }) { null, h('col', { style: 'width:180px' }), // Hash h('col', { style: 'width:100px' }), // Block Height + h('col', { style: 'width:250px' }), // Channel/Inscription (NEW) h('col', null), // Operations - h('col', { style: 'width:120px' }), // Timestamp + h('col', { style: 'width:120px' }), // Block Slot h('col', { style: 'width:180px' }), // Outputs (count / total) ), h( @@ -391,6 +453,7 @@ export default function TransactionsTable({ live, onDisableLive }) { null, h('th', null, 'Hash'), h('th', null, 'Block'), + h('th', null, 'Channel / Inscription'), h('th', null, 'Operations'), h('th', null, 'Slot'), h('th', null, 'Outputs (count / total)'),