• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hirosystems / stacks-blockchain-api / 3759933007

pending completion
3759933007

Pull #1498

github

GitHub
Merge 884b9ca06 into 2f9cb0c21
Pull Request #1498: API v7 RC (2.1-capable release)

2075 of 3094 branches covered (67.07%)

482 of 751 new or added lines in 25 files covered. (64.18%)

7 existing lines in 3 files now uncovered.

7256 of 9239 relevant lines covered (78.54%)

1199.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

20.97
/src/api/routes/debug.ts
1
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2
import * as express from 'express';
53✔
3
import { stacksToBitcoinAddress } from 'stacks-encoding-native-js';
53✔
4
import * as bodyParser from 'body-parser';
53✔
5
import { asyncHandler } from '../async-handler';
53✔
6
import { htmlEscape } from 'escape-goat';
53✔
7
import * as listEndpoints from 'express-list-endpoints';
53✔
8
import {
53✔
9
  makeSTXTokenTransfer,
10
  makeContractDeploy,
11
  PostConditionMode,
12
  makeContractCall,
13
  ClarityValue,
14
  getAddressFromPrivateKey,
15
  sponsorTransaction,
16
  makeUnsignedSTXTokenTransfer,
17
  TransactionSigner,
18
  createStacksPrivateKey,
19
  pubKeyfromPrivKey,
20
  publicKeyToString,
21
  addressFromPublicKeys,
22
  AddressHashMode,
23
  createStacksPublicKey,
24
  TransactionVersion,
25
  AddressVersion,
26
  addressToString,
27
  SignedContractCallOptions,
28
  uintCV,
29
  tupleCV,
30
  bufferCV,
31
  AnchorMode,
32
  ChainID,
33
  deserializeTransaction,
34
} from '@stacks/transactions';
35
import { StacksTestnet } from '@stacks/network';
53✔
36
import { SampleContracts } from '../../sample-data/broadcast-contract-default';
53✔
37
import { ClarityAbi, getTypeString, encodeClarityValue } from '../../event-stream/contract-abi';
53✔
38
import { cssEscape, unwrapOptional } from '../../helpers';
53✔
39
import { StacksCoreRpcClient, getCoreNodeEndpoint } from '../../core-rpc/client';
53✔
40
import { PgStore } from '../../datastore/pg-store';
41
import { DbTx } from '../../datastore/common';
42
import * as poxHelpers from '../../pox-helpers';
43
import fetch from 'node-fetch';
53✔
44
import {
45
  RosettaBlockTransactionRequest,
46
  RosettaBlockTransactionResponse,
47
  RosettaConstructionMetadataRequest,
48
  RosettaConstructionMetadataResponse,
49
  RosettaConstructionPayloadResponse,
50
  RosettaConstructionPayloadsRequest,
51
  RosettaConstructionPreprocessRequest,
52
  RosettaConstructionPreprocessResponse,
53
  RosettaConstructionSubmitRequest,
54
  RosettaConstructionSubmitResponse,
55
  RosettaOperation,
56
} from '@stacks/stacks-blockchain-api-types';
57
import { getRosettaNetworkName, RosettaConstants } from '../rosetta-constants';
53✔
58
import { decodeBtcAddress } from '@stacks/stacking';
53✔
59

60
const testnetAccounts = [
53✔
61
  {
62
    secretKey: 'cb3df38053d132895220b9ce471f6b676db5b9bf0b4adefb55f2118ece2478df01',
63
    stacksAddress: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
64
  },
65
  {
66
    secretKey: '21d43d2ae0da1d9d04cfcaac7d397a33733881081f0b2cd038062cf0ccbb752601',
67
    stacksAddress: 'ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y',
68
  },
69
  {
70
    secretKey: 'c71700b07d520a8c9731e4d0f095aa6efb91e16e25fb27ce2b72e7b698f8127a01',
71
    stacksAddress: 'ST1HB1T8WRNBYB0Y3T7WXZS38NKKPTBR3EG9EPJKR',
72
  },
73
  {
74
    secretKey: 'e75dcb66f84287eaf347955e94fa04337298dbd95aa0dbb985771104ef1913db01',
75
    stacksAddress: 'STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP',
76
  },
77
  {
78
    secretKey: 'ce109fee08860bb16337c76647dcbc02df0c06b455dd69bcf30af74d4eedd19301',
79
    stacksAddress: 'STF9B75ADQAVXQHNEQ6KGHXTG7JP305J2GRWF3A2',
80
  },
81
  {
82
    secretKey: '08c14a1eada0dd42b667b40f59f7c8dedb12113613448dc04980aea20b268ddb01',
83
    stacksAddress: 'ST18MDW2PDTBSCR1ACXYRJP2JX70FWNM6YY2VX4SS',
84
  },
85
];
86

87
interface SeededAccount {
88
  secretKey: string;
89
  stacksAddress: string;
90
  pubKey: string;
91
}
92

93
export const testnetKeys: SeededAccount[] = testnetAccounts.map(t => ({
318✔
94
  secretKey: t.secretKey,
95
  stacksAddress: t.stacksAddress,
96
  pubKey: publicKeyToString(pubKeyfromPrivKey(t.secretKey)),
97
}));
98

99
const testnetKeyMap: Record<
100
  string,
101
  { address: string; secretKey: string; pubKey: string }
102
> = Object.fromEntries(
53✔
103
  testnetKeys.map(t => [
318✔
104
    t.stacksAddress,
105
    {
106
      address: t.stacksAddress,
107
      secretKey: t.secretKey,
108
      pubKey: t.pubKey,
109
    },
110
  ])
111
);
112

113
export function getStacksTestnetNetwork() {
53✔
114
  return new StacksTestnet({
231✔
115
    url: `http://${getCoreNodeEndpoint()}`,
116
  });
117
}
118

119
function getRandomInt(min: number, max: number) {
120
  min = Math.ceil(min);
219✔
121
  max = Math.floor(max);
219✔
122
  return Math.floor(Math.random() * (max - min) + min);
219✔
123
}
124

