From acae5a33b08fe4bd0edb7560cd41b0ac5a26f273 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 8 Oct 2024 01:41:35 +0000 Subject: [PATCH] replace rune parsing dependencies with minimal reimplementation --- .../ord-data/ord-data.component.html | 30 +- .../components/ord-data/ord-data.component.ts | 72 +--- .../transactions-list.component.ts | 7 +- frontend/src/app/services/ord-api.service.ts | 41 +- frontend/src/app/shared/ord/rune.utils.ts | 258 ++++++++++++ frontend/src/app/shared/ord/rune/artifact.ts | 4 - frontend/src/app/shared/ord/rune/cenotaph.ts | 14 - frontend/src/app/shared/ord/rune/constants.ts | 7 - frontend/src/app/shared/ord/rune/edict.ts | 34 -- frontend/src/app/shared/ord/rune/etching.ts | 54 --- frontend/src/app/shared/ord/rune/flag.ts | 20 - frontend/src/app/shared/ord/rune/flaw.ts | 12 - .../src/app/shared/ord/rune/integer/index.ts | 4 - .../src/app/shared/ord/rune/integer/u128.ts | 176 -------- .../src/app/shared/ord/rune/integer/u32.ts | 58 --- .../src/app/shared/ord/rune/integer/u64.ts | 58 --- .../src/app/shared/ord/rune/integer/u8.ts | 58 --- frontend/src/app/shared/ord/rune/message.ts | 67 --- frontend/src/app/shared/ord/rune/monads.ts | 392 ------------------ frontend/src/app/shared/ord/rune/rune.ts | 23 - frontend/src/app/shared/ord/rune/runeid.ts | 89 ---- frontend/src/app/shared/ord/rune/runestone.ts | 258 ------------ frontend/src/app/shared/ord/rune/script.ts | 237 ----------- frontend/src/app/shared/ord/rune/seekarray.ts | 43 -- .../src/app/shared/ord/rune/spacedrune.ts | 21 - frontend/src/app/shared/ord/rune/tag.ts | 60 --- frontend/src/app/shared/ord/rune/terms.ts | 9 - frontend/src/app/shared/ord/rune/utils.ts | 6 - 28 files changed, 300 insertions(+), 1812 deletions(-) create mode 100644 frontend/src/app/shared/ord/rune.utils.ts delete mode 100644 frontend/src/app/shared/ord/rune/artifact.ts delete mode 100644 frontend/src/app/shared/ord/rune/cenotaph.ts delete mode 100644 frontend/src/app/shared/ord/rune/constants.ts delete mode 100644 frontend/src/app/shared/ord/rune/edict.ts delete mode 100644 frontend/src/app/shared/ord/rune/etching.ts delete mode 100644 frontend/src/app/shared/ord/rune/flag.ts delete mode 100644 frontend/src/app/shared/ord/rune/flaw.ts delete mode 100644 frontend/src/app/shared/ord/rune/integer/index.ts delete mode 100644 frontend/src/app/shared/ord/rune/integer/u128.ts delete mode 100644 frontend/src/app/shared/ord/rune/integer/u32.ts delete mode 100644 frontend/src/app/shared/ord/rune/integer/u64.ts delete mode 100644 frontend/src/app/shared/ord/rune/integer/u8.ts delete mode 100644 frontend/src/app/shared/ord/rune/message.ts delete mode 100644 frontend/src/app/shared/ord/rune/monads.ts delete mode 100644 frontend/src/app/shared/ord/rune/rune.ts delete mode 100644 frontend/src/app/shared/ord/rune/runeid.ts delete mode 100644 frontend/src/app/shared/ord/rune/runestone.ts delete mode 100644 frontend/src/app/shared/ord/rune/script.ts delete mode 100644 frontend/src/app/shared/ord/rune/seekarray.ts delete mode 100644 frontend/src/app/shared/ord/rune/spacedrune.ts delete mode 100644 frontend/src/app/shared/ord/rune/tag.ts delete mode 100644 frontend/src/app/shared/ord/rune/terms.ts delete mode 100644 frontend/src/app/shared/ord/rune/utils.ts diff --git a/frontend/src/app/components/ord-data/ord-data.component.html b/frontend/src/app/components/ord-data/ord-data.component.html index be9a24715..696e7ea17 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.html +++ b/frontend/src/app/components/ord-data/ord-data.component.html @@ -7,23 +7,23 @@ Mint {{ minted >= 100000 ? (minted | amountShortener:undefined:undefined:true) : minted }} - + } - @if (totalSupply > -1) { - @if (premined > 0) { + @if (runestone?.etching?.supply) { + @if (runestone?.etching.premine > 0) { Premine - {{ premined >= 100000 ? (premined | amountShortener:undefined:undefined:true) : premined }} - {{ etchedSymbol }} - {{ etchedName }} - ({{ premined / totalSupply * 100 | amountShortener:0}}% of total supply) + {{ runestone.etching.premine >= 100000 ? (toNumber(runestone.etching.premine) | amountShortener:undefined:undefined:true) : runestone.etching.premine }} + {{ runestone.etching.symbol }} + {{ runestone.etching.spacedName }} + ({{ toNumber(runestone.etching.premine) / toNumber(runestone.etching.supply) * 100 | amountShortener:0}}% of total supply) - } @else { + } @else { Etching of - {{ etchedSymbol }} - {{ etchedName }} + {{ runestone.etching.symbol }} + {{ runestone.etching.spacedName }} } } @@ -36,12 +36,6 @@ } - - @if (inscriptions?.length && type === 'vin') {
@@ -68,8 +62,8 @@ } - {{ runeInfo[id]?.etching.symbol.isSome() ? runeInfo[id]?.etching.symbol.unwrap() : '' }} + {{ runeInfo[id]?.etching.symbol || '' }} - {{ runeInfo[id]?.name }} + {{ runeInfo[id]?.etching.spacedName }} \ No newline at end of file diff --git a/frontend/src/app/components/ord-data/ord-data.component.ts b/frontend/src/app/components/ord-data/ord-data.component.ts index 8d7eef973..233b8d243 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.ts +++ b/frontend/src/app/components/ord-data/ord-data.component.ts @@ -1,9 +1,6 @@ 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'; +import { Runestone, Etching } from '../../shared/ord/rune.utils'; export interface Inscription { body?: Uint8Array; @@ -22,79 +19,34 @@ export interface Inscription { export class OrdDataComponent implements OnChanges { @Input() inscriptions: Inscription[]; @Input() runestone: Runestone; - @Input() runeInfo: { [id: string]: { etching: Etching; txid: string; name?: string; } }; + @Input() runeInfo: { [id: string]: { etching: Etching; txid: string } }; @Input() error: HttpErrorResponse; @Input() type: 'vin' | 'vout'; + toNumber = (value: bigint): number => Number(value); + // 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; }[] = []; + transferredRunes: { key: string; etching: Etching; txid: 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 = Object.entries(this.runeInfo).map(([key, runeInfo]) => ({ key, ...runeInfo })); + if (this.runestone.mint && this.runeInfo[this.runestone.mint.toString()]) { + const mint = this.runestone.mint.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); + const terms = this.runeInfo[mint].etching.terms; + const amount = terms?.amount; + const divisibility = this.runeInfo[mint].etching.divisibility; 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) { @@ -131,7 +83,7 @@ export class OrdDataComponent implements OnChanges { } } - getAmount(amount: u128 | bigint, divisibility: u8): number { + getAmount(amount: bigint, divisibility: number): number { const divisor = BigInt(10) ** BigInt(divisibility); const result = amount / divisor; diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 1f45d5241..706ee9684 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -6,15 +6,14 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter import { ElectrsApiService } from '../../services/electrs-api.service'; import { environment } from '../../../environments/environment'; import { AssetsService } from '../../services/assets.service'; -import { filter, map, tap, switchMap, shareReplay, catchError } from 'rxjs/operators'; +import { filter, map, tap, switchMap, catchError } from 'rxjs/operators'; import { BlockExtended } from '../../interfaces/node-api.interface'; import { ApiService } from '../../services/api.service'; import { PriceService } from '../../services/price.service'; import { StorageService } from '../../services/storage.service'; import { OrdApiService } from '../../services/ord-api.service'; import { Inscription } from '../ord-data/ord-data.component'; -import { Runestone } from '../../shared/ord/rune/runestone'; -import { Etching } from '../../shared/ord/rune/etching'; +import { Etching, Runestone } from '../../shared/ord/rune.utils'; @Component({ selector: 'app-transactions-list', @@ -261,7 +260,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { tx.vout[i].isRunestone = true; break; } - } + } } }); diff --git a/frontend/src/app/services/ord-api.service.ts b/frontend/src/app/services/ord-api.service.ts index bc726e839..da75a74af 100644 --- a/frontend/src/app/services/ord-api.service.ts +++ b/frontend/src/app/services/ord-api.service.ts @@ -3,10 +3,9 @@ import { catchError, forkJoin, map, Observable, of, switchMap, tap } from 'rxjs' import { Inscription } from '../components/ord-data/ord-data.component'; import { Transaction } from '../interfaces/electrs.interface'; import { getNextInscriptionMark, hexToBytes, extractInscriptionData } from '../shared/ord/inscription.utils'; -import { Runestone } from '../shared/ord/rune/runestone'; -import { Etching } from '../shared/ord/rune/etching'; +import { decipherRunestone, Runestone, Etching, UNCOMMON_GOODS } from '../shared/ord/rune.utils'; import { ElectrsApiService } from './electrs-api.service'; -import { UNCOMMON_GOODS } from '../shared/ord/rune/runestone'; + @Injectable({ providedIn: 'root' @@ -18,27 +17,16 @@ export class OrdApiService { ) { } decodeRunestone$(tx: Transaction): Observable<{ runestone: Runestone, runeInfo: { [id: string]: { etching: Etching; txid: string; } } }> { - const runestoneTx = { vout: tx.vout.map(vout => ({ scriptpubkey: vout.scriptpubkey })) }; - const decipher = Runestone.decipher(runestoneTx); - - // For now, ignore cenotaphs - let message = decipher.isSome() ? decipher.unwrap() : null; - if (message?.type === 'cenotaph') { - return of({ runestone: null, runeInfo: {} }); - } - - const runestone = message as Runestone; + const runestone = decipherRunestone(tx); const runeInfo: { [id: string]: { etching: Etching; txid: string; } } = {}; const runesToFetch: Set = new Set(); if (runestone) { - if (runestone.mint.isSome()) { - const mint = runestone.mint.unwrap().toString(); - - if (mint === '1:0') { - runeInfo[mint] = { etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' }; + if (runestone.mint) { + if (runestone.mint.toString() === '1:0') { + runeInfo[runestone.mint.toString()] = { etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' }; } else { - runesToFetch.add(mint); + runesToFetch.add(runestone.mint.toString()); } } @@ -65,9 +53,10 @@ export class OrdApiService { }) ); } + return of({ runestone: runestone, runeInfo }); + } else { + return of({ runestone: null, runeInfo: {} }); } - - return of({ runestone: runestone, runeInfo }); } // Get etching from runeId by looking up the transaction that etched the rune @@ -78,11 +67,11 @@ export class OrdApiService { switchMap(blockHash => this.electrsApiService.getBlockTxId$(blockHash, parseInt(txIndex))), switchMap(txId => this.electrsApiService.getTransaction$(txId)), switchMap(tx => { - const decipheredMessage = Runestone.decipher(tx); - if (decipheredMessage.isSome()) { - const message = decipheredMessage.unwrap(); - if (message?.type === 'runestone' && message.etching.isSome()) { - return of({ etching: message.etching.unwrap(), txid: tx.txid }); + const runestone = decipherRunestone(tx); + if (runestone) { + const etching = runestone.etching; + if (etching) { + return of({ etching, txid: tx.txid }); } } return of(null); diff --git a/frontend/src/app/shared/ord/rune.utils.ts b/frontend/src/app/shared/ord/rune.utils.ts new file mode 100644 index 000000000..a1f947b46 --- /dev/null +++ b/frontend/src/app/shared/ord/rune.utils.ts @@ -0,0 +1,258 @@ +import { Transaction } from '../../interfaces/electrs.interface'; + +export const U128_MAX_BIGINT = 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffffn; + +export class RuneId { + block: number; + index: number; + + constructor(block: number, index: number) { + this.block = block; + this.index = index; + } + + toString(): string { + return `${this.block}:${this.index}`; + } +} + +export type Etching = { + divisibility?: number; + premine?: bigint; + symbol?: string; + terms?: { + cap?: bigint; + amount?: bigint; + offset?: { + start?: bigint; + end?: bigint; + }; + height?: { + start?: bigint; + end?: bigint; + }; + }; + turbo?: boolean; + name?: string; + spacedName?: string; + supply?: bigint; +}; + +export type Edict = { + id: RuneId; + amount: bigint; + output: number; +}; + +export type Runestone = { + mint?: RuneId; + pointer?: number; + edicts?: Edict[]; + etching?: Etching; +}; + +type Message = { + fields: Record; + edicts: Edict[]; +} + +export const UNCOMMON_GOODS: Etching = { + divisibility: 0, + premine: 0n, + symbol: '⧉', + terms: { + cap: U128_MAX_BIGINT, + amount: 1n, + offset: { + start: 0n, + end: 0n, + }, + height: { + start: 840000n, + end: 1050000n, + }, + }, + turbo: false, + name: 'UNCOMMONGOODS', + spacedName: 'UNCOMMON•GOODS', + supply: U128_MAX_BIGINT, +}; + +enum Tag { + Body = 0, + Flags = 2, + Rune = 4, + Premine = 6, + Cap = 8, + Amount = 10, + HeightStart = 12, + HeightEnd = 14, + OffsetStart = 16, + OffsetEnd = 18, + Mint = 20, + Pointer = 22, + Cenotaph = 126, + + Divisibility = 1, + Spacers = 3, + Symbol = 5, + Nop = 127, +} + +const Flag = { + ETCHING: 1n, + TERMS: 1n << 1n, + TURBO: 1n << 2n, + CENOTAPH: 1n << 127n, +}; + +function hexToBytes(hex: string): Uint8Array { + return new Uint8Array(hex.match(/.{2}/g).map((byte) => parseInt(byte, 16))); +} + +function decodeLEB128(bytes: Uint8Array): bigint[] { + const integers: bigint[] = []; + let index = 0; + while (index < bytes.length) { + let value = BigInt(0); + let shift = 0; + let byte: number; + do { + byte = bytes[index++]; + value |= BigInt(byte & 0x7f) << BigInt(shift); + shift += 7; + } while (byte & 0x80); + integers.push(value); + } + return integers; +} + +function integersToMessage(integers: bigint[]): Message { + const message = { + fields: {}, + edicts: [], + }; + let inBody = false; + while (integers.length) { + if (!inBody) { + // The integers are interpreted as a sequence of tag/value pairs, with duplicate tags appending their value to the field value. + const tag: Tag = Number(integers.shift()); + if (tag === Tag.Body) { + inBody = true; + } else { + const value = integers.shift(); + if (message.fields[tag]) { + message.fields[tag].push(value); + } else { + message.fields[tag] = [value]; + } + } + } else { + // If a tag with value zero is encountered, all following integers are interpreted as a series of four-integer edicts, each consisting of a rune ID block height, rune ID transaction index, amount, and output. + const height = integers.shift(); + const txIndex = integers.shift(); + const amount = integers.shift(); + const output = integers.shift(); + message.edicts.push({ + id: { + block: height, + index: txIndex, + }, + amount, + output, + }); + } + } + return message; +} + +function parseRuneName(rune: bigint): string { + let name = ''; + rune += 1n; + while (rune > 0n) { + name = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Number((rune - 1n) % 26n)] + name; + rune = (rune - 1n) / 26n; + } + return name; +} + +function spaceRuneName(name: string, spacers: bigint): string { + let i = 0; + let spacedName = ''; + while (spacers > 0n || i < name.length) { + spacedName += name[i]; + if (spacers & 1n) { + spacedName += '•'; + } + if (spacers > 0n) { + spacers >>= 1n; + } + i++; + } + return spacedName; +} + +function messageToRunestone(message: Message): Runestone { + let etching: Etching | undefined; + let mint: RuneId | undefined; + let pointer: number | undefined; + + const flags = message.fields[Tag.Flags]?.[0] || 0n; + if (flags & Flag.ETCHING) { + const hasTerms = (flags & Flag.TERMS) > 0n; + const isTurbo = (flags & Flag.TURBO) > 0n; + const name = parseRuneName(message.fields[Tag.Rune][0]); + etching = { + divisibility: Number(message.fields[Tag.Divisibility][0]), + premine: message.fields[Tag.Premine]?.[0], + symbol: message.fields[Tag.Symbol]?.[0] ? String.fromCodePoint(Number(message.fields[Tag.Symbol][0])) : '¤', + terms: hasTerms ? { + cap: message.fields[Tag.Cap]?.[0], + amount: message.fields[Tag.Amount]?.[0], + offset: { + start: message.fields[Tag.OffsetStart]?.[0], + end: message.fields[Tag.OffsetEnd]?.[0], + }, + height: { + start: message.fields[Tag.HeightStart]?.[0], + end: message.fields[Tag.HeightEnd]?.[0], + }, + } : undefined, + turbo: isTurbo, + name, + spacedName: spaceRuneName(name, message.fields[Tag.Spacers]?.[0] ?? 0n), + }; + etching.supply = ( + (etching.terms?.cap ?? 0n) * (etching.terms?.amount ?? 0n) + ) + (etching.premine ?? 0n); + } + const mintField = message.fields[Tag.Mint]; + if (mintField) { + mint = new RuneId(Number(mintField[0]), Number(mintField[1])); + } + const pointerField = message.fields[Tag.Pointer]; + if (pointerField) { + pointer = Number(pointerField[0]); + } + return { + mint, + pointer, + edicts: message.edicts, + etching, + }; +} + +export function decipherRunestone(tx: Transaction): Runestone | void { + const payload = tx.vout.find((vout) => vout.scriptpubkey.startsWith('6a5d'))?.scriptpubkey_asm.replace(/OP_\w+|\s/g, ''); + if (!payload) { + return; + } + try { + const integers = decodeLEB128(hexToBytes(payload)); + const message = integersToMessage(integers); + return messageToRunestone(message); + } catch (error) { + console.error(error); + return; + } +} diff --git a/frontend/src/app/shared/ord/rune/artifact.ts b/frontend/src/app/shared/ord/rune/artifact.ts deleted file mode 100644 index 2eba9f158..000000000 --- a/frontend/src/app/shared/ord/rune/artifact.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Cenotaph } from './cenotaph'; -import { Runestone } from './runestone'; - -export type Artifact = Cenotaph | Runestone; diff --git a/frontend/src/app/shared/ord/rune/cenotaph.ts b/frontend/src/app/shared/ord/rune/cenotaph.ts deleted file mode 100644 index 368a0f938..000000000 --- a/frontend/src/app/shared/ord/rune/cenotaph.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Flaw } from './flaw'; -import { None, Option } from './monads'; -import { Rune } from './rune'; -import { RuneId } from './runeid'; - -export class Cenotaph { - readonly type = 'cenotaph'; - - constructor( - readonly flaws: Flaw[], - readonly etching: Option = None, - readonly mint: Option = None - ) {} -} diff --git a/frontend/src/app/shared/ord/rune/constants.ts b/frontend/src/app/shared/ord/rune/constants.ts deleted file mode 100644 index 0e4bab116..000000000 --- a/frontend/src/app/shared/ord/rune/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { u8 } from './integer'; -import { opcodes } from './script'; - -export const MAX_DIVISIBILITY = u8(38); - -export const OP_RETURN = opcodes.OP_RETURN; -export const MAGIC_NUMBER = opcodes.OP_13; diff --git a/frontend/src/app/shared/ord/rune/edict.ts b/frontend/src/app/shared/ord/rune/edict.ts deleted file mode 100644 index ede5865a6..000000000 --- a/frontend/src/app/shared/ord/rune/edict.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Option, Some, None } from './monads'; -import { RuneId } from './runeid'; -import { u128, u32 } from './integer'; - -export type Edict = { - id: RuneId; - amount: u128; - output: u32; -}; - -export namespace Edict { - export function fromIntegers( - numOutputs: number, - id: RuneId, - amount: u128, - output: u128 - ): Option { - if (id.block === 0n && id.tx > 0n) { - return None; - } - - const optionOutputU32 = u128.tryIntoU32(output); - if (optionOutputU32.isNone()) { - return None; - } - const outputU32 = optionOutputU32.unwrap(); - - if (outputU32 > numOutputs) { - return None; - } - - return Some({ id, amount, output: outputU32 }); - } -} diff --git a/frontend/src/app/shared/ord/rune/etching.ts b/frontend/src/app/shared/ord/rune/etching.ts deleted file mode 100644 index edc245565..000000000 --- a/frontend/src/app/shared/ord/rune/etching.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { None, Option, Some } from './monads'; -import { Terms } from './terms'; -import { Rune } from './rune'; -import { u128, u32, u8 } from './integer'; - -type RuneEtchingBase = { - divisibility?: number; - premine?: bigint; - symbol?: string; - terms?: { - cap?: bigint; - amount?: bigint; - offset?: { - start?: bigint; - end?: bigint; - }; - height?: { - start?: bigint; - end?: bigint; - }; - }; - turbo?: boolean; -}; - -export type RuneEtchingSpec = RuneEtchingBase & { runeName?: string }; - -export class Etching { - readonly symbol: Option; - - constructor( - readonly divisibility: Option, - readonly rune: Option, - readonly spacers: Option, - symbol: Option, - readonly terms: Option, - readonly premine: Option, - readonly turbo: boolean - ) { - this.symbol = symbol.andThen((value) => { - const codePoint = value.codePointAt(0); - return codePoint !== undefined ? Some(String.fromCodePoint(codePoint)) : None; - }); - } - - get supply(): Option { - const premine = this.premine.unwrapOr(u128(0)); - const cap = this.terms.andThen((terms) => terms.cap).unwrapOr(u128(0)); - const amount = this.terms.andThen((terms) => terms.amount).unwrapOr(u128(0)); - - return u128 - .checkedMultiply(cap, amount) - .andThen((multiplyResult) => u128.checkedAdd(premine, multiplyResult)); - } -} diff --git a/frontend/src/app/shared/ord/rune/flag.ts b/frontend/src/app/shared/ord/rune/flag.ts deleted file mode 100644 index 317c74ae5..000000000 --- a/frontend/src/app/shared/ord/rune/flag.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { u128 } from './integer'; - -export enum Flag { - ETCHING = 0, - TERMS = 1, - TURBO = 2, - CENOTAPH = 127, -} - -export namespace Flag { - export function mask(flag: Flag): u128 { - return u128(1n << BigInt(flag)); - } - - export function take(flags: u128, flag: Flag): { set: boolean; flags: u128 } { - const mask = Flag.mask(flag); - const set = (flags & mask) !== 0n; - return { set, flags: set ? u128(flags - mask) : flags }; - } -} diff --git a/frontend/src/app/shared/ord/rune/flaw.ts b/frontend/src/app/shared/ord/rune/flaw.ts deleted file mode 100644 index 2ed5ea506..000000000 --- a/frontend/src/app/shared/ord/rune/flaw.ts +++ /dev/null @@ -1,12 +0,0 @@ -export enum Flaw { - EDICT_OUTPUT, - EDICT_RUNE_ID, - INVALID_SCRIPT, - OPCODE, - SUPPLY_OVERFLOW, - TRAILING_INTEGERS, - TRUNCATED_FIELD, - UNRECOGNIZED_EVEN_TAG, - UNRECOGNIZED_FLAG, - VARINT, -} diff --git a/frontend/src/app/shared/ord/rune/integer/index.ts b/frontend/src/app/shared/ord/rune/integer/index.ts deleted file mode 100644 index 3c54a77e7..000000000 --- a/frontend/src/app/shared/ord/rune/integer/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { u8 } from './u8'; -export { u32 } from './u32'; -export { u64 } from './u64'; -export { u128 } from './u128'; diff --git a/frontend/src/app/shared/ord/rune/integer/u128.ts b/frontend/src/app/shared/ord/rune/integer/u128.ts deleted file mode 100644 index 78de8506f..000000000 --- a/frontend/src/app/shared/ord/rune/integer/u128.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { None, Option, Some } from '../monads'; -import { SeekArray } from '../seekarray'; -import { u64 } from './u64'; -import { u32 } from './u32'; -import { u8 } from './u8'; - -/** - * A little utility type used for nominal typing. - * - * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} - */ -type BigTypedNumber = bigint & { - /** - * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! - * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. - * @ignore - * @private - * @readonly - * @type {undefined} - */ - readonly __kind__: T; -}; - -/** - * ## 128-bit unsigned integer - * - * - **Value Range:** `0` to `340282366920938463463374607431768211455` - * - **Size in bytes:** `16` - * - **Web IDL type:** `bigint` - * - **Equivalent C type:** `uint128_t` - */ -export type u128 = BigTypedNumber<'u128'>; - -export const U128_MAX_BIGINT = 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffffn; - -/** - * Convert Number or BigInt to 128-bit unsigned integer. - * @param num - The Number or BigInt to convert. - * @returns - The resulting 128-bit unsigned integer (BigInt). - */ -export function u128(num: number | bigint): u128 { - if (typeof num == 'bigint') { - if (num < 0n || num > U128_MAX_BIGINT) { - throw new Error('num is out of range'); - } - } else { - if (!Number.isSafeInteger(num) || num < 0) { - throw new Error('num is not a valid integer'); - } - } - - return BigInt(num) as u128; -} - -export namespace u128 { - export const MAX = u128(U128_MAX_BIGINT); - - export function checkedAdd(x: u128, y: u128): Option { - const result = x + y; - if (result > u128.MAX) { - return None; - } - - return Some(u128(result)); - } - - export function checkedAddThrow(x: u128, y: u128): u128 { - const option = u128.checkedAdd(x, y); - if (option.isNone()) { - throw new Error('checked add overflow'); - } - return option.unwrap(); - } - - export function checkedSub(x: u128, y: u128): Option { - const result = x - y; - if (result < 0n) { - return None; - } - - return Some(u128(result)); - } - - export function checkedSubThrow(x: u128, y: u128): u128 { - const option = u128.checkedSub(x, y); - if (option.isNone()) { - throw new Error('checked sub overflow'); - } - return option.unwrap(); - } - - export function checkedMultiply(x: u128, y: u128): Option { - const result = x * y; - if (result > u128.MAX) { - return None; - } - - return Some(u128(result)); - } - - export function saturatingAdd(x: u128, y: u128): u128 { - const result = x + y; - return result > u128.MAX ? u128.MAX : u128(result); - } - - export function saturatingMultiply(x: u128, y: u128): u128 { - const result = x * y; - return result > u128.MAX ? u128.MAX : u128(result); - } - - export function saturatingSub(x: u128, y: u128): u128 { - return u128(x < y ? 0 : x - y); - } - - export function decodeVarInt(seekArray: SeekArray): Option { - try { - return Some(tryDecodeVarInt(seekArray)); - } catch (e) { - return None; - } - } - - export function tryDecodeVarInt(seekArray: SeekArray): u128 { - let result: u128 = u128(0); - for (let i = 0; i <= 18; i++) { - const byte = seekArray.readUInt8(); - if (byte === undefined) throw new Error('Unterminated or invalid data'); - - // Ensure all operations are done in bigint domain. - const byteBigint = BigInt(byte); - const value = u128(byteBigint & 0x7Fn); // Ensure the 'value' is treated as u128. - - if (i === 18 && (value & 0x7Cn) !== 0n) throw new Error('Overflow'); - - // Use bigint addition instead of bitwise OR to combine the results, - // and ensure shifting is handled correctly within the bigint domain. - result = u128(result + (value << (7n * BigInt(i)))); - - if ((byte & 0x80) === 0) return result; - } - throw new Error('Overlong encoding'); - } - - export function encodeVarInt(value: u128): Uint8Array { - const bytes = []; - while (value >> 7n > 0n) { - bytes.push(Number(value & 0x7Fn) | 0x80); - value = u128(value >> 7n); // Explicitly cast the shifted value back to u128 - } - bytes.push(Number(value & 0x7Fn)); - return new Uint8Array(bytes); - } - - export function tryIntoU64(n: u128): Option { - return n > u64.MAX ? None : Some(u64(n)); - } - - export function tryIntoU32(n: u128): Option { - return n > u32.MAX ? None : Some(u32(n)); - } - - export function tryIntoU8(n: u128): Option { - return n > u8.MAX ? None : Some(u8(n)); - } -} - -export function* getAllU128(data: Uint8Array): Generator { - const seekArray = new SeekArray(data); - while (!seekArray.isFinished()) { - const nextValue = u128.decodeVarInt(seekArray); - if (nextValue.isNone()) { - return; - } - yield nextValue.unwrap(); - } -} diff --git a/frontend/src/app/shared/ord/rune/integer/u32.ts b/frontend/src/app/shared/ord/rune/integer/u32.ts deleted file mode 100644 index 90e517bb8..000000000 --- a/frontend/src/app/shared/ord/rune/integer/u32.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { None, Option, Some } from '../monads'; - -/** - * A little utility type used for nominal typing. - * - * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} - */ -type BigTypedNumber = bigint & { - /** - * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! - * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. - * @ignore - * @private - * @readonly - * @type {undefined} - */ - readonly __kind__: T; -}; - -export type u32 = BigTypedNumber<'u32'>; - -export const U32_MAX_BIGINT = 0xffff_ffffn; - -export function u32(num: number | bigint): u32 { - if (typeof num == 'bigint') { - if (num < 0n || num > U32_MAX_BIGINT) { - throw new Error('num is out of range'); - } - } else { - if (!Number.isSafeInteger(num) || num < 0) { - throw new Error('num is not a valid integer'); - } - } - - return BigInt(num) as u32; -} - -export namespace u32 { - export const MAX = u32(U32_MAX_BIGINT); - - export function checkedAdd(x: u32, y: u32): Option { - const result = x + y; - if (result > u32.MAX) { - return None; - } - - return Some(u32(result)); - } - - export function checkedSub(x: u32, y: u32): Option { - const result = x - y; - if (result < 0n) { - return None; - } - - return Some(u32(result)); - } -} diff --git a/frontend/src/app/shared/ord/rune/integer/u64.ts b/frontend/src/app/shared/ord/rune/integer/u64.ts deleted file mode 100644 index 8010dd99c..000000000 --- a/frontend/src/app/shared/ord/rune/integer/u64.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { None, Option, Some } from '../monads'; - -/** - * A little utility type used for nominal typing. - * - * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} - */ -type BigTypedNumber = bigint & { - /** - * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! - * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. - * @ignore - * @private - * @readonly - * @type {undefined} - */ - readonly __kind__: T; -}; - -export type u64 = BigTypedNumber<'u64'>; - -export const U64_MAX_BIGINT = 0xffff_ffff_ffff_ffffn; - -export function u64(num: number | bigint): u64 { - if (typeof num == 'bigint') { - if (num < 0n || num > U64_MAX_BIGINT) { - throw new Error('num is out of range'); - } - } else { - if (!Number.isSafeInteger(num) || num < 0) { - throw new Error('num is not a valid integer'); - } - } - - return BigInt(num) as u64; -} - -export namespace u64 { - export const MAX = u64(U64_MAX_BIGINT); - - export function checkedAdd(x: u64, y: u64): Option { - const result = x + y; - if (result > u64.MAX) { - return None; - } - - return Some(u64(result)); - } - - export function checkedSub(x: u64, y: u64): Option { - const result = x - y; - if (result < 0n) { - return None; - } - - return Some(u64(result)); - } -} diff --git a/frontend/src/app/shared/ord/rune/integer/u8.ts b/frontend/src/app/shared/ord/rune/integer/u8.ts deleted file mode 100644 index 5676421b0..000000000 --- a/frontend/src/app/shared/ord/rune/integer/u8.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { None, Option, Some } from '../monads'; - -/** - * A little utility type used for nominal typing. - * - * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} - */ -type BigTypedNumber = bigint & { - /** - * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! - * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. - * @ignore - * @private - * @readonly - * @type {undefined} - */ - readonly __kind__: T; -}; - -export type u8 = BigTypedNumber<'u8'>; - -export const U8_MAX_BIGINT = 0xffn; - -export function u8(num: number | bigint): u8 { - if (typeof num == 'bigint') { - if (num < 0n || num > U8_MAX_BIGINT) { - throw new Error('num is out of range'); - } - } else { - if (!Number.isSafeInteger(num) || num < 0) { - throw new Error('num is not a valid integer'); - } - } - - return BigInt(num) as u8; -} - -export namespace u8 { - export const MAX = u8(U8_MAX_BIGINT); - - export function checkedAdd(x: u8, y: u8): Option { - const result = x + y; - if (result > u8.MAX) { - return None; - } - - return Some(u8(result)); - } - - export function checkedSub(x: u8, y: u8): Option { - const result = x - y; - if (result < 0n) { - return None; - } - - return Some(u8(result)); - } -} diff --git a/frontend/src/app/shared/ord/rune/message.ts b/frontend/src/app/shared/ord/rune/message.ts deleted file mode 100644 index cad1a8ced..000000000 --- a/frontend/src/app/shared/ord/rune/message.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Edict } from './edict'; -import { Flaw } from './flaw'; -import { u128, u64, u32 } from './integer'; -import { RuneId } from './runeid'; -import { Tag } from './tag'; - -export class Message { - constructor( - readonly flaws: Flaw[], - readonly edicts: Edict[], - readonly fields: Map - ) {} - - static fromIntegers(numOutputs: number, payload: u128[]): Message { - const edicts: Edict[] = []; - const fields = new Map(); - const flaws: Flaw[] = []; - - for (const i of [...Array(Math.ceil(payload.length / 2)).keys()].map((n) => n * 2)) { - const tag = payload[i]; - - if (u128(Tag.BODY) === tag) { - let id = new RuneId(u64(0), u32(0)); - const chunkSize = 4; - - const body = payload.slice(i + 1); - for (let j = 0; j < body.length; j += chunkSize) { - const chunk = body.slice(j, j + chunkSize); - if (chunk.length !== chunkSize) { - flaws.push(Flaw.TRAILING_INTEGERS); - break; - } - - const optionNext = id.next(chunk[0], chunk[1]); - if (optionNext.isNone()) { - flaws.push(Flaw.EDICT_RUNE_ID); - break; - } - const next = optionNext.unwrap(); - - const optionEdict = Edict.fromIntegers(numOutputs, next, chunk[2], chunk[3]); - if (optionEdict.isNone()) { - flaws.push(Flaw.EDICT_OUTPUT); - break; - } - const edict = optionEdict.unwrap(); - - id = next; - edicts.push(edict); - } - break; - } - - const value = payload[i + 1]; - if (value === undefined) { - flaws.push(Flaw.TRUNCATED_FIELD); - break; - } - - const values = fields.get(tag) ?? []; - values.push(value); - fields.set(tag, values); - } - - return new Message(flaws, edicts, fields); - } -} diff --git a/frontend/src/app/shared/ord/rune/monads.ts b/frontend/src/app/shared/ord/rune/monads.ts deleted file mode 100644 index 7822acca9..000000000 --- a/frontend/src/app/shared/ord/rune/monads.ts +++ /dev/null @@ -1,392 +0,0 @@ -// Copied with MIT License from link below: -// https://github.com/thames-technology/monads/blob/de957d3d68449d659518d99be4ea74bbb70dfc8e/src/option/option.ts - -/** - * Type representing any value except 'undefined'. - * This is useful when working with strict null checks, ensuring that a value can be null but not undefined. - */ -type NonUndefined = {} | null; // eslint-disable-line @typescript-eslint/ban-types - -/** - * Enum-like object to represent the type of an Option (Some or None). - */ -export const OptionType = { - Some: Symbol(':some'), - None: Symbol(':none'), -}; - -/** - * Interface for handling match operations on an Option. - * Allows executing different logic based on the Option being Some or None. - */ -interface Match { - some: (val: A) => B; - none: (() => B) | B; -} - -/** - * The Option interface representing an optional value. - * An Option is either Some, holding a value, or None, indicating the absence of a value. - */ -export interface Option { - /** - * Represents the type of the Option: either Some or None. Useful for debugging and runtime checks. - */ - type: symbol; - - /** - * Determines if the Option is a Some. - * - * @returns true if the Option is Some, otherwise false. - * - * #### Example - * - * ```ts - * console.log(Some(5).isSome()); // true - * console.log(None.isSome()); // false - * ``` - */ - isSome(): boolean; - - /** - * Determines if the Option is None. - * - * @returns true if the Option is None, otherwise false. - * - * #### Example - * - * ```ts - * console.log(Some(5).isNone()); // false - * console.log(None.isNone()); // true - * ``` - */ - isNone(): boolean; - - /** - * Performs a match operation on the Option, allowing for branching logic based on its state. - * This method takes an object with functions for each case (Some or None) and executes - * the corresponding function based on the Option's state, returning the result. - * - * @param fn An object containing two properties: `some` and `none`, which are functions - * to handle the Some and None cases, respectively. - * @returns The result of applying the corresponding function based on the Option's state. - * - * #### Example - * - * ```ts - * const optionSome = Some(5); - * const matchResultSome = optionSome.match({ - * some: (value) => `The value is ${value}.`, - * none: () => 'There is no value.', - * }); - * console.log(matchResultSome); // Outputs: "The value is 5." - * - * const optionNone = None; - * const matchResultNone = optionNone.match({ - * some: (value) => `The value is ${value}.`, - * none: () => 'There is no value.', - * }); - * console.log(matchResultNone); // Outputs: "There is no value." - * ``` - */ - match(fn: Match): U; - - /** - * Applies a function to the contained value (if any), or returns a default if None. - * - * @param fn A function that takes a value of type T and returns a value of type U. - * @returns An Option containing the function's return value if the original Option is Some, otherwise None. - * - * #### Examples - * - * ```ts - * const length = Some("hello").map(s => s.length); // Some(5) - * const noneLength = None.map(s => s.length); // None - * ``` - */ - map(fn: (val: T) => U): Option; - - inspect(fn: (val: T) => void): Option; - - /** - * Transforms the Option into another by applying a function to the contained value, - * chaining multiple potentially failing operations. - * - * @param fn A function that takes a value of type T and returns an Option of type U. - * @returns The Option returned by the function if the original Option is Some, otherwise None. - * - * #### Examples - * - * ```ts - * const parse = (s: string) => { - * const parsed = parseInt(s); - * return isNaN(parsed) ? None : Some(parsed); - * }; - * const result = Some("123").andThen(parse); // Some(123) - * const noResult = Some("abc").andThen(parse); // None - * ``` - */ - andThen(fn: (val: T) => Option): Option; - - /** - * Returns this Option if it is Some, otherwise returns the option provided as a parameter. - * - * @param optb The alternative Option to return if the original Option is None. - * @returns The original Option if it is Some, otherwise `optb`. - * - * #### Examples - * - * ```ts - * const defaultOption = Some("default"); - * const someOption = Some("some").or(defaultOption); // Some("some") - * const noneOption = None.or(defaultOption); // Some("default") - * ``` - */ - or(optb: Option): Option; - - orElse(optb: () => Option): Option; - - /** - * Returns the option provided as a parameter if the original Option is Some, otherwise returns None. - * - * @param optb The Option to return if the original Option is Some. - * @returns `optb` if the original Option is Some, otherwise None. - * - * #### Examples - * - * ```ts - * const anotherOption = Some("another"); - * const someOption = Some("some").and(anotherOption); // Some("another") - * const noneOption = None.and(anotherOption); // None - * ``` - */ - and(optb: Option): Option; - - /** - * Returns the contained value if Some, otherwise returns the provided default value. - * - * @param def The default value to return if the Option is None. - * @returns The contained value if Some, otherwise `def`. - * - * #### Examples - * - * ```ts - * const someValue = Some("value").unwrapOr("default"); // "value" - * const noneValue = None.unwrapOr("default"); // "default" - * ``` - */ - unwrapOr(def: T): T; - - /** - * Unwraps an Option, yielding the contained value if Some, otherwise throws an error. - * - * @returns The contained value. - * @throws Error if the Option is None. - * - * #### Examples - * - * ```ts - * console.log(Some("value").unwrap()); // "value" - * console.log(None.unwrap()); // throws Error - * ``` - */ - unwrap(): T | never; -} - -/** - * Implementation of Option representing a value (Some). - */ -interface SomeOption extends Option { - unwrap(): T; -} - -/** - * Implementation of Option representing the absence of a value (None). - */ -interface NoneOption extends Option { - unwrap(): never; -} - -/** - * Represents a Some value of Option. - */ -class SomeImpl implements SomeOption { - constructor(private readonly val: T) {} - - get type() { - return OptionType.Some; - } - - isSome() { - return true; - } - - isNone() { - return false; - } - - match(fn: Match): B { - return fn.some(this.val); - } - - map(fn: (val: T) => U): Option { - return Some(fn(this.val)); - } - - inspect(fn: (val: T) => void): Option { - fn(this.val); - return this; - } - - andThen(fn: (val: T) => Option): Option { - return fn(this.val); - } - - or(_optb: Option): Option { - return this; - } - - orElse(optb: () => Option): Option { - return this; - } - - and(optb: Option): Option { - return optb; - } - - unwrapOr(_def: T): T { - return this.val; - } - - unwrap(): T { - return this.val; - } -} - -/** - * Represents a None value of Option. - */ -class NoneImpl implements NoneOption { - get type() { - return OptionType.None; - } - - isSome() { - return false; - } - - isNone() { - return true; - } - - match({ none }: Match): U { - if (typeof none === 'function') { - return (none as () => U)(); - } - - return none; - } - - map(_fn: (val: T) => U): Option { - return new NoneImpl(); - } - - inspect(fn: (val: T) => void): Option { - return this; - } - - andThen(_fn: (val: T) => Option): Option { - return new NoneImpl(); - } - - or(optb: Option): Option { - return optb; - } - - orElse(optb: () => Option): Option { - return optb(); - } - - and(_optb: Option): Option { - return new NoneImpl(); - } - - unwrapOr(def: T): T { - return def; - } - - unwrap(): never { - throw new ReferenceError('Trying to unwrap None.'); - } -} - -/** - * Creates a Some instance of Option containing the given value. - * This function is used to represent the presence of a value in an operation that may not always produce a value. - * - * @param val The value to be wrapped in a Some Option. - * @returns An Option instance representing the presence of a value. - * - * #### Example - * - * ```ts - * const option = Some(42); - * console.log(option.unwrap()); // Outputs: 42 - * ``` - */ -export function Some(val: T): Option { - return new SomeImpl(val); -} - -/** - * The singleton instance representing None, an Option with no value. - * This constant is used to represent the absence of a value in operations that may not always produce a value. - * - * #### Example - * - * ```ts - * const option = None; - * console.log(option.isNone()); // Outputs: true - * ``` - */ -export const None: Option = new NoneImpl(); // eslint-disable-line @typescript-eslint/no-explicit-any - -/** - * Type guard to check if an Option is a Some value. - * This function is used to narrow down the type of an Option to SomeOption in TypeScript's type system. - * - * @param val The Option to be checked. - * @returns true if the provided Option is a SomeOption, false otherwise. - * - * #### Example - * - * ```ts - * const option = Some('Success'); - * if (isSome(option)) { - * console.log('Option has a value:', option.unwrap()); - * } - * ``` - */ -export function isSome(val: Option): val is SomeOption { - return val.isSome(); -} - -/** - * Type guard to check if an Option is a None value. - * This function is used to narrow down the type of an Option to NoneOption in TypeScript's type system. - * - * @param val The Option to be checked. - * @returns true if the provided Option is a NoneOption, false otherwise. - * - * #### Example - * - * ```ts - * const option = None; - * if (isNone(option)) { - * console.log('Option does not have a value.'); - * } - * ``` - */ -export function isNone(val: Option): val is NoneOption { - return val.isNone(); -} diff --git a/frontend/src/app/shared/ord/rune/rune.ts b/frontend/src/app/shared/ord/rune/rune.ts deleted file mode 100644 index c0dd96e1b..000000000 --- a/frontend/src/app/shared/ord/rune/rune.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { u128 } from './integer'; - -export class Rune { - - constructor(readonly value: u128) {} - - toString() { - let n = this.value; - - if (n === u128.MAX) { - return 'BCGDENLQRQWDSLRUGSNLBTMFIJAV'; - } - - n = u128(n + 1n); - let symbol = ''; - while (n > 0) { - symbol = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Number((n - 1n) % 26n)] + symbol; - n = u128((n - 1n) / 26n); - } - - return symbol; - } -} diff --git a/frontend/src/app/shared/ord/rune/runeid.ts b/frontend/src/app/shared/ord/rune/runeid.ts deleted file mode 100644 index ca0e938b7..000000000 --- a/frontend/src/app/shared/ord/rune/runeid.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { None, Option, Some } from './monads'; -import { u64, u32, u128 } from './integer'; - -export class RuneId { - constructor(readonly block: u64, readonly tx: u32) {} - - static new(block: u64, tx: u32): Option { - const id = new RuneId(block, tx); - - if (id.block === 0n && id.tx > 0) { - return None; - } - - return Some(id); - } - - static sort(runeIds: RuneId[]): RuneId[] { - return [...runeIds].sort((x, y) => Number(x.block - y.block || x.tx - y.tx)); - } - - delta(next: RuneId): Option<[u128, u128]> { - const optionBlock = u64.checkedSub(next.block, this.block); - if (optionBlock.isNone()) { - return None; - } - const block = optionBlock.unwrap(); - - let tx: u32; - if (block === 0n) { - const optionTx = u32.checkedSub(next.tx, this.tx); - if (optionTx.isNone()) { - return None; - } - tx = optionTx.unwrap(); - } else { - tx = next.tx; - } - - return Some([u128(block), u128(tx)]); - } - - next(block: u128, tx: u128): Option { - const optionBlock = u128.tryIntoU64(block); - const optionTx = u128.tryIntoU32(tx); - - if (optionBlock.isNone() || optionTx.isNone()) { - return None; - } - - const blockU64 = optionBlock.unwrap(); - const txU32 = optionTx.unwrap(); - - const nextBlock = u64.checkedAdd(this.block, blockU64); - if (nextBlock.isNone()) { - return None; - } - - let nextTx: u32; - if (blockU64 === 0n) { - const optionAdd = u32.checkedAdd(this.tx, txU32); - if (optionAdd.isNone()) { - return None; - } - - nextTx = optionAdd.unwrap(); - } else { - nextTx = txU32; - } - - return RuneId.new(nextBlock.unwrap(), nextTx); - } - - toString() { - return `${this.block}:${this.tx}`; - } - - static fromString(s: string) { - const parts = s.split(':'); - if (parts.length !== 2) { - throw new Error(`invalid rune ID: ${s}`); - } - - const [block, tx] = parts; - if (!/^\d+$/.test(block) || !/^\d+$/.test(tx)) { - throw new Error(`invalid rune ID: ${s}`); - } - return new RuneId(u64(BigInt(block)), u32(BigInt(tx))); - } -} diff --git a/frontend/src/app/shared/ord/rune/runestone.ts b/frontend/src/app/shared/ord/rune/runestone.ts deleted file mode 100644 index c71cdcd90..000000000 --- a/frontend/src/app/shared/ord/rune/runestone.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { concatUint8Arrays, hexToBytes } from '../inscription.utils'; -import { Artifact } from './artifact'; -import { Cenotaph } from './cenotaph'; -import { MAGIC_NUMBER, MAX_DIVISIBILITY, OP_RETURN } from './constants'; -import { Edict } from './edict'; -import { Etching } from './etching'; -import { Flag } from './flag'; -import { Flaw } from './flaw'; -import { u128, u32, u64, u8 } from './integer'; -import { Message } from './message'; -import { None, Option, Some } from './monads'; -import { Rune } from './rune'; -import { RuneId } from './runeid'; -import { script } from './script'; -import { SeekArray } from './seekarray'; -import { Tag } from './tag'; - -export const MAX_SPACERS = 0b00000111_11111111_11111111_11111111; - -export const UNCOMMON_GOODS = new Etching( - Some(u8(0)), - Some(new Rune(u128(2055900680524219742n))), - Some(u32(128)), - Some('⧉'), - Some({ - amount: Some(u128(1)), - cap: Some(u128(340282366920938463463374607431768211455n)), - height: [Some(u64(840000)), Some(u64(1050000))], - offset: [Some(u64(0)), Some(u64(0))], - }), - Some(u128(0)), - false -); - -// New: Esplora format instead of Bitcoin RPC format -export type RunestoneTx = { - vout: { - scriptpubkey: string - }[]; -}; - -type Payload = Uint8Array | Flaw; - -export class Runestone { - readonly type = 'runestone'; - - constructor( - readonly mint: Option, - readonly pointer: Option, - readonly edicts: Edict[], - readonly etching: Option - ) {} - - static decipher(transaction: RunestoneTx): Option { - const optionPayload = Runestone.payload(transaction); - if (optionPayload.isNone()) { - return None; - } - const payload = optionPayload.unwrap(); - if (!(payload instanceof Uint8Array)) { - return Some(new Cenotaph([payload])); - } - - const optionIntegers = Runestone.integers(payload); - if (optionIntegers.isNone()) { - return Some(new Cenotaph([Flaw.VARINT])); - } - - const { flaws, edicts, fields } = Message.fromIntegers( - transaction.vout.length, - optionIntegers.unwrap() - ); - - let flags = Tag.take(Tag.FLAGS, fields, 1, ([value]) => Some(value)).unwrapOr(u128(0)); - - const etchingResult = Flag.take(flags, Flag.ETCHING); - const etchingFlag = etchingResult.set; - flags = etchingResult.flags; - - const etching: Option = etchingFlag - ? (() => { - const divisibility = Tag.take( - Tag.DIVISIBILITY, - fields, - 1, - ([value]): Option => - u128 - .tryIntoU8(value) - .andThen((value) => (value <= MAX_DIVISIBILITY ? Some(value) : None)) - ); - - const rune = Tag.take(Tag.RUNE, fields, 1, ([value]) => Some(new Rune(value))); - - const spacers = Tag.take( - Tag.SPACERS, - fields, - 1, - ([value]): Option => - u128.tryIntoU32(value).andThen((value) => (value <= MAX_SPACERS ? Some(value) : None)) - ); - - const symbol = Tag.take(Tag.SYMBOL, fields, 1, ([value]) => - u128.tryIntoU32(value).andThen((value) => { - try { - return Some(String.fromCodePoint(Number(value))); - } catch (e) { - return None; - } - }) - ); - - const termsResult = Flag.take(flags, Flag.TERMS); - const termsFlag = termsResult.set; - flags = termsResult.flags; - - const terms = termsFlag - ? (() => { - const amount = Tag.take(Tag.AMOUNT, fields, 1, ([value]) => Some(value)); - - const cap = Tag.take(Tag.CAP, fields, 1, ([value]) => Some(value)); - - const offset = [ - Tag.take(Tag.OFFSET_START, fields, 1, ([value]) => u128.tryIntoU64(value)), - Tag.take(Tag.OFFSET_END, fields, 1, ([value]) => u128.tryIntoU64(value)), - ] as const; - - const height = [ - Tag.take(Tag.HEIGHT_START, fields, 1, ([value]) => u128.tryIntoU64(value)), - Tag.take(Tag.HEIGHT_END, fields, 1, ([value]) => u128.tryIntoU64(value)), - ] as const; - - return Some({ amount, cap, offset, height }); - })() - : None; - - const premine = Tag.take(Tag.PREMINE, fields, 1, ([value]) => Some(value)); - - const turboResult = Flag.take(flags, Flag.TURBO); - const turbo = etchingResult.set; - flags = turboResult.flags; - - return Some(new Etching(divisibility, rune, spacers, symbol, terms, premine, turbo)); - })() - : None; - - const mint = Tag.take(Tag.MINT, fields, 2, ([block, tx]): Option => { - const optionBlockU64 = u128.tryIntoU64(block); - const optionTxU32 = u128.tryIntoU32(tx); - - if (optionBlockU64.isNone() || optionTxU32.isNone()) { - return None; - } - - return RuneId.new(optionBlockU64.unwrap(), optionTxU32.unwrap()); - }); - - const pointer = Tag.take( - Tag.POINTER, - fields, - 1, - ([value]): Option => - u128 - .tryIntoU32(value) - .andThen((value) => (value < transaction.vout.length ? Some(value) : None)) - ); - - if (etching.map((etching) => etching.supply.isNone()).unwrapOr(false)) { - flaws.push(Flaw.SUPPLY_OVERFLOW); - } - - if (flags !== 0n) { - flaws.push(Flaw.UNRECOGNIZED_FLAG); - } - - if ([...fields.keys()].find((tag) => tag % 2n === 0n) !== undefined) { - flaws.push(Flaw.UNRECOGNIZED_EVEN_TAG); - } - - if (flaws.length !== 0) { - return Some( - new Cenotaph( - flaws, - etching.andThen((etching) => etching.rune), - mint - ) - ); - } - - return Some(new Runestone(mint, pointer, edicts, etching)); - } - - static payload(transaction: RunestoneTx): Option { - // search transaction outputs for payload - for (const output of transaction.vout) { - const instructions = script.decompile(hexToBytes(output.scriptpubkey)); - if (instructions === null) { - throw new Error('unable to decompile'); - } - - // payload starts with OP_RETURN - let nextInstructionResult = instructions.next(); - if (nextInstructionResult.done || nextInstructionResult.value !== OP_RETURN) { - continue; - } - - // followed by the protocol identifier - nextInstructionResult = instructions.next(); - if ( - nextInstructionResult.done || - nextInstructionResult.value instanceof Uint8Array || - nextInstructionResult.value !== MAGIC_NUMBER - ) { - continue; - } - - // construct the payload by concatinating remaining data pushes - let payloads: Uint8Array[] = []; - - do { - nextInstructionResult = instructions.next(); - - if (nextInstructionResult.done) { - const decodedSuccessfully = nextInstructionResult.value; - if (!decodedSuccessfully) { - return Some(Flaw.INVALID_SCRIPT); - } - break; - } - - const instruction = nextInstructionResult.value; - if (instruction instanceof Uint8Array) { - payloads.push(instruction); - } else { - return Some(Flaw.OPCODE); - } - } while (true); - - return Some(concatUint8Arrays(payloads)); - } - - return None; - } - - static integers(payload: Uint8Array): Option { - const integers: u128[] = []; - - const seekArray = new SeekArray(payload); - while (!seekArray.isFinished()) { - const optionInt = u128.decodeVarInt(seekArray); - if (optionInt.isNone()) { - return None; - } - integers.push(optionInt.unwrap()); - } - - return Some(integers); - } -} diff --git a/frontend/src/app/shared/ord/rune/script.ts b/frontend/src/app/shared/ord/rune/script.ts deleted file mode 100644 index 67d579ab8..000000000 --- a/frontend/src/app/shared/ord/rune/script.ts +++ /dev/null @@ -1,237 +0,0 @@ -namespace pushdata { - /** - * Calculates the encoding length of a number used for push data in Bitcoin transactions. - * @param i The number to calculate the encoding length for. - * @returns The encoding length of the number. - */ - export function encodingLength(i: number): number { - return i < OPS.OP_PUSHDATA1 ? 1 : i <= 0xff ? 2 : i <= 0xffff ? 3 : 5; - } - - /** - * Decodes a byte array and returns information about the opcode, number, and size. - * @param array - The byte array to decode. - * @param offset - The offset within the array to start decoding. - * @returns An object containing the opcode, number, and size, or null if decoding fails. - */ - export function decode( - array: Uint8Array, - offset: number - ): { - opcode: number; - number: number; - size: number; - } | null { - const dataView = new DataView(array.buffer, array.byteOffset, array.byteLength); - const opcode = dataView.getUint8(offset); - let num: number; - let size: number; - - // ~6 bit - if (opcode < OPS.OP_PUSHDATA1) { - num = opcode; - size = 1; - - // 8 bit - } else if (opcode === OPS.OP_PUSHDATA1) { - if (offset + 2 > array.length) return null; - num = dataView.getUint8(offset + 1); - size = 2; - - // 16 bit - } else if (opcode === OPS.OP_PUSHDATA2) { - if (offset + 3 > array.length) return null; - num = dataView.getUint16(offset + 1, true); // true for little-endian - size = 3; - - // 32 bit - } else { - if (offset + 5 > array.length) return null; - if (opcode !== OPS.OP_PUSHDATA4) throw new Error('Unexpected opcode'); - - num = dataView.getUint32(offset + 1, true); // true for little-endian - size = 5; - } - - return { - opcode, - number: num, - size, - }; - } -} - -const OPS = { - OP_FALSE: 0, - OP_0: 0, - OP_PUSHDATA1: 76, - OP_PUSHDATA2: 77, - OP_PUSHDATA4: 78, - OP_1NEGATE: 79, - OP_RESERVED: 80, - OP_TRUE: 81, - OP_1: 81, - OP_2: 82, - OP_3: 83, - OP_4: 84, - OP_5: 85, - OP_6: 86, - OP_7: 87, - OP_8: 88, - OP_9: 89, - OP_10: 90, - OP_11: 91, - OP_12: 92, - OP_13: 93, - OP_14: 94, - OP_15: 95, - OP_16: 96, - - OP_NOP: 97, - OP_VER: 98, - OP_IF: 99, - OP_NOTIF: 100, - OP_VERIF: 101, - OP_VERNOTIF: 102, - OP_ELSE: 103, - OP_ENDIF: 104, - OP_VERIFY: 105, - OP_RETURN: 106, - - OP_TOALTSTACK: 107, - OP_FROMALTSTACK: 108, - OP_2DROP: 109, - OP_2DUP: 110, - OP_3DUP: 111, - OP_2OVER: 112, - OP_2ROT: 113, - OP_2SWAP: 114, - OP_IFDUP: 115, - OP_DEPTH: 116, - OP_DROP: 117, - OP_DUP: 118, - OP_NIP: 119, - OP_OVER: 120, - OP_PICK: 121, - OP_ROLL: 122, - OP_ROT: 123, - OP_SWAP: 124, - OP_TUCK: 125, - - OP_CAT: 126, - OP_SUBSTR: 127, - OP_LEFT: 128, - OP_RIGHT: 129, - OP_SIZE: 130, - - OP_INVERT: 131, - OP_AND: 132, - OP_OR: 133, - OP_XOR: 134, - OP_EQUAL: 135, - OP_EQUALVERIFY: 136, - OP_RESERVED1: 137, - OP_RESERVED2: 138, - - OP_1ADD: 139, - OP_1SUB: 140, - OP_2MUL: 141, - OP_2DIV: 142, - OP_NEGATE: 143, - OP_ABS: 144, - OP_NOT: 145, - OP_0NOTEQUAL: 146, - OP_ADD: 147, - OP_SUB: 148, - OP_MUL: 149, - OP_DIV: 150, - OP_MOD: 151, - OP_LSHIFT: 152, - OP_RSHIFT: 153, - - OP_BOOLAND: 154, - OP_BOOLOR: 155, - OP_NUMEQUAL: 156, - OP_NUMEQUALVERIFY: 157, - OP_NUMNOTEQUAL: 158, - OP_LESSTHAN: 159, - OP_GREATERTHAN: 160, - OP_LESSTHANOREQUAL: 161, - OP_GREATERTHANOREQUAL: 162, - OP_MIN: 163, - OP_MAX: 164, - - OP_WITHIN: 165, - - OP_RIPEMD160: 166, - OP_SHA1: 167, - OP_SHA256: 168, - OP_HASH160: 169, - OP_HASH256: 170, - OP_CODESEPARATOR: 171, - OP_CHECKSIG: 172, - OP_CHECKSIGVERIFY: 173, - OP_CHECKMULTISIG: 174, - OP_CHECKMULTISIGVERIFY: 175, - - OP_NOP1: 176, - - OP_NOP2: 177, - OP_CHECKLOCKTIMEVERIFY: 177, - - OP_NOP3: 178, - OP_CHECKSEQUENCEVERIFY: 178, - - OP_NOP4: 179, - OP_NOP5: 180, - OP_NOP6: 181, - OP_NOP7: 182, - OP_NOP8: 183, - OP_NOP9: 184, - OP_NOP10: 185, - - OP_CHECKSIGADD: 186, - - OP_PUBKEYHASH: 253, - OP_PUBKEY: 254, - OP_INVALIDOPCODE: 255, -} as const; - -export const opcodes = OPS; - -export namespace script { - export type Instruction = number | Uint8Array; - - export function* decompile(array: Uint8Array): Generator { - let i = 0; - - while (i < array.length) { - const opcode = array[i]; - - // data chunk - if (opcode >= OPS.OP_0 && opcode <= OPS.OP_PUSHDATA4) { - const d = pushdata.decode(array, i); - - // did reading a pushDataInt fail? - if (d === null) return false; - i += d.size; - - // attempt to read too much data? - if (i + d.number > array.length) return false; - - const data = array.subarray(i, i + d.number); - i += d.number; - - yield data; - - // opcode - } else { - yield opcode; - - i += 1; - } - } - - return true; - } -} diff --git a/frontend/src/app/shared/ord/rune/seekarray.ts b/frontend/src/app/shared/ord/rune/seekarray.ts deleted file mode 100644 index 1f465cbd3..000000000 --- a/frontend/src/app/shared/ord/rune/seekarray.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * This class provides a way to read data sequentially from a Uint8Array with automatic cursor management. - * It utilizes DataView for handling multi-byte data types. - * - * This replaces the SeekBuffer from the original runestone-lib! - */ -export class SeekArray { - - public seekIndex: number = 0; - private dataView: DataView; - - /** - * Constructs a SeekArray instance. - * - * @param array - The Uint8Array from which data will be read. - */ - constructor(private array: Uint8Array) { - this.dataView = new DataView(array.buffer, array.byteOffset, array.byteLength); - } - - /** - * Reads an unsigned 8-bit integer from the current position and advances the seek index by 1 byte. - * - * @returns The read value or undefined if reading beyond the end of the array. - */ - readUInt8(): number | undefined { - if (this.isFinished()) { - return undefined; - } - const value = this.dataView.getUint8(this.seekIndex); - this.seekIndex += 1; - return value; - } - - /** - * Checks if the seek index has reached or surpassed the length of the underlying array. - * - * @returns true if there are no more bytes to read, false otherwise. - */ - isFinished(): boolean { - return this.seekIndex >= this.array.length; - } -} diff --git a/frontend/src/app/shared/ord/rune/spacedrune.ts b/frontend/src/app/shared/ord/rune/spacedrune.ts deleted file mode 100644 index b00b0da3a..000000000 --- a/frontend/src/app/shared/ord/rune/spacedrune.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Rune } from './rune'; - -export class SpacedRune { - constructor(readonly rune: Rune, readonly spacers: number) {} - - toString(): string { - const rune = this.rune.toString(); - let i = 0; - let result = ''; - for (const c of rune) { - result += c; - - if (i < rune.length - 1 && (this.spacers & (1 << i)) !== 0) { - result += '•'; - } - i++; - } - - return result; - } -} diff --git a/frontend/src/app/shared/ord/rune/tag.ts b/frontend/src/app/shared/ord/rune/tag.ts deleted file mode 100644 index 8e39925d4..000000000 --- a/frontend/src/app/shared/ord/rune/tag.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { None, Option, Some } from './monads'; -import { u128 } from './integer'; -import { FixedArray } from './utils'; - -export enum Tag { - BODY = 0, - FLAGS = 2, - RUNE = 4, - - PREMINE = 6, - CAP = 8, - AMOUNT = 10, - HEIGHT_START = 12, - HEIGHT_END = 14, - OFFSET_START = 16, - OFFSET_END = 18, - MINT = 20, - POINTER = 22, - CENOTAPH = 126, - - DIVISIBILITY = 1, - SPACERS = 3, - SYMBOL = 5, - NOP = 127, -} - -export namespace Tag { - export function take( - tag: Tag, - fields: Map, - n: N, - withFn: (values: FixedArray) => Option - ): Option { - const field = fields.get(u128(tag)); - if (field === undefined) { - return None; - } - - const values: u128[] = []; - for (const i of [...Array(n).keys()]) { - if (field[i] === undefined) { - return None; - } - values[i] = field[i]; - } - - const optionValue = withFn(values as FixedArray); - if (optionValue.isNone()) { - return None; - } - - field.splice(0, n); - - if (field.length === 0) { - fields.delete(u128(tag)); - } - - return Some(optionValue.unwrap()); - } -} diff --git a/frontend/src/app/shared/ord/rune/terms.ts b/frontend/src/app/shared/ord/rune/terms.ts deleted file mode 100644 index 464c166e0..000000000 --- a/frontend/src/app/shared/ord/rune/terms.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Option } from './monads'; -import { u128, u64 } from './integer'; - -export type Terms = { - amount: Option; - cap: Option; - height: readonly [Option, Option]; - offset: readonly [Option, Option]; -}; diff --git a/frontend/src/app/shared/ord/rune/utils.ts b/frontend/src/app/shared/ord/rune/utils.ts deleted file mode 100644 index a6fa8e0a1..000000000 --- a/frontend/src/app/shared/ord/rune/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -type GrowToSize = A['length'] extends N - ? A - : GrowToSize; - -export type FixedArray = GrowToSize; -