-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cardano-graphql-services): add TxSubmitHttpServer
- 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
Showing
14 changed files
with
792 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
packages/cardano-graphql-services/src/TxSubmit/TxSubmitHttpServer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './TxSubmitHttpServer'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
})(); |
8 changes: 8 additions & 0 deletions
8
packages/cardano-graphql-services/src/errors/InvalidLoggerLevel.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './InvalidLoggerLevel'; | ||
export * from './InvalidModuleState'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './Http'; | ||
export * from './RunnableModule'; | ||
export * from './TxSubmit'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.