Skip to content

Commit

Permalink
addd custom subscription transport to ea generator (#241)
Browse files Browse the repository at this point in the history
  • Loading branch information
karen-stepanyan authored Feb 5, 2024
1 parent 0975737 commit e2b09c3
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 18 deletions.
58 changes: 41 additions & 17 deletions scripts/generator-adapter/generators/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,7 @@ module.exports = class extends Generator<{rootPath: string}> {
this.props
)

// Copy config
this.fs.copy(
this.templatePath('src/config/index.ts'),
this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/config/index.ts`),
)
// Copy overrides
this.fs.copyTpl(
this.templatePath('src/config/overrides.json'),
this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/config/overrides.json`),
Expand Down Expand Up @@ -192,25 +188,32 @@ module.exports = class extends Generator<{rootPath: string}> {
{ endpoints: Object.values(this.props.endpoints) },
)


// Create test files
const httpEndpoints = Object.values(this.props.endpoints).filter((e: GeneratorEndpointContext) => e.inputTransports.some(t => t.type === 'http'))
const wsEndpoints = Object.values(this.props.endpoints).filter((e: GeneratorEndpointContext) => e.inputTransports.some(t => t.type === 'ws'))
const customEndpoints = Object.values(this.props.endpoints).filter((e: GeneratorEndpointContext) => e.inputTransports.some(t => t.type === 'custom'))
const customFgEndpoints = Object.values(this.props.endpoints).filter((e: GeneratorEndpointContext) => e.inputTransports.some(t => t.type === 'customfg'))
const customBgEndpoints = Object.values(this.props.endpoints).filter((e: GeneratorEndpointContext) => e.inputTransports.some(t => t.type === 'custombg'))

// Copy config
this.fs.copyTpl(
this.templatePath('src/config/index.ts.ejs'),
this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/src/config/index.ts`),
{setBgExecuteMsEnv: customBgEndpoints.length}
)

// Create test files
// Create adapter.test.ts if there is at least one endpoint with httpTransport
if (httpEndpoints.length) {
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' },
{ endpoints: httpEndpoints, transportName: 'rest', setBgExecuteMsEnv: false },
)
}

// Create adapter.test.ts or adapter-ws.test.ts if there is at least one endpoint with wsTransport
if (wsEndpoints.length) {
let fileName = 'adapter.test.ts'
if (httpEndpoints.length || customEndpoints.length) {
if (httpEndpoints.length || customFgEndpoints.length || customBgEndpoints.length) {
fileName = 'adapter-ws.test.ts'
}
this.fs.copyTpl(
Expand All @@ -224,15 +227,26 @@ module.exports = class extends Generator<{rootPath: string}> {
// Custom transport integration tests use the same template as http, but in separate file. This is not ideal
// since the setup is the same (usually) and we could have just another test describe block, but at least this is
// consistent behavior as each transport-specific test is in its own file.
if (customEndpoints.length) {
if (customFgEndpoints.length) {
let fileName = 'adapter.test.ts'
if (httpEndpoints.length || wsEndpoints.length) {
fileName = 'adapter-custom.test.ts'
fileName = 'adapter-custom-fg.test.ts'
}
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' },
{ endpoints: customFgEndpoints, transportName: 'customfg', setBgExecuteMsEnv: false },
)
}
if (customBgEndpoints.length) {
let fileName = 'adapter.test.ts'
if (httpEndpoints.length || wsEndpoints.length || customFgEndpoints.length) {
fileName = 'adapter-custom-bg.test.ts'
}
this.fs.copyTpl(
this.templatePath(`test/adapter.test.ts.ejs`),
this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/test/integration/${fileName}`),
{ endpoints: customBgEndpoints, transportName: 'custombg', setBgExecuteMsEnv: true },
)
}

Expand All @@ -242,7 +256,7 @@ module.exports = class extends Generator<{rootPath: string}> {
this.destinationPath(`${this.options.rootPath}/${this.props.adapterName}/test/integration/fixtures.ts`),
{
includeWsFixtures: wsEndpoints.length > 0,
includeHttpFixtures: httpEndpoints.length > 0 || customEndpoints.length > 0,
includeHttpFixtures: httpEndpoints.length > 0 || customBgEndpoints.length > 0 || customFgEndpoints.length > 0,
},
)

