Skip to content

Commit 4cdf40b

Browse files
Initial draft
1 parent 46f102b commit 4cdf40b

File tree

14 files changed

+380
-0
lines changed

14 files changed

+380
-0
lines changed

packages/sources/nav-libre/CHANGELOG.md

Whitespace-only changes.

packages/sources/nav-libre/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Chainlink External Adapter for nav-libre
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 nav-libre`.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@chainlink/nav-libre-adapter",
3+
"version": "0.0.0",
4+
"description": "Chainlink nav-libre adapter.",
5+
"keywords": [
6+
"Chainlink",
7+
"LINK",
8+
"blockchain",
9+
"oracle",
10+
"nav-libre"
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/crypto-js": "^4",
32+
"@types/jest": "27.5.2",
33+
"@types/node": "16.18.119",
34+
"nock": "13.5.5",
35+
"typescript": "5.6.3"
36+
},
37+
"dependencies": {
38+
"@chainlink/external-adapter-framework": "2.6.0",
39+
"crypto-js": "^4.2.0",
40+
"dayjs": "^1.11.13",
41+
"tslib": "2.4.1",
42+
"uuid": "^11.1.0"
43+
}
44+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 Data Provider',
6+
type: 'string',
7+
required: true,
8+
sensitive: true,
9+
},
10+
SECRET_KEY: {
11+
description: 'A key for Data Provider used in hashing the API key',
12+
type: 'string',
13+
required: true,
14+
sensitive: true,
15+
},
16+
API_ENDPOINT: {
17+
description: 'An API endpoint for Data Provider',
18+
type: 'string',
19+
default: 'https://api.navfundservices.com/',
20+
},
21+
MAX_RETRIES: {
22+
description: 'Maximum attempts of sending a request',
23+
type: 'number',
24+
default: 3,
25+
},
26+
})
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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2+
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
3+
import dayjs from 'dayjs'
4+
import { config } from '../config'
5+
import { httpTransport } from '../transport/nav'
6+
7+
// Input parameters define the structure of the request expected by the endpoint. The second parameter defines example input data that will be used in EA readme
8+
export const inputParameters = new InputParameters(
9+
{
10+
globalFundID: {
11+
required: true,
12+
type: 'number',
13+
description: 'The global fund ID for the Libre fund',
14+
},
15+
fromDate: {
16+
required: false,
17+
type: 'string',
18+
description: 'Start date in MM-DD-YYYY format (defaults to 7 days ago)',
19+
default: dayjs().subtract(7, 'day').format('MM-DD-YYYY'),
20+
},
21+
toDate: {
22+
required: false,
23+
type: 'string',
24+
description: 'End date in MM-DD-YYYY format (defaults to today)',
25+
default: dayjs().format('MM-DD-YYYY'),
26+
},
27+
},
28+
[
29+
{
30+
globalFundID: 139767,
31+
fromDate: '12-30-2024',
32+
toDate: '01-15-2025',
33+
},
34+
],
35+
)
36+
export type NavResultResponse = {
37+
Result: number
38+
Data: {
39+
navPerShare: number
40+
navDate: string
41+
globalFundID: number
42+
}
43+
}
44+
45+
export type BaseEndpointTypes = {
46+
Parameters: typeof inputParameters.definition
47+
Response: NavResultResponse
48+
Settings: typeof config.settings
49+
}
50+
51+
export const endpoint = new AdapterEndpoint({
52+
name: 'nav',
53+
aliases: [],
54+
transport: httpTransport,
55+
inputParameters,
56+
})
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
//Requests will direct to this endpoint if the `endpoint` input parameter is not specified.
8+
defaultEndpoint: nav.name,
9+
// Adapter name
10+
name: 'NAV_LIBRE',
11+
// Adapter configuration (environment variables)
12+
config,
13+
// List of supported endpoints
14+
endpoints: [nav],
15+
})
16+
17+
export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import CryptoJS from 'crypto-js'
2+
import { v4 as uuidv4 } from 'uuid'
3+
4+
/**
5+
* Generate the necessary headers for calling the NAV API with a 5-minute-valid signature.
6+
*/
7+
export const getNavRequestHeaders = (
8+
method: string,
9+
path: string,
10+
body: string,
11+
apiKey: string,
12+
secret: string,
13+
) => {
14+
const utcNow = new Date().toUTCString()
15+
const nonce = uuidv4()
16+
const contentHash = CryptoJS.SHA256(body).toString(CryptoJS.enc.Base64)
17+
const stringToSign = [apiKey, path, method, utcNow, nonce, contentHash].join(';')
18+
19+
// 5. Compute the HMAC-SHA256 signature, Base64-encoded
20+
const signature = CryptoJS.HmacSHA256(stringToSign, secret).toString(CryptoJS.enc.Base64)
21+
22+
return {
23+
'x-date': utcNow,
24+
'x-content-sha256': contentHash,
25+
'x-hmac256-signature': `${apiKey};${nonce};${signature}`,
26+
}
27+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { HttpTransport } from '@chainlink/external-adapter-framework/transports'
2+
import dayjs from 'dayjs'
3+
import { BaseEndpointTypes } from '../endpoint/nav'
4+
import { getNavRequestHeaders } from './authentication'
5+
6+
export interface ResponseSchema {
7+
Data: {
8+
'Trading Level Net ROR': {
9+
DTD: number
10+
MTD: number
11+
QTD: number
12+
YTD: number
13+
ITD: number
14+
}
15+
'Net ROR': {
16+
DTD: number
17+
MTD: number
18+
QTD: number
19+
YTD: number
20+
ITD: number
21+
}
22+
'NAV Per Share': number
23+
'Next NAV Price': number
24+
'Accounting Date': string
25+
'Ending Balance': number
26+
}[]
27+
}
28+
29+
// HttpTransport extends base types from endpoint and adds additional, Provider-specific types like 'RequestBody', which is the type of
30+
// request body (not the request to adapter, but the request that adapter sends to Data Provider), and 'ResponseBody' which is
31+
// the type of raw response from Data Provider
32+
export type HttpTransportTypes = BaseEndpointTypes & {
33+
Provider: {
34+
RequestBody: never
35+
ResponseBody: ResponseSchema
36+
}
37+
}
38+
// HttpTransport is used to fetch and process data from a Provider using HTTP(S) protocol. It usually needs two methods
39+
// `prepareRequests` and `parseResponse`
40+
export const httpTransport = new HttpTransport<HttpTransportTypes>({
41+
prepareRequests: (params, config) => {
42+
return params.map((param) => {
43+
// Set defaults for fromDate and toDate if not provided
44+
const now = dayjs()
45+
const fromDate = param.fromDate || now.subtract(7, 'day').format('MM-DD-YYYY')
46+
const toDate = param.toDate || now.format('MM-DD-YYYY')
47+
48+
// Validate date format MM-DD-YYYY
49+
const dateRegex = /^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])-\d{4}$/
50+
if (fromDate && !dateRegex.test(fromDate)) {
51+
throw new Error('fromDate must be in MM-DD-YYYY format')
52+
}
53+
if (toDate && !dateRegex.test(toDate)) {
54+
throw new Error('toDate must be in MM-DD-YYYY format')
55+
}
56+
57+
const method = 'GET'
58+
const path =
59+
'/navapigateway/api/v1/FundAccountingData/GetOfficialNAVAndPerformanceReturnsForFund'
60+
const query = `globalFundID=${param.globalFundID}&fromDate=${fromDate}&toDate=${toDate}`
61+
const body = ''
62+
63+
const headers = getNavRequestHeaders(
64+
method,
65+
path + '?' + query,
66+
body,
67+
config.API_KEY,
68+
config.SECRET_KEY,
69+
)
70+
return {
71+
params: [param],
72+
request: {
73+
baseURL: config.API_ENDPOINT,
74+
url: path,
75+
headers,
76+
params: {
77+
globalFundID: param.globalFundID,
78+
fromDate: param.fromDate,
79+
toDate: param.toDate,
80+
},
81+
},
82+
}
83+
})
84+
},
85+
parseResponse: (params, response) => {
86+
if (!response.data || !Array.isArray(response.data.Data) || response.data.Data.length === 0) {
87+
return params.map((param) => ({
88+
params: param,
89+
response: {
90+
errorMessage: `No NAV data returned for fund ${param.globalFundID}`,
91+
statusCode: 502,
92+
},
93+
}))
94+
}
95+
96+
// Find the latest NAV entry by Accounting Date
97+
const latest = response.data.Data.reduce((a, b) => {
98+
return new Date(a['Accounting Date']) > new Date(b['Accounting Date']) ? a : b
99+
})
100+
101+
return params.map((param) => ({
102+
params: param,
103+
response: {
104+
result: latest['NAV Per Share'],
105+
data: {
106+
navPerShare: latest['NAV Per Share'],
107+
navDate: latest['Accounting Date'],
108+
globalFundID: param.globalFundID,
109+
},
110+
},
111+
}))
112+
},
113+
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"requests": [{
3+
"from": "BTC",
4+
"to": "USD"
5+
}]
6+
}

0 commit comments

Comments
 (0)