replace rune parsing dependencies with minimal reimplementation
This commit is contained in:
parent
8b6db768cd
commit
acae5a33b0
@ -7,23 +7,23 @@
|
|||||||
<ng-container i18n="ord.mint-n-runes">
|
<ng-container i18n="ord.mint-n-runes">
|
||||||
<span>Mint</span>
|
<span>Mint</span>
|
||||||
<span class="amount"> {{ minted >= 100000 ? (minted | amountShortener:undefined:undefined:true) : minted }} </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 *ngTemplateOutlet="runeName; context: { $implicit: runestone.mint.toString() }"></ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
}
|
}
|
||||||
@if (totalSupply > -1) {
|
@if (runestone?.etching?.supply) {
|
||||||
@if (premined > 0) {
|
@if (runestone?.etching.premine > 0) {
|
||||||
<ng-container i18n="ord.premine-n-runes">
|
<ng-container i18n="ord.premine-n-runes">
|
||||||
<span>Premine</span>
|
<span>Premine</span>
|
||||||
<span class="amount"> {{ premined >= 100000 ? (premined | amountShortener:undefined:undefined:true) : premined }} </span>
|
<span class="amount"> {{ runestone.etching.premine >= 100000 ? (toNumber(runestone.etching.premine) | amountShortener:undefined:undefined:true) : runestone.etching.premine }} </span>
|
||||||
{{ etchedSymbol }}
|
{{ runestone.etching.symbol }}
|
||||||
<span class="name">{{ etchedName }}</span>
|
<span class="name">{{ runestone.etching.spacedName }}</span>
|
||||||
<span> ({{ premined / totalSupply * 100 | amountShortener:0}}% of total supply)</span>
|
<span> ({{ toNumber(runestone.etching.premine) / toNumber(runestone.etching.supply) * 100 | amountShortener:0}}% of total supply)</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
} @else {
|
} @else {
|
||||||
<ng-container i18n="ord.etch-rune">
|
<ng-container i18n="ord.etch-rune">
|
||||||
<span>Etching of</span>
|
<span>Etching of</span>
|
||||||
{{ etchedSymbol }}
|
{{ runestone.etching.symbol }}
|
||||||
<span class="name">{{ etchedName }}</span>
|
<span class="name">{{ runestone.etching.spacedName }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,12 +36,6 @@
|
|||||||
</div>
|
</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') {
|
@if (inscriptions?.length && type === 'vin') {
|
||||||
<div *ngFor="let contentType of inscriptionsData | keyvalue">
|
<div *ngFor="let contentType of inscriptionsData | keyvalue">
|
||||||
<div>
|
<div>
|
||||||
@ -68,8 +62,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<ng-template #runeName let-id>
|
<ng-template #runeName let-id>
|
||||||
{{ runeInfo[id]?.etching.symbol.isSome() ? runeInfo[id]?.etching.symbol.unwrap() : '' }}
|
{{ runeInfo[id]?.etching.symbol || '' }}
|
||||||
<a [routerLink]="id !== '1:0' ? ['/tx' | relativeUrl, runeInfo[id]?.txid] : null" [class.rune-link]="id !== '1:0'" [class.disabled]="id === '1:0'">
|
<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>
|
<span class="name">{{ runeInfo[id]?.etching.spacedName }}</span>
|
||||||
</a>
|
</a>
|
||||||
</ng-template>
|
</ng-template>
|
@ -1,9 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
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 { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { SpacedRune } from '../../shared/ord/rune/spacedrune';
|
import { Runestone, Etching } from '../../shared/ord/rune.utils';
|
||||||
|
|
||||||
export interface Inscription {
|
export interface Inscription {
|
||||||
body?: Uint8Array;
|
body?: Uint8Array;
|
||||||
@ -22,79 +19,34 @@ export interface Inscription {
|
|||||||
export class OrdDataComponent implements OnChanges {
|
export class OrdDataComponent implements OnChanges {
|
||||||
@Input() inscriptions: Inscription[];
|
@Input() inscriptions: Inscription[];
|
||||||
@Input() runestone: Runestone;
|
@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() error: HttpErrorResponse;
|
||||||
@Input() type: 'vin' | 'vout';
|
@Input() type: 'vin' | 'vout';
|
||||||
|
|
||||||
|
toNumber = (value: bigint): number => Number(value);
|
||||||
|
|
||||||
// Inscriptions
|
// Inscriptions
|
||||||
inscriptionsData: { [key: string]: { count: number, totalSize: number, text?: string; json?: JSON; tag?: string; delegate?: string } };
|
inscriptionsData: { [key: string]: { count: number, totalSize: number, text?: string; json?: JSON; tag?: string; delegate?: string } };
|
||||||
// Rune mints
|
// Rune mints
|
||||||
minted: number;
|
minted: number;
|
||||||
// Rune etching
|
|
||||||
premined: number = -1;
|
|
||||||
totalSupply: number = -1;
|
|
||||||
etchedName: string;
|
|
||||||
etchedSymbol: string;
|
|
||||||
// Rune transfers
|
// Rune transfers
|
||||||
transferredRunes: { key: string; etching: Etching; txid: string; name?: string; }[] = [];
|
transferredRunes: { key: string; etching: Etching; txid: string }[] = [];
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (changes.runestone && this.runestone) {
|
if (changes.runestone && this.runestone) {
|
||||||
|
this.transferredRunes = Object.entries(this.runeInfo).map(([key, runeInfo]) => ({ key, ...runeInfo }));
|
||||||
Object.keys(this.runeInfo).forEach((key) => {
|
if (this.runestone.mint && this.runeInfo[this.runestone.mint.toString()]) {
|
||||||
const rune = this.runeInfo[key].etching.rune.isSome() ? this.runeInfo[key].etching.rune.unwrap() : null;
|
const mint = this.runestone.mint.toString();
|
||||||
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);
|
this.transferredRunes = this.transferredRunes.filter(rune => rune.key !== mint);
|
||||||
const terms = this.runeInfo[mint].etching.terms.isSome() ? this.runeInfo[mint].etching.terms.unwrap() : null;
|
const terms = this.runeInfo[mint].etching.terms;
|
||||||
let amount: u128;
|
const amount = terms?.amount;
|
||||||
if (terms) {
|
const divisibility = this.runeInfo[mint].etching.divisibility;
|
||||||
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) {
|
if (amount) {
|
||||||
this.minted = this.getAmount(amount, divisibility);
|
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 (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 divisor = BigInt(10) ** BigInt(divisibility);
|
||||||
const result = amount / divisor;
|
const result = amount / divisor;
|
||||||
|
|
||||||
|
@ -6,15 +6,14 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter
|
|||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { AssetsService } from '../../services/assets.service';
|
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 { BlockExtended } from '../../interfaces/node-api.interface';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { PriceService } from '../../services/price.service';
|
import { PriceService } from '../../services/price.service';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { OrdApiService } from '../../services/ord-api.service';
|
import { OrdApiService } from '../../services/ord-api.service';
|
||||||
import { Inscription } from '../ord-data/ord-data.component';
|
import { Inscription } from '../ord-data/ord-data.component';
|
||||||
import { Runestone } from '../../shared/ord/rune/runestone';
|
import { Etching, Runestone } from '../../shared/ord/rune.utils';
|
||||||
import { Etching } from '../../shared/ord/rune/etching';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transactions-list',
|
selector: 'app-transactions-list',
|
||||||
@ -261,7 +260,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
tx.vout[i].isRunestone = true;
|
tx.vout[i].isRunestone = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,10 +3,9 @@ import { catchError, forkJoin, map, Observable, of, switchMap, tap } from 'rxjs'
|
|||||||
import { Inscription } from '../components/ord-data/ord-data.component';
|
import { Inscription } from '../components/ord-data/ord-data.component';
|
||||||
import { Transaction } from '../interfaces/electrs.interface';
|
import { Transaction } from '../interfaces/electrs.interface';
|
||||||
import { getNextInscriptionMark, hexToBytes, extractInscriptionData } from '../shared/ord/inscription.utils';
|
import { getNextInscriptionMark, hexToBytes, extractInscriptionData } from '../shared/ord/inscription.utils';
|
||||||
import { Runestone } from '../shared/ord/rune/runestone';
|
import { decipherRunestone, Runestone, Etching, UNCOMMON_GOODS } from '../shared/ord/rune.utils';
|
||||||
import { Etching } from '../shared/ord/rune/etching';
|
|
||||||
import { ElectrsApiService } from './electrs-api.service';
|
import { ElectrsApiService } from './electrs-api.service';
|
||||||
import { UNCOMMON_GOODS } from '../shared/ord/rune/runestone';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -18,27 +17,16 @@ export class OrdApiService {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
decodeRunestone$(tx: Transaction): Observable<{ runestone: Runestone, runeInfo: { [id: string]: { etching: Etching; txid: string; } } }> {
|
decodeRunestone$(tx: Transaction): Observable<{ runestone: Runestone, runeInfo: { [id: string]: { etching: Etching; txid: string; } } }> {
|
||||||
const runestoneTx = { vout: tx.vout.map(vout => ({ scriptpubkey: vout.scriptpubkey })) };
|
const runestone = decipherRunestone(tx);
|
||||||
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 runeInfo: { [id: string]: { etching: Etching; txid: string; } } = {};
|
const runeInfo: { [id: string]: { etching: Etching; txid: string; } } = {};
|
||||||
const runesToFetch: Set<string> = new Set();
|
const runesToFetch: Set<string> = new Set();
|
||||||
|
|
||||||
if (runestone) {
|
if (runestone) {
|
||||||
if (runestone.mint.isSome()) {
|
if (runestone.mint) {
|
||||||
const mint = runestone.mint.unwrap().toString();
|
if (runestone.mint.toString() === '1:0') {
|
||||||
|
runeInfo[runestone.mint.toString()] = { etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' };
|
||||||
if (mint === '1:0') {
|
|
||||||
runeInfo[mint] = { etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' };
|
|
||||||
} else {
|
} 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
|
// 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(blockHash => this.electrsApiService.getBlockTxId$(blockHash, parseInt(txIndex))),
|
||||||
switchMap(txId => this.electrsApiService.getTransaction$(txId)),
|
switchMap(txId => this.electrsApiService.getTransaction$(txId)),
|
||||||
switchMap(tx => {
|
switchMap(tx => {
|
||||||
const decipheredMessage = Runestone.decipher(tx);
|
const runestone = decipherRunestone(tx);
|
||||||
if (decipheredMessage.isSome()) {
|
if (runestone) {
|
||||||
const message = decipheredMessage.unwrap();
|
const etching = runestone.etching;
|
||||||
if (message?.type === 'runestone' && message.etching.isSome()) {
|
if (etching) {
|
||||||
return of({ etching: message.etching.unwrap(), txid: tx.txid });
|
return of({ etching, txid: tx.txid });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return of(null);
|
return of(null);
|
||||||
|
258
frontend/src/app/shared/ord/rune.utils.ts
Normal file
258
frontend/src/app/shared/ord/rune.utils.ts
Normal file
@ -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<number, bigint[]>;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +0,0 @@
|
|||||||
import { Cenotaph } from './cenotaph';
|
|
||||||
import { Runestone } from './runestone';
|
|
||||||
|
|
||||||
export type Artifact = Cenotaph | Runestone;
|
|
@ -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<Rune> = None,
|
|
||||||
readonly mint: Option<RuneId> = None
|
|
||||||
) {}
|
|
||||||
}
|
|
@ -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;
|
|
@ -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<Edict> {
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<string>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly divisibility: Option<u8>,
|
|
||||||
readonly rune: Option<Rune>,
|
|
||||||
readonly spacers: Option<u32>,
|
|
||||||
symbol: Option<string>,
|
|
||||||
readonly terms: Option<Terms>,
|
|
||||||
readonly premine: Option<u128>,
|
|
||||||
readonly turbo: boolean
|
|
||||||
) {
|
|
||||||
this.symbol = symbol.andThen((value) => {
|
|
||||||
const codePoint = value.codePointAt(0);
|
|
||||||
return codePoint !== undefined ? Some(String.fromCodePoint(codePoint)) : None;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get supply(): Option<u128> {
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export { u8 } from './u8';
|
|
||||||
export { u32 } from './u32';
|
|
||||||
export { u64 } from './u64';
|
|
||||||
export { u128 } from './u128';
|
|
@ -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<T> = 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<u128> {
|
|
||||||
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<u128> {
|
|
||||||
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<u128> {
|
|
||||||
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<u128> {
|
|
||||||
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<u64> {
|
|
||||||
return n > u64.MAX ? None : Some(u64(n));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tryIntoU32(n: u128): Option<u32> {
|
|
||||||
return n > u32.MAX ? None : Some(u32(n));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tryIntoU8(n: u128): Option<u8> {
|
|
||||||
return n > u8.MAX ? None : Some(u8(n));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* getAllU128(data: Uint8Array): Generator<u128> {
|
|
||||||
const seekArray = new SeekArray(data);
|
|
||||||
while (!seekArray.isFinished()) {
|
|
||||||
const nextValue = u128.decodeVarInt(seekArray);
|
|
||||||
if (nextValue.isNone()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
yield nextValue.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<T> = 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<u32> {
|
|
||||||
const result = x + y;
|
|
||||||
if (result > u32.MAX) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(u32(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkedSub(x: u32, y: u32): Option<u32> {
|
|
||||||
const result = x - y;
|
|
||||||
if (result < 0n) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(u32(result));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<T> = 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<u64> {
|
|
||||||
const result = x + y;
|
|
||||||
if (result > u64.MAX) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(u64(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkedSub(x: u64, y: u64): Option<u64> {
|
|
||||||
const result = x - y;
|
|
||||||
if (result < 0n) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(u64(result));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<T> = 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<u8> {
|
|
||||||
const result = x + y;
|
|
||||||
if (result > u8.MAX) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(u8(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkedSub(x: u8, y: u8): Option<u8> {
|
|
||||||
const result = x - y;
|
|
||||||
if (result < 0n) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(u8(result));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<u128, u128[]>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
static fromIntegers(numOutputs: number, payload: u128[]): Message {
|
|
||||||
const edicts: Edict[] = [];
|
|
||||||
const fields = new Map<u128, u128[]>();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<A, B> {
|
|
||||||
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<T extends NonUndefined> {
|
|
||||||
/**
|
|
||||||
* 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<U extends NonUndefined>(fn: Match<T, U>): 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<U extends NonUndefined>(fn: (val: T) => U): Option<U>;
|
|
||||||
|
|
||||||
inspect(fn: (val: T) => void): Option<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<U extends NonUndefined>(fn: (val: T) => Option<U>): Option<U>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<T>): Option<T>;
|
|
||||||
|
|
||||||
orElse(optb: () => Option<T>): Option<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<U extends NonUndefined>(optb: Option<U>): Option<U>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<T extends NonUndefined> extends Option<T> {
|
|
||||||
unwrap(): T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of Option representing the absence of a value (None).
|
|
||||||
*/
|
|
||||||
interface NoneOption<T extends NonUndefined> extends Option<T> {
|
|
||||||
unwrap(): never;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a Some value of Option.
|
|
||||||
*/
|
|
||||||
class SomeImpl<T extends NonUndefined> implements SomeOption<T> {
|
|
||||||
constructor(private readonly val: T) {}
|
|
||||||
|
|
||||||
get type() {
|
|
||||||
return OptionType.Some;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSome() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
isNone() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
match<B>(fn: Match<T, B>): B {
|
|
||||||
return fn.some(this.val);
|
|
||||||
}
|
|
||||||
|
|
||||||
map<U extends NonUndefined>(fn: (val: T) => U): Option<U> {
|
|
||||||
return Some(fn(this.val));
|
|
||||||
}
|
|
||||||
|
|
||||||
inspect(fn: (val: T) => void): Option<T> {
|
|
||||||
fn(this.val);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
andThen<U extends NonUndefined>(fn: (val: T) => Option<U>): Option<U> {
|
|
||||||
return fn(this.val);
|
|
||||||
}
|
|
||||||
|
|
||||||
or<U extends NonUndefined>(_optb: Option<U>): Option<T> {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
orElse(optb: () => Option<T>): Option<T> {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
and<U extends NonUndefined>(optb: Option<U>): Option<U> {
|
|
||||||
return optb;
|
|
||||||
}
|
|
||||||
|
|
||||||
unwrapOr(_def: T): T {
|
|
||||||
return this.val;
|
|
||||||
}
|
|
||||||
|
|
||||||
unwrap(): T {
|
|
||||||
return this.val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a None value of Option.
|
|
||||||
*/
|
|
||||||
class NoneImpl<T extends NonUndefined> implements NoneOption<T> {
|
|
||||||
get type() {
|
|
||||||
return OptionType.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSome() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
isNone() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
match<U>({ none }: Match<T, U>): U {
|
|
||||||
if (typeof none === 'function') {
|
|
||||||
return (none as () => U)();
|
|
||||||
}
|
|
||||||
|
|
||||||
return none;
|
|
||||||
}
|
|
||||||
|
|
||||||
map<U extends NonUndefined>(_fn: (val: T) => U): Option<U> {
|
|
||||||
return new NoneImpl<U>();
|
|
||||||
}
|
|
||||||
|
|
||||||
inspect(fn: (val: T) => void): Option<T> {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
andThen<U extends NonUndefined>(_fn: (val: T) => Option<U>): Option<U> {
|
|
||||||
return new NoneImpl<U>();
|
|
||||||
}
|
|
||||||
|
|
||||||
or<U extends NonUndefined>(optb: Option<U>): Option<U> {
|
|
||||||
return optb;
|
|
||||||
}
|
|
||||||
|
|
||||||
orElse(optb: () => Option<T>): Option<T> {
|
|
||||||
return optb();
|
|
||||||
}
|
|
||||||
|
|
||||||
and<U extends NonUndefined>(_optb: Option<U>): Option<U> {
|
|
||||||
return new NoneImpl<U>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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<T extends NonUndefined>(val: T): Option<T> {
|
|
||||||
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<any> = 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<T extends NonUndefined>(val: Option<T>): val is SomeOption<T> {
|
|
||||||
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<T extends NonUndefined>(val: Option<T>): val is NoneOption<T> {
|
|
||||||
return val.isNone();
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<RuneId> {
|
|
||||||
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<RuneId> {
|
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<RuneId>,
|
|
||||||
readonly pointer: Option<u32>,
|
|
||||||
readonly edicts: Edict[],
|
|
||||||
readonly etching: Option<Etching>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
static decipher(transaction: RunestoneTx): Option<Artifact> {
|
|
||||||
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<Etching> = etchingFlag
|
|
||||||
? (() => {
|
|
||||||
const divisibility = Tag.take(
|
|
||||||
Tag.DIVISIBILITY,
|
|
||||||
fields,
|
|
||||||
1,
|
|
||||||
([value]): Option<u8> =>
|
|
||||||
u128
|
|
||||||
.tryIntoU8(value)
|
|
||||||
.andThen<u8>((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<u32> =>
|
|
||||||
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<RuneId> => {
|
|
||||||
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<u32> =>
|
|
||||||
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<Payload> {
|
|
||||||
// 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<u128[]> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Instruction, boolean> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<N extends number, T extends {}>(
|
|
||||||
tag: Tag,
|
|
||||||
fields: Map<u128, u128[]>,
|
|
||||||
n: N,
|
|
||||||
withFn: (values: FixedArray<u128, N>) => Option<T>
|
|
||||||
): Option<T> {
|
|
||||||
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<u128, N>);
|
|
||||||
if (optionValue.isNone()) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
field.splice(0, n);
|
|
||||||
|
|
||||||
if (field.length === 0) {
|
|
||||||
fields.delete(u128(tag));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(optionValue.unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Option } from './monads';
|
|
||||||
import { u128, u64 } from './integer';
|
|
||||||
|
|
||||||
export type Terms = {
|
|
||||||
amount: Option<u128>;
|
|
||||||
cap: Option<u128>;
|
|
||||||
height: readonly [Option<u64>, Option<u64>];
|
|
||||||
offset: readonly [Option<u64>, Option<u64>];
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
type GrowToSize<T, N extends number, A extends T[]> = A['length'] extends N
|
|
||||||
? A
|
|
||||||
: GrowToSize<T, N, [...A, T]>;
|
|
||||||
|
|
||||||
export type FixedArray<T, N extends number> = GrowToSize<T, N, []>;
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user