Skip to content

Commit 0ac3976

Browse files
committed
David's changes
1 parent 45b54b2 commit 0ac3976

File tree

11 files changed

+420
-902
lines changed

11 files changed

+420
-902
lines changed
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
import * as sftp from './sftp'
2-
3-
export { sftp }
1+
export { endpoint as sftp } from './sftp'

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
22
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
3+
import { AdapterError } from '@chainlink/external-adapter-framework/validation/error'
34
import { config } from '../config'
45
import { FTSE100Data } from '../parsing/ftse100'
56
import { RussellDailyValuesData } from '../parsing/russell'
7+
import { validateInstrument } from '../transport/constants'
68
import { sftpTransport } from '../transport/sftp'
79

810
export const inputParameters = new InputParameters(
@@ -29,8 +31,6 @@ export const inputParameters = new InputParameters(
2931
],
3032
)
3133

32-
export type TInputParameters = typeof inputParameters.definition
33-
3434
/**
3535
* Union type for all possible response data structures
3636
*/
@@ -41,14 +41,20 @@ export type BaseEndpointTypes = {
4141
Response: {
4242
Result: number
4343
Data: {
44+
filename: string
4445
result: IndexResponseData
4546
}
4647
}
4748
Settings: typeof config.settings
4849
}
4950

5051
export const endpoint = new AdapterEndpoint({
51-
name: 'ftse_sftp',
52+
name: 'sftp',
5253
transport: sftpTransport,
5354
inputParameters,
55+
customInputValidation: (request, _settings): AdapterError | undefined => {
56+
const { instrument } = request.requestContext.data
57+
validateInstrument(instrument)
58+
return
59+
},
5460
})
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
22
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
33
import { config } from './config'
4-
import * as endpoints from './endpoint'
4+
import { sftp } from './endpoint'
55

66
export const adapter = new Adapter({
7+
defaultEndpoint: sftp.name,
78
name: 'FTSE_SFTP',
89
config,
9-
endpoints: [endpoints.sftp.endpoint],
10+
endpoints: [sftp],
1011
})
1112

