@vbyte/btc-dev
Version:
Batteries-included toolset for plebian bitcoin development
196 lines (195 loc) • 6.24 kB
JavaScript
import { Buff, Stream } from '@vbyte/buff';
import { Assert } from '@vbyte/micro-lib';
import { encode_script } from '../../lib/script/encode.js';
import { decode_script } from '../../lib/script/decode.js';
const _0n = BigInt(0);
const _1n = BigInt(1);
const _26n = BigInt(26);
export var InscriptionUtil;
(function (InscriptionUtil) {
InscriptionUtil.encode = encode_inscription;
InscriptionUtil.decode = decode_inscription;
})(InscriptionUtil || (InscriptionUtil = {}));
export function decode_inscription(script) {
const envelopes = parse_envelopes(script);
return envelopes.map(parse_record);
}
export function encode_inscription(data) {
return Buff.join(data.map(create_envelope));
}
function create_envelope(data) {
let asm = ['OP_0', 'OP_IF', '6f7264'];
if (typeof data.delegate === 'string') {
const id = encode_id(data.delegate);
asm.push('OP_11', id);
}
if (typeof data.ref === 'string') {
asm.push('OP_WITHIN', data.ref);
}
if (typeof data.parent === 'string') {
const id = encode_id(data.parent);
asm.push('OP_3', id);
}
if (typeof data.opcode === 'number') {
const code = encode_pointer(data.opcode);
asm.push('OP_NOP', code);
}
if (typeof data.pointer === 'number') {
const ptr = encode_pointer(data.pointer);
asm.push('OP_2', ptr);
}
if (typeof data.rune === 'string') {
const label = encode_rune_label(data.rune);
asm.push('OP_13', label);
}
if (typeof data.mimetype === 'string') {
const label = encode_label(data.mimetype);
asm.push('OP_1', label);
}
if (typeof data.content === 'string') {
const chunks = encode_content(data.content);
asm.push('OP_0', ...chunks);
}
asm.push('OP_ENDIF');
return encode_script(asm);
}
function parse_envelopes(script) {
const words = decode_script(script);
const start_idx = words.findIndex(e => e === 'OP_0');
Assert.ok(start_idx !== -1, 'inscription envelope not found');
const envelopes = [];
for (let idx = start_idx; idx < words.length; idx++) {
Assert.ok(words[idx + 1] === 'OP_IF', 'OP_IF missing from envelope');
Assert.ok(words[idx + 2] === '6f7264', 'magic bytes missing from envelope');
const stop_idx = words.findIndex(e => e === 'OP_ENDIF');
Assert.ok(stop_idx !== -1, 'inscription envelope missing END_IF statement');
const env = words.slice(idx + 3, stop_idx);
envelopes.push(env);
idx += stop_idx;
}
return envelopes;
}
function parse_record(envelope) {
const record = {};
for (let i = 0; i < envelope.length; i++) {
switch (envelope[i]) {
case 'OP_1':
record.mimetype = decode_label(envelope[i + 1]);
i += 1;
break;
case 'OP_2':
record.pointer = decode_pointer(envelope[i + 1]);
i += 1;
break;
case 'OP_3':
record.parent = decode_id(envelope[i + 1]);
i += 1;
break;
case 'OP_11':
record.delegate = decode_id(envelope[i + 1]);
i += 1;
break;
case 'OP_13':
record.rune = decode_rune_label(envelope[i + 1]);
i += 1;
break;
case 'OP_WITHIN':
record.ref = decode_bytes(envelope[i + 1]);
i += 1;
break;
case 'OP_NOP':
record.opcode = decode_pointer(envelope[i + 1]);
i += 1;
break;
case 'OP_0':
record.content = decode_content(envelope.slice(i + 1));
return record;
}
}
return record;
}
function decode_bytes(bytes) {
return Buff.bytes(bytes).hex;
}
function encode_id(identifier) {
Assert.ok(identifier.includes('i'), 'identifier must include an index');
const parts = identifier.split('i');
const bytes = Buff.hex(parts[0]);
const idx = Number(parts[1]);
const txid = bytes.reverse().hex;
return (idx !== 0) ? txid + Buff.num(idx).hex : txid;
}
function decode_id(identifier) {
const bytes = Buff.bytes(identifier);
const idx = bytes.at(-1) ?? 0;
const txid = bytes.slice(0, -1).reverse().hex;
return txid + 'i' + String(idx);
}
function encode_pointer(pointer) {
return Buff.num(pointer).reverse().hex;
}
function decode_pointer(bytes) {
return Buff.bytes(bytes).reverse().num;
}
function encode_label(label) {
return Buff.str(label).hex;
}
function decode_label(label) {
return Buff.bytes(label).str;
}
function encode_content(content) {
const bytes = Buff.is_hex(content)
? Buff.hex(content)
: Buff.str(content);
const stream = new Stream(bytes);
const chunks = [];
while (stream.size > 0) {
if (stream.size > 520) {
const chunk = stream.read(520);
chunks.push(chunk.hex);
}
else {
const chunk = stream.read(stream.size);
chunks.push(chunk.hex);
}
}
return chunks;
}
function decode_content(chunks, format = 'hex') {
const data = Buff.join(chunks);
return (format === 'hex')
? data.hex
: data.str;
}
function encode_rune_label(label) {
const str = label.toUpperCase();
let big = _0n;
for (const char of str) {
if (char >= 'A' && char <= 'Z') {
big = big * _26n + BigInt(char.charCodeAt(0) - ('A'.charCodeAt(0) - 1));
}
else {
continue;
}
}
big = big - _1n;
return Buff.big(big).reverse().hex;
}
function decode_rune_label(label) {
let big = Buff.bytes(label).reverse().big;
big = big + _1n;
let result = '';
while (big > _0n) {
const mod = big % _26n;
if (mod === _0n) {
result = 'Z' + result;
big = big / _26n - _1n;
}
else {
const charCode = Number(mod) + 'A'.charCodeAt(0) - 1;
result = String.fromCharCode(charCode) + result;
big = big / _26n;
}
}
return result;
}