Skip to content
Draft
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/heavy-readers-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/infralabs-adapter': major
---

First build with support for API authentication, signature validation, and KMS integration
5 changes: 5 additions & 0 deletions .changeset/quiet-otters-verify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/infralabs-adapter': major
---

Adapted to Infralabs' new nested response format and replaced live AWS KMS key lookups with hardcoded, rotatable public keys configured via `INFRALABS_PUBLIC_KEYS`. Removed the `KMS_*`/`AWS_*` settings and the `@aws-sdk/client-kms` dependency.
20 changes: 20 additions & 0 deletions .pnp.cjs

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

57 changes: 57 additions & 0 deletions packages/sources/infralabs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# INFRALABS

![1.0.0](https://img.shields.io/github/package-json/v/smartcontractkit/external-adapters-js?filename=packages/sources/infralabs/package.json) ![v3](https://img.shields.io/badge/framework%20version-v3-blueviolet)

This document was generated automatically. Please see [README Generator](../../scripts#readme-generator) for more info.

## Environment Variables

| Required? | Name | Description | Type | Options | Default |
| :-------: | :-----------------------: | :---------------------------------------------------------------------------: | :-----: | :-----: | :--------------------------------------------------------: |
| ✅ | API_KEY | Infralabs API key (shared across all endpoints) | string | | |
| | USHP_API_ENDPOINT | Infralabs USHP index API URL | string | | `https://ushp-index-interface.staging.infralabs.xyz/index` |
| | USHP_MAX_STALENESS_SECS | Maximum age in seconds for the USHP index value before it is considered stale | number | | `3600000` |
| | BACKGROUND_EXECUTE_MS | Milliseconds between background data refreshes | number | | `10000` |
| | KMS_KEY_TTL_MS | Milliseconds before a cached KMS public key is considered expired | number | | `60000` |
| | KMS_REGION | AWS region where the Infralabs KMS key is hosted | string | | `us-east-1` |
| ✅ | AWS_ACCESS_KEY_ID | AWS access key ID for KMS authentication | string | | |
| ✅ | AWS_SECRET_ACCESS_KEY | AWS secret access key for KMS authentication | string | | |
| | KMS_VERIFICATION_DISABLED | Disable KMS signature verification | boolean | | `true` |

---

## Data Provider Rate Limits

There are no rate limits for this adapter.

---

## Input Parameters

| Required? | Name | Description | Type | Options | Default |
| :-------: | :------: | :-----------------: | :----: | :--------------------: | :-----: |
| | endpoint | The endpoint to use | string | [ushp](#ushp-endpoint) | `ushp` |

## Ushp Endpoint

`ushp` is the only supported name for this endpoint.

### Input Params

There are no input parameters for this endpoint.

### Example

Request:

```json
{
"data": {
"endpoint": "ushp"
}
}
```

---

MIT License
40 changes: 40 additions & 0 deletions packages/sources/infralabs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@chainlink/infralabs-adapter",
"version": "1.0.0",
"description": "Chainlink external adapter for Infralabs indices",
"keywords": [
"Chainlink",
"LINK",
"blockchain",
"oracle",
"infralabs"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"repository": {
"url": "https://github.com/smartcontractkit/external-adapters-js",
"type": "git"
},
"license": "MIT",
"scripts": {
"clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
"prepack": "yarn build",
"build": "tsc -b",
"server": "node -e 'require(\"./index.js\").server()'",
"server:dist": "node -e 'require(\"./dist/index.js\").server()'",
"start": "yarn server:dist"
},
"devDependencies": {
"@types/jest": "29.5.14",
"@types/node": "22.14.1",
"nock": "13.5.6",
"typescript": "5.8.3"
},
"dependencies": {
"@chainlink/external-adapter-framework": "2.17.1",
"tslib": "2.6.3"
}
}
32 changes: 32 additions & 0 deletions packages/sources/infralabs/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AdapterConfig } from '@chainlink/external-adapter-framework/config'

// TODO change to prod default once ready
export const STAGING_USHP_API_ENDPOINT = 'https://ushp-index-interface.staging.infralabs.xyz/index'

export const config = new AdapterConfig({
API_KEY: {
description: 'Infralabs API key (shared across all endpoints)',
type: 'string',
required: true,
sensitive: true,
},
USHP_API_ENDPOINT: {
description: 'Infralabs USHP index API URL',
type: 'string',
default: STAGING_USHP_API_ENDPOINT,
},
USHP_MAX_STALENESS_SECS: {
description: 'Maximum age in seconds for the USHP index value before it is considered stale',
type: 'number',
default: 3_600_000,
},
INFRALABS_PUBLIC_KEYS: {
description:
'JSON array of PEM-encoded public keys used to verify Infralabs response signatures. ' +
'List multiple keys during a rotation window (old + new) for zero-downtime rotation — ' +
'a response is accepted if it verifies against any configured key.',
type: 'string',
required: true,
sensitive: false,
},
})
1 change: 1 addition & 0 deletions packages/sources/infralabs/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ushpEndpoint as ushp } from './ushp'
23 changes: 23 additions & 0 deletions packages/sources/infralabs/src/endpoint/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { config } from '../config'

export const inputParameters = new InputParameters({})

export type BaseEndpointTypes = {
Parameters: typeof inputParameters.definition
Settings: typeof config.settings
Provider: {
RequestBody: never
ResponseBody: string
}
Response: {
Result: string
Data: {
price: number
rawValue: string
scale: number
lastUpdatedAt: number
signature: string
}
}
}
9 changes: 9 additions & 0 deletions packages/sources/infralabs/src/endpoint/ushp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
import { ushpTransport } from '../transport/ushp'
import { BaseEndpointTypes, inputParameters } from './types'

export const ushpEndpoint = new AdapterEndpoint<BaseEndpointTypes>({
name: 'ushp',
transport: ushpTransport,
inputParameters,
})
13 changes: 13 additions & 0 deletions packages/sources/infralabs/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
import { config } from './config'
import { ushp } from './endpoint'

export const adapter = new Adapter({
defaultEndpoint: ushp.name,
name: 'INFRALABS',
config,
endpoints: [ushp],
})

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
87 changes: 87 additions & 0 deletions packages/sources/infralabs/src/transport/infralabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { HttpTransport } from '@chainlink/external-adapter-framework/transports/http'
import { BaseEndpointTypes, inputParameters } from '../endpoint/types'
import { extractSignedPayload, isFresh, isSaneSignature, parsePublicKeys, rescale } from './utils'

type RequestParams = typeof inputParameters.validated

interface InfralabsResponse {
data: {
index_name: string
value: string
scale: string
timestamp: string
schema_version: string
}
signature: string
}

export function createInfralabsTransport(
apiEndpointFn: (s: BaseEndpointTypes['Settings']) => string,
maxStalenessFn: (s: BaseEndpointTypes['Settings']) => number,
): HttpTransport<BaseEndpointTypes> {
return new HttpTransport<BaseEndpointTypes>({
prepareRequests: (params, adapterSettings) => ({
params,
request: {
url: apiEndpointFn(adapterSettings),
method: 'GET',
headers: { Authorization: `ApiKey ${adapterSettings.API_KEY}` },
responseType: 'text',
},
}),
parseResponse: (params: RequestParams[], response, adapterSettings) => {
try {
const rawResponseBody = response.data as unknown as string
const responseBody = JSON.parse(rawResponseBody) as InfralabsResponse

const publicKeys = parsePublicKeys(adapterSettings.INFRALABS_PUBLIC_KEYS)
const signedPayload = extractSignedPayload(rawResponseBody)
if (!isSaneSignature(signedPayload, publicKeys, responseBody.signature)) {
throw new Error('Signature verification failed')
}

const maxStaleness = maxStalenessFn(adapterSettings)
if (!isFresh(responseBody.data.timestamp, maxStaleness, Date.now())) {
throw new Error('Price is stale')
}

const scale = parseInt(responseBody.data.scale, 10)
const result = rescale(responseBody.data.value, scale)

return params.map((param) => ({
params: param,
response: {
result: result.toString(),
data: {
price: Number(result) / 10 ** 8,
rawValue: responseBody.data.value,
scale,
lastUpdatedAt: parseInt(responseBody.data.timestamp, 10),
signature: responseBody.signature,
},
statusCode: 200,
timestamps: {
providerDataRequestedUnixMs: 0, // overwritten by the framework with real request timing
providerDataReceivedUnixMs: 0, // overwritten by the framework with real request timing
providerIndicatedTimeUnixMs: parseInt(responseBody.data.timestamp, 10) * 1000,
},
},
}))
} catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
return params.map((param) => ({
params: param,
response: {
statusCode: 502,
errorMessage,
timestamps: {
providerDataRequestedUnixMs: 0,
providerDataReceivedUnixMs: 0,
providerIndicatedTimeUnixMs: undefined,
},
},
}))
}
},
})
}
6 changes: 6 additions & 0 deletions packages/sources/infralabs/src/transport/ushp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createInfralabsTransport } from './infralabs'

export const ushpTransport = createInfralabsTransport(
(s) => s.USHP_API_ENDPOINT,
(s) => s.USHP_MAX_STALENESS_SECS,
)
Loading
Loading