Add full monorepo: virtual-banker, backend, frontend, docs, scripts, deployment

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-10 11:32:49 -08:00
parent aafcd913c2
commit 88bc76da91
815 changed files with 125522 additions and 264 deletions

View File

@@ -0,0 +1,7 @@
import type { AppProps } from 'next/app'
import '../app/globals.css'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}

View File

@@ -0,0 +1,156 @@
'use client'
import { useEffect, useState } from 'react'
import { useParams } from 'next/navigation'
import { Card } from '@/components/common/Card'
import { Address } from '@/components/blockchain/Address'
import { Table } from '@/components/common/Table'
interface AddressInfo {
address: string
chain_id: number
transaction_count: number
token_count: number
is_contract: boolean
label?: string
tags: string[]
}
interface Transaction {
hash: string
block_number: number
from_address: string
to_address?: string
value: string
status?: number
}
export default function AddressDetailPage() {
const params = useParams()
const address = (params?.address as string) ?? ''
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
const [addressInfo, setAddressInfo] = useState<AddressInfo | null>(null)
const [transactions, setTransactions] = useState<Transaction[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
loadAddressInfo()
loadTransactions()
}, [address])
const loadAddressInfo = async () => {
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/addresses/${chainId}/${address}`
)
const data = await response.json()
setAddressInfo(data.data)
} catch (error) {
console.error('Failed to load address info:', error)
}
}
const loadTransactions = async () => {
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/transactions?chain_id=${chainId}&from_address=${address}&page=1&page_size=20`
)
const data = await response.json()
setTransactions(data.data || [])
} catch (error) {
console.error('Failed to load transactions:', error)
} finally {
setLoading(false)
}
}
if (loading) {
return <div className="p-8">Loading address...</div>
}
if (!addressInfo) {
return <div className="p-8">Address not found</div>
}
const transactionColumns = [
{
header: 'Hash',
accessor: (tx: Transaction) => (
<a href={`/transactions/${tx.hash}`} className="text-primary-600 hover:underline">
<Address address={tx.hash} truncate />
</a>
),
},
{
header: 'Block',
accessor: (tx: Transaction) => tx.block_number,
},
{
header: 'To',
accessor: (tx: Transaction) => tx.to_address ? <Address address={tx.to_address} truncate /> : 'Contract Creation',
},
{
header: 'Value',
accessor: (tx: Transaction) => {
const value = BigInt(tx.value)
const eth = Number(value) / 1e18
return eth > 0 ? `${eth.toFixed(4)} ETH` : '0 ETH'
},
},
{
header: 'Status',
accessor: (tx: Transaction) => (
<span className={tx.status === 1 ? 'text-green-600' : 'text-red-600'}>
{tx.status === 1 ? 'Success' : 'Failed'}
</span>
),
},
]
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">
{addressInfo.label || 'Address'}
</h1>
<Card title="Address Information" className="mb-6">
<div className="space-y-4">
<div>
<span className="font-semibold">Address:</span>
<Address address={addressInfo.address} className="ml-2" />
</div>
{addressInfo.tags.length > 0 && (
<div>
<span className="font-semibold">Tags:</span>
<div className="flex gap-2 mt-1">
{addressInfo.tags.map((tag, i) => (
<span key={i} className="px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded text-sm">
{tag}
</span>
))}
</div>
</div>
)}
<div>
<span className="font-semibold">Transactions:</span>
<span className="ml-2">{addressInfo.transaction_count}</span>
</div>
<div>
<span className="font-semibold">Tokens:</span>
<span className="ml-2">{addressInfo.token_count}</span>
</div>
<div>
<span className="font-semibold">Type:</span>
<span className="ml-2">{addressInfo.is_contract ? 'Contract' : 'EOA'}</span>
</div>
</div>
</Card>
<Card title="Transactions">
<Table columns={transactionColumns} data={transactions} />
</Card>
</div>
)
}

View File

@@ -0,0 +1,73 @@
'use client'
import { useEffect, useState } from 'react'
import { useParams } from 'next/navigation'
import { blocksApi, Block } from '@/services/api/blocks'
import { Card } from '@/components/common/Card'
import { Address } from '@/components/blockchain/Address'
import Link from 'next/link'
export default function BlockDetailPage() {
const params = useParams()
const blockNumber = parseInt((params?.number as string) ?? '0')
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
const [block, setBlock] = useState<Block | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
loadBlock()
}, [blockNumber])
const loadBlock = async () => {
setLoading(true)
try {
const response = await blocksApi.getByNumber(chainId, blockNumber)
setBlock(response.data)
} catch (error) {
console.error('Failed to load block:', error)
} finally {
setLoading(false)
}
}
if (loading) {
return <div className="p-8">Loading block...</div>
}
if (!block) {
return <div className="p-8">Block not found</div>
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Block #{block.number}</h1>
<Card title="Block Information">
<div className="space-y-4">
<div>
<span className="font-semibold">Hash:</span>
<Address address={block.hash} className="ml-2" />
</div>
<div>
<span className="font-semibold">Timestamp:</span>
<span className="ml-2">{new Date(block.timestamp).toLocaleString()}</span>
</div>
<div>
<span className="font-semibold">Miner:</span>
<Address address={block.miner} className="ml-2" truncate />
</div>
<div>
<span className="font-semibold">Transactions:</span>
<span className="ml-2">{block.transaction_count}</span>
</div>
<div>
<span className="font-semibold">Gas Used:</span>
<span className="ml-2">{block.gas_used.toLocaleString()} / {block.gas_limit.toLocaleString()}</span>
</div>
</div>
</Card>
</div>
)
}

