stxer 0.6.1 → 0.8.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.
@@ -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
- /** Block height for simulation (optional, uses tip if not provided) */
32
- block_height?: number;
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 */
@@ -69,7 +123,7 @@ export async function instantSimulation(
69
123
  const apiEndpoint = options.stxerApi ?? DEFAULT_STXER_API;
70
124
 
71
125
  const response = await fetch(
72
- `${apiEndpoint}/devtools/v2/simulation:instant`,
126
+ `${apiEndpoint}/devtools/v2/simulations:instant`,
73
127
  {
74
128
  method: 'POST',
75
129
  body: JSON.stringify(request),
@@ -81,8 +135,7 @@ export async function instantSimulation(
81
135
  );
82
136
 
83
137
  if (!response.ok) {
84
- const text = await response.text();
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
- const text = await response.text();
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
- const text = await response.text();
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
- const text = await response.text();
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
- const text = await response.text();
269
- throw new Error(`Failed to batch reads from simulation: ${text}`);
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>;
package/src/simulation.ts CHANGED
@@ -1,54 +1,43 @@
1
1
  import {
2
- AddressVersion,
3
2
  STACKS_MAINNET,
4
3
  STACKS_TESTNET,
5
4
  type StacksNetworkName,
6
5
  } from '@stacks/network';
7
6
  import type { Block } from '@stacks/stacks-blockchain-api-types';
8
7
  import {
9
- AddressHashMode,
8
+ Cl,
9
+ ClarityType,
10
10
  type ClarityValue,
11
11
  ClarityVersion,
12
- type MultiSigSpendingCondition,
12
+ cvToString,
13
+ deserializeCV,
13
14
  makeUnsignedContractCall,
14
15
  makeUnsignedContractDeploy,
15
16
  makeUnsignedSTXTokenTransfer,
16
17
  PostConditionMode,
17
- type StacksTransactionWire,
18
+ serializeCV,
18
19
  } from '@stacks/transactions';
19
- import { c32addressDecode } from 'c32check';
20
20
  import { type AccountDataResponse, getNodeInfo, richFetch } from 'ts-clarity';
21
+ import { bytesToHex } from './bitcoin';
21
22
  import { STXER_API_MAINNET, STXER_API_TESTNET } from './constants';
22
23
  import {
23
24
  createSimulationSession,
25
+ type SimulationApiOptions,
26
+ simulationBatchReads,
24
27
  submitSimulationSteps,
25
28
  } from './simulation-api';
26
- import type { ReadStep, SimulationStepInput } from './types';
27
-
28
- function setSender(tx: StacksTransactionWire, sender: string) {
29
- const [addressVersion, signer] = c32addressDecode(sender);
30
- switch (addressVersion) {
31
- case AddressVersion.MainnetSingleSig:
32
- case AddressVersion.TestnetSingleSig:
33
- tx.auth.spendingCondition.hashMode = AddressHashMode.P2PKH;
34
- tx.auth.spendingCondition.signer = signer;
35
- break;
36
- case AddressVersion.MainnetMultiSig:
37
- case AddressVersion.TestnetMultiSig: {
38
- const sc = tx.auth.spendingCondition;
39
- tx.auth.spendingCondition = {
40
- hashMode: AddressHashMode.P2SH,
41
- signer,
42
- fields: [],
43
- signaturesRequired: 0,
44
- nonce: sc.nonce,
45
- fee: sc.fee,
46
- } as MultiSigSpendingCondition;
47
- break;
48
- }
49
- }
50
- return tx;
51
- }
29
+ import {
30
+ buildUnsignedContractCallHex,
31
+ type ContractCallTxArgs,
32
+ setSender,
33
+ } from './transaction';
34
+ import type {
35
+ AdvanceBlocksRequest,
36
+ ReadStep,
37
+ SimulationStepInput,
38
+ TenureExtendCause,
39
+ TransactionReceipt,
40
+ } from './types';
52
41
 
53
42
  export interface SimulationEval {
54
43
  contract_id: string;
@@ -85,8 +74,7 @@ export class SimulationBuilder {
85
74
  return new SimulationBuilder(options);
86
75
  }
87
76
 
88
- // biome-ignore lint/style/useNumberNamespace: ignore this
89
- private block = NaN;
77
+ private block = Number.NaN;
90
78
  private sender = '';
91
79
  private steps: (
92
80
  | {
@@ -132,6 +120,12 @@ export class SimulationBuilder {
132
120
  | {
133
121
  // TenureExtend - V2 native step type
134
122
  type: 'TenureExtend';
123
+ cause: TenureExtendCause;
124
+ }
125
+ | {
126
+ // AdvanceBlocks - V2 native step type, synthesizes blocks
127
+ type: 'AdvanceBlocks';
128
+ request: AdvanceBlocksRequest;
135
129
  }
136
130
  )[] = [];
137
131
 
@@ -207,7 +201,7 @@ export class SimulationBuilder {
207
201
  ...params,
208
202
  deployer: params.deployer ?? this.sender,
209
203
  fee: params.fee ?? 0,
210
- clarity_version: params.clarity_version ?? ClarityVersion.Clarity4,
204
+ clarity_version: params.clarity_version ?? ClarityVersion.Clarity5,
211
205
  });
212
206
  return this;
213
207
  }
@@ -245,7 +239,7 @@ export class SimulationBuilder {
245
239
  type: 'SetContractCode',
246
240
  contract_id: params.contract_id,
247
241
  source_code: params.source_code,
248
- clarity_version: params.clarity_version ?? ClarityVersion.Clarity4,
242
+ clarity_version: params.clarity_version ?? ClarityVersion.Clarity5,
249
243
  });
250
244
  return this;
251
245
  }
@@ -258,9 +252,47 @@ export class SimulationBuilder {
258
252
  return this;
259
253
  }
260
254
 
261
- public addTenureExtend() {
255
+ /**
256
+ * Add a `TenureExtend` step. Defaults to `cause: 'Extended'` (full
257
+ * cost reset) — equivalent in server-side behavior to the legacy
258
+ * zero-arg call.
259
+ *
260
+ * Pass an explicit `TenureExtendCause` to reset only one SIP-034
261
+ * dimension (`ExtendedRuntime` / `ExtendedReadCount` / etc.).
262
+ *
263
+ * On the wire SDK 0.8.0 emits the modern `{ TenureExtend: { cause } }`
264
+ * shape. The server still parses the legacy `[]` shape for any
265
+ * caller emitting raw step objects.
266
+ */
267
+ public addTenureExtend(cause: TenureExtendCause = 'Extended') {
262
268
  this.steps.push({
263
269
  type: 'TenureExtend',
270
+ cause,
271
+ });
272
+ return this;
273
+ }
274
+
275
+ /**
276
+ * Synthesize bitcoin and stacks blocks on top of the simulation's
277
+ * pinned parent tip. Used to model burn-block / tenure boundaries
278
+ * (bridge contracts, time-locked redemptions, locked-STX unlock).
279
+ *
280
+ * The simulator validates the request shape; older simulator builds
281
+ * that don't yet support `AdvanceBlocks` reject this variant with
282
+ * HTTP 400.
283
+ *
284
+ * @example
285
+ * ```typescript
286
+ * builder.addAdvanceBlocks({
287
+ * bitcoin_blocks: 1,
288
+ * stacks_blocks_per_bitcoin: 1,
289
+ * });
290
+ * ```
291
+ */
292
+ public addAdvanceBlocks(request: AdvanceBlocksRequest) {
293
+ this.steps.push({
294
+ type: 'AdvanceBlocks',
295
+ request,
264
296
  });
265
297
  return this;
266
298
  }
@@ -414,9 +446,15 @@ To get in touch: contact@stxer.xyz
414
446
  Reads: step.reads,
415
447
  });
416
448
  } else if (step.type === 'TenureExtend') {
417
- // TenureExtend - format: []
449
+ // TenureExtend - modern wire shape (legacy `[]` is still parsed
450
+ // server-side for direct-emit consumers, but the builder always
451
+ // emits the explicit cause).
452
+ v2Steps.push({
453
+ TenureExtend: { cause: step.cause },
454
+ });
455
+ } else if (step.type === 'AdvanceBlocks') {
418
456
  v2Steps.push({
419
- TenureExtend: [],
457
+ AdvanceBlocks: step.request,
420
458
  });
421
459
  } else {
422
460
  console.log(`Invalid simulation step: ${step}`);
@@ -453,13 +491,6 @@ To get in touch: contact@stxer.xyz
453
491
  }
454
492
  }
