Skip to content

Commit

Permalink
feat: /block endpoint
Browse files Browse the repository at this point in the history
This commit includes the before mentioned api endpoint but also neededed
to introduce DB connection and environment configuration.

TODO:

- Returning transactions data is still not implemented.
- Setup and launch a test DB with mocked data

re: cardano-foundation#13
  • Loading branch information
AlanVerbner committed Jul 11, 2020
1 parent f99859a commit e1de850
Show file tree
Hide file tree
Showing 39 changed files with 509 additions and 127 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ Caradano [Rosetta API specification v1.4.0](https://github.com/coinbase/rosetta-

## Running

### Configuration

There must be a `.env` file in the root directory with the following configs:

```
# App port
PORT=8080
# App address to bind to
BIND_ADDRESS=127.0.0.1
# PostgresDB connection string
DB_CONNECTION_STRING="postgresql://postgres:password@127.0.0.1:5432/db"
```

// TODO

## Develop
Expand All @@ -17,6 +30,7 @@ In order to setup the repository you will need to have:
- `node@v14.5.0`
- `yarn@1.22.4`


### Setup

```
Expand Down
7 changes: 5 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
module.exports = {
roots: ['<rootDir>/test'],
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.ts?$': 'ts-jest',
'^.+\\.tsx?$': 'ts-jest'
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
testPathIgnorePatterns: ['/lib/', '/node_modules/'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFiles: ['./test/e2e/setup-environment.ts']
};
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage",
"generate-rosetta-types": "dtsgen --out ./types/types.d.ts ./rosetta-specifications/api.json",
"dev": "ts-node-dev --respawn --transpileOnly --watch ./src ./src/server/index.ts",
"generate-rosetta-types": "dtsgen --out ./types/rosetta-types.d.ts ./rosetta-specifications/api.json",
"dev": "ts-node-dev --respawn --transpileOnly -r dotenv/config --watch ./src ./src/server/index.ts",
"lint": "eslint \"./test/**/*.ts\" \"./src/**/*.ts\""
},
"lint-staged": {
Expand Down Expand Up @@ -40,6 +40,7 @@
"@atixlabs/eslint-config": "1.2.3",
"@types/jest": "26.0.3",
"@types/node": "14.0.14",
"@types/pg": "7.14.4",
"@typescript-eslint/eslint-plugin": "3.5.0",
"@typescript-eslint/parser": "3.5.0",
"dtsgenerator": "3.1.1",
Expand All @@ -60,9 +61,12 @@
"typescript": "3.9.5"
},
"dependencies": {
"dotenv": "8.2.0",
"fastify": "2.15.1",
"fastify-blipp": "2.3.1",
"fastify-openapi-glue": "1.6.0"
"fastify-openapi-glue": "1.6.0",
"http-status-codes": "1.4.0",
"pg": "8.3.0"
},
"resolutions": {
"typescript": "3.9.5"
Expand Down
Binary file added packages-cache/@types-pg-7.14.4.tgz
Binary file not shown.
Binary file added packages-cache/@types-pg-types-1.11.5.tgz
Binary file not shown.
Binary file added packages-cache/buffer-writer-2.0.0.tgz
Binary file not shown.
Binary file added packages-cache/dotenv-8.2.0.tgz
Binary file not shown.
Binary file added packages-cache/http-status-codes-1.4.0.tgz
Binary file not shown.
Binary file added packages-cache/packet-reader-1.0.0.tgz
Binary file not shown.
Binary file added packages-cache/pg-8.3.0.tgz
Binary file not shown.
Binary file added packages-cache/pg-connection-string-2.3.0.tgz
Binary file not shown.
Binary file added packages-cache/pg-int8-1.0.1.tgz
Binary file not shown.
Binary file added packages-cache/pg-pool-3.2.1.tgz
Binary file not shown.
Binary file added packages-cache/pg-protocol-1.2.5.tgz
Binary file not shown.
Binary file added packages-cache/pg-types-2.2.0.tgz
Binary file not shown.
Binary file added packages-cache/pgpass-1.0.2.tgz
Binary file not shown.
Binary file added packages-cache/postgres-array-2.0.0.tgz
Binary file not shown.
Binary file added packages-cache/postgres-bytea-1.0.0.tgz
Binary file not shown.
Binary file added packages-cache/postgres-date-1.0.5.tgz
Binary file not shown.
Binary file added packages-cache/postgres-interval-1.2.0.tgz
Binary file not shown.
Binary file added packages-cache/semver-4.3.2.tgz
Binary file not shown.
Binary file added packages-cache/split-1.0.1.tgz
Binary file not shown.
23 changes: 23 additions & 0 deletions src/server/api-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Custom error class to implement Rosetta Error Schema
*/
class ApiError extends Error implements Components.Schemas.Error {
code: number;
message: string;
retriable: boolean;
details?: string;

constructor(code: number, message: string, retriable: boolean, details?: string) {
super(message);

// Set the prototype explicitly.
Object.setPrototypeOf(this, ApiError.prototype);

this.code = code;
this.message = message;
this.retriable = retriable;
this.details = details;
}
}

export default ApiError;
10 changes: 5 additions & 5 deletions src/server/controllers/generic-controller.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { DefaultBody } from 'fastify';

// eslint-disable-next-line @typescript-eslint/ban-types
type Services = { [index: string]: Function };

/**
* As `fastify-openapi-glue provides an instance of FastifyRequest when invoking the handler
* but the service layer only needs the request body this wrapper function acts as a controller
I * adapting both interfaces
* adapting both interfaces
*/
const wrap = function wrap(services: Services): Services {
// eslint-disable-next-line @typescript-eslint/ban-types
const wrap = function wrap(services: NodeJS.Dict<Function>): NodeJS.Dict<Function> {
return Object.keys(services).reduce(
(accumulator, method) => ({
...accumulator,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: services[method] will never be undefined
[method]: (request: { body: DefaultBody }) => services[method](request.body)
}),
{}
Expand Down
66 changes: 66 additions & 0 deletions src/server/db/blockchain-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { hashFormatter } from '../utils/formatters';
import { Pool } from 'pg';

export interface Block {
hash: string;
number: number;
time: number;
parent: {
hash: string;
number: number;
};
// transactions: Transaction[];
}

export interface BlockchainRepository {
/**
* Finds a block based on the given block number or hash. If non sent, latest (tip) block information
* is retrieved
*
* @param blockNumber
* @param blockHash
*/
findBlock(number?: number, blockHash?: string): Promise<Block | null>;
}

const findBlockQuery = (blockNumber?: number, blockHash?: string) => `
SELECT
b1.block_no,
b1.hash,
b1.time,
b2.block_no as parent_block_no,
b2.hash as parent_hash
FROM
BLOCK b1
LEFT JOIN block b2 on (b1.block_no - 1) = b2.block_no
WHERE
${blockNumber ? 'b1.block_no = $1' : '$1 = $1'} AND
${blockHash ? 'b1.hash = $2' : '$2 = $2'}
LIMIT 1
`;

export const configure = (databaseInstance: Pool): BlockchainRepository => ({
async findBlock(blockNumber?: number, blockHash?: string): Promise<Block | null> {
const query = findBlockQuery(blockNumber, blockHash);
// Add paramter or short-circuit it
const parameters = [
blockNumber ? blockNumber : true,
blockHash ? Buffer.from(blockHash.replace('0x', ''), 'hex') : true
];
const result = await databaseInstance.query(query, parameters);
/* eslint-disable camelcase */
if (result.rows.length === 1) {
const { block_no, hash, time, parent_block_no, parent_hash } = result.rows[0];
return {
number: block_no,
hash: hashFormatter(hash),
time,
parent: {
number: parent_block_no,
hash: hashFormatter(parent_hash)
}
};
}
return null;
}
});
10 changes: 10 additions & 0 deletions src/server/db/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Pool } from 'pg';

/**
* Creates a database pool to be used to run queries. No connection will be established.
*
* @param connectionString `postgresql://dbuser:secretpassword@database.server.com:3211/mydb`
*/
const createPool = (connectionString: string): Pool => new Pool({ connectionString });

export default createPool;
16 changes: 16 additions & 0 deletions src/server/db/repositories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Pool } from 'pg';
import * as BlockchainRepository from './blockchain-repository';

export interface Repositories {
blockchainRepository: BlockchainRepository.BlockchainRepository;
}

/**
* Configures the repositories with the given DB connection to make them ready
* to be used
*
* @param database connection to be used to run queries
*/
export const configure = (database: Pool): Repositories => ({
blockchainRepository: BlockchainRepository.configure(database)
});
41 changes: 21 additions & 20 deletions src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
/* eslint-disable no-console */
import fastify, { FastifyRequest } from 'fastify';
import fastifyBlipp from 'fastify-blipp';
import openapiGlue from 'fastify-openapi-glue';
import services from './services';
import { wrap } from './controllers/generic-controller';
import { Pool } from 'pg';
import buildServer from './server';
import createPool from './db/connection';
import * as Repostories from './db/repositories';
import * as Services from './services/services';
const { PORT, BIND_ADDRESS, DB_CONNECTION_STRING, LOGGER_ENABLED }: NodeJS.ProcessEnv = process.env;

const server = fastify({ logger: true });

server.register(fastifyBlipp);
server.register(openapiGlue, {
specification: `${__dirname}/openApi.json`,
service: wrap(services),
noAdditional: true
});

const PORT = 3000;

const start = async () => {
const start = async (databaseInstance: Pool) => {
let server;
try {
await server.listen(PORT, '0.0.0.0');
const repository = Repostories.configure(databaseInstance);
const services = Services.configure(repository);
server = buildServer(services, LOGGER_ENABLED === 'true');
server.addHook('onClose', (fastify, done) => databaseInstance.end(done));
// eslint-disable-next-line no-magic-numbers
await server.listen(PORT || 3000, BIND_ADDRESS || '0.0.0.0');
server.blipp();
} catch (error) {
console.log(error);
server.log.error(error);
server?.log.error(error);
await databaseInstance?.end();
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
Expand All @@ -35,4 +32,8 @@ process.on('unhandledRejection', error => {
console.error(error);
});

start();
const connectDB = async () => await createPool(DB_CONNECTION_STRING);

connectDB()
.then(databaseInstance => start(databaseInstance))
.catch(console.error);
44 changes: 44 additions & 0 deletions src/server/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fastify from 'fastify';
import fastifyBlipp from 'fastify-blipp';
import openapiGlue from 'fastify-openapi-glue';
import { wrap } from './controllers/generic-controller';
import ApiError from './api-error';
import { Services } from './services/services';
import { Server, IncomingMessage, ServerResponse } from 'http';

/**
* This function builds a Fastify instance connecting the services with the
* corresponding fastify route handlers.
*
* @param services to be used to handle the requests
* @param logger true if logger should be enabled, false otherwise
*/
const buildServer = (
services: Services,
logger = true
): fastify.FastifyInstance<Server, IncomingMessage, ServerResponse> => {
const server = fastify({ logger });

server.register(fastifyBlipp);
server.register(openapiGlue, {
specification: `${__dirname}/openApi.json`,
service: wrap(services),
noAdditional: true
});

// Custom error handling is needed as the specified by Rosetta API doesn't match
// the fastify default one
server.setErrorHandler((error: Error, request, reply) => {
if (error instanceof ApiError) {
// eslint-disable-next-line no-magic-numbers
reply.status(500).send({
...error,
message: error.message
});
} else reply.send(error);
});

return server;
};

export default buildServer;
Loading

0 comments on commit e1de850

Please sign in to comment.