Skip to content

Commit ee23c9a

Browse files
committed
viewers: complete coverage pass + emoji conventions
Three substantive fixes plus a one-line label fix: (1) atomical-viewer was a stale placeholder ("we do not yet have a parser for Atomicals") even though ordpool-parser has had full Atomicals support including args, files, payload extraction for months. Now renders: operation label via ATOMICAL_OPERATION_LABELS (splat/split/ custom-color for the single-letter ops), inline image preview for file attachments that have an image/* contentType, args key/value table and files table under showDetails, and CBOR payload size. (2) inscription-viewer was missing three ord tags the parser already exposes: - tag 15 (note): the chisel.xyz-style inscriber-tool watermark. ord stores it but doesn't display; explorers should surface it. - tag 13 (rune commitment): little-endian u128 bytes that pin an inscription as a commitment for a rune etching. Rendered as hex with a tooltip; we don't map bytes back to a rune name because that requires walking forward to the etching tx. - tag 17 (properties): galleries + inscription-level title + traits. Properties is async (tag 19 compression may apply), so resolved via | async in the template. Gallery items link to the referenced inscription with the standard truncate helper. (3) src20-viewer header said "Stamps". The viewer is for SRC-20 specifically; STAMP / SRC-721 / SRC-101 are sibling protocols with their own viewers. Fixed to "SRC-20". Emoji convention enforced: ◉ is reserved for ordinals (inscriptions), ▣ for runes. Stripped misuses from atomical, stamp, src721, src101, labitbu, counterparty, and ots viewers. Cat-21 keeps its cat emoji. The ATOMICAL_OPERATION_LABELS dictionary now drives the atomical header text, no emoji.
1 parent 33db18e commit ee23c9a

12 files changed

Lines changed: 217 additions & 22 deletions

File tree

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,58 @@
1-
<strong>⚛ Atomical</strong><br>
2-
@if (showDetails) { <br> }
1+
@if (_atomical) {
2+
<strong>Atomical:</strong>
3+
<span title="Operation code from the witness envelope">{{ operationLabel }}</span>
4+
@if (operationLabel !== operation) {
5+
<span class="smaller-text text-muted">(opcode <code>{{ operation }}</code>)</span>
6+
}
7+
<br>
38

4-
This transaction reveals an Atomical!<br>
5-
Unfortunately, we do not yet have a parser for Atomicals.<br>
6-
If you can contribute, please send a <a href="https://github.com/ordpool-space/ordpool-parser" target="_blank">🧡 pull request (Github) to our repository</a>.
7-
9+
@for (preview of filePreviews; track preview.file.name) {
10+
@if (preview.isImage && preview.dataUri) {
11+
<img [src]="preview.dataUri" [alt]="preview.file.name" class="atomical-preview mt-2">
12+
}
13+
}
14+
15+
@if (showDetails) {
16+
@if (argEntries.length) {
17+
<div class="mt-3">
18+
<strong>Args</strong>
19+
<table class="table table-borderless table-striped mt-2">
20+
<tbody>
21+
@for (entry of argEntries; track entry.key) {
22+
<tr>
23+
<td><code>{{ entry.key }}</code></td>
24+
<td class="atomical-arg-value">{{ entry.value }}</td>
25+
</tr>
26+
}
27+
</tbody>
28+
</table>
29+
</div>
30+
}
31+
32+
@if (files.length) {
33+
<div class="mt-3">
34+
<strong>Files</strong>
35+
<table class="table table-borderless table-striped mt-2">
36+
<tbody>
37+
@for (file of files; track file.name) {
38+
<tr>
39+
<td><code>{{ file.name }}</code></td>
40+
<td>{{ file.contentType }}</td>
41+
<td class="text-end">{{ file.data.length | number }} bytes</td>
42+
</tr>
43+
}
44+
</tbody>
45+
</table>
46+
</div>
47+
}
48+
49+
<table class="mt-3 table table-borderless table-striped w-50">
50+
<tbody>
51+
<tr>
52+
<td>CBOR payload size</td>
53+
<td>{{ payloadSize | number }} bytes</td>
54+
</tr>
55+
</tbody>
56+
</table>
57+
}
58+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.atomical-preview {
2+
max-width: 320px;
3+
max-height: 320px;
4+
image-rendering: pixelated;
5+
background: #11132a;
6+
}
7+
8+
.atomical-arg-value {
9+
word-break: break-all;
10+
font-family: monospace;
11+
font-size: 0.85rem;
12+
}
Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,79 @@
1+
import { DecimalPipe } from '@angular/common';
12
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2-
import { ParsedAtomical } from 'ordpool-parser';
3+
import { ATOMICAL_OPERATION_LABELS, AtomicalFile, AtomicalOperation, ParsedAtomical } from 'ordpool-parser';
34

45
/**
56
* Test cases:
6-
* https://ordpool.space/tx/1d2f39f54320631d0432fa495a45a4f298a2ca1b18adef8e4356e327d003a694 (etching Z•Z•Z•Z•Z•FEHU•Z•Z•Z•Z•Z)
7+
* https://ordpool.space/tx/1d2f39f54320631d0432fa495a45a4f298a2ca1b18adef8e4356e327d003a694 (dft "atom")
8+
* https://ordpool.space/tx/d8c96e3920f15dfbca4bcb3a3b2fce214484cb913fdca3055dd0f7069387edd3 (nft realm "terafab")
9+
* https://ordpool.space/tx/7c8527547cc99b39f9d02fa2e8d963d78a3d60692a05ad378a87b96abed4aab6 (nft toothy #7579, with embedded PNG)
10+
* https://ordpool.space/tx/5390e86df98982122175e18a7f24a1618d14e50e0b2242c7ca2c27730ffad700 (dmt mint of "atom")
11+
* https://ordpool.space/tx/329a9fae404e4ca014b975dbcc7cb5267f47cccd2851a45ffa06c70744ae12cd (splat / x)
12+
* https://ordpool.space/tx/054cc18a8162887917a1e6e5c60389bb4b6647167e6936d231466d7b2710f413 (split / y)
13+
* https://ordpool.space/tx/914a3f3575a1da92035a57bd758da8588fd11776927ab880915f97e66612f773 (custom-color / z)
714
*/
815
@Component({
916
selector: 'app-atomical-viewer',
1017
templateUrl: './atomical-viewer.component.html',
1118
styleUrls: ['./atomical-viewer.component.scss'],
1219
changeDetection: ChangeDetectionStrategy.OnPush,
13-
standalone: true
20+
standalone: true,
21+
imports: [DecimalPipe],
1422
})
1523
export class AtomicalViewerComponent {
1624

17-
private _atomical: ParsedAtomical | undefined;
25+
_atomical: ParsedAtomical | undefined;
26+
operation: AtomicalOperation | undefined;
27+
operationLabel = '';
28+
argEntries: { key: string; value: string }[] = [];
29+
files: AtomicalFile[] = [];
30+
filePreviews: { file: AtomicalFile; isImage: boolean; dataUri: string | undefined }[] = [];
31+
payloadSize = 0;
1832

1933
@Input() showDetails = false;
2034

2135
@Input()
22-
public set parsedAtomical(parsedAtomical: ParsedAtomical | undefined) {
23-
24-
// early exit if setter is called multiple times (don't remove!)
36+
set parsedAtomical(parsedAtomical: ParsedAtomical | undefined) {
2537
if (this._atomical?.uniqueId === parsedAtomical?.uniqueId) {
2638
return;
2739
}
28-
2940
this._atomical = parsedAtomical;
41+
if (!parsedAtomical) {
42+
this.operation = undefined;
43+
this.operationLabel = '';
44+
this.argEntries = [];
45+
this.files = [];
46+
this.filePreviews = [];
47+
this.payloadSize = 0;
48+
return;
49+
}
50+
this.operation = parsedAtomical.operation;
51+
this.operationLabel = ATOMICAL_OPERATION_LABELS[parsedAtomical.operation] ?? parsedAtomical.operation;
52+
this.payloadSize = parsedAtomical.getPayloadRaw().length;
53+
54+
const args = parsedAtomical.getArgs();
55+
this.argEntries = args
56+
? Object.entries(args).map(([key, value]) => ({ key, value: formatArgValue(value) }))
57+
: [];
58+
59+
this.files = parsedAtomical.getFiles();
60+
this.filePreviews = this.files.map(file => {
61+
const isImage = file.contentType.startsWith('image/');
62+
return { file, isImage, dataUri: isImage ? file.getDataUri() : undefined };
63+
});
64+
}
65+
}
66+
67+
function formatArgValue(value: unknown): string {
68+
if (value === null || value === undefined) return '';
69+
if (typeof value === 'string') return value;
70+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
71+
// Uint8Array / objects: stringify as JSON. Show byte arrays as hex preview.
72+
if (value instanceof Uint8Array) {
73+
const preview = Array.from(value.slice(0, 32))
74+
.map(b => b.toString(16).padStart(2, '0'))
75+
.join('');
76+
return value.length <= 32 ? preview : `${preview}... (${value.length} bytes)`;
3077
}
78+
try { return JSON.stringify(value); } catch { return String(value); }
3179
}

frontend/src/app/components/_ordpool/digital-artifact-viewer/counterparty-viewer/counterparty-viewer.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@if (_parsed) {
2-
<strong>Counterparty:</strong>
2+
<strong>Counterparty:</strong>
33
<span title="Counterparty message type">{{ _parsed.messageType }}</span>
44
<span class="smaller-text text-muted">(id&nbsp;{{ _parsed.messageTypeId }})</span>
55
<br>

frontend/src/app/components/_ordpool/digital-artifact-viewer/inscription-viewer/inscription-viewer.component.html

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,55 @@
9999
Content Encoding: {{ contentEncoding }}
100100
</div>
101101

102+
<div class="mt-2" *ngIf="showDetails && note">
103+
Note (tag 15):
104+
<code class="smaller-text">{{ note }}</code>
105+
<fa-icon class="ml-1 smaller-text" [icon]="['fas', 'info-circle']" [fixedWidth]="true"
106+
ngbTooltip="Tag::Note in ord (PR #3256). Stored but not displayed by the reference indexer. chisel.xyz uses it for an inscriber-tool watermark URL."
107+
></fa-icon>
108+
</div>
109+
110+
<div class="mt-2" *ngIf="showDetails && runeCommitmentHex">
111+
Rune commitment (tag 13):
112+
<code class="smaller-text">{{ runeCommitmentHex }}</code>
113+
<fa-icon class="ml-1 smaller-text" [icon]="['fas', 'info-circle']" [fixedWidth]="true"
114+
ngbTooltip="Little-endian bytes of the rune's u128 value with trailing zeros stripped. Used by the rune protocol to commit to an etching from inside an inscription; the etching tx must spend this inscription's UTXO."
115+
></fa-icon>
116+
</div>
117+
118+
<ng-container *ngIf="showDetails && properties$ && (properties$ | async) as properties">
119+
<div class="mt-3" *ngIf="properties.title || properties.traits">
120+
<strong>Properties (tag 17)</strong>
121+
<table class="mt-2 table table-borderless table-striped">
122+
<tbody>
123+
<tr *ngIf="properties.title">
124+
<td>Title</td>
125+
<td>{{ properties.title }}</td>
126+
</tr>
127+
<tr *ngFor="let trait of properties.traits | keyvalue">
128+
<td><code>{{ trait.key }}</code></td>
129+
<td>{{ trait.value }}</td>
130+
</tr>
131+
</tbody>
132+
</table>
133+
</div>
134+
135+
<div class="mt-3" *ngIf="properties.gallery && properties.gallery.length">
136+
<strong>Gallery ({{ properties.gallery.length }})</strong>
137+
<fa-icon class="ml-1 smaller-text" [icon]="['fas', 'info-circle']" [fixedWidth]="true"
138+
ngbTooltip="A curated list of inscription IDs referenced from this inscription's tag 17. Galleries are permissionless -- inclusion does NOT imply provenance."
139+
></fa-icon>
140+
<ol class="smaller-text mt-2">
141+
<li *ngFor="let item of properties.gallery" class="mb-1">
142+
<a class="address" [routerLink]="['/tx/' | relativeUrl, item.inscriptionId.split('i')[0]]" title="{{ item.inscriptionId }}">
143+
<app-truncate [text]="item.inscriptionId" [lastChars]="10"></app-truncate>
144+
</a>
145+
<span *ngIf="item.title" class="ml-2">{{ item.title }}</span>
146+
</li>
147+
</ol>
148+
</div>
149+
</ng-container>
150+
102151
<table class="mt-2 table table-borderless table-striped w-50" *ngIf="showDetails">
103152
<tbody>
104153
<tr>

frontend/src/app/components/_ordpool/digital-artifact-viewer/inscription-viewer/inscription-viewer.component.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input } from '@angular/core';
2-
import { DecodeFailureReason, InscriptionParserService, InscriptionPreviewService, ParsedInscription } from 'ordpool-parser';
2+
import { DecodeFailureReason, InscriptionParserService, InscriptionPreviewService, InscriptionProperties, ParsedInscription } from 'ordpool-parser';
33
import { map, Observable } from 'rxjs';
44

55
import { ElectrsApiService } from '../../../../services/electrs-api.service';
@@ -44,6 +44,23 @@ export class InscriptionViewerComponent {
4444
reason?: DecodeFailureReason,
4545
}> | undefined;
4646

47+
// Tag 15 (note): chisel.xyz-style inscriber-tool watermark string. ord stores
48+
// but doesn't display it; we surface it so explorers can flag inscriber
49+
// attribution.
50+
note: string | undefined;
51+
52+
// Tag 13 (rune commitment): little-endian bytes of a rune's u128 value.
53+
// Displayed as hex; explained in a tooltip rather than turned into the
54+
// human-readable rune name, because mapping bytes -> name requires the
55+
// etching tx (which our parser can't cheaply find from the inscription
56+
// side alone). Hex is the honest minimum.
57+
runeCommitmentHex: string | undefined;
58+
59+
// Tag 17 (properties): galleries + inscription-level title + traits.
60+
// Resolved async; properties may be compressed (tag 19) so the parser's
61+
// getProperties is also async.
62+
properties$: Promise<InscriptionProperties | undefined> | undefined;
63+
4764
@Input() showDetails = false;
4865

4966
@Input()
@@ -58,9 +75,19 @@ export class InscriptionViewerComponent {
5875

5976
if (!inscription) {
6077
this.contentTypeInstructions$ = undefined;
78+
this.note = undefined;
79+
this.runeCommitmentHex = undefined;
80+
this.properties$ = undefined;
6181
return;
6282
}
6383

84+
this.note = inscription.getNote();
85+
86+
const rune = inscription.getRune();
87+
this.runeCommitmentHex = rune ? bytesToHex(rune) : undefined;
88+
89+
this.properties$ = inscription.getProperties();
90+
6491
this.delegates = inscription.getDelegates();
6592
if (this.delegates.length) {
6693

@@ -81,3 +108,11 @@ export class InscriptionViewerComponent {
81108
this.contentTypeInstructions$ = InscriptionPreviewService.getContentTypeInstructions(inscription);
82109
}
83110
}
111+
112+
function bytesToHex(bytes: Uint8Array): string {
113+
let out = '';
114+
for (let i = 0; i < bytes.length; i++) {
115+
out += bytes[i].toString(16).padStart(2, '0');
116+
}
117+
return out;
118+
}

frontend/src/app/components/_ordpool/digital-artifact-viewer/labitbu-viewer/labitbu-viewer.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@if (_parsed && dataUri) {
2-
<strong>Labitbu:</strong>
2+
<strong>Labitbu:</strong>
33
<span class="smaller-text">image/webp</span>
44
<br>
55
<img [src]="dataUri" alt="Labitbu image" class="labitbu-preview mt-2">

frontend/src/app/components/_ordpool/digital-artifact-viewer/ots-viewer/ots-viewer.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@if (loaded && row) {
22
<div class="ots-panel">
3-
<strong>OpenTimestamps:</strong>
3+
<strong>OpenTimestamps:</strong>
44
<span title="Calendar that broadcast this commit">{{ row.calendar }}</span>
55
@if (row.confirmedAt) {
66
<span class="badge confirmed">confirmed</span>

frontend/src/app/components/_ordpool/digital-artifact-viewer/src101-viewer/src101-viewer.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<strong>SRC-101:</strong>
1+
<strong>SRC-101:</strong>
22
<span class="smaller-text">Bitname registry on Bitcoin Stamps</span>
33
@if (json) {
44
<app-json-viewer [text]="json" />

frontend/src/app/components/_ordpool/digital-artifact-viewer/src20-viewer/src20-viewer.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<ng-container *ngIf="json">
22

3-
<strong>Stamps</strong><br>
3+
<strong>SRC-20</strong><br>
44
<br *ngIf="showDetails">
55

66
<app-json-viewer [text]="json" />

0 commit comments

Comments
 (0)