Skip to content

Commit 51eccff

Browse files
committed
feat(api-server): DeployContractEndpoint
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 <[email protected]>
1 parent f685a62 commit 51eccff

File tree

7 files changed

+174
-133
lines changed

7 files changed

+174
-133
lines changed

packages/bif-cmd-api-server/src/main/typescript/api-server.ts

+96-90
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { OpenApiValidator } from 'express-openapi-validator';
66
import compression from 'compression';
77
import bodyParser from 'body-parser';
88
import cors, { CorsOptions } from 'cors';
9-
import { IPluginKVStorage, PluginFactory, ICactusPlugin, PluginAspect } from '@hyperledger-labs/bif-core-api';
9+
import { IPluginKVStorage, PluginFactory, ICactusPlugin, PluginAspect, isIPluginWebService, IPluginWebService } from '@hyperledger-labs/bif-core-api';
1010
import { CreateConsortiumEndpointV1 } from './consortium/routes/create-consortium-endpoint-v1';
1111
import { IBifApiServerOptions, ConfigService } from './config/config-service';
1212
import { BIF_OPEN_API_JSON } from './openapi-spec';
@@ -68,116 +68,122 @@ export class ApiServer {
6868
}
6969
}
7070

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

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

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

81-
const app: Express = express();
82-
app.use(compression());
83-
app.use(express.static(resolvedWwwRoot));
84-
app.get('/*', (_, res) => res.sendFile(resolvedIndexHtml));
81+
const app: Express = express();
82+
app.use(compression());
83+
app.use(express.static(resolvedWwwRoot));
84+
app.get('/*', (_, res) => res.sendFile(resolvedIndexHtml));
8585

86-
const cockpitPort: number = this.options.config.get('cockpitPort');
87-
const cockpitHost: string = this.options.config.get('cockpitHost');
86+
const cockpitPort: number = this.options.config.get('cockpitPort');
87+
const cockpitHost: string = this.options.config.get('cockpitHost');
8888

89-
await new Promise<any>((resolve, reject) => {
90-
this.httpServerCockpit = app.listen(cockpitPort, cockpitHost, () => {
91-
this.log.info(`Cactus Cockpit UI reachable on port http://${cockpitHost}:${cockpitPort}`);
92-
resolve({ cockpitPort });
89+
await new Promise<any>((resolve, reject) => {
90+
this.httpServerCockpit = app.listen(cockpitPort, cockpitHost, () => {
91+
this.log.info(`Cactus Cockpit UI reachable on port http://${cockpitHost}:${cockpitPort}`);
92+
resolve({ cockpitPort });
93+
});
94+
this.httpServerCockpit.on('error', (err: any) => reject(err));
9395
});
94-
this.httpServerCockpit.on('error', (err: any) => reject(err));
95-
});
96-
}
96+
}
9797

