From 507c4f9b9ca6bb3b8f5cdcbe874895fa534b3542 Mon Sep 17 00:00:00 2001 From: Alex Kuznicki Date: Fri, 31 Jan 2025 18:24:00 -0700 Subject: [PATCH] refactor adapter to have price/lwba endoints --- .../glv-token/src/endpoint/index.ts | 1 + .../composites/glv-token/src/endpoint/lwba.ts | 38 ++ .../glv-token/src/endpoint/price.ts | 4 +- packages/composites/glv-token/src/index.ts | 4 +- .../glv-token/src/transport/base.ts | 373 +++++++++++++++++ .../glv-token/src/transport/lwba.ts | 44 ++ .../glv-token/src/transport/price.ts | 376 ++---------------- 7 files changed, 483 insertions(+), 357 deletions(-) create mode 100644 packages/composites/glv-token/src/endpoint/lwba.ts create mode 100644 packages/composites/glv-token/src/transport/base.ts create mode 100644 packages/composites/glv-token/src/transport/lwba.ts diff --git a/packages/composites/glv-token/src/endpoint/index.ts b/packages/composites/glv-token/src/endpoint/index.ts index 11a44912b4..ac9492a769 100644 --- a/packages/composites/glv-token/src/endpoint/index.ts +++ b/packages/composites/glv-token/src/endpoint/index.ts @@ -1 +1,2 @@ export { endpoint as price } from './price' +export { endpoint as lwba } from './lwba' diff --git a/packages/composites/glv-token/src/endpoint/lwba.ts b/packages/composites/glv-token/src/endpoint/lwba.ts new file mode 100644 index 0000000000..7d5e3067df --- /dev/null +++ b/packages/composites/glv-token/src/endpoint/lwba.ts @@ -0,0 +1,38 @@ +import { + AdapterEndpoint, + LwbaResponseDataFields, +} from '@chainlink/external-adapter-framework/adapter' +import { InputParameters } from '@chainlink/external-adapter-framework/validation' +import { config } from '../config' +import { glvLwbaTransport } from '../transport/lwba' + +export const inputParameters = new InputParameters( + { + glv: { + required: true, + type: 'string', + description: 'Glv address', + }, + }, + [ + { + glv: '0x528A5bac7E746C9A509A1f4F6dF58A03d44279F9', + }, + ], +) + +export type BaseEndpointTypesLwba = { + Parameters: typeof inputParameters.definition + Response: LwbaResponseDataFields & { + Data: { + sources: Record + } + } + Settings: typeof config.settings +} + +export const endpoint = new AdapterEndpoint({ + name: 'crypto-lwba', + transport: glvLwbaTransport, + inputParameters, +}) diff --git a/packages/composites/glv-token/src/endpoint/price.ts b/packages/composites/glv-token/src/endpoint/price.ts index 14d9e27028..cf8b9b7cf4 100644 --- a/packages/composites/glv-token/src/endpoint/price.ts +++ b/packages/composites/glv-token/src/endpoint/price.ts @@ -2,7 +2,7 @@ import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' import { InputParameters } from '@chainlink/external-adapter-framework/validation' import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util' import { config } from '../config' -import { glvTokenTransport } from '../transport/price' +import { glvPriceTransport } from '../transport/price' export const inputParameters = new InputParameters( { @@ -31,6 +31,6 @@ export type BaseEndpointTypes = { export const endpoint = new AdapterEndpoint({ name: 'price', - transport: glvTokenTransport, + transport: glvPriceTransport, inputParameters, }) diff --git a/packages/composites/glv-token/src/index.ts b/packages/composites/glv-token/src/index.ts index e0d17dd88c..ecd1242480 100644 --- a/packages/composites/glv-token/src/index.ts +++ b/packages/composites/glv-token/src/index.ts @@ -1,13 +1,13 @@ import { expose, ServerInstance } from '@chainlink/external-adapter-framework' import { Adapter } from '@chainlink/external-adapter-framework/adapter' import { config } from './config' -import { price } from './endpoint' +import { lwba, price } from './endpoint' export const adapter = new Adapter({ defaultEndpoint: price.name, name: 'GLV_TOKEN', config, - endpoints: [price], + endpoints: [price, lwba], }) export const server = (): Promise => expose(adapter) diff --git a/packages/composites/glv-token/src/transport/base.ts b/packages/composites/glv-token/src/transport/base.ts new file mode 100644 index 0000000000..e2b15021f0 --- /dev/null +++ b/packages/composites/glv-token/src/transport/base.ts @@ -0,0 +1,373 @@ +import { ethers, utils } from 'ethers' +import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription' +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 { + EndpointContext, + LwbaResponseDataFields, +} from '@chainlink/external-adapter-framework/adapter' +import { AdapterResponse, makeLogger } from '@chainlink/external-adapter-framework/util' +import { AdapterDataProviderError } from '@chainlink/external-adapter-framework/validation/error' +import glvAbi from '../config/glvReaderAbi.json' +import { BaseEndpointTypes, inputParameters } from '../endpoint/price' +import { BaseEndpointTypesLwba } from '../endpoint/lwba' +import { + mapParameter, + mapSymbol, + Market, + median, + PriceData, + SIGNED_PRICE_DECIMALS, + Source, + toFixed, + Token, +} from './utils' +import { TypeFromDefinition } from '@chainlink/external-adapter-framework/validation/input-params' + +const logger = makeLogger('GlvBaseTransport') + +interface glvInformation { + glvToken: string + longToken: Token + shortToken: Token + markets: Record +} + +type RequestParams = typeof inputParameters.validated + +/** + * The base class contains all logic that is shared across both + * 'price' and 'lwba' variants. The child transports will override + * `formatResponse()` to produce different output shapes. + */ +export abstract class BaseGlvTransport< + T extends BaseEndpointTypes | BaseEndpointTypesLwba, +> extends SubscriptionTransport { + abstract backgroundHandler( + context: EndpointContext, + entries: TypeFromDefinition[], + ): Promise + + abstract handleRequest(param: TypeFromDefinition): Promise + + name!: string + responseCache!: ResponseCache + requester!: Requester + provider!: ethers.providers.JsonRpcProvider + glvReaderContract!: ethers.Contract + settings!: T['Settings'] + + tokensMap: Record = {} + marketsMap: Record = {} + decimals: Record = {} + + async initialize( + dependencies: TransportDependencies, + adapterSettings: T['Settings'], + endpointName: string, + transportName: string, + ): Promise { + await super.initialize(dependencies, adapterSettings, endpointName, transportName) + this.settings = adapterSettings + this.provider = new ethers.providers.JsonRpcProvider( + adapterSettings.ARBITRUM_RPC_URL, + adapterSettings.ARBITRUM_CHAIN_ID, + ) + this.requester = dependencies.requester + + this.glvReaderContract = new ethers.Contract( + adapterSettings.GLV_READER_CONTRACT_ADDRESS, + glvAbi, + this.provider, + ) + + await this.tokenInfo() + await this.marketInfo() + + if (this.settings.METADATA_REFRESH_INTERVAL_MS > 0) { + setInterval(() => { + this.tokenInfo() + this.marketInfo() + }, this.settings.METADATA_REFRESH_INTERVAL_MS) + } + } + + protected abstract formatResponse( + result: number, + minimizedValue: number, + maximizedValue: number, + sources: Record, + timestamps: { + providerDataRequestedUnixMs: number + providerDataReceivedUnixMs: number + providerIndicatedTimeUnixMs: undefined + }, + ): AdapterResponse + + async tokenInfo() { + const requestConfig = { + url: this.settings.TOKEN_INFO_API, + method: 'GET', + } + + const response = await this.requester.request<{ tokens: Token[] }>( + JSON.stringify(requestConfig), + requestConfig, + ) + + const data: Token[] = response.response.data.tokens + data.map((token) => { + this.tokensMap[token.address] = token + this.decimals[token.symbol] = token.decimals + }) + } + + async marketInfo() { + const requestConfig = { + url: this.settings.MARKET_INFO_API, + method: 'GET', + } + + const response = await this.requester.request<{ markets: Market[] }>( + JSON.stringify(requestConfig), + requestConfig, + ) + + const data: Market[] = response.response.data.markets + data.map((market) => { + this.marketsMap[market.marketToken] = market + }) + } + + async _handleRequest(param: RequestParams): Promise> { + const providerDataRequestedUnixMs = Date.now() + const glv_address = param.glv + + const glvInfo = await this.glvReaderContract.getGlvInfo( + this.settings.DATASTORE_CONTRACT_ADDRESS, + glv_address, + ) + + const glv: glvInformation = { + glvToken: glvInfo.glv.glvToken, + longToken: mapSymbol(glvInfo.glv.longToken, this.tokensMap), + shortToken: mapSymbol(glvInfo.glv.shortToken, this.tokensMap), + markets: {}, + } + + for (let i = 0; i < glvInfo.markets.length; i++) { + glv.markets[glvInfo.markets[i]] = mapSymbol(glvInfo.markets[i], this.marketsMap) + } + + const assets: Array = [glv.longToken.symbol, glv.shortToken.symbol] + Object.keys(glv.markets).forEach((m) => { + assets.push(mapSymbol(glv.markets[m].indexToken, this.tokensMap).symbol) + }) + + assets.sort() + const priceResult = await this.fetchPrices([...new Set(assets)], providerDataRequestedUnixMs) + + const indexTokensPrices: Array[] = [] + Object.keys(glv.markets).forEach((m) => { + const symbol = mapSymbol(glv.markets[m].indexToken, this.tokensMap).symbol + indexTokensPrices.push([priceResult.prices[symbol].bid, priceResult.prices[symbol].ask]) + }) + + const glvTokenPriceContractParams = [ + this.settings.DATASTORE_CONTRACT_ADDRESS, + glvInfo.markets, + indexTokensPrices, + [priceResult.prices[glv.longToken.symbol].bid, priceResult.prices[glv.longToken.symbol].ask], + [ + priceResult.prices[glv.shortToken.symbol].bid, + priceResult.prices[glv.shortToken.symbol].ask, + ], + glv_address, + ] + + const [[maximizedValue], [minimizedValue]] = await Promise.all([ + this.glvReaderContract.getGlvTokenPrice(...glvTokenPriceContractParams, true), + this.glvReaderContract.getGlvTokenPrice(...glvTokenPriceContractParams, false), + ]) + + const maximizedPrice = Number(utils.formatUnits(maximizedValue, SIGNED_PRICE_DECIMALS)) + const minimizedPrice = Number(utils.formatUnits(minimizedValue, SIGNED_PRICE_DECIMALS)) + const result = median([minimizedPrice, maximizedPrice]) + + const timestamps = { + providerDataRequestedUnixMs, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: undefined, + } + + return this.formatResponse( + result, + minimizedPrice, + maximizedPrice, + priceResult.sources, + timestamps, + ) + } + + private async fetchPrices(assets: string[], dataRequestedTimestamp: number) { + const priceData = {} as PriceData + + const sources = [ + { url: this.settings.TIINGO_ADAPTER_URL, name: 'tiingo' }, + { url: this.settings.COINMETRICS_ADAPTER_URL, name: 'coinmetrics' }, + { url: this.settings.NCFX_ADAPTER_URL, name: 'ncfx' }, + ] + + const priceProviders: Record = {} + const promises = [] + + for (let i = 0; i < sources.length; i++) { + const source = sources[i] + const assetPromises = assets.map(async (asset) => { + const mappedAsset = mapParameter(source.name, asset) + const base = this.unwrapAsset(mappedAsset) + const requestConfig = { + url: source.url, + method: 'POST', + data: { + data: { + endpoint: 'crypto-lwba', + base, + quote: 'USD', + }, + }, + } + + try { + const response = await this.requester.request<{ data: LwbaResponseDataFields['Data'] }>( + JSON.stringify(requestConfig), + requestConfig, + ) + const { bid, ask } = response.response.data.data + + priceData[asset] = { + bids: [...(priceData[asset]?.bids || []), bid], + asks: [...(priceData[asset]?.asks || []), ask], + } + + priceProviders[asset] = priceProviders[asset] + ? [...new Set([...priceProviders[asset], source.name])] + : [source.name] + } catch (error) { + const e = error as Error + logger.error( + `Error fetching data for ${asset} from ${source.name}, url - ${source.url}: ${e.message}`, + ) + } + }) + + promises.push(...assetPromises) + } + + await Promise.all(promises) + + this.validateRequiredResponses(priceProviders, sources, assets, dataRequestedTimestamp) + + const medianValues = this.calculateMedian(assets, priceData) + + const prices: Record> = {} + + medianValues.map( + (v) => + (prices[v.asset] = { + ...v, + ask: toFixed(v.ask, this.decimals[v.asset as keyof typeof this.decimals]), + bid: toFixed(v.bid, this.decimals[v.asset as keyof typeof this.decimals]), + }), + ) + + return { + prices, + sources: priceProviders, + } + } + + private calculateMedian(assets: string[], priceData: PriceData) { + return assets.map((asset) => { + const medianBid = median([...new Set(priceData[asset].bids)]) + const medianAsk = median([...new Set(priceData[asset].asks)]) + return { asset, bid: medianBid, ask: medianAsk } + }) + } + + private unwrapAsset(asset: string) { + if (asset === 'WBTC.b') { + return 'BTC' + } + if (asset === 'WETH') { + return 'ETH' + } + return asset + } + + private validateRequiredResponses( + priceProviders: Record = {}, + sources: Source[], + assets: string[], + dataRequestedTimestamp: number, + ) { + const allSource = sources.map((s) => s.name) + if (!Object.entries(priceProviders)?.length) { + throw new AdapterDataProviderError( + { + statusCode: 502, + message: `Missing responses from '${allSource.join(',')}' for all assets.`, + }, + { + providerDataRequestedUnixMs: dataRequestedTimestamp, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: undefined, + }, + ) + } + + assets.forEach((asset) => { + const base = this.unwrapAsset(asset) + const respondedSources = priceProviders[base] + + if (respondedSources.length < this.settings.MIN_REQUIRED_SOURCE_SUCCESS) { + const missingSources = allSource.filter((s) => !respondedSources.includes(s)) + logger.error(`Missing responses from '${missingSources.join(',')}' for asset: ${asset}`) + throw new AdapterDataProviderError( + { + statusCode: 502, + message: `Cannot calculate median price for '${asset}'. At least ${ + this.settings.MIN_REQUIRED_SOURCE_SUCCESS + } EAs are required to provide a response but response was received only from ${ + respondedSources.length + } EA(s). Missing responses from '${missingSources.join(',')}'.`, + }, + { + providerDataRequestedUnixMs: dataRequestedTimestamp, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: undefined, + }, + ) + } + }) + } + + protected handleError(e: unknown): AdapterResponse { + const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred' + logger.error(e, errorMessage) + return { + statusCode: 502, + errorMessage, + timestamps: { + providerDataRequestedUnixMs: 0, + providerDataReceivedUnixMs: 0, + providerIndicatedTimeUnixMs: undefined, + }, + } as AdapterResponse + } + + getSubscriptionTtlFromConfig(adapterSettings: T['Settings']): number { + return adapterSettings.WARMUP_SUBSCRIPTION_TTL + } +} diff --git a/packages/composites/glv-token/src/transport/lwba.ts b/packages/composites/glv-token/src/transport/lwba.ts new file mode 100644 index 0000000000..ea602c220c --- /dev/null +++ b/packages/composites/glv-token/src/transport/lwba.ts @@ -0,0 +1,44 @@ +import { BaseGlvTransport } from './base' +import { EndpointContext } from '@chainlink/external-adapter-framework/adapter' +import { TypeFromDefinition } from '@chainlink/external-adapter-framework/validation/input-params' +import { AdapterResponse, sleep } from '@chainlink/external-adapter-framework/util' +import { BaseEndpointTypesLwba } from '../endpoint/lwba' + +export class GlvLwbaTransport extends BaseGlvTransport { + async backgroundHandler( + context: EndpointContext, + entries: TypeFromDefinition[], + ): Promise { + await Promise.all(entries.map(async (param) => this.handleRequest(param))) + await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS) + } + + async handleRequest( + param: TypeFromDefinition, + ): Promise { + const response = await this._handleRequest(param).catch((e) => this.handleError(e)) + await this.responseCache.write(this.name, [{ params: param, response }]) + } + + protected formatResponse( + result: number, + minimizedValue: number, + maximizedValue: number, + sources: Record, + timestamps: any, + ): AdapterResponse { + return { + data: { + mid: result, + bid: minimizedValue, + ask: maximizedValue, + sources, + }, + statusCode: 200, + result: null, + timestamps, + } + } +} + +export const glvLwbaTransport = new GlvLwbaTransport() diff --git a/packages/composites/glv-token/src/transport/price.ts b/packages/composites/glv-token/src/transport/price.ts index 7332a0ba2d..02b25f4125 100644 --- a/packages/composites/glv-token/src/transport/price.ts +++ b/packages/composites/glv-token/src/transport/price.ts @@ -1,372 +1,42 @@ -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 { BaseEndpointTypes, inputParameters } from '../endpoint/price' -import { ethers, utils } from 'ethers' -import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription' -import { - EndpointContext, - LwbaResponseDataFields, -} from '@chainlink/external-adapter-framework/adapter' -import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util' -import { - toFixed, - median, - PriceData, - SIGNED_PRICE_DECIMALS, - Source, - Token, - Market, - mapSymbol, - mapParameter, -} from './utils' -import glvAbi from '../config/glvReaderAbi.json' -import { AdapterDataProviderError } from '@chainlink/external-adapter-framework/validation/error' - -const logger = makeLogger('GlvToken') - -type RequestParams = typeof inputParameters.validated - -export type GlvTokenTransportTypes = BaseEndpointTypes - -interface glvInformation { - glvToken: string - longToken: Token - shortToken: Token - markets: Record -} - -export class GlvTokenTransport extends SubscriptionTransport { - name!: string - responseCache!: ResponseCache - requester!: Requester - provider!: ethers.providers.JsonRpcProvider - glvReaderContract!: ethers.Contract - settings!: GlvTokenTransportTypes['Settings'] - - tokensMap: Record = {} - marketsMap: Record = {} - decimals: Record = {} - - async initialize( - dependencies: TransportDependencies, - adapterSettings: GlvTokenTransportTypes['Settings'], - endpointName: string, - transportName: string, +import { BaseEndpointTypes } from '../endpoint/price' +import { BaseGlvTransport } from './base' +import { EndpointContext } from '@chainlink/external-adapter-framework/adapter' +import { TypeFromDefinition } from '@chainlink/external-adapter-framework/validation/input-params' +import { AdapterResponse, sleep } from '@chainlink/external-adapter-framework/util' + +export class GlvPriceTransport extends BaseGlvTransport { + async backgroundHandler( + context: EndpointContext, + entries: TypeFromDefinition[], ): Promise { - await super.initialize(dependencies, adapterSettings, endpointName, transportName) - this.settings = adapterSettings - this.provider = new ethers.providers.JsonRpcProvider( - adapterSettings.ARBITRUM_RPC_URL, - adapterSettings.ARBITRUM_CHAIN_ID, - ) - this.requester = dependencies.requester - - this.glvReaderContract = new ethers.Contract( - adapterSettings.GLV_READER_CONTRACT_ADDRESS, - glvAbi, - this.provider, - ) - - await this.tokenInfo() - await this.marketInfo() - - if (this.settings.METADATA_REFRESH_INTERVAL_MS > 0) { - setInterval(() => { - this.tokenInfo() - this.marketInfo() - }, this.settings.METADATA_REFRESH_INTERVAL_MS) - } - } - - async tokenInfo() { - const requestConfig = { - url: this.settings.TOKEN_INFO_API, - method: 'GET', - } - - const response = await this.requester.request<{ tokens: Token[] }>( - JSON.stringify(requestConfig), - requestConfig, - ) - - const data: Token[] = response.response.data.tokens - data.map((token) => { - this.tokensMap[token.address] = token - this.decimals[token.symbol] = token.decimals - }) - } - - async marketInfo() { - const requestConfig = { - url: this.settings.MARKET_INFO_API, - method: 'GET', - } - - const response = await this.requester.request<{ markets: Market[] }>( - JSON.stringify(requestConfig), - requestConfig, - ) - - const data: Market[] = response.response.data.markets - data.map((market) => { - this.marketsMap[market.marketToken] = market - }) - } - - async backgroundHandler(context: EndpointContext, entries: RequestParams[]) { await Promise.all(entries.map(async (param) => this.handleRequest(param))) await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS) } - async handleRequest(param: RequestParams) { + async handleRequest(param: TypeFromDefinition): Promise { let response: AdapterResponse try { response = await this._handleRequest(param) } catch (e) { - const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred' - logger.error(e, errorMessage) - response = { - statusCode: 502, - errorMessage, - timestamps: { - providerDataRequestedUnixMs: 0, - providerDataReceivedUnixMs: 0, - providerIndicatedTimeUnixMs: undefined, - }, - } + response = this.handleError(e) } await this.responseCache.write(this.name, [{ params: param, response }]) } - async _handleRequest( - param: RequestParams, - ): Promise> { - const providerDataRequestedUnixMs = Date.now() - const glv_address = param.glv - - const glvInfo = await this.glvReaderContract.getGlvInfo( - this.settings.DATASTORE_CONTRACT_ADDRESS, - glv_address, - ) - - const glv: glvInformation = { - glvToken: glvInfo.glv.glvToken, - longToken: mapSymbol(glvInfo.glv.longToken, this.tokensMap), - shortToken: mapSymbol(glvInfo.glv.shortToken, this.tokensMap), - markets: {}, - } - - for (let i = 0; i < glvInfo.markets.length; i++) { - glv.markets[glvInfo.markets[i]] = mapSymbol(glvInfo.markets[i], this.marketsMap) - } - - const assets: Array = [glv.longToken.symbol, glv.shortToken.symbol] - Object.keys(glv.markets).forEach((m) => { - assets.push(mapSymbol(glv.markets[m].indexToken, this.tokensMap).symbol) - }) - - assets.sort() - const priceResult = await this.fetchPrices([...new Set(assets)], providerDataRequestedUnixMs) - - const indexTokensPrices: Array[] = [] - Object.keys(glv.markets).forEach((m) => { - const symbol = mapSymbol(glv.markets[m].indexToken, this.tokensMap).symbol - indexTokensPrices.push([priceResult.prices[symbol].ask, priceResult.prices[symbol].bid]) - }) - - const glvTokenPriceContractParams = [ - this.settings.DATASTORE_CONTRACT_ADDRESS, - glvInfo.markets, - indexTokensPrices, - [priceResult.prices[glv.longToken.symbol].ask, priceResult.prices[glv.longToken.symbol].bid], - [ - priceResult.prices[glv.shortToken.symbol].ask, - priceResult.prices[glv.shortToken.symbol].bid, - ], - glv_address, - ] - - // Prices have a spread from min to max. The last param (maximize-true/false) decides whether to maximize the market token price - // or not. We get both values and return the median. - - const [[maximizedValue], [minimizedValue]] = await Promise.all([ - this.glvReaderContract.getGlvTokenPrice(...glvTokenPriceContractParams, true), - this.glvReaderContract.getGlvTokenPrice(...glvTokenPriceContractParams, false), - ]) - - const maximizedPrice = Number(utils.formatUnits(maximizedValue, SIGNED_PRICE_DECIMALS)) - const minimizedPrice = Number(utils.formatUnits(minimizedValue, SIGNED_PRICE_DECIMALS)) - const result = median([minimizedPrice, maximizedPrice]) - + protected formatResponse( + result: number, + _minimizedValue: number, + _maximizedValue: number, + sources: Record, + timestamps: any, + ): AdapterResponse { return { - data: { - result: result, - sources: priceResult.sources, - }, + data: { result, sources }, statusCode: 200, - result: result, - timestamps: { - providerDataRequestedUnixMs, - providerDataReceivedUnixMs: Date.now(), - providerIndicatedTimeUnixMs: undefined, - }, - } - } - - // Fetches the lwba price info from multiple source EAs, calculates the median for bids and asks per asset and fixes the price precision - private async fetchPrices(assets: string[], dataRequestedTimestamp: number) { - // priceData holds raw bid/ask values per asset from source EAs response - const priceData = {} as PriceData - - const sources = [ - { url: this.settings.TIINGO_ADAPTER_URL, name: 'tiingo' }, - { url: this.settings.COINMETRICS_ADAPTER_URL, name: 'coinmetrics' }, - { url: this.settings.NCFX_ADAPTER_URL, name: 'ncfx' }, - ] - - //priceProviders contains assets with a list of sources where asset price was successfully fetched - const priceProviders: Record = {} - - const promises = [] - - for (let i = 0; i < sources.length; i++) { - const source = sources[i] - const assetPromises = assets.map(async (asset) => { - const mappedAsset = mapParameter(source.name, asset) - const base = this.unwrapAsset(mappedAsset) - const requestConfig = { - url: source.url, - method: 'POST', - data: { - data: { - endpoint: 'crypto-lwba', - base, - quote: 'USD', - }, - }, - } - - // try/catch is needed in a case if one of source EAs fails to return a response, - // we will still get and calculate the median price based on responses of remaining EAs (based on MIN_REQUIRED_SOURCE_SUCCESS setting) - try { - const response = await this.requester.request<{ data: LwbaResponseDataFields['Data'] }>( - JSON.stringify(requestConfig), - requestConfig, - ) - const { bid, ask } = response.response.data.data - - priceData[asset] = { - bids: [...(priceData[asset]?.bids || []), bid], - asks: [...(priceData[asset]?.asks || []), ask], - } - - priceProviders[asset] = priceProviders[asset] - ? [...new Set([...priceProviders[asset], source.name])] - : [source.name] - } catch (error) { - const e = error as Error - logger.error( - `Error fetching data for ${asset} from ${source.name}, url - ${source.url}: ${e.message}`, - ) - } - }) - - promises.push(...assetPromises) + result, + timestamps, } - - await Promise.all(promises) - - this.validateRequiredResponses(priceProviders, sources, assets, dataRequestedTimestamp) - - const medianValues = this.calculateMedian(assets, priceData) - - const prices: Record> = {} - - medianValues.map( - (v) => - (prices[v.asset] = { - ...v, - ask: toFixed(v.ask, this.decimals[v.asset as keyof typeof this.decimals]), - bid: toFixed(v.bid, this.decimals[v.asset as keyof typeof this.decimals]), - }), - ) - - return { - prices, - sources: priceProviders, - } - } - - private calculateMedian(assets: string[], priceData: PriceData) { - return assets.map((asset) => { - // Since most of the gm markets have the same long and index tokens, we need to remove duplicate values from duplicate requests - const medianBid = median([...new Set(priceData[asset].bids)]) - const medianAsk = median([...new Set(priceData[asset].asks)]) - return { asset, bid: medianBid, ask: medianAsk } - }) - } - - private unwrapAsset(asset: string) { - if (asset === 'WBTC.b') { - return 'BTC' - } - if (asset === 'WETH') { - return 'ETH' - } - return asset - } - - private validateRequiredResponses( - priceProviders: Record = {}, - sources: Source[], - assets: string[], - dataRequestedTimestamp: number, - ) { - const allSource = sources.map((s) => s.name) - if (!Object.entries(priceProviders)?.length) { - throw new AdapterDataProviderError( - { - statusCode: 502, - message: `Missing responses from '${allSource.join(',')}' for all assets.`, - }, - { - providerDataRequestedUnixMs: dataRequestedTimestamp, - providerDataReceivedUnixMs: Date.now(), - providerIndicatedTimeUnixMs: undefined, - }, - ) - } - - assets.forEach((asset) => { - const base = this.unwrapAsset(asset) - const respondedSources = priceProviders[base] - - if (respondedSources.length < this.settings.MIN_REQUIRED_SOURCE_SUCCESS) { - const missingSources = allSource.filter((s) => !respondedSources.includes(s)) - throw new AdapterDataProviderError( - { - statusCode: 502, - message: `Cannot calculate median price for '${asset}'. At least ${ - this.settings.MIN_REQUIRED_SOURCE_SUCCESS - } EAs are required to provide a response but response was received only from ${ - respondedSources.length - } EA(s). Missing responses from '${missingSources.join(',')}'.`, - }, - { - providerDataRequestedUnixMs: dataRequestedTimestamp, - providerDataReceivedUnixMs: Date.now(), - providerIndicatedTimeUnixMs: undefined, - }, - ) - } - }) - } - - getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number { - return adapterSettings.WARMUP_SUBSCRIPTION_TTL } } -export const glvTokenTransport = new GlvTokenTransport() +export const glvPriceTransport = new GlvPriceTransport()