diff --git a/.pnp.cjs b/.pnp.cjs index 8b9e2eaf84..78da08919a 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -8182,7 +8182,9 @@ const RAW_RUNTIME_STATE = ["@chainlink/external-adapter-framework", "npm:2.7.0"],\ ["@types/jest", "npm:29.5.14"],\ ["@types/node", "npm:22.14.1"],\ + ["@types/node-schedule", "npm:2.1.7"],\ ["nock", "npm:13.5.6"],\ + ["node-schedule", "npm:2.1.1"],\ ["tslib", "npm:2.4.1"],\ ["typescript", "patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5"]\ ],\ diff --git a/packages/sources/nomia/package.json b/packages/sources/nomia/package.json index 948fd9bf5a..0c63b09735 100644 --- a/packages/sources/nomia/package.json +++ b/packages/sources/nomia/package.json @@ -30,11 +30,13 @@ "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "22.14.1", + "@types/node-schedule": "2.1.7", "nock": "13.5.6", "typescript": "5.8.3" }, "dependencies": { "@chainlink/external-adapter-framework": "2.7.0", + "node-schedule": "2.1.1", "tslib": "2.4.1" } } diff --git a/packages/sources/nomia/src/config/index.ts b/packages/sources/nomia/src/config/index.ts index 17e0219ce9..ea80b7d4d6 100644 --- a/packages/sources/nomia/src/config/index.ts +++ b/packages/sources/nomia/src/config/index.ts @@ -14,11 +14,16 @@ export const config = new AdapterConfig( required: true, sensitive: true, }, + API_FREQUENCY: { + description: 'Frequency of API calls in minutes', + type: 'number', + default: 20, + required: false, + }, }, { envDefaultOverrides: { CACHE_MAX_AGE: 20 * 60 * 1000, // 20 minutes - max validated setting - BACKGROUND_EXECUTE_TIMEOUT: 180_000, }, }, ) diff --git a/packages/sources/nomia/src/endpoint/price.ts b/packages/sources/nomia/src/endpoint/price.ts index 3d1333686a..552a118145 100644 --- a/packages/sources/nomia/src/endpoint/price.ts +++ b/packages/sources/nomia/src/endpoint/price.ts @@ -2,7 +2,7 @@ import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util' import { InputParameters } from '@chainlink/external-adapter-framework/validation' import { config } from '../config' -import { httpTransport } from '../transport/price' +import { nomiaTransport } from '../transport/price' export const inputParameters = new InputParameters({ query: { @@ -27,6 +27,6 @@ export type BaseEndpointTypes = { export const endpoint = new AdapterEndpoint({ name: 'price', aliases: [], - transport: httpTransport, + transport: nomiaTransport, inputParameters, }) diff --git a/packages/sources/nomia/src/index.ts b/packages/sources/nomia/src/index.ts index 7c8c730be3..e1e816f28d 100644 --- a/packages/sources/nomia/src/index.ts +++ b/packages/sources/nomia/src/index.ts @@ -10,7 +10,7 @@ export const adapter = new Adapter({ rateLimiting: { tiers: { default: { - rateLimit1m: 2, + rateLimit1m: 10, }, }, }, diff --git a/packages/sources/nomia/src/transport/price.ts b/packages/sources/nomia/src/transport/price.ts index b3e2e5bd33..fd5b20b5c3 100644 --- a/packages/sources/nomia/src/transport/price.ts +++ b/packages/sources/nomia/src/transport/price.ts @@ -1,6 +1,11 @@ -import { HttpTransport } from '@chainlink/external-adapter-framework/transports' +import { EndpointContext } from '@chainlink/external-adapter-framework/adapter' +import { calculateHttpRequestKey } from '@chainlink/external-adapter-framework/cache' +import { TransportDependencies } from '@chainlink/external-adapter-framework/transports' +import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription' import { makeLogger } from '@chainlink/external-adapter-framework/util' -import { BaseEndpointTypes } from '../endpoint/price' +import { Requester } from '@chainlink/external-adapter-framework/util/requester' +import schedule from 'node-schedule' +import { BaseEndpointTypes, inputParameters } from '../endpoint/price' const logger = makeLogger('nomia') @@ -13,68 +18,122 @@ export interface ResponseSchema { TableName: string TimePeriod: string }[] + Error?: { + APIErrorCode: string + APIErrorDescription: string + } } } } -export type HttpTransportTypes = BaseEndpointTypes & { - Provider: { - RequestBody: never - ResponseBody: ResponseSchema +type RequestParams = typeof inputParameters.validated + +export type NomiaTransportTypes = BaseEndpointTypes + +export class NomiaTransport extends SubscriptionTransport { + settings!: NomiaTransportTypes['Settings'] + requester!: Requester + endpointName!: string + params!: RequestParams[] + + async initialize( + dependencies: TransportDependencies, + adapterSettings: NomiaTransportTypes['Settings'], + endpointName: string, + transportName: string, + ): Promise { + await super.initialize(dependencies, adapterSettings, endpointName, transportName) + this.settings = adapterSettings + this.requester = dependencies.requester + this.endpointName = endpointName + this.runScheduler() } -} -export const httpTransport = new HttpTransport({ - prepareRequests: (params, config) => { - return params.map((param) => { - const query = new URLSearchParams(param.query) - query.set('UserID', config.API_KEY) - - const currentYear = new Date().getFullYear() - const lastYear = currentYear - 1 - const yearValue = param.singleYear ? currentYear : `${lastYear},${currentYear}` - - const decodedQuery = `${query.toString()}&Year=${yearValue}` - return { - params: [param], - request: { - baseURL: `${config.API_ENDPOINT}?${decodedQuery}`, - }, + + async backgroundHandler(_: EndpointContext, entries: RequestParams[]) { + this.params = entries + } + + runScheduler() { + schedule.scheduleJob(`0 */${this.settings.API_FREQUENCY} * * * *`, async () => { + logger.info(`Scheduled execution started at ${Date.now()}`) + for (let i = 0; i < this.params.length; i++) { + await this.executeRequest(this.params[i]) } }) - }, - parseResponse: (params, response) => { - const data = Object.values(response.data) - if (!data || !data[0] || !data[0].Results.Data?.length) { - logger.error('No data found in response', response.data) - return [] - } + } - return params.map((param) => { - const t = new URLSearchParams(param.query) - const record = data[0].Results.Data.filter( - (d) => d.TableName === t.get('TableName') && d.LineNumber === t.get('LineNumber'), - ).reduce((a, b) => (a.TimePeriod > b.TimePeriod ? a : b)) - if (!record || !record.DataValue) { - return { - params: param, - response: { - statusCode: 502, - errorMessage: - 'Record not found or DataValue is empty. Please check the query parameters.', - }, - } - } - const result = Number(record.DataValue.replace(/,/g, '')) // Remove commas for parsing as a number - - return { - params: param, - response: { - result: result, - data: { - result: result, - }, + getSubscriptionTtlFromConfig(adapterSettings: NomiaTransportTypes['Settings']): number { + return adapterSettings.WARMUP_SUBSCRIPTION_TTL + } + + async executeRequest(param: RequestParams) { + const providerDataRequestedUnixMs = Date.now() + const apiResponse = await this.makeRequest(param) + const providerDataReceivedUnixMs = Date.now() + + const data = Object.values(apiResponse.data) + if (!data || !data[0] || !data[0].Results.Data?.length) { + const response = { + errorMessage: + data[0].Results?.Error?.APIErrorDescription || 'No data returned from provider', + statusCode: 502, + timestamps: { + providerDataRequestedUnixMs, + providerDataReceivedUnixMs, + providerIndicatedTimeUnixMs: undefined, }, } + await this.responseCache.write(this.name, [{ params: param, response }]) + return + } + const t = new URLSearchParams(param.query) + const record = data[0].Results.Data.filter( + (d) => d.TableName === t.get('TableName') && d.LineNumber === t.get('LineNumber'), + ).reduce((a, b) => (a.TimePeriod > b.TimePeriod ? a : b)) + + const result = Number(record.DataValue.replace(/,/g, '')) // Remove commas for parsing as a number + const response = { + data: { + result, + }, + result, + statusCode: 200, + timestamps: { + providerDataRequestedUnixMs, + providerDataReceivedUnixMs, + providerIndicatedTimeUnixMs: undefined, + }, + } + await this.responseCache.write(this.name, [{ params: param, response }]) + } + + async makeRequest(param: RequestParams) { + const query = new URLSearchParams(param.query) + query.set('UserID', this.settings.API_KEY) + + const currentYear = new Date().getFullYear() + const lastYear = currentYear - 1 + const yearValue = param.singleYear ? currentYear : `${lastYear},${currentYear}` + + const decodedQuery = `${query.toString()}&Year=${yearValue}` + const requestConfig = { + baseURL: this.settings.API_ENDPOINT, + url: `${this.settings.API_ENDPOINT}?${decodedQuery}`, + } + + const reqKey = calculateHttpRequestKey({ + context: { + adapterSettings: this.settings, + inputParameters, + endpointName: this.endpointName, + }, + data: requestConfig, + transportName: this.name, }) - }, -}) + + const { response } = await this.requester.request(reqKey, requestConfig) + return response + } +} + +export const nomiaTransport = new NomiaTransport() diff --git a/yarn.lock b/yarn.lock index f08c986ab6..2194131eef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5094,7 +5094,9 @@ __metadata: "@chainlink/external-adapter-framework": "npm:2.7.0" "@types/jest": "npm:^29.5.14" "@types/node": "npm:22.14.1" + "@types/node-schedule": "npm:2.1.7" nock: "npm:13.5.6" + node-schedule: "npm:2.1.1" tslib: "npm:2.4.1" typescript: "npm:5.8.3" languageName: unknown