View File

@@ -0,0 +1,92 @@
'use client'
import { useEffect, useState } from 'react'
import { blocksApi, Block } from '@/services/api/blocks'
import { Card } from '@/components/common/Card'
import { Address } from '@/components/blockchain/Address'
import Link from 'next/link'
export default function BlocksPage() {
const [blocks, setBlocks] = useState<Block[]>([])
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
useEffect(() => {
loadBlocks()
}, [page])
const loadBlocks = async () => {
setLoading(true)
try {
const response = await blocksApi.list({
chain_id: chainId,
page,
page_size: 20,
sort: 'number',
order: 'desc',
})
setBlocks(response.data)
} catch (error) {
console.error('Failed to load blocks:', error)
} finally {
setLoading(false)
}
}
if (loading) {
return <div className="p-8">Loading blocks...</div>
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Blocks</h1>
<div className="space-y-4">
{blocks.map((block) => (
<Card key={block.number}>
<div className="flex items-center justify-between">
<div>
<Link
href={`/blocks/${block.number}`}
className="text-lg font-semibold text-primary-600 hover:text-primary-700"
>
Block #{block.number}
</Link>
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">
<Address address={block.hash} truncate />
</div>
</div>
<div className="text-right">
<div className="text-sm">
{new Date(block.timestamp).toLocaleString()}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{block.transaction_count} transactions
</div>
</div>
</div>
</Card>
))}
</div>
<div className="mt-6 flex gap-4 justify-center">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="px-4 py-2 bg-gray-200 rounded disabled:opacity-50"
>
Previous
</button>
<span className="px-4 py-2">Page {page}</span>
<button
onClick={() => setPage((p) => p + 1)}
className="px-4 py-2 bg-gray-200 rounded"
>
Next
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,97 @@
'use client'
import { useState } from 'react'
import { Card } from '@/components/common/Card'
import { Address } from '@/components/blockchain/Address'
import Link from 'next/link'
interface SearchResult {
type: string
chain_id: number
data: {
hash?: string
address?: string
number?: number
block_number?: number
}
score: number
}
export default function SearchPage() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [loading, setLoading] = useState(false)
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault()
if (!query.trim()) return
setLoading(true)
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/search?q=${encodeURIComponent(query)}`
)
const data = await response.json()
setResults(data.results || [])
} catch (error) {
console.error('Search failed:', error)
} finally {
setLoading(false)
}
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Search</h1>
<Card className="mb-6">
<form onSubmit={handleSearch} className="flex gap-4">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search by address, transaction hash, block number..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
<button
type="submit"
disabled={loading}
className="px-6 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{loading ? 'Searching...' : 'Search'}
</button>
</form>
</Card>
{results.length > 0 && (
<Card title="Search Results">
<div className="space-y-4">
{results.map((result, index) => (
<div key={index} className="border-b border-gray-200 dark:border-gray-700 pb-4 last:border-0">
{result.type === 'block' && result.data.number && (
<Link href={`/blocks/${result.data.number}`} className="text-primary-600 hover:underline">
Block #{result.data.number}
</Link>
)}
{result.type === 'transaction' && result.data.hash && (
<Link href={`/transactions/${result.data.hash}`} className="text-primary-600 hover:underline">
Transaction <Address address={result.data.hash} truncate />
</Link>
)}
{result.type === 'address' && result.data.address && (
<Link href={`/addresses/${result.data.address}`} className="text-primary-600 hover:underline">
Address <Address address={result.data.address} truncate />
</Link>
)}
<div className="text-sm text-gray-500 mt-1">
Type: {result.type} | Chain: {result.chain_id} | Score: {result.score.toFixed(2)}
</div>
</div>
))}
</div>
</Card>
)}
</div>
)
}

View File

@@ -0,0 +1,132 @@
'use client'
import { useEffect, useState } from 'react'
import { useParams } from 'next/navigation'
import { Card } from '@/components/common/Card'
import { Address } from '@/components/blockchain/Address'
import Link from 'next/link'
interface Transaction {
chain_id: number
hash: string
block_number: number
block_hash: string
transaction_index: number
from_address: string
to_address?: string
value: string
gas_price?: number
max_fee_per_gas?: number
max_priority_fee_per_gas?: number
gas_limit: number
gas_used?: number
status?: number
input_data?: string
contract_address?: string
created_at: string
}
export default function TransactionDetailPage() {
const params = useParams()
const hash = (params?.hash as string) ?? ''
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
const [transaction, setTransaction] = useState<Transaction | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
loadTransaction()
}, [hash])
const loadTransaction = async () => {
setLoading(true)
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/transactions/${chainId}/${hash}`
)
const data = await response.json()
setTransaction(data.data)
} catch (error) {
console.error('Failed to load transaction:', error)
} finally {
setLoading(false)
}
}
if (loading) {
return <div className="p-8">Loading transaction...</div>
}
if (!transaction) {
return <div className="p-8">Transaction not found</div>
}
const value = BigInt(transaction.value)
const ethValue = Number(value) / 1e18
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Transaction</h1>
<Card title="Transaction Information">
<div className="space-y-4">
<div>
<span className="font-semibold">Hash:</span>
<Address address={transaction.hash} className="ml-2" />
</div>
<div>
<span className="font-semibold">Block:</span>
<Link href={`/blocks/${transaction.block_number}`} className="ml-2 text-primary-600 hover:underline">
#{transaction.block_number}
</Link>
</div>
<div>
<span className="font-semibold">From:</span>
<Link href={`/addresses/${transaction.from_address}`} className="ml-2">
<Address address={transaction.from_address} truncate />
</Link>
</div>
{transaction.to_address && (
<div>
<span className="font-semibold">To:</span>
<Link href={`/addresses/${transaction.to_address}`} className="ml-2">
<Address address={transaction.to_address} truncate />
</Link>
</div>
)}
<div>
<span className="font-semibold">Value:</span>
<span className="ml-2">{ethValue.toFixed(4)} ETH</span>
</div>
{transaction.gas_price && (
<div>
<span className="font-semibold">Gas Price:</span>
<span className="ml-2">{transaction.gas_price / 1e9} Gwei</span>
</div>
)}
{transaction.gas_used && (
<div>
<span className="font-semibold">Gas Used:</span>
<span className="ml-2">{transaction.gas_used.toLocaleString()} / {transaction.gas_limit.toLocaleString()}</span>
</div>
)}
<div>
<span className="font-semibold">Status:</span>
<span className={`ml-2 ${transaction.status === 1 ? 'text-green-600' : 'text-red-600'}`}>
{transaction.status === 1 ? 'Success' : 'Failed'}
</span>
</div>
{transaction.contract_address && (
<div>
<span className="font-semibold">Contract Created:</span>
<Link href={`/addresses/${transaction.contract_address}`} className="ml-2">
<Address address={transaction.contract_address} truncate />
</Link>
</div>
)}
</div>
</Card>
</div>
)
}

