mirror of
https://github.com/logos-blockchain/logos-blockchain-block-explorer-template.git
synced 2026-07-31 11:13:21 +00:00
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:
parent
2d95bd3baa
commit
c49574b6cb
@ -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
|
||||
|
||||
@ -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)'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user