From 4143a5f5935d90e031f2d001cbcff7d8ffb46412 Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 7 Oct 2024 20:03:10 +0900 Subject: [PATCH] Add runestone protocol implementation --- frontend/src/app/shared/ord/rune/artifact.ts | 4 + frontend/src/app/shared/ord/rune/cenotaph.ts | 14 + frontend/src/app/shared/ord/rune/constants.ts | 7 + frontend/src/app/shared/ord/rune/edict.ts | 34 ++ frontend/src/app/shared/ord/rune/etching.ts | 54 +++ frontend/src/app/shared/ord/rune/flag.ts | 20 + frontend/src/app/shared/ord/rune/flaw.ts | 12 + .../src/app/shared/ord/rune/integer/index.ts | 4 + .../src/app/shared/ord/rune/integer/u128.ts | 176 ++++++++ .../src/app/shared/ord/rune/integer/u32.ts | 58 +++ .../src/app/shared/ord/rune/integer/u64.ts | 58 +++ .../src/app/shared/ord/rune/integer/u8.ts | 58 +++ frontend/src/app/shared/ord/rune/message.ts | 67 +++ frontend/src/app/shared/ord/rune/monads.ts | 392 ++++++++++++++++++ frontend/src/app/shared/ord/rune/rune.ts | 23 + frontend/src/app/shared/ord/rune/runeid.ts | 89 ++++ frontend/src/app/shared/ord/rune/runestone.ts | 258 ++++++++++++ frontend/src/app/shared/ord/rune/script.ts | 237 +++++++++++ frontend/src/app/shared/ord/rune/seekarray.ts | 43 ++ .../src/app/shared/ord/rune/spacedrune.ts | 21 + frontend/src/app/shared/ord/rune/tag.ts | 60 +++ frontend/src/app/shared/ord/rune/terms.ts | 9 + frontend/src/app/shared/ord/rune/utils.ts | 6 + 23 files changed, 1704 insertions(+) create mode 100644 frontend/src/app/shared/ord/rune/artifact.ts create mode 100644 frontend/src/app/shared/ord/rune/cenotaph.ts create mode 100644 frontend/src/app/shared/ord/rune/constants.ts create mode 100644 frontend/src/app/shared/ord/rune/edict.ts create mode 100644 frontend/src/app/shared/ord/rune/etching.ts create mode 100644 frontend/src/app/shared/ord/rune/flag.ts create mode 100644 frontend/src/app/shared/ord/rune/flaw.ts create mode 100644 frontend/src/app/shared/ord/rune/integer/index.ts create mode 100644 frontend/src/app/shared/ord/rune/integer/u128.ts create mode 100644 frontend/src/app/shared/ord/rune/integer/u32.ts create mode 100644 frontend/src/app/shared/ord/rune/integer/u64.ts create mode 100644 frontend/src/app/shared/ord/rune/integer/u8.ts create mode 100644 frontend/src/app/shared/ord/rune/message.ts create mode 100644 frontend/src/app/shared/ord/rune/monads.ts create mode 100644 frontend/src/app/shared/ord/rune/rune.ts create mode 100644 frontend/src/app/shared/ord/rune/runeid.ts create mode 100644 frontend/src/app/shared/ord/rune/runestone.ts create mode 100644 frontend/src/app/shared/ord/rune/script.ts create mode 100644 frontend/src/app/shared/ord/rune/seekarray.ts create mode 100644 frontend/src/app/shared/ord/rune/spacedrune.ts create mode 100644 frontend/src/app/shared/ord/rune/tag.ts create mode 100644 frontend/src/app/shared/ord/rune/terms.ts create mode 100644 frontend/src/app/shared/ord/rune/utils.ts diff --git a/frontend/src/app/shared/ord/rune/artifact.ts b/frontend/src/app/shared/ord/rune/artifact.ts new file mode 100644 index 000000000..2eba9f158 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/artifact.ts @@ -0,0 +1,4 @@ +import { Cenotaph } from './cenotaph'; +import { Runestone } from './runestone'; + +export type Artifact = Cenotaph | Runestone; diff --git a/frontend/src/app/shared/ord/rune/cenotaph.ts b/frontend/src/app/shared/ord/rune/cenotaph.ts new file mode 100644 index 000000000..368a0f938 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/cenotaph.ts @@ -0,0 +1,14 @@ +import { Flaw } from './flaw'; +import { None, Option } from './monads'; +import { Rune } from './rune'; +import { RuneId } from './runeid'; + +export class Cenotaph { + readonly type = 'cenotaph'; + + constructor( + readonly flaws: Flaw[], + readonly etching: Option = None, + readonly mint: Option = None + ) {} +} diff --git a/frontend/src/app/shared/ord/rune/constants.ts b/frontend/src/app/shared/ord/rune/constants.ts new file mode 100644 index 000000000..0e4bab116 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/constants.ts @@ -0,0 +1,7 @@ +import { u8 } from './integer'; +import { opcodes } from './script'; + +export const MAX_DIVISIBILITY = u8(38); + +export const OP_RETURN = opcodes.OP_RETURN; +export const MAGIC_NUMBER = opcodes.OP_13; diff --git a/frontend/src/app/shared/ord/rune/edict.ts b/frontend/src/app/shared/ord/rune/edict.ts new file mode 100644 index 000000000..ede5865a6 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/edict.ts @@ -0,0 +1,34 @@ +import { Option, Some, None } from './monads'; +import { RuneId } from './runeid'; +import { u128, u32 } from './integer'; + +export type Edict = { + id: RuneId; + amount: u128; + output: u32; +}; + +export namespace Edict { + export function fromIntegers( + numOutputs: number, + id: RuneId, + amount: u128, + output: u128 + ): Option { + if (id.block === 0n && id.tx > 0n) { + return None; + } + + const optionOutputU32 = u128.tryIntoU32(output); + if (optionOutputU32.isNone()) { + return None; + } + const outputU32 = optionOutputU32.unwrap(); + + if (outputU32 > numOutputs) { + return None; + } + + return Some({ id, amount, output: outputU32 }); + } +} diff --git a/frontend/src/app/shared/ord/rune/etching.ts b/frontend/src/app/shared/ord/rune/etching.ts new file mode 100644 index 000000000..edc245565 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/etching.ts @@ -0,0 +1,54 @@ +import { None, Option, Some } from './monads'; +import { Terms } from './terms'; +import { Rune } from './rune'; +import { u128, u32, u8 } from './integer'; + +type RuneEtchingBase = { + divisibility?: number; + premine?: bigint; + symbol?: string; + terms?: { + cap?: bigint; + amount?: bigint; + offset?: { + start?: bigint; + end?: bigint; + }; + height?: { + start?: bigint; + end?: bigint; + }; + }; + turbo?: boolean; +}; + +export type RuneEtchingSpec = RuneEtchingBase & { runeName?: string }; + +export class Etching { + readonly symbol: Option; + + constructor( + readonly divisibility: Option, + readonly rune: Option, + readonly spacers: Option, + symbol: Option, + readonly terms: Option, + readonly premine: Option, + readonly turbo: boolean + ) { + this.symbol = symbol.andThen((value) => { + const codePoint = value.codePointAt(0); + return codePoint !== undefined ? Some(String.fromCodePoint(codePoint)) : None; + }); + } + + get supply(): Option { + const premine = this.premine.unwrapOr(u128(0)); + const cap = this.terms.andThen((terms) => terms.cap).unwrapOr(u128(0)); + const amount = this.terms.andThen((terms) => terms.amount).unwrapOr(u128(0)); + + return u128 + .checkedMultiply(cap, amount) + .andThen((multiplyResult) => u128.checkedAdd(premine, multiplyResult)); + } +} diff --git a/frontend/src/app/shared/ord/rune/flag.ts b/frontend/src/app/shared/ord/rune/flag.ts new file mode 100644 index 000000000..317c74ae5 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/flag.ts @@ -0,0 +1,20 @@ +import { u128 } from './integer'; + +export enum Flag { + ETCHING = 0, + TERMS = 1, + TURBO = 2, + CENOTAPH = 127, +} + +export namespace Flag { + export function mask(flag: Flag): u128 { + return u128(1n << BigInt(flag)); + } + + export function take(flags: u128, flag: Flag): { set: boolean; flags: u128 } { + const mask = Flag.mask(flag); + const set = (flags & mask) !== 0n; + return { set, flags: set ? u128(flags - mask) : flags }; + } +} diff --git a/frontend/src/app/shared/ord/rune/flaw.ts b/frontend/src/app/shared/ord/rune/flaw.ts new file mode 100644 index 000000000..2ed5ea506 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/flaw.ts @@ -0,0 +1,12 @@ +export enum Flaw { + EDICT_OUTPUT, + EDICT_RUNE_ID, + INVALID_SCRIPT, + OPCODE, + SUPPLY_OVERFLOW, + TRAILING_INTEGERS, + TRUNCATED_FIELD, + UNRECOGNIZED_EVEN_TAG, + UNRECOGNIZED_FLAG, + VARINT, +} diff --git a/frontend/src/app/shared/ord/rune/integer/index.ts b/frontend/src/app/shared/ord/rune/integer/index.ts new file mode 100644 index 000000000..3c54a77e7 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/integer/index.ts @@ -0,0 +1,4 @@ +export { u8 } from './u8'; +export { u32 } from './u32'; +export { u64 } from './u64'; +export { u128 } from './u128'; diff --git a/frontend/src/app/shared/ord/rune/integer/u128.ts b/frontend/src/app/shared/ord/rune/integer/u128.ts new file mode 100644 index 000000000..78de8506f --- /dev/null +++ b/frontend/src/app/shared/ord/rune/integer/u128.ts @@ -0,0 +1,176 @@ +import { None, Option, Some } from '../monads'; +import { SeekArray } from '../seekarray'; +import { u64 } from './u64'; +import { u32 } from './u32'; +import { u8 } from './u8'; + +/** + * A little utility type used for nominal typing. + * + * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} + */ +type BigTypedNumber = bigint & { + /** + * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! + * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. + * @ignore + * @private + * @readonly + * @type {undefined} + */ + readonly __kind__: T; +}; + +/** + * ## 128-bit unsigned integer + * + * - **Value Range:** `0` to `340282366920938463463374607431768211455` + * - **Size in bytes:** `16` + * - **Web IDL type:** `bigint` + * - **Equivalent C type:** `uint128_t` + */ +export type u128 = BigTypedNumber<'u128'>; + +export const U128_MAX_BIGINT = 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffffn; + +/** + * Convert Number or BigInt to 128-bit unsigned integer. + * @param num - The Number or BigInt to convert. + * @returns - The resulting 128-bit unsigned integer (BigInt). + */ +export function u128(num: number | bigint): u128 { + if (typeof num == 'bigint') { + if (num < 0n || num > U128_MAX_BIGINT) { + throw new Error('num is out of range'); + } + } else { + if (!Number.isSafeInteger(num) || num < 0) { + throw new Error('num is not a valid integer'); + } + } + + return BigInt(num) as u128; +} + +export namespace u128 { + export const MAX = u128(U128_MAX_BIGINT); + + export function checkedAdd(x: u128, y: u128): Option { + const result = x + y; + if (result > u128.MAX) { + return None; + } + + return Some(u128(result)); + } + + export function checkedAddThrow(x: u128, y: u128): u128 { + const option = u128.checkedAdd(x, y); + if (option.isNone()) { + throw new Error('checked add overflow'); + } + return option.unwrap(); + } + + export function checkedSub(x: u128, y: u128): Option { + const result = x - y; + if (result < 0n) { + return None; + } + + return Some(u128(result)); + } + + export function checkedSubThrow(x: u128, y: u128): u128 { + const option = u128.checkedSub(x, y); + if (option.isNone()) { + throw new Error('checked sub overflow'); + } + return option.unwrap(); + } + + export function checkedMultiply(x: u128, y: u128): Option { + const result = x * y; + if (result > u128.MAX) { + return None; + } + + return Some(u128(result)); + } + + export function saturatingAdd(x: u128, y: u128): u128 { + const result = x + y; + return result > u128.MAX ? u128.MAX : u128(result); + } + + export function saturatingMultiply(x: u128, y: u128): u128 { + const result = x * y; + return result > u128.MAX ? u128.MAX : u128(result); + } + + export function saturatingSub(x: u128, y: u128): u128 { + return u128(x < y ? 0 : x - y); + } + + export function decodeVarInt(seekArray: SeekArray): Option { + try { + return Some(tryDecodeVarInt(seekArray)); + } catch (e) { + return None; + } + } + + export function tryDecodeVarInt(seekArray: SeekArray): u128 { + let result: u128 = u128(0); + for (let i = 0; i <= 18; i++) { + const byte = seekArray.readUInt8(); + if (byte === undefined) throw new Error('Unterminated or invalid data'); + + // Ensure all operations are done in bigint domain. + const byteBigint = BigInt(byte); + const value = u128(byteBigint & 0x7Fn); // Ensure the 'value' is treated as u128. + + if (i === 18 && (value & 0x7Cn) !== 0n) throw new Error('Overflow'); + + // Use bigint addition instead of bitwise OR to combine the results, + // and ensure shifting is handled correctly within the bigint domain. + result = u128(result + (value << (7n * BigInt(i)))); + + if ((byte & 0x80) === 0) return result; + } + throw new Error('Overlong encoding'); + } + + export function encodeVarInt(value: u128): Uint8Array { + const bytes = []; + while (value >> 7n > 0n) { + bytes.push(Number(value & 0x7Fn) | 0x80); + value = u128(value >> 7n); // Explicitly cast the shifted value back to u128 + } + bytes.push(Number(value & 0x7Fn)); + return new Uint8Array(bytes); + } + + export function tryIntoU64(n: u128): Option { + return n > u64.MAX ? None : Some(u64(n)); + } + + export function tryIntoU32(n: u128): Option { + return n > u32.MAX ? None : Some(u32(n)); + } + + export function tryIntoU8(n: u128): Option { + return n > u8.MAX ? None : Some(u8(n)); + } +} + +export function* getAllU128(data: Uint8Array): Generator { + const seekArray = new SeekArray(data); + while (!seekArray.isFinished()) { + const nextValue = u128.decodeVarInt(seekArray); + if (nextValue.isNone()) { + return; + } + yield nextValue.unwrap(); + } +} diff --git a/frontend/src/app/shared/ord/rune/integer/u32.ts b/frontend/src/app/shared/ord/rune/integer/u32.ts new file mode 100644 index 000000000..90e517bb8 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/integer/u32.ts @@ -0,0 +1,58 @@ +import { None, Option, Some } from '../monads'; + +/** + * A little utility type used for nominal typing. + * + * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} + */ +type BigTypedNumber = bigint & { + /** + * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! + * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. + * @ignore + * @private + * @readonly + * @type {undefined} + */ + readonly __kind__: T; +}; + +export type u32 = BigTypedNumber<'u32'>; + +export const U32_MAX_BIGINT = 0xffff_ffffn; + +export function u32(num: number | bigint): u32 { + if (typeof num == 'bigint') { + if (num < 0n || num > U32_MAX_BIGINT) { + throw new Error('num is out of range'); + } + } else { + if (!Number.isSafeInteger(num) || num < 0) { + throw new Error('num is not a valid integer'); + } + } + + return BigInt(num) as u32; +} + +export namespace u32 { + export const MAX = u32(U32_MAX_BIGINT); + + export function checkedAdd(x: u32, y: u32): Option { + const result = x + y; + if (result > u32.MAX) { + return None; + } + + return Some(u32(result)); + } + + export function checkedSub(x: u32, y: u32): Option { + const result = x - y; + if (result < 0n) { + return None; + } + + return Some(u32(result)); + } +} diff --git a/frontend/src/app/shared/ord/rune/integer/u64.ts b/frontend/src/app/shared/ord/rune/integer/u64.ts new file mode 100644 index 000000000..8010dd99c --- /dev/null +++ b/frontend/src/app/shared/ord/rune/integer/u64.ts @@ -0,0 +1,58 @@ +import { None, Option, Some } from '../monads'; + +/** + * A little utility type used for nominal typing. + * + * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} + */ +type BigTypedNumber = bigint & { + /** + * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! + * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. + * @ignore + * @private + * @readonly + * @type {undefined} + */ + readonly __kind__: T; +}; + +export type u64 = BigTypedNumber<'u64'>; + +export const U64_MAX_BIGINT = 0xffff_ffff_ffff_ffffn; + +export function u64(num: number | bigint): u64 { + if (typeof num == 'bigint') { + if (num < 0n || num > U64_MAX_BIGINT) { + throw new Error('num is out of range'); + } + } else { + if (!Number.isSafeInteger(num) || num < 0) { + throw new Error('num is not a valid integer'); + } + } + + return BigInt(num) as u64; +} + +export namespace u64 { + export const MAX = u64(U64_MAX_BIGINT); + + export function checkedAdd(x: u64, y: u64): Option { + const result = x + y; + if (result > u64.MAX) { + return None; + } + + return Some(u64(result)); + } + + export function checkedSub(x: u64, y: u64): Option { + const result = x - y; + if (result < 0n) { + return None; + } + + return Some(u64(result)); + } +} diff --git a/frontend/src/app/shared/ord/rune/integer/u8.ts b/frontend/src/app/shared/ord/rune/integer/u8.ts new file mode 100644 index 000000000..5676421b0 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/integer/u8.ts @@ -0,0 +1,58 @@ +import { None, Option, Some } from '../monads'; + +/** + * A little utility type used for nominal typing. + * + * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} + */ +type BigTypedNumber = bigint & { + /** + * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! + * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. + * @ignore + * @private + * @readonly + * @type {undefined} + */ + readonly __kind__: T; +}; + +export type u8 = BigTypedNumber<'u8'>; + +export const U8_MAX_BIGINT = 0xffn; + +export function u8(num: number | bigint): u8 { + if (typeof num == 'bigint') { + if (num < 0n || num > U8_MAX_BIGINT) { + throw new Error('num is out of range'); + } + } else { + if (!Number.isSafeInteger(num) || num < 0) { + throw new Error('num is not a valid integer'); + } + } + + return BigInt(num) as u8; +} + +export namespace u8 { + export const MAX = u8(U8_MAX_BIGINT); + + export function checkedAdd(x: u8, y: u8): Option { + const result = x + y; + if (result > u8.MAX) { + return None; + } + + return Some(u8(result)); + } + + export function checkedSub(x: u8, y: u8): Option { + const result = x - y; + if (result < 0n) { + return None; + } + + return Some(u8(result)); + } +} diff --git a/frontend/src/app/shared/ord/rune/message.ts b/frontend/src/app/shared/ord/rune/message.ts new file mode 100644 index 000000000..cad1a8ced --- /dev/null +++ b/frontend/src/app/shared/ord/rune/message.ts @@ -0,0 +1,67 @@ +import { Edict } from './edict'; +import { Flaw } from './flaw'; +import { u128, u64, u32 } from './integer'; +import { RuneId } from './runeid'; +import { Tag } from './tag'; + +export class Message { + constructor( + readonly flaws: Flaw[], + readonly edicts: Edict[], + readonly fields: Map + ) {} + + static fromIntegers(numOutputs: number, payload: u128[]): Message { + const edicts: Edict[] = []; + const fields = new Map(); + const flaws: Flaw[] = []; + + for (const i of [...Array(Math.ceil(payload.length / 2)).keys()].map((n) => n * 2)) { + const tag = payload[i]; + + if (u128(Tag.BODY) === tag) { + let id = new RuneId(u64(0), u32(0)); + const chunkSize = 4; + + const body = payload.slice(i + 1); + for (let j = 0; j < body.length; j += chunkSize) { + const chunk = body.slice(j, j + chunkSize); + if (chunk.length !== chunkSize) { + flaws.push(Flaw.TRAILING_INTEGERS); + break; + } + + const optionNext = id.next(chunk[0], chunk[1]); + if (optionNext.isNone()) { + flaws.push(Flaw.EDICT_RUNE_ID); + break; + } + const next = optionNext.unwrap(); + + const optionEdict = Edict.fromIntegers(numOutputs, next, chunk[2], chunk[3]); + if (optionEdict.isNone()) { + flaws.push(Flaw.EDICT_OUTPUT); + break; + } + const edict = optionEdict.unwrap(); + + id = next; + edicts.push(edict); + } + break; + } + + const value = payload[i + 1]; + if (value === undefined) { + flaws.push(Flaw.TRUNCATED_FIELD); + break; + } + + const values = fields.get(tag) ?? []; + values.push(value); + fields.set(tag, values); + } + + return new Message(flaws, edicts, fields); + } +} diff --git a/frontend/src/app/shared/ord/rune/monads.ts b/frontend/src/app/shared/ord/rune/monads.ts new file mode 100644 index 000000000..7822acca9 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/monads.ts @@ -0,0 +1,392 @@ +// Copied with MIT License from link below: +// https://github.com/thames-technology/monads/blob/de957d3d68449d659518d99be4ea74bbb70dfc8e/src/option/option.ts + +/** + * Type representing any value except 'undefined'. + * This is useful when working with strict null checks, ensuring that a value can be null but not undefined. + */ +type NonUndefined = {} | null; // eslint-disable-line @typescript-eslint/ban-types + +/** + * Enum-like object to represent the type of an Option (Some or None). + */ +export const OptionType = { + Some: Symbol(':some'), + None: Symbol(':none'), +}; + +/** + * Interface for handling match operations on an Option. + * Allows executing different logic based on the Option being Some or None. + */ +interface Match { + some: (val: A) => B; + none: (() => B) | B; +} + +/** + * The Option interface representing an optional value. + * An Option is either Some, holding a value, or None, indicating the absence of a value. + */ +export interface Option { + /** + * Represents the type of the Option: either Some or None. Useful for debugging and runtime checks. + */ + type: symbol; + + /** + * Determines if the Option is a Some. + * + * @returns true if the Option is Some, otherwise false. + * + * #### Example + * + * ```ts + * console.log(Some(5).isSome()); // true + * console.log(None.isSome()); // false + * ``` + */ + isSome(): boolean; + + /** + * Determines if the Option is None. + * + * @returns true if the Option is None, otherwise false. + * + * #### Example + * + * ```ts + * console.log(Some(5).isNone()); // false + * console.log(None.isNone()); // true + * ``` + */ + isNone(): boolean; + + /** + * Performs a match operation on the Option, allowing for branching logic based on its state. + * This method takes an object with functions for each case (Some or None) and executes + * the corresponding function based on the Option's state, returning the result. + * + * @param fn An object containing two properties: `some` and `none`, which are functions + * to handle the Some and None cases, respectively. + * @returns The result of applying the corresponding function based on the Option's state. + * + * #### Example + * + * ```ts + * const optionSome = Some(5); + * const matchResultSome = optionSome.match({ + * some: (value) => `The value is ${value}.`, + * none: () => 'There is no value.', + * }); + * console.log(matchResultSome); // Outputs: "The value is 5." + * + * const optionNone = None; + * const matchResultNone = optionNone.match({ + * some: (value) => `The value is ${value}.`, + * none: () => 'There is no value.', + * }); + * console.log(matchResultNone); // Outputs: "There is no value." + * ``` + */ + match(fn: Match): U; + + /** + * Applies a function to the contained value (if any), or returns a default if None. + * + * @param fn A function that takes a value of type T and returns a value of type U. + * @returns An Option containing the function's return value if the original Option is Some, otherwise None. + * + * #### Examples + * + * ```ts + * const length = Some("hello").map(s => s.length); // Some(5) + * const noneLength = None.map(s => s.length); // None + * ``` + */ + map(fn: (val: T) => U): Option; + + inspect(fn: (val: T) => void): Option; + + /** + * Transforms the Option into another by applying a function to the contained value, + * chaining multiple potentially failing operations. + * + * @param fn A function that takes a value of type T and returns an Option of type U. + * @returns The Option returned by the function if the original Option is Some, otherwise None. + * + * #### Examples + * + * ```ts + * const parse = (s: string) => { + * const parsed = parseInt(s); + * return isNaN(parsed) ? None : Some(parsed); + * }; + * const result = Some("123").andThen(parse); // Some(123) + * const noResult = Some("abc").andThen(parse); // None + * ``` + */ + andThen(fn: (val: T) => Option): Option; + + /** + * Returns this Option if it is Some, otherwise returns the option provided as a parameter. + * + * @param optb The alternative Option to return if the original Option is None. + * @returns The original Option if it is Some, otherwise `optb`. + * + * #### Examples + * + * ```ts + * const defaultOption = Some("default"); + * const someOption = Some("some").or(defaultOption); // Some("some") + * const noneOption = None.or(defaultOption); // Some("default") + * ``` + */ + or(optb: Option): Option; + + orElse(optb: () => Option): Option; + + /** + * Returns the option provided as a parameter if the original Option is Some, otherwise returns None. + * + * @param optb The Option to return if the original Option is Some. + * @returns `optb` if the original Option is Some, otherwise None. + * + * #### Examples + * + * ```ts + * const anotherOption = Some("another"); + * const someOption = Some("some").and(anotherOption); // Some("another") + * const noneOption = None.and(anotherOption); // None + * ``` + */ + and(optb: Option): Option; + + /** + * Returns the contained value if Some, otherwise returns the provided default value. + * + * @param def The default value to return if the Option is None. + * @returns The contained value if Some, otherwise `def`. + * + * #### Examples + * + * ```ts + * const someValue = Some("value").unwrapOr("default"); // "value" + * const noneValue = None.unwrapOr("default"); // "default" + * ``` + */ + unwrapOr(def: T): T; + + /** + * Unwraps an Option, yielding the contained value if Some, otherwise throws an error. + * + * @returns The contained value. + * @throws Error if the Option is None. + * + * #### Examples + * + * ```ts + * console.log(Some("value").unwrap()); // "value" + * console.log(None.unwrap()); // throws Error + * ``` + */ + unwrap(): T | never; +} + +/** + * Implementation of Option representing a value (Some). + */ +interface SomeOption extends Option { + unwrap(): T; +} + +/** + * Implementation of Option representing the absence of a value (None). + */ +interface NoneOption extends Option { + unwrap(): never; +} + +/** + * Represents a Some value of Option. + */ +class SomeImpl implements SomeOption { + constructor(private readonly val: T) {} + + get type() { + return OptionType.Some; + } + + isSome() { + return true; + } + + isNone() { + return false; + } + + match(fn: Match): B { + return fn.some(this.val); + } + + map(fn: (val: T) => U): Option { + return Some(fn(this.val)); + } + + inspect(fn: (val: T) => void): Option { + fn(this.val); + return this; + } + + andThen(fn: (val: T) => Option): Option { + return fn(this.val); + } + + or(_optb: Option): Option { + return this; + } + + orElse(optb: () => Option): Option { + return this; + } + + and(optb: Option): Option { + return optb; + } + + unwrapOr(_def: T): T { + return this.val; + } + + unwrap(): T { + return this.val; + } +} + +/** + * Represents a None value of Option. + */ +class NoneImpl implements NoneOption { + get type() { + return OptionType.None; + } + + isSome() { + return false; + } + + isNone() { + return true; + } + + match({ none }: Match): U { + if (typeof none === 'function') { + return (none as () => U)(); + } + + return none; + } + + map(_fn: (val: T) => U): Option { + return new NoneImpl(); + } + + inspect(fn: (val: T) => void): Option { + return this; + } + + andThen(_fn: (val: T) => Option): Option { + return new NoneImpl(); + } + + or(optb: Option): Option { + return optb; + } + + orElse(optb: () => Option): Option { + return optb(); + } + + and(_optb: Option): Option { + return new NoneImpl(); + } + + unwrapOr(def: T): T { + return def; + } + + unwrap(): never { + throw new ReferenceError('Trying to unwrap None.'); + } +} + +/** + * Creates a Some instance of Option containing the given value. + * This function is used to represent the presence of a value in an operation that may not always produce a value. + * + * @param val The value to be wrapped in a Some Option. + * @returns An Option instance representing the presence of a value. + * + * #### Example + * + * ```ts + * const option = Some(42); + * console.log(option.unwrap()); // Outputs: 42 + * ``` + */ +export function Some(val: T): Option { + return new SomeImpl(val); +} + +/** + * The singleton instance representing None, an Option with no value. + * This constant is used to represent the absence of a value in operations that may not always produce a value. + * + * #### Example + * + * ```ts + * const option = None; + * console.log(option.isNone()); // Outputs: true + * ``` + */ +export const None: Option = new NoneImpl(); // eslint-disable-line @typescript-eslint/no-explicit-any + +/** + * Type guard to check if an Option is a Some value. + * This function is used to narrow down the type of an Option to SomeOption in TypeScript's type system. + * + * @param val The Option to be checked. + * @returns true if the provided Option is a SomeOption, false otherwise. + * + * #### Example + * + * ```ts + * const option = Some('Success'); + * if (isSome(option)) { + * console.log('Option has a value:', option.unwrap()); + * } + * ``` + */ +export function isSome(val: Option): val is SomeOption { + return val.isSome(); +} + +/** + * Type guard to check if an Option is a None value. + * This function is used to narrow down the type of an Option to NoneOption in TypeScript's type system. + * + * @param val The Option to be checked. + * @returns true if the provided Option is a NoneOption, false otherwise. + * + * #### Example + * + * ```ts + * const option = None; + * if (isNone(option)) { + * console.log('Option does not have a value.'); + * } + * ``` + */ +export function isNone(val: Option): val is NoneOption { + return val.isNone(); +} diff --git a/frontend/src/app/shared/ord/rune/rune.ts b/frontend/src/app/shared/ord/rune/rune.ts new file mode 100644 index 000000000..c0dd96e1b --- /dev/null +++ b/frontend/src/app/shared/ord/rune/rune.ts @@ -0,0 +1,23 @@ +import { u128 } from './integer'; + +export class Rune { + + constructor(readonly value: u128) {} + + toString() { + let n = this.value; + + if (n === u128.MAX) { + return 'BCGDENLQRQWDSLRUGSNLBTMFIJAV'; + } + + n = u128(n + 1n); + let symbol = ''; + while (n > 0) { + symbol = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Number((n - 1n) % 26n)] + symbol; + n = u128((n - 1n) / 26n); + } + + return symbol; + } +} diff --git a/frontend/src/app/shared/ord/rune/runeid.ts b/frontend/src/app/shared/ord/rune/runeid.ts new file mode 100644 index 000000000..ca0e938b7 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/runeid.ts @@ -0,0 +1,89 @@ +import { None, Option, Some } from './monads'; +import { u64, u32, u128 } from './integer'; + +export class RuneId { + constructor(readonly block: u64, readonly tx: u32) {} + + static new(block: u64, tx: u32): Option { + const id = new RuneId(block, tx); + + if (id.block === 0n && id.tx > 0) { + return None; + } + + return Some(id); + } + + static sort(runeIds: RuneId[]): RuneId[] { + return [...runeIds].sort((x, y) => Number(x.block - y.block || x.tx - y.tx)); + } + + delta(next: RuneId): Option<[u128, u128]> { + const optionBlock = u64.checkedSub(next.block, this.block); + if (optionBlock.isNone()) { + return None; + } + const block = optionBlock.unwrap(); + + let tx: u32; + if (block === 0n) { + const optionTx = u32.checkedSub(next.tx, this.tx); + if (optionTx.isNone()) { + return None; + } + tx = optionTx.unwrap(); + } else { + tx = next.tx; + } + + return Some([u128(block), u128(tx)]); + } + + next(block: u128, tx: u128): Option { + const optionBlock = u128.tryIntoU64(block); + const optionTx = u128.tryIntoU32(tx); + + if (optionBlock.isNone() || optionTx.isNone()) { + return None; + } + + const blockU64 = optionBlock.unwrap(); + const txU32 = optionTx.unwrap(); + + const nextBlock = u64.checkedAdd(this.block, blockU64); + if (nextBlock.isNone()) { + return None; + } + + let nextTx: u32; + if (blockU64 === 0n) { + const optionAdd = u32.checkedAdd(this.tx, txU32); + if (optionAdd.isNone()) { + return None; + } + + nextTx = optionAdd.unwrap(); + } else { + nextTx = txU32; + } + + return RuneId.new(nextBlock.unwrap(), nextTx); + } + + toString() { + return `${this.block}:${this.tx}`; + } + + static fromString(s: string) { + const parts = s.split(':'); + if (parts.length !== 2) { + throw new Error(`invalid rune ID: ${s}`); + } + + const [block, tx] = parts; + if (!/^\d+$/.test(block) || !/^\d+$/.test(tx)) { + throw new Error(`invalid rune ID: ${s}`); + } + return new RuneId(u64(BigInt(block)), u32(BigInt(tx))); + } +} diff --git a/frontend/src/app/shared/ord/rune/runestone.ts b/frontend/src/app/shared/ord/rune/runestone.ts new file mode 100644 index 000000000..c71cdcd90 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/runestone.ts @@ -0,0 +1,258 @@ +import { concatUint8Arrays, hexToBytes } from '../inscription.utils'; +import { Artifact } from './artifact'; +import { Cenotaph } from './cenotaph'; +import { MAGIC_NUMBER, MAX_DIVISIBILITY, OP_RETURN } from './constants'; +import { Edict } from './edict'; +import { Etching } from './etching'; +import { Flag } from './flag'; +import { Flaw } from './flaw'; +import { u128, u32, u64, u8 } from './integer'; +import { Message } from './message'; +import { None, Option, Some } from './monads'; +import { Rune } from './rune'; +import { RuneId } from './runeid'; +import { script } from './script'; +import { SeekArray } from './seekarray'; +import { Tag } from './tag'; + +export const MAX_SPACERS = 0b00000111_11111111_11111111_11111111; + +export const UNCOMMON_GOODS = new Etching( + Some(u8(0)), + Some(new Rune(u128(2055900680524219742n))), + Some(u32(128)), + Some('⧉'), + Some({ + amount: Some(u128(1)), + cap: Some(u128(340282366920938463463374607431768211455n)), + height: [Some(u64(840000)), Some(u64(1050000))], + offset: [Some(u64(0)), Some(u64(0))], + }), + Some(u128(0)), + false +); + +// New: Esplora format instead of Bitcoin RPC format +export type RunestoneTx = { + vout: { + scriptpubkey: string + }[]; +}; + +type Payload = Uint8Array | Flaw; + +export class Runestone { + readonly type = 'runestone'; + + constructor( + readonly mint: Option, + readonly pointer: Option, + readonly edicts: Edict[], + readonly etching: Option + ) {} + + static decipher(transaction: RunestoneTx): Option { + const optionPayload = Runestone.payload(transaction); + if (optionPayload.isNone()) { + return None; + } + const payload = optionPayload.unwrap(); + if (!(payload instanceof Uint8Array)) { + return Some(new Cenotaph([payload])); + } + + const optionIntegers = Runestone.integers(payload); + if (optionIntegers.isNone()) { + return Some(new Cenotaph([Flaw.VARINT])); + } + + const { flaws, edicts, fields } = Message.fromIntegers( + transaction.vout.length, + optionIntegers.unwrap() + ); + + let flags = Tag.take(Tag.FLAGS, fields, 1, ([value]) => Some(value)).unwrapOr(u128(0)); + + const etchingResult = Flag.take(flags, Flag.ETCHING); + const etchingFlag = etchingResult.set; + flags = etchingResult.flags; + + const etching: Option = etchingFlag + ? (() => { + const divisibility = Tag.take( + Tag.DIVISIBILITY, + fields, + 1, + ([value]): Option => + u128 + .tryIntoU8(value) + .andThen((value) => (value <= MAX_DIVISIBILITY ? Some(value) : None)) + ); + + const rune = Tag.take(Tag.RUNE, fields, 1, ([value]) => Some(new Rune(value))); + + const spacers = Tag.take( + Tag.SPACERS, + fields, + 1, + ([value]): Option => + u128.tryIntoU32(value).andThen((value) => (value <= MAX_SPACERS ? Some(value) : None)) + ); + + const symbol = Tag.take(Tag.SYMBOL, fields, 1, ([value]) => + u128.tryIntoU32(value).andThen((value) => { + try { + return Some(String.fromCodePoint(Number(value))); + } catch (e) { + return None; + } + }) + ); + + const termsResult = Flag.take(flags, Flag.TERMS); + const termsFlag = termsResult.set; + flags = termsResult.flags; + + const terms = termsFlag + ? (() => { + const amount = Tag.take(Tag.AMOUNT, fields, 1, ([value]) => Some(value)); + + const cap = Tag.take(Tag.CAP, fields, 1, ([value]) => Some(value)); + + const offset = [ + Tag.take(Tag.OFFSET_START, fields, 1, ([value]) => u128.tryIntoU64(value)), + Tag.take(Tag.OFFSET_END, fields, 1, ([value]) => u128.tryIntoU64(value)), + ] as const; + + const height = [ + Tag.take(Tag.HEIGHT_START, fields, 1, ([value]) => u128.tryIntoU64(value)), + Tag.take(Tag.HEIGHT_END, fields, 1, ([value]) => u128.tryIntoU64(value)), + ] as const; + + return Some({ amount, cap, offset, height }); + })() + : None; + + const premine = Tag.take(Tag.PREMINE, fields, 1, ([value]) => Some(value)); + + const turboResult = Flag.take(flags, Flag.TURBO); + const turbo = etchingResult.set; + flags = turboResult.flags; + + return Some(new Etching(divisibility, rune, spacers, symbol, terms, premine, turbo)); + })() + : None; + + const mint = Tag.take(Tag.MINT, fields, 2, ([block, tx]): Option => { + const optionBlockU64 = u128.tryIntoU64(block); + const optionTxU32 = u128.tryIntoU32(tx); + + if (optionBlockU64.isNone() || optionTxU32.isNone()) { + return None; + } + + return RuneId.new(optionBlockU64.unwrap(), optionTxU32.unwrap()); + }); + + const pointer = Tag.take( + Tag.POINTER, + fields, + 1, + ([value]): Option => + u128 + .tryIntoU32(value) + .andThen((value) => (value < transaction.vout.length ? Some(value) : None)) + ); + + if (etching.map((etching) => etching.supply.isNone()).unwrapOr(false)) { + flaws.push(Flaw.SUPPLY_OVERFLOW); + } + + if (flags !== 0n) { + flaws.push(Flaw.UNRECOGNIZED_FLAG); + } + + if ([...fields.keys()].find((tag) => tag % 2n === 0n) !== undefined) { + flaws.push(Flaw.UNRECOGNIZED_EVEN_TAG); + } + + if (flaws.length !== 0) { + return Some( + new Cenotaph( + flaws, + etching.andThen((etching) => etching.rune), + mint + ) + ); + } + + return Some(new Runestone(mint, pointer, edicts, etching)); + } + + static payload(transaction: RunestoneTx): Option { + // search transaction outputs for payload + for (const output of transaction.vout) { + const instructions = script.decompile(hexToBytes(output.scriptpubkey)); + if (instructions === null) { + throw new Error('unable to decompile'); + } + + // payload starts with OP_RETURN + let nextInstructionResult = instructions.next(); + if (nextInstructionResult.done || nextInstructionResult.value !== OP_RETURN) { + continue; + } + + // followed by the protocol identifier + nextInstructionResult = instructions.next(); + if ( + nextInstructionResult.done || + nextInstructionResult.value instanceof Uint8Array || + nextInstructionResult.value !== MAGIC_NUMBER + ) { + continue; + } + + // construct the payload by concatinating remaining data pushes + let payloads: Uint8Array[] = []; + + do { + nextInstructionResult = instructions.next(); + + if (nextInstructionResult.done) { + const decodedSuccessfully = nextInstructionResult.value; + if (!decodedSuccessfully) { + return Some(Flaw.INVALID_SCRIPT); + } + break; + } + + const instruction = nextInstructionResult.value; + if (instruction instanceof Uint8Array) { + payloads.push(instruction); + } else { + return Some(Flaw.OPCODE); + } + } while (true); + + return Some(concatUint8Arrays(payloads)); + } + + return None; + } + + static integers(payload: Uint8Array): Option { + const integers: u128[] = []; + + const seekArray = new SeekArray(payload); + while (!seekArray.isFinished()) { + const optionInt = u128.decodeVarInt(seekArray); + if (optionInt.isNone()) { + return None; + } + integers.push(optionInt.unwrap()); + } + + return Some(integers); + } +} diff --git a/frontend/src/app/shared/ord/rune/script.ts b/frontend/src/app/shared/ord/rune/script.ts new file mode 100644 index 000000000..67d579ab8 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/script.ts @@ -0,0 +1,237 @@ +namespace pushdata { + /** + * Calculates the encoding length of a number used for push data in Bitcoin transactions. + * @param i The number to calculate the encoding length for. + * @returns The encoding length of the number. + */ + export function encodingLength(i: number): number { + return i < OPS.OP_PUSHDATA1 ? 1 : i <= 0xff ? 2 : i <= 0xffff ? 3 : 5; + } + + /** + * Decodes a byte array and returns information about the opcode, number, and size. + * @param array - The byte array to decode. + * @param offset - The offset within the array to start decoding. + * @returns An object containing the opcode, number, and size, or null if decoding fails. + */ + export function decode( + array: Uint8Array, + offset: number + ): { + opcode: number; + number: number; + size: number; + } | null { + const dataView = new DataView(array.buffer, array.byteOffset, array.byteLength); + const opcode = dataView.getUint8(offset); + let num: number; + let size: number; + + // ~6 bit + if (opcode < OPS.OP_PUSHDATA1) { + num = opcode; + size = 1; + + // 8 bit + } else if (opcode === OPS.OP_PUSHDATA1) { + if (offset + 2 > array.length) return null; + num = dataView.getUint8(offset + 1); + size = 2; + + // 16 bit + } else if (opcode === OPS.OP_PUSHDATA2) { + if (offset + 3 > array.length) return null; + num = dataView.getUint16(offset + 1, true); // true for little-endian + size = 3; + + // 32 bit + } else { + if (offset + 5 > array.length) return null; + if (opcode !== OPS.OP_PUSHDATA4) throw new Error('Unexpected opcode'); + + num = dataView.getUint32(offset + 1, true); // true for little-endian + size = 5; + } + + return { + opcode, + number: num, + size, + }; + } +} + +const OPS = { + OP_FALSE: 0, + OP_0: 0, + OP_PUSHDATA1: 76, + OP_PUSHDATA2: 77, + OP_PUSHDATA4: 78, + OP_1NEGATE: 79, + OP_RESERVED: 80, + OP_TRUE: 81, + OP_1: 81, + OP_2: 82, + OP_3: 83, + OP_4: 84, + OP_5: 85, + OP_6: 86, + OP_7: 87, + OP_8: 88, + OP_9: 89, + OP_10: 90, + OP_11: 91, + OP_12: 92, + OP_13: 93, + OP_14: 94, + OP_15: 95, + OP_16: 96, + + OP_NOP: 97, + OP_VER: 98, + OP_IF: 99, + OP_NOTIF: 100, + OP_VERIF: 101, + OP_VERNOTIF: 102, + OP_ELSE: 103, + OP_ENDIF: 104, + OP_VERIFY: 105, + OP_RETURN: 106, + + OP_TOALTSTACK: 107, + OP_FROMALTSTACK: 108, + OP_2DROP: 109, + OP_2DUP: 110, + OP_3DUP: 111, + OP_2OVER: 112, + OP_2ROT: 113, + OP_2SWAP: 114, + OP_IFDUP: 115, + OP_DEPTH: 116, + OP_DROP: 117, + OP_DUP: 118, + OP_NIP: 119, + OP_OVER: 120, + OP_PICK: 121, + OP_ROLL: 122, + OP_ROT: 123, + OP_SWAP: 124, + OP_TUCK: 125, + + OP_CAT: 126, + OP_SUBSTR: 127, + OP_LEFT: 128, + OP_RIGHT: 129, + OP_SIZE: 130, + + OP_INVERT: 131, + OP_AND: 132, + OP_OR: 133, + OP_XOR: 134, + OP_EQUAL: 135, + OP_EQUALVERIFY: 136, + OP_RESERVED1: 137, + OP_RESERVED2: 138, + + OP_1ADD: 139, + OP_1SUB: 140, + OP_2MUL: 141, + OP_2DIV: 142, + OP_NEGATE: 143, + OP_ABS: 144, + OP_NOT: 145, + OP_0NOTEQUAL: 146, + OP_ADD: 147, + OP_SUB: 148, + OP_MUL: 149, + OP_DIV: 150, + OP_MOD: 151, + OP_LSHIFT: 152, + OP_RSHIFT: 153, + + OP_BOOLAND: 154, + OP_BOOLOR: 155, + OP_NUMEQUAL: 156, + OP_NUMEQUALVERIFY: 157, + OP_NUMNOTEQUAL: 158, + OP_LESSTHAN: 159, + OP_GREATERTHAN: 160, + OP_LESSTHANOREQUAL: 161, + OP_GREATERTHANOREQUAL: 162, + OP_MIN: 163, + OP_MAX: 164, + + OP_WITHIN: 165, + + OP_RIPEMD160: 166, + OP_SHA1: 167, + OP_SHA256: 168, + OP_HASH160: 169, + OP_HASH256: 170, + OP_CODESEPARATOR: 171, + OP_CHECKSIG: 172, + OP_CHECKSIGVERIFY: 173, + OP_CHECKMULTISIG: 174, + OP_CHECKMULTISIGVERIFY: 175, + + OP_NOP1: 176, + + OP_NOP2: 177, + OP_CHECKLOCKTIMEVERIFY: 177, + + OP_NOP3: 178, + OP_CHECKSEQUENCEVERIFY: 178, + + OP_NOP4: 179, + OP_NOP5: 180, + OP_NOP6: 181, + OP_NOP7: 182, + OP_NOP8: 183, + OP_NOP9: 184, + OP_NOP10: 185, + + OP_CHECKSIGADD: 186, + + OP_PUBKEYHASH: 253, + OP_PUBKEY: 254, + OP_INVALIDOPCODE: 255, +} as const; + +export const opcodes = OPS; + +export namespace script { + export type Instruction = number | Uint8Array; + + export function* decompile(array: Uint8Array): Generator { + let i = 0; + + while (i < array.length) { + const opcode = array[i]; + + // data chunk + if (opcode >= OPS.OP_0 && opcode <= OPS.OP_PUSHDATA4) { + const d = pushdata.decode(array, i); + + // did reading a pushDataInt fail? + if (d === null) return false; + i += d.size; + + // attempt to read too much data? + if (i + d.number > array.length) return false; + + const data = array.subarray(i, i + d.number); + i += d.number; + + yield data; + + // opcode + } else { + yield opcode; + + i += 1; + } + } + + return true; + } +} diff --git a/frontend/src/app/shared/ord/rune/seekarray.ts b/frontend/src/app/shared/ord/rune/seekarray.ts new file mode 100644 index 000000000..1f465cbd3 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/seekarray.ts @@ -0,0 +1,43 @@ +/** + * This class provides a way to read data sequentially from a Uint8Array with automatic cursor management. + * It utilizes DataView for handling multi-byte data types. + * + * This replaces the SeekBuffer from the original runestone-lib! + */ +export class SeekArray { + + public seekIndex: number = 0; + private dataView: DataView; + + /** + * Constructs a SeekArray instance. + * + * @param array - The Uint8Array from which data will be read. + */ + constructor(private array: Uint8Array) { + this.dataView = new DataView(array.buffer, array.byteOffset, array.byteLength); + } + + /** + * Reads an unsigned 8-bit integer from the current position and advances the seek index by 1 byte. + * + * @returns The read value or undefined if reading beyond the end of the array. + */ + readUInt8(): number | undefined { + if (this.isFinished()) { + return undefined; + } + const value = this.dataView.getUint8(this.seekIndex); + this.seekIndex += 1; + return value; + } + + /** + * Checks if the seek index has reached or surpassed the length of the underlying array. + * + * @returns true if there are no more bytes to read, false otherwise. + */ + isFinished(): boolean { + return this.seekIndex >= this.array.length; + } +} diff --git a/frontend/src/app/shared/ord/rune/spacedrune.ts b/frontend/src/app/shared/ord/rune/spacedrune.ts new file mode 100644 index 000000000..b00b0da3a --- /dev/null +++ b/frontend/src/app/shared/ord/rune/spacedrune.ts @@ -0,0 +1,21 @@ +import { Rune } from './rune'; + +export class SpacedRune { + constructor(readonly rune: Rune, readonly spacers: number) {} + + toString(): string { + const rune = this.rune.toString(); + let i = 0; + let result = ''; + for (const c of rune) { + result += c; + + if (i < rune.length - 1 && (this.spacers & (1 << i)) !== 0) { + result += '•'; + } + i++; + } + + return result; + } +} diff --git a/frontend/src/app/shared/ord/rune/tag.ts b/frontend/src/app/shared/ord/rune/tag.ts new file mode 100644 index 000000000..8e39925d4 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/tag.ts @@ -0,0 +1,60 @@ +import { None, Option, Some } from './monads'; +import { u128 } from './integer'; +import { FixedArray } from './utils'; + +export enum Tag { + BODY = 0, + FLAGS = 2, + RUNE = 4, + + PREMINE = 6, + CAP = 8, + AMOUNT = 10, + HEIGHT_START = 12, + HEIGHT_END = 14, + OFFSET_START = 16, + OFFSET_END = 18, + MINT = 20, + POINTER = 22, + CENOTAPH = 126, + + DIVISIBILITY = 1, + SPACERS = 3, + SYMBOL = 5, + NOP = 127, +} + +export namespace Tag { + export function take( + tag: Tag, + fields: Map, + n: N, + withFn: (values: FixedArray) => Option + ): Option { + const field = fields.get(u128(tag)); + if (field === undefined) { + return None; + } + + const values: u128[] = []; + for (const i of [...Array(n).keys()]) { + if (field[i] === undefined) { + return None; + } + values[i] = field[i]; + } + + const optionValue = withFn(values as FixedArray); + if (optionValue.isNone()) { + return None; + } + + field.splice(0, n); + + if (field.length === 0) { + fields.delete(u128(tag)); + } + + return Some(optionValue.unwrap()); + } +} diff --git a/frontend/src/app/shared/ord/rune/terms.ts b/frontend/src/app/shared/ord/rune/terms.ts new file mode 100644 index 000000000..464c166e0 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/terms.ts @@ -0,0 +1,9 @@ +import { Option } from './monads'; +import { u128, u64 } from './integer'; + +export type Terms = { + amount: Option; + cap: Option; + height: readonly [Option, Option]; + offset: readonly [Option, Option]; +}; diff --git a/frontend/src/app/shared/ord/rune/utils.ts b/frontend/src/app/shared/ord/rune/utils.ts new file mode 100644 index 000000000..a6fa8e0a1 --- /dev/null +++ b/frontend/src/app/shared/ord/rune/utils.ts @@ -0,0 +1,6 @@ +type GrowToSize = A['length'] extends N + ? A + : GrowToSize; + +export type FixedArray = GrowToSize; +