Skip to content

Commit

Permalink
Bump micro-packed
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed May 17, 2024
1 parent 63d4ef3 commit 93cf32a
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 89 deletions.
21 changes: 11 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 5 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@
"name": "micro-ordinals",
"version": "0.1.0",
"description": "Manage ordinals, inscriptions and runes using scure-btc-signer",
"files": [
"lib",
"src"
],
"files": ["lib", "src"],
"bin": {
"ord": "lib/esm/cli.js"
},
"main": "lib/index.js",
"module": "lib/esm/index.js",
"types": "lib/index.d.ts",
"dependencies": {
"@scure/base": "~1.1.6",
"@scure/btc-signer": "~1.3.1",
"@scure/base": "~1.1.5",
"@scure/btc-signer": "~1.3.2",
"enquirer": "2.4.1",
"micro-packed": "~0.5.3"
"micro-packed": "~0.6.2"
},
"devDependencies": {
"@noble/curves": "^1.4.0",
"@paulmillr/jsbt": "0.1.0",
"@types/node": "20.12.7",
"micro-bmark": "0.3.1",
Expand Down
42 changes: 9 additions & 33 deletions src/cbor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,8 @@ const F16BE = P.wrap({
},
});

const createView = (arr: Uint8Array) => new DataView(arr.buffer, arr.byteOffset, arr.byteLength);

const f32 = (le?: boolean) =>
P.wrap({
encodeStream(w, value: number) {
if (Math.fround(value) !== value) throw w.err(`f32: wrong value=${value}`);
const buf = new Uint8Array(4);
createView(buf).setFloat32(0, value, le);
w.bytes(buf);
},
decodeStream: (r) => createView(r.bytes(4)).getFloat32(0, le),
});

const f64 = (le?: boolean) =>
P.wrap({
encodeStream(w, value: number) {
// no validation, any JS number can be encoded as float64
const buf = new Uint8Array(8);
createView(buf).setFloat64(0, value, le);
w.bytes(buf);
},
decodeStream: (r) => createView(r.bytes(8)).getFloat64(0, le),
});

const F32BE = /* @__PURE__ */ f32(false); // const F32LE = /* @__PURE__ */ f32(true);
const F64BE = /* @__PURE__ */ f64(false); // const F64LE = /* @__PURE__ */ f64(true);

const INFO = P.bits(5); // additional info
const U64LEN = P.apply(P.U64BE, P.coders.number);
const U64LEN = P.apply(P.U64BE, P.coders.numberBigint);

// Number/lengths limits
const CBOR_LIMITS: Record<
Expand Down Expand Up @@ -186,10 +159,10 @@ const cborSimple: P.CoderType<boolean | null | undefined | number> = P.wrap({
// If can be encoded as F32 without rounding
if (Math.fround(value) === value) {
INFO.encodeStream(w, 26);
return F32BE.encodeStream(w, value);
return P.F32BE.encodeStream(w, value);
}
INFO.encodeStream(w, 27);
return F64BE.encodeStream(w, value);
return P.F64BE.encodeStream(w, value);
},
decodeStream(r) {
const ai = INFO.decodeStream(r);
Expand All @@ -199,8 +172,8 @@ const cborSimple: P.CoderType<boolean | null | undefined | number> = P.wrap({
if (ai === 23) return undefined;
// ai === 24 is P.U8 with simple, reserved
if (ai === 25) return F16BE.decodeStream(r);
if (ai === 26) return F32BE.decodeStream(r);
if (ai === 27) return F64BE.decodeStream(r);
if (ai === 26) return P.F32BE.decodeStream(r);
if (ai === 27) return P.F64BE.decodeStream(r);
throw r.err('cbor/simple: unassigned');
},
});
Expand Down Expand Up @@ -280,7 +253,10 @@ export const CBOR = P.apply(cborValue, {
return { TAG: 'simple', data: data };
}
if (typeof data === 'object') {
return { TAG: 'map', data: Object.entries(data).map((kv) => kv.map((i) => this.decode(i))) };
return {
TAG: 'map',
data: Object.entries(data).map((kv) => kv.map((i) => this.decode(i))),
};
}
throw new Error('unknown type');
},
Expand Down
95 changes: 58 additions & 37 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Coder, hex, utf8 } from '@scure/base';
import * as P from 'micro-packed';
import { Coder, hex, utf8 } from "@scure/base";
import * as P from "micro-packed";
import {
Script,
ScriptType,
OptScript,
CustomScript,
MAX_SCRIPT_BYTE_LENGTH,
utils,
} from '@scure/btc-signer';
import { CBOR } from './cbor.js';
} from "@scure/btc-signer";
import { CBOR } from "./cbor.js";