455
493
 
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
494
  // Helper function to convert ClarityVersion to number
464
495
  function clarityVersionToNumber(version: ClarityVersion): number {
465
496
  switch (version) {
@@ -471,9 +502,177 @@ function clarityVersionToNumber(version: ClarityVersion): number {
471
502
  return 3;
472
503
  case ClarityVersion.Clarity4:
473
504
  return 4;
505
+ case ClarityVersion.Clarity5:
506
+ return 5;
474
507
  default:
475
- return 4;
508
+ return 5;
509
+ }
510
+ }
511
+
512
+ // =============================================================================
513
+ // Session-bound helpers
514
+ // =============================================================================
515
+ //
516
+ // Higher-level helpers that bind to an existing simulation session.
517
+ // Wrap `submitSimulationSteps` / `simulationBatchReads` for the common
518
+ // patterns simulation tests reach for: read a fungible-token balance,
519
+ // read a principal's STX balance / nonce, send a contract call and
520
+ // decode the result.
521
+ //
522
+ // Errors:
523
+ // - Wire-level failures (HTTP non-2xx) propagate as the typed
524
+ // `SimulationError` from `./simulation-api`.
525
+ // - Per-step `Err` results (validation failures) throw a normal
526
+ // `Error` carrying the upstream message.
527
+ // - VM-level failures (`vm_error`) and post-condition aborts are
528
+ // returned via {@link CallContractResult.vmError} / `pcAborted` —
529
+ // they do NOT throw, so callers can assert on them.
530
+
531
+ export interface CallContractResult {
532
+ /** Decoded Clarity result (e.g. `(ok true)`, `(err u100)`, `u5`). */
533
+ result: string;
534
+ /** Hex-encoded raw Clarity result (SIP-005). */
535
+ resultHex: string;
536
+ /** VM-level error message, if any (e.g. arithmetic overflow). */
537
+ vmError: string | null;
538
+ /** True when one or more post-conditions tripped. */
539
+ pcAborted: boolean;
540
+ /** Full upstream receipt for callers that need more detail. */
541
+ receipt: TransactionReceipt;
542
+ }
543
+
544
+ /**
545
+ * Build, submit, and decode a contract-call transaction. The caller
546
+ * supplies the principal to use as `tx-sender`; nonce is auto-fetched
547
+ * via {@link getNonce} unless `nonce` is explicitly provided.
548
+ */
549
+ export async function callContract(
550
+ sessionId: string,
551
+ args: Omit<ContractCallTxArgs, 'nonce'> & {
552
+ nonce?: number;
553
+ postConditionMode?: PostConditionMode;
554
+ },
555
+ options: SimulationApiOptions = {},
556
+ ): Promise<CallContractResult> {
557
+ const nonce =
558
+ args.nonce ?? Number(await getNonce(sessionId, args.sender, options));
559
+ const txHex = await buildUnsignedContractCallHex({ ...args, nonce });
560
+ const r = await submitSimulationSteps(
561
+ sessionId,
562
+ { steps: [{ Transaction: txHex }] },
563
+ options,
564
+ );
565
+ const step = r.steps[0];
566
+ if (!('Transaction' in step)) {
567
+ throw new Error(
568
+ `expected Transaction step, got ${JSON.stringify(step).slice(0, 200)}`,
569
+ );
570
+ }
571
+ if ('Err' in step.Transaction) {
572
+ throw new Error(`Transaction Err: ${step.Transaction.Err}`);
573
+ }
574
+ const receipt = step.Transaction.Ok;
575
+ return {
576
+ result: cvToString(deserializeCV(receipt.result)),
577
+ resultHex: receipt.result,
578
+ vmError: receipt.vm_error,
579
+ pcAborted: receipt.post_condition_aborted,
580
+ receipt,
581
+ };
582
+ }
583
+
584
+ /**
585
+ * Read the SIP-010 / hBTC-style `(get-balance principal)` value.
586
+ * Returns 0n if the read returns `(err …)`. Throws on infra failures.
587
+ */
588
+ export async function getFtBalance(
589
+ sessionId: string,
590
+ contractId: string,
591
+ principal: string,
592
+ options: SimulationApiOptions = {},
593
+ ): Promise<bigint> {
594
+ const r = await simulationBatchReads(
595
+ sessionId,
596
+ {
597
+ readonly: [
598
+ [contractId, 'get-balance', serializeCV(Cl.principal(principal))],
599
+ ],
600
+ },
601
+ options,
602
+ );
603
+ const v = r.readonly?.[0];
604
+ if (!v || !('Ok' in v)) {
605
+ throw new Error(`get-balance(${contractId}) failed: ${JSON.stringify(v)}`);
606
+ }
607
+ const decoded = deserializeCV(v.Ok);
608
+ if (decoded.type === ClarityType.ResponseErr) {
609
+ return 0n;
610
+ }
611
+ if (decoded.type !== ClarityType.ResponseOk) {
612
+ throw new Error(`unexpected get-balance shape: ${cvToString(decoded)}`);
613
+ }
614
+ if (decoded.value.type !== ClarityType.UInt) {
615
+ throw new Error(`expected uint inside (ok …), got ${decoded.value.type}`);
616
+ }
617
+ return BigInt(decoded.value.value);
618
+ }
619
+
620
+ /** Read a principal's STX balance (uSTX) from a simulation session. */
621
+ export async function getStxBalance(
622
+ sessionId: string,
623
+ principal: string,
624
+ options: SimulationApiOptions = {},
625
+ ): Promise<bigint> {
626
+ const r = await simulationBatchReads(
627
+ sessionId,
628
+ { stx: [principal] },
629
+ options,
630
+ );
631
+ const v = r.stx?.[0];
632
+ if (!v || !('Ok' in v)) {
633
+ throw new Error(`stx balance read for ${principal} failed`);
634
+ }
635
+ return BigInt(v.Ok);
636
+ }
637
+
638
+ /** Read a principal's current nonce. Returns 0n if never seen. */
639
+ export async function getNonce(
640
+ sessionId: string,
641
+ principal: string,
642
+ options: SimulationApiOptions = {},
643
+ ): Promise<bigint> {
644
+ const r = await simulationBatchReads(
645
+ sessionId,
646
+ { nonces: [principal] },
647
+ options,
648
+ );
649
+ const v = r.nonces?.[0];
650
+ if (!v || !('Ok' in v)) return 0n;
651
+ return BigInt(v.Ok);
652
+ }
653
+
654
+ /**
655
+ * Read a Clarity data variable. Returns the decoded `ClarityValue`,
656
+ * or throws if the read failed.
657
+ */
658
+ export async function readDataVar(
659
+ sessionId: string,
660
+ contractId: string,
661
+ varName: string,
662
+ options: SimulationApiOptions = {},
663
+ ): Promise<ClarityValue> {
664
+ const r = await simulationBatchReads(
665
+ sessionId,
666
+ { vars: [[contractId, varName]] },
667
+ options,
668
+ );
669
+ const v = r.vars?.[0];
670
+ if (!v || !('Ok' in v)) {
671
+ throw new Error(
672
+ `var-get ${contractId} ${varName} failed: ${JSON.stringify(v)}`,
673
+ );
476
674
  }
675
+ return deserializeCV(v.Ok);
477
676
  }
478
677
 
479
678
  // Re-export simulation types
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Transaction-building helpers for the simulator.
3
+ *
4
+ * The simulator does NOT verify signatures — it derives `tx-sender`
5
+ * from the `signer` field (= hash160 of the public key) plus the
6
+ * `hashMode` byte. By overriding those two fields on an *unsigned*
7
+ * transaction, you can simulate any sender without a private key.
8
+ *
9
+ * {@link setSender} performs the override; {@link buildUnsignedContractCallHex}
10
+ * combines the unsigned-tx builder from `@stacks/transactions` with
11
+ * `setSender` and returns a hex string ready to drop into a
12
+ * `{ Transaction: <hex> }` simulation step.
13
+ */
14
+ import { AddressVersion, type StacksNetworkName } from '@stacks/network';
15
+ import {
16
+ AddressHashMode,
17
+ Cl,
18
+ type ClarityValue,
19
+ type MultiSigSpendingCondition,
20
+ makeUnsignedContractCall,
21
+ PostConditionMode,
22
+ type StacksTransactionWire,
23
+ serializeTransaction,
24
+ } from '@stacks/transactions';
25
+ import { c32addressDecode } from 'c32check';
26
+
27
+ /**
28
+ * Override the spending condition's `signer` (and `hashMode`) so the
29
+ * simulator treats `tx-sender` as `sender`.
30
+ *
31
+ * Single-sig and multi-sig flavours are both handled — the multi-sig
32
+ * spending condition is rebuilt with empty signature fields because
33
+ * the simulator never validates signatures.
34
+ */
35
+ export function setSender(
36
+ tx: StacksTransactionWire,
37
+ sender: string,
38
+ ): StacksTransactionWire {
39
+ const [addressVersion, signer] = c32addressDecode(sender);
40
+ switch (addressVersion) {
41
+ case AddressVersion.MainnetSingleSig:
42
+ case AddressVersion.TestnetSingleSig:
43
+ tx.auth.spendingCondition.hashMode = AddressHashMode.P2PKH;
44
+ tx.auth.spendingCondition.signer = signer;
45
+ break;
46
+ case AddressVersion.MainnetMultiSig:
47
+ case AddressVersion.TestnetMultiSig: {
48
+ const sc = tx.auth.spendingCondition;
49
+ tx.auth.spendingCondition = {
50
+ hashMode: AddressHashMode.P2SH,
51
+ signer,
52
+ fields: [],
53
+ signaturesRequired: 0,
54
+ nonce: sc.nonce,
55
+ fee: sc.fee,
56
+ } as MultiSigSpendingCondition;
57
+ break;
58
+ }
59
+ default:
60
+ throw new Error(`unsupported address version: ${addressVersion}`);
61
+ }
62
+ return tx;
63
+ }
64
+
65
+ export interface ContractCallTxArgs {
66
+ /** Principal to set as `tx-sender`. */
67
+ sender: string;
68
+ /** Fully-qualified contract id, e.g. `"SP1G48….dia-oracle"`. */
69
+ contract: string;
70
+ functionName: string;
71
+ functionArgs: ClarityValue[];
72
+ /** microSTX. Defaults to 1_000. */
73
+ fee?: number;
74
+ /** Nonce to declare. Caller is responsible for sequencing. */
75
+ nonce: number;
76
+ network?: StacksNetworkName;
77
+ /**
78
+ * Post-condition mode. Defaults to `Allow` so the simulator runs
79
+ * the call regardless of asset movements; switch to `Deny` + an
80
+ * explicit list when a test wants to assert post-condition
81
+ * enforcement.
82
+ */
83
+ postConditionMode?: PostConditionMode;
84
+ }
85
+
86
+ /**
87
+ * Build an unsigned contract-call transaction with `sender` patched
88
+ * in via {@link setSender}, then return the serialized hex (no `0x`
89
+ * prefix). Drop the result directly into `{ Transaction: <hex> }`.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * import { Cl } from '@stacks/transactions';
94
+ * import { buildUnsignedContractCallHex } from 'stxer';
95
+ *
96
+ * const txHex = await buildUnsignedContractCallHex({
97
+ * sender: 'SP3573...',
98
+ * contract: 'SM3VDXK...sbtc-deposit',
99
+ * functionName: 'complete-deposit-wrapper',
100
+ * functionArgs: [Cl.bufferFromHex(txid), Cl.uint(0), Cl.uint(amount), ...],
101
+ * nonce: 5,
102
+ * });
103
+ * await submitSimulationSteps(sim, { steps: [{ Transaction: txHex }] });
104
+ * ```
105
+ */
106
+ export async function buildUnsignedContractCallHex(
107
+ args: ContractCallTxArgs,
108
+ ): Promise<string> {
109
+ const [contractAddress, contractName] = args.contract.split('.');
110
+ if (!contractName) {
111
+ throw new Error(`bad contract id "${args.contract}" — expected SP….name`);
112
+ }
113
+ const tx = await makeUnsignedContractCall({
114
+ contractAddress,
115
+ contractName,
116
+ functionName: args.functionName,
117
+ functionArgs: args.functionArgs,
118
+ fee: args.fee ?? 1_000,
119
+ nonce: args.nonce,
120
+ // Dummy 33-byte compressed pubkey. The simulator never validates
121
+ // the signature; setSender overwrites the derived signer hash.
122
+ publicKey: '0'.repeat(66),
123
+ network: args.network ?? 'mainnet',
124
+ postConditionMode: args.postConditionMode ?? PostConditionMode.Allow,
125
+ });
126
+ setSender(tx, args.sender);
127
+ return serializeTransaction(tx);
128
+ }
129
+
130
+ /**
131
+ * Build a `Cl.contractPrincipal` from a `"SP….name"` id string.
132
+ * Trips on standard principals or malformed ids — those should use
133
+ * `Cl.principal` directly.
134
+ */
135
+ export function ftPrincipal(contractId: string): ClarityValue {
136
+ const [addr, name] = contractId.split('.');
137
+ if (!name) {
138
+ throw new Error(`bad contract id "${contractId}" — expected SP….name`);
139
+ }
140
+ return Cl.contractPrincipal(addr, name);
141
+ }