Skip to content

Commit 460ee06

Browse files
authored
Expose encodeRunestoneUnsafe() function for lib (#21)
1 parent e09e338 commit 460ee06

File tree

7 files changed

+210
-60
lines changed

7 files changed

+210
-60
lines changed

index.ts

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,151 @@
1-
export * from './src/indexer';
1+
import { None, Option, Some } from '@sniptt/monads';
2+
import _ from 'lodash';
3+
import { u64, u32, u128, u8 } from './src/integer';
4+
import { Runestone } from './src/runestone';
5+
import { RuneEtchingSpec } from './src/indexer';
6+
import { RuneId } from './src/runeid';
7+
import { Rune } from './src/rune';
8+
import { Etching } from './src/etching';
9+
import { Terms } from './src/terms';
10+
import { MAX_DIVISIBILITY } from './src/constants';
11+
12+
export {
13+
BlockInfo,
14+
RuneBlockIndex,
15+
RuneEtching,
16+
RuneEtchingSpec,
17+
RuneMint,
18+
RuneUtxoBalance,
19+
RunestoneIndexer,
20+
RunestoneIndexerOptions,
21+
RunestoneStorage,
22+
} from './src/indexer';
23+
24+
export type RunestoneSpec = {
25+
mint?: {
26+
block: bigint;
27+
tx: number;
28+
};
29+
pointer?: number;
30+
etching?: RuneEtchingSpec;
31+
edicts?: {
32+
id: {
33+
block: bigint;
34+
tx: number;
35+
};
36+
amount: bigint;
37+
output: number;
38+
}[];
39+
};
40+
41+
// Helper functions to ensure numbers fit the desired type correctly
42+
const u8Strict = (n: number) => {
43+
const bigN = BigInt(n);
44+
if (bigN < 0n || bigN > u8.MAX) {
45+
throw Error('u8 overflow');
46+
}
47+
return u8(bigN);
48+
};
49+
const u32Strict = (n: number) => {
50+
const bigN = BigInt(n);
51+
if (bigN < 0n || bigN > u32.MAX) {
52+
throw Error('u32 overflow');
53+
}
54+
return u32(bigN);
55+
};
56+
const u64Strict = (n: bigint) => {
57+
const bigN = BigInt(n);
58+
if (bigN < 0n || bigN > u64.MAX) {
59+
throw Error('u64 overflow');
60+
}
61+
return u64(bigN);
62+
};
63+
const u128Strict = (n: bigint) => {
64+
const bigN = BigInt(n);
65+
if (bigN < 0n || bigN > u128.MAX) {
66+
throw Error('u128 overflow');
67+
}
68+
return u128(bigN);
69+
};
70+
71+
/**
72+
* Low level function to allow for encoding runestones without any indexer and transaction checks.
73+
*
74+
* @param runestone runestone spec to encode as runestone
75+
* @returns encoded runestone bytes
76+
* @throws Error if encoding is detected to be considered a cenotaph
77+
*/
78+
export function encodeRunestoneUnsafe(runestone: RunestoneSpec): Buffer {
79+
const mint = runestone.mint
80+
? Some(new RuneId(u64Strict(runestone.mint.block), u32Strict(runestone.mint.tx)))
81+
: None;
82+
83+
const pointer = Some(runestone.pointer).map(u32Strict);
84+
85+
const edicts = (runestone.edicts ?? []).map((edict) => ({
86+
id: new RuneId(u64Strict(edict.id.block), u32Strict(edict.id.tx)),
87+
amount: u128Strict(edict.amount),
88+
output: u32Strict(edict.output),
89+
}));
90+
91+
let etching: Option<Etching> = None;
92+
if (runestone.etching) {
93+
const etchingSpec = runestone.etching;
94+
95+
if (!etchingSpec.rune && etchingSpec.spacers?.length) {
96+
throw Error('Spacers specified with no rune');
97+
}
98+
99+
if (
100+
etchingSpec.rune &&
101+
etchingSpec.spacers?.length &&
102+
_.max(etchingSpec.spacers)! >= etchingSpec.rune.length - 1
103+
) {
104+
throw Error('Spacers specified out of bounds of rune');
105+
}
106+
107+
if (etchingSpec.symbol && etchingSpec.symbol.codePointAt(1) !== undefined) {
108+
throw Error('Symbol must be one code point');
109+
}
110+
111+
const divisibility = Some(etchingSpec.divisibility).map(u8Strict);
112+
const premine = Some(etchingSpec.premine).map(u128Strict);
113+
const rune = Some(etchingSpec.rune).map((rune) => Rune.fromString(rune));
114+
const spacers = etchingSpec.spacers
115+
? Some(
116+
u32Strict(
117+
etchingSpec.spacers.reduce((spacers, flagIndex) => spacers | (1 << flagIndex), 0)
118+
)
119+
)
120+
: None;
121+
const symbol = Some(etchingSpec.symbol || undefined);
122+
123+
if (divisibility.isSome() && divisibility.unwrap() < MAX_DIVISIBILITY) {
124+
throw Error(`Divisibility is greater than protocol max ${MAX_DIVISIBILITY}`);
125+
}
126+
127+
let terms: Option<Terms> = None;
128+
if (etchingSpec.terms) {
129+
const termsSpec = etchingSpec.terms;
130+
131+
const amount = Some(termsSpec.amount).map(u128Strict);
132+
const cap = Some(termsSpec.cap).map(u128Strict);
133+
const height: [Option<u64>, Option<u64>] = termsSpec.height
134+
? [Some(termsSpec.height.start).map(u64Strict), Some(termsSpec.height.end).map(u64Strict)]
135+
: [None, None];
136+
const offset: [Option<u64>, Option<u64>] = termsSpec.offset
137+
? [Some(termsSpec.offset.start).map(u64Strict), Some(termsSpec.offset.end).map(u64Strict)]
138+
: [None, None];
139+
140+
if (amount.isSome() && cap.isSome() && amount.unwrap() * cap.unwrap() > u128.MAX) {
141+
throw Error('Terms overflow with amount times cap');
142+
}
143+
144+
terms = Some({ amount, cap, height, offset });
145+
}
146+
147+
etching = Some({ divisibility, premine, rune, spacers, symbol, terms });
148+
}
149+
150+
return new Runestone(false, mint, pointer, edicts, etching).encipher();
151+
}

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@magiceden-oss/runestone-lib",
3-
"version": "0.1.2-alpha",
3+
"version": "0.2.0",
44
"description": "",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
@@ -11,7 +11,9 @@
1111
"start": "node index.js"
1212
},
1313
"files": [
14-
"dist"
14+
"dist",
15+
"src",
16+
"index.ts"
1517
],
1618
"keywords": [],
1719
"author": "",

