Skip to content

Commit

Permalink
feat(cardano-graphql-services): add TxSubmitHttpServer
Browse files Browse the repository at this point in the history
- uses the `TxSubmitProvider` interface to facilitate both queue and direct submission
- /submit accepts cbor-encoded binary data to remain aligned with other Cardano APIs
- serialize tx submit errors in HTTP response
- Sets the status code dynamically based on presence of tx submission errors.
- Note: This currently looses the custom type precision, however it appears there's
a feature in development to address this:
sindresorhus/serialize-error#48
- CLI entrypoint using Commander
- run entrypoint for ENV configuration
- configurable request limit in TxSubmitHttpServer
- ensure txSubmitProvider is healthy on TxSubmitHttpServer init, otherwise throw
`ProviderFailure.Unhealthy`.
- throw provider error if unhealthy during TxSubmitHttpServer submit, and return 503
 HTTP status code
  • Loading branch information
rhyslbw committed Mar 25, 2022
1 parent 98fb4a7 commit cb03f69
Show file tree
Hide file tree
Showing 14 changed files with 792 additions and 4 deletions.
20 changes: 18 additions & 2 deletions packages/cardano-graphql-services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"node": "^14"
},
"main": "dist/index.js",
"bin": {
"tx-submit": "./dist/TxSubmit/cli.js"
},
"repository": "https://github.com/input-output-hk/cardano-js-sdk/packages/cardano-graphql-services",
"contributors": [
"Martynas Kazlauskas <martynas.kazlauskas@iohk.io>",
Expand All @@ -25,7 +28,9 @@
"test:e2e": "shx echo 'test:e2e' command not implemented yet",
"coverage": "yarn test --coverage",
"prepack": "yarn build",
"test:debug": "DEBUG=true yarn test"
"test:debug": "DEBUG=true yarn test",
"run:tx-submit": "ts-node --transpile-only src/TxSubmit/run.ts",
"cli:tx-submit": "ts-node --transpile-only src/TxSubmit/cli.ts"
},
"devDependencies": {
"@cardano-sdk/util-dev": "0.2.0",
Expand All @@ -35,14 +40,25 @@
"get-port-please": "^2.4.3",
"got": "^11",
"npm-run-all": "^4.1.5",
"shx": "^0.3.3"
"shx": "^0.3.3",
"wait-on": "^6.0.1"
},
"dependencies": {
"@cardano-sdk/core": "0.2.0",
"@cardano-sdk/ogmios": "0.2.0",
"@types/bunyan": "^1.8.8",
"@types/death": "^1.1.2",
"@types/wait-on": "^5.3.1",
"body-parser": "^1.19.2",
"bunyan": "^1.8.15",
"commander": "^9.1.0",
"death": "^1.1.0",
"debug": "^4.3.4",
"envalid": "^7.3.0",
"express": "^4.17.3",
"graphql-request": "npm:graphql-request-configurable-serializer@4.0.0",
"reflect-metadata": "~0.1.13",
"serialize-error": "^8",
"ts-log": "^2.2.4",
"type-graphql": "~1.1.1"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Cardano, ProviderError, ProviderFailure, TxSubmitProvider } from '@cardano-sdk/core';
import { ErrorObject, serializeError } from 'serialize-error';
import { HttpServer } from '../Http';
import { Logger, dummyLogger } from 'ts-log';
import bodyParser, { Options } from 'body-parser';
import express, { Router } from 'express';
import net from 'net';

export interface TxSubmitServerDependencies {
txSubmitProvider: TxSubmitProvider;
logger?: Logger;
}

export interface TxSubmitHttpServerConfig {
listen: net.ListenOptions;
bodyParser?: {
limit?: Options['limit'];
};
}

