Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/plenty-garlics-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/proof-of-reserves-adapter': minor
---

Add support for decimals value being returned by view-function-multi-chain
1 change: 1 addition & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 13 additions & 4 deletions packages/composites/proof-of-reserves/src/utils/reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,28 @@ export const runReduceAdapter = async (
})
}
break
case viewFunctionMultiChain.name:
if (!viewFunctionIndexerResultDecimals) {
case viewFunctionMultiChain.name: {
let decimalsOffset: number

const decimalsHex = input.data.decimals
if (decimalsHex != null) {
decimalsOffset = 18 - Number(decimalsHex)
} else if (viewFunctionIndexerResultDecimals != null) {
decimalsOffset = 18 - Number(viewFunctionIndexerResultDecimals)
} else {
throw new Error(
'viewFunctionIndexerResultDecimals is a required parameter when using the view-function-multi-chain indexer',
`Missing decimals: neither input.data.decimals nor viewFunctionIndexerResultDecimals provided`,
)
}

return returnParsedUnits(
input.jobRunID,
parseHexToBigInt(input.data.result).toString(),
18 - (viewFunctionIndexerResultDecimals as number),
decimalsOffset,
false,
18,
)
}
}

const next = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ exports[`execute multiReserves endpoint should return success 1`] = `
}
`;

exports[`execute multiReserves endpoint view-function-multi-chain fails 1`] = `
exports[`execute multiReserves endpoint view-function-multi-chain should fail 1`] = `
{
"error": {
"feedID": "054baf47a9ff27c8384416a3ce83c17c",
"message": "viewFunctionIndexerResultDecimals is a required parameter when using the view-function-multi-chain indexer",
"message": "Missing decimals: neither input.data.decimals nor viewFunctionIndexerResultDecimals provided",
"name": "Error",
},
"jobRunID": "1",
Expand All @@ -80,6 +80,19 @@ exports[`execute multiReserves endpoint view-function-multi-chain fails 1`] = `
}
`;

exports[`execute multiReserves endpoint view-function-multi-chain should return success - decimals value 1`] = `
{
"data": {
"decimals": 18,
"result": "184467440737095516150000000000",
"statusCode": 200,
},
"jobRunID": "1",
"result": "184467440737095516150000000000",
"statusCode": 200,
}
`;

exports[`execute multiReserves endpoint view-function-multi-chain should return success 1`] = `
{
"data": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
mockLotusSuccess,
mockPoRindexerSuccess,
mockViewFunctionMultiChainSuccess,
mockViewFunctionMultiChainSuccess2,
} from './fixtures'

describe('execute', () => {
Expand Down Expand Up @@ -223,7 +224,41 @@ describe('execute', () => {
expect(response.body).toMatchSnapshot()
})

it('view-function-multi-chain fails', async () => {
it('view-function-multi-chain should return success - decimals value', async () => {
const data: AdapterRequest = {
id: '1',
data: {
endpoint: 'reserves',
protocol: 'list',
addresses: [''],
indexer: 'view_function_multi_chain',
indexerEndpoint: 'function',
indexerParams: {
signature: 'function getPending() public view returns (uint256)',
address: '0xa69b964a597435A2F938cc55FaAbe34F2A9AF278',
network: 'BASE',
data: {
decimals: {
signature: 'function decimals() view returns (uint8)',
},
},
},
disableDuplicateAddressFiltering: true,
},
}
mockViewFunctionMultiChainSuccess2()

const response = await (context.req as SuperTest<Test>)
.post('/')
.send(data)
.set('Accept', '*/*')
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
expect(response.body).toMatchSnapshot()
})

it('view-function-multi-chain should fail', async () => {
const data: AdapterRequest = {
id: '1',
data: {
Expand All @@ -249,7 +284,6 @@ describe('execute', () => {
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(500)

expect(response.body).toMatchSnapshot()
})
})
Expand Down
18 changes: 18 additions & 0 deletions packages/composites/proof-of-reserves/test/integration/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ export const mockViewFunctionMultiChainSuccess = (): nock.Scope => {
})
}

export const mockViewFunctionMultiChainSuccess2 = (): nock.Scope => {
return nock('https://view-function-multi-chain-adapter.com')
.post('/')
.reply(200, {
jobRunID: '1',
result: '0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF',
statusCode: 200,
data: {
result: '0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF',
decimals: '0x0000000000000000000000000000000000000000000000000000000000000008',
},
metricsMeta: {
feedId:
'{"signature":"function getpending() public view returns (uint256)","address":"0xa69b964a597435a2f938cc55faabe34f2a9af278","inputParams":[],"network":"base"}',
},
})
}

export const mockPoRindexerSuccess = (): nock.Scope => {
return nock('https://por-indexer-adapter.com')
.post('/')
Expand Down
1 change: 1 addition & 0 deletions packages/sources/view-function-multi-chain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"dependencies": {
"@chainlink/external-adapter-framework": "2.8.0",
"ethers": "^6.13.2",
"p-limit": "3",
"tslib": "2.4.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export const inputParamDefinition = {
description: 'RPC network name',
type: 'string',
},
additionalRequests: {
description: 'Optional map of function calls',
type: 'object' as unknown as Record<string, any>,
required: false,
},
} as const

export const inputParameters = new InputParameters(inputParamDefinition)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import {
} 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 {
AdapterError,
AdapterInputError,
} from '@chainlink/external-adapter-framework/validation/error'
import { ethers } from 'ethers'
import pLimit from 'p-limit'

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

Expand All @@ -16,6 +20,7 @@ interface RequestParams {
inputParams?: Array<string>
network: string
resultField?: string
additionalRequests?: Record<string, RequestParams>
}

export type RawOnchainResponse = {
Expand Down Expand Up @@ -80,7 +85,50 @@ export class MultiChainFunctionTransport<
}

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

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

if (mainResult.status === 'rejected') {
throw new AdapterError({
statusCode: mainResult.reason?.statusCode || null,
message: `${mainResult.reason}`,
})
}

// Nested result is optional
const nestedResults =
nestedResultOutcome.status === 'fulfilled'
? nestedResultOutcome.value
: (console.warn('Nested result failed:', nestedResultOutcome.reason), null)

const combinedData = { result: mainResult.value.result, ...nestedResults }

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

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,30 +150,78 @@ 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: Record<string, RequestParams> | undefined,
parentAddress: string,
parentNetwork: string,
): Promise<Record<string, any>> {
const limit = pLimit(5)
const results: Record<string, any> = {}

if (!additionalRequests || typeof additionalRequests !== 'object') return results

const tasks = Object.entries(additionalRequests).map(([key, subReq]) =>
limit(async () => {
try {
const req = subReq as RequestParams

if (!req.signature) {
logger.warn(`Skipping nested key "${key}" — no signature provided.`)
return [key, null]
}

const nestedParam = {
address: req.address || parentAddress,
network: req.network || parentNetwork,
signature: req.signature,
inputParams: req.inputParams,
resultField: req.resultField,
}

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

const settled = await Promise.allSettled(tasks)

for (const outcome of settled) {
if (outcome.status === 'fulfilled') {
const [key, value] = outcome.value as [string, string]
results[key] = value
}
}

return results
}

getSubscriptionTtlFromConfig(adapterSettings: T['Settings']): number {
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)"
}
}
}]
}
Loading
Loading