@chainlink/external-adapter-framework 0.30.2 → 0.31.0
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.
- package/adapter/por.d.ts +112 -0
- package/adapter/por.js +76 -0
- package/adapter/por.js.map +1 -0
- package/adapter/price.js +2 -1
- package/adapter/price.js.map +1 -1
- package/adapter-generator.js +9 -0
- package/config/index.d.ts +21 -1
- package/config/index.js +19 -0
- package/config/index.js.map +1 -1
- package/debug/router.d.ts +10 -0
- package/debug/router.js +49 -0
- package/debug/router.js.map +1 -0
- package/debug/settings-page.d.ts +9 -0
- package/debug/settings-page.js +116 -0
- package/debug/settings-page.js.map +1 -0
- package/generator-adapter/generators/app/index.js +337 -0
- package/generator-adapter/generators/app/templates/CHANGELOG.md +0 -0
- package/generator-adapter/generators/app/templates/README.md +3 -0
- package/generator-adapter/generators/app/templates/babel.config.js +3 -0
- package/generator-adapter/generators/app/templates/jest.config.js +3 -0
- package/generator-adapter/generators/app/templates/package.json +30 -0
- package/generator-adapter/generators/app/templates/src/config/index.ts +25 -0
- package/generator-adapter/generators/app/templates/src/config/overrides.json +3 -0
- package/generator-adapter/generators/app/templates/src/endpoint/base.ts.ejs +21 -0
- package/generator-adapter/generators/app/templates/src/endpoint/endpoint-router.ts.ejs +33 -0
- package/generator-adapter/generators/app/templates/src/endpoint/endpoint.ts.ejs +26 -0
- package/generator-adapter/generators/app/templates/src/endpoint/index.ts.ejs +1 -0
- package/generator-adapter/generators/app/templates/src/index.ts.ejs +21 -0
- package/generator-adapter/generators/app/templates/src/transport/custom.ts.ejs +87 -0
- package/generator-adapter/generators/app/templates/src/transport/http.ts.ejs +98 -0
- package/generator-adapter/generators/app/templates/src/transport/ws.ts.ejs +94 -0
- package/generator-adapter/generators/app/templates/test/adapter-ws.test.ts.ejs +58 -0
- package/generator-adapter/generators/app/templates/test/adapter.test.ts.ejs +50 -0
- package/generator-adapter/generators/app/templates/test/fixtures.ts.ejs +44 -0
- package/generator-adapter/generators/app/templates/test-payload.json +6 -0
- package/generator-adapter/generators/app/templates/tsconfig.base.json +40 -0
- package/generator-adapter/generators/app/templates/tsconfig.json +9 -0
- package/generator-adapter/generators/app/templates/tsconfig.test.json +7 -0
- package/generator-adapter/package.json +12 -0
- package/index.js +6 -0
- package/index.js.map +1 -1
- package/package.json +11 -5
|
@@ -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
|
+
};
|
|
File without changes
|
|
@@ -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,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()
|