@chainlink/external-adapter-framework 0.30.2 → 0.30.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/adapter/price.js +2 -1
  2. package/adapter/price.js.map +1 -1
  3. package/adapter-generator.js +9 -0
  4. package/generator-adapter/generators/app/index.js +337 -0
  5. package/generator-adapter/generators/app/templates/CHANGELOG.md +0 -0
  6. package/generator-adapter/generators/app/templates/README.md +3 -0
  7. package/generator-adapter/generators/app/templates/babel.config.js +3 -0
  8. package/generator-adapter/generators/app/templates/jest.config.js +3 -0
  9. package/generator-adapter/generators/app/templates/package.json +30 -0
  10. package/generator-adapter/generators/app/templates/src/config/index.ts +25 -0
  11. package/generator-adapter/generators/app/templates/src/config/overrides.json +3 -0
  12. package/generator-adapter/generators/app/templates/src/endpoint/base.ts.ejs +21 -0
  13. package/generator-adapter/generators/app/templates/src/endpoint/endpoint-router.ts.ejs +33 -0
  14. package/generator-adapter/generators/app/templates/src/endpoint/endpoint.ts.ejs +26 -0
  15. package/generator-adapter/generators/app/templates/src/endpoint/index.ts.ejs +1 -0
  16. package/generator-adapter/generators/app/templates/src/index.ts.ejs +21 -0
  17. package/generator-adapter/generators/app/templates/src/transport/custom.ts.ejs +87 -0
  18. package/generator-adapter/generators/app/templates/src/transport/http.ts.ejs +98 -0
  19. package/generator-adapter/generators/app/templates/src/transport/ws.ts.ejs +94 -0
  20. package/generator-adapter/generators/app/templates/test/adapter-ws.test.ts.ejs +58 -0
  21. package/generator-adapter/generators/app/templates/test/adapter.test.ts.ejs +50 -0
  22. package/generator-adapter/generators/app/templates/test/fixtures.ts.ejs +44 -0
  23. package/generator-adapter/generators/app/templates/test-payload.json +6 -0
  24. package/generator-adapter/generators/app/templates/tsconfig.base.json +40 -0
  25. package/generator-adapter/generators/app/templates/tsconfig.json +9 -0
  26. package/generator-adapter/generators/app/templates/tsconfig.test.json +7 -0
  27. package/generator-adapter/package.json +12 -0
  28. package/package.json +10 -4
