diff --git a/README.md b/README.md index defc9d5..ed86c85 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,118 @@ # runestone-lib -A Typescript implementation of the Bitcoin Runestone protocol +This is a Typescript implementation of the Bitcoin Runestone protocol. +To see the original version, please go to the [Ordinals repo](/ordinals/ord); +you can find various [data structures](/ordinals/ord/tree/master/crates/ordinals/src) and +[indexer implementation](/ordinals/ord/blob/master/src/index/updater/rune_updater.rs) there. +General documentation of the runes protocol and how runestones are used can be found +[here](https://docs.ordinals.com/runes.html). + +## Encode Runestone + +To encode a runestone, use `encodeRunestone()` method, with an example below: + +```ts +import { encodeRunestone } from '@magiceden-oss/runestone-lib'; + +// To deploy a new rune ticker +// (this will require a commitment in an input script) +const etchingRunestone = encodeRunestone({ + etching: { + runeName: 'THIS•IS•AN•EXAMPLE•RUNE', + divisibility: 0, + premine: 0, + symbol: '', + terms: { + cap: 69, + amount: 420, + offset: { + end: 9001, + }, + }, + turbo: true, + }, +}); + +// To mint UNCOMMON•GOODS +const mintRunestone = encodeRunestone({ + mint: { + block: 1n, + tx: 0, + }, +}); + +// Transfer 10 UNCOMMON•GOODS to output 1 +const edictRunestone = encodeRunestone({ + edicts: [ + { + id: { + block: 1n, + tx: 0, + }, + amount: 10n, + output: 1, + }, + ], +}); +``` + +## Decode Runestone + +Decoding a runestone within a transaction is as simple as passing in +the transaction data from Bitcoin Core RPC server. + +```ts +import { + tryDecodeRunestone, + isRunestoneArtifact, + RunestoneSpec, + Cenotaph +} from '@magiceden-oss/runestone-lib'; + +// transaction retrieved with getrawtransaction RPC call +const tx = ...; + +const artifact = tryDecodeRunestone(tx); + +if (isRunestone(artifact)) { + const runestone: RunestoneSpec = artifact; + ... +} else { + const cenotaph: Cenotaph = artifact; + ... +} +``` + +## Indexing + +To index, initialize a RunestoneIndexer, implement the interface arguments +to RunestoneIndexer constructor. Then it is just a matter of start() to finish +initializing the indexer, and then controlling the rate of syncing indexing +to latest state in RPC server. + +```ts +// Initialize indexer +const indexer = new RunestoneIndexer(...); + +// Preps the indexer to be ready to run updateRuneUtxoBalances() +await indexer.start() + +// Example of a polling job running updateRuneUtxoBalances() +// every minute, with stop cleanup handling +let stop = false; +... + +const intervalId = setInterval(async () => { + try { + await index.updateRuneUtxoBalances(); + } catch (err) { + console.error('Error occurred while indexing runes', err); + } + + if (stop) { + clearInterval(intervalId); + await indexer.stop(); + } +}, 60 * 1000 /* one minute */); + +``` diff --git a/index.ts b/index.ts index 0aeb1df..9fb4924 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,7 @@ -import { isRunestone } from './src/artifact'; +import { isRunestone as isRunestoneArtifact } from './src/artifact'; import { MAX_DIVISIBILITY } from './src/constants'; import { Etching } from './src/etching'; +import { Flaw as FlawEnum } from './src/flaw'; import { RuneEtchingSpec } from './src/indexer'; import { u128, u32, u64, u8 } from './src/integer'; import { None, Option, Some } from './src/monads'; @@ -56,8 +57,20 @@ export type RunestoneSpec = { }[]; }; +export type Flaw = + | 'edict_output' + | 'edict_rune_id' + | 'invalid_script' + | 'opcode' + | 'supply_overflow' + | 'trailing_integers' + | 'truncated_field' + | 'unrecognized_even_tag' + | 'unrecognized_flag' + | 'varint'; + export type Cenotaph = { - flaws: string[]; + flaws: Flaw[]; etching?: string; mint?: { block: bigint; @@ -65,6 +78,31 @@ export type Cenotaph = { }; }; +function getFlawString(flaw: FlawEnum): Flaw { + switch (flaw) { + case FlawEnum.EDICT_OUTPUT: + return 'edict_output'; + case FlawEnum.EDICT_RUNE_ID: + return 'edict_rune_id'; + case FlawEnum.INVALID_SCRIPT: + return 'invalid_script'; + case FlawEnum.OPCODE: + return 'opcode'; + case FlawEnum.SUPPLY_OVERFLOW: + return 'supply_overflow'; + case FlawEnum.TRAILING_INTEGERS: + return 'trailing_integers'; + case FlawEnum.TRUNCATED_FIELD: + return 'truncated_field'; + case FlawEnum.UNRECOGNIZED_EVEN_TAG: + return 'unrecognized_even_tag'; + case FlawEnum.UNRECOGNIZED_FLAG: + return 'unrecognized_flag'; + case FlawEnum.VARINT: + return 'varint'; + } +} + // Helper functions to ensure numbers fit the desired type correctly const u8Strict = (n: number) => { const bigN = BigInt(n); @@ -195,6 +233,10 @@ export function encodeRunestone(runestone: RunestoneSpec): { }; } +export function isRunestone(artifact: RunestoneSpec | Cenotaph): artifact is RunestoneSpec { + return !('flaws' in artifact); +} + export function tryDecodeRunestone(tx: RunestoneTx): RunestoneSpec | Cenotaph | null { const optionArtifact = Runestone.decipher(tx); if (optionArtifact.isNone()) { @@ -202,7 +244,7 @@ export function tryDecodeRunestone(tx: RunestoneTx): RunestoneSpec | Cenotaph | } const artifact = optionArtifact.unwrap(); - if (isRunestone(artifact)) { + if (isRunestoneArtifact(artifact)) { const runestone = artifact; const etching = () => runestone.etching.unwrap(); @@ -286,7 +328,7 @@ export function tryDecodeRunestone(tx: RunestoneTx): RunestoneSpec | Cenotaph | } else { const cenotaph = artifact; return { - flaws: [], + flaws: cenotaph.flaws.map(getFlawString), ...(cenotaph.etching.isSome() ? { etching: cenotaph.etching.unwrap().toString() } : {}), ...(cenotaph.mint.isSome() ? { mint: { block: cenotaph.mint.unwrap().block, tx: Number(cenotaph.mint.unwrap().tx) } } diff --git a/package.json b/package.json index f196de7..439ac28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@magiceden-oss/runestone-lib", - "version": "0.9.10-alpha", + "version": "1.0.0", "description": "", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/indexer/types.ts b/src/indexer/types.ts index 237f1e5..ea50f08 100644 --- a/src/indexer/types.ts +++ b/src/indexer/types.ts @@ -71,16 +71,8 @@ export interface RunestoneStorage { export type RunestoneIndexerOptions = { bitcoinRpcClient: BitcoinRpcClient; - network: Network; - storage: RunestoneStorage; - - /** - * The interval at which to poll the RPC for new blocks, in milliseconds. - * Defaults to `10000` (10 seconds), and must be positive. - */ - pollIntervalMs?: number; }; export type BlockIdentifier = { @@ -119,7 +111,7 @@ export type RuneUtxoBalance = { export type RuneMintCount = { mint: RuneLocation; count: number }; export type RuneBalance = { runeId: RuneLocation; amount: bigint }; -export type RuneEtchingBase = { +type RuneEtchingBase = { divisibility?: number; premine?: bigint; symbol?: string;