type Bytes = Uint8Array;
const PROTOCOL_ID = /* @__PURE__ */ utf8.decode('ord');
const PROTOCOL_ID = /* @__PURE__ */ utf8.decode("ord");

function splitChunks(buf: Bytes): Bytes[] {
const res = [];
Expand All @@ -22,13 +22,14 @@ function splitChunks(buf: Bytes): Bytes[] {

const RawInscriptionId = /* @__PURE__ */ P.tuple([
P.bytes(32, true),
P.apply(P.bigint(4, true, false, false), P.coders.number),
P.apply(P.bigint(4, true, false, false), P.coders.numberBigint),
] as const);

export const InscriptionId: P.Coder<string, Bytes> = {
encode(data: string) {
const [txId, index] = data.split('i', 2);
if (`${+index}` !== index) throw new Error(`InscriptionId wrong index: ${index}`);
const [txId, index] = data.split("i", 2);
if (`${+index}` !== index)
throw new Error(`InscriptionId wrong index: ${index}`);
return RawInscriptionId.encode([hex.decode(txId), +index]);
},
decode(data: Bytes) {
Expand Down Expand Up @@ -96,28 +97,34 @@ const TagCoder: P.Coder<TagRaw[], Tags> = {
const res: Partial<Tags> = {};
if (unknown.length) res.unknown = unknown;
for (const field in tmp) {
if (field === 'parent' && tmp[field].length > 1) {
res[field as TagName] = tmp[field].map((i) => TagCoders.parent.decode(i));
if (field === "parent" && tmp[field].length > 1) {
res[field as TagName] = tmp[field].map((i) =>
TagCoders.parent.decode(i),
);
continue;
}
res[field as TagName] = TagCoders[field as TagName].decode(utils.concatBytes(...tmp[field]));
res[field as TagName] = TagCoders[field as TagName].decode(
utils.concatBytes(...tmp[field]),
);
}
return res as Tags;
},
decode(to: Tags): TagRaw[] {
const res: TagRaw[] = [];
for (const field in to) {
if (field === 'unknown') continue;
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) });
if (field === "parent" && Array.isArray(to.parent)) {
for (const p of to.parent)
res.push({ tag: tagName, data: TagCoders.parent.encode(p) });
continue;
}
const bytes = TagCoders[field as TagName].encode(to[field as TagName]);
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');
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;
Expand All @@ -126,24 +133,28 @@ const TagCoder: P.Coder<TagRaw[], Tags> = {

export type Inscription = { tags: Tags; body: Bytes; cursed?: boolean };
type OutOrdinalRevealType = {
type: 'tr_ord_reveal';
type: "tr_ord_reveal";
pubkey: Bytes;
inscriptions: Inscription[];
};

const parseEnvelopes = (script: ScriptType, pos = 0) => {
if (!Number.isSafeInteger(pos)) throw new Error(`parseInscription: wrong pos=${typeof pos}`);
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] !== "IF") {
if (script[pos + 1] === 0) stutter = true;
continue main;
}
if (!utils.isBytes(script[pos + 2]) || !P.equalBytes(script[pos + 2] as any, PROTOCOL_ID)) {
if (
!utils.isBytes(script[pos + 2]) ||
!P.utils.equalBytes(script[pos + 2] as any, PROTOCOL_ID)
) {
if (script[pos + 2] === 0) stutter = true;
continue main;
}
Expand All @@ -152,17 +163,17 @@ const parseEnvelopes = (script: ScriptType, pos = 0) => {
for (let j = pos + 3; j < script.length; j++) {
const op = script[j];
// done
if (op === 'ENDIF') {
if (op === "ENDIF") {
envelopes.push({ start: pos + 3, end: j, pushnum, payload, stutter });
pos = j;
break;
}
if (op === '1NEGATE') {
if (op === "1NEGATE") {
pushnum = true;
payload.push(new Uint8Array([0x81]));
continue;
}
if (typeof op === 'number' && 1 <= op && op <= 16) {
if (typeof op === "number" && 1 <= op && op <= 16) {
pushnum = true;
payload.push(new Uint8Array([op]));
continue;
Expand All @@ -179,9 +190,12 @@ const parseEnvelopes = (script: ScriptType, pos = 0) => {
};

// Additional API for parsing inscriptions
export function parseInscriptions(script: ScriptType, strict = false): Inscription[] | undefined {
export function parseInscriptions(
script: ScriptType,
strict = false,
): Inscription[] | undefined {
if (strict && (!utils.isBytes(script[0]) || script[0].length !== 32)) return;
if (strict && script[1] !== 'CHECKSIG') return;
if (strict && script[1] !== "CHECKSIG") return;

const envelopes = parseEnvelopes(script);
const inscriptions: Inscription[] = [];
Expand All @@ -196,8 +210,10 @@ export function parseInscriptions(script: ScriptType, strict = false): Inscripti
for (; i < payload.length && payload[i] !== 0; i += 2) {
const tag = payload[i];
const data = payload[i + 1];
if (!utils.isBytes(tag)) throw new Error('parseInscription: non-bytes tag');
if (!utils.isBytes(data)) throw new Error('parseInscription: non-bytes tag data');
if (!utils.isBytes(tag))
throw new Error("parseInscription: non-bytes tag");
if (!utils.isBytes(data))
throw new Error("parseInscription: non-bytes tag data");
tags.push({ tag, data });
}
while (payload[i] === 0 && i < payload.length) i++;
Expand All @@ -222,15 +238,19 @@ export function parseInscriptions(script: ScriptType, strict = false): Inscripti
* Parse inscriptions from reveal tx input witness (tx.inputs[0].finalScriptWitness)
*/
export function parseWitness(witness: Bytes[]): Inscription[] | undefined {
if (witness.length !== 3) throw new Error('Wrong 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(Script.decode(witness[1]));
}

export const OutOrdinalReveal: Coder<OptScript, OutOrdinalRevealType | undefined> & CustomScript = {
export const OutOrdinalReveal: Coder<
OptScript,
OutOrdinalRevealType | undefined
> &
CustomScript = {
encode(from: ScriptType): OutOrdinalRevealType | undefined {
const res: Partial<OutOrdinalRevealType> = { type: 'tr_ord_reveal' };
const res: Partial<OutOrdinalRevealType> = { type: "tr_ord_reveal" };
try {
res.inscriptions = parseInscriptions(from, true);
res.pubkey = from[0] as Bytes;
Expand All @@ -240,23 +260,24 @@ export const OutOrdinalReveal: Coder<OptScript, OutOrdinalRevealType | undefined
return res as OutOrdinalRevealType;
},
decode: (to: OutOrdinalRevealType): OptScript => {
if (to.type !== 'tr_ord_reveal') return;
const out: ScriptType = [to.pubkey, 'CHECKSIG'];
if (to.type !== "tr_ord_reveal") return;
const out: ScriptType = [to.pubkey, "CHECKSIG"];
for (const { tags, body } of to.inscriptions) {
out.push(0, 'IF', PROTOCOL_ID);
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');
out.push("ENDIF");
}
return out as any;
},
finalizeTaproot: (script: any, parsed: any, signatures: any) => {
if (signatures.length !== 1) throw new Error('tr_ord_reveal/finalize: wrong signatures array');
if (signatures.length !== 1)
throw new Error("tr_ord_reveal/finalize: wrong signatures array");
const [{ pubKey }, sig] = signatures[0];
if (!P.equalBytes(pubKey, parsed.pubkey)) return;
if (!P.utils.equalBytes(pubKey, parsed.pubkey)) return;
return [sig, script];
},
};
Expand All @@ -267,9 +288,9 @@ export const OutOrdinalReveal: Coder<OptScript, OutOrdinalRevealType | undefined
*/
export function p2tr_ord_reveal(pubkey: Bytes, inscriptions: Inscription[]) {
return {
type: 'tr_ord_reveal',
type: "tr_ord_reveal",
script: P.apply(Script, P.coders.match([OutOrdinalReveal])).encode({
type: 'tr_ord_reveal',
type: "tr_ord_reveal",
pubkey,
inscriptions,
}),
Expand Down
4 changes: 2 additions & 2 deletions test/ordinals.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { describe, should } from 'micro-should';
import { hex, utf8 } from '@scure/base';
import * as btc from '@scure/btc-signer';
import * as ordinals from '../lib/esm/index.js';
import {TEST_NETWORK} from '@scure/btc-signer';
import { TEST_NETWORK } from '@scure/btc-signer';
import { schnorr as secp256k1_schnorr } from '@noble/curves/secp256k1';
import { default as ordvectors } from './fixtures/ordinals.json' assert { type: 'json' };
import { default as ordvectors } from './fixtures/ordinals.json' with { type: 'json' };

// The file misses a bunch of valid test vectors for inscriptions.
// There are only a few official test vectors.
Expand Down

0 comments on commit 93cf32a

Please sign in to comment.