stxer 0.4.5 → 0.7.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/src/simulation.ts CHANGED
@@ -7,24 +7,23 @@ import {
7
7
  import type { Block } from '@stacks/stacks-blockchain-api-types';
8
8
  import {
9
9
  AddressHashMode,
10
- bufferCV,
11
10
  type ClarityValue,
12
11
  ClarityVersion,
13
- contractPrincipalCV,
14
- deserializeTransaction,
15
12
  type MultiSigSpendingCondition,
16
13
  makeUnsignedContractCall,
17
14
  makeUnsignedContractDeploy,
18
15
  makeUnsignedSTXTokenTransfer,
19
16
  PostConditionMode,
20
17
  type StacksTransactionWire,
21
- serializeCVBytes,
22
- stringAsciiCV,
23
- tupleCV,
24
- uintCV,
25
18
  } from '@stacks/transactions';
26
19
  import { c32addressDecode } from 'c32check';
27
20
  import { type AccountDataResponse, getNodeInfo, richFetch } from 'ts-clarity';
21
+ import { STXER_API_MAINNET, STXER_API_TESTNET } from './constants';
22
+ import {
23
+ createSimulationSession,
24
+ submitSimulationSteps,
25
+ } from './simulation-api';
26
+ import type { ReadStep, SimulationStepInput } from './types';
28
27
 
29
28
  function setSender(tx: StacksTransactionWire, sender: string) {
30
29
  const [addressVersion, signer] = c32addressDecode(sender);
@@ -51,107 +50,23 @@ function setSender(tx: StacksTransactionWire, sender: string) {
51
50
  return tx;
52
51
  }
53
52
 
54
- function runTx(tx: StacksTransactionWire) {
55
- // type 0: run transaction
56
- return tupleCV({ type: uintCV(0), data: bufferCV(tx.serializeBytes()) });
57
- }
58
-
59
53
  export interface SimulationEval {
60
54
  contract_id: string;
61
55
  code: string;
62
56
  }
63
57
 
64
- export function runEval({ contract_id, code }: SimulationEval) {
65
- const [contract_address, contract_name] = contract_id.split('.');
66
- // type 1: eval arbitrary code inside a contract
67
- return tupleCV({
68
- type: uintCV(1),
69
- data: bufferCV(
70
- serializeCVBytes(
71
- tupleCV({
72
- contract: contractPrincipalCV(contract_address, contract_name),
73
- code: stringAsciiCV(code),
74
- }),
75
- ),
76
- ),
77
- });
78
- }
79
-
80
- export async function runSimulation(
81
- apiEndpoint: string,
82
- block_hash: string,
83
- block_height: number,
84
- txs: (StacksTransactionWire | SimulationEval)[],
85
- ) {
86
- // Convert 'sim-v1' to Uint8Array
87
- const header = new TextEncoder().encode('sim-v1');
88
- // Create 8 bytes for block height
89
- const heightBytes = new Uint8Array(8);
90
- // Convert block height to bytes
91
- const view = new DataView(heightBytes.buffer);
92
- view.setBigUint64(0, BigInt(block_height), false); // false for big-endian
93
-
94
- // Convert block hash to bytes
95
- const hashHex = block_hash.startsWith('0x')
96
- ? block_hash.substring(2)
97
- : block_hash;
98
- // Replace non-null assertion with null check
99
- const matches = hashHex.match(/.{1,2}/g);
100
- if (!matches) {
101
- throw new Error('Invalid block hash format');
102
- }
103
- const hashBytes = new Uint8Array(
104
- matches.map((byte) => Number.parseInt(byte, 16)),
105
- );
106
-
107
- // Convert transactions to bytes
108
- const txBytes = txs
109
- .map((t) => ('contract_id' in t && 'code' in t ? runEval(t) : runTx(t)))
110
- .map((t) => serializeCVBytes(t));
111
-
112
- // Combine all byte arrays
113
- const totalLength =
114
- header.length +
115
- heightBytes.length +
116
- hashBytes.length +
117
- txBytes.reduce((acc, curr) => acc + curr.length, 0);
118
- const body = new Uint8Array(totalLength);
119
-
120
- let offset = 0;
121
- body.set(header, offset);
122
- offset += header.length;
123
- body.set(heightBytes, offset);
124
- offset += heightBytes.length;
125
- body.set(hashBytes, offset);
126
- offset += hashBytes.length;
127
- for (const tx of txBytes) {
128
- body.set(tx, offset);
129
- offset += tx.length;
130
- }
131
-
132
- const rs = await fetch(apiEndpoint, {
133
- method: 'POST',
134
- body,
135
- }).then(async (rs) => {
136
- const response = await rs.text();
137
- if (!response.startsWith('{')) {
138
- throw new Error(`failed to submit simulation: ${response}`);
139
- }
140
- return JSON.parse(response) as { id: string };
141
- });
142
- return rs.id;
143
- }
144
-
145
58
  interface SimulationBuilderOptions {
146
59
  apiEndpoint?: string;
147
60
  stacksNodeAPI?: string;
148
61
  network?: StacksNetworkName | string;
62
+ skipTracing?: boolean;
149
63
  }
150
64
 
151
65
  export class SimulationBuilder {
152
66
  private apiEndpoint: string;
153
67
  private stacksNodeAPI: string;
154
68
  private network: StacksNetworkName | string;
69
+ private skipTracing: boolean;
155
70
 
156
71
  private constructor(options: SimulationBuilderOptions = {}) {
157
72
  this.network = options.network ?? 'mainnet';
@@ -159,10 +74,11 @@ export class SimulationBuilder {
159
74
 
160
75
  this.apiEndpoint =
161
76
  options.apiEndpoint ??
162
- (isTestnet ? 'https://testnet-api.stxer.xyz' : 'https://api.stxer.xyz');
77
+ (isTestnet ? STXER_API_TESTNET : STXER_API_MAINNET);
163
78
  this.stacksNodeAPI =
164
79
  options.stacksNodeAPI ??
165
80
  (isTestnet ? 'https://api.testnet.hiro.so' : 'https://api.hiro.so');
81
+ this.skipTracing = options.skipTracing ?? false;
166
82
  }
167
83
 
168
84
  public static new(options?: SimulationBuilderOptions) {
@@ -174,7 +90,7 @@ export class SimulationBuilder {
174
90
  private sender = '';
175
91
  private steps: (
176
92
  | {
177
- // inline simulation
93
+ // inline simulation (V1 - not supported in V2, but kept for potential future support)
178
94
  simulationId: string;
179
95
  }
180
96
  | {
@@ -201,22 +117,41 @@ export class SimulationBuilder {
201
117
  fee: number;
202
118
  }
203
119
  | SimulationEval
120
+ | {
121
+ // SetContractCode - V2 native step type
122
+ type: 'SetContractCode';
123
+ contract_id: string;
124
+ source_code: string;
125
+ clarity_version: ClarityVersion;
126
+ }
127
+ | {
128
+ // Reads - V2 native batch reads step type
129
+ type: 'Reads';
130
+ reads: ReadStep[];
131
+ }
132
+ | {
133
+ // TenureExtend - V2 native step type
134
+ type: 'TenureExtend';
135
+ }
204
136
  )[] = [];
205
137
 
206
138
  public useBlockHeight(block: number) {
207
139
  this.block = block;
208
140
  return this;
209
141
  }
142
+
210
143
  public withSender(address: string) {
211
144
  this.sender = address;
212
145
  return this;
213
146
  }
147
+
214
148
  public inlineSimulation(simulationId: string) {
215
149
  this.steps.push({
216
150
  simulationId,
217
151
  });
218
152
  return this;
219
153
  }
154
+
220
155
  public addSTXTransfer(params: {
221
156
  recipient: string;
222
157
  amount: number;
@@ -235,6 +170,7 @@ export class SimulationBuilder {
235
170
  });
236
171
  return this;
237
172
  }
173
+
238
174
  public addContractCall(params: {
239
175
  contract_id: string;
240
176
  function_name: string;
@@ -254,6 +190,7 @@ export class SimulationBuilder {
254
190
  });
255
191
  return this;
256
192
  }
193
+
257
194
  public addContractDeploy(params: {
258
195
  contract_name: string;
259
196
  source_code: string;
@@ -274,6 +211,7 @@ export class SimulationBuilder {
274
211
  });
275
212
  return this;
276
213
  }
214
+
277
215
  public addEvalCode(inside_contract_id: string, code: string) {
278
216
  this.steps.push({
279
217
  contract_id: inside_contract_id,
@@ -281,6 +219,7 @@ export class SimulationBuilder {
281
219
  });
282
220
  return this;
283
221
  }
222
+
284
223
  public addMapRead(contract_id: string, map: string, key: string) {
285
224
  this.steps.push({
286
225
  contract_id,
@@ -288,6 +227,7 @@ export class SimulationBuilder {
288
227
  });
289
228
  return this;
290
229
  }
230
+
291
231
  public addVarRead(contract_id: string, variable: string) {
292
232
  this.steps.push({
293
233
  contract_id,
@@ -296,6 +236,35 @@ export class SimulationBuilder {
296
236
  return this;
297
237
  }
298
238
 
239
+ public addSetContractCode(params: {
240
+ contract_id: string;
241
+ source_code: string;
242
+ clarity_version?: ClarityVersion;
243
+ }) {
244
+ this.steps.push({
245
+ type: 'SetContractCode',
246
+ contract_id: params.contract_id,
247
+ source_code: params.source_code,
248
+ clarity_version: params.clarity_version ?? ClarityVersion.Clarity4,
249
+ });
250
+ return this;
251
+ }
252
+
253
+ public addReads(reads: ReadStep[]) {
254
+ this.steps.push({
255
+ type: 'Reads',
256
+ reads,
257
+ });
258
+ return this;
259
+ }
260
+
261
+ public addTenureExtend() {
262
+ this.steps.push({
263
+ type: 'TenureExtend',
264
+ });
265
+ return this;
266
+ }
267
+
299
268
  private async getBlockInfo() {
300
269
  if (Number.isNaN(this.block)) {
301
270
  const { stacks_tip_height } = await getNodeInfo({
@@ -322,7 +291,7 @@ export class SimulationBuilder {
322
291
  };
323
292
  }
324
293
 
325
- public async run() {
294
+ public async run(): Promise<string> {
326
295
  console.log(
327
296
  `--------------------------------
328
297
  This product can never exist without your support!
@@ -338,7 +307,8 @@ To get in touch: contact@stxer.xyz
338
307
  console.log(
339
308
  `Using block height ${block.block_height} hash 0x${block.block_hash} to run simulation.`,
340
309
  );
341
- const txs: (StacksTransactionWire | SimulationEval)[] = [];
310
+
311
+ const v2Steps: SimulationStepInput[] = [];
342
312
  const nonce_by_address = new Map<string, number>();
343
313
  const nextNonce = async (sender: string) => {
344
314
  const nonce = nonce_by_address.get(sender);
@@ -355,6 +325,7 @@ To get in touch: contact@stxer.xyz
355
325
  nonce_by_address.set(sender, nonce + 1);
356
326
  return nonce;
357
327
  };
328
+
358
329
  let network = this.network === 'mainnet' ? STACKS_MAINNET : STACKS_TESTNET;
359
330
  if (this.stacksNodeAPI) {
360
331
  network = {
@@ -365,39 +336,21 @@ To get in touch: contact@stxer.xyz
365
336
  },
366
337
  };
367
338
  }
339
+
368
340
  for (const step of this.steps) {
369
341
  if ('simulationId' in step) {
370
- const previousSimulation: {
371
- steps: ({ tx: string } | { code: string; contract: string })[];
372
- } = await fetch(
373
- `https://api.stxer.xyz/simulations/${step.simulationId}/request`,
374
- ).then(async (rs) => {
375
- const body = await rs.text();
376
- if (!body.startsWith('{')) {
377
- throw new Error(
378
- `failed to get simulation ${step.simulationId}: ${body}`,
379
- );
380
- }
381
- return JSON.parse(body) as {
382
- steps: ({ tx: string } | { code: string; contract: string })[];
383
- };
384
- });
385
- for (const step of previousSimulation.steps) {
386
- if ('tx' in step) {
387
- txs.push(deserializeTransaction(step.tx));
388
- } else if ('code' in step && 'contract' in step) {
389
- txs.push({
390
- contract_id: step.contract,
391
- code: step.code,
392
- });
393
- }
394
- }
395
- } else if ('sender' in step && 'function_name' in step) {
342
+ // Inline simulation - for V2 we would need to fetch the previous simulation
343
+ // and convert its steps to V2 format. This is complex and may not be
344
+ // commonly used, so for now we'll throw an error.
345
+ throw new Error(
346
+ 'inlineSimulation is not yet supported in V2 API. Please run simulations from scratch.',
347
+ );
348
+ }
349
+ if ('sender' in step && 'function_name' in step) {
396
350
  const nonce = await nextNonce(step.sender);
397
- const [contractAddress, contractName] = step.contract_id.split('.');
398
351
  const tx = await makeUnsignedContractCall({
399
- contractAddress,
400
- contractName,
352
+ contractAddress: step.contract_id.split('.')[0] as string,
353
+ contractName: step.contract_id.split('.')[1] as string,
401
354
  functionName: step.function_name,
402
355
  functionArgs: step.function_args ?? [],
403
356
  nonce,
@@ -407,7 +360,7 @@ To get in touch: contact@stxer.xyz
407
360
  fee: step.fee,
408
361
  });
409
362
  setSender(tx, step.sender);
410
- txs.push(tx);
363
+ v2Steps.push({ Transaction: bytesToHex(tx.serializeBytes()) });
411
364
  } else if ('sender' in step && 'recipient' in step) {
412
365
  const nonce = await nextNonce(step.sender);
413
366
  const tx = await makeUnsignedSTXTokenTransfer({
@@ -419,7 +372,7 @@ To get in touch: contact@stxer.xyz
419
372
  fee: step.fee,
420
373
  });
421
374
  setSender(tx, step.sender);
422
- txs.push(tx);
375
+ v2Steps.push({ Transaction: bytesToHex(tx.serializeBytes()) });
423
376
  } else if ('deployer' in step) {
424
377
  const nonce = await nextNonce(step.deployer);
425
378
  const tx = await makeUnsignedContractDeploy({
@@ -430,26 +383,67 @@ To get in touch: contact@stxer.xyz
430
383
  publicKey: '',
431
384
  postConditionMode: PostConditionMode.Allow,
432
385
  fee: step.fee,
433
- clarityVersion: step.clarity_version
386
+ clarityVersion: step.clarity_version,
434
387
  });
435
388
  setSender(tx, step.deployer);
436
- txs.push(tx);
389
+ v2Steps.push({ Transaction: bytesToHex(tx.serializeBytes()) });
437
390
  } else if ('code' in step) {
438
- txs.push(step);
391
+ // Eval step - format: [sender, sponsor, contract_id, code]
392
+ // For eval without a sender, we use empty string for sponsor
393
+ const [contractAddress, contractName] = step.contract_id.split('.');
394
+ v2Steps.push({
395
+ Eval: [
396
+ this.sender || contractAddress,
397
+ '',
398
+ `${contractAddress}.${contractName}`,
399
+ step.code,
400
+ ],
401
+ });
402
+ } else if (step.type === 'SetContractCode') {
403
+ // SetContractCode - format: [contract_id, code, clarity_version]
404
+ v2Steps.push({
405
+ SetContractCode: [
406
+ step.contract_id,
407
+ step.source_code,
408
+ clarityVersionToNumber(step.clarity_version),
409
+ ],
410
+ });
411
+ } else if (step.type === 'Reads') {
412
+ // Reads - batch read operations
413
+ v2Steps.push({
414
+ Reads: step.reads,
415
+ });
416
+ } else if (step.type === 'TenureExtend') {
417
+ // TenureExtend - format: []
418
+ v2Steps.push({
419
+ TenureExtend: [],
420
+ });
439
421
  } else {
440
422
  console.log(`Invalid simulation step: ${step}`);
441
423
  }
442
424
  }
443
- const id = await runSimulation(
444
- `${this.apiEndpoint}/simulations`,
445
- block.block_hash,
446
- block.block_height,
447
- txs,
425
+
426
+ // Create V2 simulation session
427
+ const simulationId = await createSimulationSession(
428
+ {
429
+ block_height: block.block_height,
430
+ block_hash: block.block_hash,
431
+ skip_tracing: this.skipTracing,
432
+ },
433
+ { stxerApi: this.apiEndpoint },
448
434
  );
435
+
436
+ // Submit steps
437
+ await submitSimulationSteps(
438
+ simulationId,
439
+ { steps: v2Steps },
440
+ { stxerApi: this.apiEndpoint },
441
+ );
442
+
449
443
  console.log(
450
- `Simulation will be available at: https://stxer.xyz/simulations/${this.network}/${id}`,
444
+ `Simulation will be available at: https://stxer.xyz/simulations/${this.network}/${simulationId}`,
451
445
  );
452
- return id;
446
+ return simulationId;
453
447
  }
454
448
 
455
449
  public pipe(
@@ -458,3 +452,40 @@ To get in touch: contact@stxer.xyz
458
452
  return transform(this);
459
453
  }
460
454
  }
455
+
456
+ // Helper function to convert Uint8Array to hex string
457
+ function bytesToHex(bytes: Uint8Array): string {
458
+ return Array.from(bytes)
459
+ .map((b) => b.toString(16).padStart(2, '0'))
460
+ .join('');
461
+ }
462
+
463
+ // Helper function to convert ClarityVersion to number
464
+ function clarityVersionToNumber(version: ClarityVersion): number {
465
+ switch (version) {
466
+ case ClarityVersion.Clarity1:
467
+ return 1;
468
+ case ClarityVersion.Clarity2:
469
+ return 2;
470
+ case ClarityVersion.Clarity3:
471
+ return 3;
472
+ case ClarityVersion.Clarity4:
473
+ return 4;
474
+ default:
475
+ return 4;
476
+ }
477
+ }
478
+
479
+ // Re-export simulation types
480
+ export type {
481
+ CreateSimulationRequest,
482
+ ExecutionCost,
483
+ InstantSimulationRequest,
484
+ InstantSimulationResponse,
485
+ ReadResult,
486
+ ReadStep,
487
+ SimulationMetadata,
488
+ SimulationResult,
489
+ SimulationStepInput,
490
+ TransactionReceipt,
491
+ } from './types';
package/src/tip.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * WARNING:
3
+ *
4
+ * this file will be used in cross-runtime environments (browser, cloudflare workers, XLinkSDK, etc.),
5
+ * so please be careful when adding `import`s to it.
6
+ */
7
+
8
+ import { DEFAULT_STXER_API } from './constants';
9
+ import type { SidecarTip } from './types';
10
+
11
+ export interface TipOptions {
12
+ stxerApi?: string;
13
+ }
14
+
15
+ /**
16
+ * Fetch the current tip information from the sidecar.
17
+ * @param options - Optional API endpoint configuration
18
+ * @returns The current chain tip information
19
+ */
20
+ export async function getTip(options: TipOptions = {}): Promise<SidecarTip> {
21
+ const url = `${options.stxerApi ?? DEFAULT_STXER_API}/sidecar/tip`;
22
+
23
+ const response = await fetch(url, {
24
+ method: 'GET',
25
+ headers: {
26
+ Accept: 'application/json',
27
+ },
28
+ });
29
+
30
+ if (!response.ok) {
31
+ throw new Error(
32
+ `Failed to fetch tip: ${response.status} ${response.statusText}`,
33
+ );
34
+ }
35
+
36
+ const text = await response.text();
37
+ if (!text.startsWith('{')) {
38
+ throw new Error(`Invalid response from tip endpoint: ${text}`);
39
+ }
40
+
41
+ return JSON.parse(text) as SidecarTip;
42
+ }