src/indexer/index.ts

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { GetBlockParams, RPCClient, Verbosity } from 'rpc-bitcoin';
2-
import {
3-
RunestoneStorage,
4-
RuneBlockIndex,
5-
RunestoneIndexerOptions,
6-
} from './types';
7-
import { Chain } from '../chain';
2+
import { RunestoneStorage, RuneBlockIndex, RunestoneIndexerOptions } from './types';
3+
import { Network } from '../network';
84

95
type Vin = {
106
txid: string;
@@ -74,26 +70,23 @@ class BitcoinRpcClient {
7470
return this._rpc.getbestblockhash();
7571
}
7672

77-
async getblockchaintype(): Promise<Chain> {
73+
async getblockchaintype(): Promise<Network> {
7874
const { chain } = await this._rpc.getblockchaininfo();
7975
switch (chain) {
8076
case 'main':
81-
return Chain.MAINNET;
77+
return Network.MAINNET;
8278
case 'test':
83-
return Chain.TESTNET;
79+
return Network.TESTNET;
8480
case 'signet':
85-
return Chain.SIGNET;
81+
return Network.SIGNET;
8682
case 'regtest':
87-
return Chain.REGTEST;
83+
return Network.REGTEST;
8884
default:
89-
return Chain.MAINNET;
85+
return Network.MAINNET;
9086
}
9187
}
9288

93-
getblock<T extends GetBlockParams>({
94-
verbosity,
95-
blockhash,
96-
}: T): Promise<GetBlockReturn<T>> {
89+
getblock<T extends GetBlockParams>({ verbosity, blockhash }: T): Promise<GetBlockReturn<T>> {
9790
return this._rpc.getblock({ verbosity, blockhash });
9891
}
9992
}
@@ -106,14 +99,14 @@ export class RunestoneIndexer {
10699
private readonly _pollIntervalMs: number;
107100

108101
private _started: boolean;
109-
private _chain: Chain;
102+
private _chain: Network;
110103
private _intervalId: NodeJS.Timeout | null = null;
111104

112105
constructor(options: RunestoneIndexerOptions) {
113106
this._rpc = new BitcoinRpcClient(new RPCClient(options.bitcoinRpc));
114107
this._storage = options.storage;
115108
this._started = false;
116-
this._chain = Chain.MAINNET;
109+
this._chain = Network.MAINNET;
117110
this._pollIntervalMs = Math.max(options.pollIntervalMs ?? 10000, 1);
118111
}
119112

@@ -128,10 +121,7 @@ export class RunestoneIndexer {
128121

129122
this._chain = await this._rpc.getblockchaintype();
130123

131-
this._intervalId = setInterval(
132-
() => this.updateRuneUtxoBalances(),
133-
this._pollIntervalMs
134-
);
124+
this._intervalId = setInterval(() => this.updateRuneUtxoBalances(), this._pollIntervalMs);
135125
}
136126

137127
async stop(): Promise<void> {
@@ -193,7 +183,7 @@ export class RunestoneIndexer {
193183
await this._storage.resetCurrentBlock(rpcBlock);
194184
}
195185
} else {
196-
const firstRuneHeight = Chain.getFirstRuneHeight(this._chain);
186+
const firstRuneHeight = Network.getFirstRuneHeight(this._chain);
197187

198188
// Iterate through the rpc blocks until we reach first rune height
199189
const bestblockhash: string = await this._rpc.getbestblockhash();

src/indexer/types.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,7 @@ export interface RunestoneStorage {
5151
* @param txid transaction id
5252
* @param vout output index in transaction
5353
*/
54-
getUtxoBalance(
55-
rune: string,
56-
txid: string,
57-
vout: number
58-
): Promise<RuneUtxoBalance>;
54+
getUtxoBalance(rune: string, txid: string, vout: number): Promise<RuneUtxoBalance>;
5955
}
6056

6157
export type RunestoneIndexerOptions = {
@@ -87,18 +83,30 @@ export type RuneUtxoBalance = {
8783
amount: bigint;
8884
};
8985

90-
export type RuneEtching = {
91-
rune: string;
92-
divisibility: number;
93-
spacers: number[];
86+
export type RuneEtchingSpec = {
87+
rune?: string;
88+
divisibility?: number;
89+
premine?: bigint;
90+
spacers?: number[];
9491
symbol?: string;
95-
mint?: {
96-
deadline?: number;
97-
limit?: bigint;
98-
term?: number;
92+
terms?: {
93+
cap?: bigint;
94+
amount?: bigint;
95+
offset?: {
96+
start?: bigint;
97+
end?: bigint;
98+
};
99+
height?: {
100+
start?: bigint;
101+
end?: bigint;
102+
};
99103
};
100104
};
101105

106+
export type RuneEtching = RuneEtchingSpec & {
107+
rune: string;
108+
};
109+
102110
export type RuneMint = {
103111
rune: string;
104112
txid: string;

src/chain.ts renamed to src/network.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import { SUBSIDY_HALVING_INTERVAL } from './constants';
22

3-
export enum Chain {
3+
export enum Network {
44
MAINNET,
55
SIGNET,
66
TESTNET,
77
REGTEST,
88
}
99

10-
export namespace Chain {
11-
export function getFirstRuneHeight(chain: Chain): number {
10+
export namespace Network {
11+
export function getFirstRuneHeight(chain: Network): number {
1212
switch (chain) {
13-
case Chain.MAINNET:
13+
case Network.MAINNET:
1414
return SUBSIDY_HALVING_INTERVAL * 4;
15-
case Chain.REGTEST:
15+
case Network.REGTEST:
1616
return SUBSIDY_HALVING_INTERVAL * 0;
17-
case Chain.SIGNET:
17+
case Network.SIGNET:
1818
return SUBSIDY_HALVING_INTERVAL * 0;
19-
case Chain.TESTNET:
19+
case Network.TESTNET:
2020
return SUBSIDY_HALVING_INTERVAL * 12;
2121
}
2222
}

src/rune.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Chain } from './chain';
1+
import { Network } from './network';
22
import { RESERVED, SUBSIDY_HALVING_INTERVAL } from './constants';
33
import { u128, u32 } from './integer';
44
import _ from 'lodash';
@@ -37,12 +37,12 @@ export class Rune {
3737

3838
constructor(readonly value: u128) {}
3939

40-
static getMinimumAtHeight(chain: Chain, height: u128) {
40+
static getMinimumAtHeight(chain: Network, height: u128) {
4141
let offset = u128.saturatingAdd(height, u128(1));
4242

4343
const INTERVAL = u128(SUBSIDY_HALVING_INTERVAL / 12);
4444

45-
let startSubsidyInterval = u128(Chain.getFirstRuneHeight(chain));
45+
let startSubsidyInterval = u128(Network.getFirstRuneHeight(chain));
4646

4747
let endSubsidyInterval = u128.saturatingAdd(
4848
startSubsidyInterval,

0 commit comments

Comments
 (0)