Skip to content

Commit 45b54b2

Browse files
committed
Chray's changes
1 parent 99cd639 commit 45b54b2

File tree

8 files changed

+1100
-5
lines changed

8 files changed

+1100
-5
lines changed

.changeset/warm-hornets-compete.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/ftse-sftp-adapter': major
3+
---
4+
5+
Adding Downloading and parsing logic for russell and ftse csv files from ftse sftp server
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
1-
export * from './parsing'
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 * as endpoints from './endpoint'
5+
6+
export const adapter = new Adapter({
7+
name: 'FTSE_SFTP',
8+
config,
9+
endpoints: [endpoints.sftp.endpoint],
10+
})
11+
12+
export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export const instrumentToFilePathMap: Record<string, string> = {
2+
FTSE100INDEX: '/data/valuation/uk_all_share/',
3+
Russell1000INDEX: '/data/Returns_and_Values/Russell_US_Indexes_Daily_Index_Values_Real_Time_TXT/',
4+
Russell2000INDEX: '/data/Returns_and_Values/Russell_US_Indexes_Daily_Index_Values_Real_Time_TXT/',
5+
Russell3000INDEX: '/data/Returns_and_Values/Russell_US_Indexes_Daily_Index_Values_Real_Time_TXT/',
6+
}
7+
8+
export const instrumentToFileTemplateMap: Record<string, string> = {
9+
FTSE100INDEX: 'ukallv*.csv',
10+
Russell1000INDEX: 'daily_values_russell_*.CSV',
11+
Russell2000INDEX: 'daily_values_russell_*.CSV',
12+
Russell3000INDEX: 'daily_values_russell_*.CSV',
13+
}
14+
15+
export const instrumentToFileRegexMap: Record<string, RegExp> = {
16+
FTSE100INDEX: /^ukallv\d{4}\.csv$/,
17+
Russell1000INDEX: /^daily_values_russell_\d{6}\.CSV$/,
18+
Russell2000INDEX: /^daily_values_russell_\d{6}\.CSV$/,
19+
Russell3000INDEX: /^daily_values_russell_\d{6}\.CSV$/,
20+
}
21+
22+
/**
23+
* Validates if an instrument is supported by checking if it has all required mappings
24+
* @param instrument The instrument identifier to validate
25+
* @returns true if the instrument is supported, false otherwise
26+
*/
27+
export function isInstrumentSupported(instrument: string): boolean {
28+
return !!(instrumentToFilePathMap[instrument] && instrumentToFileRegexMap[instrument])
29+
}

packages/sources/ftse-sftp/src/transport/sftp.ts

Lines changed: 170 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
11
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
2+
import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response'
23
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
34
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
4-
import { sleep } from '@chainlink/external-adapter-framework/util'
5-
import SftpClient from 'ssh2-sftp-client'
6-
import { BaseEndpointTypes } from '../endpoint/sftp'
5+
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
6+
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
7+
import SftpClient, { FileInfo } from 'ssh2-sftp-client'
8+
import { BaseEndpointTypes, IndexResponseData, inputParameters } from '../endpoint/sftp'
9+
import { CSVParserFactory } from '../parsing/factory'
10+
import {
11+
instrumentToFilePathMap,
12+
instrumentToFileRegexMap,
13+
isInstrumentSupported,
14+
} from './constants'
15+
16+
const logger = makeLogger('FTSE SFTP Adapter')
17+
18+
type RequestParams = typeof inputParameters.validated
19+
20+
interface SftpConnectionConfig {
21+
host: string
22+
port: number
23+
username: string
24+
password: string
25+
readyTimeout: number
26+
}
727

