micro-ordinals
Version:
Manage ordinals, inscriptions and runes using scure-btc-signer
283 lines • 9.78 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.__test__ = exports.OutOrdinalReveal = exports.InscriptionId = void 0;
exports.parseInscriptions = parseInscriptions;
exports.parseWitness = parseWitness;
exports.p2tr_ord_reveal = p2tr_ord_reveal;
const base_1 = require("@scure/base");
const btc_signer_1 = require("@scure/btc-signer");
const P = require("micro-packed");
const cbor_ts_1 = require("./cbor.js");
const PROTOCOL_ID = /* @__PURE__ */ base_1.utf8.decode('ord');
function splitChunks(buf) {
const res = [];
for (let i = 0; i < buf.length; i += btc_signer_1.MAX_SCRIPT_BYTE_LENGTH)
res.push(buf.subarray(i, i + btc_signer_1.MAX_SCRIPT_BYTE_LENGTH));
return res;
}
const RawInscriptionId = /* @__PURE__ */ P.tuple([
P.bytes(32, true),
P.apply(P.bigint(4, true, false, false), P.coders.numberBigint),
]);
exports.InscriptionId = {
encode(data) {
const [txId, index] = data.split('i', 2);
if (`${+index}` !== index)
throw new Error(`InscriptionId wrong index: ${index}`);
return RawInscriptionId.encode([base_1.hex.decode(txId), +index]);
},
decode(data) {
const [txId, index] = RawInscriptionId.decode(data);
return `${base_1.hex.encode(txId)}i${index}`;
},
};
const TagEnum = {
// Would be simpler to have body tag here,
// but body chunks don't have body tag near them
contentType: 1,
pointer: 2,
parent: 3,
metadata: 5,
metaprotocol: 7,
contentEncoding: 9,
delegate: 11,
rune: 13,
note: 15,
// Unrecognized even tag makes inscription unbound
// unbound: 66,
// Odd fields are ignored
// nop: 255,
};
const TagCoderInternal = /* @__PURE__ */ P.map(P.U8, TagEnum);
const TagCoders = /* @__PURE__ */ {
pointer: P.bigint(8, true, false, false), // U64
contentType: P.string(null),
parent: exports.InscriptionId,
metadata: cbor_ts_1.CBOR,
metaprotocol: P.string(null),
contentEncoding: P.string(null),
delegate: exports.InscriptionId,
rune: P.bigint(16, true, false, false), // U128
note: P.string(null),
// unbound: P.bytes(null),
// nop: P.bytes(null),
};
// We can't use mappedTag here, because tags can be split in chunks
const TagCoder = {
encode(from) {
const tmp = {};
const unknown = [];
// collect tag parts
for (const { tag, data } of from) {
try {
const tagName = TagCoderInternal.decode(tag);
if (!tmp[tagName])
tmp[tagName] = [];
tmp[tagName].push(data);
}
catch (e) {
unknown.push([tag, data]);
}
}
const res = {};
if (unknown.length)
res.unknown = unknown;
for (const field in tmp) {
if (field === 'parent' && tmp[field].length > 1) {
res[field] = tmp[field].map((i) => TagCoders.parent.decode(i));
continue;
}
res[field] = TagCoders[field].decode(btc_signer_1.utils.concatBytes(...tmp[field]));
}
return res;
},
decode(to) {
const res = [];
for (const field in to) {
if (field === 'unknown')
continue;
const tagName = TagCoderInternal.encode(field);
if (field === 'parent' && Array.isArray(to.parent)) {
for (const p of to.parent)
res.push({ tag: tagName, data: TagCoders.parent.encode(p) });
continue;
}
let bytes = TagCoders[field].encode(to[field]);
// Handle pointer = 0:
if (field === 'pointer' && bytes.length === 0) {
bytes = new Uint8Array([0]);
}
for (const data of splitChunks(bytes))
res.push({ tag: tagName, data });
}
if (to.unknown) {
if (!Array.isArray(to.unknown))
throw new Error('ordinals/TagCoder: unknown should be array');
for (const [tag, data] of to.unknown)
res.push({ tag, data });
}
return res;
},
};
const parseEnvelopes = (script, pos = 0) => {
if (!Number.isSafeInteger(pos))
throw new Error(`parseInscription: wrong pos=${typeof pos}`);
const envelopes = [];
// Inscriptions with broken parsing are called 'cursed' (stutter or pushnum)
let stutter = false;
main: for (; pos < script.length; pos++) {
const instr = script[pos];
if (instr !== 0)
continue;
if (script[pos + 1] !== 'IF') {
if (script[pos + 1] === 0)
stutter = true;
continue main;
}
if (!btc_signer_1.utils.isBytes(script[pos + 2]) ||
!P.utils.equalBytes(script[pos + 2], PROTOCOL_ID)) {
if (script[pos + 2] === 0)
stutter = true;
continue main;
}
let pushnum = false;
const payload = []; // bytes or 0
for (let j = pos + 3; j < script.length; j++) {
const op = script[j];
// done
if (op === 'ENDIF') {
envelopes.push({ start: pos + 3, end: j, pushnum, payload, stutter });
pos = j;
break;
}
if (op === '1NEGATE') {
pushnum = true;
payload.push(new Uint8Array([0x81]));
continue;
}
if (typeof op === 'number' && 1 <= op && op <= 16) {
pushnum = true;
payload.push(new Uint8Array([op]));
continue;
}
if (btc_signer_1.utils.isBytes(op) || op === 0) {
payload.push(op);
continue;
}
stutter = false;
break;
}
}
return envelopes;
};
// Additional API for parsing inscriptions
function parseInscriptions(script, strict = false) {
if (strict && (!btc_signer_1.utils.isBytes(script[0]) || script[0].length !== 32))
return;
if (strict && script[1] !== 'CHECKSIG')
return;
const envelopes = parseEnvelopes(script);
const inscriptions = [];
// Check that all inscriptions are sequential inside script
let pos = 5;
for (const envelope of envelopes) {
if (strict && (envelope.stutter || envelope.pushnum))
return;
if (strict && envelope.start !== pos)
return;
const { payload } = envelope;
let i = 0;
const tags = [];
for (; i < payload.length && payload[i] !== 0; i += 2) {
const tag = payload[i];
const data = payload[i + 1];
if (!btc_signer_1.utils.isBytes(tag))
throw new Error('parseInscription: non-bytes tag');
if (!btc_signer_1.utils.isBytes(data))
throw new Error('parseInscription: non-bytes tag data');
tags.push({ tag, data });
}
while (payload[i] === 0 && i < payload.length)
i++;
const chunks = [];
for (; i < payload.length; i++) {
if (!btc_signer_1.utils.isBytes(payload[i]))
break;
chunks.push(payload[i]);
}
inscriptions.push({
tags: TagCoder.encode(tags),
body: btc_signer_1.utils.concatBytes(...chunks),
cursed: envelope.pushnum || envelope.stutter,
});
pos = envelope.end + 4;
}
if (pos - 3 !== script.length)
return;
return inscriptions;
}
/**
* Parse inscriptions from reveal tx input witness (tx.inputs[0].finalScriptWitness)
*/
function parseWitness(witness) {
if (witness.length !== 3)
throw new Error('Wrong witness');
// We don't validate other parts of witness here since we want to parse
// as much stuff as possible. When creating inscription, it is done more strictly
return parseInscriptions(btc_signer_1.Script.decode(witness[1]));
}
exports.OutOrdinalReveal = {
encode(from) {
const res = { type: 'tr_ord_reveal' };
try {
res.inscriptions = parseInscriptions(from, true);
res.pubkey = from[0];
}
catch (e) {
return;
}
return res;
},
decode: (to) => {
if (to.type !== 'tr_ord_reveal')
return;
const out = [to.pubkey, 'CHECKSIG'];
for (const { tags, body } of to.inscriptions) {
out.push(0, 'IF', PROTOCOL_ID);
const rawTags = TagCoder.decode(tags);
for (const tag of rawTags)
out.push(tag.tag, tag.data);
// Body
out.push(0);
for (const c of splitChunks(body))
out.push(c);
out.push('ENDIF');
}
return out;
},
finalizeTaproot: (script, parsed, signatures) => {
if (signatures.length !== 1)
throw new Error('tr_ord_reveal/finalize: wrong signatures array');
const [{ pubKey }, sig] = signatures[0];
if (!P.utils.equalBytes(pubKey, parsed.pubkey))
return;
return [sig, script];
},
};
/**
* Create reveal transaction. Inscription created on spending output from this address by
* revealing taproot script.
*/
function p2tr_ord_reveal(pubkey, inscriptions) {
return {
type: 'tr',
script: P.apply(btc_signer_1.Script, P.coders.match([exports.OutOrdinalReveal])).encode({
type: 'tr_ord_reveal',
pubkey,
inscriptions,
}),
};
}
// Internal methods for tests
exports.__test__ = { TagCoders, TagCoder, parseEnvelopes };
//# sourceMappingURL=index.js.map