259 lines
5.9 KiB
TypeScript
259 lines
5.9 KiB
TypeScript
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;
|
|
}
|
|
}
|