runes-js
Version:
A typescript library for Runes protocol of Bitcoin
535 lines (456 loc) • 16.1 kB
text/typescript
import { Edict } from '../edict';
import { Etching } from '../etching';
import * as bitcoin from 'bitcoinjs-lib';
import { Transaction } from 'bitcoinjs-lib';
import * as varint from '../varint';
import { MAGIC_NUMBER, MAX_DIVISIBILITY, MAX_SPACERS, Rune } from '../rune';
import assert from 'assert';
import { Flag, FlagTypes } from '../flag';
import { Tag, tagEncodeList, tagEncodeOption, tagTaker } from '../tag';
import { RuneId } from '../rune_id';
import { Terms } from '../terms';
import { Artifact } from '../artifacts';
import { Message } from './message';
import { Flaw, FlawTypes } from '../flaw';
import { Cenotaph } from '../cenotaph';
export const TAG_BODY: bigint = BigInt(Tag.Body);
export const TAG_DIVISIBILITY: bigint = BigInt(Tag.Divisibility);
export const TAG_FLAGS: bigint = BigInt(Tag.Flags);
export const TAG_SPACERS: bigint = BigInt(Tag.Spacers);
export const TAG_RUNE: bigint = BigInt(Tag.Rune);
export const TAG_SYMBOL: bigint = BigInt(Tag.Symbol);
export const TAG_PREMINE: bigint = BigInt(Tag.Premine);
export const TAG_CAP: bigint = BigInt(Tag.Cap);
export const TAG_AMOUNT: bigint = BigInt(Tag.Amount);
export const TAG_HEIGHT_START: bigint = BigInt(Tag.HeightStart);
export const TAG_HEIGHT_END: bigint = BigInt(Tag.HeightEnd);
export const TAG_OFFSET_START: bigint = BigInt(Tag.OffsetStart);
export const TAG_OFFSET_END: bigint = BigInt(Tag.OffsetEnd);
export const TAG_MINT: bigint = BigInt(Tag.Mint);
export const TAG_POINTER: bigint = BigInt(Tag.Pointer);
export const TAG_CENOTAPH: bigint = BigInt(Tag.Cenotaph);
export const TAG_BURN: bigint = BigInt(Tag.Burn);
export const TAG_NOP: bigint = BigInt(Tag.Nop);
export const U128_MAX = BigInt(2) ** BigInt(128) - BigInt(1);
export class RuneStone extends Artifact {
public edicts: Edict[];
public etching: Etching | null;
// public mint: RuneId | null;
public pointer: bigint | null;
constructor({ edicts, etching, mint, pointer }: {
edicts?: Edict[];
etching?: Etching | null;
mint?: RuneId | null;
pointer?: bigint | null
}) {
super();
this.edicts = edicts ?? [];
this.etching = etching ?? null;
this.pointer = pointer ?? null;
this.setMint(mint ?? null);
}
static fromTransaction(transaction: Transaction): Artifact | null {
const rune = new RuneStone({
edicts: [],
etching: null,
mint: null,
pointer: null,
});
const runestone = rune.decipher(transaction);
if (!runestone) {
return null;
}
return runestone;
}
static fromTransactionHex(txhex: string): Artifact | null {
return RuneStone.fromTransaction(bitcoin.Transaction.fromHex(txhex));
}
public encipher(): Buffer {
let payload: number[] = [];
if (this.etching) {
let flags = BigInt(0);
flags = new Flag(FlagTypes.Etch).set(flags);
if (this.etching.terms !== null) {
flags = new Flag(FlagTypes.Terms).set(flags);
}
if (this.etching.turbo === true) {
flags = new Flag(FlagTypes.Turbo).set(flags);
}
payload = tagEncodeList(TAG_FLAGS, [flags], payload);
payload = tagEncodeOption(TAG_RUNE, this.etching.rune === null ? null : this.etching.rune.id, payload);
payload = tagEncodeOption(TAG_DIVISIBILITY, BigInt(this.etching.divisibility), payload);
payload = tagEncodeOption(TAG_SPACERS, BigInt(this.etching.spacers), payload);
payload = tagEncodeOption(TAG_SYMBOL, this.etching.symbol == null ? null : BigInt(this.etching.symbol.charCodeAt(0)), payload);
payload = tagEncodeOption(TAG_PREMINE, this.etching.premine, payload);
if (this.etching.terms !== null) {
payload = tagEncodeOption(TAG_AMOUNT, this.etching.terms.amount, payload);
payload = tagEncodeOption(TAG_CAP, this.etching.terms.cap, payload);
payload = tagEncodeOption(TAG_HEIGHT_START, this.etching.terms.height === null ? null : this.etching.terms.height[0], payload);
payload = tagEncodeOption(TAG_HEIGHT_END, this.etching.terms.height === null ? null : this.etching.terms.height[1], payload);
payload = tagEncodeOption(TAG_OFFSET_START, this.etching.terms.offset === null ? null : this.etching.terms.offset[0], payload);
payload = tagEncodeOption(TAG_HEIGHT_END, this.etching.terms.offset === null ? null : this.etching.terms.offset[1], payload);
}
}
const mint = this.mint();
if (mint) {
payload = tagEncodeList(TAG_MINT, [mint!.block, mint!.tx], payload);
}
payload = tagEncodeOption(TAG_POINTER, this.pointer, payload);
if (this.edicts.length > 0) {
payload = varint.encodeToVec(TAG_BODY, payload);
const edicts = this.edicts.slice();
edicts.sort((a, b) => Number((a.id.toBigInt() - b.id.toBigInt()).toString(10)));
let previous = new RuneId(BigInt(0), BigInt(0));
for (const edict of edicts) {
const [block, tx] = previous.delta(edict.id);
payload = varint.encodeToVec(block, payload);
payload = varint.encodeToVec(tx, payload);
payload = varint.encodeToVec(edict.amount, payload);
payload = varint.encodeToVec(edict.output, payload);
previous = edict.id;
}
}
let buffers = chunkBuffer(Buffer.from(new Uint8Array(payload)), 520);
let script = bitcoin.script.compile([bitcoin.opcodes.OP_RETURN, MAGIC_NUMBER, ...buffers]);
return script;
}
public decipher(transaction: bitcoin.Transaction): Artifact | null {
const payload = this.payload(transaction);
if (!payload) {
return null;
}
let integers: bigint[] = [];
let i = 0;
while (i < payload.length) {
const _payload = payload.subarray(i);
const [integer, length] = varint.decode(_payload);
integers.push(integer);
i += length;
}
const message = Message.fromIntegers(transaction, integers);
let fields = message.fields;
let flaws = message.flaws;
let etching: Etching | null | undefined = null;
let mint = tagTaker<RuneId>(TAG_MINT, 2, fields, values => {
return new RuneId(values[0], values[1]);
});
let pointer = tagTaker(TAG_POINTER, 1, fields, values => {
let _pointer = values[0];
if (Number(_pointer) < transaction.outs.length) {
return _pointer;
} else {
return null;
}
});
let divisibility = tagTaker(TAG_DIVISIBILITY, 1, fields, values => {
let _divisibility = values[0];
if (_divisibility < BigInt(MAX_DIVISIBILITY)) {
return _divisibility;
} else {
return null;
}
});
let amount = tagTaker(TAG_AMOUNT, 1, fields, values => {
return values[0] ?? null;
});
let rune = tagTaker(TAG_RUNE, 1, fields, values => {
return values[0] !== null && values[0] !== undefined ? new Rune(values[0]) : null;
});
let cap = tagTaker(TAG_CAP, 1, fields, values => {
return values[0] ?? null;
});
let premine = tagTaker(TAG_PREMINE, 1, fields, values => {
return values[0] ?? null;
});
let spacers = tagTaker(TAG_SPACERS, 1, fields, values => {
let _spacers = values[0];
if (_spacers <= BigInt(MAX_SPACERS)) {
return _spacers;
} else {
return null;
}
});
let symbol = tagTaker(TAG_SYMBOL, 1, fields, values => {
return values[0] ? charFromU32(Number(values[0])) : null;
});
let offset = (() => {
let start = tagTaker(TAG_OFFSET_START, 1, fields, values => {
return values[0] ?? null;
});
let end = tagTaker(TAG_OFFSET_END, 1, fields, values => {
return values[0] ?? null;
});
return start === null && end === null ? null : [start, end];
})();
let height = (() => {
let start = tagTaker(TAG_HEIGHT_START, 1, fields, values => {
return values[0] ?? null;
});
let end = tagTaker(TAG_HEIGHT_END, 1, fields, values => {
return values[0] ?? null;
});
return start === null && end === null ? null : [start, end];
})();
let etch: boolean = false;
let terms: boolean = false;
let turbo: boolean = false;
let flags = tagTaker(TAG_FLAGS, 1, fields, values => {
return values[0] ?? null;
});
if (flags !== null) {
let _etch = new Flag(FlagTypes.Etch).take(flags);
etch = _etch[0];
flags = _etch[1];
let _terms = new Flag(FlagTypes.Terms).take(flags);
terms = _terms[0];
flags = _terms[1];
let _turbo = new Flag(FlagTypes.Turbo).take(flags);
turbo = _turbo[0];
flags = _turbo[1];
}
if (etch) {
etching = new Etching({
divisibility: Number(divisibility),
rune,
symbol,
spacers,
premine,
terms: terms
? new Terms({
cap,
height,
amount,
offset,
})
: null,
turbo,
});
if (etching.supply() == null) {
flaws = new Flaw(FlawTypes.SupplyOverflow);
}
}
if (flags !== undefined && flags !== BigInt(0) && flags !== null) {
flaws = new Flaw(FlawTypes.UnrecognizedFlag);
}
if (Array.from(fields.keys()).some(tag => Number.parseInt(tag.toString()) % 2 === 0)) {
flaws = new Flaw(FlawTypes.UnrecognizedEvenTag);
}
if (flaws !== null) {
return new Cenotaph({
flaws,
etching: etching?.rune ?? null,
mint,
});
} else {
return new RuneStone({
edicts: message.edicts,
etching,
mint,
pointer,
});
}
}
public payload(transaction: bitcoin.Transaction): Buffer | null {
let solution: Buffer | null = null;
for (const output of transaction.outs) {
const script = bitcoin.script.decompile(output.script);
if (script && script[0] === bitcoin.opcodes.OP_RETURN) {
if (script.length > 1 && !Buffer.isBuffer(script[1]) && script[1] === MAGIC_NUMBER) {
let payload = Buffer.alloc(0);
for (let i = 2; i < script.length; i++) {
if (Buffer.isBuffer(script[i])) {
payload = Buffer.concat([payload, script[i] as Buffer]);
}
}
solution = payload;
break;
} else {
continue;
}
} else {
continue;
}
}
return solution;
}
}
export function decodeOpReturn(scriptHex: string | Buffer, outLength: number): Artifact | null {
const scriptBuf = typeof scriptHex === 'string' ? Buffer.from(scriptHex, 'hex') : scriptHex;
const script = bitcoin.script.decompile(scriptBuf);
let payload: Buffer | null = null;
if (script && script[0] === bitcoin.opcodes.OP_RETURN) {
if (script.length > 1 && !Buffer.isBuffer(script[1]) && script[1] === MAGIC_NUMBER) {
let _payload = Buffer.alloc(0);
for (let i = 2; i < script.length; i++) {
if (Buffer.isBuffer(script[i])) {
_payload = Buffer.concat([_payload, script[i] as Buffer]);
}
}
payload = _payload;
}
}
if (payload !== null) {
let integers: bigint[] = [];
let i = 0;
while (i < payload.length) {
const _payload = payload.subarray(i);
const [integer, length] = varint.decode(_payload);
integers.push(integer);
i += length;
}
const message = Message.fromOpReturn(integers);
let fields = message.fields;
let flaws = message.flaws;
let etching: Etching | null | undefined = null;
let mint = tagTaker<RuneId>(TAG_MINT, 2, fields, values => {
return new RuneId(values[0], values[1]);
});
// fields.delete(TAG_MINT);
let pointer = tagTaker(TAG_POINTER, 1, fields, values => {
let _pointer = values[0];
if (Number(_pointer) < outLength) {
return _pointer;
} else {
return null;
}
});
let divisibility = tagTaker(TAG_DIVISIBILITY, 1, fields, values => {
let _divisibility = values[0];
if (_divisibility < BigInt(MAX_DIVISIBILITY)) {
return _divisibility;
} else {
return null;
}
});
let amount = tagTaker(TAG_AMOUNT, 1, fields, values => {
return values[0] ?? null;
});
let rune = tagTaker(TAG_RUNE, 1, fields, values => {
return values[0] !== null && values[0] !== undefined ? new Rune(values[0]) : null;
});
let cap = tagTaker(TAG_CAP, 1, fields, values => {
return values[0] ?? null;
});
let premine = tagTaker(TAG_PREMINE, 1, fields, values => {
return values[0] ?? null;
});
let spacers = tagTaker(TAG_SPACERS, 1, fields, values => {
let _spacers = values[0];
if (_spacers < BigInt(MAX_SPACERS)) {
return _spacers;
} else {
return null;
}
});
let symbol = tagTaker(TAG_SYMBOL, 1, fields, values => {
return values[0] ? charFromU32(Number(values[0])) : null;
});
let offset = (() => {
let start = tagTaker(TAG_OFFSET_START, 1, fields, values => {
return values[0] ?? null;
});
// fields.delete(TAG_OFFSET_START);
let end = tagTaker(TAG_OFFSET_END, 1, fields, values => {
return values[0] ?? null;
});
// fields.delete(TAG_OFFSET_END);
return start === null && end === null ? null : [start, end];
})();
// console.log({ fields });
let height = (() => {
let start = tagTaker(TAG_HEIGHT_START, 1, fields, values => {
return values[0] ?? null;
});
let end = tagTaker(TAG_HEIGHT_END, 1, fields, values => {
return values[0] ?? null;
});
return start === null && end === null ? null : [start, end];
})();
let etch: boolean = false;
let terms: boolean = false;
let flags = tagTaker(TAG_FLAGS, 1, fields, values => {
return values[0] ?? null;
});
if (flags !== null) {
let _etch = new Flag(FlagTypes.Etch).take(flags);
etch = _etch[0];
flags = _etch[1];
let _terms = new Flag(FlagTypes.Terms).take(flags);
terms = _terms[0];
flags = _terms[1];
}
if (etch) {
etching = new Etching({
divisibility: Number(divisibility),
rune,
symbol,
spacers,
premine,
terms: terms
? new Terms({
cap,
height,
amount,
offset,
})
: null,
});
if (etching.supply() == null) {
flaws = new Flaw(FlawTypes.SupplyOverflow);
}
}
if (flags !== undefined && flags !== BigInt(0) && flags !== null) {
flaws = new Flaw(FlawTypes.UnrecognizedFlag);
}
if (Array.from(fields.keys()).some(tag => Number.parseInt(tag.toString()) % 2 === 0)) {
flaws = new Flaw(FlawTypes.UnrecognizedEvenTag);
}
if (flaws !== null) {
return new Cenotaph({
flaws,
etching: etching?.rune ?? null,
mint,
});
} else {
return new RuneStone({
edicts: message.edicts,
etching,
mint,
pointer,
});
}
} else {
return null;
}
}
export function getScriptInstructions(script: Buffer) {
const chunks = bitcoin.script.decompile(script);
if (chunks === null) throw new Error('Invalid script');
return chunks.map(chunk => {
if (Buffer.isBuffer(chunk)) {
return { type: 'data', value: chunk.toString('hex') };
} else {
return {
type: 'opcode',
value: bitcoin.script.toASM([chunk]).split(' ')[0],
};
}
});
}
function charFromU32(code: number) {
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
// 超出 Unicode 范围或是代理对的编码
return null;
}
return String.fromCodePoint(code);
}
export function chunkBuffer(buffer: Buffer, chunkSize: number) {
assert(!isNaN(chunkSize) && chunkSize > 0, 'Chunk size should be positive number');
const result: Buffer[] = [];
const len = buffer.byteLength;
let i = 0;
while (i < len) {
result.push(buffer.subarray(i, (i += chunkSize)));
}
return result;
}