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

hirosystems / stacks-blockchain-api / 4863643345

pending completion
4863643345

Pull #1630

github

GitHub
Merge b9b14018f into a6c8f16b6
Pull Request #1630: Logging migration to Pino library

2140 of 3228 branches covered (66.29%)

131 of 231 new or added lines in 41 files covered. (56.71%)

38 existing lines in 2 files now uncovered.

7490 of 9606 relevant lines covered (77.97%)

1789.6 hits per line

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

77.22
/src/api/init.ts
1
import { Server, createServer } from 'http';
51✔
2
import { Socket } from 'net';
3
import * as express from 'express';
51✔
4
import { v4 as uuid } from 'uuid';
51✔
5
import * as cors from 'cors';
51✔
6

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

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

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

41
import * as path from 'path';
51✔
42
import * as fs from 'fs';
51✔
43
import { PgStore } from '../datastore/pg-store';
44
import { PgWriteStore } from '../datastore/pg-write-store';
45
import { WebSocketTransmitter } from './routes/ws/web-socket-transmitter';
51✔
46
import { createPox2EventsRouter } from './routes/pox2';
51✔
47
import { isPgConnectionError } from '../datastore/helpers';
51✔
48
import { createStackingRouter } from './routes/stacking';
51✔
49
import { logger, loggerMiddleware } from '../logger';
51✔
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 } = {};
51✔
63

64
export async function startApiServer(opts: {
51✔
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;
222✔
74

75
  try {
222✔
76
    const [branch, commit, tag] = fs.readFileSync('.git-info', 'utf-8').split('\n');
222✔
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`);
222✔
82
  }
83

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

88
  if (!apiHost) {
222!
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) {
222!
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
  }[] = [];
222✔
106

107
  if (isProdEnv) {
222!
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) => {
222✔
144
    res.setHeader(
1,834✔
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,834✔
149
    next();
1,834✔
150
  });
151

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

155
  app.set('json spaces', 2);
222✔
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);
222✔
161

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

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

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

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

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

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

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

281
  //handle invalid request gracefully
282
  app.use((req, res) => {
222✔
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) => {
222✔
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 => ({
20,910✔
327
    path: endpoint.path,
328
    regexp: pathToRegex.pathToRegexp(endpoint.path),
329
  }));
330

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

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

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

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

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

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

408
  const forceKill = async () => {
222✔
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();
222✔
420
  if (addr === null) {
222!
421
    throw new Error('server missing address');
×
422
  }
423
  const addrStr = typeof addr === 'string' ? addr : `${addr.address}:${addr.port}`;
222!
424
  return {
222✔
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

© 2025 Coveralls, Inc