package/adapter/price.js CHANGED
@@ -75,7 +75,8 @@ class PriceAdapter extends index_1.Adapter {
75
75
  const response = await super.handleRequest(req, replySent);
76
76
  if (this.includesMap && req.requestContext.priceMeta?.inverse) {
77
77
  // We need to search in the reverse order (quote -> base) because the request transform will have inverted the pair
78
- const cloneResponse = { ...response };
78
+ // Deep clone the response, as it may contain objects which won't be cloned by simply destructuring
79
+ const cloneResponse = JSON.parse(JSON.stringify(response));
79
80
  const inverseResult = 1 / cloneResponse.result;
80
81
  cloneResponse.result = inverseResult;
81
82
  // Check if response data has a result within it
@@ -1 +1 @@
1
- {"version":3,"file":"price.js","sourceRoot":"","sources":["../../../src/adapter/price.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"price.js","sourceRoot":"","sources":["../../../src/adapter/price.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ var path_1 = require("path");
5
+ var child_process_1 = require("child_process");
6
+ var pathArg = process.argv[2] || '';
7
+ var generatorPath = (0, path_1.resolve)(__dirname, './generator-adapter');
8
+ var generatorCommand = "yo ".concat(generatorPath, " ").concat(pathArg, " ");
9
+ (0, child_process_1.execSync)(generatorCommand, { stdio: 'inherit' });
@@ -0,0 +1,337 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const Generator = require("yeoman-generator");
4
+ module.exports = class extends Generator {
5
+ constructor(args, opts) {
6
+ super(args, opts);
7
+ this.endpointsAndAliases = new Set();
8
+ // When EXTERNAL_ADAPTER_GENERATOR_NO_INTERACTIVE is set to true, the generator will not prompt the user and will use the
9
+ // default values to create one endpoint with all transports. This is useful for testing the generator in CI, or in cases
10
+ // where the user wants to quickly generate the boilerplate code.
11
+ this.promptDisabled = process.env.EXTERNAL_ADAPTER_GENERATOR_NO_INTERACTIVE === 'true';
12
+ // When EXTERNAL_ADAPTER_GENERATOR_STANDALONE is set to true, tsconfig files (tsconfig.json and tsconfig.test.json) will not
13
+ // extend tsconfig.base.json which is present in external-adapters-js monorepo, but rather generator will create new tsconfig.base.json
14
+ // with the same content in the same directory and extend from it. Also, new packages and config files (jest, babel) will be added
15
+ // to be able to run the tests
16
+ this.standalone = process.env.EXTERNAL_ADAPTER_GENERATOR_STANDALONE === 'true';
17
+ this.argument('rootPath', {
18
+ type: String,
19
+ required: false,
20
+ default: './',
21
+ description: 'Root path where new External Adapter will be created',
22
+ });
23
+ }
24
+ // prompting stage is used to get input from the user, validate and store it to use it for next stages
25
+ async prompting() {
26
+ const adapterName = await this._promptAdapterName();
27
+ const endpointCount = await this._promptEndpointCount();
28
+ const endpoints = {};
29
+ for (let i = 0; i < endpointCount; i++) {
30
+ let inputEndpointName = await this._promptEndpointName(i);
31
+ this.endpointsAndAliases.add(inputEndpointName);
32
+ let endpointAliases = await this._promptAliases(inputEndpointName);
33
+ endpointAliases.forEach(alias => this.endpointsAndAliases.add(alias));
34
+ const inputTransports = await this._promptTransports(inputEndpointName);
35
+ endpoints[i] = {
36
+ inputEndpointName,
37
+ normalizedEndpointName: this._normalizeEndpointName(inputEndpointName),
38
+ inputTransports,
39
+ normalizedEndpointNameCap: '',
40
+ endpointAliases,
41
+ };
42
+ endpoints[i].normalizedEndpointNameCap = endpoints[i].normalizedEndpointName.charAt(0).toUpperCase() + endpoints[i].normalizedEndpointName.slice(1);
43
+ }
44
+ const endpointNames = Object.values(endpoints).map(e => e.normalizedEndpointName).join(', ');
45
+ const includeComments = await this._promptConfirmation(adapterName, endpointNames);
46
+ this.props = {
47
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
48
+ // @ts-ignore
49
+ frameworkVersion: (await Promise.resolve().then(() => require('../../../package.json'))).version,
50
+ adapterName,
51
+ endpoints,
52
+ endpointNames,
53
+ defaultEndpoint: endpoints[0],
54
+ includeComments,
55
+ };
56
+ }
57
+ // writing stage is used to create new folder/files with templates based on user-provided input
58
+ writing() {
59
+ // Copy base files
60
+ const baseFiles = [
61
+ 'CHANGELOG.md',
62
+ 'package.json',
63
+ 'README.md',
64
+ 'test-payload.json',
65
+ 'tsconfig.json',
66
+ 'tsconfig.test.json',
67
+ ];
68
+ // If the generator is in standalone mode, also create tsconfig.base.json in the same directory
69
+ // so that both tsconfig and tsconfig.test can extend it. If the generator is not in standalone mode,
70
+ // tsconfig files will extend base settings from external-adapter-js monorepo base tsconfig.
71
+ // Same way jest and babel config files are also created to be able to run the integration tests
72
+ if (this.standalone) {
73
+ baseFiles.push('tsconfig.base.json', 'babel.config.js', 'jest.config.js');
74
+ }
75
+ baseFiles.forEach(fileName => {
76
+ this.fs.copyTpl(this.templatePath(fileName), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/${fileName}`), { ...this.props, standalone: this.standalone });
77
+ });
78
+ // copy main index.ts file
79
+ this.fs.copyTpl(this.templatePath(`src/index.ts.ejs`), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/index.ts`), this.props);
80
+ // Copy config
81
+ this.fs.copy(this.templatePath('src/config/index.ts'), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/config/index.ts`));
82
+ this.fs.copyTpl(this.templatePath('src/config/overrides.json'), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/config/overrides.json`), this.props);
83
+ // Create endpoint and transport files
84
+ Object.values(this.props.endpoints).forEach(({ inputEndpointName, inputTransports, endpointAliases }) => {
85
+ if (inputTransports.length > 1) {
86
+ // Router endpoints
87
+ this.fs.copyTpl(this.templatePath('src/endpoint/endpoint-router.ts.ejs'), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/endpoint/${inputEndpointName}.ts`), {
88
+ inputEndpointName,
89
+ inputTransports,
90
+ endpointAliases,
91
+ adapterName: this.props.adapterName,
92
+ includeComments: this.props.includeComments,
93
+ });
94
+ inputTransports.forEach(transport => {
95
+ this.fs.copyTpl(this.templatePath(`src/transport/${transport.type}.ts.ejs`), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/transport/${inputEndpointName}-${transport.type}.ts`), { inputEndpointName, includeComments: this.props.includeComments });
96
+ });
97
+ }
98
+ else {
99
+ // Single transport endpoints
100
+ this.fs.copyTpl(this.templatePath('src/endpoint/endpoint.ts.ejs'), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/endpoint/${inputEndpointName}.ts`), {
101
+ inputEndpointName,
102
+ inputTransports,
103
+ endpointAliases,
104
+ adapterName: this.props.adapterName,
105
+ includeComments: this.props.includeComments,
106
+ });
107
+ this.fs.copyTpl(this.templatePath(`src/transport/${inputTransports[0].type}.ts.ejs`), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/transport/${inputEndpointName}.ts`), { inputEndpointName, includeComments: this.props.includeComments });
108
+ }
109
+ });
110
+ // Create endpoint barrel file
111
+ this.fs.copyTpl(this.templatePath(`src/endpoint/index.ts.ejs`), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/endpoint/index.ts`), { endpoints: Object.values(this.props.endpoints) });
112
+ // Create test files
113
+ const httpEndpoints = Object.values(this.props.endpoints).filter((e) => e.inputTransports.some(t => t.type === 'http'));
114
+ const wsEndpoints = Object.values(this.props.endpoints).filter((e) => e.inputTransports.some(t => t.type === 'ws'));
115
+ const customEndpoints = Object.values(this.props.endpoints).filter((e) => e.inputTransports.some(t => t.type === 'custom'));
116
+ // Create adapter.test.ts if there is at least one endpoint with httpTransport
117
+ if (httpEndpoints.length) {
118
+ this.fs.copyTpl(this.templatePath(`test/adapter.test.ts.ejs`), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/test/integration/adapter.test.ts`), { endpoints: httpEndpoints, transportName: 'rest' });
119
+ }
120
+ // Create adapter.test.ts or adapter-ws.test.ts if there is at least one endpoint with wsTransport
121
+ if (wsEndpoints.length) {
122
+ let fileName = 'adapter.test.ts';
123
+ if (httpEndpoints.length || customEndpoints.length) {
124
+ fileName = 'adapter-ws.test.ts';
125
+ }
126
+ this.fs.copyTpl(this.templatePath(`test/adapter-ws.test.ts.ejs`), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/test/integration/${fileName}`), { endpoints: wsEndpoints });
127
+ }
128
+ // Create adapter.test.ts or adapter-custom.test.ts if there is at least one endpoint with customTransport.
129
+ // Custom transport integration tests use the same template as http, but in separate file. This is not ideal
130
+ // since the setup is the same (usually) and we could have just another test describe block, but at least this is
131
+ // consistent behavior as each transport-specific test is in its own file.
132
+ if (customEndpoints.length) {
133
+ let fileName = 'adapter.test.ts';
134
+ if (httpEndpoints.length || wsEndpoints.length) {
135
+ fileName = 'adapter-custom.test.ts';
136
+ }
137
+ this.fs.copyTpl(this.templatePath(`test/adapter.test.ts.ejs`), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/test/integration/${fileName}`), { endpoints: customEndpoints, transportName: 'custom' });
138
+ }
139
+ // Copy test fixtures
140
+ this.fs.copyTpl(this.templatePath(`test/fixtures.ts.ejs`), this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/test/integration/fixtures.ts`), {
141
+ includeWsFixtures: wsEndpoints.length > 0,
142
+ includeHttpFixtures: httpEndpoints.length > 0 || customEndpoints.length > 0,
143
+ });
144
+ // Add dependencies to existing package.json
145
+ const pkgJson = {
146
+ devDependencies: {
147
+ '@types/jest': '27.5.2',
148
+ '@types/node': '16.11.51',
149
+ nock: '13.2.9',
150
+ typescript: '5.0.4',
151
+ },
152
+ dependencies: {
153
+ '@chainlink/external-adapter-framework': this.props.frameworkVersion,
154
+ tslib: '2.4.1',
155
+ },
156
+ scripts: {}
157
+ };
158
+ // If EA has websocket transports add additional packages for tests.
159
+ if (wsEndpoints.length) {
160
+ pkgJson.devDependencies['@sinonjs/fake-timers'] = '9.1.2';
161
+ pkgJson.devDependencies['@types/sinonjs__fake-timers'] = '8.1.2';
162
+ }
163
+ // If the generator is in standalone mode, add additional packages and a script for running the tests with jest
164
+ if (this.standalone) {
165
+ pkgJson.devDependencies['@babel/core'] = '7.21.8';
166
+ pkgJson.devDependencies['@babel/preset-env'] = '7.20.2';
167
+ pkgJson.devDependencies['@babel/preset-typescript'] = "7.21.5";
168
+ pkgJson.devDependencies['jest'] = '29.5.0';
169
+ pkgJson.scripts['test'] = 'EA_PORT=0 METRICS_ENABLED=false jest --updateSnapshot';
170
+ }
171
+ this.fs.extendJSON(this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/package.json`), pkgJson);
172
+ }
173
+ // install stage is used to run npm or yarn install scripts
174
+ install() {
175
+ this.yarnInstall([], { cwd: `${this.options.rootPath}/${this.props.adapterName}` });
176
+ }
177
+ // end is the last stage. can be used for messages or cleanup
178
+ end() {
179
+ this.log(`🚀 Adapter '${this.props.adapterName}' was successfully created. 📍${this.options.rootPath}/${this.props.adapterName}`);
180
+ }
181
+ async _promptAdapterName() {
182
+ if (this.promptDisabled) {
183
+ return 'example-adapter';
184
+ }
185
+ let { adapterName } = await this.prompt({
186
+ type: 'input',
187
+ name: 'adapterName',
188
+ message: 'What is the name of adapter?:',
189
+ default: 'example-adapter',
190
+ });
191
+ adapterName = this._normalizeStringInput(adapterName);
192
+ if (adapterName === '') {
193
+ this.log('Adapter name cannot be empty');
194
+ return this._promptAdapterName();
195
+ }
196
+ return adapterName;
197
+ }
198
+ async _promptEndpointCount() {
199
+ if (this.promptDisabled) {
200
+ return 1;
201
+ }
202
+ let { endpointCount } = await this.prompt({
203
+ type: 'input',
204
+ name: 'endpointCount',
205
+ message: 'How many endpoints does adapter have?:',
206
+ default: '1',
207
+ });
208
+ endpointCount = parseInt(endpointCount);
209
+ if (isNaN(endpointCount) || endpointCount <= 0) {
210
+ this.log('Adapter should have at least one endpoint');
211
+ return this._promptEndpointCount();
212
+ }
213
+ return endpointCount;
214
+ }
215
+ async _promptEndpointName(index) {
216
+ if (this.promptDisabled) {
217
+ return 'price';
218
+ }
219
+ const { inputEndpointName } = await this.prompt({
220
+ type: 'input',
221
+ name: 'inputEndpointName',
222
+ message: `What is the name of endpoint #${index + 1}:`,
223
+ default: 'price',
224
+ });
225
+ const endpointName = this._normalizeStringInput(inputEndpointName);
226
+ if (endpointName === '') {
227
+ this.log('Endpoint name cannot be empty');
228
+ return this._promptEndpointName(index);
229
+ }
230
+ if (this.endpointsAndAliases.has(endpointName)) {
231
+ this.log(`Endpoint named or aliased '${endpointName}' already exists`);
232
+ return this._promptEndpointName(index);
233
+ }
234
+ return endpointName;
235
+ }
236
+ async _promptAliases(inputEndpointName) {
237
+ if (this.promptDisabled) {
238
+ return [];
239
+ }
240
+ const { endpointAliasesAnswer } = await this.prompt({
241
+ type: 'input',
242
+ name: 'endpointAliasesAnswer',
243
+ message: `Comma separated aliases for endpoint '${inputEndpointName}':`,
244
+ default: 'empty',
245
+ });
246
+ let endpointAliases;
247
+ if (endpointAliasesAnswer === 'empty' || endpointAliasesAnswer.trim().length === 0) {
248
+ return [];
249
+ }
250
+ else {
251
+ endpointAliases = [...new Set(...[endpointAliasesAnswer.split(',').map(a => this._normalizeStringInput(a.trim()))])];
252
+ }
253
+ let existingEndpoint = endpointAliases.some(a => this.endpointsAndAliases.has(a));
254
+ if (existingEndpoint) {
255
+ this.log(`One of endpoints already contains one or more provided aliases.`);
256
+ return this._promptAliases(inputEndpointName);
257
+ }
258
+ return endpointAliases;
259
+ }
260
+ async _promptTransports(inputEndpointName) {
261
+ if (this.promptDisabled) {
262
+ return [
263
+ { type: 'http', name: 'httpTransport' }, { type: 'ws', name: 'wsTransport' }, { type: 'custom', name: 'customTransport', }
264
+ ];
265
+ }
266
+ const { inputTransports } = await this.prompt({
267
+ type: 'checkbox',
268
+ name: 'inputTransports',
269
+ message: `Select transports that endpoint '${inputEndpointName}' supports:`,
270
+ choices: [
271
+ {
272
+ name: 'Http',
273
+ value: {
274
+ type: 'http',
275
+ name: 'httpTransport',
276
+ },
277
+ checked: true,
278
+ },
279
+ {
280
+ name: 'Websocket',
281
+ value: {
282
+ type: 'ws',
283
+ name: 'wsTransport',
284
+ },
285
+ },
286
+ {
287
+ name: 'Custom',
288
+ value: {
289
+ type: 'custom',
290
+ name: 'customTransport',
291
+ },
292
+ },
293
+ ],
294
+ });
295
+ if (!inputTransports.length) {
296
+ this.log('Endpoint should have at least one transport');
297
+ return this._promptTransports(inputEndpointName);
298
+ }
299
+ return inputTransports;
300
+ }
301
+ async _promptConfirmation(adapterName, endpointNames) {
302
+ if (this.promptDisabled) {
303
+ return true;
304
+ }
305
+ const { useComments } = await this.prompt({
306
+ type: 'confirm',
307
+ name: 'useComments',
308
+ default: true,
309
+ message: `Do you want helpful explicative comments to be included along with the source code? (These are usually not included with adapters but can be helpful if you're new to EA development):`,
310
+ });
311
+ const { confirmed } = await this.prompt({
312
+ type: 'confirm',
313
+ name: 'confirmed',
314
+ message: `New adapter '${adapterName}' will be created with following endpoints '${endpointNames}'`,
315
+ });
316
+ if (!confirmed) {
317
+ process.exit(0);
318
+ }
319
+ return useComments;
320
+ }
321
+ //convert endpoint name to normalized name that can be used in imports/exports, i.e. crypto-one-two -> cryptoOneTwo
322
+ _normalizeEndpointName(endpointName) {
323
+ const words = endpointName.split('-');
324
+ const capitalizedWords = words.map((word, index) => {
325
+ if (index === 0) {
326
+ return word;
327
+ }
328
+ else {
329
+ return word.charAt(0).toUpperCase() + word.slice(1);
330
+ }
331
+ });
332
+ return capitalizedWords.join('');
333
+ }
334
+ _normalizeStringInput(input) {
335
+ return input.trim().replace(/ /g, '-');
336
+ }
337
+ };
@@ -0,0 +1,3 @@
1
+ # Chainlink External Adapter for <%= adapterName %>
2
+
3
+ This README will be generated automatically when code is merged to `main`. If you would like to generate a preview of the README, please run `yarn generate:readme <%= adapterName %>`.
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
3
+ }
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ modulePathIgnorePatterns: ['./dist'],
3
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@chainlink/<%= adapterName %>-adapter",
3
+ "version": "0.0.0",
4
+ "description": "Chainlink <%= adapterName %> adapter.",
5
+ "keywords": [
6
+ "Chainlink",
7
+ "LINK",
8
+ "blockchain",
9
+ "oracle",
10
+ "<%= adapterName %>"
11
+ ],
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "repository": {
18
+ "url": "https://github.com/smartcontractkit/external-adapters-js",
19
+ "type": "git"
20
+ },
21
+ "license": "MIT",
22
+ "scripts": {
23
+ "clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
24
+ "prepack": "yarn build",
25
+ "build": "tsc -b",
26
+ "server": "node -e 'require(\"./index.js\").server()'",
27
+ "server:dist": "node -e 'require(\"./dist/index.js\").server()'",
28
+ "start": "yarn server:dist"
29
+ }
30
+ }
@@ -0,0 +1,25 @@
1
+ import { AdapterConfig } from '@chainlink/external-adapter-framework/config'
2
+
3
+ export const config = new AdapterConfig(
4
+ {
5
+ API_KEY: {
6
+ description:
7
+ 'An API key for Data Provider',
8
+ type: 'string',
9
+ required: true,
10
+ sensitive: true,
11
+ },
12
+ API_ENDPOINT: {
13
+ description:
14
+ 'An API endpoint for Data Provider',
15
+ type: 'string',
16
+ default: 'https://dataproviderapi.com',
17
+ },
18
+ WS_API_ENDPOINT: {
19
+ description:
20
+ 'WS endpoint for Data Provider',
21
+ type: 'string',
22
+ default: 'ws://localhost:9090',
23
+ },
24
+ },
25
+ )
@@ -0,0 +1,3 @@
1
+ {
2
+ "<%= adapterName %>": {}
3
+ }
@@ -0,0 +1,21 @@
1
+ <% if (includeComments) { %>// Input parameters define the structure of the request expected by the endpoint.<% } %>
2
+ export const inputParameters = new InputParameters({
3
+ base: {
4
+ aliases: ['from', 'coin', 'symbol', 'market'],
5
+ required: true,
6
+ type: 'string',
7
+ description: 'The symbol of symbols of the currency to query',
8
+ },
9
+ quote: {
10
+ aliases: ['to', 'convert'],
11
+ required: true,
12
+ type: 'string',
13
+ description: 'The symbol of the currency to convert to',
14
+ },
15
+ })
16
+ <% if (includeComments) { %>// Endpoints contain a type parameter that allows specifying relevant types of an endpoint, for example, request payload type, Adapter response type and Adapter configuration (environment variables) type<% } %>
17
+ export type BaseEndpointTypes = {
18
+ Parameters: typeof inputParameters.definition
19
+ Response: SingleNumberResultResponse
20
+ Settings: typeof config.settings
21
+ }
@@ -0,0 +1,33 @@
1
+ import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2
+ import { InputParameters } from '@chainlink/external-adapter-framework/validation'
3
+ import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util'
4
+ import { TransportRoutes } from '@chainlink/external-adapter-framework/transports'
5
+ import { config } from '../config'
6
+ import overrides from '../config/overrides.json'
7
+ <% for(let i=0; i<inputTransports.length; i++) {%>
8
+ import { <%= inputTransports[i].name %> } from '../transport/<%= inputEndpointName %>-<%= inputTransports[i].type %>' <% }
9
+ %>
10
+
11
+ <%- include ./base.ts.ejs %>
12
+
13
+ export const endpoint = new AdapterEndpoint({
14
+ <% if (includeComments) { -%>
15
+ // Endpoint name
16
+ <% } -%><%= ' ' %> name: '<%= inputEndpointName %>',
17
+ <% if (includeComments) { -%>
18
+ // Alternative endpoint names for this endpoint
19
+ <% } -%><%= ' ' %> aliases: <%- endpointAliases.length ? JSON.stringify(endpointAliases) : JSON.stringify([]) -%>,
20
+ <% if (includeComments) { -%>
21
+ // Transport handles incoming requests, data processing and communication for this endpoint.
22
+ // In case endpoint supports multiple transports (i.e. http and websocket) TransportRoutes is used to register all supported transports.
23
+ // To use specific transport, provide `transport: [transportName]` in the request
24
+ <% } -%><%= ' ' %> transportRoutes: new TransportRoutes<BaseEndpointTypes>()
25
+ <% for(let i=0; i<inputTransports.length; i++) {-%>
26
+ .register('<%- inputTransports[i].type === "http" ? `rest` : inputTransports[i].type %>', <%- inputTransports[i].name %>)<%}%>,
27
+ <% if (includeComments) { -%>
28
+ // Supported input parameters for this endpoint
29
+ <% } -%><%= ' ' %> inputParameters,
30
+ <% if (includeComments) { -%>
31
+ // Overrides are defined in the `/config/overrides.json` file. They allow input parameters to be overriden from a generic symbol to something more specific for the data provider such as an ID.
32
+ <% } -%><%= ' ' %> overrides: overrides['<%= adapterName %>']
33
+ })
@@ -0,0 +1,26 @@
1
+ import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2
+ import { InputParameters } from '@chainlink/external-adapter-framework/validation'
3
+ import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util'
4
+ import { config } from '../config'
5
+ import overrides from '../config/overrides.json'
6
+ import { <%= inputTransports[0].name %> } from '../transport/<%= inputEndpointName %>'
7
+
8
+ <%- include ./base.ts.ejs %>
9
+
10
+ export const endpoint = new AdapterEndpoint({
11
+ <% if (includeComments) { -%>
12
+ // Endpoint name
13
+ <% } -%><%= ' ' %> name: '<%= inputEndpointName %>',
14
+ <% if (includeComments) { -%>
15
+ // Alternative endpoint names for this endpoint
16
+ <% } -%><%= ' ' %> aliases: <%- endpointAliases.length ? JSON.stringify(endpointAliases) : JSON.stringify([]) -%>,
17
+ <% if (includeComments) { -%>
18
+ // Transport handles incoming requests, data processing and communication for this endpoint
19
+ <% } -%><%= ' ' %> transport: <%= inputTransports[0].name %>,
20
+ <% if (includeComments) { -%>
21
+ // Supported input parameters for this endpoint
22
+ <% } -%><%= ' ' %> inputParameters,
23
+ <% if (includeComments) { -%>
24
+ // Overrides are defined in the `/config/overrides.json` file. They allow input parameters to be overriden from a generic symbol to something more specific for the data provider such as an ID.
25
+ <% } -%><%= ' ' %> overrides: overrides['<%= adapterName %>']
26
+ })
@@ -0,0 +1 @@
1
+ <% for(let i=0; i<endpoints.length; i++) {%>export { endpoint as <%- endpoints[i].normalizedEndpointName %> } from './<%= endpoints[i].inputEndpointName %>' <%- '\n' %><%}%>
@@ -0,0 +1,21 @@
1
+ import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
2
+ import { Adapter } from '@chainlink/external-adapter-framework/adapter'
3
+ import { config } from './config'
4
+ import { <%= endpointNames %> } from './endpoint'
5
+
6
+ export const adapter = new Adapter({
7
+ <% if (includeComments) { -%>
8
+ //Requests will direct to this endpoint if the `endpoint` input parameter is not specified.
9
+ <% } -%><%= ' ' %> defaultEndpoint: <%= defaultEndpoint.normalizedEndpointName %>.name,
10
+ <% if (includeComments) { -%>
11
+ // Adapter name
12
+ <% } -%><%= ' ' %> name: '<%= adapterName.toUpperCase() %>',
13
+ <% if (includeComments) { -%>
14
+ // Adapter configuration (environment variables)
15
+ <% } -%><%= ' ' %> config,
16
+ <% if (includeComments) { -%>
17
+ // List of supported endpoints
18
+ <% } -%><%= ' ' %> endpoints: [<%= endpointNames %>],
19
+ })
20
+
21
+ export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
@@ -0,0 +1,87 @@
1
+ import { Transport, TransportDependencies } from '@chainlink/external-adapter-framework/transports'
2
+ import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response'
3
+ import { Requester } from '@chainlink/external-adapter-framework/util/requester'
4
+ import {
5
+ AdapterRequest,
6
+ AdapterResponse,
7
+ } from '@chainlink/external-adapter-framework/util'
8
+ import { TypeFromDefinition } from '@chainlink/external-adapter-framework/validation/input-params'
9
+ import { BaseEndpointTypes } from '../endpoint/<%= inputEndpointName %>'
10
+
11
+ <% if (includeComments) { -%>
12
+ // CustomTransport extends base types from endpoint and adds additional, Provider-specific types (if needed).
13
+ <% } -%>
14
+ export type CustomTransportTypes = BaseEndpointTypes & {
15
+ Provider: {
16
+ RequestBody: never
17
+ ResponseBody: any
18
+ }
19
+ }
20
+ <% if (includeComments) { -%>
21
+ // CustomTransport is used to perform custom data fetching and processing from a Provider. The framework provides built-in transports to
22
+ // fetch data from a Provider using several protocols, including `http`, `websocket`, and `sse`. Use CustomTransport when the Provider uses
23
+ // different protocol, or you need custom functionality that built-in transports don't support. For example, custom, multistep authentication
24
+ // for requests, paginated requests, on-chain data retrieval using third party libraries, and so on.
25
+ <% } -%>
26
+ export class CustomTransport<T extends CustomTransportTypes> implements Transport<T> {
27
+ <% if (includeComments) { -%>
28
+ // name of the transport, used for logging
29
+ <% } -%>
30
+ name!: string
31
+ <% if (includeComments) { -%>
32
+ // cache instance for caching responses from provider
33
+ <% } -%>
34
+ responseCache!: ResponseCache<T>
35
+ <% if (includeComments) { -%>
36
+ // instance of Requester to be used for data fetching. Use this instance to perform http calls
37
+ <% } -%>
38
+ requester!: Requester
39
+
40
+ <% if (includeComments) { -%>
41
+ // REQUIRED. Transport will be automatically initialized by the framework using this method. It will be called with transport
42
+ // dependencies, adapter settings, endpoint name, and transport name as arguments. Use this method to initialize transport state
43
+ <% } -%>
44
+ async initialize(dependencies: TransportDependencies<T>, _adapterSettings: CustomTransportTypes['Settings'], _endpointName: string, transportName: string): Promise<void> {
45
+ this.responseCache = dependencies.responseCache
46
+ this.requester = dependencies.requester
47
+ this.name = transportName
48
+ }
49
+ <% if (includeComments) { -%>
50
+ // 'foregroundExecute' performs synchronous fetch/processing of information within the lifecycle of an incoming request. It takes
51
+ // request object (adapter request, which is wrapper around fastify request) and adapter settings. Use this method to handle the incoming
52
+ // request, process it,save it in the cache and return to user.
53
+ <% } -%>
54
+ async foregroundExecute(
55
+ req: AdapterRequest<TypeFromDefinition<T['Parameters']>>,
56
+ ): Promise<AdapterResponse<CustomTransportTypes['Response']>> {
57
+
58
+ // Custom transport logic
59
+
60
+ const response = {
61
+ data: {
62
+ result: 100,
63
+ },
64
+ statusCode: 200,
65
+ result: 100,
66
+ timestamps: {
67
+ providerDataRequestedUnixMs: Date.now(),
68
+ providerDataReceivedUnixMs: Date.now(),
69
+ providerIndicatedTimeUnixMs: undefined,
70
+ },
71
+ }
72
+ <% if (includeComments) { -%>
73
+ // Once the response object is ready, write it to the cache. Use the transport name, request payload and constructed response. Once cache is
74
+ // saved, return the response to the user.
75
+ <% } -%>
76
+ await this.responseCache.write(this.name, [
77
+ {
78
+ params: req.requestContext.data,
79
+ response,
80
+ },
81
+ ])
82
+
83
+ return response
84
+ }
85
+ }
86
+
87
+ export const customTransport = new CustomTransport()
@@ -0,0 +1,98 @@
1
+ import { HttpTransport } from '@chainlink/external-adapter-framework/transports'
2
+ import { BaseEndpointTypes } from '../endpoint/<%= inputEndpointName %>'
3
+
4
+ export interface ResponseSchema {
5
+ [key: string]: {
6
+ price: number
7
+ errorMessage?: string
8
+ }
9
+ }
10
+
11
+ <% if (includeComments) { -%>
12
+ // HttpTransport extends base types from endpoint and adds additional, Provider-specific types like 'RequestBody', which is the type of
13
+ // request body (not the request to adapter, but the request that adapter sends to Data Provider), and 'ResponseBody' which is
14
+ // the type of raw response from Data Provider
15
+ <% } -%>
16
+ export type HttpTransportTypes = BaseEndpointTypes & {
17
+ Provider: {
18
+ RequestBody: never
19
+ ResponseBody: ResponseSchema
20
+ }
21
+ }
22
+ <% if (includeComments) { -%>
23
+ // HttpTransport is used to fetch and process data from a Provider using HTTP(S) protocol. It usually needs two methods
24
+ // `prepareRequests` and `parseResponse`
25
+ <% } -%>
26
+ export const httpTransport = new HttpTransport<HttpTransportTypes>({
27
+ <% if (includeComments) { -%>
28
+ // `prepareRequests` method receives request payloads sent to associated endpoint alongside adapter config(environment variables)
29
+ // and should return 'request information' to the Data Provider. Use this method to construct one or many requests, and the framework
30
+ // will send them to Data Provider
31
+ <% } -%>
32
+ prepareRequests: (params, config) => {
33
+ return params.map((param) => {
34
+ return {
35
+ <% if (includeComments) { -%>
36
+ // `params` are parameters associated to this single request and will also be available in the 'parseResponse' method.
37
+ <% } -%>
38
+ params: [param],
39
+ <% if (includeComments) { -%>
40
+ // `request` contains any valid axios request configuration
41
+ <% } -%>
42
+ request: {
43
+ baseURL: config.API_ENDPOINT,
44
+ url: '/cryptocurrency/price',
45
+ headers: {
46
+ 'X_API_KEY': config.API_KEY,
47
+ },
48
+ params: {
49
+ symbol: param.base.toUpperCase(),
50
+ convert: param.quote.toUpperCase(),
51
+ },
52
+ },
53
+ }
54
+ })
55
+ },
56
+ <% if (includeComments) { -%>
57
+ // `parseResponse` takes the 'params' specified in the `prepareRequests` and the 'response' from Data Provider and should return
58
+ // an array of response objects to be stored in cache. Use this method to construct a list of response objects for every parameter in 'params'
59
+ // and the framework will save them in cache and return to user
60
+ <% } -%>
61
+ parseResponse: (params, response) => {
62
+ <% if (includeComments) { -%>
63
+ // In case error was received, it's a good practice to return meaningful information to user
64
+ <% } -%>
65
+ if (!response.data) {
66
+ return params.map((param) => {
67
+ return {
68
+ params: param,
69
+ response: {
70
+ errorMessage: `The data provider didn't return any value for ${param.base}/${param.quote}`,
71
+ statusCode: 502,
72
+ },
73
+ }
74
+ })
75
+ }
76
+
77
+ <% if (includeComments) { -%>
78
+ // For successful responses for each 'param' a new response object is created and returned as an array
79
+ <% } -%>
80
+ return params.map((param) => {
81
+ const result = response.data[param.base.toUpperCase()].price
82
+ <% if (includeComments) { -%>
83
+ // Response objects, whether successful or errors, contain two properties, 'params' and 'response'. 'response' is what will be
84
+ // stored in the cache and returned as adapter response and 'params' determines the identifier so that the next request with same 'params'
85
+ // will immediately return the response from the cache
86
+ <% } -%>
87
+ return {
88
+ params: param,
89
+ response: {
90
+ result,
91
+ data: {
92
+ result
93
+ }
94
+ },
95
+ }
96
+ })
97
+ },
98
+ })
@@ -0,0 +1,94 @@
1
+ import { WebSocketTransport } from '@chainlink/external-adapter-framework/transports'
2
+ import { BaseEndpointTypes } from '../endpoint/<%= inputEndpointName %>'
3
+
4
+ export interface WSResponse {
5
+ success: boolean
6
+ price: number
7
+ base: string
8
+ quote: string
9
+ time: number
10
+ }
11
+
12
+ <% if (includeComments) { -%>
13
+ // WsTransport extends base types from endpoint and adds additional, Provider-specific types like 'WsMessage', which is the type of
14
+ // websocket received message
15
+ <% } -%>
16
+ export type WsTransportTypes = BaseEndpointTypes & {
17
+ Provider: {
18
+ WsMessage: WSResponse
19
+ }
20
+ }
21
+ <% if (includeComments) { -%>
22
+ // WebSocketTransport is used to fetch and process data from a Provider using Websocket protocol.
23
+ <% } -%>
24
+ export const wsTransport = new WebSocketTransport<WsTransportTypes>({
25
+ <% if (includeComments) { -%>
26
+ // use `url` method to provide connection url. It accepts adapter context, so you have access to adapter config(environment variables) and
27
+ // request payload if needed
28
+ <% } -%>
29
+ url: (context) => context.adapterSettings.WS_API_ENDPOINT,
30
+ <% if (includeComments) { -%>
31
+ // 'handler' contains two helpful methods. one of them is `message`. This method is called when there is a new websocket message.
32
+ // The other one is 'open' method. It is called when the websocket connection is successfully opened. Use this method to execute some logic
33
+ // when the connection is established (custom authentication, logging, ...)
34
+ <% } -%>
35
+ handlers: {
36
+ <% if (includeComments) { -%>
37
+ // 'message' handler receives a raw websocket message as first argument and adapter context as second and should return an array of
38
+ // response objects. Use this method to construct a list of response objects, and the framework will save them in cache and return to user
39
+ <% } -%>
40
+ message(message) {
41
+ <% if (includeComments) { -%>
42
+ // in cases when error or unknown message is received, use 'return' to skip the iteration.
43
+ <% } -%>
44
+ if (message.success === false) {
45
+ return
46
+ }
47
+
48
+ <% if (includeComments) { -%>
49
+ // Response objects, whether successful or errors (if not skipped), contain two properties, 'params' and 'response'. 'response' is what
50
+ // will be stored in the cache and returned as adapter response and 'params' determines the identifier so that the next request with
51
+ // same 'params' will immediately return the response from the cache
52
+ <% } -%>
53
+ return [
54
+ {
55
+ params: { base: message.base, quote: message.quote },
56
+ response: {
57
+ result: message.price,
58
+ data: {
59
+ result: message.price
60
+ },
61
+ timestamps: {
62
+ providerIndicatedTimeUnixMs: message.time,
63
+ },
64
+ },
65
+ },
66
+ ]
67
+ },
68
+ },
69
+ <% if (includeComments) { -%>
70
+ // `builders` are builder methods, that will be used to prepare specific WS messages to be sent to Data Provider
71
+ <% } -%>
72
+ builders: {
73
+ <% if (includeComments) { -%>
74
+ // `subscribeMessage` accepts request parameters and should construct and return a payload that will be sent to Data Provider
75
+ // Use this method to subscribe to live feeds
76
+ <% } -%>
77
+ subscribeMessage: (params) => {
78
+ return {
79
+ type: 'subscribe',
80
+ symbols: `${params.base}/${params.quote}`.toUpperCase()
81
+ }
82
+ },
83
+ <% if (includeComments) { -%>
84
+ // `unsubscribeMessage` accepts request parameters and should construct and return a payload that will be sent to Data Provider
85
+ // Use this method to unsubscribe from live feeds
86
+ <% } -%>
87
+ unsubscribeMessage: (params) => {
88
+ return {
89
+ type: 'unsubscribe',
90
+ symbols: `${params.base}/${params.quote}`.toUpperCase()
91
+ }
92
+ },
93
+ },
94
+ })
@@ -0,0 +1,58 @@
1
+ import { WebSocketClassProvider } from '@chainlink/external-adapter-framework/transports'
2
+ import {
3
+ TestAdapter,
4
+ setEnvVariables,
5
+ mockWebSocketProvider,
6
+ MockWebsocketServer,
7
+ } from '@chainlink/external-adapter-framework/util/testing-utils'
8
+ import FakeTimers from '@sinonjs/fake-timers'
9
+ import { mockWebsocketServer } from './fixtures'
10
+
11
+
12
+ describe('websocket', () => {
13
+ let mockWsServer: MockWebsocketServer | undefined
14
+ let testAdapter: TestAdapter
15
+ const wsEndpoint = 'ws://localhost:9090'
16
+ let oldEnv: NodeJS.ProcessEnv
17
+ <% for(let i=0; i<endpoints.length; i++) {%>
18
+ const data<%- endpoints[i].normalizedEndpointNameCap %> = {
19
+ base: 'ETH',
20
+ quote: 'USD',
21
+ endpoint: '<%- endpoints[i].inputEndpointName %>',
22
+ transport: 'ws'
23
+ }
24
+ <% } %>
25
+ beforeAll(async () => {
26
+ oldEnv = JSON.parse(JSON.stringify(process.env))
27
+ process.env['WS_API_ENDPOINT'] = wsEndpoint
28
+ process.env['API_KEY'] = 'fake-api-key'
29
+ mockWebSocketProvider(WebSocketClassProvider)
30
+ mockWsServer = mockWebsocketServer(wsEndpoint)
31
+
32
+ const adapter = (await import('./../../src')).adapter
33
+ testAdapter = await TestAdapter.startWithMockedCache(adapter, {
34
+ clock: FakeTimers.install(),
35
+ testAdapter: {} as TestAdapter<never>,
36
+ })
37
+
38
+ // Send initial request to start background execute and wait for cache to be filled with results
39
+ <% for(var i=0; i<endpoints.length; i++) {%>
40
+ await testAdapter.request(data<%- endpoints[i].normalizedEndpointNameCap %>) <% } %>
41
+ await testAdapter.waitForCache(<%- endpoints.length %>)
42
+ })
43
+
44
+ afterAll(async () => {
45
+ setEnvVariables(oldEnv)
46
+ mockWsServer?.close()
47
+ testAdapter.clock?.uninstall()
48
+ await testAdapter.api.close()
49
+ })
50
+ <% for(var i=0; i<endpoints.length; i++) {%>
51
+ describe('<%= endpoints[i].inputEndpointName %> endpoint', () => {
52
+ it('should return success', async () => {
53
+ const response = await testAdapter.request(data<%- endpoints[i].normalizedEndpointNameCap %>)
54
+ expect(response.json()).toMatchSnapshot()
55
+ })
56
+ })
57
+ <% } %>
58
+ })
@@ -0,0 +1,50 @@
1
+ import {
2
+ TestAdapter,
3
+ setEnvVariables,
4
+ } from '@chainlink/external-adapter-framework/util/testing-utils'
5
+ import * as nock from 'nock'
6
+ import { mockResponseSuccess } from './fixtures'
7
+
8
+ describe('execute', () => {
9
+ let spy: jest.SpyInstance
10
+ let testAdapter: TestAdapter
11
+ let oldEnv: NodeJS.ProcessEnv
12
+
13
+ beforeAll(async () => {
14
+ oldEnv = JSON.parse(JSON.stringify(process.env))
15
+ process.env.API_KEY = process.env.API_KEY ?? 'fake-api-key'
16
+ const mockDate = new Date('2001-01-01T11:11:11.111Z')
17
+ spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())
18
+
19
+ const adapter = (await import('./../../src')).adapter
20
+ adapter.rateLimiting = undefined
21
+ testAdapter = await TestAdapter.startWithMockedCache(adapter, {
22
+ testAdapter: {} as TestAdapter<never>,
23
+ })
24
+ })
25
+
26
+ afterAll(async () => {
27
+ setEnvVariables(oldEnv)
28
+ await testAdapter.api.close()
29
+ nock.restore()
30
+ nock.cleanAll()
31
+ spy.mockRestore()
32
+ })
33
+
34
+ <% for(var i=0; i<endpoints.length; i++) {%>
35
+ describe('<%= endpoints[i].inputEndpointName %> endpoint', () => {
36
+ it('should return success', async () => {
37
+ const data = {
38
+ base: 'ETH',
39
+ quote: 'USD',
40
+ endpoint: '<%= endpoints[i].inputEndpointName %>',
41
+ transport: '<%= transportName %>'
42
+ }
43
+ mockResponseSuccess()
44
+ const response = await testAdapter.request(data)
45
+ expect(response.statusCode).toBe(200)
46
+ expect(response.json()).toMatchSnapshot()
47
+ })
48
+ })
49
+ <% } %>
50
+ })
@@ -0,0 +1,44 @@
1
+ <% if (includeHttpFixtures) { %>import nock from 'nock'<% } %>
2
+ <% if (includeWsFixtures) { %>import { MockWebsocketServer } from '@chainlink/external-adapter-framework/util/testing-utils'<% } %>
3
+ <% if (includeHttpFixtures) { %>
4
+ export const mockResponseSuccess = (): nock.Scope =>
5
+ nock('https://dataproviderapi.com', {
6
+ encodedQueryParams: true,
7
+ })
8
+ .get('/cryptocurrency/price')
9
+ .query({
10
+ symbol: 'ETH',
11
+ convert: 'USD',
12
+ })
13
+ .reply(200, () => ({ ETH: { price: 10000 } }), [
14
+ 'Content-Type',
15
+ 'application/json',
16
+ 'Connection',
17
+ 'close',
18
+ 'Vary',
19
+ 'Accept-Encoding',
20
+ 'Vary',
21
+ 'Origin',
22
+ ])
23
+ .persist()
24
+ <% } %>
25
+ <% if (includeWsFixtures) { %>
26
+ export const mockWebsocketServer = (URL: string): MockWebsocketServer => {
27
+ const mockWsServer = new MockWebsocketServer(URL, { mock: false })
28
+ mockWsServer.on('connection', (socket) => {
29
+ socket.on('message', (message) => {
30
+ return socket.send(
31
+ JSON.stringify({
32
+ success: true,
33
+ price: 1000,
34
+ base: 'ETH',
35
+ quote: 'USD',
36
+ time: '1999999'
37
+ }),
38
+ )
39
+ })
40
+ })
41
+
42
+ return mockWsServer
43
+ }
44
+ <% } %>
@@ -0,0 +1,6 @@
1
+ {
2
+ "requests": [{
3
+ "from": "BTC",
4
+ "to": "USD"
5
+ }]
6
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "compilerOptions": {
3
+ /* Basic Options */
4
+ "incremental": true /* Enable incremental compilation */,
5
+ "target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
6
+ "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
7
+ "composite": true /* Enable project compilation */,
8
+ "declaration": true /* Generates corresponding '.d.ts' file. */,
9
+ "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
10
+ "noEmit": false /* Do not emit outputs. */,
11
+ "noErrorTruncation": true /* Do not truncate error messages */,
12
+ "skipLibCheck": true /* Skip type checking of declaration files. Requires TypeScript version 2.0 or later. */,
13
+ "importHelpers": true /* Import emit helpers from 'tslib'. */,
14
+
15
+ /* Strict Type-Checking Options */
16
+ "strict": true /* Enable all strict type-checking options. */,
17
+
18
+ /* Additional Checks */
19
+ "noUnusedLocals": true /* Report errors on unused locals. */,
20
+ "noUnusedParameters": true /* Report errors on unused parameters. */,
21
+ "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
22
+ "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
23
+ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
24
+
25
+ /* Module Resolution Options */
26
+ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
27
+ "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
28
+ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
29
+
30
+ /* Source Map Options */
31
+ "inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */,
32
+ "inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */,
33
+
34
+ /* Experimental Options */
35
+ "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
36
+ "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
37
+
38
+ "resolveJsonModule": true /* Allows importing modules with a ‘.json’ extension */
39
+ }
40
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "<%- standalone ? "./tsconfig.base.json" : "../../tsconfig.base.json" %>",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*", "src/**/*.json"],
8
+ "exclude": ["dist", "**/*.spec.ts", "**/*.test.ts"]
9
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "<%- standalone ? "./tsconfig.base.json" : "../../tsconfig.base.json" %>",
3
+ "include": ["src/**/*", "**/test", "src/**/*.json"],
4
+ "compilerOptions": {
5
+ "noEmit": true
6
+ }
7
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "generator-adapter",
3
+ "version": "0.0.1",
4
+ "files": [
5
+ "generators"
6
+ ],
7
+ "main": "generators/app/index.js",
8
+ "keywords": [
9
+ "yeoman-generator",
10
+ "extnerl-adapter-generator"
11
+ ]
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainlink/external-adapter-framework",
3
- "version": "0.30.2",
3
+ "version": "0.30.3",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -14,10 +14,12 @@
14
14
  "prom-client": "13.2.0",
15
15
  "ws": "8.9.0",
16
16
  "redlock": "5.0.0-beta.2",
17
- "mock-socket": "9.1.5"
17
+ "mock-socket": "9.1.5",
18
+ "yeoman-generator": "3.1.1"
18
19
  },
19
20
  "scripts": {
20
- "build": "mkdir -p ./dist/src && cp package.json dist/src && cp README.md dist/src && tsc",
21
+ "build": "mkdir -p ./dist/src && cp package.json dist/src && cp README.md dist/src && tsc && yarn build-generator",
22
+ "build-generator": "mkdir -p ./dist/src/generator-adapter/generators/app/templates && cp -R scripts/generator-adapter/generators/app/templates dist/src/generator-adapter/generators/app && cp scripts/generator-adapter/package.json dist/src/generator-adapter && tsc --project scripts/generator-adapter/tsconfig.json && tsc scripts/adapter-generator.ts --outDir dist/src",
21
23
  "generate-docs": "typedoc src/**/*.ts",
22
24
  "generate-ref-tables": "ts-node scripts/metrics-table.ts > docs/reference-tables/metrics.md && ts-node scripts/ea-settings-table.ts > docs/reference-tables/ea-settings.md && yarn prettier --write docs/reference-tables",
23
25
  "lint-fix": "eslint --max-warnings=0 --fix . && prettier --write ./src/**/*.ts ./test/**/*.ts ./*.{json,js,yaml}",
@@ -28,6 +30,9 @@
28
30
  "verify": "yarn lint && yarn build && yarn build -p ./test/tsconfig.json && yarn test && yarn code-coverage",
29
31
  "code-coverage": "c8 check-coverage --statements 95 --lines 95 --functions 95 --branches 90"
30
32
  },
33
+ "bin": {
34
+ "create-external-adapter": "adapter-generator.js"
35
+ },
31
36
  "devDependencies": {
32
37
  "@sinonjs/fake-timers": "9.1.2",
33
38
  "@types/eventsource": "1.1.11",
@@ -47,7 +52,8 @@
47
52
  "ts-node": "10.9.1",
48
53
  "ts-node-dev": "2.0.0",
49
54
  "typedoc": "0.23.21",
50
- "typescript": "5.0.4"
55
+ "typescript": "5.0.4",
56
+ "@types/yeoman-generator": "5.2.11"
51
57
  },
52
58
  "prettier": {
53
59
  "semi": false,