828
export class SftpTransport extends SubscriptionTransport<BaseEndpointTypes> {
929
config!: BaseEndpointTypes['Settings']
1030
endpointName!: string
31+
name!: string
32+
responseCache!: ResponseCache<BaseEndpointTypes>
1133
sftpClient: SftpClient
1234

1335
constructor() {
@@ -24,12 +46,156 @@ export class SftpTransport extends SubscriptionTransport<BaseEndpointTypes> {
2446
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
2547
this.config = adapterSettings
2648
this.endpointName = endpointName
49+
this.name = transportName
50+
this.responseCache = dependencies.responseCache
2751
}
2852

29-
async backgroundHandler(context: EndpointContext<BaseEndpointTypes>): Promise<void> {
53+
async backgroundHandler(
54+
context: EndpointContext<BaseEndpointTypes>,
55+
entries: RequestParams[],
56+
): Promise<void> {
57+
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
3058
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
3159
}
3260

61+
async handleRequest(param: RequestParams) {
62+
let response: AdapterResponse<BaseEndpointTypes['Response']>
63+
try {
64+
response = await this._handleRequest(param)
65+
} catch (e) {
66+
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
67+
response = {
68+
statusCode: 502,
69+
errorMessage,
70+
timestamps: {
71+
providerDataRequestedUnixMs: 0,
72+
providerDataReceivedUnixMs: 0,
73+
providerIndicatedTimeUnixMs: undefined,
74+
},
75+
}
76+
} finally {
77+
try {
78+
await this.sftpClient.end()
79+
logger.info('SFTP connection closed')
80+
} catch (error) {
81+
logger.error('Error closing SFTP connection:', error)
82+
}
83+
}
84+
85+
await this.responseCache.write(this.name, [{ params: param, response }])
86+
}
87+
88+
async _handleRequest(
89+
param: RequestParams,
90+
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> {
91+
const providerDataRequestedUnixMs = Date.now()
92+
93+
await this.connectToSftp()
94+
95+
const parsedData = await this.tryDownloadAndParseFile(param.instrument)
96+
97+
// Extract the numeric result based on the data type
98+
let result: number
99+
if ('gbpIndex' in parsedData) {
100+
// FTSE data
101+
result = (parsedData.gbpIndex as number) ?? 0
102+
} else if ('close' in parsedData) {
103+
// Russell data
104+
result = parsedData.close as number
105+
} else {
106+
throw new Error('Unknown data format received from parser')
107+
}
108+
109+
logger.info(`Successfully processed data for instrument: ${param.instrument}`)
110+
return {
111+
data: {
112+
result: parsedData,
113+
},
114+
statusCode: 200,
115+
result,
116+
timestamps: {
117+
providerDataRequestedUnixMs,
118+
providerDataReceivedUnixMs: Date.now(),
119+
providerIndicatedTimeUnixMs: undefined,
120+
},
121+
}
122+
}
123+
124+
private async connectToSftp(): Promise<void> {
125+
const connectConfig: SftpConnectionConfig = {
126+
host: this.config.SFTP_HOST,
127+
port: this.config.SFTP_PORT || 22,
128+
username: this.config.SFTP_USERNAME,
129+
password: this.config.SFTP_PASSWORD,
130+
readyTimeout: 30000,
131+
}
132+
133+
try {
134+
// Create a new client instance to avoid connection state issues
135+
this.sftpClient = new SftpClient()
136+
await this.sftpClient.connect(connectConfig)
137+
logger.info('Successfully connected to SFTP server')
138+
} catch (error) {
139+
logger.error(error, 'Failed to connect to SFTP server')
140+
throw new AdapterInputError({
141+
statusCode: 500,
142+
message: `Failed to connect to SFTP server: ${
143+
error instanceof Error ? error.message : 'Unknown error'
144+
}`,
145+
})
146+
}
147+
}
148+
149+
private async tryDownloadAndParseFile(instrument: string): Promise<IndexResponseData> {
150+
// Validate that the instrument is supported
151+
if (!isInstrumentSupported(instrument)) {
152+
throw new AdapterInputError({
153+
statusCode: 400,
154+
message: `Unsupported instrument: ${instrument}`,
155+
})
156+
}
157+
158+
const filePath = instrumentToFilePathMap[instrument]
159+
const fileRegex = instrumentToFileRegexMap[instrument]
160+
161+
const fileList = await this.sftpClient.list(filePath)
162+
// Filter files based on the regex pattern
163+
const matchingFiles = fileList
164+
.map((file: FileInfo) => file.name)
165+
.filter((fileName: string) => fileRegex.test(fileName))
166+
167+
if (matchingFiles.length === 0) {
168+
throw new AdapterInputError({
169+
statusCode: 500,
170+
message: `No files matching pattern ${fileRegex} found in directory: ${filePath}`,
171+
})
172+
} else if (matchingFiles.length > 1) {
173+
throw new AdapterInputError({
174+
statusCode: 500,
175+
message: `Multiple files matching pattern ${fileRegex} found in directory: ${filePath}.`,
176+
})
177+
}
178+
const fullPath = `${filePath}${matchingFiles[0]}`
179+
180+
// Log the download attempt
181+
logger.info(`Downloading file: ${fullPath}`)
182+
183+
const fileContent = await this.sftpClient.get(fullPath)
184+
// we need latin1 here because the file contains special characters like "®"
185+
const csvContent = fileContent.toString('latin1')
186+
187+
const parser = CSVParserFactory.detectParserByInstrument(instrument)
188+
189+
if (!parser) {
190+
throw new AdapterInputError({
191+
statusCode: 500,
192+
message: `Parser initialization failed for instrument: ${instrument}`,
193+
})
194+
}
195+
196+
return (await parser.parse(csvContent)) as IndexResponseData
197+
}
198+
33199
getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number {
34200
return adapterSettings.BACKGROUND_EXECUTE_MS || 60000
35201
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`execute ftse_sftp endpoint should return success for FTSE100INDEX 1`] = `
4+
{
5+
"data": {
6+
"result": {
7+
"gbpIndex": 8045.12345678,
8+
"indexBaseCurrency": "GBP",
9+
"indexCode": "UKX",
10+
"indexSectorName": "FTSE 100 Index",
11+
"numberOfConstituents": 100,
12+
},
13+
},
14+
"result": 8045.12345678,
15+
"statusCode": 200,
16+
"timestamps": {
17+
"providerDataReceivedUnixMs": 1641035471111,
18+
"providerDataRequestedUnixMs": 1641035471111,
19+
},
20+
}
21+
`;
22+
23+
exports[`execute ftse_sftp endpoint should return success for Russell1000INDEX 1`] = `
24+
{
25+
"data": {
26+
"result": {
27+
"close": 2654.123456,
28+
"indexName": "Russell 1000® Index",
29+
},
30+
},
31+
"result": 2654.123456,
32+
"statusCode": 200,
33+
"timestamps": {
34+
"providerDataReceivedUnixMs": 1641035471111,
35+
"providerDataRequestedUnixMs": 1641035471111,
36+
},
37+
}
38+
`;
39+
40+
exports[`execute ftse_sftp endpoint should return success for Russell2000INDEX 1`] = `
41+
{
42+
"data": {
43+
"result": {
44+
"close": 1987.654321,
45+
"indexName": "Russell 2000® Index",
46+
},
47+
},
48+
"result": 1987.654321,
49+
"statusCode": 200,
50+
"timestamps": {
51+
"providerDataReceivedUnixMs": 1641035471111,
52+
"providerDataRequestedUnixMs": 1641035471111,
53+
},
54+
}
55+
`;
56+
57+
exports[`execute ftse_sftp endpoint should return success for Russell3000INDEX 1`] = `
58+
{
59+
"data": {
60+
"result": {
61+
"close": 3456.789012,
62+
"indexName": "Russell 3000® Index",
63+
},
64+
},
65+
"result": 3456.789012,
66+
"statusCode": 200,
67+
"timestamps": {
68+
"providerDataReceivedUnixMs": 1641035471111,
69+
"providerDataRequestedUnixMs": 1641035471111,
70+
},
71+
}
72+
`;

0 commit comments

Comments
 (0)