feat: explorer API, wallet, CCIP scripts, and config refresh
- Backend REST/gateway/track routes, analytics, Blockscout proxy paths. - Frontend wallet and liquidity surfaces; MetaMask token list alignment. - Deployment docs, verification scripts, address inventory updates. Check: go build ./... under backend/ (pass). Made-with: Cursor
This commit is contained in:
31
frontend/libs/frontend-api-client/api-base.test.ts
Normal file
31
frontend/libs/frontend-api-client/api-base.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveExplorerApiBase } from './api-base'
|
||||
|
||||
describe('resolveExplorerApiBase', () => {
|
||||
it('prefers an explicit env value when present', () => {
|
||||
expect(
|
||||
resolveExplorerApiBase({
|
||||
envValue: 'https://explorer.d-bis.org/',
|
||||
browserOrigin: 'http://127.0.0.1:3000',
|
||||
})
|
||||
).toBe('https://explorer.d-bis.org')
|
||||
})
|
||||
|
||||
it('falls back to same-origin in the browser when env is empty', () => {
|
||||
expect(
|
||||
resolveExplorerApiBase({
|
||||
envValue: '',
|
||||
browserOrigin: 'http://127.0.0.1:3000/',
|
||||
})
|
||||
).toBe('http://127.0.0.1:3000')
|
||||
})
|
||||
|
||||
it('falls back to the local backend on the server when no other base is available', () => {
|
||||
expect(
|
||||
resolveExplorerApiBase({
|
||||
envValue: '',
|
||||
browserOrigin: '',
|
||||
})
|
||||
).toBe('http://localhost:8080')
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { resolveExplorerApiBase } from './api-base'
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T
|
||||
@@ -21,9 +22,9 @@ export interface ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
export function createApiClient(baseURL: string = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080', getApiKey?: () => string | null) {
|
||||
export function createApiClient(baseURL?: string, getApiKey?: () => string | null) {
|
||||
const client = axios.create({
|
||||
baseURL,
|
||||
baseURL: baseURL || resolveExplorerApiBase(),
|
||||
timeout: 30000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
@@ -25,24 +25,51 @@ export function Address({
|
||||
: address
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(address)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
try {
|
||||
await navigator.clipboard.writeText(address)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
setCopied(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('flex items-center gap-2', className)}>
|
||||
<span className="font-mono text-sm">{displayAddress}</span>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex min-w-0 items-start gap-2',
|
||||
truncate ? 'flex-nowrap' : 'flex-wrap',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
'min-w-0 font-mono text-sm leading-6 text-gray-900 dark:text-gray-100',
|
||||
truncate ? 'truncate' : 'break-all'
|
||||
)}
|
||||
>
|
||||
{displayAddress}
|
||||
</span>
|
||||
{showCopy && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
className="shrink-0 rounded-md p-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
title="Copy address"
|
||||
aria-label="Copy address"
|
||||
>
|
||||
{copied ? '✓' : '📋'}
|
||||
{copied ? (
|
||||
<svg className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden>
|
||||
<path fillRule="evenodd" d="M16.704 5.29a1 1 0 0 1 .006 1.414l-7.25 7.313a1 1 0 0 1-1.42 0L4.79 10.766a1 1 0 1 1 1.414-1.414l2.546 2.546 6.544-6.602a1 1 0 0 1 1.41-.006Z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden>
|
||||
<path d="M6 2.75A2.25 2.25 0 0 0 3.75 5v8A2.25 2.25 0 0 0 6 15.25h1.25V14H6A1 1 0 0 1 5 13V5a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v1.25h1.25V5A2.25 2.25 0 0 0 11 2.75H6Z" />
|
||||
<path d="M9 6.75A2.25 2.25 0 0 0 6.75 9v6A2.25 2.25 0 0 0 9 17.25h5A2.25 2.25 0 0 0 16.25 15V9A2.25 2.25 0 0 0 14 6.75H9Zm0 1.25h5a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export function Button({
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'font-medium rounded-lg transition-colors',
|
||||
'font-medium rounded-lg transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
{
|
||||
'bg-primary-600 text-white hover:bg-primary-700': variant === 'primary',
|
||||
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
|
||||
@@ -34,4 +34,3 @@ export function Button({
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ export function Card({ children, className, title }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-white dark:bg-gray-800 rounded-lg shadow-md p-6',
|
||||
'rounded-xl bg-white p-4 shadow-md dark:bg-gray-800 sm:p-6',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
<h3 className="mb-3 text-lg font-semibold text-gray-900 dark:text-white sm:mb-4 sm:text-xl">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
@@ -24,4 +24,3 @@ export function Card({ children, className, title }: CardProps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,48 +11,91 @@ interface TableProps<T> {
|
||||
columns: Column<T>[]
|
||||
data: T[]
|
||||
className?: string
|
||||
emptyMessage?: string
|
||||
/** Stable key for each row (e.g. row => row.id or row => row.hash). Falls back to index if not provided. */
|
||||
keyExtractor?: (row: T) => string | number
|
||||
}
|
||||
|
||||
export function Table<T>({ columns, data, className, keyExtractor }: TableProps<T>) {
|
||||
export function Table<T>({
|
||||
columns,
|
||||
data,
|
||||
className,
|
||||
emptyMessage = 'No data available right now.',
|
||||
keyExtractor,
|
||||
}: TableProps<T>) {
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'rounded-xl border border-dashed border-gray-300 bg-white px-4 py-6 text-sm text-gray-600 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{emptyMessage}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('overflow-x-auto', className)}>
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
className={clsx(
|
||||
'px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider',
|
||||
column.className
|
||||
)}
|
||||
>
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{data.map((row, rowIndex) => (
|
||||
<tr key={keyExtractor ? keyExtractor(row) : rowIndex} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<div className={clsx('space-y-3', className)}>
|
||||
<div className="grid gap-3 md:hidden">
|
||||
{data.map((row, rowIndex) => (
|
||||
<div
|
||||
key={keyExtractor ? keyExtractor(row) : rowIndex}
|
||||
className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900"
|
||||
>
|
||||
<dl className="space-y-3">
|
||||
{columns.map((column, colIndex) => (
|
||||
<td
|
||||
key={colIndex}
|
||||
<div key={colIndex} className="space-y-1">
|
||||
<dt className="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{column.header}
|
||||
</dt>
|
||||
<dd className={clsx('min-w-0 text-sm text-gray-900 dark:text-gray-100', column.className)}>
|
||||
{column.accessor(row)}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden overflow-x-auto md:block">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<tr>
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
className={clsx(
|
||||
'px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100',
|
||||
'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400 lg:px-6',
|
||||
column.className
|
||||
)}
|
||||
>
|
||||
{column.accessor(row)}
|
||||
</td>
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
|
||||
{data.map((row, rowIndex) => (
|
||||
<tr key={keyExtractor ? keyExtractor(row) : rowIndex} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
{columns.map((column, colIndex) => (
|
||||
<td
|
||||
key={colIndex}
|
||||
className={clsx(
|
||||
'px-4 py-4 align-top text-sm text-gray-900 dark:text-gray-100 lg:px-6',
|
||||
column.className
|
||||
)}
|
||||
>
|
||||
{column.accessor(row)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user