98-
async startApiServer(): Promise < void> {
99-
const app: Application = express();
100-
app.use(compression());
98+
async startApiServer(): Promise<void> {
99+
const app: Application = express();
100+
app.use(compression());
101101

102-
const corsMiddleware = this.createCorsMiddleware()
102+
const corsMiddleware = this.createCorsMiddleware()
103103
app.use(corsMiddleware);
104104

105-
app.use(bodyParser.json({ limit: '50mb' }));
105+
app.use(bodyParser.json({ limit: '50mb' }));
106106

107-
const openApiValidator = this.createOpenApiValidator();
108-
await openApiValidator.install(app);
107+
const openApiValidator = this.createOpenApiValidator();
108+
await openApiValidator.install(app);
109109

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

114-
const storage: IPluginKVStorage = await this.createStoragePlugin();
115-
const configService = new ConfigService();
116-
const config = configService.getOrCreate();
114+
const storage: IPluginKVStorage = await this.createStoragePlugin();
115+
const configService = new ConfigService();
116+
const config = configService.getOrCreate();
117117
{
118-
const endpoint = new CreateConsortiumEndpointV1({ storage, config });
119-
app.post(endpoint.getPath(), endpoint.handleRequest.bind(endpoint));
120-
}
121-
122-
// FIXME
123-
// app.get('/api/v1/consortium/:consortiumId', (req: Request, res: Response, next: NextFunction) => {
124-
// res.json({ swagger: 'TODO' });
125-
// });
126-
127-
const apiPort: number = this.options.config.get('apiPort');
128-
const apiHost: string = this.options.config.get('apiHost');
129-
this.log.info(`Binding Cactus API to port ${apiPort}...`);
130-
await new Promise<any>((resolve, reject) => {
131-
const httpServerApi = app.listen(apiPort, apiHost, () => {
132-
const address: any = httpServerApi.address();
133-
this.log.info(`Successfully bound API to port ${apiPort}`, { address });
134-
if (address && address.port) {
135-
resolve({ port: address.port });
136-
} else {
137-
resolve({ port: apiPort });
118+
const endpoint = new CreateConsortiumEndpointV1({ storage, config });
119+
app.post(endpoint.getPath(), endpoint.handleRequest.bind(endpoint));
138120
}
139-
});
140-
this.httpServerApi = httpServerApi;
141-
this.httpServerApi.on('error', (err) => reject(err));
142-
});
143-
}
144121

145-
createOpenApiValidator(): OpenApiValidator {
146-
return new OpenApiValidator({
147-
apiSpec: BIF_OPEN_API_JSON,
148-
validateRequests: true,
149-
validateResponses: false
150-
});
151-
}
122+
this.options.plugins
123+
.filter((pluginInstance) => isIPluginWebService(pluginInstance))
124+
.forEach((pluginInstance: ICactusPlugin) => {
125+
(pluginInstance as IPluginWebService).installWebService(app);
126+
});
127+
128+
// FIXME
129+
// app.get('/api/v1/consortium/:consortiumId', (req: Request, res: Response, next: NextFunction) => {
130+
// res.json({ swagger: 'TODO' });
131+
// });
132+
133+
const apiPort: number = this.options.config.get('apiPort');
134+
const apiHost: string = this.options.config.get('apiHost');
135+
this.log.info(`Binding Cactus API to port ${apiPort}...`);
136+
await new Promise<any>((resolve, reject) => {
137+
const httpServerApi = app.listen(apiPort, apiHost, () => {
138+
const address: any = httpServerApi.address();
139+
this.log.info(`Successfully bound API to port ${apiPort}`, { address });
140+
if (address && address.port) {
141+
resolve({ port: address.port });
142+
} else {
143+
resolve({ port: apiPort });
144+
}
145+
});
146+
this.httpServerApi = httpServerApi;
147+
this.httpServerApi.on('error', (err) => reject(err));
148+
});
149+
}
152150