1213
export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
export const instrumentToFilePathMap: Record<string, string> = {
1+
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
2+
3+
export const instrumentToDirectoryMap: Record<string, string> = {
24
FTSE100INDEX: '/data/valuation/uk_all_share/',
35
Russell1000INDEX: '/data/Returns_and_Values/Russell_US_Indexes_Daily_Index_Values_Real_Time_TXT/',
46
Russell2000INDEX: '/data/Returns_and_Values/Russell_US_Indexes_Daily_Index_Values_Real_Time_TXT/',
57
Russell3000INDEX: '/data/Returns_and_Values/Russell_US_Indexes_Daily_Index_Values_Real_Time_TXT/',
68
}
79

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-
1510
export const instrumentToFileRegexMap: Record<string, RegExp> = {
1611
FTSE100INDEX: /^ukallv\d{4}\.csv$/,
1712
Russell1000INDEX: /^daily_values_russell_\d{6}\.CSV$/,
@@ -24,6 +19,15 @@ export const instrumentToFileRegexMap: Record<string, RegExp> = {
2419
* @param instrument The instrument identifier to validate
2520
* @returns true if the instrument is supported, false otherwise
2621
*/
27-
export function isInstrumentSupported(instrument: string): boolean {
28-
return !!(instrumentToFilePathMap[instrument] && instrumentToFileRegexMap[instrument])
22+
const isInstrumentSupported = (instrument: string): boolean => {
23+
return !!(instrumentToDirectoryMap[instrument] && instrumentToFileRegexMap[instrument])
24+
}
25+
26+
export const validateInstrument = (instrument: string): void => {
27+
if (!isInstrumentSupported(instrument)) {
28+
throw new AdapterInputError({
29+
statusCode: 400,
30+
message: `Unsupported instrument: ${instrument}`,
31+
})
32+
}
2933
}

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

Lines changed: 29 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,24 @@
11
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
2-
import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response'
32
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
43
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
54
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
65
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
7-
import SftpClient, { FileInfo } from 'ssh2-sftp-client'
6+
import { ConnectOptions } from 'ssh2-sftp-client'
87
import { BaseEndpointTypes, IndexResponseData, inputParameters } from '../endpoint/sftp'
98
import { CSVParserFactory } from '../parsing/factory'
10-
import {
11-
instrumentToFilePathMap,
12-
instrumentToFileRegexMap,
13-
isInstrumentSupported,
14-
} from './constants'
9+
import { instrumentToDirectoryMap, instrumentToFileRegexMap, validateInstrument } from './constants'
10+
import { getFileContentsFromFileRegex } from './utils'
1511

1612
const logger = makeLogger('FTSE SFTP Adapter')
1713

1814
type RequestParams = typeof inputParameters.validated
1915

20-
interface SftpConnectionConfig {
21-
host: string
22-
port: number
23-
username: string
24-
password: string
25-
readyTimeout: number
26-
}
27-
2816
export class SftpTransport extends SubscriptionTransport<BaseEndpointTypes> {
2917
config!: BaseEndpointTypes['Settings']
3018
endpointName!: string
31-
name!: string
32-
responseCache!: ResponseCache<BaseEndpointTypes>
33-
sftpClient: SftpClient
3419

3520
constructor() {
3621
super()
37-
this.sftpClient = new SftpClient()
3822
}
3923

4024
async initialize(
@@ -45,9 +29,6 @@ export class SftpTransport extends SubscriptionTransport<BaseEndpointTypes> {
4529
): Promise<void> {
4630
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
4731
this.config = adapterSettings
48-
this.endpointName = endpointName
49-
this.name = transportName
50-
this.responseCache = dependencies.responseCache
5132
}
5233

5334
async backgroundHandler(
@@ -73,13 +54,6 @@ export class SftpTransport extends SubscriptionTransport<BaseEndpointTypes> {
7354
providerIndicatedTimeUnixMs: undefined,
7455
},
7556
}
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-
}
8357
}
8458

8559
await this.responseCache.write(this.name, [{ params: param, response }])
@@ -90,25 +64,12 @@ export class SftpTransport extends SubscriptionTransport<BaseEndpointTypes> {
9064
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> {
9165
const providerDataRequestedUnixMs = Date.now()
9266

93-
await this.connectToSftp()
94-
95-
const parsedData = await this.tryDownloadAndParseFile(param.instrument)
67+
const { filename, result, parsedData } = await this.tryDownloadAndParseFile(param.instrument)
9668

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}`)
69+
logger.debug(`Successfully processed data for instrument: ${param.instrument}`)
11070
return {
11171
data: {
72+
filename,
11273
result: parsedData,
11374
},
11475
statusCode: 200,
@@ -121,66 +82,30 @@ export class SftpTransport extends SubscriptionTransport<BaseEndpointTypes> {
12182
}
12283
}
12384

124-
private async connectToSftp(): Promise<void> {
125-
const connectConfig: SftpConnectionConfig = {
85+
private async tryDownloadAndParseFile(instrument: string): Promise<{
86+
filename: string
87+
result: number
88+
parsedData: IndexResponseData
89+
}> {
90+
validateInstrument(instrument)
91+
92+
const connectOptions: ConnectOptions = {
12693
host: this.config.SFTP_HOST,
127-
port: this.config.SFTP_PORT || 22,
94+
port: this.config.SFTP_PORT,
12895
username: this.config.SFTP_USERNAME,
12996
password: this.config.SFTP_PASSWORD,
13097
readyTimeout: 30000,
13198
}
13299

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-
}
100+
const directory = instrumentToDirectoryMap[instrument]
101+
const filenameRegex = instrumentToFileRegexMap[instrument]
148102

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-
}
103+
const { filename, fileContent } = await getFileContentsFromFileRegex({
104+
connectOptions,
105+
directory,
106+
filenameRegex,
107+
})
157108

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)
184109
// we need latin1 here because the file contains special characters like "®"
185110
const csvContent = fileContent.toString('latin1')
186111

@@ -193,7 +118,13 @@ export class SftpTransport extends SubscriptionTransport<BaseEndpointTypes> {
193118
})
194119
}
195120

196-
return (await parser.parse(csvContent)) as IndexResponseData
121+
const { result, parsedData } = await parser.parse(csvContent)
122+
123+
return {
124+
filename,
125+
result,
126+
parsedData: parsedData as IndexResponseData,
127+
}
197128
}
198129

199130
getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number {

packages/sources/ftse-sftp/test/fixtures/index.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'fs'
22
import path from 'path'
3+
import { FileInfo } from 'ssh2-sftp-client'
34

45
// Helper function to read fixture files
56
export function readFixtureFile(filename: string): string {
@@ -24,23 +25,26 @@ export const expectedRussellData = {
2425
close: 3547.4,
2526
}
2627

27-
// Test data generation helpers
28-
export const createFTSETestData = (dataRows: string[]): string => {
29-
const header = `02/09/2025 (C) FTSE International Limited 2025. All Rights Reserved
30-
FTSE UK All-Share Indices Valuation Service
28+
export const ftseFilename = 'ukallv0209.csv'
29+
export const russellFilename = 'daily_values_russell_250827.CSV'
3130

32-
Index Code,Index/Sector Name,Number of Constituents,Index Base Currency,USD Index,GBP Index,EUR Index,JPY Index,AUD Index,CNY Index,HKD Index,CAD Index,LOC Index,Base Currency (GBP) Index,USD TRI,GBP TRI,EUR TRI,JPY TRI,AUD TRI,CNY TRI,HKD TRI,CAD TRI,LOC TRI,Base Currency (GBP) TRI,Mkt Cap (USD),Mkt Cap (GBP),Mkt Cap (EUR),Mkt Cap (JPY),Mkt Cap (AUD),Mkt Cap (CNY),Mkt Cap (HKD),Mkt Cap (CAD),Mkt Cap (LOC),Mkt Cap Base Currency (GBP),XD Adjustment (YTD),Dividend Yield`
31+
export const ftseDirectory = '/data/valuation/uk_all_share/'
32+
export const russellDirectory =
33+
'/data/Returns_and_Values/Russell_US_Indexes_Daily_Index_Values_Real_Time_TXT/'
3334

34-
return header + '\n' + dataRows.join('\n')
35+
export const fileContents: Record<string, string> = {
36+
[path.join(ftseDirectory, ftseFilename)]: ftseCsvFixture,
37+
[path.join(russellDirectory, russellFilename)]: russellCsvFixture,
3538
}
3639

37-
export const createRussellTestData = (dataRows: string[]): string => {
38-
const header = `"Daily Values",,,,,,,,,,,,,,
39-
,,,,,,,,,,,,,,
40-
,,,,,,,,,,,,,,
41-
"As of August 27, 2025",,,,,,,,"Last 5 Trading Days",,,,"1 Year Ending",,
42-
,,,,,,,,"Closing Values",,,,"Closing Values",,
43-
,"Open","High","Low","Close","Net Chg","% Chg","High","Low","Net Chg","% Chg","High","Low","Net Chg","% Chg"`
44-
45-
return header + '\n' + dataRows.join('\n')
46-
}
40+
export const directoryListings = {
41+
[ftseDirectory]: [
42+
'vall_icb2302.csv',
43+
'vall1809.csv',
44+
'valllst.csv',
45+
ftseFilename,
46+
'ukallvlst.csv',
47+
'vall_icb2302_v1.csv',
48+
].map((name) => ({ name })),
49+
[russellDirectory]: ['history', russellFilename].map((name) => ({ name })),
50+
} as unknown as Record<string, FileInfo[]>

0 commit comments

Comments
 (0)