UNPKG

@vbyte/btc-dev

Version:

Batteries-included toolset for plebian bitcoin development

196 lines (195 loc) 6.24 kB
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; }