Skip to content

Commit

Permalink
New market status composite adapter (#3395)
Browse files Browse the repository at this point in the history
* New market status composite adapter

* V3 adapter

* Use custom transport

* Use requester

* PR comments

---------

Co-authored-by: app-token-issuer-data-feeds[bot] <134377064+app-token-issuer-data-feeds[bot]@users.noreply.github.com>
  • Loading branch information
1 parent bbee9bd commit e708db1
Show file tree
Hide file tree
Showing 20 changed files with 671 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/good-points-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/market-status-adapter': major
---

Initial implementation
27 changes: 27 additions & 0 deletions .pnp.cjs

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

Empty file.
3 changes: 3 additions & 0 deletions packages/composites/market-status/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Chainlink External Adapter for market-status

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 market-status`.
47 changes: 47 additions & 0 deletions packages/composites/market-status/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@chainlink/market-status-adapter",
"version": "0.0.0",
"description": "Chainlink market-status adapter.",
"keywords": [
"Chainlink",
"LINK",
"blockchain",
"oracle",
"market-status"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"repository": {
"url": "https://github.com/smartcontractkit/external-adapters-js",
"type": "git"
},
"license": "MIT",
"scripts": {
"clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
"prepack": "yarn build",
"build": "tsc -b",
"server": "node -e 'require(\"./index.js\").server()'",
"server:dist": "node -e 'require(\"./dist/index.js\").server()'",
"start": "yarn server:dist"
},
"devDependencies": {
"@sinonjs/fake-timers": "9.1.2",
"@types/jest": "27.5.2",
"@types/node": "16.18.96",
"@types/sinonjs__fake-timers": "8.1.5",
"@types/supertest": "2.0.16",
"mock-socket": "9.3.1",
"nock": "13.5.4",
"supertest": "6.2.4",
"typescript": "5.0.4"
},
"dependencies": {
"@chainlink/external-adapter-framework": "1.3.0",
"@chainlink/ncfx-adapter": "workspace:*",
"@chainlink/tradinghours-adapter": "workspace:*",
"tslib": "2.4.1"
}
}
11 changes: 11 additions & 0 deletions packages/composites/market-status/src/config/adapters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const adapterNames = ['NCFX', 'TRADINGHOURS'] as const

export type AdapterName = (typeof adapterNames)[number]

// Mapping from market to primary and secondary adapters.
export const marketAdapters: Record<string, Record<'primary' | 'secondary', AdapterName>> = {
__default: {
primary: 'TRADINGHOURS',
secondary: 'NCFX',
},
}
20 changes: 20 additions & 0 deletions packages/composites/market-status/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AdapterConfig } from '@chainlink/external-adapter-framework/config'

export const config = new AdapterConfig({
TRADINGHOURS_ADAPTER_URL: {
description: 'URL of the TradingHours adapter',
type: 'string',
required: true,
},
NCFX_ADAPTER_URL: {
description: 'URL of the NCFX adapter',
type: 'string',
required: true,
},
BACKGROUND_EXECUTE_MS: {
description:
'The amount of time the background execute should sleep before performing the next request',
type: 'number',
default: 1_000,
},
})
1 change: 1 addition & 0 deletions packages/composites/market-status/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { marketStatusEndpoint as marketStatus } from './market-status'
29 changes: 29 additions & 0 deletions packages/composites/market-status/src/endpoint/market-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
MarketStatusEndpoint,
MarketStatusResultResponse,
marketStatusEndpointInputParametersDefinition,
} from '@chainlink/external-adapter-framework/adapter'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'

import { config } from '../config'
import { transport } from '../transport/market-status'

export const inputParameters = new InputParameters(marketStatusEndpointInputParametersDefinition)

export type CompositeMarketStatusResultResponse = MarketStatusResultResponse & {
Data: {
source?: string
}
}

export type BaseEndpointTypes = {
Parameters: typeof inputParameters.definition
Response: CompositeMarketStatusResultResponse
Settings: typeof config.settings
}

export const marketStatusEndpoint = new MarketStatusEndpoint({
name: 'market-status',
transport,
inputParameters,
})
13 changes: 13 additions & 0 deletions packages/composites/market-status/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
import { config } from './config'
import { marketStatus } from './endpoint'

export const adapter = new Adapter({
name: 'MARKET_STATUS',
endpoints: [marketStatus],
defaultEndpoint: marketStatus.name,
config,
})

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
159 changes: 159 additions & 0 deletions packages/composites/market-status/src/transport/market-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import {
EndpointContext,
MarketStatus,
MarketStatusResultResponse,
} from '@chainlink/external-adapter-framework/adapter'
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
import { makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
import { AdapterResponse } from '@chainlink/external-adapter-framework/util/types'

import { AdapterName, marketAdapters } from '../config/adapters'
import { inputParameters } from '../endpoint/market-status'
import type { BaseEndpointTypes } from '../endpoint/market-status'

const logger = makeLogger('MarketStatusTransport')

type MarketStatusResult = {
marketStatus: MarketStatus
providerIndicatedTimeUnixMs: number
source?: AdapterName
}

type RequestParams = typeof inputParameters.validated

export class MarketStatusTransport extends SubscriptionTransport<BaseEndpointTypes> {
requester!: Requester

async initialize(
dependencies: TransportDependencies<BaseEndpointTypes>,
adapterSettings: BaseEndpointTypes['Settings'],
endpointName: string,
transportName: string,
): Promise<void> {
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
this.requester = dependencies.requester
}

async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(context, param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
}

async handleRequest(context: EndpointContext<BaseEndpointTypes>, param: RequestParams) {
const requestedAt = Date.now()

let response: AdapterResponse<BaseEndpointTypes['Response']>
try {
const result = await this._handleRequest(context, param)
response = {
data: {
result: result.marketStatus,
source: result.source,
},
result: result.marketStatus,
statusCode: 200,
timestamps: {
providerDataRequestedUnixMs: requestedAt,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: result.providerIndicatedTimeUnixMs,
},
}
} catch (e) {
const errorMessage = e instanceof Error ? e.message : `Unknown error occurred: ${e}`
logger.error(e, errorMessage)
response = {
statusCode: 502,
errorMessage,
timestamps: {
providerDataRequestedUnixMs: requestedAt,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: undefined,
},
}
}
await this.responseCache.write(this.name, [{ params: param, response }])
}

async _handleRequest(
context: EndpointContext<BaseEndpointTypes>,
param: RequestParams,
): Promise<MarketStatusResult> {
const market = param.market
const adapterNames = marketAdapters[market] ?? marketAdapters.__default

const primaryResponse = await this.sendAdapterRequest(context, adapterNames.primary, market)
if (primaryResponse.marketStatus !== MarketStatus.UNKNOWN) {
return primaryResponse
}

logger.warn(
`Primary adapter ${adapterNames.primary} for market ${market} returned unknown market status`,
)

const secondaryResponse = await this.sendAdapterRequest(context, adapterNames.secondary, market)
if (secondaryResponse.marketStatus !== MarketStatus.UNKNOWN) {
return secondaryResponse
}

logger.error(
`Secondary adapter ${adapterNames.secondary} for market ${market} returned unknown market status, defaulting to CLOSED`,
)

return {
marketStatus: MarketStatus.CLOSED,
providerIndicatedTimeUnixMs: Date.now(),
}
}

async sendAdapterRequest(
context: EndpointContext<BaseEndpointTypes>,
adapterName: AdapterName,
market: string,
): Promise<MarketStatusResult> {
const key = `${market}:${adapterName}`
const config = {
method: 'POST',
baseURL: context.adapterSettings[`${adapterName}_ADAPTER_URL`],
data: {
data: {
endpoint: 'market-status',
market,
},
},
}

try {
const resp = await this.requester.request<AdapterResponse<MarketStatusResultResponse>>(
key,
config,
)
if (resp.response.status === 200) {
return {
marketStatus: resp.response.data?.result ?? MarketStatus.UNKNOWN,
providerIndicatedTimeUnixMs:
resp.response.data?.timestamps?.providerIndicatedTimeUnixMs ?? Date.now(),
source: adapterName,
}
} else {
logger.error(
`Request to ${adapterName} for market ${market} got status ${resp.response.status}: ${resp.response.data}`,
)
}
} catch (e) {
logger.error(`Request to ${adapterName} for market ${market} failed: ${e}`)
}

return {
marketStatus: MarketStatus.UNKNOWN,
providerIndicatedTimeUnixMs: Date.now(),
}
}

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

export const transport = new MarketStatusTransport()
7 changes: 7 additions & 0 deletions packages/composites/market-status/test-payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"requests": [{
"market": "forex"
}, {
"market": "metals"
}]
}
Loading

0 comments on commit e708db1

Please sign in to comment.