Expand Down Expand Up @@ -383,7 +397,10 @@ module.exports = class extends Generator<{rootPath: string}> {
private async _promptTransports(inputEndpointName: string): Promise<InputTransport[]> {
if (this.promptDisabled) {
return [
{type: 'http', name: 'httpTransport'}, { type: 'ws', name: 'wsTransport' }, {type: 'custom', name: 'customTransport',}
{type: 'http', name: 'httpTransport'},
{ type: 'ws', name: 'wsTransport' },
{type: 'customfg' , name: 'customTransport'},
{type: 'custombg', name: 'customSubscriptionTransport'}
]
}
const { inputTransports } = await this.prompt<{ inputTransports: InputTransport[] }>({
Expand All @@ -407,9 +424,16 @@ module.exports = class extends Generator<{rootPath: string}> {
},
},
{
name: 'Custom',
name: 'Custom (subscription based)',
value: {
type: 'custombg',
name: 'customSubscriptionTransport',
},
},
{
name: 'Custom (simple)',
value: {
type: 'custom',
type: 'customfg',
name: 'customTransport',
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,12 @@ export const config = new AdapterConfig(
type: 'string',
default: 'ws://localhost:9090',
},
<% if (setBgExecuteMsEnv) { %>
BACKGROUND_EXECUTE_MS: {
description:
'The amount of time the background execute should sleep before performing the next request',
type: 'number',
default: 10_000,
},<% } %>
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const adapter = new Adapter({
<% } -%><%= ' ' %> defaultEndpoint: <%= defaultEndpoint.normalizedEndpointName %>.name,
<% if (includeComments) { -%>
// Adapter name
<% } -%><%= ' ' %> name: '<%= adapterName.toUpperCase() %>',
<% } -%><%= ' ' %> name: '<%= adapterName.toUpperCase().replace('-', '_') %>',
<% if (includeComments) { -%>
// Adapter configuration (environment variables)
<% } -%><%= ' ' %> config,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response'
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
import {
AdapterResponse, sleep
} from '@chainlink/external-adapter-framework/util'
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
import { BaseEndpointTypes, inputParameters } from '../endpoint/<%= inputEndpointName %>'

type RequestParams = typeof inputParameters.validated

<% if (includeComments) { -%>
// CustomTransport extends base types from endpoint and adds additional, Provider-specific types (if needed).
<% } -%>
export type CustomTransportTypes = BaseEndpointTypes & {
Provider: {
RequestBody: never
ResponseBody: any
}
}
<% if (includeComments) { -%>
// CustomTransport is used to perform custom data fetching and processing from a Provider. The framework provides built-in transports to
// fetch data from a Provider using several protocols, including `http`, `websocket`, and `sse`. Use CustomTransport when the Provider uses
// different protocol, or you need custom functionality that built-in transports don't support. For example, custom, multistep authentication
// for requests, paginated requests, on-chain data retrieval using third party libraries, and so on.
<% } -%>
export class CustomTransport extends SubscriptionTransport<CustomTransportTypes> {
<% if (includeComments) { -%>
// name of the transport, used for logging
<% } -%>
name!: string
<% if (includeComments) { -%>
// cache instance for caching responses from provider
<% } -%>
responseCache!: ResponseCache<CustomTransportTypes>
<% if (includeComments) { -%>
// instance of Requester to be used for data fetching. Use this instance to perform http calls
<% } -%>
requester!: Requester

<% if (includeComments) { -%>
// REQUIRED. Transport will be automatically initialized by the framework using this method. It will be called with transport
// dependencies, adapter settings, endpoint name, and transport name as arguments. Use this method to initialize transport state
<% } -%>
async initialize(dependencies: TransportDependencies<CustomTransportTypes>, adapterSettings: CustomTransportTypes['Settings'], endpointName: string, transportName: string): Promise<void> {
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
this.requester = dependencies.requester
}
<% if (includeComments) { -%>
// 'backgroundHandler' is called on each background execution iteration. It receives endpoint context as first argument
// and an array of all the entries in the subscription set as second argument. Use this method to handle the incoming
// request, process it and save it in the cache.
<% } -%>
async backgroundHandler(context: EndpointContext<CustomTransportTypes>, entries: RequestParams[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
}

async handleRequest(param: RequestParams) {
let response: AdapterResponse<CustomTransportTypes['Response']>
try {
response = await this._handleRequest(param)
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
response = {
statusCode: 502,
errorMessage,
timestamps: {
providerDataRequestedUnixMs: 0,
providerDataReceivedUnixMs: 0,
providerIndicatedTimeUnixMs: undefined,
},
}
}
await this.responseCache.write(this.name, [{ params: param, response }])
}

async _handleRequest(
_: RequestParams,
): Promise<AdapterResponse<CustomTransportTypes['Response']>> {

const providerDataRequestedUnixMs = Date.now()

// custom transport logic

return {
data: {
result: 2000,
},
statusCode: 200,
result: 2000,
timestamps: {
providerDataRequestedUnixMs,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: undefined,
},
}
}

getSubscriptionTtlFromConfig(adapterSettings: CustomTransportTypes['Settings']): number {
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
}
}

export const customSubscriptionTransport = new CustomTransport()
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('execute', () => {
beforeAll(async () => {
oldEnv = JSON.parse(JSON.stringify(process.env))
process.env.API_KEY = process.env.API_KEY ?? 'fake-api-key'
<% if (setBgExecuteMsEnv) { %>process.env.BACKGROUND_EXECUTE_MS = process.env.BACKGROUND_EXECUTE_MS ?? '0'<% } %>
const mockDate = new Date('2001-01-01T11:11:11.111Z')
spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())
Expand Down

0 comments on commit e2b09c3

Please sign in to comment.