125
export function createDebugRouter(db: PgStore): express.Router {
53✔
126
  const defaultTxFee = 123450;
219✔
127
  const stacksNetwork = getStacksTestnetNetwork();
219✔
128

129
  const router = express.Router();
219✔
130
  router.use(express.urlencoded({ extended: true }));
219✔
131
  router.use(bodyParser.raw({ type: 'application/octet-stream' }));
219✔
132

133
  async function sendCoreTx(serializedTx: Buffer): Promise<{ txId: string }> {
134
    const submitResult = await new StacksCoreRpcClient().sendTransaction(serializedTx);
×
135
    return submitResult;
×
136
  }
137

138
  router.get('/broadcast', (req, res) => {
219✔
139
    const endpoints = listEndpoints((router as unknown) as express.Express);
×
140
    const paths: Set<string> = new Set();
×
141
    endpoints.forEach(e => {
×
142
      if (e.methods.includes('GET')) {
×
143
        paths.add(req.baseUrl + e.path);
×
144
      }
145
    });
146
    const links = [...paths].map(e => {
×
147
      return `<a href="${e}">${e}</a>`;
×
148
    });
149
    const html = links.join('</br>');
×
150
    res.set('Content-Type', 'text/html').send(html);
×
151
  });
152

153
  const tokenTransferFromMultisigHtml = `
219✔
154
    <style>
155
      * { font-family: "Lucida Console", Monaco, monospace; }
156
      input, select {
157
        display: block;
158
        width: 100%;
159
        margin-bottom: 10;
160
      }
161
    </style>
162
    <form action="" method="post">
163
      <label for="signers">Signers</label>
164
      <select name="signers" id="signers" multiple>
165
        ${testnetKeys
166
          .map(k => `<option value="${k.stacksAddress}">${k.stacksAddress}</option>`)
1,314✔
167
          .join('\n')}
168
      </select>
169

170
      <label for="signatures_required">Signatures required</label>
171
      <input type="number" id="signatures_required" name="signatures_required" value="1">
172

173
      <label for="recipient_address">Recipient address</label>
174
      <input list="recipient_addresses" name="recipient_address" value="${
175
        testnetKeys[1].stacksAddress
176
      }">
177
      <datalist id="recipient_addresses">
178
        ${testnetKeys.map(k => '<option value="' + k.stacksAddress + '">').join('\n')}
1,314✔
179
      </datalist>
180

181
      <label for="stx_amount">uSTX amount</label>
182
      <input type="number" id="stx_amount" name="stx_amount" value="5000">
183

184
      <label for="memo">Memo</label>
185
      <input type="text" id="memo" name="memo" value="hello" maxlength="34">
186

187
      <input type="checkbox" id="sponsored" name="sponsored" value="sponsored" style="display:initial;width:auto">
188
      <label for="sponsored">Create sponsored transaction</label>
189

190
      <input type="submit" value="Submit">
191
    </form>
192
  `;
193

194
  router.get(
219✔
195
    '/broadcast/token-transfer-from-multisig',
196
    asyncHandler((req, res) => {
197
      res.set('Content-Type', 'text/html').send(tokenTransferFromMultisigHtml);
×
198
    })
199
  );
200

201
  router.post(
219✔
202
    '/broadcast/token-transfer-from-multisig',
203
    asyncHandler(async (req, res) => {
204
      const {
205
        signers: signersInput,
206
        signatures_required,
207
        recipient_address,
208
        stx_amount,
209
        memo,
210
      } = req.body as {
×
211
        signers: string[] | string;
212
        signatures_required: string;
213
        recipient_address: string;
214
        stx_amount: string;
215
        memo: string;
216
      };
217
      const sponsored = !!req.body.sponsored;
×
218
      const sigsRequired = parseInt(signatures_required);
×
219

220
      const signers = Array.isArray(signersInput) ? signersInput : [signersInput];
×
221
      const signerPubKeys = signers.map(addr => testnetKeyMap[addr].pubKey);
×
222
      const signerPrivateKeys = signers.map(addr => testnetKeyMap[addr].secretKey);
×
223

224
      /*
225
    const transferTx1 = await makeSTXTokenTransfer({
226
      recipient: recipient_address,
227
      amount: new BN(stx_amount),
228
      memo: memo,
229
      network: stacksNetwork,
230
      sponsored: sponsored,
231
      numSignatures: sigsRequired,
232
      // TODO: should this field be named `signerPublicKeys`?
233
      publicKeys: signerPubKeys,
234
      // TODO: should this field be named `signerPrivateKeys`?
235
      signerKeys: signerPrivateKeys,
236
    });
237
    */
238

239
      const transferTx = await makeUnsignedSTXTokenTransfer({
×
240
        recipient: recipient_address,
241
        amount: BigInt(stx_amount),
242
        memo: memo,
243
        network: stacksNetwork,
244
        numSignatures: sigsRequired,
245
        publicKeys: signerPubKeys,
246
        sponsored: sponsored,
247
        fee: defaultTxFee,
248
        anchorMode: AnchorMode.Any,
249
      });
250

251
      const signer = new TransactionSigner(transferTx);
×
252
      let i = 0;
×
253
      for (; i < sigsRequired; i++) {
×
254
        signer.signOrigin(createStacksPrivateKey(signerPrivateKeys[i]));
×
255
      }
256
      for (; i < signers.length; i++) {
×
257
        signer.appendOrigin(createStacksPublicKey(signerPubKeys[i]));
×
258
      }
259

260
      let serialized: Buffer;
261
      let expectedTxId: string;
262
      if (sponsored) {
×
263
        const sponsorKey = testnetKeys[testnetKeys.length - 1].secretKey;
×
264
        const sponsoredTx = await sponsorTransaction({
×
265
          network: stacksNetwork,
266
          transaction: transferTx,
267
          sponsorPrivateKey: sponsorKey,
268
          fee: defaultTxFee,
269
        });
NEW
270
        serialized = Buffer.from(sponsoredTx.serialize());
×
271
        expectedTxId = sponsoredTx.txid();
×
272
      } else {
NEW
273
        serialized = Buffer.from(transferTx.serialize());
×
274
        expectedTxId = transferTx.txid();
×
275
      }
276

277
      const { txId } = await sendCoreTx(serialized);
×
278
      if (txId !== '0x' + expectedTxId) {
×
279
        throw new Error(`Expected ${expectedTxId}, core ${txId}`);
×
280
      }
281
      res
×
282
        .set('Content-Type', 'text/html')
283
        .send(
284
          tokenTransferFromMultisigHtml +
285
            '<h3>Broadcasted transaction:</h3>' +
286
            `<a href="/extended/v1/tx/${txId}">${txId}</a>`
287
        );
288
    })
289
  );
290

291
  const tokenTransferMultisigHtml = `
219✔
292
    <style>
293
      * { font-family: "Lucida Console", Monaco, monospace; }
294
      input, select {
295
        display: block;
296
        width: 100%;
297
        margin-bottom: 10;
298
      }
299
    </style>
300
    <form action="" method="post">
301
      <label for="origin_key">Sender key</label>
302
      <input list="origin_keys" name="origin_key" value="${testnetKeys[0].secretKey}">
303
      <datalist id="origin_keys">
304
        ${testnetKeys.map(k => '<option value="' + k.secretKey + '">').join('\n')}
1,314✔
305
      </datalist>
306

307
      <label for="recipient_addresses">Recipient addresses</label>
308
      <select name="recipient_addresses" id="recipient_addresses" multiple>
309
        ${testnetKeys
310
          .map(k => `<option value="${k.stacksAddress}">${k.stacksAddress}</option>`)
1,314✔
311
          .join('\n')}
312
      </select>
313

314
      <label for="signatures_required">Signatures required</label>
315
      <input type="number" id="signatures_required" name="signatures_required" value="1">
316

317
      <label for="stx_amount">uSTX amount</label>
318
      <input type="number" id="stx_amount" name="stx_amount" value="5000">
319

320
      <label for="memo">Memo</label>
321
      <input type="text" id="memo" name="memo" value="hello" maxlength="34">
322

323
      <input type="checkbox" id="sponsored" name="sponsored" value="sponsored" style="display:initial;width:auto">
324
      <label for="sponsored">Create sponsored transaction</label>
325

326
      <input type="submit" value="Submit">
327
    </form>
328
  `;
329

330
  router.get(
219✔
331
    '/broadcast/token-transfer-multisig',
332
    asyncHandler((req, res) => {
333
      res.set('Content-Type', 'text/html').send(tokenTransferMultisigHtml);
×
334
    })
335
  );
336

337
  router.post(
219✔
338
    '/broadcast/token-transfer-multisig',
339
    asyncHandler(async (req, res) => {
340
      const {
341
        origin_key,
342
        recipient_addresses: recipientInput,
343
        signatures_required,
344
        stx_amount,
345
        memo,
346
      } = req.body as {
×
347
        origin_key: string;
348
        recipient_addresses: string[] | string;
349
        signatures_required: string;
350
        stx_amount: string;
351
        memo: string;
352
      };
353
      const sponsored = !!req.body.sponsored;
×
354

355
      const recipientAddresses = Array.isArray(recipientInput) ? recipientInput : [recipientInput];
×
356
      const recipientPubKeys = recipientAddresses
×
357
        .map(s => testnetKeyMap[s].pubKey)
×
358
        .map(k => createStacksPublicKey(k));
×
359
      const sigRequired = parseInt(signatures_required);
×
360
      const recipientAddress = addressToString(
×
361
        addressFromPublicKeys(
362
          stacksNetwork.version === TransactionVersion.Testnet
×
363
            ? AddressVersion.TestnetMultiSig
364
            : AddressVersion.MainnetMultiSig,
365
          AddressHashMode.SerializeP2SH,
366
          sigRequired,
367
          recipientPubKeys
368
        )
369
      );
370

371
      const transferTx = await makeSTXTokenTransfer({
×
372
        recipient: recipientAddress,
373
        amount: BigInt(stx_amount),
374
        memo: memo,
375
        network: stacksNetwork,
376
        senderKey: origin_key,
377
        sponsored: sponsored,
378
        anchorMode: AnchorMode.Any,
379
        fee: defaultTxFee,
380
      });
381

382
      let serialized: Buffer;
383
      let expectedTxId: string;
384
      if (sponsored) {
×
385
        const sponsorKey = testnetKeys[testnetKeys.length - 1].secretKey;
×
386
        const sponsoredTx = await sponsorTransaction({
×
387
          network: stacksNetwork,
388
          transaction: transferTx,
389
          sponsorPrivateKey: sponsorKey,
390
          fee: defaultTxFee,
391
        });
NEW
392
        serialized = Buffer.from(sponsoredTx.serialize());
×
393
        expectedTxId = sponsoredTx.txid();
×
394
      } else {
NEW
395
        serialized = Buffer.from(transferTx.serialize());
×
396
        expectedTxId = transferTx.txid();
×
397
      }
398

399
      const { txId } = await sendCoreTx(serialized);
×
400
      if (txId !== '0x' + expectedTxId) {
×
401
        throw new Error(`Expected ${expectedTxId}, core ${txId}`);
×
402
      }
403
      res
×
404
        .set('Content-Type', 'text/html')
405
        .send(
406
          tokenTransferMultisigHtml +
407
            '<h3>Broadcasted transaction:</h3>' +
408
            `<a href="/extended/v1/tx/${txId}">${txId}</a>`
409
        );
410
    })
411
  );
412

413
  const tokenTransferHtml = `
219✔
414
    <style>
415
      * { font-family: "Lucida Console", Monaco, monospace; }
416
      input, select {
417
        display: block;
418
        width: 100%;
419
        margin-bottom: 10;
420
      }
421
    </style>
422
    <form action="" method="post">
423
      <label for="origin_key">Sender key</label>
424
      <input list="origin_keys" name="origin_key" value="${testnetKeys[0].secretKey}">
425
      <datalist id="origin_keys">
426
        ${testnetKeys.map(k => '<option value="' + k.secretKey + '">').join('\n')}
1,314✔
427
      </datalist>
428

429
      <label for="recipient_address">Recipient address</label>
430
      <input list="recipient_addresses" name="recipient_address" value="${
431
        testnetKeys[1].stacksAddress
432
      }">
433
      <datalist id="recipient_addresses">
434
        ${testnetKeys.map(k => '<option value="' + k.stacksAddress + '">').join('\n')}
1,314✔
435
      </datalist>
436

437
      <label for="stx_amount">uSTX amount</label>
438
      <input type="number" id="stx_amount" name="stx_amount" value="5000">
439

440
      <label for="memo">Memo</label>
441
      <input type="text" id="memo" name="memo" value="hello" maxlength="34">
442

443
      <label for="nonce">Nonce (empty for auto)</label>
444
      <input type="number" id="nonce" name="nonce" value="">
445

446
      <label for="anchor_mode">Anchor mode</label>
447
      <select id="anchor_mode" name="anchor_mode" size="3">
448
        <option value="1">on chain only</option>
449
        <option value="2">off chain only</option>
450
        <option value="3" selected>any</option>
451
      </select>
452

453
      <input type="checkbox" id="sponsored" name="sponsored" value="sponsored" style="display:initial;width:auto">
454
      <label for="sponsored">Create sponsored transaction</label>
455

456
      <input type="submit" value="Submit">
457
    </form>
458
  `;
459

460
  router.get(
219✔
461
    '/broadcast/token-transfer',
462
    asyncHandler((req, res) => {
463
      res.set('Content-Type', 'text/html').send(tokenTransferHtml);
×
464
    })
465
  );
466

467
  router.post(
219✔
468
    '/broadcast/token-transfer',
469
    asyncHandler(async (req, res) => {
470
      const { origin_key, recipient_address, stx_amount, memo, nonce, anchor_mode } = req.body;
×
471
      const sponsored = !!req.body.sponsored;
×
472

473
      const senderAddress = getAddressFromPrivateKey(origin_key, TransactionVersion.Testnet);
×
474
      const rpcClient = new StacksCoreRpcClient();
×
475
      // const nonce = await rpcClient.getAccountNonce(senderAddress, true);
476
      let txNonce = 0;
×
477
      if (Number.isInteger(Number.parseInt(nonce))) {
×
478
        txNonce = Number.parseInt(nonce);
×
479
      } else {
480
        const latestNonces = await db.getAddressNonces({ stxAddress: senderAddress });
×
481
        txNonce = latestNonces.possibleNextNonce;
×
482
      }
483

484
      const anchorMode: AnchorMode = Number(anchor_mode);
×
485
      const transferTx = await makeSTXTokenTransfer({
×
486
        recipient: recipient_address,
487
        amount: BigInt(stx_amount),
488
        senderKey: origin_key,
489
        network: stacksNetwork,
490
        memo: memo,
491
        sponsored: sponsored,
492
        nonce: txNonce,
493
        anchorMode: anchorMode,
494
        fee: defaultTxFee,
495
      });
496

497
      let serialized: Buffer;
498
      let expectedTxId: string;
499
      if (sponsored) {
×
500
        const sponsorKey = testnetKeys[testnetKeys.length - 1].secretKey;
×
501
        const sponsoredTx = await sponsorTransaction({
×
502
          network: stacksNetwork,
503
          transaction: transferTx,
504
          sponsorPrivateKey: sponsorKey,
505
          fee: defaultTxFee,
506
        });
NEW
507
        serialized = Buffer.from(sponsoredTx.serialize());
×
508
        expectedTxId = sponsoredTx.txid();
×
509
      } else {
NEW
510
        serialized = Buffer.from(transferTx.serialize());
×
511
        expectedTxId = transferTx.txid();
×
512
      }
513

514
      const { txId } = await sendCoreTx(serialized);
×
515
      if (txId !== '0x' + expectedTxId) {
×
516
        throw new Error(`Expected ${expectedTxId}, core ${txId}`);
×
517
      }
518
      res
×
519
        .set('Content-Type', 'text/html')
520
        .send(
521
          tokenTransferHtml +
522
            '<h3>Broadcasted transaction:</h3>' +
523
            `<a href="/extended/v1/tx/${txId}">${txId}</a>`
524
        );
525
    })
526
  );
527

528
  const sendPoxHtml = `
219✔
529
    <style>
530
      * { font-family: "Lucida Console", Monaco, monospace; }
531
      input {
532
        display: block;
533
        width: 100%;
534
        margin-bottom: 10;
535
      }
536
    </style>
537
    <form action="" method="post">
538
      <label for="origin_key">Sender key</label>
539
      <input list="origin_keys" name="origin_key" value="${testnetKeys[0].secretKey}">
540
      <datalist id="origin_keys">
541
        ${testnetKeys.map(k => '<option value="' + k.secretKey + '">').join('\n')}
1,314✔
542
      </datalist>
543

544
      <label for="recipient_address">Recipient address</label>
545
      <input list="recipient_addresses" name="recipient_address" value="${stacksToBitcoinAddress(
546
        testnetKeys[1].stacksAddress
547
      )}">
548
      <datalist id="recipient_addresses">
549
        ${testnetKeys
550
          .map(k => '<option value="' + stacksToBitcoinAddress(k.stacksAddress) + '">')
1,314✔
551
          .join('\n')}
552
      </datalist>
553

554
      <label for="stx_amount">uSTX amount (0 for automatic min_amount_ustx)</label>
555
      <input type="number" id="stx_amount" name="stx_amount" value="0">
556

557
      <label for="cycle_count">Cycles</label>
558
      <input type="number" id="cycle_count" name="cycle_count" value="1">
559

560
      <input type="checkbox" id="use_rosetta" name="use_rosetta" value="use_rosetta" style="display:initial;width:auto">
561
      <label for="use_rosetta">Use Rosetta</label>
562

563
      <input type="submit" value="Submit">
564
    </form>
565
  `;
566

567
  router.get('/broadcast/stack', (req, res) => {
219✔
568
    res.set('Content-Type', 'text/html').send(sendPoxHtml);
×
569
  });
570

571
  async function fetchRosetta<TPostBody, TRes>(port: number, endpoint: string, body: TPostBody) {
NEW
572
    const req = await fetch(`http://localhost:${port}${endpoint}`, {
×
573
      method: 'POST',
574
      headers: { 'Content-Type': 'application/json' },
575
      body: JSON.stringify(body),
576
    });
NEW
577
    const result = await req.json();
×
NEW
578
    return result as TRes;
×
579
  }
580

581
  const rosettaNetwork = {
219✔
582
    blockchain: RosettaConstants.blockchain,
583
    network: getRosettaNetworkName(ChainID.Testnet),
584
  };
585

586
  async function stackWithRosetta(
587
    port: number,
588
    account: SeededAccount,
589
    ustxAmount: bigint,
590
    btcAddr: string,
591
    cycleCount: number
592
  ): Promise<{ txId: string; burnBlockHeight: number }> {
NEW
593
    const stackingOperations: RosettaOperation[] = [
×
594
      {
595
        operation_identifier: {
596
          index: 0,
597
          network_index: 0,
598
        },
599
        related_operations: [],
600
        type: 'stack_stx',
601
        account: {
602
          address: account.stacksAddress,
603
          metadata: {},
604
        },
605
        amount: {
606
          value: '-' + ustxAmount.toString(),
607
          currency: { symbol: 'STX', decimals: 6 },
608
          metadata: {},
609
        },
610
        metadata: {
611
          number_of_cycles: cycleCount,
612
          pox_addr: btcAddr,
613
        },
614
      },
615
      {
616
        operation_identifier: {
617
          index: 1,
618
          network_index: 0,
619
        },
620
        related_operations: [],
621
        type: 'fee',
622
        account: {
623
          address: account.stacksAddress,
624
          metadata: {},
625
        },
626
        amount: {
627
          value: '10000',
628
          currency: { symbol: 'STX', decimals: 6 },
629
        },
630
      },
631
    ];
632

633
    // preprocess
NEW
634
    const preprocessResult = await fetchRosetta<
×
635
      RosettaConstructionPreprocessRequest,
636
      RosettaConstructionPreprocessResponse
637
    >(port, '/rosetta/v1/construction/preprocess', {
638
      network_identifier: rosettaNetwork,
639
      operations: stackingOperations,
640
      metadata: {},
641
      max_fee: [
642
        {
643
          value: '12380898',
644
          currency: { symbol: 'STX', decimals: 6 },
645
          metadata: {},
646
        },
647
      ],
648
      suggested_fee_multiplier: 1,
649
    });
650

651
    // metadata
NEW
652
    const resultMetadata = await fetchRosetta<
×
653
      RosettaConstructionMetadataRequest,
654
      RosettaConstructionMetadataResponse
655
    >(port, '/rosetta/v1/construction/metadata', {
656
      network_identifier: rosettaNetwork,
657
      options: preprocessResult.options!, // using options returned from preprocess
658
      public_keys: [{ hex_bytes: account.pubKey, curve_type: 'secp256k1' }],
659
    });
660

661
    // payload
NEW
662
    const payloadsResult = await fetchRosetta<
×
663
      RosettaConstructionPayloadsRequest,
664
      RosettaConstructionPayloadResponse
665
    >(port, '/rosetta/v1/construction/payloads', {
666
      network_identifier: rosettaNetwork,
667
      operations: stackingOperations, // using same operations as preprocess request
668
      metadata: resultMetadata.metadata, // using metadata from metadata response
669
      public_keys: [{ hex_bytes: account.pubKey, curve_type: 'secp256k1' }],
670
    });
671

672
    // sign tx
NEW
673
    const stacksTx = deserializeTransaction(payloadsResult.unsigned_transaction);
×
NEW
674
    const signer = new TransactionSigner(stacksTx);
×
NEW
675
    signer.signOrigin(createStacksPrivateKey(account.secretKey));
×
NEW
676
    const signedSerializedTx = Buffer.from(stacksTx.serialize()).toString('hex');
×
677

678
    // submit
NEW
679
    const submitResult = await fetchRosetta<
×
680
      RosettaConstructionSubmitRequest,
681
      RosettaConstructionSubmitResponse
682
    >(port, '/rosetta/v1/construction/submit', {
683
      network_identifier: rosettaNetwork,
684
      signed_transaction: '0x' + signedSerializedTx,
685
    });
686

NEW
687
    return {
×
688
      txId: submitResult.transaction_identifier.hash,
689
      burnBlockHeight: resultMetadata.metadata.burn_block_height as number,
690
    };
691
  }
692

693
  router.post(
219✔
694
    '/broadcast/stack',
695
    asyncHandler(async (req, res) => {
NEW
696
      const { origin_key, recipient_address, stx_amount, cycle_count } = req.body;
×
NEW
697
      const cycles = Number(cycle_count);
×
NEW
698
      const useRosetta = !!req.body.use_rosetta;
×
699

700
      const client = new StacksCoreRpcClient();
×
701
      const poxInfo = await client.getPox();
×
702
      const ustxAmount =
NEW
703
        Number(stx_amount) > 0
×
704
          ? BigInt(stx_amount)
705
          : BigInt(Math.round(Number(poxInfo.min_amount_ustx) * 1.1).toString());
706
      const sender = testnetKeys.filter(t => t.secretKey === origin_key)[0];
×
707
      const accountBalance = await client.getAccountBalance(sender.stacksAddress);
×
NEW
708
      if (accountBalance < ustxAmount) {
×
709
        throw new Error(
×
710
          `Min requirement pox amount is ${ustxAmount} but account balance is only ${accountBalance}`
711
        );
712
      }
713

714
      let txId: string;
715
      let burnBlockHeight: number;
716

NEW
717
      if (useRosetta) {
×
NEW
718
        const serverPort = req.socket.localPort as number;
×
719
        // const serverPort = new URL('http://' + req.headers.host).port;
NEW
720
        ({ txId, burnBlockHeight } = await stackWithRosetta(
×
721
          serverPort,
722
          sender,
723
          ustxAmount,
724
          recipient_address,
725
          cycles
726
        ));
727
      } else {
NEW
728
        const [contractAddress, contractName] = poxInfo.contract_id.split('.');
×
NEW
729
        const decodedBtcAddr = decodeBtcAddress(recipient_address);
×
NEW
730
        burnBlockHeight = poxInfo.current_burnchain_block_height as number;
×
NEW
731
        const txOptions: SignedContractCallOptions = {
×
732
          senderKey: sender.secretKey,
733
          contractAddress,
734
          contractName,
735
          functionName: 'stack-stx',
736
          functionArgs: [
737
            uintCV(ustxAmount.toString()),
738
            tupleCV({
739
              hashbytes: bufferCV(decodedBtcAddr.data),
740
              version: bufferCV(Buffer.from([decodedBtcAddr.version])),
741
            }),
742
            uintCV(burnBlockHeight),
743
            uintCV(cycles),
744
          ],
745
          network: stacksNetwork,
746
          anchorMode: AnchorMode.Any,
747
          fee: 10000,
748
          validateWithAbi: false,
749
        };
NEW
750
        const tx = await makeContractCall(txOptions);
×
NEW
751
        const expectedTxId = tx.txid();
×
NEW
752
        const serializedTx = Buffer.from(tx.serialize());
×
NEW
753
        const sendResult = await sendCoreTx(serializedTx);
×
NEW
754
        txId = sendResult.txId;
×
NEW
755
        if (txId !== '0x' + expectedTxId) {
×
NEW
756
          throw new Error(`Expected ${expectedTxId}, core ${txId}`);
×
757
        }
758
      }
759

NEW
760
      res.set('Content-Type', 'text/html').send(
×
761
        sendPoxHtml +
762
          `
763
          <h3>Broadcasted transaction:</h3>
764
          <ul>
765
            <li>Tx: <a href="/extended/v1/tx/${txId}">/extended/v1/tx/${txId}</a></li>
766
            <li>Rosetta lookup: <a href="/extended/v1/debug/rosetta/tx/${txId}">/extended/v1/debug/rosetta/tx/${txId}</a></li>
767
            <li>Used Rosetta: <code>${useRosetta}</code></li>
768
            <li>Contract used: <code>${poxInfo.contract_id}</code></li>
769
            <li>Burn block height: <code>${burnBlockHeight}</code></li>
770
            <li>uSTX amount: <code>${ustxAmount}</code></li>
771
            <li>Cycles: <code>${cycles}</code></li>
772
            <li>Stacking account: <code>${sender.stacksAddress}</code></li>
773
            <li>Reward address: <code>${recipient_address}</code></li>
774
            <li>RPC account: <a href="/v2/accounts/${sender.stacksAddress}?proof=0">/v2/accounts/${sender.stacksAddress}</a></li>
775
            <li>STX balance: <a href="/extended/v1/address/${sender.stacksAddress}/stx">/extended/v1/address/${sender.stacksAddress}/stx</a></li>
776
            <li>Reward slots: <a href="/extended/v1/burnchain/reward_slot_holders/${recipient_address}">/extended/v1/burnchain/reward_slot_holders/${recipient_address}</a></li>
777
            <li>Rewards: <a href="/extended/v1/burnchain/rewards/${recipient_address}">/extended/v1/burnchain/rewards/${recipient_address}</a></li>
778
            <li>Rewards (total): <a href="/extended/v1/burnchain/rewards/${recipient_address}/total">/extended/v1/burnchain/rewards/${recipient_address}/total</a></li>
779
          </ul>
780
          `
781
      );
782
    })
783
  );
784

785
  router.get(
219✔
786
    '/rosetta/tx/:tx_id',
787
    asyncHandler(async (req, res) => {
NEW
788
      const { tx_id } = req.params;
×
NEW
789
      const searchResult = await db.searchHash({ hash: tx_id });
×
NEW
790
      if (!searchResult.found) {
×
NEW
791
        res.status(404).send('Transaction not found');
×
NEW
792
        return;
×
793
      }
NEW
794
      if (searchResult.result.entity_type === 'mempool_tx_id') {
×
NEW
795
        res.status(404).send('Transaction still pending in mempool');
×
NEW
796
        return;
×
797
      }
NEW
798
      const dbTx = searchResult.result.entity_data as DbTx;
×
NEW
799
      const port = req.socket.localPort as number;
×
NEW
800
      const txResult = await fetchRosetta<
×
801
        RosettaBlockTransactionRequest,
802
        RosettaBlockTransactionResponse
803
      >(port, '/rosetta/v1/block/transaction', {
804
        network_identifier: rosettaNetwork,
805
        block_identifier: { hash: dbTx.block_hash },
806
        transaction_identifier: { hash: dbTx.tx_id },
807
      });
808

NEW
809
      res.set('Content-Type', 'application/json').send(JSON.stringify(txResult, null, 2));
×
810
    })
811
  );
812

813
  const contractDeployHtml = `
219✔
814
    <style>
815
      * { font-family: "Lucida Console", Monaco, monospace; }
816
      input, textarea {
817
        display: block;
818
        width: 100%;
819
        margin-bottom: 10;
820
      }
821
    </style>
822
    <form action="" method="post">
823
      <label for="origin_key">Sender key</label>
824
      <input list="origin_keys" name="origin_key" value="${testnetKeys[0].secretKey}">
825
      <datalist id="origin_keys">
826
        ${testnetKeys.map(k => '<option value="' + k.secretKey + '">').join('\n')}
1,314✔
827
      </datalist>
828

829
      <label for="contract_name">Contract name</label>
830
      <input type="text" id="contract_name" name="contract_name" value="${htmlEscape(
831
        `${SampleContracts[0].contractName}-${getRandomInt(1000, 9999)}`
832
      )}" pattern="^[a-zA-Z]([a-zA-Z0-9]|[-_!?+&lt;&gt;=/*])*$|^[-+=/*]$|^[&lt;&gt;]=?$" maxlength="128">
833

834
      <label for="source_code">Contract Clarity source code</label>
835
      <textarea id="source_code" name="source_code" rows="40">${htmlEscape(
836
        SampleContracts[0].contractSource
837
      )}</textarea>
838

839
      <input type="checkbox" id="sponsored" name="sponsored" value="sponsored" style="display:initial;width:auto">
840
      <label for="sponsored">Create sponsored transaction</label>
841

842
      <input type="submit" value="Submit">
843
    </form>
844
  `;
845

846
  router.get('/broadcast/contract-deploy', (req, res) => {
219✔
847
    res.set('Content-Type', 'text/html').send(contractDeployHtml);
×
848
  });
849

850
  router.post(
219✔
851
    '/broadcast/contract-deploy',
852
    asyncHandler(async (req, res) => {
853
      const { origin_key, contract_name, source_code } = req.body;
×
854
      const sponsored = !!req.body.sponsored;
×
855

856
      const senderAddress = getAddressFromPrivateKey(origin_key, stacksNetwork.version);
×
857

858
      const normalized_contract_source = (source_code as string)
×
859
        .replace(/\r/g, '')
860
        .replace(/\t/g, ' ');
861
      const contractDeployTx = await makeContractDeploy({
×
862
        contractName: contract_name,
863
        clarityVersion: 2,
864
        codeBody: normalized_contract_source,
865
        senderKey: origin_key,
866
        network: getStacksTestnetNetwork(),
867
        fee: defaultTxFee,
868
        postConditionMode: PostConditionMode.Allow,
869
        sponsored: sponsored,
870
        anchorMode: AnchorMode.Any,
871
      });
872

873
      let serializedTx: Buffer;
874
      let expectedTxId: string;
875
      if (sponsored) {
×
876
        const sponsorKey = testnetKeys[testnetKeys.length - 1].secretKey;
×
877
        const sponsoredTx = await sponsorTransaction({
×
878
          network: stacksNetwork,
879
          transaction: contractDeployTx,
880
          sponsorPrivateKey: sponsorKey,
881
          fee: defaultTxFee,
882
        });
NEW
883
        serializedTx = Buffer.from(sponsoredTx.serialize());
×
884
        expectedTxId = sponsoredTx.txid();
×
885
      } else {
NEW
886
        serializedTx = Buffer.from(contractDeployTx.serialize());
×
887
        expectedTxId = contractDeployTx.txid();
×
888
      }
889

890
      const contractId = senderAddress + '.' + contract_name;
×
891
      const { txId } = await sendCoreTx(serializedTx);
×
892
      if (txId !== '0x' + expectedTxId) {
×
893
        throw new Error(`Expected ${expectedTxId}, core ${txId}`);
×
894
      }
895
      res
×
896
        .set('Content-Type', 'text/html')
897
        .send(
898
          contractDeployHtml +
899
            '<h3>Broadcasted transaction:</h3>' +
900
            `<a href="/extended/v1/tx/${txId}">${txId}</a>` +
901
            '<h3>Deployed contract:</h3>' +
902
            `<a href="contract-call/${contractId}">${contractId}</a>`
903
        );
904
    })
905
  );
906

907
  const contractCallHtml = `
219✔
908
    <style>
909
      * { font-family: "Lucida Console", Monaco, monospace; }
910
      textarea, input:not([type="radio"]) {
911
        display: block;
912
        width: 100%;
913
        margin-bottom: 5;
914
      }
915
      fieldset {
916
        margin: 10;
917
      }
918
    </style>
919
    <div>Contract ABI:</div>
920
    <textarea readonly rows="15">{contract_abi}</textarea>
921
    <hr/>
922
    <form action="" method="post" target="_blank">
923

924
      <label for="origin_key">Sender key</label>
925
      <input list="origin_keys" name="origin_key" value="${testnetKeys[0].secretKey}">
926
      <datalist id="origin_keys">
927
        ${testnetKeys.map(k => '<option value="' + k.secretKey + '">').join('\n')}
1,314✔
928
      </datalist>
929

930
      <hr/>
931

932
      {function_arg_controls}
933
      <hr/>
934

935
      <input type="checkbox" id="sponsored" name="sponsored" value="sponsored" style="display:initial;width:auto">
936
      <label for="sponsored">Create sponsored transaction</label>
937

938
      <input type="submit" value="Submit">
939

940
    </form>
941
  `;
942

943
  router.get(
219✔
944
    '/broadcast/contract-call/:contract_id',
945
    asyncHandler(async (req, res) => {
946
      const { contract_id } = req.params;
×
947
      const dbContractQuery = await db.getSmartContract(contract_id);
×
948
      if (!dbContractQuery.found) {
×
949
        res.status(404).json({ error: `cannot find contract by ID ${contract_id}` });
×
950
        return;
×
951
      }
952
      const contractAbi: ClarityAbi = JSON.parse(dbContractQuery.result.abi as string);
×
953
      let formHtml = contractCallHtml;
×
954
      let funcHtml = '';
×
955

956
      for (const fn of contractAbi.functions) {
×
957
        const fnName = htmlEscape(fn.name);
×
958

959
        let fnArgsHtml = '';
×
960
        for (const fnArg of fn.args) {
×
961
          const argName = htmlEscape(fn.name + ':' + fnArg.name);
×
962
          fnArgsHtml += `
×
963
          <label for="${argName}">${htmlEscape(fnArg.name)}</label>
964
          <input type="text" name="${argName}" id="${argName}" placeholder="${htmlEscape(
965
            getTypeString(fnArg.type)
966
          )}">`;
967
        }
968

969
        funcHtml += `
×
970
        <style>
971
          #${cssEscape(fn.name)}:not(:checked) ~ #${cssEscape(fn.name)}_args {
972
            pointer-events: none;
973
            opacity: 0.5;
974
          }
975
        </style>
976
        <input type="radio" name="fn_name" id="${fnName}" value="${fnName}">
977
        <label for="${fnName}">Function "${fnName}"</label>
978
        <fieldset id="${fnName}_args">
979
          ${fnArgsHtml}
980
        </fieldset>`;
981
      }
982

983
      formHtml = formHtml.replace(
×
984
        '{contract_abi}',
985
        htmlEscape(JSON.stringify(contractAbi, null, '  '))
986
      );
987
      formHtml = formHtml.replace('{function_arg_controls}', funcHtml);
×
988

989
      res.set('Content-Type', 'text/html').send(formHtml);
×
990
    })
991
  );
992

993
  router.post(
219✔
994
    '/broadcast/contract-call/:contract_id',
995
    asyncHandler(async (req, res) => {
996
      const contractId: string = req.params['contract_id'];
×
997
      const dbContractQuery = await db.getSmartContract(contractId);
×
998
      if (!dbContractQuery.found) {
×
999
        res.status(404).json({ error: `could not find contract by ID ${contractId}` });
×
1000
        return;
×
1001
      }
1002
      const contractAbi: ClarityAbi = JSON.parse(dbContractQuery.result.abi as string);
×
1003

1004
      const body = req.body as Record<string, string>;
×
1005
      const originKey = body['origin_key'];
×
1006
      const functionName = body['fn_name'];
×
1007
      const functionArgs = new Map<string, string>();
×
1008
      for (const entry of Object.entries(body)) {
×
1009
        const [fnName, argName] = entry[0].split(':', 2);
×
1010
        if (fnName === functionName) {
×
1011
          functionArgs.set(argName, entry[1]);
×
1012
        }
1013
      }
1014

1015
      const abiFunction = contractAbi.functions.find(f => f.name === functionName);
×
1016
      if (abiFunction === undefined) {
×
1017
        throw new Error(`Contract ${contractId} ABI does not have function "${functionName}"`);
×
1018
      }
1019

1020
      const clarityValueArgs: ClarityValue[] = new Array(abiFunction.args.length);
×
1021
      for (let i = 0; i < clarityValueArgs.length; i++) {
×
1022
        const abiArg = abiFunction.args[i];
×
1023
        const stringArg = unwrapOptional(functionArgs.get(abiArg.name));
×
1024
        const clarityVal = encodeClarityValue(abiArg.type, stringArg);
×
1025
        clarityValueArgs[i] = clarityVal;
×
1026
      }
1027
      const [contractAddr, contractName] = contractId.split('.');
×
1028

1029
      const sponsored = !!req.body.sponsored;
×
1030

1031
      const contractCallTx = await makeContractCall({
×
1032
        contractAddress: contractAddr,
1033
        contractName: contractName,
1034
        functionName: functionName,
1035
        functionArgs: clarityValueArgs,
1036
        senderKey: originKey,
1037
        network: stacksNetwork,
1038
        fee: defaultTxFee,
1039
        postConditionMode: PostConditionMode.Allow,
1040
        sponsored: sponsored,
1041
        anchorMode: AnchorMode.Any,
1042
      });
1043

1044
      let serialized: Buffer;
1045
      let expectedTxId: string;
1046
      if (sponsored) {
×
1047
        const sponsorKey = testnetKeys[testnetKeys.length - 1].secretKey;
×
1048
        const sponsoredTx = await sponsorTransaction({
×
1049
          network: stacksNetwork,
1050
          transaction: contractCallTx,
1051
          sponsorPrivateKey: sponsorKey,
1052
          fee: defaultTxFee,
1053
        });
NEW
1054
        serialized = Buffer.from(sponsoredTx.serialize());
×
1055
        expectedTxId = sponsoredTx.txid();
×
1056
      } else {
NEW
1057
        serialized = Buffer.from(contractCallTx.serialize());
×
1058
        expectedTxId = contractCallTx.txid();
×
1059
      }
1060

1061
      const { txId } = await sendCoreTx(serialized);
×
1062
      if (txId !== '0x' + expectedTxId) {
×
1063
        throw new Error(`Expected ${expectedTxId}, core ${txId}`);
×
1064
      }
1065
      res
×
1066
        .set('Content-Type', 'text/html')
1067
        .send(
1068
          '<h3>Broadcasted transaction:</h3>' + `<a href="/extended/v1/tx/${txId}">${txId}</a>`
1069
        );
1070
    })
1071
  );
1072

1073
  const txWatchHtml = `
219✔
1074
    <style>
1075
      * { font-family: "Lucida Console", Monaco, monospace; }
1076
      p { white-space: pre-wrap; }
1077
    </style>
1078
    <script>
1079
      const sse = new EventSource('/extended/v1/tx/stream?protocol=eventsource');
1080
      sse.addEventListener('tx', e => {
1081
        console.log(JSON.parse(e.data));
1082
        const p = document.createElement('p');
1083
        p.textContent = JSON.stringify(JSON.parse(e.data), null, '    ');
1084
        document.body.append(p);
1085
        document.body.append(document.createElement('hr'));
1086
      });
1087
    </script>
1088
  `;
1089

1090
  router.get('/watch-tx', (req, res) => {
219✔
1091
    res.set('Content-Type', 'text/html').send(txWatchHtml);
×
1092
  });
1093

1094
  router.post('/faucet', (req, res) => {
219✔
1095
    // Redirect with 307 because: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307
1096
    // "... the difference between 307 and 302 is that 307 guarantees that the method and the body
1097
    //  will not be changed when the redirected request is made ... the behavior with non-GET
1098
    //  methods and 302 is then unpredictable on the Web."
1099
    const address: string = req.query.address || req.body.address;
×
1100
    res.redirect(307, `../faucets/stx?address=${encodeURIComponent(address)}`);
×
1101
  });
1102

1103
  return router;
219✔
1104
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc