Skip to content

Commit a72f52e

Browse files
Feat/OPDATA-4101 decode response selector view-function-multi-chain (#4003)
* OPDATA-4101 decoded response selector endpoint for vfmc * add changeset * Review fixes and refactoring: function-common file, generics, and separation into individual endpoint/transports * more review fixes --------- Co-authored-by: app-token-issuer-data-feeds[bot] <134377064+app-token-issuer-data-feeds[bot]@users.noreply.github.com>
1 parent d46cac7 commit a72f52e

File tree

11 files changed

+299
-115
lines changed

11 files changed

+299
-115
lines changed

.changeset/silent-brooms-invent.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/view-function-multi-chain-adapter': minor
3+
---
4+
5+
function-responseSelector endpoint
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2+
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
3+
import { config } from '../config'
4+
import { multiChainFunctionResponseSelectorTransport } from '../transport/function-response-selector'
5+
import { inputParamDefinition as functionInputParamDefinition } from './function'
6+
7+
const inputParameters = new InputParameters({
8+
...functionInputParamDefinition,
9+
resultField: {
10+
required: true,
11+
description:
12+
"If present, returns the named parameter specified from the signature's response. Has precedence over resultIndex.",
13+
type: 'string',
14+
},
15+
})
16+
17+
export type BaseEndpointTypes = {
18+
Parameters: typeof inputParameters.definition
19+
Response: {
20+
Data: {
21+
result: string
22+
}
23+
Result: string
24+
}
25+
Settings: typeof config.settings
26+
}
27+
28+
export const endpoint = new AdapterEndpoint({
29+
name: 'function-response-selector',
30+
transport: multiChainFunctionResponseSelectorTransport,
31+
inputParameters,
32+
})

packages/sources/view-function-multi-chain/src/endpoint/function.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
22
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
33
import { config } from '../config'
4-
import { multiChainFunctionTransport } from '../transport/function'
4+
import { functionTransport } from '../transport/function'
55

6-
export const inputParameters = new InputParameters({
6+
export const inputParamDefinition = {
77
signature: {
88
type: 'string',
99
aliases: ['function'],
@@ -27,7 +27,9 @@ export const inputParameters = new InputParameters({
2727
description: 'RPC network name',
2828
type: 'string',
2929
},
30-
})
30+
} as const
31+
32+
export const inputParameters = new InputParameters(inputParamDefinition)
3133

3234
export type BaseEndpointTypes = {
3335
Parameters: typeof inputParameters.definition
@@ -42,6 +44,6 @@ export type BaseEndpointTypes = {
4244

4345
export const endpoint = new AdapterEndpoint({
4446
name: 'function',
45-
transport: multiChainFunctionTransport,
47+
transport: functionTransport,
4648
inputParameters,
4749
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { endpoint as aptosEndpoint } from './aptos'
22
export { endpoint as aptosDfReaderEndpoint } from './aptos-df-reader'
33
export { endpoint as functionEndpoint } from './function'
4+
export { endpoint as functionResponseSelectorEndpoint } from './function-response-selector'

packages/sources/view-function-multi-chain/src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
22
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
33
import { config } from './config'
4-
import { aptosDfReaderEndpoint, aptosEndpoint, functionEndpoint } from './endpoint'
4+
import {
5+
aptosDfReaderEndpoint,
6+
aptosEndpoint,
7+
functionEndpoint,
8+
functionResponseSelectorEndpoint,
9+
} from './endpoint'
510

611
export const adapter = new Adapter({
712
defaultEndpoint: functionEndpoint.name,
813
name: 'VIEW_FUNCTION_MULTI_CHAIN',
914
config,
10-
endpoints: [functionEndpoint, aptosEndpoint, aptosDfReaderEndpoint],
15+
endpoints: [
16+
functionEndpoint,
17+
functionResponseSelectorEndpoint,
18+
aptosEndpoint,
19+
aptosDfReaderEndpoint,
20+
],
1121
rateLimiting: {
1222
tiers: {
1323
default: {
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
2+
import {
3+
TransportDependencies,
4+
TransportGenerics,
5+
} from '@chainlink/external-adapter-framework/transports'
6+
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
7+
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
8+
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
9+
import { ethers } from 'ethers'
10+
11+
const logger = makeLogger('View Function Multi Chain')
12+
13+
interface RequestParams {
14+
signature: string
15+
address: string
16+
inputParams?: Array<string>
17+
network: string
18+
resultField?: string
19+
}
20+
21+
export type RawOnchainResponse = {
22+
iface: ethers.Interface
23+
fnName: string
24+
encodedResult: string
25+
}
26+
27+
export type HexResultPostProcessor = (
28+
onchainResponse: RawOnchainResponse,
29+
resultField?: string | undefined,
30+
) => string
31+
32+
export class MultiChainFunctionTransport<
33+
T extends TransportGenerics,
34+
> extends SubscriptionTransport<T> {
35+
providers: Record<string, ethers.JsonRpcProvider> = {}
36+
hexResultPostProcessor: HexResultPostProcessor
37+
38+
constructor(hexResultPostProcessor: HexResultPostProcessor) {
39+
super()
40+
this.hexResultPostProcessor = hexResultPostProcessor
41+
}
42+
43+
async initialize(
44+
dependencies: TransportDependencies<T>,
45+
adapterSettings: T['Settings'],
46+
endpointName: string,
47+
transportName: string,
48+
): Promise<void> {
49+
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
50+
}
51+
52+
async backgroundHandler(context: EndpointContext<T>, entries: Array<T['Parameters']>) {
53+
await Promise.all(
54+
entries.map(async (param) => this.handleRequest(param as unknown as RequestParams)),
55+
)
56+
await sleep(
57+
(context.adapterSettings as unknown as { BACKGROUND_EXECUTE_MS: number })
58+
.BACKGROUND_EXECUTE_MS,
59+
)
60+
}
61+
62+
async handleRequest(param: RequestParams) {
63+
let response: AdapterResponse<T['Response']>
64+
try {
65+
response = await this._handleRequest(param)
66+
} catch (e: unknown) {
67+
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
68+
logger.error(e, errorMessage)
69+
response = {
70+
statusCode: (e as AdapterInputError)?.statusCode || 502,
71+
errorMessage,
72+
timestamps: {
73+
providerDataRequestedUnixMs: 0,
74+
providerDataReceivedUnixMs: 0,
75+
providerIndicatedTimeUnixMs: undefined,
76+
},
77+
}
78+
}
79+
await this.responseCache.write(this.name, [{ params: param as any, response }])
80+
}
81+
82+
async _handleRequest(param: RequestParams): Promise<AdapterResponse<T['Response']>> {
83+
const { address, signature, inputParams, network } = param
84+
85+
const networkName = network.toUpperCase()
86+
const networkEnvName = `${networkName}_RPC_URL`
87+
const chainIdEnvName = `${networkName}_CHAIN_ID`
88+
89+
const rpcUrl = process.env[networkEnvName]
90+
const chainId = Number(process.env[chainIdEnvName])
91+
92+
if (!rpcUrl || isNaN(chainId)) {
93+
throw new AdapterInputError({
94+
statusCode: 400,
95+
message: `Missing '${networkEnvName}' or '${chainIdEnvName}' environment variables.`,
96+
})
97+
}
98+
99+
if (!this.providers[networkName]) {
100+
this.providers[networkName] = new ethers.JsonRpcProvider(rpcUrl, chainId)
101+
}
102+
103+
const iface = new ethers.Interface([signature])
104+
const fnName = iface.getFunctionName(signature)
105+
const encoded = iface.encodeFunctionData(fnName, [...(inputParams || [])])
106+
107+
const providerDataRequestedUnixMs = Date.now()
108+
const encodedResult = await this.providers[networkName].call({
109+
to: address,
110+
data: encoded,
111+
})
112+
113+
const timestamps = {
114+
providerDataRequestedUnixMs,
115+
providerDataReceivedUnixMs: Date.now(),
116+
providerIndicatedTimeUnixMs: undefined,
117+
}
118+
119+
const result = this.hexResultPostProcessor({ iface, fnName, encodedResult }, param.resultField)
120+
121+
return {
122+
data: {
123+
result,
124+
},
125+
statusCode: 200,
126+
result,
127+
timestamps,
128+
}
129+
}
130+
131+
getSubscriptionTtlFromConfig(adapterSettings: T['Settings']): number {
132+
return (adapterSettings as { WARMUP_SUBSCRIPTION_TTL: number }).WARMUP_SUBSCRIPTION_TTL
133+
}
134+
}
135+
136+
// Export a factory function to create transport instances
137+
export function createMultiChainFunctionTransport<T extends TransportGenerics>(
138+
postProcessor: HexResultPostProcessor,
139+
): MultiChainFunctionTransport<T> {
140+
return new MultiChainFunctionTransport<T>(postProcessor)
141+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
2+
import { BaseEndpointTypes } from '../endpoint/function-response-selector'
3+
import { createMultiChainFunctionTransport, RawOnchainResponse } from './function-common'
4+
5+
function selectFieldFromDecodedResult(
6+
onchainResponse: RawOnchainResponse,
7+
resultField?: string | undefined,
8+
): string {
9+
if (!resultField) {
10+
throw new AdapterInputError({
11+
message: 'Missing resultField input param',
12+
statusCode: 400,
13+
})
14+
}
15+
16+
const { iface, fnName, encodedResult } = onchainResponse
17+
const decodedResult = iface.decodeFunctionResult(fnName, encodedResult)
18+
if (decodedResult[resultField] == null) {
19+
throw new AdapterInputError({
20+
message: 'Invalid resultField not found in response',
21+
statusCode: 400,
22+
})
23+
}
24+
return BigInt(decodedResult[resultField]).toString()
25+
}
26+
27+
export const multiChainFunctionResponseSelectorTransport =
28+
createMultiChainFunctionTransport<BaseEndpointTypes>(selectFieldFromDecodedResult)
Lines changed: 5 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,6 @@
1-
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
2-
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
3-
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
4-
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
5-
import { ethers } from 'ethers'
6-
import { BaseEndpointTypes, inputParameters } from '../endpoint/function'
7-
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
1+
import { BaseEndpointTypes } from '../endpoint/function'
2+
import { createMultiChainFunctionTransport } from './function-common'
83

9-
const logger = makeLogger('View Function Multi Chain')
10-
11-
export type MultiChainFunctionTransportTypes = BaseEndpointTypes
12-
13-
type RequestParams = typeof inputParameters.validated
14-
15-
export class MultiChainFunctionTransport extends SubscriptionTransport<MultiChainFunctionTransportTypes> {
16-
providers: Record<string, ethers.JsonRpcProvider> = {}
17-
18-
async initialize(
19-
dependencies: TransportDependencies<MultiChainFunctionTransportTypes>,
20-
adapterSettings: MultiChainFunctionTransportTypes['Settings'],
21-
endpointName: string,
22-
transportName: string,
23-
): Promise<void> {
24-
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
25-
}
26-
27-
async backgroundHandler(
28-
context: EndpointContext<MultiChainFunctionTransportTypes>,
29-
entries: RequestParams[],
30-
) {
31-
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
32-
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
33-
}
34-
35-
async handleRequest(param: RequestParams) {
36-
let response: AdapterResponse<MultiChainFunctionTransportTypes['Response']>
37-
try {
38-
response = await this._handleRequest(param)
39-
} catch (e: unknown) {
40-
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
41-
logger.error(e, errorMessage)
42-
response = {
43-
statusCode: (e as AdapterInputError)?.statusCode || 502,
44-
errorMessage,
45-
timestamps: {
46-
providerDataRequestedUnixMs: 0,
47-
providerDataReceivedUnixMs: 0,
48-
providerIndicatedTimeUnixMs: undefined,
49-
},
50-
}
51-
}
52-
await this.responseCache.write(this.name, [{ params: param, response }])
53-
}
54-
55-
async _handleRequest(
56-
param: RequestParams,
57-
): Promise<AdapterResponse<MultiChainFunctionTransportTypes['Response']>> {
58-
const { address, signature, inputParams, network } = param
59-
60-
const networkName = network.toUpperCase()
61-
const networkEnvName = `${networkName}_RPC_URL`
62-
const chainIdEnvName = `${networkName}_CHAIN_ID`
63-
64-
const rpcUrl = process.env[networkEnvName]
65-
const chainId = Number(process.env[chainIdEnvName])
66-
67-
if (!rpcUrl || isNaN(chainId)) {
68-
throw new AdapterInputError({
69-
statusCode: 400,
70-
message: `Missing '${networkEnvName}' or '${chainIdEnvName}' environment variables.`,
71-
})
72-
}
73-
74-
if (!this.providers[networkName]) {
75-
this.providers[networkName] = new ethers.JsonRpcProvider(rpcUrl, chainId)
76-
}
77-
78-
const iface = new ethers.Interface([signature])
79-
const fnName = iface.getFunctionName(signature)
80-
81-
const encoded = iface.encodeFunctionData(fnName, [...(inputParams || [])])
82-
83-
const providerDataRequestedUnixMs = Date.now()
84-
const result = await this.providers[networkName].call({
85-
to: address,
86-
data: encoded,
87-
})
88-
89-
return {
90-
data: {
91-
result,
92-
},
93-
statusCode: 200,
94-
result,
95-
timestamps: {
96-
providerDataRequestedUnixMs,
97-
providerDataReceivedUnixMs: Date.now(),
98-
providerIndicatedTimeUnixMs: undefined,
99-
},
100-
}
101-
}
102-
103-
getSubscriptionTtlFromConfig(
104-
adapterSettings: MultiChainFunctionTransportTypes['Settings'],
105-
): number {
106-
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
107-
}
108-
}
109-
110-
export const multiChainFunctionTransport = new MultiChainFunctionTransport()
4+
export const functionTransport = createMultiChainFunctionTransport<BaseEndpointTypes>(
5+
(rawResponse) => rawResponse.encodedResult,
6+
)

0 commit comments

Comments
 (0)