diff --git a/.changeset/two-numbers-confess.md b/.changeset/two-numbers-confess.md new file mode 100644 index 0000000000..96f2e8e2fe --- /dev/null +++ b/.changeset/two-numbers-confess.md @@ -0,0 +1,5 @@ +--- +'@chainlink/multi-address-list-adapter': minor +--- + +added address-debug endpoint diff --git a/packages/composites/multi-address-list/src/endpoint/address-debug.ts b/packages/composites/multi-address-list/src/endpoint/address-debug.ts new file mode 100644 index 0000000000..c187b17926 --- /dev/null +++ b/packages/composites/multi-address-list/src/endpoint/address-debug.ts @@ -0,0 +1,14 @@ +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. + */ +export const endpoint = new PoRAddressEndpoint({ + name: 'address-debug', + transport: addressDebugTransport, + inputParameters, + customInputValidation, +}) diff --git a/packages/composites/multi-address-list/src/endpoint/address.ts b/packages/composites/multi-address-list/src/endpoint/address.ts index 2dadb03b57..d029122828 100644 --- a/packages/composites/multi-address-list/src/endpoint/address.ts +++ b/packages/composites/multi-address-list/src/endpoint/address.ts @@ -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: { @@ -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, }) diff --git a/packages/composites/multi-address-list/src/endpoint/index.ts b/packages/composites/multi-address-list/src/endpoint/index.ts index 812dfe92f6..daa2939370 100644 --- a/packages/composites/multi-address-list/src/endpoint/index.ts +++ b/packages/composites/multi-address-list/src/endpoint/index.ts @@ -1 +1,2 @@ export { endpoint as address } from './address' +export { endpoint as addressDebug } from './address-debug' diff --git a/packages/composites/multi-address-list/src/index.ts b/packages/composites/multi-address-list/src/index.ts index 8796f87712..0345fb15db 100644 --- a/packages/composites/multi-address-list/src/index.ts +++ b/packages/composites/multi-address-list/src/index.ts @@ -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 => expose(adapter) diff --git a/packages/composites/multi-address-list/src/transport/address-debug.ts b/packages/composites/multi-address-list/src/transport/address-debug.ts new file mode 100644 index 0000000000..b22872a987 --- /dev/null +++ b/packages/composites/multi-address-list/src/transport/address-debug.ts @@ -0,0 +1,64 @@ +import { EndpointContext } from '@chainlink/external-adapter-framework/adapter' +import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response' +import { TransportDependencies } from '@chainlink/external-adapter-framework/transports' +import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util' +import { Requester } from '@chainlink/external-adapter-framework/util/requester' +import { BaseEndpointTypes } from '../endpoint/address' +import { AddressListTransportTypes, BaseAddressListTransport, RequestParams } from './common' + +const logger = makeLogger('BaseAddressListTransport') + +export class AddressDebugTransport extends BaseAddressListTransport { + name!: string + responseCache!: ResponseCache + requester!: Requester + settings!: AddressListTransportTypes['Settings'] + activeParams: RequestParams[] = [] + + async initialize( + dependencies: TransportDependencies, + adapterSettings: AddressListTransportTypes['Settings'], + endpointName: string, + transportName: string, + ): Promise { + await super.initialize(dependencies, adapterSettings, endpointName, transportName) + this.requester = dependencies.requester + this.settings = adapterSettings + } + + 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) { + 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, + }, + } + } + await this.responseCache.write(this.name, [{ params: param, response }]) + } + + async _handleRequest( + param: RequestParams, + ): Promise> { + return this.getAggregatedAddressList(param) + } +} + +export const addressDebugTransport = new AddressDebugTransport() diff --git a/packages/composites/multi-address-list/src/transport/address.ts b/packages/composites/multi-address-list/src/transport/address.ts index 7465b87241..804b217847 100644 --- a/packages/composites/multi-address-list/src/transport/address.ts +++ b/packages/composites/multi-address-list/src/transport/address.ts @@ -1,35 +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 { 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, BaseAddressListTransport, 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 { +export class AddressListTransport extends BaseAddressListTransport { name!: string responseCache!: ResponseCache requester!: Requester @@ -76,29 +55,13 @@ export class AddressListTransport extends SubscriptionTransport= 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 this.getAggregatedAddressList(params) await this.responseCache.write(this.name, [ { params, @@ -111,41 +74,6 @@ export class AddressListTransport extends SubscriptionTransport this.execute(params, retryCount), this.settings.RETRY_INTERVAL_MS) } } - - 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( - 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 - } } export const addressListTransport = new AddressListTransport() diff --git a/packages/composites/multi-address-list/src/transport/common.ts b/packages/composites/multi-address-list/src/transport/common.ts new file mode 100644 index 0000000000..0b7e44b57c --- /dev/null +++ b/packages/composites/multi-address-list/src/transport/common.ts @@ -0,0 +1,90 @@ +import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response' +import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription' +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 abstract class BaseAddressListTransport extends SubscriptionTransport { + name!: string + responseCache!: ResponseCache + requester!: Requester + settings!: AddressListTransportTypes['Settings'] + activeParams: RequestParams[] = [] + + async getAggregatedAddressList(params: RequestParams) { + const providerDataRequestedUnixMs = Date.now() + + 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, + }, + } + return response + } + + 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, + network, + }, + }, + } + + const sourceResponse = await this.requester.request( + 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 + } +} diff --git a/packages/composites/multi-address-list/test/integration/__snapshots__/adapter-noschedulemock.test.ts.snap b/packages/composites/multi-address-list/test/integration/__snapshots__/adapter-noschedulemock.test.ts.snap new file mode 100644 index 0000000000..77ea406153 --- /dev/null +++ b/packages/composites/multi-address-list/test/integration/__snapshots__/adapter-noschedulemock.test.ts.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`execute address-debug endpoint should return success 1`] = ` +{ + "data": { + "result": [ + { + "address": "bc2434567890123456789012345678901234567890", + "chainId": "mainnet", + "network": "bitcoin", + }, + { + "address": "bc3534567890123456789012345678901234567890", + "chainId": "mainnet", + "network": "bitcoin", + }, + { + "address": "bc4634567890123456789012345678901234567890", + "chainId": "mainnet", + "network": "bitcoin", + }, + { + "address": "tb1q44alsfkysj4zxvwk6ktwjq3c0wysrxmunmxkh3n84dpqfg85l7msqn8a83", + "chainId": "mainnet", + "network": "bitcoin", + }, + { + "address": "tb1qq93j04yg2klrfnfhhmr7k6ha0kz9qmm6p5gmrvhtpsc4l620h8cq8gzqfr", + "chainId": "mainnet", + "network": "bitcoin", + }, + { + "address": "tb1qa5c93xvk45m34lqe52sfcu2ls9n7zexy9g9rhn6emzzr4t7hv35qwulqce", + "chainId": "mainnet", + "network": "bitcoin", + }, + { + "address": "tb1qm4et3f642cct77s99zmwctl9mmdaemprwlndylpusl2lmesl62aq3kzg6s", + "chainId": "mainnet", + "network": "bitcoin", + }, + { + "address": "bc0987654321098765432109876543210987654321", + "chainId": "mainnet", + "network": "bitcoin", + }, + { + "address": "bc1234567890123456789012345678901234567890", + "chainId": "mainnet", + "network": "bitcoin", + }, + ], + }, + "result": null, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": 978347471111, + "providerDataRequestedUnixMs": 978347471111, + }, +} +`; diff --git a/packages/composites/multi-address-list/test/integration/adapter-noschedulemock.test.ts b/packages/composites/multi-address-list/test/integration/adapter-noschedulemock.test.ts new file mode 100644 index 0000000000..3bc38d74f2 --- /dev/null +++ b/packages/composites/multi-address-list/test/integration/adapter-noschedulemock.test.ts @@ -0,0 +1,66 @@ +import { + TestAdapter, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import * as nock from 'nock' +import { mockAnchorageSuccess, mockBitgoSuccess, mockCBPSuccess } from './fixtures' + +describe('execute', () => { + let spy: jest.SpyInstance + let testAdapter: TestAdapter + let oldEnv: NodeJS.ProcessEnv + + beforeAll(async () => { + oldEnv = JSON.parse(JSON.stringify(process.env)) + process.env.ANCHORAGE_ADAPTER_URL = 'https://localhost:8081' + process.env.BITGO_ADAPTER_URL = 'https://localhost:8082' + process.env.COINBASE_PRIME_ADAPTER_URL = 'https://localhost:8083' + process.env.BACKGROUND_EXECUTE_MS = '0' + + const mockDate = new Date('2001-01-01T11:11:11.111Z') + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + + const adapter = (await import('../../src')).adapter + adapter.rateLimiting = undefined + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + testAdapter: {} as TestAdapter, + }) + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + await testAdapter.api.close() + nock.restore() + nock.cleanAll() + spy.mockRestore() + }) + + describe('address-debug endpoint', () => { + it('should return success', async () => { + const data = { + endpoint: 'address-debug', + network: 'bitcoin', + chainId: 'mainnet', + anchorage: { + vaultId: 'b0bb5449c1e4926542ce693b4db2e883', + coin: 'BTC', + }, + bitgo: { + coin: 'tbtc', + enterpriseId: '1234', + }, + coinbase_prime: { + batchSize: 100, + portfolio: '12345622', + symbols: ['BTC'], + }, + } + mockAnchorageSuccess() + mockBitgoSuccess() + mockCBPSuccess() + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + }) +}) diff --git a/packages/composites/multi-address-list/test/integration/adapter.test.ts b/packages/composites/multi-address-list/test/integration/adapter.test.ts index 8aacf46f41..f8be1fe8d8 100644 --- a/packages/composites/multi-address-list/test/integration/adapter.test.ts +++ b/packages/composites/multi-address-list/test/integration/adapter.test.ts @@ -3,8 +3,8 @@ import { setEnvVariables, } from '@chainlink/external-adapter-framework/util/testing-utils' import * as nock from 'nock' -import { mockAnchorageSuccess, mockBitgoSuccess, mockCBPSuccess } from './fixtures' import { RecurrenceRule } from 'node-schedule' +import { mockAnchorageSuccess, mockBitgoSuccess, mockCBPSuccess } from './fixtures' jest.mock('node-schedule', () => { const actualNodeSchedule = jest.requireActual('node-schedule') @@ -28,12 +28,10 @@ describe('execute', () => { beforeAll(async () => { oldEnv = JSON.parse(JSON.stringify(process.env)) - process.env.ANCHORAGE_ADAPTER_URL = - process.env.ANCHORAGE_ADAPTER_URL ?? 'https://localhost:8081' - process.env.BITGO_ADAPTER_URL = process.env.BITGO_ADAPTER_URL ?? 'https://localhost:8082' - process.env.COINBASE_PRIME_ADAPTER_URL = - process.env.COINBASE_PRIME_ADAPTER_URL ?? 'https://localhost:8083' - process.env.BACKGROUND_EXECUTE_MS = process.env.BACKGROUND_EXECUTE_MS ?? '0' + process.env.ANCHORAGE_ADAPTER_URL = 'https://localhost:8081' + process.env.BITGO_ADAPTER_URL = 'https://localhost:8082' + process.env.COINBASE_PRIME_ADAPTER_URL = 'https://localhost:8083' + process.env.BACKGROUND_EXECUTE_MS = '0' const mockDate = new Date('2001-01-01T11:11:11.111Z') spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())