Decode inscription / rune data client-side

This commit is contained in:
natsoni
2024-10-07 20:03:36 +09:00
parent 4143a5f593
commit 8b6db768cd
11 changed files with 473 additions and 3 deletions

View File

@@ -0,0 +1,75 @@
@if (error) {
<div>
<i>Error fetching data (code {{ error.status }})</i>
</div>
} @else {
@if (minted) {
<ng-container i18n="ord.mint-n-runes">
<span>Mint</span>
<span class="amount"> {{ minted >= 100000 ? (minted | amountShortener:undefined:undefined:true) : minted }} </span>
<ng-container *ngTemplateOutlet="runeName; context: { $implicit: runestone.mint.unwrap().toString() }"></ng-container>
</ng-container>
}
@if (totalSupply > -1) {
@if (premined > 0) {
<ng-container i18n="ord.premine-n-runes">
<span>Premine</span>
<span class="amount"> {{ premined >= 100000 ? (premined | amountShortener:undefined:undefined:true) : premined }} </span>
{{ etchedSymbol }}
<span class="name">{{ etchedName }}</span>
<span> ({{ premined / totalSupply * 100 | amountShortener:0}}% of total supply)</span>
</ng-container>
} @else {
<ng-container i18n="ord.etch-rune">
<span>Etching of</span>
{{ etchedSymbol }}
<span class="name">{{ etchedName }}</span>
</ng-container>
}
}
@if (transferredRunes?.length && type === 'vout') {
<div *ngFor="let rune of transferredRunes">
<ng-container i18n="ord.transfer-rune">
<span>Transfer</span>
<ng-container *ngTemplateOutlet="runeName; context: { $implicit: rune.key }"></ng-container>
</ng-container>
</div>
}
<!-- @if (runestone && !runestone?.etching && !runestone?.mint && !transferredRunes?.length && type === 'vout') {
<div>
<i>No content in this runestone</i>
</div>
} -->
@if (inscriptions?.length && type === 'vin') {
<div *ngFor="let contentType of inscriptionsData | keyvalue">
<div>
<span class="badge badge-ord mr-1">{{ contentType.value.count > 1 ? contentType.value.count + " " : "" }}{{ contentType.value?.tag || contentType.key }}</span>
<span class="badge badge-ord" *ngIf="contentType.value.totalSize > 0">{{ contentType.value.totalSize | bytes:2:'B':undefined:true }}</span>
<a *ngIf="contentType.value.delegate" [routerLink]="['/tx' | relativeUrl, contentType.value.delegate]">
<span i18n="ord.source-inscription">Source inscription</span>
</a>
</div>
<pre *ngIf="contentType.value.json" class="name" style="white-space: pre-wrap; word-break: break-word;">{{ contentType.value.json | json }}</pre>
<pre *ngIf="contentType.value.text" class="name" style="white-space: pre-wrap; word-break: break-word;">{{ contentType.value.text }}</pre>
</div>
}
@if (!runestone && type === 'vout') {
<div class="skeleton-loader" style="width: 50%;"></div>
}
@if (!inscriptions?.length && type === 'vin') {
<div>
<i>Error decoding inscription data</i>
</div>
}
}
<ng-template #runeName let-id>
{{ runeInfo[id]?.etching.symbol.isSome() ? runeInfo[id]?.etching.symbol.unwrap() : '' }}
<a [routerLink]="id !== '1:0' ? ['/tx' | relativeUrl, runeInfo[id]?.txid] : null" [class.rune-link]="id !== '1:0'" [class.disabled]="id === '1:0'">
<span class="name">{{ runeInfo[id]?.name }}</span>
</a>
</ng-template>

View File

@@ -0,0 +1,35 @@
.amount {
font-weight: bold;
}
a.rune-link {
color: inherit;
&:hover {
text-decoration: underline;
text-decoration-color: var(--transparent-fg);
}
}
a.disabled {
text-decoration: none;
}
.name {
color: var(--transparent-fg);
font-weight: 700;
}
.badge-ord {
background-color: var(--grey);
position: relative;
top: -2px;
font-size: 81%;
&.primary {
background-color: var(--primary);
}
}
pre {
margin-top: 5px;
max-height: 150px;
}

View File