153-
async createStoragePlugin(): Promise < IPluginKVStorage > {
154-
const kvStoragePlugin = this.options.plugins.find((p) => p.getAspect() === PluginAspect.KV_STORAGE);
155-
if(kvStoragePlugin) {
156-
return kvStoragePlugin as IPluginKVStorage;
151+
createOpenApiValidator(): OpenApiValidator {
152+
return new OpenApiValidator({
153+
apiSpec: BIF_OPEN_API_JSON,
154+
validateRequests: true,
155+
validateResponses: false
156+
});
157157
}
158+
159+
async createStoragePlugin(): Promise<IPluginKVStorage> {
160+
const kvStoragePlugin = this.options.plugins.find((p) => p.getAspect() === PluginAspect.KV_STORAGE);
161+
if (kvStoragePlugin) {
162+
return kvStoragePlugin as IPluginKVStorage;
163+
}
158164
const storagePluginPackage = this.options.config.get('storagePluginPackage');
159-
const { PluginFactoryKVStorage } = await import(storagePluginPackage);
160-
const storagePluginOptionsJson = this.options.config.get('storagePluginOptionsJson');
161-
const storagePluginOptions = JSON.parse(storagePluginOptionsJson);
162-
const pluginFactory: PluginFactory<IPluginKVStorage, unknown> = new PluginFactoryKVStorage();
163-
const plugin = await pluginFactory.create(storagePluginOptions);
164-
return plugin;
165-
}
165+
const { PluginFactoryKVStorage } = await import(storagePluginPackage);
166+
const storagePluginOptionsJson = this.options.config.get('storagePluginOptionsJson');
167+
const storagePluginOptions = JSON.parse(storagePluginOptionsJson);
168+
const pluginFactory: PluginFactory<IPluginKVStorage, unknown> = new PluginFactoryKVStorage();
169+
const plugin = await pluginFactory.create(storagePluginOptions);
170+
return plugin;
171+
}
166172

167-
createCorsMiddleware(): RequestHandler {
168-
const apiCorsDomainCsv = this.options.config.get('apiCorsDomainCsv');
169-
const allowedDomains = apiCorsDomainCsv.split(',');
170-
const allDomainsAllowed = allowedDomains.includes('*');
171-
172-
const corsOptions: CorsOptions = {
173-
origin: (origin: string | undefined, callback) => {
174-
if (allDomainsAllowed || origin && allowedDomains.indexOf(origin) !== -1) {
175-
callback(null, true);
176-
} else {
177-
callback(new Error(`CORS not allowed for Origin "${origin}".`));
173+
createCorsMiddleware(): RequestHandler {
174+
const apiCorsDomainCsv = this.options.config.get('apiCorsDomainCsv');
175+
const allowedDomains = apiCorsDomainCsv.split(',');
176+
const allDomainsAllowed = allowedDomains.includes('*');
177+
178+
const corsOptions: CorsOptions = {
179+
origin: (origin: string | undefined, callback) => {
180+
if (allDomainsAllowed || origin && allowedDomains.indexOf(origin) !== -1) {
181+
callback(null, true);
182+
} else {
183+
callback(new Error(`CORS not allowed for Origin "${origin}".`));
184+
}
178185
}
179186
}
187+
return cors(corsOptions);
180188
}
181-
return cors(corsOptions);
182-
}
183189
}

packages/bif-core-api/src/main/typescript/plugin/web-service/i-plugin-web-service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ export interface IPluginWebService extends ICactusPlugin {
55
installWebService(expressApp: any): IWebServiceEndpoint[];
66
}
77

8-
export function isIPluginWebService(pluginInstance: IPluginWebService): pluginInstance is IPluginWebService {
8+
export function isIPluginWebService(pluginInstance: any): pluginInstance is IPluginWebService {
99
return typeof pluginInstance.installWebService === 'function';
1010
}

packages/bif-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export class PluginLedgerConnectorQuorum implements IPluginLedgerConnector<any,
3535
if (!options) {
3636
throw new Error(`PluginLedgerConnectorQuorum#ctor options falsy.`);
3737
}
38+
if (!options.rpcApiHttpHost) {
39+
throw new Error(`PluginLedgerConnectorQuorum#ctor options.rpcApiHttpHost falsy.`);
40+
}
3841
const web3Provider = new Web3.providers.HttpProvider(this.options.rpcApiHttpHost);
3942
this.web3 = new Web3(web3Provider);
4043
this.log = LoggerProvider.getOrCreate({ label: 'plugin-ledger-connector-quorum', level: 'trace' })
@@ -43,9 +46,12 @@ export class PluginLedgerConnectorQuorum implements IPluginLedgerConnector<any,
4346
public installWebService(expressApp: any): IWebServiceEndpoint[] {
4447
const endpoints: IWebServiceEndpoint[] = [];
4548
{
46-
const endpoint: IWebServiceEndpoint = new DeployContractEndpoint({ path: '/deploy-contract', plugin: this });
49+
const pluginId = this.getId(); // @hyperledger/cactus-plugin-ledger-connector-quorum
50+
const path = `/api/v1/plugins/${pluginId}/contract/deploy`;
51+
const endpoint: IWebServiceEndpoint = new DeployContractEndpoint({ path, plugin: this });
4752
expressApp.use(endpoint.getPath(), endpoint.getExpressRequestHandler());
4853
endpoints.push(endpoint);
54+
this.log.info(`Registered contract deployment endpoint at ${path}`);
4955
}
5056
return endpoints;
5157
}

packages/bif-plugin-ledger-connector-quorum/src/main/typescript/web-services/deploy-contract-endpoint.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ export class DeployContractEndpoint implements IWebServiceEndpoint {
2323
return this.handleRequest.bind(this);
2424
}
2525

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

3232
// FIXME: this should actually validate the request?

packages/bif-test-plugin-ledger-connector-quorum/package-lock.json

+26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/bif-test-plugin-ledger-connector-quorum/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@hyperledger-labs/bif-plugin-kv-storage-memory": "0.2.0",
5656
"@hyperledger-labs/bif-plugin-ledger-connector-quorum": "^0.2.0",
5757
"@hyperledger-labs/bif-sdk": "0.2.0",
58+
"axios": "0.19.2",
5859
"joi": "14.3.1",
5960
"web3": "1.2.7",
6061
"web3-eth-contract": "1.2.7",

0 commit comments

Comments
 (0)