Skip to content

Commit 0486d98

Browse files
Subarna-Singhdskloetcapp-token-issuer-data-feeds[bot]
authored
Opdata 3775 add functionality query decimals (#4107)
* OPDATA-3775 view-function-multi-chain add functionality * add changeset * add functionality to support call to additional contract functions * update test-payload * remove hard coded decimals field * FIX: add concurrency * FIX: incorrect testcase * FIX: Add GroupRunner * Update dependencies * FIX: additonalrequest manadatory * FIX: use Promise.all; Add Hardcoded values * Remove comments * FIX: post review comments * Remove config cast * Make RequestParams generic * remove more casts * REMOVE: null check * FIX: refactor code --------- Co-authored-by: David de Kloet <[email protected]> Co-authored-by: app-token-issuer-data-feeds[bot] <134377064+app-token-issuer-data-feeds[bot]@users.noreply.github.com>
1 parent 3fec923 commit 0486d98

File tree

8 files changed

+231
-41
lines changed

8 files changed

+231
-41
lines changed

.changeset/curly-bees-destroy.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+
Add functionality to query decimals from contract

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ export const config = new AdapterConfig({
1313
required: false,
1414
default: '',
1515
},
16+
GROUP_SIZE: {
17+
description:
18+
'Number of requests to execute asynchronously before the adapter waits to execute the next group of requests.',
19+
type: 'number',
20+
default: 10,
21+
},
1622
BACKGROUND_EXECUTE_MS: {
1723
description:
1824
'The amount of time the background execute should sleep before performing the next request',

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ export const inputParamDefinition = {
2727
description: 'RPC network name',
2828
type: 'string',
2929
},
30+
additionalRequests: {
31+
description: 'Optional map of function calls',
32+
array: true,
33+
type: {
34+
name: {
35+
required: true,
36+
type: 'string',
37+
description: 'Unique name or identifier for this additional request',
38+
},
39+
signature: {
40+
required: true,
41+
type: 'string',
42+
description:
43+
'Function signature, formatted as human-readable ABI (e.g., balanceOf(address))',
44+
},
45+
},
46+
required: false,
47+
},
3048
} as const
3149

3250
export const inputParameters = new InputParameters(inputParamDefinition)

packages/sources/view-function-multi-chain/src/transport/function-common.ts

Lines changed: 110 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
2-
import {
3-
TransportDependencies,
4-
TransportGenerics,
5-
} from '@chainlink/external-adapter-framework/transports'
2+
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
63
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
74
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
8-
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
5+
import { GroupRunner } from '@chainlink/external-adapter-framework/util/group-runner'
6+
import {
7+
AdapterError,
8+
AdapterInputError,
9+
} from '@chainlink/external-adapter-framework/validation/error'
10+
import { TypeFromDefinition } from '@chainlink/external-adapter-framework/validation/input-params'
911
import { ethers } from 'ethers'
12+
import { BaseEndpointTypes as FunctionEndpointTypes } from '../endpoint/function'
13+
import { BaseEndpointTypes as FunctionResponseSelectorEndpointTypes } from '../endpoint/function-response-selector'
1014

1115
const logger = makeLogger('View Function Multi Chain')
1216

13-
interface RequestParams {
14-
signature: string
15-
address: string
16-
inputParams?: Array<string>
17-
network: string
18-
resultField?: string
19-
}
17+
type GenericFunctionEndpointTypes = FunctionEndpointTypes | FunctionResponseSelectorEndpointTypes
18+
19+
// The `extends any ? ... : never` construct forces the compiler to distribute
20+
// over unions. Without it, the compiler doesn't know that T is either
21+
// FunctionEndpointTypes or FunctionResponseSelectorEndpointTypes.
22+
type RequestParams<T extends GenericFunctionEndpointTypes> = T extends any
23+
? TypeFromDefinition<T['Parameters']>
24+
: never
2025

2126
export type RawOnchainResponse = {
2227
iface: ethers.Interface
@@ -30,8 +35,9 @@ export type HexResultPostProcessor = (
3035
) => string
3136

3237
export class MultiChainFunctionTransport<
33-
T extends TransportGenerics,
38+
T extends GenericFunctionEndpointTypes,
3439
> extends SubscriptionTransport<T> {
40+
config!: T['Settings']
3541
providers: Record<string, ethers.JsonRpcProvider> = {}
3642
hexResultPostProcessor: HexResultPostProcessor
3743

@@ -47,19 +53,15 @@ export class MultiChainFunctionTransport<
4753
transportName: string,
4854
): Promise<void> {
4955
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
56+
this.config = adapterSettings
5057
}
5158

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-
)
59+
async backgroundHandler(context: EndpointContext<T>, entries: RequestParams<T>[]) {
60+
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
61+
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
6062
}
6163

62-
async handleRequest(param: RequestParams) {
64+
async handleRequest(param: RequestParams<T>) {
6365
let response: AdapterResponse<T['Response']>
6466
try {
6567
response = await this._handleRequest(param)
@@ -76,11 +78,42 @@ export class MultiChainFunctionTransport<
7678
},
7779
}
7880
}
79-
await this.responseCache.write(this.name, [{ params: param as any, response }])
81+
82+
await this.responseCache.write(this.name, [{ params: param, response }])
83+
}
84+
85+
async _handleRequest(param: RequestParams<T>): Promise<AdapterResponse<T['Response']>> {
86+
const { address, signature, inputParams, network, additionalRequests, resultField } = param
87+
88+
const [mainResult, nestedResultOutcome] = await Promise.all([
89+
this._executeFunction({
90+
address,
91+
signature,
92+
inputParams,
93+
network,
94+
resultField,
95+
}),
96+
this._processNestedDataRequest(additionalRequests, address, network),
97+
])
98+
99+
const combinedData = { result: mainResult.result, ...nestedResultOutcome }
100+
101+
return {
102+
data: combinedData,
103+
statusCode: 200,
104+
result: mainResult.result,
105+
timestamps: mainResult.timestamps,
106+
}
80107
}
81108

82-
async _handleRequest(param: RequestParams): Promise<AdapterResponse<T['Response']>> {
83-
const { address, signature, inputParams, network } = param
109+
private async _executeFunction(params: {
110+
address: string
111+
signature: string
112+
inputParams?: Array<string>
113+
network: string
114+
resultField?: string
115+
}) {
116+
const { address, signature, inputParams, network, resultField } = params
84117

85118
const networkName = network.toUpperCase()
86119
const networkEnvName = `${networkName}_RPC_URL`
@@ -102,39 +135,76 @@ export class MultiChainFunctionTransport<
102135

103136
const iface = new ethers.Interface([signature])
104137
const fnName = iface.getFunctionName(signature)
105-
const encoded = iface.encodeFunctionData(fnName, [...(inputParams || [])])
138+
const encoded = iface.encodeFunctionData(fnName, inputParams || [])
106139

107140
const providerDataRequestedUnixMs = Date.now()
108-
const encodedResult = await this.providers[networkName].call({
109-
to: address,
110-
data: encoded,
111-
})
141+
142+
let encodedResult
143+
try {
144+
encodedResult = await this.providers[networkName].call({ to: address, data: encoded })
145+
} catch (err) {
146+
throw new AdapterError({
147+
statusCode: 500,
148+
message: `RPC call failed for ${fnName} on ${networkName}: ${err}`,
149+
})
150+
}
112151

113152
const timestamps = {
114153
providerDataRequestedUnixMs,
115154
providerDataReceivedUnixMs: Date.now(),
116155
providerIndicatedTimeUnixMs: undefined,
117156
}
118157

119-
const result = this.hexResultPostProcessor({ iface, fnName, encodedResult }, param.resultField)
158+
const result = this.hexResultPostProcessor({ iface, fnName, encodedResult }, resultField)
120159

121-
return {
122-
data: {
123-
result,
124-
},
125-
statusCode: 200,
126-
result,
127-
timestamps,
160+
return { result, timestamps }
161+
}
162+
163+
private async _processNestedDataRequest(
164+
additionalRequests:
165+
| Array<{
166+
name: string
167+
signature: string
168+
}>
169+
| undefined,
170+
parentAddress: string,
171+
parentNetwork: string,
172+
): Promise<Record<string, string>> {
173+
if (!Array.isArray(additionalRequests) || additionalRequests.length === 0) {
174+
return {}
128175
}
176+
177+
const runner = new GroupRunner(this.config.GROUP_SIZE)
178+
179+
const processNested = runner.wrapFunction(
180+
async (req: { name: string; signature: string }): Promise<[string, string]> => {
181+
const key = req.name
182+
try {
183+
const nestedParam = {
184+
address: parentAddress,
185+
network: parentNetwork,
186+
signature: req.signature,
187+
}
188+
189+
const subRes = await this._executeFunction(nestedParam)
190+
return [key, subRes.result]
191+
} catch (err) {
192+
throw new Error(`Nested function "${key}" failed: ${err}`)
193+
}
194+
},
195+
)
196+
197+
const settled: [string, string][] = await Promise.all(additionalRequests.map(processNested))
198+
return Object.fromEntries(settled)
129199
}
130200

131201
getSubscriptionTtlFromConfig(adapterSettings: T['Settings']): number {
132-
return (adapterSettings as { WARMUP_SUBSCRIPTION_TTL: number }).WARMUP_SUBSCRIPTION_TTL
202+
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
133203
}
134204
}
135205

136206
// Export a factory function to create transport instances
137-
export function createMultiChainFunctionTransport<T extends TransportGenerics>(
207+
export function createMultiChainFunctionTransport<T extends GenericFunctionEndpointTypes>(
138208
postProcessor: HexResultPostProcessor,
139209
): MultiChainFunctionTransport<T> {
140210
return new MultiChainFunctionTransport<T>(postProcessor)

packages/sources/view-function-multi-chain/test-payload.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
"requests": [{
33
"contract": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c",
44
"function": "function latestAnswer() view returns (int256)",
5-
"network": "ETHEREUM"
5+
"network": "ETHEREUM",
6+
"data": {
7+
"decimals": {
8+
"signature": "function decimals() view returns (uint8)"
9+
}
10+
}
611
}]
712
}

packages/sources/view-function-multi-chain/test/integration/__snapshots__/adapter.test.ts.snap

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ exports[`execute aptos-df-reader endpoint should return success 1`] = `
2828
}
2929
`;
3030

31+
exports[`execute function endpoint should fail additional data requests in case of missing signature 1`] = `
32+
{
33+
"errorMessage": "Nested function "decimals" failed: TypeError: no matching function (argument="key", value="", code=INVALID_ARGUMENT, version=6.15.0)",
34+
"statusCode": 502,
35+
"timestamps": {
36+
"providerDataReceivedUnixMs": 0,
37+
"providerDataRequestedUnixMs": 0,
38+
},
39+
}
40+
`;
41+
3142
exports[`execute function endpoint should return success 1`] = `
3243
{
3344
"data": {
@@ -56,6 +67,21 @@ exports[`execute function endpoint should return success for different network 1
5667
}
5768
`;
5869

70+
exports[`execute function endpoint should return success with additional data requests 1`] = `
71+
{
72+
"data": {
73+
"decimals": "0x0000000000000000000000000000000000000000000000000000000000000008",
74+
"result": "0x000000000000000000000000000000000000000000000000000000005ad789f8",
75+
},
76+
"result": "0x000000000000000000000000000000000000000000000000000000005ad789f8",
77+
"statusCode": 200,
78+
"timestamps": {
79+
"providerDataReceivedUnixMs": 978347471111,
80+
"providerDataRequestedUnixMs": 978347471111,
81+
},
82+
}
83+
`;
84+
5985
exports[`execute function endpoint should return success with parameters 1`] = `
6086
{
6187
"data": {

packages/sources/view-function-multi-chain/test/integration/adapter.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ describe('execute', () => {
4545
spy.mockRestore()
4646
})
4747

48+
afterEach(() => {
49+
nock.cleanAll()
50+
})
51+
4852
describe('function endpoint', () => {
4953
it('should return success', async () => {
5054
const data = {
@@ -58,6 +62,42 @@ describe('execute', () => {
5862
expect(response.json()).toMatchSnapshot()
5963
})
6064

65+
it('should return success with additional data requests ', async () => {
66+
const data = {
67+
contract: '0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c',
68+
function: 'function latestAnswer() external view returns (int256)',
69+
network: 'ethereum_mainnet',
70+
additionalRequests: [
71+
{
72+
name: 'decimals',
73+
signature: 'function decimals() view returns (uint8)',
74+
},
75+
],
76+
}
77+
mockETHMainnetContractCallResponseSuccess()
78+
const response = await testAdapter.request(data)
79+
expect(response.statusCode).toBe(200)
80+
expect(response.json()).toMatchSnapshot()
81+
})
82+
83+
it('should fail additional data requests in case of missing signature', async () => {
84+
const data = {
85+
contract: '0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c',
86+
function: 'function latestAnswer() external view returns (int256)',
87+
network: 'ethereum_mainnet',
88+
additionalRequests: [
89+
{
90+
name: 'decimals',
91+
signature: '',
92+
},
93+
],
94+
}
95+
mockETHMainnetContractCallResponseSuccess()
96+
const response = await testAdapter.request(data)
97+
expect(response.statusCode).toBe(502)
98+
expect(response.json()).toMatchSnapshot()
99+
})
100+
61101
it('should return success for different network', async () => {
62102
const data = {
63103
contract: '0x779877a7b0d9e8603169ddbd7836e478b4624789',

packages/sources/view-function-multi-chain/test/integration/fixtures.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ export const mockETHMainnetContractCallResponseSuccess = (): nock.Scope =>
3737
id: request.id,
3838
result: '0x000000000000000000000000000000000000000000000000000000005ad789f8',
3939
}
40+
} else if (
41+
request.method === 'eth_call' &&
42+
request.params[0].to === '0x2c1d072e956affc0d435cb7ac38ef18d24d9127c' &&
43+
request.params[0].data === '0x313ce567' // decimals()
44+
) {
45+
return {
46+
jsonrpc: '2.0',
47+
id: request.id,
48+
result: '0x0000000000000000000000000000000000000000000000000000000000000008',
49+
}
4050
} else if (
4151
request.method === 'eth_call' &&
4252
request.params[0].to === '0x2c1d072e956affc0d435cb7ac38ef18d24d9127c' &&
@@ -110,6 +120,16 @@ export const mockETHGoerliContractCallResponseSuccess = (): nock.Scope =>
110120
id: request.id,
111121
result: '0x000000000000000000000000000000000000000000000000eead809f678d30f0',
112122
}
123+
} else if (
124+
request.method === 'eth_call' &&
125+
request.params[0].to === '0x779877a7b0d9e8603169ddbd7836e478b4624789' &&
126+
request.params[0].data === '0x313ce567' // decimals()
127+
) {
128+
return {
129+
jsonrpc: '2.0',
130+
id: request.id,
131+
result: '0x0000000000000000000000000000000000000000000000000000000000000018',
132+
}
113133
} else {
114134
// Default response for unsupported calls
115135
return {

0 commit comments

Comments
 (0)