-
Notifications
You must be signed in to change notification settings - Fork 0
chore: bundle size shrink #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,45 +1,38 @@ | ||
| import client from './client'; | ||
| import type { Address, PaginatedResponse } from '../types'; | ||
| import type { AddressTransfer } from '../types'; | ||
| import type { Address, PaginatedResponse, AddressTransfer } from '../types'; | ||
|
|
||
| export async function getAddress(address: string): Promise<Address> { | ||
| const response = await client.get<Address>(`/addresses/${address}`); | ||
| return response.data; | ||
| return client.get<Address>(`/addresses/${address}`); | ||
| } | ||
|
|
||
| export interface GetAddressesParams { | ||
| page?: number; | ||
| limit?: number; | ||
| // Updated to align with backend API | ||
| address_type?: 'eoa' | 'contract' | 'erc20' | 'nft'; | ||
| from_block?: number; | ||
| to_block?: number; | ||
| } | ||
|
|
||
| export async function getAddresses(params: GetAddressesParams = {}): Promise<PaginatedResponse<Address>> { | ||
| const response = await client.get<PaginatedResponse<Address>>('/addresses', { params }); | ||
| return response.data; | ||
| return client.get<PaginatedResponse<Address>>('/addresses', { params: params as Record<string, unknown> }); | ||
| } | ||
|
|
||
| // Etherscan-compatible endpoint for native balance: GET /api?module=account&action=balance&address=... | ||
| interface EtherscanLikeResponse<T> { result: T } | ||
|
|
||
| export async function getEthBalance(address: string): Promise<string> { | ||
| const response = await client.get<EtherscanLikeResponse<string>>('', { | ||
| const data = await client.get<EtherscanLikeResponse<string>>('', { | ||
| params: { module: 'account', action: 'balance', address }, | ||
| }); | ||
| // Returns Wei as string | ||
| return response.data.result ?? '0'; | ||
| return data.result ?? '0'; | ||
| } | ||
|
|
||
| // New: Address transfers (ERC-20 + NFT) | ||
| export interface GetAddressTransfersParams { | ||
| page?: number; | ||
| limit?: number; | ||
| transfer_type?: 'erc20' | 'nft'; | ||
| } | ||
|
|
||
| export async function getAddressTransfers(address: string, params: GetAddressTransfersParams = {}): Promise<PaginatedResponse<AddressTransfer>> { | ||
| const response = await client.get<PaginatedResponse<AddressTransfer>>(`/addresses/${address}/transfers`, { params }); | ||
| return response.data; | ||
| return client.get<PaginatedResponse<AddressTransfer>>(`/addresses/${address}/transfers`, { params: params as Record<string, unknown> }); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,81 +1,95 @@ | ||||||||||||||||||||
| import axios from 'axios'; | ||||||||||||||||||||
| import type { AxiosInstance, AxiosError } from 'axios'; | ||||||||||||||||||||
| import type { ApiError } from '../types'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const client: AxiosInstance = axios.create({ | ||||||||||||||||||||
| baseURL: API_BASE_URL, | ||||||||||||||||||||
| timeout: 10000, | ||||||||||||||||||||
| headers: { | ||||||||||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Response interceptor for error handling | ||||||||||||||||||||
| client.interceptors.response.use( | ||||||||||||||||||||
| (response) => response, | ||||||||||||||||||||
| (error: AxiosError<ApiError>) => { | ||||||||||||||||||||
| if (error.response) { | ||||||||||||||||||||
| const data = error.response.data as Partial<ApiError> | undefined; | ||||||||||||||||||||
| const retryAfterSeconds = parseRetryAfterSeconds(error.response.headers, data); | ||||||||||||||||||||
| const apiError: ApiError = { | ||||||||||||||||||||
| error: data?.error || error.message, | ||||||||||||||||||||
| status: error.response.status, | ||||||||||||||||||||
| ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}), | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| return Promise.reject(apiError); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| const TIMEOUT_MS = 10_000; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (error.request) { | ||||||||||||||||||||
| const networkError: ApiError = { | ||||||||||||||||||||
| error: 'Unable to connect to the API server', | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| return Promise.reject(networkError); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| function buildUrl(path: string, params?: Record<string, unknown>): string { | ||||||||||||||||||||
| const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL; | ||||||||||||||||||||
| let url = base + path; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return Promise.reject({ | ||||||||||||||||||||
| error: error.message, | ||||||||||||||||||||
| } as ApiError); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function parseRetryAfterSeconds( | ||||||||||||||||||||
| headers: unknown, | ||||||||||||||||||||
| data: Partial<ApiError> | undefined | ||||||||||||||||||||
| ): number | undefined { | ||||||||||||||||||||
| const bodyRetryAfter = (data as { retry_after_seconds?: unknown } | undefined)?.retry_after_seconds; | ||||||||||||||||||||
| const headerRetryAfter = getHeaderValue(headers, 'retry-after'); | ||||||||||||||||||||
| const rawRetryAfter = bodyRetryAfter ?? headerRetryAfter; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (typeof rawRetryAfter === 'number' && Number.isFinite(rawRetryAfter) && rawRetryAfter >= 0) { | ||||||||||||||||||||
| return rawRetryAfter; | ||||||||||||||||||||
| if (params) { | ||||||||||||||||||||
| const qs = Object.entries(params) | ||||||||||||||||||||
| .filter(([, v]) => v !== undefined && v !== null) | ||||||||||||||||||||
| .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) | ||||||||||||||||||||
| .join('&'); | ||||||||||||||||||||
| if (qs) url += '?' + qs; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (typeof rawRetryAfter === 'string') { | ||||||||||||||||||||
| const parsed = Number(rawRetryAfter); | ||||||||||||||||||||
| if (Number.isFinite(parsed) && parsed >= 0) { | ||||||||||||||||||||
| return parsed; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return url; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| async function request<T>( | ||||||||||||||||||||
| method: 'GET' | 'POST', | ||||||||||||||||||||
| path: string, | ||||||||||||||||||||
| options: { params?: Record<string, unknown>; body?: unknown } = {} | ||||||||||||||||||||
| ): Promise<T> { | ||||||||||||||||||||
| const url = buildUrl(path, options.params); | ||||||||||||||||||||
| const controller = new AbortController(); | ||||||||||||||||||||
| const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const retryDate = Date.parse(rawRetryAfter); | ||||||||||||||||||||
| if (!Number.isNaN(retryDate)) { | ||||||||||||||||||||
| return Math.max(0, Math.ceil((retryDate - Date.now()) / 1000)); | ||||||||||||||||||||
| let response: Response; | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| response = await fetch(url, { | ||||||||||||||||||||
| method, | ||||||||||||||||||||
| headers: options.body !== undefined ? { 'Content-Type': 'application/json' } : undefined, | ||||||||||||||||||||
| body: options.body !== undefined ? JSON.stringify(options.body) : undefined, | ||||||||||||||||||||
| signal: controller.signal, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||
| clearTimeout(timer); | ||||||||||||||||||||
| if (err instanceof DOMException && err.name === 'AbortError') { | ||||||||||||||||||||
| throw { error: 'Request timed out' } as ApiError; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| throw { error: 'Unable to connect to the API server' } as ApiError; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return undefined; | ||||||||||||||||||||
| clearTimeout(timer); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||
| let data: { error?: string; retry_after_seconds?: unknown } = {}; | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| data = await response.json(); | ||||||||||||||||||||
| } catch { /* ignore */ } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const retryAfterSeconds = parseRetryAfterSeconds( | ||||||||||||||||||||
| response.headers.get('retry-after'), | ||||||||||||||||||||
| data.retry_after_seconds | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| throw { | ||||||||||||||||||||
| error: data.error ?? response.statusText, | ||||||||||||||||||||
| status: response.status, | ||||||||||||||||||||
| ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}), | ||||||||||||||||||||
| } as ApiError; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return response.json() as Promise<T>; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+66
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle JSON parse failure on successful responses. If the server returns a 2xx response with invalid JSON (or empty body), 🛡️ Proposed defensive fix- return response.json() as Promise<T>;
+ try {
+ return await response.json() as T;
+ } catch {
+ throw { error: 'Invalid response from server' } as ApiError;
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| function getHeaderValue(headers: unknown, key: string): unknown { | ||||||||||||||||||||
| if (!headers || typeof headers !== 'object') return undefined; | ||||||||||||||||||||
| const headerObject = headers as { get?: (name: string) => unknown } & Record<string, unknown>; | ||||||||||||||||||||
| function parseRetryAfterSeconds(header: string | null, body: unknown): number | undefined { | ||||||||||||||||||||
| const raw = body ?? header; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return raw; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (typeof raw === 'string') { | ||||||||||||||||||||
| const parsed = Number(raw); | ||||||||||||||||||||
| if (Number.isFinite(parsed) && parsed >= 0) return parsed; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (typeof headerObject.get === 'function') { | ||||||||||||||||||||
| return headerObject.get(key); | ||||||||||||||||||||
| const retryDate = Date.parse(raw); | ||||||||||||||||||||
| if (!Number.isNaN(retryDate)) return Math.max(0, Math.ceil((retryDate - Date.now()) / 1000)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return headerObject[key] ?? headerObject[key.toLowerCase()] ?? headerObject[key.toUpperCase()]; | ||||||||||||||||||||
| return undefined; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const client = { | ||||||||||||||||||||
| get<T>(path: string, options: { params?: Record<string, unknown> } = {}): Promise<T> { | ||||||||||||||||||||
| return request<T>('GET', path, { params: options.params }); | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| post<T>(path: string, body?: unknown): Promise<T> { | ||||||||||||||||||||
| return request<T>('POST', path, { body }); | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export default client; | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider dark mode support for the loader text.
The
text-gray-500class won't adapt when dark mode is active. Since the app usesThemeProvider, consider adding a dark mode variant.🎨 Proposed fix for dark mode support
function PageLoader() { return ( <div className="flex items-center justify-center h-64"> - <span className="text-gray-500 text-sm">Loading...</span> + <span className="text-gray-500 dark:text-gray-400 text-sm">Loading...</span> </div> ); }Based on learnings: "Frontend uses Tailwind CSS for styling across all components and pages."
📝 Committable suggestion
🤖 Prompt for AI Agents