Skip to content

Commit

Permalink
feat(api-server): DeployContractEndpoint
Browse files Browse the repository at this point in the history
Dynamically registers an endpoint that corresponds to the
quorum ledger connector deployContract method.
There is an integration test to verify this end to end by
launching a ledger, starting an API server configured to
connect to that ledger and then issuing a REST API request
deploying a contract.

Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
  • Loading branch information
petermetz committed May 19, 2020
1 parent f685a62 commit 51eccff
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 133 deletions.
186 changes: 96 additions & 90 deletions packages/bif-cmd-api-server/src/main/typescript/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { OpenApiValidator } from 'express-openapi-validator';
import compression from 'compression';
import bodyParser from 'body-parser';
import cors, { CorsOptions } from 'cors';
import { IPluginKVStorage, PluginFactory, ICactusPlugin, PluginAspect } from '@hyperledger-labs/bif-core-api';
import { IPluginKVStorage, PluginFactory, ICactusPlugin, PluginAspect, isIPluginWebService, IPluginWebService } from '@hyperledger-labs/bif-core-api';
import { CreateConsortiumEndpointV1 } from './consortium/routes/create-consortium-endpoint-v1';
import { IBifApiServerOptions, ConfigService } from './config/config-service';
import { BIF_OPEN_API_JSON } from './openapi-spec';
Expand Down Expand Up @@ -68,116 +68,122 @@ export class ApiServer {
}
}

