stxer 0.2.6 → 0.3.1

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.
@@ -0,0 +1,157 @@
1
+ import {
2
+ type ClarityValue,
3
+ type ContractPrincipalCV,
4
+ deserializeCV,
5
+ type OptionalCV,
6
+ serializeCV,
7
+ } from '@stacks/transactions';
8
+
9
+ export interface BatchReads {
10
+ variables?: {
11
+ contract: ContractPrincipalCV;
12
+ variableName: string;
13
+ }[];
14
+ maps?: {
15
+ contract: ContractPrincipalCV;
16
+ mapName: string;
17
+ mapKey: ClarityValue;
18
+ }[];
19
+ index_block_hash?: string;
20
+ }
21
+
22
+ export interface BatchReadsResult {
23
+ variables: (ClarityValue | Error)[];
24
+ maps: (OptionalCV | Error)[];
25
+ }
26
+
27
+ export interface BatchApiOptions {
28
+ stxerApi?: string;
29
+ }
30
+
31
+ const DEFAULT_STXER_API = 'https://api.stxer.xyz';
32
+
33
+ export async function batchRead(
34
+ reads: BatchReads,
35
+ options: BatchApiOptions = {}
36
+ ): Promise<BatchReadsResult> {
37
+ const payload: string[][] = [];
38
+ if (reads.variables != null) {
39
+ for (const variable of reads.variables) {
40
+ payload.push([serializeCV(variable.contract), variable.variableName]);
41
+ }
42
+ }
43
+ if (reads.maps != null) {
44
+ for (const map of reads.maps) {
45
+ payload.push([
46
+ serializeCV(map.contract),
47
+ map.mapName,
48
+ serializeCV(map.mapKey),
49
+ ]);
50
+ }
51
+ }
52
+
53
+ const ibh =
54
+ reads.index_block_hash == null
55
+ ? null
56
+ : reads.index_block_hash.startsWith('0x')
57
+ ? reads.index_block_hash.substring(2)
58
+ : reads.index_block_hash;
59
+
60
+ const url = `${options.stxerApi ?? DEFAULT_STXER_API}/sidecar/v2/batch-reads${
61
+ ibh == null ? '' : `?tip=${ibh}`
62
+ }`;
63
+ const data = await fetch(url, {
64
+ method: 'POST',
65
+ body: JSON.stringify(payload),
66
+ });
67
+ const text = await data.text();
68
+ if (!text.includes('Ok') && !text.includes('Err')) {
69
+ throw new Error(
70
+ `Requesting batch reads failed: ${text}, url: ${url}, payload: ${JSON.stringify(
71
+ payload
72
+ )}`
73
+ );
74
+ }
75
+ const results = JSON.parse(text) as [{ Ok: string } | { Err: string }];
76
+ const rs: BatchReadsResult = {
77
+ variables: [],
78
+ maps: [],
79
+ };
80
+ let variablesLength = 0;
81
+ if (reads.variables != null) {
82
+ variablesLength = reads.variables.length;
83
+ for (let i = 0; i < variablesLength; i++) {
84
+ const result = results[i];
85
+ if ('Ok' in result) {
86
+ rs.variables.push(deserializeCV(result.Ok));
87
+ } else {
88
+ rs.variables.push(new Error(result.Err));
89
+ }
90
+ }
91
+ }
92
+ if (reads.maps != null) {
93
+ for (let i = 0; i < reads.maps.length; i++) {
94
+ const result = results[i + variablesLength];
95
+ if ('Ok' in result) {
96
+ rs.maps.push(deserializeCV(result.Ok) as OptionalCV);
97
+ } else {
98
+ rs.maps.push(new Error(result.Err));
99
+ }
100
+ }
101
+ }
102
+ return rs;
103
+ }
104
+
105
+ export interface BatchReadonlyRequest {
106
+ readonly: {
107
+ contract: ContractPrincipalCV;
108
+ functionName: string;
109
+ functionArgs: ClarityValue[];
110
+ }[];
111
+ index_block_hash?: string;
112
+ }
113
+
114
+ export async function batchReadonly(
115
+ req: BatchReadonlyRequest,
116
+ options: BatchApiOptions = {}
117
+ ): Promise<(ClarityValue | Error)[]> {
118
+ const payload = req.readonly.map((r) => [
119
+ serializeCV(r.contract),
120
+ r.functionName,
121
+ ...r.functionArgs.map((arg) => serializeCV(arg)),
122
+ ]);
123
+
124
+ const ibh =
125
+ req.index_block_hash == null
126
+ ? null
127
+ : req.index_block_hash.startsWith('0x')
128
+ ? req.index_block_hash.substring(2)
129
+ : req.index_block_hash;
130
+
131
+ const url = `${options.stxerApi ?? DEFAULT_STXER_API}/sidecar/v2/batch-readonly${
132
+ ibh == null ? '' : `?tip=${ibh}`
133
+ }`;
134
+ const data = await fetch(url, {
135
+ method: 'POST',
136
+ body: JSON.stringify(payload),
137
+ });
138
+ const text = await data.text();
139
+ if (!text.includes('Ok') && !text.includes('Err')) {
140
+ throw new Error(
141
+ `Requesting batch readonly failed: ${text}, url: ${url}, payload: ${JSON.stringify(
142
+ payload
143
+ )}`
144
+ );
145
+ }
146
+ const results = JSON.parse(text) as [{ Ok: string } | { Err: string }];
147
+ const rs: (ClarityValue | Error)[] = [];
148
+ for (const result of results) {
149
+ if ('Ok' in result) {
150
+ rs.push(deserializeCV(result.Ok));
151
+ } else {
152
+ rs.push(new Error(result.Err));
153
+ }
154
+ }
155
+ return rs;
156
+ }
157
+
package/src/index.ts CHANGED
@@ -1,388 +1,2 @@
1
- import { type AccountDataResponse, getNodeInfo, richFetch } from 'ts-clarity';
2
- import type { Block } from '@stacks/stacks-blockchain-api-types';
3
- import {
4
- networkFrom,
5
- STACKS_MAINNET,
6
- STACKS_TESTNET,
7
- type StacksNetworkName,
8
- } from '@stacks/network';
9
- import {
10
- type ClarityValue,
11
- ClarityVersion,
12
- PostConditionMode,
13
- type StacksTransactionWire,
14
- bufferCV,
15
- contractPrincipalCV,
16
- makeUnsignedContractCall,
17
- makeUnsignedContractDeploy,
18
- makeUnsignedSTXTokenTransfer,
19
- serializeCVBytes,
20
- stringAsciiCV,
21
- tupleCV,
22
- uintCV,
23
- } from '@stacks/transactions';
24
- import { c32addressDecode } from 'c32check';
25
-
26
- function runTx(tx: StacksTransactionWire) {
27
- // type 0: run transaction
28
- return tupleCV({ type: uintCV(0), data: bufferCV(tx.serializeBytes()) });
29
- }
30
-
31
- export interface SimulationEval {
32
- contract_id: string;
33
- code: string;
34
- }
35
-
36
- export function runEval({ contract_id, code }: SimulationEval) {
37
- const [contract_address, contract_name] = contract_id.split('.');
38
- // type 1: eval arbitrary code inside a contract
39
- return tupleCV({
40
- type: uintCV(1),
41
- data: bufferCV(
42
- serializeCVBytes(
43
- tupleCV({
44
- contract: contractPrincipalCV(contract_address, contract_name),
45
- code: stringAsciiCV(code),
46
- })
47
- )
48
- ),
49
- });
50
- }
51
-
52
- export async function runSimulation(
53
- apiEndpoint: string,
54
- block_hash: string,
55
- block_height: number,
56
- txs: (StacksTransactionWire | SimulationEval)[]
57
- ) {
58
- // Convert 'sim-v1' to Uint8Array
59
- const header = new TextEncoder().encode('sim-v1');
60
- // Create 8 bytes for block height
61
- const heightBytes = new Uint8Array(8);
62
- // Convert block height to bytes
63
- const view = new DataView(heightBytes.buffer);
64
- view.setBigUint64(0, BigInt(block_height), false); // false for big-endian
65
-
66
- // Convert block hash to bytes
67
- const hashHex = block_hash.startsWith('0x')
68
- ? block_hash.substring(2)
69
- : block_hash;
70
- // Replace non-null assertion with null check
71
- const matches = hashHex.match(/.{1,2}/g);
72
- if (!matches) {
73
- throw new Error('Invalid block hash format');
74
- }
75
- const hashBytes = new Uint8Array(
76
- matches.map((byte) => Number.parseInt(byte, 16))
77
- );
78
-
79
- // Convert transactions to bytes
80
- const txBytes = txs
81
- .map((t) => ('contract_id' in t && 'code' in t ? runEval(t) : runTx(t)))
82
- .map((t) => serializeCVBytes(t));
83
-
84
- // Combine all byte arrays
85
- const totalLength =
86
- header.length +
87
- heightBytes.length +
88
- hashBytes.length +
89
- txBytes.reduce((acc, curr) => acc + curr.length, 0);
90
- const body = new Uint8Array(totalLength);
91
-
92
- let offset = 0;
93
- body.set(header, offset);
94
- offset += header.length;
95
- body.set(heightBytes, offset);
96
- offset += heightBytes.length;
97
- body.set(hashBytes, offset);
98
- offset += hashBytes.length;
99
- for (const tx of txBytes) {
100
- body.set(tx, offset);
101
- offset += tx.length;
102
- }
103
-
104
- const rs = await fetch(apiEndpoint, {
105
- method: 'POST',
106
- body,
107
- }).then(async (rs) => {
108
- const response = await rs.text();
109
- if (!response.startsWith('{')) {
110
- throw new Error(`failed to submit simulation: ${response}`);
111
- }
112
- return JSON.parse(response) as { id: string };
113
- });
114
- return rs.id;
115
- }
116
-
117
- interface SimulationBuilderOptions {
118
- apiEndpoint?: string;
119
- stacksNodeAPI?: string;
120
- network?: StacksNetworkName | string;
121
- }
122
-
123
- export class SimulationBuilder {
124
- private apiEndpoint: string;
125
- private stacksNodeAPI: string;
126
- private network: StacksNetworkName | string;
127
-
128
- private constructor(options: SimulationBuilderOptions = {}) {
129
- this.apiEndpoint = options.apiEndpoint ?? 'https://api.stxer.xyz';
130
- this.stacksNodeAPI = options.stacksNodeAPI ?? 'https://api.hiro.so';
131
- this.network = options.network ?? 'mainnet';
132
- }
133
-
134
- public static new(options?: SimulationBuilderOptions) {
135
- return new SimulationBuilder(options);
136
- }
137
-
138
- // biome-ignore lint/style/useNumberNamespace: <explanation>
139
- private block = NaN;
140
- private sender = '';
141
- private steps: (
142
- | {
143
- // contract call
144
- contract_id: string;
145
- function_name: string;
146
- function_args?: ClarityValue[];
147
- sender: string;
148
- fee: number;
149
- }
150
- | {
151
- // contract deploy
152
- contract_name: string;
153
- source_code: string;
154
- deployer: string;
155
- fee: number;
156
- clarity_version: ClarityVersion;
157
- }
158
- | {
159
- // STX transfer
160
- recipient: string;
161
- amount: number;
162
- sender: string;
163
- fee: number;
164
- }
165
- | SimulationEval
166
- )[] = [];
167
-
168
- public useBlockHeight(block: number) {
169
- this.block = block;
170
- return this;
171
- }
172
- public withSender(address: string) {
173
- this.sender = address;
174
- return this;
175
- }
176
- public addSTXTransfer(params: {
177
- recipient: string;
178
- amount: number;
179
- sender?: string;
180
- fee?: number;
181
- }) {
182
- if (params.sender == null && this.sender === '') {
183
- throw new Error(
184
- 'Please specify a sender with useSender or adding a sender paramenter'
185
- );
186
- }
187
- this.steps.push({
188
- ...params,
189
- sender: params.sender ?? this.sender,
190
- fee: params.fee ?? 0,
191
- });
192
- return this;
193
- }
194
- public addContractCall(params: {
195
- contract_id: string;
196
- function_name: string;
197
- function_args?: ClarityValue[];
198
- sender?: string;
199
- fee?: number;
200
- }) {
201
- if (params.sender == null && this.sender === '') {
202
- throw new Error(
203
- 'Please specify a sender with useSender or adding a sender paramenter'
204
- );
205
- }
206
- this.steps.push({
207
- ...params,
208
- sender: params.sender ?? this.sender,
209
- fee: params.fee ?? 0,
210
- });
211
- return this;
212
- }
213
- public addContractDeploy(params: {
214
- contract_name: string;
215
- source_code: string;
216
- deployer?: string;
217
- fee?: number;
218
- clarity_version?: ClarityVersion;
219
- }) {
220
- if (params.deployer == null && this.sender === '') {
221
- throw new Error(
222
- 'Please specify a deployer with useSender or adding a deployer paramenter'
223
- );
224
- }
225
- this.steps.push({
226
- ...params,
227
- deployer: params.deployer ?? this.sender,
228
- fee: params.fee ?? 0,
229
- clarity_version: params.clarity_version ?? ClarityVersion.Clarity3,
230
- });
231
- return this;
232
- }
233
- public addEvalCode(inside_contract_id: string, code: string) {
234
- this.steps.push({
235
- contract_id: inside_contract_id,
236
- code,
237
- });
238
- return this;
239
- }
240
- public addMapRead(contract_id: string, map: string, key: string) {
241
- this.steps.push({
242
- contract_id,
243
- code: `(map-get ${map} ${key})`,
244
- });
245
- return this;
246
- }
247
- public addVarRead(contract_id: string, variable: string) {
248
- this.steps.push({
249
- contract_id,
250
- code: `(var-get ${variable})`,
251
- });
252
- return this;
253
- }
254
-
255
- private async getBlockInfo() {
256
- if (Number.isNaN(this.block)) {
257
- const { stacks_tip_height } = await getNodeInfo({
258
- stacksEndpoint: this.stacksNodeAPI,
259
- });
260
- this.block = stacks_tip_height;
261
- }
262
- const info: Block = await richFetch(
263
- `${this.stacksNodeAPI}/extended/v1/block/by_height/${this.block}?unanchored=true`
264
- ).then((r) => r.json());
265
- if (
266
- info.height !== this.block ||
267
- typeof info.hash !== 'string' ||
268
- !info.hash.startsWith('0x')
269
- ) {
270
- throw new Error(
271
- `failed to get block info for block height ${this.block}`
272
- );
273
- }
274
- return {
275
- block_height: this.block,
276
- block_hash: info.hash.substring(2),
277
- index_block_hash: info.index_block_hash.substring(2),
278
- };
279
- }
280
-
281
- public async run() {
282
- console.log(
283
- `--------------------------------
284
- This product can never exist without your support!
285
-
286
- We receive sponsorship funds with:
287
- SP212Y5JKN59YP3GYG07K3S8W5SSGE4KH6B5STXER
288
-
289
- Feedbacks and feature requests are welcome.
290
- To get in touch: contact@stxer.xyz
291
- --------------------------------`
292
- );
293
- const block = await this.getBlockInfo();
294
- console.log(
295
- `Using block height ${block.block_height} hash 0x${block.block_hash} to run simulation.`
296
- );
297
- const txs: (StacksTransactionWire | SimulationEval)[] = [];
298
- const nonce_by_address = new Map<string, number>();
299
- const nextNonce = async (sender: string) => {
300
- const nonce = nonce_by_address.get(sender);
301
- if (nonce == null) {
302
- const url = `${
303
- this.stacksNodeAPI
304
- }/v2/accounts/${sender}?proof=${false}&tip=${block.index_block_hash}`;
305
- const account: AccountDataResponse = await richFetch(url).then((r) =>
306
- r.json()
307
- );
308
- nonce_by_address.set(sender, account.nonce + 1);
309
- return account.nonce;
310
- }
311
- nonce_by_address.set(sender, nonce + 1);
312
- return nonce;
313
- };
314
- for (const step of this.steps) {
315
- let network = this.network === 'mainnet' ? STACKS_MAINNET : STACKS_TESTNET;
316
- if (this.stacksNodeAPI) {
317
- network = {
318
- ...network,
319
- client: {
320
- ...network.client,
321
- baseUrl: this.stacksNodeAPI,
322
- },
323
- };
324
- }
325
- if ('sender' in step && 'function_name' in step) {
326
- const nonce = await nextNonce(step.sender);
327
- const [contractAddress, contractName] = step.contract_id.split('.');
328
- const tx = await makeUnsignedContractCall({
329
- contractAddress,
330
- contractName,
331
- functionName: step.function_name,
332
- functionArgs: step.function_args ?? [],
333
- nonce,
334
- network,
335
- publicKey: '',
336
- postConditionMode: PostConditionMode.Allow,
337
- fee: step.fee,
338
- });
339
- tx.auth.spendingCondition.signer = c32addressDecode(step.sender)[1];
340
- txs.push(tx);
341
- } else if ('sender' in step && 'recipient' in step) {
342
- const nonce = await nextNonce(step.sender);
343
- const tx = await makeUnsignedSTXTokenTransfer({
344
- recipient: step.recipient,
345
- amount: step.amount,
346
- nonce,
347
- network,
348
- publicKey: '',
349
- fee: step.fee,
350
- });
351
- tx.auth.spendingCondition.signer = c32addressDecode(step.sender)[1];
352
- txs.push(tx);
353
- } else if ('deployer' in step) {
354
- const nonce = await nextNonce(step.deployer);
355
- const tx = await makeUnsignedContractDeploy({
356
- contractName: step.contract_name,
357
- codeBody: step.source_code,
358
- nonce,
359
- network,
360
- publicKey: '',
361
- postConditionMode: PostConditionMode.Allow,
362
- fee: step.fee,
363
- });
364
- tx.auth.spendingCondition.signer = c32addressDecode(step.deployer)[1];
365
- txs.push(tx);
366
- } else if ('code' in step) {
367
- txs.push(step);
368
- } else {
369
- // biome-ignore lint/style/noUnusedTemplateLiteral: <explanation>
370
- console.log(`Invalid simulation step:`, step);
371
- }
372
- }
373
- const id = await runSimulation(
374
- `${this.apiEndpoint}/simulations`,
375
- block.block_hash,
376
- block.block_height,
377
- txs
378
- );
379
- console.log(
380
- `Simulation will be available at: https://stxer.xyz/simulations/${this.network}/${id}`
381
- );
382
- return id;
383
- }
384
-
385
- public pipe(transform: (builder: SimulationBuilder) => SimulationBuilder): SimulationBuilder {
386
- return transform(this);
387
- }
388
- }
1
+ export * from './batch-api';
2
+ export * from './simulation';
@@ -1,8 +1,9 @@
1
1
  import { uintCV } from '@stacks/transactions';
