diff --git a/package.json b/package.json index d276327..dfeabc2 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "alchemy-sdk": "^3.5.8", "assert": "^2.1.0", "async-retry": "^1.3.3", + "axios": "^1.11.0", "body-parser": "^2.2.0", "bullmq": "^5.52.0", "connect-ensure-login": "^0.1.1", @@ -46,9 +47,9 @@ }, "devDependencies": { "@types/async-retry": "^1.4.9", - "@types/express": "^5.0.1", - "@types/mocha": "^10.0.10", + "@types/express": "^5.0.3", "@types/memoizee": "^0.4.12", + "@types/mocha": "^10.0.10", "@types/node": "^22.15.3", "@types/passport": "^1.0.17", "@types/passport-local": "^1.0.38", diff --git a/src/api-router/everstake.ts b/src/api-router/everstake.ts new file mode 100644 index 0000000..182d4ee --- /dev/null +++ b/src/api-router/everstake.ts @@ -0,0 +1,86 @@ +import axios, { AxiosResponse, RawAxiosRequestHeaders } from 'axios'; +import { IncomingHttpHeaders } from 'http'; +import { omit } from 'lodash'; + +import logger from '../utils/logger'; + +class NotAllowedMethodError extends Error { + constructor(message: string) { + super(message); + this.name = 'NotAllowedMethodError'; + } +} + +const allowedBodyMethods = ['post', 'patch']; +const isAllowedBodyMethod = (method: string): method is 'post' | 'patch' => allowedBodyMethods.includes(method); +const allowedNoBodyMethods = ['get', 'delete']; +const isAllowedNoBodyMethod = (method: string): method is 'get' | 'delete' => allowedNoBodyMethods.includes(method); + +const toAxiosRequestHeaders = (headers: IncomingHttpHeaders) => { + const axiosHeaders: RawAxiosRequestHeaders = {}; + for (const key in headers) { + if (key === 'host') { + continue; + } + + const value = headers[key]; + + if (value === undefined) { + continue; + } + + axiosHeaders[key] = typeof value === 'string' ? value : value.join(', '); + } + + return axiosHeaders; +}; + +const createRequestsProxy = (baseURL: string) => { + const api = axios.create({ baseURL }); + + return async (req, res) => { + const methodName = req.method.toLowerCase(); + + try { + const commonRequestConfig = { + params: req.query, + headers: omit( + toAxiosRequestHeaders(req.headers), + 'connection', + 'Connection', + 'content-length', + 'Content-Length' + ) + }; + + let response: AxiosResponse; + if (isAllowedNoBodyMethod(methodName)) { + response = await api[methodName](req.path, commonRequestConfig); + } else if (isAllowedBodyMethod(methodName)) { + response = await api[methodName](req.path, req.body, commonRequestConfig); + } else { + throw new NotAllowedMethodError('Method Not Allowed'); + } + + res.status(response.status).send(response.data); + } catch (error) { + logger.error(error); + + if (error instanceof NotAllowedMethodError) { + res.status(405).json({ error: 'Method Not Allowed' }); + + return; + } + + if (axios.isAxiosError(error) && error.response) { + // TODO: add setting headers to response if needed + res.status(error.response.status).send(error.response.data); + } else { + res.status(500).json({ error: 'Internal Server Error' }); + } + } + }; +}; +export const everstakeWalletRequestsProxy = createRequestsProxy('https://wallet-sdk-api.everstake.one'); +export const everstakeDashboardRequestsProxy = createRequestsProxy('https://dashboard-api.everstake.one'); +export const everstakeEthRequestsProxy = createRequestsProxy('https://eth-api-b2c.everstake.one/api/v1'); diff --git a/src/api-router/index.ts b/src/api-router/index.ts index 9da5cf7..2c6a8b7 100644 --- a/src/api-router/index.ts +++ b/src/api-router/index.ts @@ -13,6 +13,7 @@ import { import { fetchTransactions } from './alchemy'; import { getEvmAccountActivity, getEvmBalances, getEvmCollectiblesMetadata, getEvmTokensMetadata } from './covalent'; +import { everstakeDashboardRequestsProxy, everstakeEthRequestsProxy, everstakeWalletRequestsProxy } from './everstake'; import { getSwapConnectionsRoute, getSwapRoute, getSwapTokensMetadata } from './lifi'; export const apiRouter = Router(); @@ -120,4 +121,7 @@ apiRouter res ); }) - ); + ) + .use('/everstake-wallet', everstakeWalletRequestsProxy) + .use('/everstake-dashboard', everstakeDashboardRequestsProxy) + .use('/everstake-eth-api', everstakeEthRequestsProxy); diff --git a/yarn.lock b/yarn.lock index efdc6c4..3aee2e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -917,7 +917,7 @@ "@types/range-parser" "*" "@types/send" "*" -"@types/express@*", "@types/express@^5.0.1": +"@types/express@*", "@types/express@^5.0.3": version "5.0.3" resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw== @@ -1542,6 +1542,15 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" +axios@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6" + integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.4" + proxy-from-env "^1.1.0" + axios@^1.7.4: version "1.10.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54" @@ -2749,6 +2758,17 @@ form-data@^4.0.0: hasown "^2.0.2" mime-types "^2.1.12" +form-data@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"