Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion 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
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
4 changes: 2 additions & 2 deletions apps/frontend/src/hooks/useRampHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ 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) {
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: Record<string, unknown>, 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.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