2
2
  import { SimulationBuilder } from '..';
3
3
 
4
- SimulationBuilder.new()
4
+ const test = () => SimulationBuilder.new()
5
5
  .withSender('SP212Y5JKN59YP3GYG07K3S8W5SSGE4KH6B5STXER')
6
+ .inlineSimulation('1ab04a8d13d72a301b77b6af9d4f612b')
6
7
  .addContractDeploy({
7
8
  contract_name: 'test-simulation',
8
9
  source_code: `
@@ -37,46 +38,9 @@ SimulationBuilder.new()
37
38
  '(get-counter)'
38
39
  )
39
40
  .run()
40
- .catch(console.error);
41
41
 
42
- SimulationBuilder.new({
43
- apiEndpoint: 'https://testnet-api.stxer.xyz',
44
- stacksNodeAPI: 'https://api.testnet.hiro.so',
45
- network: 'testnet',
46
- })
47
- .withSender('ST3MZM9WJ34Y4311XBJDBKQ41SXX5DY68406J26WJ')
48
- .addContractDeploy({
49
- contract_name: 'test-simulation',
50
- source_code: `
51
- ;; counter example
52
- (define-data-var counter uint u0)
53
-
54
- (define-public (increment (delta uint))
55
- (begin
56
- (var-set counter (+ (var-get counter) delta))
57
- (ok (var-get counter))))
58
-
59
- (define-public (decrement)
60
- (begin
61
- (var-set counter (- (var-get counter) u1))
62
- (ok (var-get counter))))
63
-
64
- (define-read-only (get-counter)
65
- (ok (var-get counter)))
66
- `,
67
- })
68
- .addEvalCode(
69
- 'ST3MZM9WJ34Y4311XBJDBKQ41SXX5DY68406J26WJ.test-simulation',
70
- '(get-counter)'
71
- )
72
- .addContractCall({
73
- contract_id: 'ST3MZM9WJ34Y4311XBJDBKQ41SXX5DY68406J26WJ.test-simulation',
74
- function_name: 'increment',
75
- function_args: [uintCV(10)],
76
- })
77
- .addEvalCode(
78
- 'ST3MZM9WJ34Y4311XBJDBKQ41SXX5DY68406J26WJ.test-simulation',
79
- '(get-counter)'
80
- )
81
- .run()
82
- .catch(console.error);
42
+ if (require.main === module) {
43
+ ; (async () => {
44
+ await test()
45
+ })().catch(console.error);
46
+ }