Skip to content

Commit 5c7de6f

Browse files
Feat/OPDATA-4164 securitize (#4013)
* OPDATA-4164 Securitize new EA * add changeset * remove unused file * add unit test, small refactors to enable * add sigutils for signature verification * move tweetnacl from devdeps to deps * remove console logs * remove duplicate code block * add more detail to sig verify error messages * change error message in transport
1 parent 725e85e commit 5c7de6f

File tree

22 files changed

+1207
-0
lines changed

22 files changed

+1207
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/securitize-adapter': major
3+
---
4+
5+
Securitize Initial release

.pnp.cjs

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sources/securitize/CHANGELOG.md

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Chainlink External Adapter for securitize
2+
3+
This README will be generated automatically when code is merged to `main`. If you would like to generate a preview of the README, please run `yarn generate:readme securitize`.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@chainlink/securitize-adapter",
3+
"version": "0.0.0",
4+
"description": "Chainlink securitize adapter.",
5+
"keywords": [
6+
"Chainlink",
7+
"LINK",
8+
"blockchain",
9+
"oracle",
10+
"securitize"
11+
],
12+
"main": "dist/index.js",
13+
"types": "dist/index.d.ts",
14+
"files": [
15+
"dist"
16+
],
17+
"repository": {
18+
"url": "https://github.com/smartcontractkit/external-adapters-js",
19+
"type": "git"
20+
},
21+
"license": "MIT",
22+
"scripts": {
23+
"clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
24+
"prepack": "yarn build",
25+
"build": "tsc -b",
26+
"server": "node -e 'require(\"./index.js\").server()'",
27+
"server:dist": "node -e 'require(\"./dist/index.js\").server()'",
28+
"start": "yarn server:dist"
29+
},
30+
"devDependencies": {
31+
"@types/jest": "^29.5.14",
32+
"@types/node": "22.14.1",
33+
"nock": "13.5.6",
34+
"typescript": "5.8.3"
35+
},
36+
"dependencies": {
37+
"@chainlink/external-adapter-framework": "2.7.0",
38+
"tslib": "2.4.1",
39+
"tweetnacl": "^1.0.3"
40+
}
41+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { AdapterConfig } from '@chainlink/external-adapter-framework/config'
2+
3+
export const config = new AdapterConfig({
4+
API_KEY: {
5+
description: 'An API key for Securitize NAV',
6+
type: 'string',
7+
required: true,
8+
sensitive: true,
9+
},
10+
API_ENDPOINT: {
11+
description: 'The API endpoint for Securitize NAV',
12+
type: 'string',
13+
default: 'https://partners-api.securitize.io/asset-metrics/api/v1/nav',
14+
},
15+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { endpoint as nav } from './nav'
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2+
import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util'
3+
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
4+
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
5+
import { config } from '../config'
6+
import { getPubKeys, httpTransport } from '../transport/nav'
7+
8+
export const inputParameters = new InputParameters(
9+
{
10+
assetId: {
11+
required: true,
12+
type: 'string',
13+
description: 'The assetId of the fund',
14+
},
15+
envVarPrefix: {
16+
required: true,
17+
type: 'string',
18+
description: 'Maps the assetId to the {envVarPrefix.toUpperCase()}_PUBKEYS env var',
19+
},
20+
},
21+
[
22+
{
23+
assetId: 'c52c3d79-8317-4692-86f8-4e0dfd508672',
24+
envVarPrefix: 'testAsset',
25+
},
26+
],
27+
)
28+
29+
export type BaseEndpointTypes = {
30+
Parameters: typeof inputParameters.definition
31+
Response: SingleNumberResultResponse
32+
Settings: typeof config.settings
33+
}
34+
35+
export const endpoint = new AdapterEndpoint({
36+
name: 'nav',
37+
transport: httpTransport,
38+
inputParameters,
39+
customInputValidation: (req): AdapterInputError | undefined => {
40+
getPubKeys(req.requestContext.data.envVarPrefix)
41+
return
42+
},
43+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
2+
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
3+
import { config } from './config'
4+
import { nav } from './endpoint'
5+
6+
export const adapter = new Adapter({
7+
defaultEndpoint: nav.name,
8+
name: 'SECURITIZE',
9+
config,
10+
endpoints: [nav],
11+
rateLimiting: {
12+
tiers: {
13+
default: {
14+
rateLimit1m: 30,
15+
note: '500/minute but setting reasonable limits',
16+
},
17+
},
18+
},
19+
})
20+
21+
export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import {
2+
HttpTransport,
3+
HttpTransportConfig,
4+
} from '@chainlink/external-adapter-framework/transports'
5+
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
6+
import { BaseEndpointTypes } from '../endpoint/nav'
7+
import { validateResponseSignature } from './sigutils'
8+
9+
export interface SingleResponseSchema {
10+
assetId: string
11+
name?: string
12+
nav: number
13+
seqNum: number
14+
yieldOneDay?: string
15+
yieldSevenDay?: string
16+
recordDate: string
17+
staleness?: number
18+
signedMessage: {
19+
signature: string
20+
content: string
21+
hash: string
22+
prevHash: string | null
23+
prevSig: string | null
24+
prevContent: string | null
25+
}
26+
}
27+
28+
export interface ResponseSchema {
29+
docs: SingleResponseSchema[]
30+
}
31+
32+
export type HttpTransportTypes = BaseEndpointTypes & {
33+
Provider: {
34+
RequestBody: never
35+
ResponseBody: ResponseSchema
36+
}
37+
}
38+
39+
export function getPubKeys(assetEnvVarPrefix: string): string[] {
40+
const envVarName = `${assetEnvVarPrefix.toUpperCase()}_PUBKEYS`
41+
const pubkeys = process.env[envVarName]
42+
if (!pubkeys) {
43+
throw new AdapterInputError({
44+
message: `Missing env var ${envVarName}`,
45+
statusCode: 400,
46+
})
47+
}
48+
return pubkeys.split(',').map((s) => s.trim()) ?? []
49+
}
50+
51+
const transportConfig: HttpTransportConfig<HttpTransportTypes> = {
52+
prepareRequests: (params, config) => {
53+
return params.map((param) => {
54+
return {
55+
params: [param],
56+
request: {
57+
baseURL: config.API_ENDPOINT,
58+
headers: {
59+
apikey: config.API_KEY,
60+
},
61+
params: {
62+
assetId: param.assetId,
63+
sortBy: 'recordDate',
64+
sortOrder: 'DESC',
65+
page: 1,
66+
limit: 1,
67+
},
68+
},
69+
}
70+
})
71+
},
72+
parseResponse: (params, response) => {
73+
const asset = response?.data?.docs?.[0]
74+
return params.map((param) => {
75+
if (!asset) {
76+
return {
77+
params: param,
78+
response: {
79+
errorMessage: `The data provider didn't return any value for ${param.assetId}`,
80+
statusCode: 502,
81+
},
82+
}
83+
}
84+
85+
const timestamps = {
86+
providerIndicatedTimeUnixMs: new Date(asset.recordDate).getTime(),
87+
}
88+
89+
const pubkeys = getPubKeys(param.envVarPrefix)
90+
validateResponseSignature(asset, pubkeys)
91+
92+
if (asset.assetId !== param.assetId) {
93+
return {
94+
params: param,
95+
response: {
96+
errorMessage: `Could not find ${param.assetId} in the response`,
97+
statusCode: 502,
98+
},
99+
timestamps,
100+
}
101+
}
102+
const result = asset.nav
103+
return {
104+
params: param,
105+
response: {
106+
result,
107+
data: {
108+
result,
109+
},
110+
timestamps,
111+
},
112+
}
113+
})
114+
},
115+
}
116+
117+
// Exported for testing
118+
export class NavTransport extends HttpTransport<HttpTransportTypes> {
119+
constructor() {
120+
super(transportConfig)
121+
}
122+
}
123+
124+
export const httpTransport = new NavTransport()

0 commit comments

Comments
 (0)