-
Notifications
You must be signed in to change notification settings - Fork 308
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
New market status composite adapter (#3395)
* 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
Showing
20 changed files
with
671 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@chainlink/market-status-adapter': major | ||
--- | ||
|
||
Initial implementation |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
29
packages/composites/market-status/src/endpoint/market-status.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
159
packages/composites/market-status/src/transport/market-status.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"requests": [{ | ||
"market": "forex" | ||
}, { | ||
"market": "metals" | ||
}] | ||
} |
Oops, something went wrong.