export class TxSubmitHttpServer extends HttpServer {
#txSubmitProvider: TxSubmitProvider;

private constructor(config: TxSubmitHttpServerConfig, router: Router, dependencies: TxSubmitServerDependencies) {
super({ listen: config.listen, name: 'TxSubmitServer', router }, dependencies.logger);
this.#txSubmitProvider = dependencies.txSubmitProvider;
}
static create(
config: TxSubmitHttpServerConfig,
{ txSubmitProvider, logger = dummyLogger }: TxSubmitServerDependencies
) {
const router = express.Router();
router.use(bodyParser.raw({ limit: config.bodyParser?.limit || '500kB', type: 'application/cbor' }));
router.get('/health', async (req, res) => {
logger.debug('/health', { ip: req.ip });
let body: { ok: boolean } | Error['message'];
try {
body = await txSubmitProvider.healthCheck();
} catch (error) {
logger.error(error);
body = error instanceof ProviderError ? error.message : 'Unknown error';
res.statusCode = 500;
}
res.send(body);
});
router.post('/submit', async (req, res) => {
if (req.header('Content-Type') !== 'application/cbor') {
res.statusCode = 400;
return res.send('Must use application/cbor Content-Type header');
}
logger.debug('/submit', { ip: req.ip });
let body: Error['message'] | undefined;
try {
await txSubmitProvider.submitTx(new Uint8Array(req.body));
body = undefined;
} catch (error) {
if (!(await txSubmitProvider.healthCheck()).ok) {
res.statusCode = 503;
body = JSON.stringify(serializeError(new ProviderError(ProviderFailure.Unhealthy, error)));
} else {
res.statusCode = Cardano.util.asTxSubmissionError(error) ? 400 : 500;
body = JSON.stringify(
Array.isArray(error) ? error.map<ErrorObject>((e) => serializeError(e)) : serializeError(error)
);
}
logger.error(body);
}
res.send(body);
});
return new TxSubmitHttpServer(config, router, { logger, txSubmitProvider });
}

async initializeImpl(): Promise<void> {
if (!(await this.#txSubmitProvider.healthCheck()).ok) {
throw new ProviderError(ProviderFailure.Unhealthy);
}
await super.initializeImpl();
}
}
71 changes: 71 additions & 0 deletions packages/cardano-graphql-services/src/TxSubmit/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node
/* eslint-disable import/imports-first */
require('../../scripts/patchRequire');
import { Command } from 'commander';
import { InvalidLoggerLevel } from '../errors';
import { LogLevel, createLogger } from 'bunyan';
import { TxSubmitHttpServer } from './TxSubmitHttpServer';
import { URL } from 'url';
import { loggerMethodNames } from '../util';
import { ogmiosTxSubmitProvider } from '@cardano-sdk/ogmios';
import onDeath from 'death';
const clear = require('clear');
const packageJson = require('../../package.json');
clear();
// eslint-disable-next-line no-console
console.log('Tx Submit CLI');

const program = new Command('tx-submit');

program.description('Submit transactions to the Cardano network').version(packageJson.version);

program
.command('start-server')
.description('Start the HTTP server')
.option('--api-url <apiUrl>', 'Server URL', (url) => new URL(url))
.option('--ogmios-url <ogmiosUrl>', 'Ogmios URL', (url) => new URL(url))
.option('--logger-min-severity <level>', 'Log level', (level) => {
if (!loggerMethodNames.includes(level)) {
throw new InvalidLoggerLevel(level);
}
return level;
})
.action(
async ({ apiUrl, loggerMinSeverity, ogmiosUrl }: { apiUrl: URL; loggerMinSeverity: string; ogmiosUrl: URL }) => {
const logger = createLogger({ level: loggerMinSeverity as LogLevel, name: 'tx-submit-http-server' });
const txSubmitProvider = ogmiosTxSubmitProvider({
host: ogmiosUrl?.hostname,
port: ogmiosUrl ? Number.parseInt(ogmiosUrl.port) : undefined,
tls: ogmiosUrl?.protocol === 'wss'
});
const server = TxSubmitHttpServer.create(
{
listen: {
host: apiUrl.hostname,
port: Number.parseInt(apiUrl.port)
}
},
{
logger,
txSubmitProvider
}
);
await server.initialize();
await server.start();
onDeath(async () => {
await server.shutdown();
process.exit(1);
});
}
);

if (process.argv.slice(2).length === 0) {
program.outputHelp();
process.exit(1);
} else {
program.parseAsync(process.argv).catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
process.exit(0);
});
}
1 change: 1 addition & 0 deletions packages/cardano-graphql-services/src/TxSubmit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TxSubmitHttpServer';
43 changes: 43 additions & 0 deletions packages/cardano-graphql-services/src/TxSubmit/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-disable import/imports-first */
require('../../scripts/patchRequire');
import * as envalid from 'envalid';
import { LogLevel, createLogger } from 'bunyan';
import { Logger } from 'ts-log';
import { TxSubmitHttpServer } from '../TxSubmit';
import { URL } from 'url';
import { loggerMethodNames } from '../util';
import { ogmiosTxSubmitProvider } from '@cardano-sdk/ogmios';
import onDeath from 'death';

// Todo: Hoist some to ogmios package, import and merge here and in wallet e2e tests
const envSpecs = {
API_URL: envalid.url({ default: 'http://localhost:3000' }),
LOGGER_MIN_SEVERITY: envalid.str({ choices: loggerMethodNames as string[], default: 'info' }),
OGMIOS_URL: envalid.url({ default: 'ws://localhost:1337' })
};

void (async () => {
const env = envalid.cleanEnv(process.env, envSpecs);
const apiUrl = new URL(env.API_URL);
const ogmiosUrl = new URL(env.OGMIOS_URL);
const logger: Logger = createLogger({
level: env.LOGGER_MIN_SEVERITY as LogLevel,
name: 'tx-submit-http-server'
});
const txSubmitProvider = await ogmiosTxSubmitProvider({
host: ogmiosUrl?.hostname,
port: ogmiosUrl ? Number.parseInt(ogmiosUrl.port) : undefined,
tls: ogmiosUrl?.protocol === 'wss'
});
const server = TxSubmitHttpServer.create(
{ listen: { host: apiUrl.hostname, port: Number.parseInt(apiUrl.port) } },
{ logger, txSubmitProvider }
);
await server.initialize();
await server.start();
onDeath(async () => {
await server.shutdown();
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CustomError } from 'ts-custom-error';

export class InvalidLoggerLevel extends CustomError {
public constructor(value: string) {
super();
this.message = `${value} is an invalid logger level`;
}
}
1 change: 1 addition & 0 deletions packages/cardano-graphql-services/src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './InvalidLoggerLevel';
export * from './InvalidModuleState';
1 change: 1 addition & 0 deletions packages/cardano-graphql-services/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Http';
export * from './RunnableModule';
export * from './TxSubmit';
3 changes: 2 additions & 1 deletion packages/cardano-graphql-services/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"rootDir": "."
},
"references": [
{ "path": "../../core/src" }
{ "path": "../../core/src" },
{ "path": "../../ogmios/src" }
]
}
Loading

0 comments on commit cb03f69

Please sign in to comment.