diff --git a/apps/api/package.json b/apps/api/package.json index 43e2381dd..28ccbb945 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -14,7 +14,7 @@ "@supabase/supabase-js": "catalog:", "@vortexfi/shared": "workspace:*", "@wagmi/core": "catalog:", - "axios": "catalog:", + "bcrypt": "catalog:", "big.js": "catalog:", "body-parser": "^1.17.0", @@ -80,7 +80,7 @@ "typescript": "catalog:" }, "engines": { - "node": ">=12" + "node": ">=18" }, "license": "MIT", "name": "vortex-backend", diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 45bc0c956..8128ab85f 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -43,7 +43,7 @@ "@walletconnect/universal-provider": "^2.21.10", "@walletconnect/utils": "catalog:", "@xstate/react": "^6.0.0", - "axios": "catalog:", + "big.js": "catalog:", "bn.js": "^5.2.1", "buffer": "^6.0.3", diff --git a/apps/frontend/src/hooks/alfredpay/useFiatAccounts.ts b/apps/frontend/src/hooks/alfredpay/useFiatAccounts.ts index eec131449..2b56d4ecd 100644 --- a/apps/frontend/src/hooks/alfredpay/useFiatAccounts.ts +++ b/apps/frontend/src/hooks/alfredpay/useFiatAccounts.ts @@ -18,7 +18,7 @@ export function useFiatAccounts(country: string, options?: { enabled?: boolean } const enabled = (options?.enabled ?? true) && !!country; return useQuery({ enabled, - queryFn: () => AlfredpayService.listFiatAccounts(country), + queryFn: ({ signal }) => AlfredpayService.listFiatAccounts(country, signal), queryKey: [cacheKeys.fiatAccounts, country], ...(inactiveOptions["5m"] as FiatAccountsQueryPartialOptions) }); diff --git a/apps/frontend/src/hooks/useRampHistory.ts b/apps/frontend/src/hooks/useRampHistory.ts index 018967c80..2c0255b9e 100644 --- a/apps/frontend/src/hooks/useRampHistory.ts +++ b/apps/frontend/src/hooks/useRampHistory.ts @@ -30,15 +30,16 @@ export function useRampHistory(walletAddress?: string) { return useQuery({ enabled: addresses.length > 0, - queryFn: async () => { + queryFn: async ({ signal }) => { const allTransactions: Transaction[] = []; for (const address of addresses) { try { - const response = await RampService.getRampHistory(address, 100); + const response = await RampService.getRampHistory(address, 100, undefined, signal); const transactions = response.transactions.map(formatTransaction); allTransactions.push(...transactions); } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") throw error; console.warn(`Failed to fetch wallet history for ${address}:`, error); } } diff --git a/apps/frontend/src/pages/alfredpay/FiatAccountRegistration/RegisterFiatAccountScreen.tsx b/apps/frontend/src/pages/alfredpay/FiatAccountRegistration/RegisterFiatAccountScreen.tsx index 1557e1938..a19888b37 100644 --- a/apps/frontend/src/pages/alfredpay/FiatAccountRegistration/RegisterFiatAccountScreen.tsx +++ b/apps/frontend/src/pages/alfredpay/FiatAccountRegistration/RegisterFiatAccountScreen.tsx @@ -112,14 +112,14 @@ export function RegisterFiatAccountScreen({ country, accountType, onSuccess }: R toast.success(t("components.fiatAccountRegistration.registeredSuccess")); onSuccess(); } catch (err: unknown) { - const axiosErr = err as { + const apiErr = err as { response?: { status?: number; data?: { error?: string; message?: string; fields?: { field: string; message: string }[] }; }; }; - const status = axiosErr?.response?.status; - const body = axiosErr?.response?.data; + const status = apiErr?.response?.status; + const body = apiErr?.response?.data; if (status === 409) { toast.error(t("components.fiatAccountRegistration.alreadyRegistered")); diff --git a/apps/frontend/src/sections/individuals/FeeComparison/FeeComparisonTable/hooks/useFeeComparisonData.ts b/apps/frontend/src/sections/individuals/FeeComparison/FeeComparisonTable/hooks/useFeeComparisonData.ts index 195d8dc8e..8cf92d30c 100644 --- a/apps/frontend/src/sections/individuals/FeeComparison/FeeComparisonTable/hooks/useFeeComparisonData.ts +++ b/apps/frontend/src/sections/individuals/FeeComparison/FeeComparisonTable/hooks/useFeeComparisonData.ts @@ -27,13 +27,14 @@ export function useFeeComparisonData( ) { // Fetch prices from all providers (including vortex) const { data: allPricesResponse, isLoading: isLoadingPrices } = useQuery({ - queryFn: () => { + queryFn: ({ signal }) => { return PriceService.getAllPricesBundled( sourceAssetSymbol.toLowerCase() as Currency, targetAssetSymbol.toLowerCase() as Currency, amount, direction, - network + network, + signal ); }, queryKey: [cacheKeys.allPrices, amount, sourceAssetSymbol, targetAssetSymbol, network, direction], diff --git a/apps/frontend/src/services/api/alfredpay.service.ts b/apps/frontend/src/services/api/alfredpay.service.ts index f0af68376..3b56c2a4e 100644 --- a/apps/frontend/src/services/api/alfredpay.service.ts +++ b/apps/frontend/src/services/api/alfredpay.service.ts @@ -14,120 +14,46 @@ import { import { apiClient } from "./api-client"; export const AlfredpayService = { - /** - * Register a new fiat account. - */ async addFiatAccount(payload: AlfredpayAddFiatAccountRequest): Promise { - const response = await apiClient.post("/alfredpay/fiatAccounts", payload); - return response.data; + return apiClient.post("/alfredpay/fiatAccounts", payload); }, async createBusinessCustomer(country: string): Promise { - const response = await apiClient.post("/alfredpay/createBusinessCustomer", { - country - }); - return response.data; + return apiClient.post("/alfredpay/createBusinessCustomer", { country }); }, - /** - * Create a new Alfredpay individual customer. - */ async createIndividualCustomer(country: string): Promise { - const request: AlfredpayCreateCustomerRequest = { - country - }; - const response = await apiClient.post("/alfredpay/createIndividualCustomer", request); - return response.data; + const request: AlfredpayCreateCustomerRequest = { country }; + return apiClient.post("/alfredpay/createIndividualCustomer", request); }, - - /** - * Delete a registered fiat account. - */ async deleteFiatAccount(fiatAccountId: string, country: string): Promise { await apiClient.delete(`/alfredpay/fiatAccounts/${fiatAccountId}`, { params: { country } }); }, - /** - * Check Alfredpay status for a user in a specific country. - */ async getAlfredpayStatus(country: string): Promise { - const response = await apiClient.get("/alfredpay/alfredpayStatus", { - params: { country } - }); - return response.data; + return apiClient.get("/alfredpay/alfredpayStatus", { params: { country } }); }, - - /** - * Get dynamic form requirements for a country + payment method combo. - */ async getFiatAccountRequirements(country: string, paymentMethod: string): Promise { - const response = await apiClient.get("/alfredpay/fiatAccountRequirements", { + return apiClient.get("/alfredpay/fiatAccountRequirements", { params: { country, paymentMethod } }); - return response.data; }, - async getKybRedirectLink(country: string): Promise { - const response = await apiClient.get("/alfredpay/getKybRedirectLink", { - params: { country } - }); - return response.data; + return apiClient.get("/alfredpay/getKybRedirectLink", { params: { country } }); }, - - /** - * Get the KYC redirect link for a user. - */ async getKycRedirectLink(country: string): Promise { - const response = await apiClient.get("/alfredpay/getKycRedirectLink", { - params: { country } - }); - return response.data; + return apiClient.get("/alfredpay/getKycRedirectLink", { params: { country } }); }, - - /** - * Get the status of a specific KYC submission. - */ async getKycStatus(country: string, type?: AlfredpayCustomerType): Promise { - const response = await apiClient.get("/alfredpay/getKycStatus", { - params: { country, type } - }); - return response.data; + return apiClient.get("/alfredpay/getKycStatus", { params: { country, type } }); }, - - /** - * List all registered fiat accounts for the current user in a given country. - */ - async listFiatAccounts(country: string): Promise { - const response = await apiClient.get("/alfredpay/fiatAccounts", { - params: { country } - }); - return response.data; + async listFiatAccounts(country: string, signal?: AbortSignal): Promise { + return apiClient.get("/alfredpay/fiatAccounts", { params: { country }, signal }); }, - - /** - * Notify that the KYC redirect process is finished. - */ async notifyKycRedirectFinished(country: string, type?: AlfredpayCustomerType): Promise<{ success: boolean }> { - const response = await apiClient.post<{ success: boolean }>("/alfredpay/kycRedirectFinished", { - country, - type - }); - return response.data; + return apiClient.post<{ success: boolean }>("/alfredpay/kycRedirectFinished", { country, type }); }, - - /** - * Notify that the KYC redirect link has been opened. - */ async notifyKycRedirectOpened(country: string, type?: AlfredpayCustomerType): Promise<{ success: boolean }> { - const response = await apiClient.post<{ success: boolean }>("/alfredpay/kycRedirectOpened", { - country, - type - }); - return response.data; + return apiClient.post<{ success: boolean }>("/alfredpay/kycRedirectOpened", { country, type }); }, - async retryKyc(country: string, type?: AlfredpayCustomerType): Promise { - const response = await apiClient.post("/alfredpay/retryKyc", { - country, - type - }); - return response.data; + return apiClient.post("/alfredpay/retryKyc", { country, type }); } }; diff --git a/apps/frontend/src/services/api/api-client.ts b/apps/frontend/src/services/api/api-client.ts index 025fcfd29..3c8508538 100644 --- a/apps/frontend/src/services/api/api-client.ts +++ b/apps/frontend/src/services/api/api-client.ts @@ -1,83 +1,90 @@ -import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios"; import { SIGNING_SERVICE_URL } from "../../constants/constants"; import { AuthService } from "../auth"; -// TODO: CONSIDER REACT TANSTACK QUERY +export class ApiError extends Error { + status: number; + data: { error?: string; message?: string; details?: string }; -/** - * Base API client for making requests to the backend - */ -export const apiClient: AxiosInstance = axios.create({ - baseURL: `${SIGNING_SERVICE_URL}/v1`, - headers: { - "Content-Type": "application/json" - }, - timeout: 30000 -}); + constructor(status: number, data: { error?: string; message?: string; details?: string }, message: string) { + super(message); + this.status = status; + this.data = data; + } +} + +export function isApiError(error: unknown): error is ApiError { + return error instanceof ApiError; +} -// Add request interceptor for common headers and auth token -apiClient.interceptors.request.use( - config => { - // Add Authorization header if user is authenticated - const tokens = AuthService.getTokens(); - if (tokens?.accessToken) { - config.headers.Authorization = `Bearer ${tokens.accessToken}`; +async function apiFetch( + method: string, + path: string, + options: { + data?: unknown; + params?: Record; + headers?: Record; + signal?: AbortSignal; + } = {} +): Promise { + const tokens = AuthService.getTokens(); + + const url = new URL(`${SIGNING_SERVICE_URL}/v1${path}`); + if (options.params) { + for (const [key, value] of Object.entries(options.params)) { + if (value !== undefined) url.searchParams.set(key, String(value)); } - return config; - }, - error => { - return Promise.reject(error); } -); -// Add response interceptor for error handling -apiClient.interceptors.response.use( - response => { - return response; - }, - (error: AxiosError) => { - console.error("API Error:", error.response?.data || error.message); - return Promise.reject(error); + const isFormData = options.data instanceof FormData; + + const response = await fetch(url.toString(), { + body: isFormData ? (options.data as FormData) : options.data !== undefined ? JSON.stringify(options.data) : undefined, + headers: { + ...(tokens?.accessToken ? { Authorization: `Bearer ${tokens.accessToken}` } : {}), + ...(!isFormData ? { "Content-Type": "application/json" } : {}), + ...options.headers + }, + method, + signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(30000)]) : AbortSignal.timeout(30000) + }); + + if (!response.ok) { + const errorData = (await response.json().catch(() => ({}))) as { error?: string; message?: string }; + console.error("API Error:", errorData); + throw new ApiError(response.status, errorData, errorData.error ?? errorData.message ?? response.statusText); } -); -/** - * Helper function to handle API errors - * @param error The error object - * @param defaultMessage Default error message - * @returns Formatted error message - */ + if (response.status === 204) return undefined as T; + return response.json() as Promise; +} + export const handleApiError = (error: unknown, defaultMessage = "An error occurred"): string => { - if (axios.isAxiosError(error)) { - const responseData = error.response?.data as { error?: string; message?: string; details?: string } | undefined; - return responseData?.error || responseData?.message || error.message || defaultMessage; + if (isApiError(error)) { + return error.data?.error ?? error.data?.message ?? error.message ?? defaultMessage; } return error instanceof Error ? error.message : defaultMessage; }; -/** - * Generic API request function with error handling - * @param method The HTTP method - * @param url The endpoint URL - * @param data The request data - * @param config Additional axios config - * @returns The response data - */ export async function apiRequest( method: "get" | "post" | "put" | "delete", url: string, data?: unknown, - config?: AxiosRequestConfig -): Promise { - try { - const response = await apiClient.request({ - data, - method, - url, - ...config - }); - return response.data; - } catch (error) { - throw new Error(handleApiError(error)); + config?: { + params?: Record; + headers?: Record; + signal?: AbortSignal; } +): Promise { + return apiFetch(method, url, { data, ...config }); } + +type Params = Record; + +export const apiClient = { + delete: (url: string, config?: { params?: Params }) => apiFetch("DELETE", url, { params: config?.params }), + get: (url: string, config?: { params?: Params; signal?: AbortSignal }) => + apiFetch("GET", url, { params: config?.params, signal: config?.signal }), + post: (url: string, data?: unknown, config?: { headers?: Record; params?: Params }) => + apiFetch("POST", url, { data, headers: config?.headers, params: config?.params }), + put: (url: string, data?: unknown) => apiFetch("PUT", url, { data }) +}; diff --git a/apps/frontend/src/services/api/auth.api.ts b/apps/frontend/src/services/api/auth.api.ts index dddee6b1e..7424d8c09 100644 --- a/apps/frontend/src/services/api/auth.api.ts +++ b/apps/frontend/src/services/api/auth.api.ts @@ -13,70 +13,43 @@ export interface VerifyOTPResponse { } export class AuthAPI { - /** - * Check if email exists - */ static async checkEmail(email: string): Promise { - const response = await apiClient.get("/auth/check-email", { - params: { email } - }); - return response.data; + return apiClient.get("/auth/check-email", { params: { email } }); } - /** - * Request OTP - */ static async requestOTP(email: string, locale?: string): Promise { - await apiClient.post("/auth/request-otp", { - email, - locale - }); + await apiClient.post("/auth/request-otp", { email, locale }); } - /** - * Verify OTP - */ static async verifyOTP(email: string, token: string): Promise { - const response = await apiClient.post<{ success: boolean; access_token: string; refresh_token: string; user_id: string }>( + const data = await apiClient.post<{ success: boolean; access_token: string; refresh_token: string; user_id: string }>( "/auth/verify-otp", - { - email, - token - } + { email, token } ); return { - accessToken: response.data.access_token, - refreshToken: response.data.refresh_token, - success: response.data.success, - userId: response.data.user_id + accessToken: data.access_token, + refreshToken: data.refresh_token, + success: data.success, + userId: data.user_id }; } - /** - * Refresh token - */ static async refreshToken(refreshToken: string): Promise { - const response = await apiClient.post<{ success: boolean; access_token: string; refresh_token: string }>("/auth/refresh", { + const data = await apiClient.post<{ success: boolean; access_token: string; refresh_token: string }>("/auth/refresh", { refresh_token: refreshToken }); return { - accessToken: response.data.access_token, - refreshToken: response.data.refresh_token, - success: response.data.success, + accessToken: data.access_token, + refreshToken: data.refresh_token, + success: data.success, userId: "" // Refresh doesn't return user_id, but interface requires it }; } - /** - * Verify access token - */ static async verifyToken(accessToken: string): Promise<{ valid: boolean; userId?: string }> { - const response = await apiClient.post<{ valid: boolean; user_id?: string }>("/auth/verify", { + const data = await apiClient.post<{ valid: boolean; user_id?: string }>("/auth/verify", { access_token: accessToken }); - return { - userId: response.data.user_id, - valid: response.data.valid - }; + return { userId: data.user_id, valid: data.valid }; } } diff --git a/apps/frontend/src/services/api/monerium.service.ts b/apps/frontend/src/services/api/monerium.service.ts index bd93acee1..fcd922c6c 100644 --- a/apps/frontend/src/services/api/monerium.service.ts +++ b/apps/frontend/src/services/api/monerium.service.ts @@ -1,31 +1,23 @@ -import axios from "axios"; import { MONERIUM_MINT_NETWORK } from "../monerium/moneriumAuth"; -import { apiClient } from "./api-client"; +import { apiClient, isApiError } from "./api-client"; export interface MoneriumUserStatus { isNewUser: boolean; } export const MoneriumService = { - /** - * Check if a user exists in Monerium from our backend. - */ async checkUserStatus(address: string): Promise { try { console.log("Checking Monerium user status for address:", address); await apiClient.get("/monerium/address-exists", { params: { address, network: MONERIUM_MINT_NETWORK } }); - return { - isNewUser: false - }; + return { isNewUser: false }; } catch (error: unknown) { - if (axios.isAxiosError(error)) { - if (error.response && error.response.status === 404) { + if (isApiError(error)) { + if (error.status === 404) { console.log("Monerium user not found"); - return { - isNewUser: true - }; + return { isNewUser: true }; } throw new Error(`Error checking Monerium user status: ${error.message}`); } @@ -33,24 +25,15 @@ export const MoneriumService = { } }, - /** - * Create signature for Monerium offrampm, 10 minutes from now. - */ async createRampMessage(amount: string, iban: string): Promise { const date = new Date(Date.now() + 1000 * 60 * 10).toISOString(); return `Send EUR ${amount} to ${iban} at ${date}`; }, - /** - * Validate Monerium auth tokens - */ async validateAuthTokens(authCode: string, codeVerifier: string): Promise { try { - const response = await apiClient.post("/monerium/validate-auth", { - authCode, - codeVerifier - }); - return response.data.valid; + const data = await apiClient.post<{ valid: boolean }>("/monerium/validate-auth", { authCode, codeVerifier }); + return data.valid; } catch (error) { console.error("Error validating Monerium auth tokens:", error); return false; diff --git a/apps/frontend/src/services/api/price.service.ts b/apps/frontend/src/services/api/price.service.ts index 13c482841..a4eab539d 100644 --- a/apps/frontend/src/services/api/price.service.ts +++ b/apps/frontend/src/services/api/price.service.ts @@ -118,7 +118,8 @@ export class PriceService { targetCurrency: Currency, amount: string, direction: RampDirection, - network?: string + network?: string, + signal?: AbortSignal ): Promise { return apiRequest("get", `${this.BASE_PATH}/all`, undefined, { params: { @@ -127,7 +128,8 @@ export class PriceService { network, sourceCurrency, targetCurrency - } + }, + signal }); } diff --git a/apps/frontend/src/services/api/ramp.service.ts b/apps/frontend/src/services/api/ramp.service.ts index bdc8feefc..7d31672db 100644 --- a/apps/frontend/src/services/api/ramp.service.ts +++ b/apps/frontend/src/services/api/ramp.service.ts @@ -137,7 +137,12 @@ export class RampService { * @param offset The offset for pagination * @returns The transaction history */ - static async getRampHistory(walletAddress: string, limit?: number, offset?: number): Promise { + static async getRampHistory( + walletAddress: string, + limit?: number, + offset?: number, + signal?: AbortSignal + ): Promise { const queryParams = new URLSearchParams(); if (limit) queryParams.append("limit", limit.toString()); if (offset) queryParams.append("offset", offset.toString()); @@ -145,6 +150,6 @@ export class RampService { const queryString = queryParams.toString(); const url = `${this.BASE_PATH}/history/${walletAddress}${queryString ? `?${queryString}` : ""}`; - return apiRequest("get", url); + return apiRequest("get", url, undefined, { signal }); } } diff --git a/apps/rebalancer/package.json b/apps/rebalancer/package.json index bc19634f4..64b3f1389 100644 --- a/apps/rebalancer/package.json +++ b/apps/rebalancer/package.json @@ -20,6 +20,9 @@ "@types/bun": "^1.3.1", "typescript": "catalog:" }, + "engines": { + "node": ">=18" + }, "module": "index.ts", "name": "vortex-rebalancer", "peerDependencies": {}, diff --git a/bun.lock b/bun.lock index 1138fe351..df8b56619 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,6 @@ "name": "vortex-monorepo", "dependencies": { "big.js": "^7.0.1", - "cobe": "^2.0.1", "husky": "^9.1.7", "lint-staged": "^16.1.0", "numora-react": "^3.0.3", @@ -34,7 +33,6 @@ "@supabase/supabase-js": "catalog:", "@vortexfi/shared": "workspace:*", "@wagmi/core": "catalog:", - "axios": "catalog:", "bcrypt": "catalog:", "big.js": "catalog:", "body-parser": "^1.17.0", @@ -146,12 +144,12 @@ "@walletconnect/universal-provider": "^2.21.10", "@walletconnect/utils": "catalog:", "@xstate/react": "^6.0.0", - "axios": "catalog:", "big.js": "catalog:", "bn.js": "^5.2.1", "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cobe": "catalog:", "crypto-js": "^4.2.0", "i18next": "^24.2.3", "input-otp": "^1.4.2", @@ -291,7 +289,6 @@ "@scure/bip39": "catalog:", "@types/node-forge": "^1.3.14", "@wagmi/core": "catalog:", - "axios": "catalog:", "big.js": "catalog:", "node-forge": "^1.3.1", "p-queue": "^9.1.0", @@ -335,7 +332,7 @@ "@polkadot/util": "^13.5.6", "@polkadot/util-crypto": "^13.5.6", "adm-zip": "0.5.2", - "axios": "1.13.5", + "axios": "^1.15.0", "big.js": "^7.0.1", "handlebars": "4.7.9", "lodash": "4.18.0", @@ -377,10 +374,10 @@ "@wagmi/core": "^2.22.1", "@walletconnect/types": "^2.23.0", "@walletconnect/utils": "^2.23.0", - "axios": "1.9.0", "bcrypt": "5.1.1", "big.js": "^7.0.1", "clsx": "^1.2.1", + "cobe": "^2.0.1", "concurrently": "^9.1.2", "prettier": "^2.8.4", "stellar-sdk": "^13.1.0", @@ -2332,7 +2329,7 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], + "axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="], "axios-retry": ["axios-retry@4.5.0", "", { "dependencies": { "is-retry-allowed": "^2.2.0" }, "peerDependencies": { "axios": "0.x || 1.x" } }, "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ=="], @@ -3870,7 +3867,7 @@ "proxy-compare": ["proxy-compare@3.0.1", "", {}, "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="], - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="], @@ -4278,7 +4275,7 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], @@ -4648,8 +4645,6 @@ "@coinbase/cdp-sdk/abitype": ["abitype@1.0.6", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3 >=3.22.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A=="], - "@coinbase/cdp-sdk/axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], - "@coinbase/cdp-sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], "@coinbase/wallet-sdk/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], @@ -4820,8 +4815,6 @@ "@nomicfoundation/hardhat-verify/cbor": ["cbor@8.1.0", "", { "dependencies": { "nofilter": "^3.1.0" } }, "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg=="], - "@nomicfoundation/ignition-core/lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "@paraspell/sdk-core/@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="], "@polkadot/rpc-provider/@polkadot/x-global": ["@polkadot/x-global@14.0.3", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-MzMEynJ7HMTy/plLmdyP8rv14RS/6s29HZodUG9aCOscBnEiEDxVEax/ztRJqxhhQuHeYdx0LYDwVbdQDTkqNw=="], @@ -4880,6 +4873,8 @@ "@sentry/bundler-plugin-core/unplugin": ["unplugin@1.0.1", "", { "dependencies": { "acorn": "^8.8.1", "chokidar": "^3.5.3", "webpack-sources": "^3.2.3", "webpack-virtual-modules": "^0.5.0" } }, "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA=="], + "@sentry/cli/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "@sentry/hub/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], "@sentry/minimal/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -5136,8 +5131,6 @@ "eth-gas-reporter/@solidity-parser/parser": ["@solidity-parser/parser@0.14.5", "", { "dependencies": { "antlr4ts": "^0.5.0-alpha.4" } }, "sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg=="], - "eth-gas-reporter/axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="], - "eth-gas-reporter/ethers": ["ethers@5.8.0", "", { "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", "@ethersproject/abstract-signer": "5.8.0", "@ethersproject/address": "5.8.0", "@ethersproject/base64": "5.8.0", "@ethersproject/basex": "5.8.0", "@ethersproject/bignumber": "5.8.0", "@ethersproject/bytes": "5.8.0", "@ethersproject/constants": "5.8.0", "@ethersproject/contracts": "5.8.0", "@ethersproject/hash": "5.8.0", "@ethersproject/hdnode": "5.8.0", "@ethersproject/json-wallets": "5.8.0", "@ethersproject/keccak256": "5.8.0", "@ethersproject/logger": "5.8.0", "@ethersproject/networks": "5.8.0", "@ethersproject/pbkdf2": "5.8.0", "@ethersproject/properties": "5.8.0", "@ethersproject/providers": "5.8.0", "@ethersproject/random": "5.8.0", "@ethersproject/rlp": "5.8.0", "@ethersproject/sha2": "5.8.0", "@ethersproject/signing-key": "5.8.0", "@ethersproject/solidity": "5.8.0", "@ethersproject/strings": "5.8.0", "@ethersproject/transactions": "5.8.0", "@ethersproject/units": "5.8.0", "@ethersproject/wallet": "5.8.0", "@ethersproject/web": "5.8.0", "@ethersproject/wordlists": "5.8.0" } }, "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg=="], "eth-json-rpc-filters/pify": ["pify@5.0.0", "", {}, "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA=="], @@ -5256,8 +5249,6 @@ "miller-rabin/bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="], - "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "mocha/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "mocha/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], diff --git a/package.json b/package.json index f3a710a73..ffe508814 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@wagmi/core": "^2.22.1", "@walletconnect/types": "^2.23.0", "@walletconnect/utils": "^2.23.0", - "axios": "1.9.0", "bcrypt": "5.1.1", "big.js": "^7.0.1", "clsx": "^1.2.1", @@ -56,6 +55,9 @@ "concurrently": "catalog:", "typescript": "catalog:" }, + "engines": { + "node": ">=18" + }, "lint-staged": { "*": [ "biome check --write --no-errors-on-unmatched --formatter-enabled=true --linter-enabled=false" @@ -84,7 +86,7 @@ "@polkadot/util": "^13.5.6", "@polkadot/util-crypto": "^13.5.6", "adm-zip": "0.5.2", - "axios": "1.13.5", + "axios": "^1.15.0", "big.js": "^7.0.1", "handlebars": "4.7.9", "lodash": "4.18.0", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index cf4b44413..8b01fbd82 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -6,6 +6,9 @@ "@types/bun": "^1.3.1", "typescript": "^5.0.0" }, + "engines": { + "node": ">=18" + }, "exports": { ".": { "import": "./dist/index.js", diff --git a/packages/shared/package.json b/packages/shared/package.json index 9dd8dcf41..6d8323863 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -13,7 +13,7 @@ "@scure/bip39": "catalog:", "@types/node-forge": "^1.3.14", "@wagmi/core": "catalog:", - "axios": "catalog:", + "big.js": "catalog:", "node-forge": "^1.3.1", "p-queue": "^9.1.0", @@ -30,6 +30,9 @@ "tsup": "catalog:", "typescript": "catalog:" }, + "engines": { + "node": ">=18" + }, "exports": { ".": { "browser": "./src/index.ts", diff --git a/packages/shared/src/services/squidrouter/route.ts b/packages/shared/src/services/squidrouter/route.ts index e8861568c..d22111f7e 100644 --- a/packages/shared/src/services/squidrouter/route.ts +++ b/packages/shared/src/services/squidrouter/route.ts @@ -1,4 +1,3 @@ -import axios, { AxiosError } from "axios"; import PQueue from "p-queue"; import logger from "../../logger"; import { squidRouterConfigBase } from "./config"; @@ -112,16 +111,34 @@ export interface GetRouteOptions { // This prevents hitting SquidRouter API rate limits for the same user when multiple getRoute() calls happen in quick succession. const routeQueues = new Map(); +class HttpError extends Error { + status: number; + data: unknown; + + constructor(status: number, data: unknown) { + super(`HTTP ${status}`); + this.status = status; + this.data = data; + } +} + +async function squidFetch(url: string, options: RequestInit): Promise<{ data: T; headers: Headers }> { + const response = await fetch(url, options); + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new HttpError(response.status, errorData); + } + const data = (await response.json()) as T; + return { data, headers: response.headers }; +} + /** * Get a route from Squidrouter. * * When useCache is true, returns a stripped-down SquidrouterCachedRouteResult without transactionRequest. * When useCache is false or not specified (default), returns the full SquidrouterRouteResult. */ -export async function getRoute( - params: RouteParams, - options: { useCache: true } -): Promise; +export async function getRoute(params: RouteParams, options: { useCache: true }): Promise; export async function getRoute(params: RouteParams, options?: { useCache?: false }): Promise; export async function getRoute( params: RouteParams, @@ -148,7 +165,8 @@ export async function getRoute( } try { - const result = (await queue.add(() => getRouteInternal(params))) as SquidrouterRouteResult; + const result = await queue.add(() => getRouteInternal(params)); + if (!result) throw new Error("Route fetch returned no result"); if (useCache) { const cacheKey = generateRouteCacheKey(params); @@ -167,47 +185,54 @@ export async function getRoute( } async function getRouteInternal(params: RouteParams): Promise { - // This is the integrator ID for the Squidrouter API const { integratorId } = squidRouterConfigBase; const url = `${SQUIDROUTER_BASE_URL}/route`; + let fetchResult: Awaited>>; try { - const result = await axios.post(url, params, { + fetchResult = await squidFetch<{ route: SquidrouterRoute }>(url, { + body: JSON.stringify(params), headers: { "Content-Type": "application/json", "x-integrator-id": integratorId - } + }, + method: "POST" }); - - const requestId = result.headers["x-request-id"]; // Retrieve request ID from response headers - - if (!result.data || !result.data.route) { - logger.current.error(`Invalid API response structure. Request ID: ${requestId}`); - throw new Error("Invalid response from Squid Router API"); - } - - // FIXME remove this check once squidRouter works as expected again. - // Check if slippage of received route is reasonable. - const route = result.data.route; - if (route.estimate?.aggregateSlippage !== undefined) { - const slippage = route.estimate.aggregateSlippage; - if (slippage > 2.5) { - logger.current.warn(`Received route with high slippage: ${slippage}%. Request ID: ${requestId}`); - // FIXME: temporarily disabled because we are facing issues with squidrouter routes failing the swap to USDT - // throw new Error(`The slippage of the route is too high: ${slippage}%. Please try again later.`); - } - } - - return { data: { route }, requestId }; } catch (error) { - if (error instanceof AxiosError && error.response) { - logger.current.error(`Error fetching route from Squidrouter API: ${JSON.stringify(error.response?.data)}}`); - throw new Error(`Failed to fetch route: ${error.response?.data?.message || "Unknown error"}`); + if (error instanceof HttpError) { + logger.current.error(`Error fetching route from Squidrouter API: ${JSON.stringify(error.data)}`); + const message = + typeof error.data === "object" && error.data !== null && "message" in error.data + ? String((error.data as { message: unknown }).message) + : "Unknown error"; + error.message = `Failed to fetch route: ${message}`; } else { logger.current.error(`Error with parameters: ${JSON.stringify(params)}`); - throw error; + } + throw error; + } + + const { data, headers } = fetchResult; + const requestId = headers.get("x-request-id"); + + if (!data || !data.route) { + logger.current.error(`Invalid API response structure. Request ID: ${requestId}`); + throw new Error("Invalid response from Squid Router API"); + } + + // FIXME remove this check once squidRouter works as expected again. + // Check if slippage of received route is reasonable. + const route = data.route; + if (route.estimate?.aggregateSlippage !== undefined) { + const slippage = route.estimate.aggregateSlippage; + if (slippage > 2.5) { + logger.current.warn(`Received route with high slippage: ${slippage}%. Request ID: ${requestId}`); + // FIXME: temporarily disabled because we are facing issues with squidrouter routes failing the swap to USDT + // throw new Error(`The slippage of the route is too high: ${slippage}%. Please try again later.`); } } + + return { data: { route }, requestId: requestId ?? "" }; } // Function to get the status of the transaction using Squid API @@ -225,24 +250,23 @@ export async function getStatus( logger.current.debug( `Fetching status for transaction ID: ${transactionId} with integrator ID: ${integratorId} from Squidrouter API.` ); + + const url = new URL(`${SQUIDROUTER_BASE_URL}/status`); + if (fromChainId) url.searchParams.set("fromChainId", fromChainId); + if (toChainId) url.searchParams.set("toChainId", toChainId); + url.searchParams.set("transactionId", transactionId); + if (quoteId) url.searchParams.set("quoteId", quoteId); + try { - const result = await axios.get(`${SQUIDROUTER_BASE_URL}/status`, { - headers: { - "x-integrator-id": integratorId - }, - params: { - fromChainId, - quoteId, - toChainId, - transactionId - } + const { data } = await squidFetch(url.toString(), { + headers: { "x-integrator-id": integratorId } }); - return result.data; + return data; } catch (error) { - if (error instanceof AxiosError && error.response) { - logger.current.error("API error:", error.response.data); + if (error instanceof HttpError) { + logger.current.error("API error:", error.data); } - logger.current.error(`Couldn't get status from squidRouter for transactionID ${transactionId}.}`); + logger.current.error(`Couldn't get status from squidRouter for transactionID ${transactionId}.`); throw error; } } diff --git a/packages/shared/src/tokens/evm/dynamicEvmTokens.ts b/packages/shared/src/tokens/evm/dynamicEvmTokens.ts index 039cdbb1b..5211c72e2 100644 --- a/packages/shared/src/tokens/evm/dynamicEvmTokens.ts +++ b/packages/shared/src/tokens/evm/dynamicEvmTokens.ts @@ -1,4 +1,3 @@ -import axios from "axios"; import { EvmNetworks, getNetworkId, isNetworkEVM, Networks } from "../../helpers/networks"; import logger from "../../logger"; import { squidRouterConfigBase } from "../../services/squidrouter/config"; @@ -245,12 +244,12 @@ function buildPriceLookup(tokensByNetwork: Record { - const result = await axios.get(SQUID_ROUTER_API_URL, { - headers: { - "x-integrator-id": squidRouterConfigBase.integratorId - } + const response = await fetch(SQUID_ROUTER_API_URL, { + headers: { "x-integrator-id": squidRouterConfigBase.integratorId } }); - return result.data.tokens; + if (!response.ok) throw new Error(`Failed to fetch SquidRouter tokens: ${response.status}`); + const data = await response.json(); + return data.tokens; } function buildFallbackFromStaticConfig(): Record>> {