Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2e0b59c
OPDATA-3775 view-function-multi-chain add functionality
Subarna-Singh Oct 14, 2025
4597e42
add changeset
Subarna-Singh Oct 14, 2025
3c9d0e9
add functionality to support call to additional contract functions
Subarna-Singh Oct 15, 2025
44df7f7
update test-payload
Subarna-Singh Oct 15, 2025
3f9f342
remove hard coded decimals field
Subarna-Singh Oct 15, 2025
59f380e
FIX: add concurrency
Subarna-Singh Oct 29, 2025
094ab12
FIX: incorrect testcase
Subarna-Singh Oct 29, 2025
99d278a
FIX: Add GroupRunner
Subarna-Singh Oct 30, 2025
3fa4519
Update dependencies
Subarna-Singh Oct 30, 2025
a77102d
FIX: additonalrequest manadatory
Subarna-Singh Oct 31, 2025
c659e5f
FIX: use Promise.all; Add Hardcoded values
Subarna-Singh Nov 4, 2025
ba1504d
Remove comments
Subarna-Singh Nov 4, 2025
8b019a4
FIX: post review comments
Subarna-Singh Nov 4, 2025
c2cfb44
Remove config cast
dskloetc Nov 5, 2025
cf4979d
Make RequestParams generic
dskloetc Nov 5, 2025
957660f
remove more casts
dskloetc Nov 5, 2025
3cc69b0
Merge branch 'main' into OPDATA-3775-add-functionality-query-decimals
Subarna-Singh Nov 6, 2025
6d280d0
REMOVE: null check
Subarna-Singh Nov 6, 2025
98fd914
FIX: refactor code
Subarna-Singh Nov 6, 2025
fa92fd3
Merge branch 'main' into OPDATA-3775-add-functionality-query-decimals
app-token-issuer-data-feeds[bot] Nov 6, 2025
da29122
Merge branch 'main' into OPDATA-3775-add-functionality-query-decimals
app-token-issuer-data-feeds[bot] Nov 6, 2025
f570633
Merge branch 'main' into OPDATA-3775-add-functionality-query-decimals
Subarna-Singh Nov 6, 2025
51e885c
Merge branch 'main' into OPDATA-3775-add-functionality-query-decimals
app-token-issuer-data-feeds[bot] Nov 6, 2025
6d5568a
Merge branch 'main' into OPDATA-3775-add-functionality-query-decimals
Subarna-Singh Nov 6, 2025
4b1622e
Merge branch 'main' into OPDATA-3775-add-functionality-query-decimals
app-token-issuer-data-feeds[bot] Nov 6, 2025
31e6c1d
Merge branch 'main' into OPDATA-3775-add-functionality-query-decimals
app-token-issuer-data-feeds[bot] Nov 6, 2025
4f2f13a
Merge branch 'main' into OPDATA-3775-add-functionality-query-decimals
app-token-issuer-data-feeds[bot] Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curly-bees-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/view-function-multi-chain-adapter': minor
---