async startCockpitFileServer(): Promise < void> {
const cockpitWwwRoot = this.options.config.get('cockpitWwwRoot');
this.log.info(`wwwRoot: ${cockpitWwwRoot}`);
async startCockpitFileServer(): Promise<void> {
const cockpitWwwRoot = this.options.config.get('cockpitWwwRoot');
this.log.info(`wwwRoot: ${cockpitWwwRoot}`);

const resolvedWwwRoot = path.resolve(process.cwd(), cockpitWwwRoot);
this.log.info(`resolvedWwwRoot: ${resolvedWwwRoot}`);
const resolvedWwwRoot = path.resolve(process.cwd(), cockpitWwwRoot);
this.log.info(`resolvedWwwRoot: ${resolvedWwwRoot}`);

const resolvedIndexHtml = path.resolve(resolvedWwwRoot + '/index.html');
this.log.info(`resolvedIndexHtml: ${resolvedIndexHtml}`);
const resolvedIndexHtml = path.resolve(resolvedWwwRoot + '/index.html');
this.log.info(`resolvedIndexHtml: ${resolvedIndexHtml}`);

const app: Express = express();
app.use(compression());
app.use(express.static(resolvedWwwRoot));
app.get('/*', (_, res) => res.sendFile(resolvedIndexHtml));
const app: Express = express();
app.use(compression());
app.use(express.static(resolvedWwwRoot));
app.get('/*', (_, res) => res.sendFile(resolvedIndexHtml));

const cockpitPort: number = this.options.config.get('cockpitPort');
const cockpitHost: string = this.options.config.get('cockpitHost');
const cockpitPort: number = this.options.config.get('cockpitPort');
const cockpitHost: string = this.options.config.get('cockpitHost');

await new Promise<any>((resolve, reject) => {
this.httpServerCockpit = app.listen(cockpitPort, cockpitHost, () => {
this.log.info(`Cactus Cockpit UI reachable on port http://${cockpitHost}:${cockpitPort}`);
resolve({ cockpitPort });
await new Promise<any>((resolve, reject) => {
this.httpServerCockpit = app.listen(cockpitPort, cockpitHost, () => {
this.log.info(`Cactus Cockpit UI reachable on port http://${cockpitHost}:${cockpitPort}`);
resolve({ cockpitPort });
});
this.httpServerCockpit.on('error', (err: any) => reject(err));
});
this.httpServerCockpit.on('error', (err: any) => reject(err));
});
}
}

async startApiServer(): Promise < void> {
const app: Application = express();
app.use(compression());
async startApiServer(): Promise<void> {
const app: Application = express();
app.use(compression());

const corsMiddleware = this.createCorsMiddleware()
const corsMiddleware = this.createCorsMiddleware()
app.use(corsMiddleware);

app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.json({ limit: '50mb' }));

const openApiValidator = this.createOpenApiValidator();
await openApiValidator.install(app);
const openApiValidator = this.createOpenApiValidator();
await openApiValidator.install(app);

app.get('/healthcheck', (req: Request, res: Response, next: NextFunction) => {
res.json({ 'success': true, timestamp: new Date() });
});
app.get('/healthcheck', (req: Request, res: Response, next: NextFunction) => {
res.json({ 'success': true, timestamp: new Date() });
});

const storage: IPluginKVStorage = await this.createStoragePlugin();
const configService = new ConfigService();
const config = configService.getOrCreate();
const storage: IPluginKVStorage = await this.createStoragePlugin();
const configService = new ConfigService();
const config = configService.getOrCreate();
{
const endpoint = new CreateConsortiumEndpointV1({ storage, config });
app.post(endpoint.getPath(), endpoint.handleRequest.bind(endpoint));
}

// FIXME
// app.get('/api/v1/consortium/:consortiumId', (req: Request, res: Response, next: NextFunction) => {
// res.json({ swagger: 'TODO' });
// });

const apiPort: number = this.options.config.get('apiPort');
const apiHost: string = this.options.config.get('apiHost');
this.log.info(`Binding Cactus API to port ${apiPort}...`);
await new Promise<any>((resolve, reject) => {
const httpServerApi = app.listen(apiPort, apiHost, () => {
const address: any = httpServerApi.address();
this.log.info(`Successfully bound API to port ${apiPort}`, { address });
if (address && address.port) {
resolve({ port: address.port });
} else {
resolve({ port: apiPort });
const endpoint = new CreateConsortiumEndpointV1({ storage, config });
app.post(endpoint.getPath(), endpoint.handleRequest.bind(endpoint));
}
});
this.httpServerApi = httpServerApi;
this.httpServerApi.on('error', (err) => reject(err));
});
}

createOpenApiValidator(): OpenApiValidator {
return new OpenApiValidator({
apiSpec: BIF_OPEN_API_JSON,
validateRequests: true,
validateResponses: false
});
}
this.options.plugins
.filter((pluginInstance) => isIPluginWebService(pluginInstance))
.forEach((pluginInstance: ICactusPlugin) => {
(pluginInstance as IPluginWebService).installWebService(app);
});

// FIXME
// app.get('/api/v1/consortium/:consortiumId', (req: Request, res: Response, next: NextFunction) => {
// res.json({ swagger: 'TODO' });
// });

const apiPort: number = this.options.config.get('apiPort');
const apiHost: string = this.options.config.get('apiHost');
this.log.info(`Binding Cactus API to port ${apiPort}...`);
await new Promise<any>((resolve, reject) => {
const httpServerApi = app.listen(apiPort, apiHost, () => {
const address: any = httpServerApi.address();
this.log.info(`Successfully bound API to port ${apiPort}`, { address });
if (address && address.port) {
resolve({ port: address.port });
} else {
resolve({ port: apiPort });
}
});
this.httpServerApi = httpServerApi;
this.httpServerApi.on('error', (err) => reject(err));
});
}

async createStoragePlugin(): Promise < IPluginKVStorage > {
const kvStoragePlugin = this.options.plugins.find((p) => p.getAspect() === PluginAspect.KV_STORAGE);
if(kvStoragePlugin) {
return kvStoragePlugin as IPluginKVStorage;
createOpenApiValidator(): OpenApiValidator {
return new OpenApiValidator({
apiSpec: BIF_OPEN_API_JSON,
validateRequests: true,
validateResponses: false
});
}

async createStoragePlugin(): Promise<IPluginKVStorage> {
const kvStoragePlugin = this.options.plugins.find((p) => p.getAspect() === PluginAspect.KV_STORAGE);
if (kvStoragePlugin) {
return kvStoragePlugin as IPluginKVStorage;
}
const storagePluginPackage = this.options.config.get('storagePluginPackage');
const { PluginFactoryKVStorage } = await import(storagePluginPackage);
const storagePluginOptionsJson = this.options.config.get('storagePluginOptionsJson');
const storagePluginOptions = JSON.parse(storagePluginOptionsJson);
const pluginFactory: PluginFactory<IPluginKVStorage, unknown> = new PluginFactoryKVStorage();
const plugin = await pluginFactory.create(storagePluginOptions);
return plugin;
}
const { PluginFactoryKVStorage } = await import(storagePluginPackage);
const storagePluginOptionsJson = this.options.config.get('storagePluginOptionsJson');
const storagePluginOptions = JSON.parse(storagePluginOptionsJson);
const pluginFactory: PluginFactory<IPluginKVStorage, unknown> = new PluginFactoryKVStorage();
const plugin = await pluginFactory.create(storagePluginOptions);
return plugin;
}

createCorsMiddleware(): RequestHandler {
const apiCorsDomainCsv = this.options.config.get('apiCorsDomainCsv');
const allowedDomains = apiCorsDomainCsv.split(',');
const allDomainsAllowed = allowedDomains.includes('*');

const corsOptions: CorsOptions = {
origin: (origin: string | undefined, callback) => {
if (allDomainsAllowed || origin && allowedDomains.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error(`CORS not allowed for Origin "${origin}".`));
createCorsMiddleware(): RequestHandler {
const apiCorsDomainCsv = this.options.config.get('apiCorsDomainCsv');
const allowedDomains = apiCorsDomainCsv.split(',');
const allDomainsAllowed = allowedDomains.includes('*');

const corsOptions: CorsOptions = {
origin: (origin: string | undefined, callback) => {
if (allDomainsAllowed || origin && allowedDomains.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error(`CORS not allowed for Origin "${origin}".`));
}
}
}
return cors(corsOptions);
}
return cors(corsOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export interface IPluginWebService extends ICactusPlugin {
installWebService(expressApp: any): IWebServiceEndpoint[];
}

export function isIPluginWebService(pluginInstance: IPluginWebService): pluginInstance is IPluginWebService {
export function isIPluginWebService(pluginInstance: any): pluginInstance is IPluginWebService {
return typeof pluginInstance.installWebService === 'function';
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export class PluginLedgerConnectorQuorum implements IPluginLedgerConnector<any,
if (!options) {
throw new Error(`PluginLedgerConnectorQuorum#ctor options falsy.`);
}
if (!options.rpcApiHttpHost) {
throw new Error(`PluginLedgerConnectorQuorum#ctor options.rpcApiHttpHost falsy.`);
}
const web3Provider = new Web3.providers.HttpProvider(this.options.rpcApiHttpHost);
this.web3 = new Web3(web3Provider);
this.log = LoggerProvider.getOrCreate({ label: 'plugin-ledger-connector-quorum', level: 'trace' })
Expand All @@ -43,9 +46,12 @@ export class PluginLedgerConnectorQuorum implements IPluginLedgerConnector<any,
public installWebService(expressApp: any): IWebServiceEndpoint[] {
const endpoints: IWebServiceEndpoint[] = [];
{
const endpoint: IWebServiceEndpoint = new DeployContractEndpoint({ path: '/deploy-contract', plugin: this });
const pluginId = this.getId(); // @hyperledger/cactus-plugin-ledger-connector-quorum
const path = `/api/v1/plugins/${pluginId}/contract/deploy`;
const endpoint: IWebServiceEndpoint = new DeployContractEndpoint({ path, plugin: this });
expressApp.use(endpoint.getPath(), endpoint.getExpressRequestHandler());
endpoints.push(endpoint);
this.log.info(`Registered contract deployment endpoint at ${path}`);
}
return endpoints;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export class DeployContractEndpoint implements IWebServiceEndpoint {
return this.handleRequest.bind(this);
}

public handleRequest(req: any, res: any, next: any): void {
public async handleRequest(req: any, res: any, next: any): Promise<void> {
const options: IQuorumDeployContractOptions = req.body;
this.options.plugin.deployContract(options);
res.json({ success: true });
const data = await this.options.plugin.deployContract(options);
res.json({ success: true, data });
}

// FIXME: this should actually validate the request?
Expand Down
26 changes: 26 additions & 0 deletions packages/bif-test-plugin-ledger-connector-quorum/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@hyperledger-labs/bif-plugin-kv-storage-memory": "0.2.0",
"@hyperledger-labs/bif-plugin-ledger-connector-quorum": "^0.2.0",
"@hyperledger-labs/bif-sdk": "0.2.0",
"axios": "0.19.2",
"joi": "14.3.1",
"web3": "1.2.7",
"web3-eth-contract": "1.2.7",
Expand Down
Loading

0 comments on commit 51eccff

Please sign in to comment.