stxer 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/README.md +237 -56
- package/dist/ast.d.ts +1 -1
- package/dist/bitcoin.d.ts +121 -0
- package/dist/index.d.ts +2 -0
- package/dist/simulation-api.d.ts +52 -3
- package/dist/simulation.d.ts +70 -3
- package/dist/stxer.cjs.development.js +906 -101
- package/dist/stxer.cjs.development.js.map +1 -1
- package/dist/stxer.cjs.production.min.js +1 -1
- package/dist/stxer.cjs.production.min.js.map +1 -1
- package/dist/stxer.esm.js +887 -104
- package/dist/stxer.esm.js.map +1 -1
- package/dist/transaction.d.ts +71 -0
- package/dist/types.d.ts +247 -25
- package/package.json +26 -23
- package/src/ast.ts +1 -1
- package/src/bitcoin.ts +391 -0
- package/src/index.ts +2 -0
- package/src/simulation-api.ts +109 -12
- package/src/simulation.ts +247 -46
- package/src/transaction.ts +141 -0
- package/src/types.ts +256 -27
package/src/bitcoin.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitcoin SPV utilities for cross-chain simulations.
|
|
3
|
+
*
|
|
4
|
+
* Stacks bridge contracts (sBTC, Brotocol, hBTC, …) verify Bitcoin
|
|
5
|
+
* deposit transactions against burn-block headers and Merkle proofs
|
|
6
|
+
* stored in the Stacks chainstate. To exercise these contracts under
|
|
7
|
+
* the simulator you need three things on the wire:
|
|
8
|
+
*
|
|
9
|
+
* 1. A non-witness Bitcoin transaction whose `txid` (= `sha256d` of
|
|
10
|
+
* the bytes) the Stacks contract can recover.
|
|
11
|
+
* 2. An 80-byte block header whose hash matches the
|
|
12
|
+
* `BurnchainHeaderHash` value the simulator stores. The simulator
|
|
13
|
+
* accepts any 32-byte hash via the `burn_header_hashes` override
|
|
14
|
+
* on `AdvanceBlocks`; pair the override with `buildHeader` so the
|
|
15
|
+
* contract's `verify-block-header` resolves to the same value.
|
|
16
|
+
* 3. A Merkle proof linking the txid to the block header's
|
|
17
|
+
* `merkleRoot`. {@link merkleProof} produces the proof; the
|
|
18
|
+
* contract's on-chain verifier follows the same Bitcoin
|
|
19
|
+
* convention this module implements (leaves are NOT pre-hashed;
|
|
20
|
+
* `sha256d` of concatenated children for inner nodes).
|
|
21
|
+
*
|
|
22
|
+
* All wire-byte conventions match `clarity-bitcoin-v1-07` (the
|
|
23
|
+
* current on-chain SPV reference): hash slots inside the header are
|
|
24
|
+
* stored in *internal byte order* (raw `sha256d`), not the
|
|
25
|
+
* display-reversed form Bitcoin block explorers show.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import {
|
|
30
|
+
* forgeBitcoinTx, p2wpkhScript, opReturnScript,
|
|
31
|
+
* buildBitcoinHeader, singleTxMerkleRoot,
|
|
32
|
+
* hexToBytes, bytesToHex,
|
|
33
|
+
* } from 'stxer';
|
|
34
|
+
*
|
|
35
|
+
* const tx = forgeBitcoinTx({
|
|
36
|
+
* inputs: [{ prevTxid: hexToBytes('00'.repeat(32)), prevVout: 0,
|
|
37
|
+
* scriptSig: new Uint8Array() }],
|
|
38
|
+
* outputs: [
|
|
39
|
+
* { value: 100_000n, scriptPubKey: p2wpkhScript(hexToBytes('aa'.repeat(20))) },
|
|
40
|
+
* { value: 0n, scriptPubKey: opReturnScript(hexToBytes('5832' + '00'.repeat(20))) },
|
|
41
|
+
* ],
|
|
42
|
+
* });
|
|
43
|
+
* const header = buildBitcoinHeader({
|
|
44
|
+
* merkleRoot: singleTxMerkleRoot(tx.txid),
|
|
45
|
+
* });
|
|
46
|
+
* // Pass `bytesToHex(header.hash)` as `burn_header_hashes[0]` on
|
|
47
|
+
* // `addAdvanceBlocks`; pass `bytesToHex(tx.txid)` to the bridge's
|
|
48
|
+
* // `complete-deposit-wrapper` (or equivalent).
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js';
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Hashing primitives
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* SHA-256. Pure-JS via `@noble/hashes` so this module works in
|
|
59
|
+
* browsers, Node, and Bun without a polyfill.
|
|
60
|
+
*/
|
|
61
|
+
export function sha256(data: Uint8Array): Uint8Array {
|
|
62
|
+
return nobleSha256(data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Bitcoin-style double-sha256 used for txids, block hashes, and Merkle nodes. */
|
|
66
|
+
export function sha256d(data: Uint8Array): Uint8Array {
|
|
67
|
+
return sha256(sha256(data));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Hex encoding
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
/** Decode a hex string (with or without `0x` prefix). */
|
|
75
|
+
export function hexToBytes(s: string): Uint8Array {
|
|
76
|
+
const t = s.toLowerCase().replace(/^0x/, '');
|
|
77
|
+
if (t.length % 2 !== 0) throw new Error('odd-length hex');
|
|
78
|
+
const out = new Uint8Array(t.length / 2);
|
|
79
|
+
for (let i = 0; i < out.length; i++) {
|
|
80
|
+
out[i] = Number.parseInt(t.slice(i * 2, i * 2 + 2), 16);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Lower-case hex (no `0x` prefix). */
|
|
86
|
+
export function bytesToHex(b: Uint8Array): string {
|
|
87
|
+
return Array.from(b)
|
|
88
|
+
.map((x) => x.toString(16).padStart(2, '0'))
|
|
89
|
+
.join('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Bitcoin script builders
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/** P2WPKH output script: `OP_0 <20-byte pubkey-hash>`. */
|
|
97
|
+
export function p2wpkhScript(pubKeyHash20: Uint8Array): Uint8Array {
|
|
98
|
+
if (pubKeyHash20.length !== 20) {
|
|
99
|
+
throw new Error('pubKeyHash must be 20 bytes');
|
|
100
|
+
}
|
|
101
|
+
const out = new Uint8Array(22);
|
|
102
|
+
out[0] = 0x00; // OP_0
|
|
103
|
+
out[1] = 0x14; // pushdata length 20
|
|
104
|
+
out.set(pubKeyHash20, 2);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** P2PKH output script: `OP_DUP OP_HASH160 <hash> OP_EQUALVERIFY OP_CHECKSIG`. */
|
|
109
|
+
export function p2pkhScript(pubKeyHash20: Uint8Array): Uint8Array {
|
|
110
|
+
if (pubKeyHash20.length !== 20) {
|
|
111
|
+
throw new Error('pubKeyHash must be 20 bytes');
|
|
112
|
+
}
|
|
113
|
+
const out = new Uint8Array(25);
|
|
114
|
+
out[0] = 0x76; // OP_DUP
|
|
115
|
+
out[1] = 0xa9; // OP_HASH160
|
|
116
|
+
out[2] = 0x14; // pushdata 20
|
|
117
|
+
out.set(pubKeyHash20, 3);
|
|
118
|
+
out[23] = 0x88; // OP_EQUALVERIFY
|
|
119
|
+
out[24] = 0xac; // OP_CHECKSIG
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* `OP_RETURN <data>` output. Used to embed peg-in metadata
|
|
125
|
+
* (recipient principal, marker bytes, etc.). Bitcoin's standard relay
|
|
126
|
+
* limit caps `data` at 80 bytes; this helper rejects > 75 to leave
|
|
127
|
+
* room for the prefix bytes.
|
|
128
|
+
*/
|
|
129
|
+
export function opReturnScript(data: Uint8Array): Uint8Array {
|
|
130
|
+
if (data.length > 75) {
|
|
131
|
+
throw new Error(`OP_RETURN data too long: ${data.length}`);
|
|
132
|
+
}
|
|
133
|
+
const out = new Uint8Array(2 + data.length);
|
|
134
|
+
out[0] = 0x6a; // OP_RETURN
|
|
135
|
+
out[1] = data.length; // direct push
|
|
136
|
+
out.set(data, 2);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Bitcoin transaction forging
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
export interface BitcoinTxInput {
|
|
145
|
+
/** 32-byte previous txid in *internal byte order* (raw `sha256d`). */
|
|
146
|
+
prevTxid: Uint8Array;
|
|
147
|
+
prevVout: number;
|
|
148
|
+
/** Script signature bytes. Empty for segwit-style inputs. */
|
|
149
|
+
scriptSig: Uint8Array;
|
|
150
|
+
/** Sequence number; default `0xfffffffd`. */
|
|
151
|
+
sequence?: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface BitcoinTxOutput {
|
|
155
|
+
/** Satoshi amount. */
|
|
156
|
+
value: bigint;
|
|
157
|
+
/** Locking script — use {@link p2wpkhScript} / {@link p2pkhScript} / {@link opReturnScript}. */
|
|
158
|
+
scriptPubKey: Uint8Array;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface ForgedBitcoinTx {
|
|
162
|
+
/** Wire-encoded non-witness tx bytes (this is what the txid is computed over). */
|
|
163
|
+
bytes: Uint8Array;
|
|
164
|
+
/** Canonical txid: `sha256d(bytes)`. Internal byte order. */
|
|
165
|
+
txid: Uint8Array;
|
|
166
|
+
/** Display-form (reversed) txid — what block explorers render. */
|
|
167
|
+
txidDisplay: Uint8Array;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build a non-witness Bitcoin transaction (legacy serialization). The
|
|
172
|
+
* txid is `sha256d` of the returned `bytes`. Use this form when you
|
|
173
|
+
* only need to assert SPV inclusion; segwit witness data is irrelevant
|
|
174
|
+
* to the txid.
|
|
175
|
+
*
|
|
176
|
+
* Wire format:
|
|
177
|
+
* ```
|
|
178
|
+
* 4 version (LE)
|
|
179
|
+
* varint in-count
|
|
180
|
+
* per-input: 32 prev_txid + 4 prev_vout + varint script-len + script + 4 sequence
|
|
181
|
+
* varint out-count
|
|
182
|
+
* per-output: 8 value (LE) + varint script-len + script
|
|
183
|
+
* 4 locktime (LE)
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function forgeBitcoinTx(opts: {
|
|
187
|
+
version?: number;
|
|
188
|
+
inputs: BitcoinTxInput[];
|
|
189
|
+
outputs: BitcoinTxOutput[];
|
|
190
|
+
locktime?: number;
|
|
191
|
+
}): ForgedBitcoinTx {
|
|
192
|
+
const chunks: Uint8Array[] = [];
|
|
193
|
+
const u32le = (n: number) => {
|
|
194
|
+
const b = new Uint8Array(4);
|
|
195
|
+
new DataView(b.buffer).setUint32(0, n >>> 0, true);
|
|
196
|
+
return b;
|
|
197
|
+
};
|
|
198
|
+
const u64le = (n: bigint) => {
|
|
199
|
+
const b = new Uint8Array(8);
|
|
200
|
+
const view = new DataView(b.buffer);
|
|
201
|
+
view.setUint32(0, Number(n & 0xffffffffn), true);
|
|
202
|
+
view.setUint32(4, Number((n >> 32n) & 0xffffffffn), true);
|
|
203
|
+
return b;
|
|
204
|
+
};
|
|
205
|
+
const varint = (n: number): Uint8Array => {
|
|
206
|
+
if (n < 0xfd) return new Uint8Array([n]);
|
|
207
|
+
if (n <= 0xffff) {
|
|
208
|
+
const b = new Uint8Array(3);
|
|
209
|
+
b[0] = 0xfd;
|
|
210
|
+
new DataView(b.buffer).setUint16(1, n, true);
|
|
211
|
+
return b;
|
|
212
|
+
}
|
|
213
|
+
if (n <= 0xffffffff) {
|
|
214
|
+
const b = new Uint8Array(5);
|
|
215
|
+
b[0] = 0xfe;
|
|
216
|
+
new DataView(b.buffer).setUint32(1, n, true);
|
|
217
|
+
return b;
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`varint too large: ${n}`);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
chunks.push(u32le(opts.version ?? 2));
|
|
223
|
+
chunks.push(varint(opts.inputs.length));
|
|
224
|
+
for (const i of opts.inputs) {
|
|
225
|
+
if (i.prevTxid.length !== 32) {
|
|
226
|
+
throw new Error('prevTxid must be 32 bytes');
|
|
227
|
+
}
|
|
228
|
+
chunks.push(i.prevTxid);
|
|
229
|
+
chunks.push(u32le(i.prevVout));
|
|
230
|
+
chunks.push(varint(i.scriptSig.length));
|
|
231
|
+
chunks.push(i.scriptSig);
|
|
232
|
+
chunks.push(u32le(i.sequence ?? 0xfffffffd));
|
|
233
|
+
}
|
|
234
|
+
chunks.push(varint(opts.outputs.length));
|
|
235
|
+
for (const o of opts.outputs) {
|
|
236
|
+
chunks.push(u64le(o.value));
|
|
237
|
+
chunks.push(varint(o.scriptPubKey.length));
|
|
238
|
+
chunks.push(o.scriptPubKey);
|
|
239
|
+
}
|
|
240
|
+
chunks.push(u32le(opts.locktime ?? 0));
|
|
241
|
+
|
|
242
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
243
|
+
const bytes = new Uint8Array(total);
|
|
244
|
+
let off = 0;
|
|
245
|
+
for (const c of chunks) {
|
|
246
|
+
bytes.set(c, off);
|
|
247
|
+
off += c.length;
|
|
248
|
+
}
|
|
249
|
+
const txid = sha256d(bytes);
|
|
250
|
+
const txidDisplay = reverseBytes(txid);
|
|
251
|
+
return { bytes, txid, txidDisplay };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// Bitcoin block header
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
export interface BitcoinHeader {
|
|
259
|
+
version: number;
|
|
260
|
+
prevHash: Uint8Array;
|
|
261
|
+
merkleRoot: Uint8Array;
|
|
262
|
+
timestamp: number;
|
|
263
|
+
bits: number;
|
|
264
|
+
nonce: number;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface BuiltBitcoinHeader {
|
|
268
|
+
/** Raw 80-byte serialized header. */
|
|
269
|
+
header: Uint8Array;
|
|
270
|
+
/**
|
|
271
|
+
* Display-form hash (`reverse(sha256d(header))`) — what the Stacks
|
|
272
|
+
* chainstate stores as `BurnchainHeaderHash`. Pass this as the
|
|
273
|
+
* `burn_header_hashes[i]` override on `addAdvanceBlocks`.
|
|
274
|
+
*/
|
|
275
|
+
hash: Uint8Array;
|
|
276
|
+
/** Raw `sha256d(header)` (internal byte order). */
|
|
277
|
+
rawHash: Uint8Array;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Build an 80-byte Bitcoin block header. `prevHash` and `merkleRoot`
|
|
282
|
+
* are written in internal byte order (raw `sha256d` output) — matching
|
|
283
|
+
* the on-chain `clarity-bitcoin-v1-07` parser convention.
|
|
284
|
+
*/
|
|
285
|
+
export function buildBitcoinHeader(
|
|
286
|
+
h: Partial<BitcoinHeader> & { merkleRoot: Uint8Array },
|
|
287
|
+
): BuiltBitcoinHeader {
|
|
288
|
+
const out = new Uint8Array(80);
|
|
289
|
+
const view = new DataView(out.buffer);
|
|
290
|
+
view.setUint32(0, h.version ?? 0x20000000, true);
|
|
291
|
+
out.set(h.prevHash ?? new Uint8Array(32), 4);
|
|
292
|
+
out.set(h.merkleRoot, 36);
|
|
293
|
+
view.setUint32(68, h.timestamp ?? Math.floor(Date.now() / 1000), true);
|
|
294
|
+
view.setUint32(72, h.bits ?? 0x1d00ffff, true);
|
|
295
|
+
view.setUint32(76, h.nonce ?? 0, true);
|
|
296
|
+
const rawHash = sha256d(out);
|
|
297
|
+
return { header: out, hash: reverseBytes(rawHash), rawHash };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// Merkle proofs (Bitcoin convention)
|
|
302
|
+
// ============================================================================
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Single-tx Merkle root — the txid itself. Bitcoin's Merkle tree
|
|
306
|
+
* convention does NOT pre-hash the leaves; for a one-tx block the
|
|
307
|
+
* root equals the txid.
|
|
308
|
+
*/
|
|
309
|
+
export function singleTxMerkleRoot(txid: Uint8Array): Uint8Array {
|
|
310
|
+
return new Uint8Array(txid);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Compute a Bitcoin Merkle proof for `txid` at `index` in `txids`
|
|
315
|
+
* (transaction order within the block). Returns the sibling hashes
|
|
316
|
+
* from leaf to root.
|
|
317
|
+
*
|
|
318
|
+
* Bitcoin convention:
|
|
319
|
+
* - Leaves are the txids themselves (no pre-hashing).
|
|
320
|
+
* - Inner nodes are `sha256d(left || right)`.
|
|
321
|
+
* - Odd levels duplicate the last entry to pair.
|
|
322
|
+
*/
|
|
323
|
+
export function merkleProof(
|
|
324
|
+
txids: Uint8Array[],
|
|
325
|
+
index: number,
|
|
326
|
+
): { proof: Uint8Array[]; root: Uint8Array } {
|
|
327
|
+
if (index < 0 || index >= txids.length) {
|
|
328
|
+
throw new Error(`merkleProof: index ${index} out of range ${txids.length}`);
|
|
329
|
+
}
|
|
330
|
+
let level: Uint8Array[] = txids.map((t) => new Uint8Array(t));
|
|
331
|
+
const proof: Uint8Array[] = [];
|
|
332
|
+
let i = index;
|
|
333
|
+
while (level.length > 1) {
|
|
334
|
+
if (level.length % 2 === 1) level.push(level[level.length - 1]);
|
|
335
|
+
const sibling = level[i ^ 1];
|
|
336
|
+
proof.push(sibling);
|
|
337
|
+
const next: Uint8Array[] = [];
|
|
338
|
+
for (let j = 0; j < level.length; j += 2) {
|
|
339
|
+
next.push(sha256d(concatBytes(level[j], level[j + 1])));
|
|
340
|
+
}
|
|
341
|
+
level = next;
|
|
342
|
+
i = Math.floor(i / 2);
|
|
343
|
+
}
|
|
344
|
+
return { proof, root: level[0] };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Verify a Bitcoin Merkle proof against `root`. Returns `true` iff the
|
|
349
|
+
* proof reconstructs the root from `txid` at `index`. Matches the
|
|
350
|
+
* on-chain `verify-merkle-proof` semantic.
|
|
351
|
+
*/
|
|
352
|
+
export function verifyMerkleProof(
|
|
353
|
+
txid: Uint8Array,
|
|
354
|
+
index: number,
|
|
355
|
+
proof: Uint8Array[],
|
|
356
|
+
root: Uint8Array,
|
|
357
|
+
): boolean {
|
|
358
|
+
let h: Uint8Array = new Uint8Array(txid);
|
|
359
|
+
let i = index;
|
|
360
|
+
for (const sibling of proof) {
|
|
361
|
+
h =
|
|
362
|
+
(i & 1) === 0
|
|
363
|
+
? sha256d(concatBytes(h, sibling))
|
|
364
|
+
: sha256d(concatBytes(sibling, h));
|
|
365
|
+
i = Math.floor(i / 2);
|
|
366
|
+
}
|
|
367
|
+
return bytesEqual(h, root);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ============================================================================
|
|
371
|
+
// Internal byte helpers
|
|
372
|
+
// ============================================================================
|
|
373
|
+
|
|
374
|
+
function reverseBytes(a: Uint8Array): Uint8Array {
|
|
375
|
+
const out = new Uint8Array(a.length);
|
|
376
|
+
for (let i = 0; i < a.length; i++) out[i] = a[a.length - 1 - i];
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
381
|
+
const out = new Uint8Array(a.length + b.length);
|
|
382
|
+
out.set(a, 0);
|
|
383
|
+
out.set(b, a.length);
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
388
|
+
if (a.length !== b.length) return false;
|
|
389
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
390
|
+
return true;
|
|
391
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// the build (babel falls back to flow grammar). Stick with `export *`.
|
|
5
5
|
export * from './ast';
|
|
6
6
|
export * from './batch-api';
|
|
7
|
+
export * from './bitcoin';
|
|
7
8
|
export * from './clarity-api';
|
|
8
9
|
export * from './constants';
|
|
9
10
|
export * from './simulation';
|
|
10
11
|
export * from './simulation-api';
|
|
11
12
|
export * from './tip';
|
|
13
|
+
export * from './transaction';
|
|
12
14
|
export * from './types';
|
package/src/simulation-api.ts
CHANGED
|
@@ -12,10 +12,61 @@ import type {
|
|
|
12
12
|
SimulationBatchReadsRequest,
|
|
13
13
|
SimulationBatchReadsResponse,
|
|
14
14
|
SimulationResult,
|
|
15
|
+
SimulationTipResponse,
|
|
15
16
|
SubmitSimulationStepsRequest,
|
|
16
17
|
SubmitSimulationStepsResponse,
|
|
18
|
+
U64,
|
|
17
19
|
} from './types';
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Server-side error markers. `simulation_busy` ↔ HTTP 409,
|
|
23
|
+
* `simulation_outdated` ↔ HTTP 410. Other errors keep `marker = null`.
|
|
24
|
+
*/
|
|
25
|
+
export type SimulationErrorMarker =
|
|
26
|
+
| 'simulation_busy'
|
|
27
|
+
| 'simulation_outdated'
|
|
28
|
+
| null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Thrown by the simulation-api fetch wrappers when the API responds
|
|
32
|
+
* with a non-2xx status. `status` carries the wire HTTP code so
|
|
33
|
+
* callers can branch on 409 (busy) / 410 (outdated) without parsing
|
|
34
|
+
* the message string. `marker` is the server-side classification
|
|
35
|
+
* extracted from the body prefix (`simulation_busy:` /
|
|
36
|
+
* `simulation_outdated:`).
|
|
37
|
+
*
|
|
38
|
+
* Pre-0.8.0 SDKs threw a plain `Error` whose message embedded the body
|
|
39
|
+
* text; that lossy form is still used for the message field here so
|
|
40
|
+
* existing log scrapers keep working.
|
|
41
|
+
*/
|
|
42
|
+
export class SimulationError extends Error {
|
|
43
|
+
readonly status: number;
|
|
44
|
+
readonly marker: SimulationErrorMarker;
|
|
45
|
+
readonly body: string;
|
|
46
|
+
constructor(operation: string, status: number, body: string) {
|
|
47
|
+
const marker = detectMarker(body);
|
|
48
|
+
super(`${operation} (HTTP ${status}): ${body}`);
|
|
49
|
+
this.name = 'SimulationError';
|
|
50
|
+
this.status = status;
|
|
51
|
+
this.marker = marker;
|
|
52
|
+
this.body = body;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function detectMarker(body: string): SimulationErrorMarker {
|
|
57
|
+
if (body.includes('simulation_busy:')) return 'simulation_busy';
|
|
58
|
+
if (body.includes('simulation_outdated:')) return 'simulation_outdated';
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function throwSimulationError(
|
|
63
|
+
operation: string,
|
|
64
|
+
response: Response,
|
|
65
|
+
): Promise<never> {
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
throw new SimulationError(operation, response.status, text);
|
|
68
|
+
}
|
|
69
|
+
|
|
19
70
|
/**
|
|
20
71
|
* Options for API calls
|
|
21
72
|
*/
|
|
@@ -28,8 +79,11 @@ export interface SimulationApiOptions {
|
|
|
28
79
|
* Create simulation session options
|
|
29
80
|
*/
|
|
30
81
|
export interface CreateSessionOptions {
|
|
31
|
-
/**
|
|
32
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Block height for simulation (optional, uses tip if not provided).
|
|
84
|
+
* u64 — `number | string`; see {@link U64}.
|
|
85
|
+
*/
|
|
86
|
+
block_height?: U64;
|
|
33
87
|
/** Block hash corresponding to block_height */
|
|
34
88
|
block_hash?: string;
|
|
35
89
|
/** Skip debug tracing for faster simulations */
|
|
@@ -81,8 +135,7 @@ export async function instantSimulation(
|
|
|
81
135
|
);
|
|
82
136
|
|
|
83
137
|
if (!response.ok) {
|
|
84
|
-
|
|
85
|
-
throw new Error(`Instant simulation failed: ${text}`);
|
|
138
|
+
await throwSimulationError('Instant simulation failed', response);
|
|
86
139
|
}
|
|
87
140
|
|
|
88
141
|
return response.json() as Promise<InstantSimulationResponse>;
|
|
@@ -123,8 +176,7 @@ export async function createSimulationSession(
|
|
|
123
176
|
});
|
|
124
177
|
|
|
125
178
|
if (!response.ok) {
|
|
126
|
-
|
|
127
|
-
throw new Error(`Failed to create simulation session: ${text}`);
|
|
179
|
+
await throwSimulationError('Failed to create simulation session', response);
|
|
128
180
|
}
|
|
129
181
|
|
|
130
182
|
const result = (await response.json()) as { id: string };
|
|
@@ -174,8 +226,7 @@ export async function submitSimulationSteps(
|
|
|
174
226
|
);
|
|
175
227
|
|
|
176
228
|
if (!response.ok) {
|
|
177
|
-
|
|
178
|
-
throw new Error(`Failed to submit simulation steps: ${text}`);
|
|
229
|
+
await throwSimulationError('Failed to submit simulation steps', response);
|
|
179
230
|
}
|
|
180
231
|
|
|
181
232
|
return response.json() as Promise<SubmitSimulationStepsResponse>;
|
|
@@ -216,13 +267,57 @@ export async function getSimulationResult(
|
|
|
216
267
|
);
|
|
217
268
|
|
|
218
269
|
if (!response.ok) {
|
|
219
|
-
|
|
220
|
-
throw new Error(`Failed to get simulation result: ${text}`);
|
|
270
|
+
await throwSimulationError('Failed to get simulation result', response);
|
|
221
271
|
}
|
|
222
272
|
|
|
223
273
|
return response.json() as Promise<SimulationResult>;
|
|
224
274
|
}
|
|
225
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Get the current tip of a simulation session.
|
|
278
|
+
*
|
|
279
|
+
* Returns the latest synthetic tip when at least one `AdvanceBlocks`
|
|
280
|
+
* step has run (`synthetic: true`, includes `vrf_seed` and
|
|
281
|
+
* `tenure_change`). Returns the parent metadata pinned at session
|
|
282
|
+
* start otherwise (`synthetic: false`, `vrf_seed` and `tenure_change`
|
|
283
|
+
* omitted).
|
|
284
|
+
*
|
|
285
|
+
* Backed by `GET /devtools/v2/simulations/{id}/tip`. Older simulator
|
|
286
|
+
* builds that predate this route respond 404.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```typescript
|
|
290
|
+
* import { getSimulationTip } from 'stxer';
|
|
291
|
+
*
|
|
292
|
+
* const tip = await getSimulationTip(sessionId);
|
|
293
|
+
* if (tip.synthetic) {
|
|
294
|
+
* console.log('synthetic tip vrf_seed:', tip.vrf_seed);
|
|
295
|
+
* }
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export async function getSimulationTip(
|
|
299
|
+
sessionId: string,
|
|
300
|
+
options: SimulationApiOptions = {},
|
|
301
|
+
): Promise<SimulationTipResponse> {
|
|
302
|
+
const apiEndpoint = options.stxerApi ?? DEFAULT_STXER_API;
|
|
303
|
+
|
|
304
|
+
const response = await fetch(
|
|
305
|
+
`${apiEndpoint}/devtools/v2/simulations/${sessionId}/tip`,
|
|
306
|
+
{
|
|
307
|
+
method: 'GET',
|
|
308
|
+
headers: {
|
|
309
|
+
Accept: 'application/json',
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
await throwSimulationError('Failed to get simulation tip', response);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return response.json() as Promise<SimulationTipResponse>;
|
|
319
|
+
}
|
|
320
|
+
|
|
226
321
|
/**
|
|
227
322
|
* Batch reads from a simulation session
|
|
228
323
|
*
|
|
@@ -265,8 +360,10 @@ export async function simulationBatchReads(
|
|
|
265
360
|
);
|
|
266
361
|
|
|
267
362
|
if (!response.ok) {
|
|
268
|
-
|
|
269
|
-
|
|
363
|
+
await throwSimulationError(
|
|
364
|
+
'Failed to batch reads from simulation',
|
|
365
|
+
response,
|
|
366
|
+
);
|
|
270
367
|
}
|
|
271
368
|
|
|
272
369
|
return response.json() as Promise<SimulationBatchReadsResponse>;
|