Add functionality to query decimals from contract
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export const config = new AdapterConfig({
required: false,
default: '',
},
GROUP_SIZE: {
description:
'Number of requests to execute asynchronously before the adapter waits to execute the next group of requests.',
type: 'number',
default: 10,
},
BACKGROUND_EXECUTE_MS: {
description:
'The amount of time the background execute should sleep before performing the next request',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ export const inputParamDefinition = {
description: 'RPC network name',
type: 'string',
},
additionalRequests: {
description: 'Optional map of function calls',
array: true,
type: {
name: {
required: true,
type: 'string',
description: 'Unique name or identifier for this additional request',
},
signature: {
required: true,
type: 'string',
description:
'Function signature, formatted as human-readable ABI (e.g., balanceOf(address))',
},
},
required: false,
},
} as const

export const inputParameters = new InputParameters(inputParamDefinition)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
import {
TransportDependencies,
TransportGenerics,
} from '@chainlink/external-adapter-framework/transports'
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
import { GroupRunner } from '@chainlink/external-adapter-framework/util/group-runner'
import {
AdapterError,
AdapterInputError,
} from '@chainlink/external-adapter-framework/validation/error'
import { TypeFromDefinition } from '@chainlink/external-adapter-framework/validation/input-params'
import { ethers } from 'ethers'
import { BaseEndpointTypes as FunctionEndpointTypes } from '../endpoint/function'
import { BaseEndpointTypes as FunctionResponseSelectorEndpointTypes } from '../endpoint/function-response-selector'

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

interface RequestParams {
signature: string
address: string
inputParams?: Array<string>
network: string
resultField?: string
}
type GenericFunctionEndpointTypes = FunctionEndpointTypes | FunctionResponseSelectorEndpointTypes

// The `extends any ? ... : never` construct forces the compiler to distribute
// over unions. Without it, the compiler doesn't know that T is either
// FunctionEndpointTypes or FunctionResponseSelectorEndpointTypes.
type RequestParams<T extends GenericFunctionEndpointTypes> = T extends any
? TypeFromDefinition<T['Parameters']>
: never

export type RawOnchainResponse = {
iface: ethers.Interface
Expand All @@ -30,8 +35,9 @@ export type HexResultPostProcessor = (
) => string

export class MultiChainFunctionTransport<
T extends TransportGenerics,
T extends GenericFunctionEndpointTypes,
> extends SubscriptionTransport<T> {
config!: T['Settings']
providers: Record<string, ethers.JsonRpcProvider> = {}
hexResultPostProcessor: HexResultPostProcessor

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

async backgroundHandler(context: EndpointContext<T>, entries: Array<T['Parameters']>) {
await Promise.all(
entries.map(async (param) => this.handleRequest(param as unknown as RequestParams)),
)
await sleep(
(context.adapterSettings as unknown as { BACKGROUND_EXECUTE_MS: number })
.BACKGROUND_EXECUTE_MS,
)
async backgroundHandler(context: EndpointContext<T>, entries: RequestParams<T>[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
}

async handleRequest(param: RequestParams) {
async handleRequest(param: RequestParams<T>) {
let response: AdapterResponse<T['Response']>
try {
response = await this._handleRequest(param)
Expand All @@ -76,11 +78,42 @@ export class MultiChainFunctionTransport<
},
}
}
await this.responseCache.write(this.name, [{ params: param as any, response }])

await this.responseCache.write(this.name, [{ params: param, response }])
}

async _handleRequest(param: RequestParams<T>): Promise<AdapterResponse<T['Response']>> {
const { address, signature, inputParams, network, additionalRequests, resultField } = param

const [mainResult, nestedResultOutcome] = await Promise.all([
this._executeFunction({
address,
signature,
inputParams,
network,
resultField,
}),
this._processNestedDataRequest(additionalRequests, address, network),
])

const combinedData = { result: mainResult.result, ...nestedResultOutcome }

return {
data: combinedData,
statusCode: 200,
result: mainResult.result,
timestamps: mainResult.timestamps,
}
}

async _handleRequest(param: RequestParams): Promise<AdapterResponse<T['Response']>> {
const { address, signature, inputParams, network } = param
private async _executeFunction(params: {
address: string
signature: string
inputParams?: Array<string>
network: string
resultField?: string
}) {
const { address, signature, inputParams, network, resultField } = params

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

const iface = new ethers.Interface([signature])
const fnName = iface.getFunctionName(signature)
const encoded = iface.encodeFunctionData(fnName, [...(inputParams || [])])
const encoded = iface.encodeFunctionData(fnName, inputParams || [])

const providerDataRequestedUnixMs = Date.now()
const encodedResult = await this.providers[networkName].call({
to: address,
data: encoded,
})

let encodedResult
try {
encodedResult = await this.providers[networkName].call({ to: address, data: encoded })
} catch (err) {
throw new AdapterError({
statusCode: 500,
message: `RPC call failed for ${fnName} on ${networkName}: ${err}`,
})
}

const timestamps = {
providerDataRequestedUnixMs,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: undefined,
}

const result = this.hexResultPostProcessor({ iface, fnName, encodedResult }, param.resultField)
const result = this.hexResultPostProcessor({ iface, fnName, encodedResult }, resultField)

return {
data: {
result,
},
statusCode: 200,
result,
timestamps,
return { result, timestamps }
}

private async _processNestedDataRequest(
additionalRequests:
| Array<{
name: string
signature: string
}>
| undefined,
parentAddress: string,
parentNetwork: string,
): Promise<Record<string, string>> {
if (!Array.isArray(additionalRequests) || additionalRequests.length === 0) {
return {}
}

const runner = new GroupRunner(this.config.GROUP_SIZE)

const processNested = runner.wrapFunction(
async (req: { name: string; signature: string }): Promise<[string, string]> => {
const key = req.name
try {
const nestedParam = {
address: parentAddress,
network: parentNetwork,
signature: req.signature,
}

const subRes = await this._executeFunction(nestedParam)
return [key, subRes.result]
} catch (err) {
throw new Error(`Nested function "${key}" failed: ${err}`)
}
},
)

const settled: [string, string][] = await Promise.all(additionalRequests.map(processNested))
return Object.fromEntries(settled)
}

getSubscriptionTtlFromConfig(adapterSettings: T['Settings']): number {
return (adapterSettings as { WARMUP_SUBSCRIPTION_TTL: number }).WARMUP_SUBSCRIPTION_TTL
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
}
}

// Export a factory function to create transport instances
export function createMultiChainFunctionTransport<T extends TransportGenerics>(
export function createMultiChainFunctionTransport<T extends GenericFunctionEndpointTypes>(
postProcessor: HexResultPostProcessor,
): MultiChainFunctionTransport<T> {
return new MultiChainFunctionTransport<T>(postProcessor)
Expand Down
7 changes: 6 additions & 1 deletion packages/sources/view-function-multi-chain/test-payload.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
"requests": [{
"contract": "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c",
"function": "function latestAnswer() view returns (int256)",
"network": "ETHEREUM"
"network": "ETHEREUM",
"data": {
"decimals": {
"signature": "function decimals() view returns (uint8)"
}
}
}]
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ exports[`execute aptos-df-reader endpoint should return success 1`] = `
}
`;

exports[`execute function endpoint should fail additional data requests in case of missing signature 1`] = `
{
"errorMessage": "Nested function "decimals" failed: TypeError: no matching function (argument="key", value="", code=INVALID_ARGUMENT, version=6.15.0)",
"statusCode": 502,
"timestamps": {
"providerDataReceivedUnixMs": 0,
"providerDataRequestedUnixMs": 0,
},
}
`;

exports[`execute function endpoint should return success 1`] = `
{
"data": {
Expand Down Expand Up @@ -56,6 +67,21 @@ exports[`execute function endpoint should return success for different network 1
}
`;

exports[`execute function endpoint should return success with additional data requests 1`] = `
{
"data": {
"decimals": "0x0000000000000000000000000000000000000000000000000000000000000008",
"result": "0x000000000000000000000000000000000000000000000000000000005ad789f8",
},
"result": "0x000000000000000000000000000000000000000000000000000000005ad789f8",
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 978347471111,
"providerDataRequestedUnixMs": 978347471111,
},
}
`;

exports[`execute function endpoint should return success with parameters 1`] = `
{
"data": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ describe('execute', () => {
spy.mockRestore()
})

afterEach(() => {
nock.cleanAll()
})

describe('function endpoint', () => {
it('should return success', async () => {
const data = {
Expand All @@ -58,6 +62,42 @@ describe('execute', () => {
expect(response.json()).toMatchSnapshot()
})

it('should return success with additional data requests ', async () => {
const data = {
contract: '0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c',
function: 'function latestAnswer() external view returns (int256)',
network: 'ethereum_mainnet',
additionalRequests: [
{
name: 'decimals',
signature: 'function decimals() view returns (uint8)',
},
],
}
mockETHMainnetContractCallResponseSuccess()
const response = await testAdapter.request(data)
expect(response.statusCode).toBe(200)
expect(response.json()).toMatchSnapshot()
})

it('should fail additional data requests in case of missing signature', async () => {
const data = {
contract: '0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c',
function: 'function latestAnswer() external view returns (int256)',
network: 'ethereum_mainnet',
additionalRequests: [
{
name: 'decimals',
signature: '',
},
],
}
mockETHMainnetContractCallResponseSuccess()
const response = await testAdapter.request(data)
expect(response.statusCode).toBe(502)
expect(response.json()).toMatchSnapshot()
})

it('should return success for different network', async () => {
const data = {
contract: '0x779877a7b0d9e8603169ddbd7836e478b4624789',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export const mockETHMainnetContractCallResponseSuccess = (): nock.Scope =>
id: request.id,
result: '0x000000000000000000000000000000000000000000000000000000005ad789f8',
}
} else if (
request.method === 'eth_call' &&
request.params[0].to === '0x2c1d072e956affc0d435cb7ac38ef18d24d9127c' &&
request.params[0].data === '0x313ce567' // decimals()
) {
return {
jsonrpc: '2.0',
id: request.id,
result: '0x0000000000000000000000000000000000000000000000000000000000000008',
}
} else if (
request.method === 'eth_call' &&
request.params[0].to === '0x2c1d072e956affc0d435cb7ac38ef18d24d9127c' &&
Expand Down Expand Up @@ -110,6 +120,16 @@ export const mockETHGoerliContractCallResponseSuccess = (): nock.Scope =>
id: request.id,
result: '0x000000000000000000000000000000000000000000000000eead809f678d30f0',
}
} else if (
request.method === 'eth_call' &&
request.params[0].to === '0x779877a7b0d9e8603169ddbd7836e478b4624789' &&
request.params[0].data === '0x313ce567' // decimals()
) {
return {
jsonrpc: '2.0',
id: request.id,
result: '0x0000000000000000000000000000000000000000000000000000000000000018',
}
} else {
// Default response for unsupported calls
return {
Expand Down
Loading