Skip to content
Open
5 changes: 5 additions & 0 deletions .changeset/two-numbers-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/multi-address-list-adapter': minor
---

added address-debug endpoint
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PoRAddressEndpoint } from '@chainlink/external-adapter-framework/adapter/por'
import { addressDebugTransport } from '../transport/address-debug'
import { customInputValidation, inputParameters } from './address'

/**
* This endpoint is meant to be used for debug/diagnostic
* purposes and not for production feeds.
* Additionally, this endpoint will not contain a
* meta field in its response.
*/
export const endpoint = new PoRAddressEndpoint({
name: 'address-debug',
transport: addressDebugTransport,
inputParameters,
customInputValidation,
})
58 changes: 35 additions & 23 deletions packages/composites/multi-address-list/src/endpoint/address.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { config } from '../config'
import { addressListTransport } from '../transport/address'
import { walletParameters as anchorageParams } from '@chainlink/anchorage-adapter'
import { walletParameters as bitGoParams } from '@chainlink/bitgo-adapter'
import { walletParameters as coinbasePrimeParams } from '@chainlink/coinbase-prime-adapter'
import {
PoRAddressEndpoint,
PoRAddressResponse,
} from '@chainlink/external-adapter-framework/adapter/por'
import { walletParameters as anchorageParams } from '@chainlink/anchorage-adapter'
import { walletParameters as coinbasePrimeParams } from '@chainlink/coinbase-prime-adapter'
import { walletParameters as bitGoParams } from '@chainlink/bitgo-adapter'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
import { config } from '../config'
import { addressListTransport } from '../transport/address'

