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

hirosystems / stacks-blockchain-api / 4874244011

pending completion
4874244011

push

github

GitHub
Logging migration to Pino library (#1630)

2071 of 3228 branches covered (64.16%)

130 of 231 new or added lines in 41 files covered. (56.28%)

71 existing lines in 2 files now uncovered.

7294 of 9606 relevant lines covered (75.93%)

1764.88 hits per line

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

76.45
/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 { v4 as uuid } from 'uuid';
32✔
5
import * as cors from 'cors';
32✔
6

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

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

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

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

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

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

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

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

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

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

99
  // app.use(compression());
100
  // app.disable('x-powered-by');
101

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

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

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

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

152
  // Common logger middleware for the whole API.
153
  app.use(loggerMiddleware);
207✔
154

155
  app.set('json spaces', 2);
207✔
156

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

162
  app.get('/', (req, res) => {
207✔
163
    res.redirect(`/extended/v1/status`);
×
164
  });
165

166
  app.use('/doc', (req, res) => {
207✔
167
    // if env variable for API_DOCS_URL is given
168
    if (apiDocumentationUrl) {
×
169
      return res.redirect(apiDocumentationUrl);
×
170
    } else if (!isProdEnv) {
×
171
      // use local documentation if serving locally
172
      const apiDocumentationPath = path.join(__dirname + '../../../docs/.tmp/index.html');
×
173
      if (fs.existsSync(apiDocumentationPath)) {
×
174
        return res.sendFile(apiDocumentationPath);
×
175
      }
176

177
      const docNotFound = {
×
178
        error: 'Local documentation not found',
179
        desc: 'Please run the command: `npm run build:docs` and restart your server',
180
      };
181
      return res.send(docNotFound).status(404);
×
182
    }
183
    // for production and no API_DOCS_URL provided
184
    const errObj = {
×
185
      error: 'Documentation is not available',
186
      desc: `You can still read documentation from https://docs.hiro.so/api`,
187
    };
188
    res.send(errObj).status(404);
×
189
  });
190

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

224
  app.use(
207✔
225
    '/extended/beta',
226
    (() => {
227
      const router = express.Router();
207✔
228
      router.use(cors());
207✔
229
      router.use((req, res, next) => {
207✔
230
        // Set caching on all routes to be disabled by default, individual routes can override
231
        res.set('Cache-Control', 'no-store');
×
232
        next();
×
233
      });
234
      router.use('/stacking', createStackingRouter(datastore));
207✔
235
      return router;
207✔
236
    })()
237
  );
238

239
  // Setup direct proxy to core-node RPC endpoints (/v2)
240
  // pricing endpoint
241
  app.use(
207✔
242
    '/v2',
243
    (() => {
244
      const router = express.Router();
207✔
245
      router.use(cors());
207✔
246
      router.use('/prices', createBnsPriceRouter(datastore, chainId));
207✔
247
      router.use('/', createCoreNodeRpcProxyRouter(datastore));
207✔
248

249
      return router;
207✔
250
    })()
251
  );
252

253
  // Rosetta API -- https://www.rosetta-api.org
254
  app.use(
207✔
255
    '/rosetta/v1',
256
    (() => {
257
      const router = express.Router();
207✔
258
      router.use(cors());
207✔
259
      router.use('/network', createRosettaNetworkRouter(datastore, chainId));
207✔
260
      router.use('/mempool', createRosettaMempoolRouter(datastore, chainId));
207✔
261
      router.use('/block', createRosettaBlockRouter(datastore, chainId));
207✔
262
      router.use('/account', createRosettaAccountRouter(datastore, chainId));
207✔
263
      router.use('/construction', createRosettaConstructionRouter(datastore, chainId));
207✔
264
      return router;
207✔
265
    })()
266
  );
267

268
  // Setup legacy API v1 and v2 routes
269
  app.use(
207✔
270
    '/v1',
271
    (() => {
272
      const router = express.Router();
207✔
273
      router.use(cors());
207✔
274
      router.use('/namespaces', createBnsNamespacesRouter(datastore));
207✔
275
      router.use('/names', createBnsNamesRouter(datastore, chainId));
207✔
276
      router.use('/addresses', createBnsAddressesRouter(datastore, chainId));
207✔
277
      return router;
207✔
278
    })()
279
  );
280

281
  //handle invalid request gracefully
282
  app.use((req, res) => {
207✔
283
    res.status(404).json({ message: `${req.method} ${req.path} not found` });
×
284
  });
285

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

325
  // Store all the registered express routes for usage with metrics reporting
326
  routes = expressListEndpoints(app).map(endpoint => ({
19,467✔
327
    path: endpoint.path,
328
    regexp: pathToRegex.pathToRegexp(endpoint.path),
329
  }));
330

331
  // Manual route definitions for the /v2/ proxied endpoints
332
  routes.push({
207✔
333
    path: '/v2/pox',
334
    regexp: /^\/v2\/pox(.*)/,
335
  });
336
  routes.push({
207✔
337
    path: '/v2/info',
338
    regexp: /^\/v2\/info(.*)/,
339
  });
340
  routes.push({
207✔
341
    path: '/v2/accounts/*',
342
    regexp: /^\/v2\/accounts(.*)/,
343
  });
344
  routes.push({
207✔
345
    path: '/v2/contracts/call-read/*',
346
    regexp: /^\/v2\/contracts\/call-read(.*)/,
347
  });
348
  routes.push({
207✔
349
    path: '/v2/map_entry/*',
350
    regexp: /^\/v2\/map_entry(.*)/,
351
  });
352
  routes.push({
207✔
353
    path: '/v2/*',
354
    regexp: /^\/v2(.*)/,
355
  });
356

357
  const server = createServer(app);
207✔
358

359
  const serverSockets = new Set<Socket>();
207✔
360
  server.on('connection', socket => {
207✔
361
    serverSockets.add(socket);
913✔
362
    socket.once('close', () => {
913✔
363
      serverSockets.delete(socket);
912✔
364
    });
365
  });
366

367
  const ws = new WebSocketTransmitter(datastore, server);
207✔
368
  ws.connect();
207✔
369

370
  await new Promise<void>((resolve, reject) => {
207✔
371
    try {
207✔
372
      server.once('error', error => {
207✔
373
        reject(error);
×
374
      });
375
      server.listen(apiPort, apiHost, () => {
207✔
376
        resolve();
207✔
377
      });
378
    } catch (error) {
379
      reject(error);
×
380
    }
381
  });
382

383
  const terminate = async () => {
207✔
384
    await new Promise<void>((resolve, reject) => {
207✔
385
      logger.info('Closing WebSocket channels...');
207✔
386
      ws.close(error => {
207✔
387
        if (error) {
207!
NEW
388
          logger.error(error, 'Failed to gracefully close WebSocket channels');
×
389
          reject(error);
×
390
        } else {
391
          logger.info('API WebSocket channels closed.');
207✔
392
          resolve();
207✔
393
        }
394
      });
395
    });
396
    for (const socket of serverSockets) {
207✔
397
      socket.destroy();
164✔
398
    }
399
    await new Promise<void>(resolve => {
207✔
400
      logger.info('Closing API http server...');
207✔
401
      server.close(() => {
207✔
402
        logger.info('API http server closed.');
207✔
403
        resolve();
207✔
404
      });
405
    });
406
  };
407

408
  const forceKill = async () => {
207✔
409
    logger.info('Force closing API server...');
×
410
    const [wsClosePromise, serverClosePromise] = [waiter(), waiter()];
×
411
    ws.close(() => wsClosePromise.finish());
×
412
    server.close(() => serverClosePromise.finish());
×
413
    for (const socket of serverSockets) {
×
414
      socket.destroy();
×
415
    }
416
    await Promise.allSettled([wsClosePromise, serverClosePromise]);
×
417
  };
418

419
  const addr = server.address();
207✔
420
  if (addr === null) {
207!
421
    throw new Error('server missing address');
×
422
  }
423
  const addrStr = typeof addr === 'string' ? addr : `${addr.address}:${addr.port}`;
207!
424
  return {
207✔
425
    expressApp: app,
426
    server: server,
427
    ws: ws,
428
    address: addrStr,
429
    datastore: datastore,
430
    terminate: terminate,
431
    forceKill: forceKill,
432
  };
433
}
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