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
This commit is contained in:
waclaw-claw 2026-03-28 04:29:58 -04:00
parent 2d95bd3baa
commit c49574b6cb
2 changed files with 107 additions and 8 deletions

View File

@ -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

View File

@ -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)'),