export const inputParameters = new InputParameters({
chainId: {
Expand Down Expand Up @@ -63,25 +63,37 @@ export type BaseEndpointTypes = {
Settings: typeof config.settings
}

type RequestType = {
requestContext: {
data: typeof inputParameters.validated
}
}

export const customInputValidation = (
request: RequestType,
adapterSettings: BaseEndpointTypes['Settings'],
): AdapterInputError | undefined => {
// Check if the required environment variables for source EAs are set.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { chainId, network, ...sources } = request.requestContext.data

Object.keys(sources).forEach((source) => {
const envName = `${source.toUpperCase()}_ADAPTER_URL` as keyof typeof adapterSettings
const params = sources[source as keyof typeof sources]
if (params && !adapterSettings[envName]) {
throw new AdapterInputError({
statusCode: 400,
message: `Error: missing environment variable ${envName}`,
})
}
return
})
return
}

export const endpoint = new PoRAddressEndpoint({
name: 'address',
transport: addressListTransport,
inputParameters,
customInputValidation: (request, adapterSettings): AdapterInputError | undefined => {
// Check if the required environment variables for source EAs are set.
const { chainId, network, ...sources } = request.requestContext.data

Object.keys(sources).forEach((source) => {
const envName = `${source.toUpperCase()}_ADAPTER_URL` as keyof typeof adapterSettings
const params = sources[source as keyof typeof sources]
if (params && !adapterSettings[envName]) {
throw new AdapterInputError({
statusCode: 400,
message: `Error: missing environment variable ${envName}`,
})
}
return
})
return
},
customInputValidation,
})
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { endpoint as address } from './address'
export { endpoint as addressDebug } from './address-debug'
6 changes: 3 additions & 3 deletions packages/composites/multi-address-list/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
import { config } from './config'
import { address } from './endpoint'
import { PoRAdapter } from '@chainlink/external-adapter-framework/adapter/por'
import { config } from './config'
import { address, addressDebug } from './endpoint'

export const adapter = new PoRAdapter({
defaultEndpoint: address.name,
name: 'MULTI_ADDRESS_LIST',
config,
endpoints: [address],
endpoints: [address, addressDebug],
})

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response'
import { Transport, TransportDependencies } from '@chainlink/external-adapter-framework/transports'
import {
AdapterRequest,
AdapterResponse,
makeLogger,
} from '@chainlink/external-adapter-framework/util'
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
import { BaseEndpointTypes, inputParameters } from '../endpoint/address'
import { AddressListTransportTypes, getAggregatedAddressList, RequestParams } from './common'

const logger = makeLogger('BaseAddressListTransport')

export class AddressDebugTransport implements Transport<AddressListTransportTypes> {
name!: string
responseCache!: ResponseCache<AddressListTransportTypes>
requester!: Requester
settings!: AddressListTransportTypes['Settings']
activeParams: RequestParams[] = []

async initialize(
dependencies: TransportDependencies<AddressListTransportTypes>,
adapterSettings: AddressListTransportTypes['Settings'],
_endpointName: string,
transportName: string,
): Promise<void> {
this.requester = dependencies.requester
this.responseCache = dependencies.responseCache
this.settings = adapterSettings
this.name = transportName
}

async foregroundExecute(
req: AdapterRequest<typeof inputParameters.validated>,
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> {
const entries = req.requestContext.data
return await this.handleRequest(entries)
}

async handleRequest(param: RequestParams) {
let response: AdapterResponse<BaseEndpointTypes['Response']>
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,
},
}
}
return response
}

async _handleRequest(
param: RequestParams,
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> {
return getAggregatedAddressList(param, this.requester, this.settings)
}
}

export const addressDebugTransport = new AddressDebugTransport()
79 changes: 6 additions & 73 deletions packages/composites/multi-address-list/src/transport/address.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,14 @@
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response'
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
import { makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
import { BaseEndpointTypes, inputParameters } from '../endpoint/address'
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
import schedule from 'node-schedule'
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
import { AddressListTransportTypes, getAggregatedAddressList, RequestParams } from './common'

const logger = makeLogger('AddressListTransport')

export type AddressListTransportTypes = BaseEndpointTypes

type RequestParams = typeof inputParameters.validated

interface PoRAdapterResponse {
data: {
result: {
network: string
chainId: string
address: string
}[]
}
statusCode: number
result: null
timestamps: {
providerDataRequestedUnixMs: number
providerDataReceivedUnixMs: number
}
}

export class AddressListTransport extends SubscriptionTransport<AddressListTransportTypes> {
name!: string
responseCache!: ResponseCache<AddressListTransportTypes>
Expand Down Expand Up @@ -76,29 +56,13 @@ export class AddressListTransport extends SubscriptionTransport<AddressListTrans
}

async execute(params: RequestParams, retryCount = 0) {
const providerDataRequestedUnixMs = Date.now()

if (retryCount >= this.settings.MAX_RETRIES) {
logger.error(`Max retry count reached for params: ${JSON.stringify(params)}`)
return
}

try {
const addresses = await this.fetchSourceAddresses(params)
logger.info(`Fetched ${addresses.length} addresses`)

const response = {
data: {
result: addresses,
},
statusCode: 200,
result: null,
timestamps: {
providerDataRequestedUnixMs,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: undefined,
},
}
const response = await getAggregatedAddressList(params, this.requester, this.settings)
await this.responseCache.write(this.name, [
{
params,
Expand All @@ -112,37 +76,6 @@ export class AddressListTransport extends SubscriptionTransport<AddressListTrans
}
}

async fetchSourceAddresses(params: RequestParams) {
const { chainId, network, ...sources } = params

const promises = Object.entries(sources)
.filter(([_, sourceParams]) => sourceParams)
.map(async ([sourceName, sourceParams]) => {
// customInputValidation ensures that if the source EA is present in the input params, the corresponding env variable is also present
const adapterUrl = `${sourceName.toUpperCase()}_ADAPTER_URL` as keyof typeof this.settings
const requestConfig = {
url: this.settings[adapterUrl] as string,
method: 'POST',
data: {
data: {
...sourceParams,
chainId: params.chainId,
network: params.network,
},
},
}

const sourceResponse = await this.requester.request<PoRAdapterResponse>(
JSON.stringify(requestConfig),
requestConfig,
)
return sourceResponse.response.data.data.result
})

const addresses = await Promise.all(promises)
return addresses.flat()
}

getSubscriptionTtlFromConfig(adapterSettings: AddressListTransportTypes['Settings']): number {
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
}
Expand Down
85 changes: 85 additions & 0 deletions packages/composites/multi-address-list/src/transport/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { AdapterSettings } from '@chainlink/external-adapter-framework/config'
import { makeLogger } from '@chainlink/external-adapter-framework/util'
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
import { BaseEndpointTypes, inputParameters } from '../endpoint/address'

const logger = makeLogger('BaseAddressListTransport')

export type AddressListTransportTypes = BaseEndpointTypes
export type RequestParams = typeof inputParameters.validated

interface PoRAdapterResponse {
data: {
result: {
network: string
chainId: string
address: string
}[]
}
statusCode: number
result: null
timestamps: {
providerDataRequestedUnixMs: number
providerDataReceivedUnixMs: number
}
}

export async function getAggregatedAddressList(
params: RequestParams,
requester: Requester,
settings: AdapterSettings,
) {
const providerDataRequestedUnixMs = Date.now()

const addresses = await fetchSourceAddresses(params, requester, settings)
logger.info(`Fetched ${addresses.length} addresses`)

const response = {
data: {
result: addresses,
},
statusCode: 200,
result: null,
timestamps: {
providerDataRequestedUnixMs,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: undefined,
},
}
return response
}

async function fetchSourceAddresses(
params: RequestParams,
requester: Requester,
settings: AdapterSettings,
) {
const { chainId, network, ...sources } = params

const promises = Object.entries(sources)
.filter(([_, sourceParams]) => sourceParams)
.map(async ([sourceName, sourceParams]) => {
// customInputValidation ensures that if the source EA is present in the input params, the corresponding env variable is also present
const adapterUrl = `${sourceName.toUpperCase()}_ADAPTER_URL` as keyof typeof settings
const requestConfig = {
url: settings[adapterUrl] as string,
method: 'POST',
data: {
data: {
...sourceParams,
chainId,
network,
},
},
}

const sourceResponse = await requester.request<PoRAdapterResponse>(
JSON.stringify(requestConfig),
requestConfig,
)
return sourceResponse.response.data.data.result
})

const addresses = await Promise.all(promises)
return addresses.flat()
}
Loading
Loading