Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/sources/nomia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
7 changes: 6 additions & 1 deletion packages/sources/nomia/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ export const config = new AdapterConfig(
required: true,
sensitive: true,
},
API_FREQUENCY: {
description: 'Frequency of API calls in minutes',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be per minute?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is for CRON, can be minutes or seconds I don't have preference

type: 'number',
default: 20,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the default 2, as we know that works

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since there is no background process limiting here, why not 20 minutes for 6 requests ? essentially 3.3 minute per request

required: false,
},
},
{
envDefaultOverrides: {
CACHE_MAX_AGE: 20 * 60 * 1000, // 20 minutes - max validated setting
BACKGROUND_EXECUTE_TIMEOUT: 180_000,
},
},
)
4 changes: 2 additions & 2 deletions packages/sources/nomia/src/endpoint/price.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -27,6 +27,6 @@ export type BaseEndpointTypes = {
export const endpoint = new AdapterEndpoint({
name: 'price',
aliases: [],
transport: httpTransport,
transport: nomiaTransport,
inputParameters,
})
2 changes: 1 addition & 1 deletion packages/sources/nomia/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const adapter = new Adapter({
rateLimiting: {
tiers: {
default: {
rateLimit1m: 2,
rateLimit1m: 10,
},
},
},
Expand Down
171 changes: 115 additions & 56 deletions packages/sources/nomia/src/transport/price.ts
Original file line number Diff line number Diff line change
@@ -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')

Expand All @@ -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<NomiaTransportTypes> {
settings!: NomiaTransportTypes['Settings']
requester!: Requester
endpointName!: string
params!: RequestParams[]

async initialize(
dependencies: TransportDependencies<NomiaTransportTypes>,
adapterSettings: NomiaTransportTypes['Settings'],
endpointName: string,
transportName: string,
): Promise<void> {
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
this.settings = adapterSettings
this.requester = dependencies.requester
this.endpointName = endpointName
this.runScheduler()
}
}
export const httpTransport = new HttpTransport<HttpTransportTypes>({
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<BaseEndpointTypes>, entries: RequestParams[]) {
this.params = entries
}

runScheduler() {
schedule.scheduleJob(`0 */${this.settings.API_FREQUENCY} * * * *`, async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How often does this make it run the cron? The superstate EA uses a const rule = new schedule.RecurrenceRule() I would be in favour of doing the same.

Copy link
Contributor Author

@karen-stepanyan karen-stepanyan Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this runs every this.settings.API_FREQUENCY minutes. superstate uses slightly different logic (runs at a specific time per day) so that's why it's different rule.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Karen explained, standard cron syntax

logger.info(`Scheduled execution started at ${Date.now()}`)
for (let i = 0; i < this.params.length; i++) {
await this.executeRequest(this.params[i])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this call the subscriptions one after the other? Does it wait 60/API_FREQUENCY seconds?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, so this is make a burst of requests (once the previous one is fetched)

}
})
},
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<BaseEndpointTypes>({
context: {
adapterSettings: this.settings,
inputParameters,
endpointName: this.endpointName,
},
data: requestConfig,
transportName: this.name,
})
},
})

const { response } = await this.requester.request<ResponseSchema>(reqKey, requestConfig)
return response
}
}

export const nomiaTransport = new NomiaTransport()
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading