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

hirosystems / stacks-blockchain-api / 4756819452

pending completion
4756819452

push

github

GitHub
Merge pull request #1624 from hirosystems/develop

2077 of 3233 branches covered (64.24%)

139 of 293 new or added lines in 24 files covered. (47.44%)

348 existing lines in 8 files now uncovered.

7306 of 9664 relevant lines covered (75.6%)

1757.68 hits per line

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

76.54
/src/api/init.ts
1
import { Server, createServer } from 'http';
32✔
2
import { Socket } from 'net';
3
import * as express from 'express';
32✔
4
import * as expressWinston from 'express-winston';
32✔
5
import * as winston from 'winston';
6
import { v4 as uuid } from 'uuid';
32✔
7
import * as cors from 'cors';
32✔
8

9
import { createTxRouter } from './routes/tx';
32✔
10
import { createDebugRouter } from './routes/debug';
32✔
11
import { createInfoRouter } from './routes/info';
32✔
12
import { createContractRouter } from './routes/contract';
32✔
13
import { createCoreNodeRpcProxyRouter } from './routes/core-node-rpc-proxy';
32✔
14
import { createBlockRouter } from './routes/block';
32✔
15
import { createFaucetRouter } from './routes/faucets';
32✔
16
import { createAddressRouter } from './routes/address';
32✔
17
import { createSearchRouter } from './routes/search';
32✔
18
import { createStxSupplyRouter } from './routes/stx-supply';
32✔
19
import { createRosettaNetworkRouter } from './routes/rosetta/network';
32✔
20
import { createRosettaMempoolRouter } from './routes/rosetta/mempool';
32✔
21
import { createRosettaBlockRouter } from './routes/rosetta/block';
32✔
22
import { createRosettaAccountRouter } from './routes/rosetta/account';
32✔
23
import { createRosettaConstructionRouter } from './routes/rosetta/construction';
32✔
24
import { apiDocumentationUrl, isProdEnv, logError, logger, LogLevel, waiter } from '../helpers';
32✔
25
import { InvalidRequestError } from '../errors';
32✔
26
import { createBurnchainRouter } from './routes/burnchain';
32✔
27
import { createBnsNamespacesRouter } from './routes/bns/namespaces';
32✔
28
import { createBnsPriceRouter } from './routes/bns/pricing';
32✔
29
import { createBnsNamesRouter } from './routes/bns/names';
32✔
30
import { createBnsAddressesRouter } from './routes/bns/addresses';
32✔
31

32
import { ChainID } from '@stacks/transactions';
32✔
33

34
import * as pathToRegex from 'path-to-regexp';
32✔
35
import * as expressListEndpoints from 'express-list-endpoints';
32✔
36
import { createMiddleware as createPrometheusMiddleware } from '@promster/express';
32✔
37
import { createMicroblockRouter } from './routes/microblock';
32✔
38
import { createStatusRouter } from './routes/status';
32✔
39
import { createTokenRouter } from './routes/tokens/tokens';
32✔
40
import { createFeeRateRouter } from './routes/fee-rate';
32✔
41
import { setResponseNonCacheable } from './controllers/cache-controller';
32✔
42

43
import * as path from 'path';
32✔
44
import * as fs from 'fs';
32✔
45
import { PgStore } from '../datastore/pg-store';
46
import { PgWriteStore } from '../datastore/pg-write-store';
47
import { WebSocketTransmitter } from './routes/ws/web-socket-transmitter';
32✔
48
import { createPox2EventsRouter } from './routes/pox2';
32✔
49
import { isPgConnectionError } from '../datastore/helpers';
32✔
50
import { createStackingRouter } from './routes/stacking';
32✔
51

52
export interface ApiServer {
53
  expressApp: express.Express;
54
  server: Server;
55
  ws: WebSocketTransmitter;
56
  address: string;
57
  datastore: PgStore;
58
  terminate: () => Promise<void>;
59
  forceKill: () => Promise<void>;
60
}
61

62
/** API version as given by .git-info */
63
export const API_VERSION: { branch?: string; commit?: string; tag?: string } = {};
32✔
64