@@ -0,0 +1,140 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Runestone } from '../../shared/ord/rune/runestone';
import { Etching } from '../../shared/ord/rune/etching';
import { u128, u32, u8 } from '../../shared/ord/rune/integer';
import { HttpErrorResponse } from '@angular/common/http';
import { SpacedRune } from '../../shared/ord/rune/spacedrune';
export interface Inscription {
body?: Uint8Array;
body_length?: number;
content_type?: Uint8Array;
content_type_str?: string;
delegate_txid?: string;
}
@Component({
selector: 'app-ord-data',
templateUrl: './ord-data.component.html',
styleUrls: ['./ord-data.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OrdDataComponent implements OnChanges {
@Input() inscriptions: Inscription[];
@Input() runestone: Runestone;
@Input() runeInfo: { [id: string]: { etching: Etching; txid: string; name?: string; } };
@Input() error: HttpErrorResponse;
@Input() type: 'vin' | 'vout';
// Inscriptions
inscriptionsData: { [key: string]: { count: number, totalSize: number, text?: string; json?: JSON; tag?: string; delegate?: string } };
// Rune mints
minted: number;
// Rune etching
premined: number = -1;
totalSupply: number = -1;
etchedName: string;
etchedSymbol: string;
// Rune transfers
transferredRunes: { key: string; etching: Etching; txid: string; name?: string; }[] = [];
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
if (changes.runestone && this.runestone) {
Object.keys(this.runeInfo).forEach((key) => {
const rune = this.runeInfo[key].etching.rune.isSome() ? this.runeInfo[key].etching.rune.unwrap() : null;
const spacers = this.runeInfo[key].etching.spacers.isSome() ? this.runeInfo[key].etching.spacers.unwrap() : u32(0);
if (rune) {
this.runeInfo[key].name = new SpacedRune(rune, Number(spacers)).toString();
}
this.transferredRunes.push({ key, ...this.runeInfo[key] });
});
if (this.runestone.mint.isSome() && this.runeInfo[this.runestone.mint.unwrap().toString()]) {
const mint = this.runestone.mint.unwrap().toString();
this.transferredRunes = this.transferredRunes.filter(rune => rune.key !== mint);
const terms = this.runeInfo[mint].etching.terms.isSome() ? this.runeInfo[mint].etching.terms.unwrap() : null;
let amount: u128;
if (terms) {
amount = terms.amount.isSome() ? terms.amount.unwrap() : u128(0);
}
const divisibility = this.runeInfo[mint].etching.divisibility.isSome() ? this.runeInfo[mint].etching.divisibility.unwrap() : u8(0);
if (amount) {
this.minted = this.getAmount(amount, divisibility);
}
}
if (this.runestone.etching.isSome()) {
const etching = this.runestone.etching.unwrap();
const rune = etching.rune.isSome() ? etching.rune.unwrap() : null;
const spacers = etching.spacers.isSome() ? etching.spacers.unwrap() : u32(0);
if (rune) {
this.etchedName = new SpacedRune(rune, Number(spacers)).toString();
}
this.etchedSymbol = etching.symbol.isSome() ? etching.symbol.unwrap() : '';
const divisibility = etching.divisibility.isSome() ? etching.divisibility.unwrap() : u8(0);
const premine = etching.premine.isSome() ? etching.premine.unwrap() : u128(0);
if (premine) {
this.premined = this.getAmount(premine, divisibility);
} else {
this.premined = 0;
}
const terms = etching.terms.isSome() ? etching.terms.unwrap() : null;
let amount: u128;
if (terms) {
amount = terms.amount.isSome() ? terms.amount.unwrap() : u128(0);
if (amount) {
const cap = terms.cap.isSome() ? terms.cap.unwrap() : u128(0);
this.totalSupply = this.premined + this.getAmount(amount, divisibility) * Number(cap);
}
} else {
this.totalSupply = this.premined;
}
}
}
if (changes.inscriptions && this.inscriptions) {
if (this.inscriptions?.length) {
this.inscriptionsData = {};
this.inscriptions.forEach((inscription) => {
// General: count, total size, delegate
const key = inscription.content_type_str || 'undefined';
if (!this.inscriptionsData[key]) {
this.inscriptionsData[key] = { count: 0, totalSize: 0 };
}
this.inscriptionsData[key].count++;
this.inscriptionsData[key].totalSize += inscription.body_length;
if (inscription.delegate_txid && !this.inscriptionsData[key].delegate) {
this.inscriptionsData[key].delegate = inscription.delegate_txid;
}
// Text / JSON data
if ((key.includes('text') || key.includes('json')) && inscription.body?.length && !this.inscriptionsData[key].text && !this.inscriptionsData[key].json) {
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(inscription.body);
try {
this.inscriptionsData[key].json = JSON.parse(text);
if (this.inscriptionsData[key].json['p']) {
this.inscriptionsData[key].tag = this.inscriptionsData[key].json['p'].toUpperCase();
}
} catch (e) {
this.inscriptionsData[key].text = text;
}
}
});
}
}
}
getAmount(amount: u128 | bigint, divisibility: u8): number {
const divisor = BigInt(10) ** BigInt(divisibility);
const result = amount / divisor;
return result <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(result) : Number.MAX_SAFE_INTEGER;
}
}