Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -80,7 +80,7 @@
"typescript": "catalog:"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"license": "MIT",
"name": "vortex-backend",
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/hooks/alfredpay/useFiatAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function useFiatAccounts(country: string, options?: { enabled?: boolean }
const enabled = (options?.enabled ?? true) && !!country;
return useQuery<AlfredpayListFiatAccountsResponse>({
enabled,
queryFn: () => AlfredpayService.listFiatAccounts(country),
queryFn: ({ signal }) => AlfredpayService.listFiatAccounts(country, signal),
queryKey: [cacheKeys.fiatAccounts, country],
...(inactiveOptions["5m"] as FiatAccountsQueryPartialOptions)
});
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/hooks/useRampHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ export function useFeeComparisonData(
) {
// Fetch prices from all providers (including vortex)
const { data: allPricesResponse, isLoading: isLoadingPrices } = useQuery<AllPricesResponse, Error>({
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],
Expand Down
102 changes: 14 additions & 88 deletions apps/frontend/src/services/api/alfredpay.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,120 +14,46 @@ import {
import { apiClient } from "./api-client";

export const AlfredpayService = {
/**
* Register a new fiat account.
*/
async addFiatAccount(payload: AlfredpayAddFiatAccountRequest): Promise<AlfredpayAddFiatAccountResponse> {
const response = await apiClient.post<AlfredpayAddFiatAccountResponse>("/alfredpay/fiatAccounts", payload);
return response.data;
return apiClient.post<AlfredpayAddFiatAccountResponse>("/alfredpay/fiatAccounts", payload);
},
async createBusinessCustomer(country: string): Promise<AlfredpayCreateCustomerResponse> {
const response = await apiClient.post<AlfredpayCreateCustomerResponse>("/alfredpay/createBusinessCustomer", {
country
});
return response.data;
return apiClient.post<AlfredpayCreateCustomerResponse>("/alfredpay/createBusinessCustomer", { country });
},
/**
* Create a new Alfredpay individual customer.
*/
async createIndividualCustomer(country: string): Promise<AlfredpayCreateCustomerResponse> {
const request: AlfredpayCreateCustomerRequest = {
country
};
const response = await apiClient.post<AlfredpayCreateCustomerResponse>("/alfredpay/createIndividualCustomer", request);
return response.data;
const request: AlfredpayCreateCustomerRequest = { country };
return apiClient.post<AlfredpayCreateCustomerResponse>("/alfredpay/createIndividualCustomer", request);
},

/**
* Delete a registered fiat account.
*/
async deleteFiatAccount(fiatAccountId: string, country: string): Promise<void> {
await apiClient.delete(`/alfredpay/fiatAccounts/${fiatAccountId}`, { params: { country } });
},
/**
* Check Alfredpay status for a user in a specific country.
*/
async getAlfredpayStatus(country: string): Promise<AlfredpayStatusResponse> {
const response = await apiClient.get<AlfredpayStatusResponse>("/alfredpay/alfredpayStatus", {
params: { country }
});
return response.data;
return apiClient.get<AlfredpayStatusResponse>("/alfredpay/alfredpayStatus", { params: { country } });
},

/**
* Get dynamic form requirements for a country + payment method combo.
*/
async getFiatAccountRequirements(country: string, paymentMethod: string): Promise<AlfredpayFiatAccountRequirementsResponse> {
const response = await apiClient.get<AlfredpayFiatAccountRequirementsResponse>("/alfredpay/fiatAccountRequirements", {
return apiClient.get<AlfredpayFiatAccountRequirementsResponse>("/alfredpay/fiatAccountRequirements", {
params: { country, paymentMethod }
});
return response.data;
},

async getKybRedirectLink(country: string): Promise<AlfredpayGetKybRedirectLinkResponse> {
const response = await apiClient.get<AlfredpayGetKybRedirectLinkResponse>("/alfredpay/getKybRedirectLink", {
params: { country }
});
return response.data;
return apiClient.get<AlfredpayGetKybRedirectLinkResponse>("/alfredpay/getKybRedirectLink", { params: { country } });
},

/**
* Get the KYC redirect link for a user.
*/
async getKycRedirectLink(country: string): Promise<AlfredpayGetKycRedirectLinkResponse> {
const response = await apiClient.get<AlfredpayGetKycRedirectLinkResponse>("/alfredpay/getKycRedirectLink", {
params: { country }
});
return response.data;
return apiClient.get<AlfredpayGetKycRedirectLinkResponse>("/alfredpay/getKycRedirectLink", { params: { country } });
},

/**
* Get the status of a specific KYC submission.
*/
async getKycStatus(country: string, type?: AlfredpayCustomerType): Promise<AlfredpayGetKycStatusResponse> {
const response = await apiClient.get<AlfredpayGetKycStatusResponse>("/alfredpay/getKycStatus", {
params: { country, type }
});
return response.data;
return apiClient.get<AlfredpayGetKycStatusResponse>("/alfredpay/getKycStatus", { params: { country, type } });
},

/**
* List all registered fiat accounts for the current user in a given country.
*/
async listFiatAccounts(country: string): Promise<AlfredpayListFiatAccountsResponse> {
const response = await apiClient.get<AlfredpayListFiatAccountsResponse>("/alfredpay/fiatAccounts", {
params: { country }
});
return response.data;
async listFiatAccounts(country: string, signal?: AbortSignal): Promise<AlfredpayListFiatAccountsResponse> {
return apiClient.get<AlfredpayListFiatAccountsResponse>("/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<AlfredpayGetKycRedirectLinkResponse> {
const response = await apiClient.post<AlfredpayGetKycRedirectLinkResponse>("/alfredpay/retryKyc", {
country,
type
});
return response.data;
return apiClient.post<AlfredpayGetKycRedirectLinkResponse>("/alfredpay/retryKyc", { country, type });
}
};
131 changes: 69 additions & 62 deletions apps/frontend/src/services/api/api-client.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +4 to +12
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApiError.data is declared as { error?: string; message?: string; details?: string }, but the constructor accepts Record<string, unknown> and assigns it directly. This is not type-safe (and likely fails TS assignability because unknown isn’t assignable to string). Consider typing data as Record<string, unknown> (or making ApiError generic) and extracting error/message/details via a helper when needed.

Copilot uses AI. Check for mistakes.
}

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<T>(
method: string,
path: string,
options: {
data?: unknown;
params?: Record<string, string | number | boolean | undefined>;
headers?: Record<string, string>;
signal?: AbortSignal;
} = {}
): Promise<T> {
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)
});
Comment on lines +40 to +49
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signal: options.signal ?? AbortSignal.timeout(30000) changes timeout semantics: when a caller provides a signal, the 30s timeout is no longer applied (axios previously always enforced it). Also, AbortSignal.timeout isn’t supported in all browsers/environments. Consider combining signals (e.g., timeout + caller signal via AbortSignal.any or a manual AbortController) and falling back when AbortSignal.timeout is unavailable.

Copilot uses AI. Check for mistakes.

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<T>;
}

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;
};
Comment on lines +4 to 66
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new client now throws ApiError (with status/data) rather than an axios error shape (error.response.status/data). There are still call sites in the frontend that inspect err.response to branch on status/body, which will break with this change. Either update those call sites or consider exposing a backward-compatible shape/helper to reduce migration risk.

Copilot uses AI. Check for mistakes.

/**
* 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<T>(
method: "get" | "post" | "put" | "delete",
url: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<T> {
try {
const response = await apiClient.request<T>({
data,
method,
url,
...config
});
return response.data;
} catch (error) {
throw new Error(handleApiError(error));
config?: {
params?: Record<string, string | number | boolean | undefined>;
headers?: Record<string, string>;
signal?: AbortSignal;
}
): Promise<T> {
return apiFetch<T>(method, url, { data, ...config });
}

type Params = Record<string, string | number | boolean | undefined>;

export const apiClient = {
delete: <T>(url: string, config?: { params?: Params }) => apiFetch<T>("DELETE", url, { params: config?.params }),
get: <T>(url: string, config?: { params?: Params; signal?: AbortSignal }) =>
apiFetch<T>("GET", url, { params: config?.params, signal: config?.signal }),
post: <T>(url: string, data?: unknown, config?: { headers?: Record<string, string>; params?: Params }) =>
apiFetch<T>("POST", url, { data, headers: config?.headers, params: config?.params }),
put: <T>(url: string, data?: unknown) => apiFetch<T>("PUT", url, { data })
};
Loading
Loading