65
export async function startApiServer(opts: {
32✔
66
  datastore: PgStore;
67
  writeDatastore?: PgWriteStore;
68
  chainId: ChainID;
69
  /** If not specified, this is read from the STACKS_BLOCKCHAIN_API_HOST env var. */
70
  serverHost?: string;
71
  /** If not specified, this is read from the STACKS_BLOCKCHAIN_API_PORT env var. */
72
  serverPort?: number;
73
  httpLogLevel?: LogLevel;
74
}): Promise<ApiServer> {
75
  const { datastore, writeDatastore, chainId, serverHost, serverPort, httpLogLevel } = opts;
207✔
76

77
  try {
207✔
78
    const [branch, commit, tag] = fs.readFileSync('.git-info', 'utf-8').split('\n');
207✔
79
    API_VERSION.branch = branch;
×
80
    API_VERSION.commit = commit;
×
81
    API_VERSION.tag = tag;
×
82
  } catch (error) {
83
    logger.error(`Unable to read API version from .git-info`, error);
207✔
84
  }
85

86
  const app = express();
207✔
87
  const apiHost = serverHost ?? process.env['STACKS_BLOCKCHAIN_API_HOST'];
207✔
88
  const apiPort = serverPort ?? parseInt(process.env['STACKS_BLOCKCHAIN_API_PORT'] ?? '');
207!
89

90
  if (!apiHost) {
207!
91
    throw new Error(
×
92
      `STACKS_BLOCKCHAIN_API_HOST must be specified, e.g. "STACKS_BLOCKCHAIN_API_HOST=127.0.0.1"`
93
    );
94
  }
95
  if (!apiPort) {
207!
96
    throw new Error(
×
97
      `STACKS_BLOCKCHAIN_API_PORT must be specified, e.g. "STACKS_BLOCKCHAIN_API_PORT=3999"`
98
    );
99
  }
100

101
  // app.use(compression());
102
  // app.disable('x-powered-by');
103

104
  let routes: {
105
    path: string;
106
    regexp: RegExp;
107
  }[] = [];
207✔
108

109
  if (isProdEnv) {
207!
110
    // The default from
111
    // https://github.com/tdeekens/promster/blob/696803abf03a9a657d4af46d312fa9fb70a75320/packages/metrics/src/create-metric-types/create-metric-types.ts#L16
112
    const defaultPromHttpRequestDurationInSeconds = [0.05, 0.1, 0.3, 0.5, 0.8, 1, 1.5, 2, 3, 10];
×
113

114
    // Add a few more buckets to account for requests that take longer than 10 seconds
115
    defaultPromHttpRequestDurationInSeconds.push(25, 50, 100, 250, 500);
×
116

117
    const promMiddleware = createPrometheusMiddleware({
×
118
      options: {
119
        buckets: defaultPromHttpRequestDurationInSeconds as [number],
120
        normalizePath: path => {
121
          // Get the url pathname without a query string or fragment
122
          // (note base url doesn't matter, but required by URL constructor)
123
          try {
×
124
            let pathTemplate = new URL(path, 'http://x').pathname;
×
125
            // Match request url to the Express route, e.g.:
126
            // `/extended/v1/address/ST26DR4VGV507V1RZ1JNM7NN4K3DTGX810S62SBBR/stx` to
127
            // `/extended/v1/address/:stx_address/stx`
128
            for (const pathRegex of routes) {
×
129
              if (pathRegex.regexp.test(pathTemplate)) {
×
130
                pathTemplate = pathRegex.path;
×
131
                break;
×
132
              }
133
            }
134
            return pathTemplate;
×
135
          } catch (error) {
136
            logger.warn(`Warning: ${error}`);
×
137
            return path;
×
138
          }
139
        },
140
      },
141
    });
142
    app.use(promMiddleware);
×
143
  }
144
  // Add API version to header
145
  app.use((_, res, next) => {
207✔
146
    res.setHeader(
1,288✔
147
      'X-API-Version',
148
      `${API_VERSION.tag} (${API_VERSION.branch}:${API_VERSION.commit})`
149
    );
150
    res.append('Access-Control-Expose-Headers', 'X-API-Version');
1,288✔
151
    next();
1,288✔
152
  });
153
  // Setup request logging
154
  app.use(
207✔
155
    expressWinston.logger({
156
      format: logger.format,
157
      transports: logger.transports,
158
      metaField: (null as unknown) as string,
159
      statusLevels: true,
160
    })
161
  );
162

163
  app.set('json spaces', 2);
207✔
164

165
  // Turn off Express's etag handling. By default CRC32 hashes are generated over response payloads
166
  // which are useless for our use case and wastes CPU.
167
  // See https://expressjs.com/en/api.html#etag.options.table
168
  app.set('etag', false);
207✔
169

170
  app.get('/', (req, res) => {
207✔
171
    res.redirect(`/extended/v1/status`);
×
172
  });
173

174
  app.use('/doc', (req, res) => {
207✔
175
    // if env variable for API_DOCS_URL is given
176
    if (apiDocumentationUrl) {
×
177
      return res.redirect(apiDocumentationUrl);
×
178
    } else if (!isProdEnv) {
×
179
      // use local documentation if serving locally
180
      const apiDocumentationPath = path.join(__dirname + '../../../docs/.tmp/index.html');
×
181
      if (fs.existsSync(apiDocumentationPath)) {
×
182
        return res.sendFile(apiDocumentationPath);
×
183
      }
184

185
      const docNotFound = {
×
186
        error: 'Local documentation not found',
187
        desc: 'Please run the command: `npm run build:docs` and restart your server',
188
      };
189
      return res.send(docNotFound).status(404);
×
190
    }
191
    // for production and no API_DOCS_URL provided
192
    const errObj = {
×
193
      error: 'Documentation is not available',
194
      desc: `You can still read documentation from https://docs.hiro.so/api`,
195
    };
196
    res.send(errObj).status(404);
×
197
  });
198

199
  // Setup extended API v1 routes
200
  app.use(
207✔
201
    '/extended/v1',
202
    (() => {
203
      const router = express.Router();
207✔
204
      router.use(cors());
207✔
205
      router.use((req, res, next) => {
207✔
206
        // Set caching on all routes to be disabled by default, individual routes can override
207
        res.set('Cache-Control', 'no-store');
374✔
208
        next();
374✔
209
      });
210
      router.use('/tx', createTxRouter(datastore));
207✔
211
      router.use('/block', createBlockRouter(datastore));
207✔
212
      router.use('/microblock', createMicroblockRouter(datastore));
207✔
213
      router.use('/burnchain', createBurnchainRouter(datastore));
207✔
214
      router.use('/contract', createContractRouter(datastore));
207✔
215
      // same here, exclude account nonce route
216
      router.use('/address', createAddressRouter(datastore, chainId));
207✔
217
      router.use('/search', createSearchRouter(datastore));
207✔
218
      router.use('/info', createInfoRouter(datastore));
207✔
219
      router.use('/stx_supply', createStxSupplyRouter(datastore));
207✔
220
      router.use('/debug', createDebugRouter(datastore));
207✔
221
      router.use('/status', createStatusRouter(datastore));
207✔
222
      router.use('/fee_rate', createFeeRateRouter(datastore));
207✔
223
      router.use('/tokens', createTokenRouter(datastore));
207✔
224
      router.use('/pox2_events', createPox2EventsRouter(datastore));
207✔
225
      if (chainId !== ChainID.Mainnet && writeDatastore) {
207✔
226
        router.use('/faucets', createFaucetRouter(writeDatastore));
3✔
227
      }
228
      return router;
207✔
229
    })()
230
  );
231

232
  app.use(
207✔
233
    '/extended/beta',
234
    (() => {
235
      const router = express.Router();
207✔
236
      router.use(cors());
207✔
237
      router.use((req, res, next) => {
207✔
238
        // Set caching on all routes to be disabled by default, individual routes can override
NEW
239
        res.set('Cache-Control', 'no-store');
×
NEW
240
        next();
×
241
      });
242
      router.use('/stacking', createStackingRouter(datastore));
207✔
243
      return router;
207✔
244
    })()
245
  );
246

247
  // Setup direct proxy to core-node RPC endpoints (/v2)
248
  // pricing endpoint
249
  app.use(
207✔
250
    '/v2',
251
    (() => {
252
      const router = express.Router();
207✔
253
      router.use(cors());
207✔
254
      router.use('/prices', createBnsPriceRouter(datastore, chainId));
207✔
255
      router.use('/', createCoreNodeRpcProxyRouter(datastore));
207✔
256

257
      return router;
207✔
258
    })()
259
  );
260

261
  // Rosetta API -- https://www.rosetta-api.org
262
  app.use(
207✔
263
    '/rosetta/v1',
264
    (() => {
265
      const router = express.Router();
207✔
266
      router.use(cors());
207✔
267
      router.use('/network', createRosettaNetworkRouter(datastore, chainId));
207✔
268
      router.use('/mempool', createRosettaMempoolRouter(datastore, chainId));
207✔
269
      router.use('/block', createRosettaBlockRouter(datastore, chainId));
207✔
270
      router.use('/account', createRosettaAccountRouter(datastore, chainId));
207✔
271
      router.use('/construction', createRosettaConstructionRouter(datastore, chainId));
207✔
272
      return router;
207✔
273
    })()
274
  );
275

276
  // Setup legacy API v1 and v2 routes
277
  app.use(
207✔
278
    '/v1',
279
    (() => {
280
      const router = express.Router();
207✔
281
      router.use(cors());
207✔
282
      router.use('/namespaces', createBnsNamespacesRouter(datastore));
207✔
283
      router.use('/names', createBnsNamesRouter(datastore, chainId));
207✔
284
      router.use('/addresses', createBnsAddressesRouter(datastore, chainId));
207✔
285
      return router;
207✔
286
    })()
287
  );
288

289
  //handle invalid request gracefully
290
  app.use((req, res) => {
207✔
291
    res.status(404).json({ message: `${req.method} ${req.path} not found` });
×
292
  });
293

294
  // Setup error handler (must be added at the end of the middleware stack)
295
  app.use(((error, req, res, next) => {
207✔
296
    if (req.method === 'GET' && res.statusCode !== 200 && res.hasHeader('ETag')) {
32!
297
      logger.error(
×
298
        `Non-200 request has ETag: ${res.header('ETag')}, Cache-Control: ${res.header(
299
          'Cache-Control'
300
        )}`
301
      );
302
    }
303
    if (error && res.headersSent && res.statusCode !== 200 && res.hasHeader('ETag')) {
32!
304
      logger.error(
×
305
        `A non-200 response with an error in request processing has ETag: ${res.header(
306
          'ETag'
307
        )}, Cache-Control: ${res.header('Cache-Control')}`
308
      );
309
    }
310
    if (!res.headersSent && (error || res.statusCode !== 200)) {
32!
311
      setResponseNonCacheable(res);
17✔
312
    }
313
    if (error && !res.headersSent) {
32✔
314
      if (error instanceof InvalidRequestError) {
17✔
315
        res.status(error.status).json({ error: error.message }).end();
14✔
316
      } else if (isPgConnectionError(error)) {
3✔
317
        res.status(503).json({ error: `The database service is unavailable` }).end();
1✔
318
      } else {
319
        res.status(500);
2✔
320
        const errorTag = uuid();
2✔
321
        Object.assign(error, { errorTag: errorTag });
2✔
322
        res
2✔
323
          .json({ error: error.toString(), stack: (error as Error).stack, errorTag: errorTag })
324
          .end();
325
      }
326
    }
327
    next(error);
32✔
328
  }) as express.ErrorRequestHandler);
329

330
  app.use(
207✔
331
    expressWinston.errorLogger({
332
      winstonInstance: logger as winston.Logger,
333
      metaField: (null as unknown) as string,
334
      blacklistedMetaFields: ['trace', 'os', 'process'],
335
      skip: (_req, _res, error) => {
336
        // Do not log errors for client 4xx responses
337
        return error instanceof InvalidRequestError;
32✔
338
      },
339
    })
340
  );
341

342
  // Store all the registered express routes for usage with metrics reporting
343
  routes = expressListEndpoints(app).map(endpoint => ({
19,467✔
344
    path: endpoint.path,
345
    regexp: pathToRegex.pathToRegexp(endpoint.path),
346
  }));
347

348
  // Manual route definitions for the /v2/ proxied endpoints
349
  routes.push({
207✔
350
    path: '/v2/pox',
351
    regexp: /^\/v2\/pox(.*)/,
352
  });
353
  routes.push({
207✔
354
    path: '/v2/info',
355
    regexp: /^\/v2\/info(.*)/,
356
  });
357
  routes.push({
207✔
358
    path: '/v2/accounts/*',
359
    regexp: /^\/v2\/accounts(.*)/,
360
  });
361
  routes.push({
207✔
362
    path: '/v2/contracts/call-read/*',
363
    regexp: /^\/v2\/contracts\/call-read(.*)/,
364
  });
365
  routes.push({
207✔
366
    path: '/v2/map_entry/*',
367
    regexp: /^\/v2\/map_entry(.*)/,
368
  });
369
  routes.push({
207✔
370
    path: '/v2/*',
371
    regexp: /^\/v2(.*)/,
372
  });
373

374
  const server = createServer(app);
207✔
375

376
  const serverSockets = new Set<Socket>();
207✔
377
  server.on('connection', socket => {
207✔
378
    serverSockets.add(socket);
915✔
379
    socket.once('close', () => {
915✔
380
      serverSockets.delete(socket);
914✔
381
    });
382
  });
383

384
  const ws = new WebSocketTransmitter(datastore, server);
207✔
385
  ws.connect();
207✔
386

387
  await new Promise<void>((resolve, reject) => {
207✔
388
    try {
207✔
389
      server.once('error', error => {
207✔
390
        reject(error);
×
391
      });
392
      server.listen(apiPort, apiHost, () => {
207✔
393
        resolve();
207✔
394
      });
395
    } catch (error) {
396
      reject(error);
×
397
    }
398
  });
399

400
  const terminate = async () => {
207✔
401
    await new Promise<void>((resolve, reject) => {
207✔
402
      logger.info('Closing WebSocket channels...');
207✔
403
      ws.close(error => {
207✔
404
        if (error) {
207!
405
          logError('Failed to gracefully close WebSocket channels', error);
×
406
          reject(error);
×
407
        } else {
408
          logger.info('API WebSocket channels closed.');
207✔
409
          resolve();
207✔
410
        }
411
      });
412
    });
413
    for (const socket of serverSockets) {
207✔
414
      socket.destroy();
166✔
415
    }
416
    await new Promise<void>(resolve => {
207✔
417
      logger.info('Closing API http server...');
207✔
418
      server.close(() => {
207✔
419
        logger.info('API http server closed.');
207✔
420
        resolve();
207✔
421
      });
422
    });
423
  };
424

425
  const forceKill = async () => {
207✔
426
    logger.info('Force closing API server...');
×
427
    const [wsClosePromise, serverClosePromise] = [waiter(), waiter()];
×
428
    ws.close(() => wsClosePromise.finish());
×
429
    server.close(() => serverClosePromise.finish());
×
430
    for (const socket of serverSockets) {
×
431
      socket.destroy();
×
432
    }
433
    await Promise.allSettled([wsClosePromise, serverClosePromise]);
×
434
  };
435

436
  const addr = server.address();
207✔
437
  if (addr === null) {
207!
438
    throw new Error('server missing address');
×
439
  }
440
  const addrStr = typeof addr === 'string' ? addr : `${addr.address}:${addr.port}`;
207!
441
  return {
207✔
442
    expressApp: app,
443
    server: server,
444
    ws: ws,
445
    address: addrStr,
446
    datastore: datastore,
447
    terminate: terminate,
448
    forceKill: forceKill,
449
  };
450
}
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