Skip to content
Merged

R25 EA #4346

Show file tree
Hide file tree
Changes from 7 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/fresh-ducks-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/r25-adapter': major
---

This change contains an external adapter for the r25 endpoint
22 changes: 22 additions & 0 deletions .pnp.cjs

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

42 changes: 42 additions & 0 deletions packages/sources/r25/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@chainlink/r25-adapter",
"version": "0.0.0",
"description": "Chainlink r25 adapter.",
"keywords": [
"Chainlink",
"LINK",
"blockchain",
"oracle",
"r25"
],
"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"
},
"dependencies": {
"@chainlink/external-adapter-framework": "2.11.1",
"crypto-js": "4.2.0",
"tslib": "2.4.1"
},
"devDependencies": {
"@types/crypto-js": "4.2.2",
"@types/jest": "^29.5.14",
"@types/node": "22.14.1",
"nock": "13.5.6",
"typescript": "5.8.3"
}
}
21 changes: 21 additions & 0 deletions packages/sources/r25/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AdapterConfig } from '@chainlink/external-adapter-framework/config'

export const config = new AdapterConfig({
API_KEY: {
description: 'An API key for R25',
type: 'string',
required: true,
sensitive: true,
},
API_SECRET: {
description: 'An API secret for R25 used to sign requests',
type: 'string',
required: true,
sensitive: true,
},
API_ENDPOINT: {
description: 'An API endpoint for R25',
type: 'string',
default: 'https://app.r25.xyz',
},
})
1 change: 1 addition & 0 deletions packages/sources/r25/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { endpoint as nav } from './nav'
38 changes: 38 additions & 0 deletions packages/sources/r25/src/endpoint/nav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { config } from '../config'
import { httpTransport } from '../transport/nav'

export const inputParameters = new InputParameters(
{
chainType: {
type: 'string',
description: 'The chain type (e.g., polygon, sui)',
required: true,
},
tokenName: {
type: 'string',
description: 'The token name (e.g., rcusdp)',
required: true,
},
},
[
{
chainType: 'polygon',
tokenName: 'rcusdp',
},
],
)

export type BaseEndpointTypes = {
Parameters: typeof inputParameters.definition
Response: SingleNumberResultResponse
Settings: typeof config.settings
}

export const endpoint = new AdapterEndpoint({
name: 'nav',
transport: httpTransport,
inputParameters,
})
20 changes: 20 additions & 0 deletions packages/sources/r25/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
import { config } from './config'
import { endpoint as navEndpoint } from './endpoint/nav'

export const adapter = new Adapter({
defaultEndpoint: navEndpoint.name,
name: 'R25',
config,
endpoints: [navEndpoint],
rateLimiting: {
tiers: {
default: {
rateLimit1s: 5, //5 requests per second
},
},
},
})

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
54 changes: 54 additions & 0 deletions packages/sources/r25/src/transport/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import CryptoJS from 'crypto-js'

export interface GetRequestHeadersParams {
method: string
path: string
params: Record<string, string>
apiKey: string
secret: string
timestamp: number
}

/**
* Generate the HMAC-SHA256 signature for R25 API requests.
*
* The signature string is constructed as:
* {method}\n{path}\n{sorted_params}\n{timestamp}\n{api_key}
*
* Where:
* - method: HTTP method in lowercase (e.g., "get")
* - path: Request path (e.g., "/api/public/current/nav")
* - sorted_params: Query parameters sorted by key, formatted as key=value, joined with &
* - timestamp: Current UTC timestamp in milliseconds
* - api_key: API key
*/
export const getRequestHeaders = ({
method,
path,
params,
apiKey,
secret,
timestamp,
}: GetRequestHeadersParams): Record<string, string> => {
// Sort parameters by key in lexicographical order and format as key=value
const sortedParams = Object.keys(params)
.sort()
.map((key) => `${key}=${params[key]}`)
.join('&')

const stringToSign = [
method.toLowerCase(),
path,
sortedParams,
timestamp.toString(),
apiKey,
].join('\n')

const signature = CryptoJS.HmacSHA256(stringToSign, secret).toString(CryptoJS.enc.Hex)

return {
'x-api-key': apiKey,
'x-utc-timestamp': timestamp.toString(),
'x-signature': signature,
}
}
86 changes: 86 additions & 0 deletions packages/sources/r25/src/transport/nav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { HttpTransport } from '@chainlink/external-adapter-framework/transports'
import { BaseEndpointTypes } from '../endpoint/nav'
import { getRequestHeaders } from './authentication'

export interface ResponseSchema {
code: string
success: boolean
message: string
data: {
lastUpdate: string
tokenName: string
chainType: string
totalSupply: number
totalAsset: number
currentNav: string
}
}

export type HttpTransportTypes = BaseEndpointTypes & {
Provider: {
RequestBody: never
ResponseBody: ResponseSchema
}
}

export const httpTransport = new HttpTransport<HttpTransportTypes>({
prepareRequests: (params, config) => {
return params.map((param) => {
const method = 'GET'
const path = '/api/public/current/nav'
const timestamp = Date.now()

const queryParams = {
chainType: param.chainType,
tokenName: param.tokenName,
}

const headers = getRequestHeaders({
method,
path,
params: queryParams,
apiKey: config.API_KEY,
secret: config.API_SECRET,
timestamp,
})

return {
params: [param],
request: {
baseURL: config.API_ENDPOINT,
url: path,
params: queryParams,
headers,
},
}
})
},
parseResponse: (params, response) => {
return params.map((param) => {
const apiResponse = response.data as ResponseSchema
if (!apiResponse.success) {
return {
params: param,
response: {
errorMessage: apiResponse.message || 'Request failed',
statusCode: 502,
},
}
}

const result = Number(apiResponse.data.currentNav)
return {
params: param,
response: {
result,
data: {
result,
},
timestamps: {
providerIndicatedTimeUnixMs: new Date(apiResponse.data.lastUpdate).getTime(),
},
},
}
})
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`execute nav endpoint should return error for invalid chainType 1`] = `
{
"errorMessage": "Invalid chainType combination",
"statusCode": 502,
"timestamps": {
"providerDataReceivedUnixMs": 978347471111,
"providerDataRequestedUnixMs": 978347471111,
},
}
`;

exports[`execute nav endpoint should return error for invalid chainType and tokenName 1`] = `
{
"errorMessage": "Invalid tokenName combination",
"statusCode": 502,
"timestamps": {
"providerDataReceivedUnixMs": 978347471111,
"providerDataRequestedUnixMs": 978347471111,
},
}
`;

exports[`execute nav endpoint should return error for invalid token 1`] = `
{
"errorMessage": "Invalid tokenName combination",
"statusCode": 502,
"timestamps": {
"providerDataReceivedUnixMs": 978347471111,
"providerDataRequestedUnixMs": 978347471111,
},
}
`;

exports[`execute nav endpoint should return success 1`] = `
{
"data": {
"result": 1.020408163265306,
},
"result": 1.020408163265306,
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 978347471111,
"providerDataRequestedUnixMs": 978347471111,
"providerIndicatedTimeUnixMs": 1762880153448,
},
}
`;
Loading
Loading