View File

@@ -0,0 +1,119 @@
'use client'
import { useEffect, useState } from 'react'
import { Table } from '@/components/common/Table'
import { Address } from '@/components/blockchain/Address'
import Link from 'next/link'
interface Transaction {
chain_id: number
hash: string
block_number: number
transaction_index: number
from_address: string
to_address?: string
value: string
gas_price?: number
gas_used?: number
status?: number
created_at: string
}
export default function TransactionsPage() {
const [transactions, setTransactions] = useState<Transaction[]>([])
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || '138')
useEffect(() => {
loadTransactions()
}, [page])
const loadTransactions = async () => {
setLoading(true)
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/transactions?chain_id=${chainId}&page=${page}&page_size=20`
)
const data = await response.json()
setTransactions(data.data || [])
} catch (error) {
console.error('Failed to load transactions:', error)
} finally {
setLoading(false)
}
}
const columns = [
{
header: 'Hash',
accessor: (tx: Transaction) => (
<Link href={`/transactions/${tx.hash}`} className="text-primary-600 hover:underline">
<Address address={tx.hash} truncate />
</Link>
),
},
{
header: 'Block',
accessor: (tx: Transaction) => (
<Link href={`/blocks/${tx.block_number}`} className="text-primary-600 hover:underline">
{tx.block_number}
</Link>
),
},
{
header: 'From',
accessor: (tx: Transaction) => <Address address={tx.from_address} truncate />,
},
{
header: 'To',
accessor: (tx: Transaction) => tx.to_address ? <Address address={tx.to_address} truncate /> : <span className="text-gray-400">Contract Creation</span>,
},
{
header: 'Value',
accessor: (tx: Transaction) => {
const value = BigInt(tx.value)
const eth = Number(value) / 1e18
return eth > 0 ? `${eth.toFixed(4)} ETH` : '0 ETH'
},
},
{
header: 'Status',
accessor: (tx: Transaction) => (
<span className={tx.status === 1 ? 'text-green-600' : 'text-red-600'}>
{tx.status === 1 ? 'Success' : 'Failed'}
</span>
),
},
]
if (loading) {
return <div className="p-8">Loading transactions...</div>
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Transactions</h1>
<Table columns={columns} data={transactions} />
<div className="mt-6 flex gap-4 justify-center">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="px-4 py-2 bg-gray-200 rounded disabled:opacity-50"
>
Previous
</button>
<span className="px-4 py-2">Page {page}</span>
<button
onClick={() => setPage((p) => p + 1)}
className="px-4 py-2 bg-gray-200 rounded"
>
Next
</button>
</div>
</div>
)
}