diff --git a/Makefile b/Makefile index 7e04113e2..48da27561 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ help: ## build/canopy: build the canopy binary into the GO_BIN_DIR build/canopy: + npm install --prefix $(EXPLORER_DIR) && npm run build --prefix $(EXPLORER_DIR) go build -o $(GO_BIN_DIR)/canopy $(CLI_DIR) ## build/canopy-full: build the canopy binary and its wallet and explorer altogether diff --git a/cmd/rpc/server.go b/cmd/rpc/server.go index 7f47afd94..a6c5cf903 100644 --- a/cmd/rpc/server.go +++ b/cmd/rpc/server.go @@ -37,7 +37,7 @@ const ( ApplicationJSON = "application/json; charset=utf-8" walletStaticDir = "web/wallet/out" - explorerStaticDir = "web/explorer/out" + explorerStaticDir = "web/explorer/dist" ) // Server represents a Canopy RPC server with configuration options. @@ -335,7 +335,7 @@ func (h logHandler) Handle(resp http.ResponseWriter, req *http.Request, p httpro h.h(resp, req, p) } -//go:embed all:web/explorer/out +//go:embed all:web/explorer/dist var explorerFS embed.FS //go:embed all:web/wallet/out @@ -352,23 +352,13 @@ func (s *Server) runStaticFileServer(fileSys fs.FS, dir, port string, conf lib.C // Create a new ServeMux to handle incoming HTTP requests mux := http.NewServeMux() + fileServer := http.FileServer(http.FS(distFS)) // Define a handler function for the root path mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // serve `index.html` with dynamic config injection - if r.URL.Path == "/" || r.URL.Path == "/index.html" { - + serveIndex := func() { // Construct the file path for `index.html` filePath := path.Join(dir, "index.html") - - // Open the file and defer closing until the function exits - data, e := fileSys.Open(filePath) - if e != nil { - http.NotFound(w, r) - return - } - defer data.Close() - // Read the content of `index.html` into a byte slice htmlBytes, e := fs.ReadFile(fileSys, filePath) if e != nil { @@ -382,12 +372,26 @@ func (s *Server) runStaticFileServer(fileSys fs.FS, dir, port string, conf lib.C // Set the response header as HTML and write the injected content to the response w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusOK) - w.Write([]byte(injectedHTML)) + _, _ = w.Write([]byte(injectedHTML)) + } + + // Serve `index.html` with dynamic config injection + if r.URL.Path == "/" || r.URL.Path == "/index.html" { + serveIndex() return } - // For all other requests, serve the files directly from the file system - http.FileServer(http.FS(distFS)).ServeHTTP(w, r) + // Serve real static assets if they exist. + requestPath := strings.TrimPrefix(path.Clean(r.URL.Path), "/") + if requestPath != "" { + if _, e := fs.Stat(distFS, requestPath); e == nil { + fileServer.ServeHTTP(w, r) + return + } + } + + // SPA fallback: unknown client-side routes resolve to index.html. + serveIndex() }) // Start the HTTP server in a new goroutine and listen on the specified port diff --git a/cmd/rpc/web/explorer/.eslintignore b/cmd/rpc/web/explorer/.eslintignore new file mode 100644 index 000000000..2d7bb6a10 --- /dev/null +++ b/cmd/rpc/web/explorer/.eslintignore @@ -0,0 +1,7 @@ +dist/ +node_modules/ +*.config.js +*.config.ts +vite.config.ts +postcss.config.js +tailwind.config.js diff --git a/cmd/rpc/web/explorer/.gitignore b/cmd/rpc/web/explorer/.gitignore index fb30cbdf8..a547bf36d 100644 --- a/cmd/rpc/web/explorer/.gitignore +++ b/cmd/rpc/web/explorer/.gitignore @@ -1,37 +1,24 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug +# Logs +logs +*.log npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* +lerna-debug.log* -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts +node_modules +dist +dist-ssr +*.local +# Editor directories and files +.vscode/* +!.vscode/extensions.json .idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/cmd/rpc/web/explorer/.nvmrc b/cmd/rpc/web/explorer/.nvmrc new file mode 100644 index 000000000..209e3ef4b --- /dev/null +++ b/cmd/rpc/web/explorer/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/cmd/rpc/web/explorer/.prettierrc b/cmd/rpc/web/explorer/.prettierrc deleted file mode 100644 index dea2b8d2e..000000000 --- a/cmd/rpc/web/explorer/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "singleQuote": false, - "tabWidth": 2, - "semi": true, - "arrowParens": "always", - "jsxSingleQuote": false, - "printWidth": 120 -} diff --git a/cmd/rpc/web/explorer/API_ENDPOINTS_IMPLEMENTATION.md b/cmd/rpc/web/explorer/API_ENDPOINTS_IMPLEMENTATION.md new file mode 100644 index 000000000..2d1dd4f24 --- /dev/null +++ b/cmd/rpc/web/explorer/API_ENDPOINTS_IMPLEMENTATION.md @@ -0,0 +1,597 @@ +# Canopy Explorer - API Endpoints Implementation Guide + +## Overview + +This document outlines all the API endpoints that need to be implemented to complete the Canopy Explorer functionality. The explorer has three main views: **Analytics View**, **Transaction View**, and **Validator View**. + +## Current Status + +### ✅ **Already Implemented Endpoints** + +The following endpoints are already available in the RPC server (`cmd/rpc/routes.go`): + +#### Core Query Endpoints +- `GET /v1/` - Version information +- `POST /v1/query/height` - Get current block height +- `POST /v1/query/account` - Get account details +- `POST /v1/query/accounts` - Get accounts list +- `POST /v1/query/validator` - Get validator details +- `POST /v1/query/validators` - Get validators list +- `POST /v1/query/block-by-height` - Get block by height +- `POST /v1/query/block-by-hash` - Get block by hash +- `POST /v1/query/blocks` - Get blocks list +- `POST /v1/query/tx-by-hash` - Get transaction by hash +- `POST /v1/query/txs-by-height` - Get transactions by block height +- `POST /v1/query/txs-by-sender` - Get transactions by sender +- `POST /v1/query/txs-by-rec` - Get transactions by recipient +- `POST /v1/query/pending` - Get pending transactions +- `POST /v1/query/params` - Get network parameters +- `POST /v1/query/supply` - Get supply information +- `POST /v1/query/pool` - Get pool information +- `POST /v1/query/committee` - Get committee information +- `POST /v1/query/orders` - Get orders information + +#### Admin Endpoints +- `GET /v1/admin/config` - Get server configuration +- `GET /v1/admin/peer-info` - Get peer information +- `GET /v1/admin/consensus-info` - Get consensus information + +--- + +## 🚀 **Required Endpoints for Complete Implementation** + +### 1. **Analytics View Endpoints** + +#### 1.1 Network Health & Performance +```http +POST /v1/query/network-uptime +``` +**Purpose**: Get network uptime percentage and health metrics +**Request Body**: +```json +{ + "chainId": 1, + "timeRange": "7d" // 1d, 7d, 30d, 90d +} +``` +**Response**: +```json +{ + "uptime": 99.98, + "downtime": 0.02, + "lastOutage": "2024-01-15T10:30:00Z", + "averageBlockTime": 6.2, + "networkVersion": "v1.2.4" +} +``` + +#### 1.2 Historical Fee Data +```http +POST /v1/query/fee-trends +``` +**Purpose**: Get historical transaction fee trends +**Request Body**: +```json +{ + "chainId": 1, + "timeRange": "7d", + "granularity": "hour" // hour, day, week +} +``` +**Response**: +```json +{ + "trends": [ + { + "timestamp": "2024-01-15T00:00:00Z", + "averageFee": 0.0023, + "medianFee": 0.0021, + "minFee": 0.0015, + "maxFee": 0.0050, + "transactionCount": 1250 + } + ], + "summary": { + "average7d": 0.0023, + "change24h": 0.05, + "trend": "increasing" + } +} +``` + +#### 1.3 Staking Rewards History +```http +POST /v1/query/staking-rewards +``` +**Purpose**: Get historical staking rewards and trends +**Request Body**: +```json +{ + "chainId": 1, + "timeRange": "30d", + "validatorAddress": "optional" +} +``` +**Response**: +```json +{ + "rewards": [ + { + "timestamp": "2024-01-15T00:00:00Z", + "totalRewards": 1250.5, + "averageAPY": 8.5, + "activeValidators": 128, + "totalStaked": 45513085780613 + } + ], + "summary": { + "averageAPY": 8.5, + "totalRewards30d": 37500.0, + "trend": "stable" + } +} +``` + +#### 1.4 Network Activity Metrics +```http +POST /v1/query/network-activity +``` +**Purpose**: Get detailed network activity metrics +**Request Body**: +```json +{ + "chainId": 1, + "timeRange": "7d", + "granularity": "hour" +} +``` +**Response**: +```json +{ + "activity": [ + { + "timestamp": "2024-01-15T00:00:00Z", + "transactions": 1250, + "blocks": 144, + "uniqueAddresses": 450, + "volume": 125000.5 + } + ], + "summary": { + "totalTransactions": 21000, + "averageTPS": 0.35, + "peakTPS": 2.1, + "uniqueAddresses": 1250 + } +} +``` + +#### 1.5 Block Production Analytics +```http +POST /v1/query/block-production +``` +**Purpose**: Get block production rate and validator performance +**Request Body**: +```json +{ + "chainId": 1, + "timeRange": "7d" +} +``` +**Response**: +```json +{ + "production": [ + { + "timestamp": "2024-01-15T00:00:00Z", + "blocksProduced": 144, + "averageBlockTime": 6.2, + "validatorPerformance": { + "totalValidators": 128, + "activeValidators": 125, + "averageUptime": 99.2 + } + } + ], + "summary": { + "averageBlockTime": 6.2, + "totalBlocks": 1008, + "productionRate": 144.0 + } +} +``` + +### 2. **Transaction View Endpoints** + +#### 2.1 Enhanced Transaction Search +```http +POST /v1/query/transactions-advanced +``` +**Purpose**: Advanced transaction search with multiple filters +**Request Body**: +```json +{ + "chainId": 1, + "pageNumber": 1, + "perPage": 50, + "filters": { + "type": "send", + "status": "success", + "fromDate": "2024-01-01T00:00:00Z", + "toDate": "2024-01-31T23:59:59Z", + "minAmount": 100, + "maxAmount": 10000, + "address": "0x123...", + "blockHeight": 1000 + }, + "sortBy": "timestamp", + "sortOrder": "desc" +} +``` +**Response**: +```json +{ + "results": [ + { + "hash": "0xabc123...", + "type": "send", + "from": "0x123...", + "to": "0x456...", + "amount": 1000.5, + "fee": 0.0023, + "status": "success", + "blockHeight": 1000, + "blockHash": "0xdef456...", + "timestamp": "2024-01-15T10:30:00Z", + "gasUsed": 21000, + "gasPrice": 0.0000001, + "messageType": "send", + "rawData": "..." + } + ], + "totalCount": 50000, + "pageNumber": 1, + "perPage": 50, + "totalPages": 1000, + "hasMore": true +} +``` + +#### 2.2 Transaction Statistics +```http +POST /v1/query/transaction-stats +``` +**Purpose**: Get transaction statistics and metrics +**Request Body**: +```json +{ + "chainId": 1, + "timeRange": "7d" +} +``` +**Response**: +```json +{ + "stats": { + "totalTransactions": 50000, + "successfulTransactions": 49500, + "failedTransactions": 500, + "pendingTransactions": 0, + "averageTransactionTime": 6.2, + "transactionTypes": { + "send": 40000, + "stake": 5000, + "unstake": 2000, + "governance": 1000, + "other": 2000 + }, + "volume": { + "total": 1250000.5, + "average": 25000.0, + "median": 15000.0 + } + }, + "trends": { + "dailyGrowth": 5.2, + "weeklyGrowth": 12.5, + "monthlyGrowth": 25.8 + } +} +``` + +#### 2.3 Failed Transactions Analysis +```http +POST /v1/query/failed-transactions +``` +**Purpose**: Get detailed information about failed transactions +**Request Body**: +```json +{ + "chainId": 1, + "pageNumber": 1, + "perPage": 50, + "timeRange": "7d" +} +``` +**Response**: +```json +{ + "results": [ + { + "hash": "0xabc123...", + "from": "0x123...", + "to": "0x456...", + "amount": 1000.5, + "fee": 0.0023, + "errorCode": "INSUFFICIENT_FUNDS", + "errorMessage": "Account balance too low", + "blockHeight": 1000, + "timestamp": "2024-01-15T10:30:00Z", + "gasUsed": 21000, + "gasLimit": 21000 + } + ], + "totalCount": 500, + "errorSummary": { + "INSUFFICIENT_FUNDS": 200, + "GAS_LIMIT_EXCEEDED": 150, + "INVALID_SIGNATURE": 100, + "OTHER": 50 + } +} +``` + +### 3. **Validator View Endpoints** + +#### 3.1 Validator Performance Metrics +```http +POST /v1/query/validator-performance +``` +**Purpose**: Get detailed validator performance metrics +**Request Body**: +```json +{ + "chainId": 1, + "validatorAddress": "0x123...", + "timeRange": "30d" +} +``` +**Response**: +```json +{ + "performance": { + "address": "0x123...", + "name": "CanopyGuard", + "uptime": 99.8, + "blocksProduced": 1250, + "blocksMissed": 5, + "averageBlockTime": 6.1, + "commission": 5.0, + "delegationCount": 450, + "totalDelegated": 1000000.5, + "selfStake": 50000.0, + "rewards": { + "totalEarned": 2500.5, + "last30Days": 250.0, + "averageDaily": 8.33, + "apy": 8.5 + }, + "rank": 15, + "status": "active", + "jailed": false, + "unstakingHeight": 0 + }, + "history": [ + { + "timestamp": "2024-01-15T00:00:00Z", + "blocksProduced": 144, + "uptime": 100.0, + "rewards": 8.33 + } + ] +} +``` + +#### 3.2 Validator Rewards History +```http +POST /v1/query/validator-rewards +``` +**Purpose**: Get detailed validator rewards history +**Request Body**: +```json +{ + "chainId": 1, + "validatorAddress": "0x123...", + "timeRange": "30d", + "pageNumber": 1, + "perPage": 100 +} +``` +**Response**: +```json +{ + "rewards": [ + { + "timestamp": "2024-01-15T10:30:00Z", + "blockHeight": 1000, + "reward": 0.5, + "commission": 0.025, + "netReward": 0.475, + "delegatorRewards": 0.45, + "type": "block_reward" + } + ], + "summary": { + "totalRewards": 250.0, + "totalCommission": 12.5, + "netRewards": 237.5, + "averageDaily": 8.33, + "apy": 8.5 + }, + "totalCount": 720, + "pageNumber": 1, + "perPage": 100 +} +``` + +#### 3.3 Validator Delegations +```http +POST /v1/query/validator-delegations +``` +**Purpose**: Get validator delegation information +**Request Body**: +```json +{ + "chainId": 1, + "validatorAddress": "0x123...", + "pageNumber": 1, + "perPage": 50 +} +``` +**Response**: +```json +{ + "delegations": [ + { + "delegatorAddress": "0x456...", + "amount": 10000.0, + "shares": 10000.0, + "timestamp": "2024-01-15T10:30:00Z", + "blockHeight": 1000, + "reward": 0.5, + "commission": 0.025 + } + ], + "summary": { + "totalDelegations": 450, + "totalAmount": 1000000.5, + "averageDelegation": 2222.22, + "largestDelegation": 50000.0 + }, + "totalCount": 450, + "pageNumber": 1, + "perPage": 50 +} +``` + +#### 3.4 Validator Chain Participation +```http +POST /v1/query/validator-chains +``` +**Purpose**: Get validator participation in different chains/committees +**Request Body**: +```json +{ + "chainId": 1, + "validatorAddress": "0x123..." +} +``` +**Response**: +```json +{ + "chains": [ + { + "chainId": 1, + "chainName": "Canopy Mainnet", + "committeeId": 1, + "stakeAmount": 50000.0, + "status": "active", + "rewards": 250.0, + "uptime": 99.8, + "blocksProduced": 1250 + }, + { + "chainId": 2, + "chainName": "Canopy Testnet", + "committeeId": 2, + "stakeAmount": 25000.0, + "status": "active", + "rewards": 125.0, + "uptime": 98.5, + "blocksProduced": 625 + } + ], + "summary": { + "totalChains": 2, + "totalStake": 75000.0, + "totalRewards": 375.0, + "averageUptime": 99.15 + } +} +``` + +### 4. **Additional Utility Endpoints** + +#### 4.1 Network Statistics +```http +POST /v1/query/network-stats +``` +**Purpose**: Get comprehensive network statistics +**Request Body**: +```json +{ + "chainId": 1 +} +``` +**Response**: +```json +{ + "stats": { + "totalBlocks": 1000000, + "totalTransactions": 50000000, + "totalAccounts": 125000, + "totalValidators": 128, + "activeValidators": 125, + "totalStaked": 45513085780613, + "averageBlockTime": 6.2, + "networkUptime": 99.98, + "currentHeight": 1000000, + "genesisTime": "2023-01-01T00:00:00Z" + } +} +``` + +--- + +## 🔧 **Implementation Priority** + +### **Phase 1 - Critical (High Priority)** +1. `POST /v1/query/transactions-advanced` - Enhanced transaction search +2. `POST /v1/query/validator-performance` - Validator performance metrics +3. `POST /v1/query/network-stats` - Network statistics + +### **Phase 2 - Important (Medium Priority)** +1. `POST /v1/query/fee-trends` - Fee trends for analytics +2. `POST /v1/query/validator-rewards` - Validator rewards history +3. `POST /v1/query/transaction-stats` - Transaction statistics +4. `POST /v1/query/network-activity` - Network activity metrics + +### **Phase 3 - Enhancement (Low Priority)** +1. `POST /v1/query/network-uptime` - Network uptime +2. `POST /v1/query/staking-rewards` - Staking rewards history +3. `POST /v1/query/block-production` - Block production analytics +4. `POST /v1/query/validator-delegations` - Validator delegations +5. `POST /v1/query/validator-chains` - Validator chain participation +6. `POST /v1/query/failed-transactions` - Failed transactions analysis + +--- + +## 📝 **Implementation Notes** + +### **Request/Response Format** +- All endpoints use POST method with JSON request body +- Include `chainId` in all requests for multi-chain support +- Use consistent pagination with `pageNumber`, `perPage`, `totalCount`, `totalPages` +- Include proper error handling with HTTP status codes + +--- + +## 🎯 **Expected Outcomes** + +Once all endpoints are implemented, the Canopy Explorer will have: + +1. **Complete Analytics View** with real-time network metrics, fee trends, and staking analytics +2. **Advanced Transaction View** with comprehensive search, filtering, and analysis capabilities +3. **Detailed Validator View** with performance metrics, rewards history, and delegation information +4. **Enhanced User Experience** with fast search, real-time updates, and comprehensive data visualization + diff --git a/cmd/rpc/web/explorer/README.md b/cmd/rpc/web/explorer/README.md new file mode 100644 index 000000000..aaefd23c3 --- /dev/null +++ b/cmd/rpc/web/explorer/README.md @@ -0,0 +1,209 @@ +# Explorer + +A modern React application built with Vite, TypeScript, Tailwind CSS, React Hook Form, Framer Motion, and React Query for efficient data fetching and state management. + +## Features + +- ⚡ **Vite** - Fast build tool and dev server +- ⚛️ **React 18** - Latest React features +- 🔷 **TypeScript** - Type safety and better developer experience +- 🎨 **Tailwind CSS** - Utility-first CSS framework +- 📝 **React Hook Form** - Performant forms with easy validation +- ✨ **Framer Motion** - Production-ready motion library for React +- 🔄 **React Query** - Powerful data fetching and caching library + +## Getting Started + +### Prerequisites + +- Node.js (version 18 or higher) +- npm or yarn +- Canopy blockchain node running on port 50001 + +### Installation + +1. Clone the repository and navigate to the project directory: +```bash +cd cmd/rpc/web/explorer +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Ensure your Canopy blockchain node is running on port 50001: +```bash +# Your Canopy node should be accessible at: +# http://localhost:50001 +``` + +4. Start the development server: +```bash +npm run dev +``` + +5. Open your browser and navigate to `http://localhost:5173` + +### Quick Setup + +The application will automatically connect to your Canopy node at `http://localhost:50001`. If your node is running on a different port, you can configure it by setting `window.__CONFIG__` in your HTML or modifying the API configuration. + +### Available Scripts + +- `npm run dev` - Start development server +- `npm run build` - Build for production +- `npm run preview` - Preview production build +- `npm run lint` - Run ESLint +- `npm run type-check` - Run TypeScript type checking + +## Project Structure + +``` +src/ +├── components/ # Reusable components +│ ├── analytics/ # Analytics dashboard components +│ │ ├── AnalyticsFilters.tsx +│ │ ├── BlockProductionRate.tsx +│ │ ├── FeeTrends.tsx +│ │ ├── KeyMetrics.tsx +│ │ ├── NetworkActivity.tsx +│ │ ├── NetworkAnalyticsPage.tsx +│ │ ├── StakingTrends.tsx +│ │ ├── TransactionTypes.tsx +│ │ └── ValidatorWeights.tsx +│ ├── block/ # Block-related components +│ │ ├── BlockTransactions.tsx +│ │ ├── BlocksFilters.tsx +│ │ ├── BlocksPage.tsx +│ │ └── BlocksTable.tsx +│ ├── Home/ # Home page components +│ │ ├── ExtraTables.tsx +│ │ ├── HomePage.tsx +│ │ └── TableCard.tsx +│ ├── transaction/ # Transaction components +│ │ ├── TransactionsPage.tsx +│ │ └── TransactionsTable.tsx +│ ├── validator/ # Validator components +│ │ ├── ValidatorsFilters.tsx +│ │ ├── ValidatorsPage.tsx +│ │ └── ValidatorsTable.tsx +│ ├── token-swaps/ # Token swap components +│ │ ├── RecentSwapsTable.tsx +│ │ ├── SwapFilters.tsx +│ │ └── TokenSwapsPage.tsx +│ ├── common/ # Shared UI components +│ │ ├── Footer.tsx +│ │ ├── Logo.tsx +│ │ └── Navbar.tsx +│ └── ui/ # Basic UI components +│ ├── AnimatedNumber.tsx +│ ├── LoadingSpinner.tsx +│ └── SearchInput.tsx +├── hooks/ # Custom React hooks +│ ├── useApi.ts # React Query hooks for API calls +│ └── useSearch.ts # Search functionality hook +├── lib/ # API functions and utilities +│ └── api.ts # All API endpoint functions +├── types/ # TypeScript type definitions +│ ├── api.ts # API response types +│ └── common.ts # Common type definitions +├── data/ # Static data and configurations +│ ├── blocks.json # Block-related text content +│ ├── navbar.json # Navigation menu configuration +│ └── transactions.json # Transaction-related text content +├── App.tsx # Main application component +├── main.tsx # Application entry point +└── index.css # Global styles with Tailwind +``` + +### Component Mapping + +| Component | Purpose | Location | +|-----------|---------|----------| +| **Analytics** | Dashboard with network metrics and charts | `/analytics` | +| **Blocks** | Block explorer with filtering and pagination | `/blocks` | +| **Transactions** | Transaction history and details | `/transactions` | +| **Validators** | Validator information and ranking | `/validators` | +| **Token Swaps** | Token swap orders and trading | `/token-swaps` | +| **Home** | Main dashboard with overview tables | `/` | + +## API Integration + +This project includes a complete API integration system with React Query: + +### API Functions (`src/lib/api.ts`) +- All backend API calls from the original explorer project +- TypeScript support for better type safety +- Error handling and response processing + +### React Query Hooks (`src/hooks/useApi.ts`) +- Custom hooks for each API endpoint +- Automatic caching and background updates +- Loading and error states +- Optimistic updates support + +### Available Hooks +- `useBlocks(page)` - Fetch blocks data +- `useTransactions(page, height)` - Fetch transactions +- `useAccounts(page)` - Fetch accounts +- `useValidators(page)` - Fetch validators +- `useCommittee(page, chainId)` - Fetch committee data +- `useDAO(height)` - Fetch DAO data +- `useAccount(height, address)` - Fetch account details +- `useParams(height)` - Fetch parameters +- `useSupply(height)` - Fetch supply data +- `useCardData()` - Fetch dashboard card data +- `useTableData(page, category, committee)` - Fetch table data +- And many more... + +### Usage Example +```typescript +import { useBlocks, useValidators } from './hooks/useApi' + +function MyComponent() { + const { data: blocks, isLoading, error } = useBlocks(1) + const { data: validators } = useValidators(1) + + if (isLoading) return
Loading...
+ if (error) return
Error: {error.message}
+ + return ( +
+

Blocks: {blocks?.totalCount}

+

Validators: {validators?.totalCount}

+
+ ) +} +``` + +## Technologies Used + +- **Vite** - Build tool and dev server +- **React** - UI library +- **TypeScript** - Type safety +- **Tailwind CSS** - Styling +- **React Hook Form** - Form handling +- **Framer Motion** - Animations +- **React Query** - Data fetching and caching + +## Development + +This project uses: +- ESLint for code linting +- Prettier for code formatting +- TypeScript for type checking +- React Query DevTools for debugging queries + +## API Configuration + +The application automatically configures API endpoints based on the environment: +- Default RPC URL: `http://localhost:50002` +- Default Admin RPC URL: `http://localhost:50002` +- Default Chain ID: `1` + +You can override these settings by setting `window.__CONFIG__` in your HTML. + +## License + +MIT diff --git a/cmd/rpc/web/explorer/components/api.js b/cmd/rpc/web/explorer/components/api.js deleted file mode 100644 index 25e59d2e1..000000000 --- a/cmd/rpc/web/explorer/components/api.js +++ /dev/null @@ -1,288 +0,0 @@ -let rpcURL = "http://localhost:50002"; // default value for the RPC URL -let adminRPCURL = "http://localhost:50003"; // default Admin RPC URL -let chainId = 1; // default chain id - -if (typeof window !== "undefined") { - if (window.__CONFIG__) { - rpcURL = window.__CONFIG__.rpcURL; - adminRPCURL = window.__CONFIG__.adminRPCURL; - chainId = Number(window.__CONFIG__.chainId); - } - rpcURL = rpcURL.replace("localhost", window.location.hostname); - adminRPCURL = adminRPCURL.replace("localhost", window.location.hostname); - console.log(rpcURL); -} else { - console.log("config undefined"); -} - -// RPC PATHS BELOW - -const blocksPath = "/v1/query/blocks"; -const blockByHashPath = "/v1/query/block-by-hash"; -const blockByHeightPath = "/v1/query/block-by-height"; -const txByHashPath = "/v1/query/tx-by-hash"; -const txsBySender = "/v1/query/txs-by-sender"; -const txsByRec = "/v1/query/txs-by-rec"; -const txsByHeightPath = "/v1/query/txs-by-height"; -const pendingPath = "/v1/query/pending"; -const ecoParamsPath = "/v1/query/eco-params"; -const validatorsPath = "/v1/query/validators"; -const accountsPath = "/v1/query/accounts"; -const poolPath = "/v1/query/pool"; -const accountPath = "/v1/query/account"; -const validatorPath = "/v1/query/validator"; -const paramsPath = "/v1/query/params"; -const supplyPath = "/v1/query/supply"; -const ordersPath = "/v1/query/orders"; -const dexBatchPath = "/v1/query/dex-batch"; -const nextDexBatchPath = "/v1/query/next-dex-batch"; -const poolsPath = "/v1/query/pools"; -const configPath = "/v1/admin/config"; - -// POST - -export async function POST(url, request, path) { - return fetch(url + path, { - method: "POST", - body: request, - }) - .then(async (response) => { - if (!response.ok) { - return Promise.reject(response); - } - return response.json(); - }) - .catch((rejected) => { - console.log(rejected); - return Promise.reject(rejected); - }); -} - -export async function GET(url, path) { - return fetch(url + path, { - method: "GET", - }) - .then(async (response) => { - if (!response.ok) { - return Promise.reject(response); - } - return response.json(); - }) - .catch((rejected) => { - console.log(rejected); - return Promise.reject(rejected); - }); -} - -// REQUEST OBJECTS BELOW - -function chainRequest(chain_id) { - return JSON.stringify({ chainId: chain_id }); -} - -function heightRequest(height) { - return JSON.stringify({ height: height }); -} - -function hashRequest(hash) { - return JSON.stringify({ hash: hash }); -} - -function pageAddrReq(page, addr) { - return JSON.stringify({ pageNumber: page, perPage: 10, address: addr }); -} - -function heightAndAddrRequest(height, address) { - return JSON.stringify({ height: height, address: address }); -} - -function heightAndIDRequest(height, id) { - return JSON.stringify({ height: height, id: id }); -} - -function pageHeightReq(page, height) { - return JSON.stringify({ pageNumber: page, perPage: 10, height: height }); -} - -function validatorsReq(page, height, committee) { - return JSON.stringify({ height: height, pageNumber: page, perPage: 1000, committee: committee }); -} - -// API CALLS BELOW - -export function Blocks(page, _) { - return POST(rpcURL, pageHeightReq(page, 0), blocksPath); -} - -export function Transactions(page, height) { - return POST(rpcURL, pageHeightReq(page, height), txsByHeightPath); -} - -export function Accounts(page, _) { - return POST(rpcURL, pageHeightReq(page, 0), accountsPath); -} - -export function Validators(page, _) { - return POST(rpcURL, pageHeightReq(page, 0), validatorsPath); -} - -export function Committee(page, chain_id) { - return POST(rpcURL, validatorsReq(page, 0, chain_id), validatorsPath); -} - -export function DAO(height, _) { - return POST(rpcURL, heightAndIDRequest(height, 131071), poolPath); -} - -export function Account(height, address) { - return POST(rpcURL, heightAndAddrRequest(height, address), accountPath); -} - -export async function AccountWithTxs(height, address, page) { - let result = {}; - result.account = await Account(height, address); - result.sent_transactions = await TransactionsBySender(page, address); - result.rec_transactions = await TransactionsByRec(page, address); - return result; -} - -export function Params(height, _) { - return POST(rpcURL, heightRequest(height), paramsPath); -} - -export function Supply(height, _) { - return POST(rpcURL, heightRequest(height), supplyPath); -} - -export function Validator(height, address) { - return POST(rpcURL, heightAndAddrRequest(height, address), validatorPath); -} - -export function BlockByHeight(height) { - return POST(rpcURL, heightRequest(height), blockByHeightPath); -} - -export function BlockByHash(hash) { - return POST(rpcURL, hashRequest(hash), blockByHashPath); -} - -export function TxByHash(hash) { - return POST(rpcURL, hashRequest(hash), txByHashPath); -} - -export function TransactionsBySender(page, sender) { - return POST(rpcURL, pageAddrReq(page, sender), txsBySender); -} - -export function TransactionsByRec(page, rec) { - return POST(rpcURL, pageAddrReq(page, rec), txsByRec); -} - -export function Pending(page, _) { - return POST(rpcURL, pageAddrReq(page, ""), pendingPath); -} - -export function EcoParams(chain_id) { - return POST(rpcURL, chainRequest(chain_id), ecoParamsPath); -} - -export function Orders(chain_id) { - return POST(rpcURL, heightAndIDRequest(0, chain_id), ordersPath); -} - -export async function DexBatch(committee_id) { - const [currentBatch, nextBatch] = await Promise.allSettled([ - POST(rpcURL, JSON.stringify({ height: 0, id: committee_id, points: true}), dexBatchPath), - POST(rpcURL, heightAndIDRequest(0, committee_id), nextDexBatchPath) - ]); - - const current = currentBatch.status === "fulfilled" ? currentBatch.value : null; - const next = nextBatch.status === "fulfilled" ? nextBatch.value : null; - - if (current) { - current.nextBatch = next; - } - - return current; -} - -export function Config() { - return GET(adminRPCURL, configPath); -} - -// COMPONENT SPECIFIC API CALLS BELOW - -// getModalData() executes API call(s) and prepares data for the modal component based on the search type -export async function getModalData(query, page) { - const noResult = "no result found"; - - // Handle object queries (like DexBatch data) - if (typeof query === "object" && query !== null) { - return { dexBatch: query }; - } - - // Handle string query cases - if (typeof query === "string") { - // Block by hash - if (query.length === 64) { - const block = await BlockByHash(query); - if (block?.blockHeader?.hash) return { block }; - - const tx = await TxByHash(query); - return tx?.sender ? tx : noResult; - } - - // Validator or account by address - if (query.length === 40) { - const [valResult, accResult] = await Promise.allSettled([Validator(0, query), AccountWithTxs(0, query, page)]); - - const val = valResult.status === "fulfilled" ? valResult.value : null; - const acc = accResult.status === "fulfilled" ? accResult.value : null; - - if (!acc?.account?.address && !val?.address) return noResult; - return acc?.account?.address ? { ...acc, validator: val } : { validator: val }; - } - - return noResult; - } - - // Handle block by height - const block = await BlockByHeight(query); - return block?.blockHeader?.hash ? { block } : noResult; -} - -// getCardData() executes api calls and prepares the data for the cards -export async function getCardData() { - let cardData = {}; - cardData.blocks = await Blocks(1, 0); - cardData.canopyCommittee = await Committee(1, chainId); - cardData.supply = await Supply(0, 0); - cardData.pool = await DAO(0, 0); - cardData.params = await Params(0, 0); - cardData.ecoParams = await EcoParams(0, 0); - return cardData; -} - -// getTableData() executes an api call for the table based on the page and category -export async function getTableData(page, category, committee) { - switch (category) { - case 0: - return await Blocks(page, 0); - case 1: - return await Transactions(page, 0); - case 2: - return await Pending(page, 0); - case 3: - return await Accounts(page, 0); - case 4: - return await Validators(page, 0); - case 5: - return await Params(page, 0); - case 6: - return await Orders(committee); - case 7: - return await Supply(0); - case 8: - return await DexBatch(committee); - } -} diff --git a/cmd/rpc/web/explorer/components/cards.jsx b/cmd/rpc/web/explorer/components/cards.jsx deleted file mode 100644 index 5564335dc..000000000 --- a/cmd/rpc/web/explorer/components/cards.jsx +++ /dev/null @@ -1,227 +0,0 @@ -import { addDate, convertBytes, convertNumber, convertTime } from "@/components/util"; -import { Card, Col, Row } from "react-bootstrap"; -import Truncate from "react-truncate-inside"; - -const cardImages = [ - - - - - , - - - - - , - - - - - , - - - - - , -]; -const cardTitles = ["Latest Block", "Supply", "Transactions", "Validators"]; - -// getCardHeader() returns the header information for the card -function getCardHeader(props, idx) { - const blks = props.blocks; - if (blks.results.length === 0) { - return "Loading"; - } - switch (idx) { - case 0: - return convertNumber(blks.results[0].blockHeader.height); - case 1: - return convertNumber(props.supply.total, 1000, true); - case 2: - if (blks.results[0].blockHeader.numTxs == null) { - return "+0"; - } - return "+" + convertNumber(blks.results[0].blockHeader.numTxs); - case 3: - let totalStake = 0; - if (!props.canopyCommittee.results) { - return 0; - } - props.canopyCommittee.results.forEach(function (validator) { - totalStake += Number(validator.stakedAmount); - }); - return ( - <> - {convertNumber(totalStake, 1000, true)} - {" stake"} - - ); - } -} - -// getCardSubHeader() returns the sub header of the card (right below the header) -function getCardSubHeader(props, consensusDuration, idx) { - const v = props.blocks; - if (v.results.length === 0) { - return "Loading"; - } - switch (idx) { - case 0: - return convertTime(v.results[0].blockHeader.time - consensusDuration); - case 1: - return convertNumber(Number(props.supply.total) - Number(props.supply.staked), 1000, true) + " liquid"; - case 2: - return "blk size: " + convertBytes(v.results[0].meta.size); - case 3: - if (!props.canopyCommittee.results) { - return 0 + " vals"; - } - return props.canopyCommittee.results.length + " vals"; - } -} - -// getCardRightAligned() returns the data for the right aligned note -function getCardRightAligned(props, idx) { - const v = props.blocks; - if (v.results.length === 0) { - return "Loading"; - } - switch (idx) { - case 0: - return v.results[0].meta.took; - case 1: - return convertNumber(props.supply.staked, 1000, true) + " staked"; - case 2: - return "block #" + v.results[0].blockHeader.height; - case 3: - return "stake threshold " + convertNumber(props.params.validator.stakePercentForSubsidizedCommittee, 1000) + "%"; - } -} - -// getCardNote() returns the data for the small text above the footer -function getCardNote(props, idx) { - const v = props.blocks; - if (v.results.length === 0) { - return "Loading"; - } - switch (idx) { - case 0: - return ; - case 1: - return "+" + Number(props.ecoParams.MintPerBlock/1000000) + "/blk"; - case 2: - return "TOTAL " + convertNumber(v.results[0].blockHeader.totalTxs); - case 3: - if (!props.canopyCommittee.results) { - return "MaxStake: " + 0; - } - return "MaxStake: " + convertNumber(props.canopyCommittee.results[0].stakedAmount, 1000, true); - default: - return "?"; - } -} - -// getCardFooter() returns the data for the footer of the card -function getCardFooter(props, consensusDuration, idx) { - const v = props.blocks; - if (v.results.length === 0) { - return "Loading"; - } - switch (idx) { - case 0: - return "Next block: " + addDate(v.results[0].blockHeader.time, consensusDuration); - case 1: - let s = "DAO pool supply: "; - if (props.pool != null) { - return s + convertNumber(props.pool.amount, 1000, true); - } - return s; - case 2: - let totalFee = 0, - txs = v.results[0].transactions; - if (txs == null || txs.length === 0) { - return "Average fee in last blk: 0"; - } - txs.forEach(function (tx) { - let fee = Number(tx.transaction.fee); - totalFee += !isNaN(fee) ? fee : 0; - }); - let txWithFee = txs.filter((tx) => tx.transaction.fee != null); - - return `Average fee in last blk: ${totalFee > 0 ? convertNumber(totalFee / txWithFee.length, 1000000) : 0}`; - case 3: - let totalStake = 0; - if (!props.canopyCommittee.results) { - return 0 + "% in validator set"; - } - props.canopyCommittee.results.forEach(function (validator) { - totalStake += Number(validator.stakedAmount); - }); - return ((totalStake / props.supply.staked) * 100).toFixed(1) + "% in validator set"; - } -} - -// getCardOnClick() returns the callback function when a certain card is clicked -function getCardOnClick(props, index) { - if (index === 0) { - return () => props.openModal(0); - } else { - if (index === 1) { - return () => props.selectTable(7, 0); - } else if (index === 2) { - return () => props.selectTable(1, 0); - } - return () => props.selectTable(index + 1, 0); - } -} - -// Cards() returns the main component -export default function Cards(props) { - const cardData = props.state.cardData; - const consensusDuration = props.state.consensusDuration; - return ( - - {Array.from({ length: 4 }, (_, idx) => { - return ( - - - -
{cardImages[idx]}
- {cardTitles[idx]} -
{getCardHeader(cardData, idx)}
-
- - {getCardSubHeader(cardData, consensusDuration, idx)} - - {getCardRightAligned(cardData, idx)} -
-
{getCardNote(cardData, idx)}
- {getCardFooter(cardData, consensusDuration, idx)} -
-
- - ); - })} -
- ); -} diff --git a/cmd/rpc/web/explorer/components/modal.jsx b/cmd/rpc/web/explorer/components/modal.jsx deleted file mode 100644 index 90120547a..000000000 --- a/cmd/rpc/web/explorer/components/modal.jsx +++ /dev/null @@ -1,495 +0,0 @@ -import React from "react"; -import Truncate from "react-truncate-inside"; -import { JsonViewer } from "@textea/json-viewer"; -import { Modal, Table, Tab, Tabs, CardGroup, Card, Toast, ToastContainer, Button } from "react-bootstrap"; -import * as API from "@/components/api"; -import { - copy, - cpyObj, - convertIfTime, - isEmpty, - pagination, - upperCaseAndRepUnderscore, - withTooltip, - convertTx, - toCNPY, -} from "@/components/util"; - -// convertCardData() converts the data from state into a display object for rendering -function convertCardData(state, v) { - if (!v) return { None: "" }; - const value = cpyObj(v); - if (value.transaction) { - delete value.transaction; - return value; - } - if (value.dexBatch) { - const successfulReceipts = value.dexBatch.receipts?.filter(amount => amount > 0).length || 0; - const totalReceipts = value.dexBatch.receipts?.length || 0; - return { - Committee: value.dexBatch.Committee || value.dexBatch.committee, - Orders: value.dexBatch.orders?.length || 0, - PoolSize: toCNPY(value.dexBatch.pool_size || value.dexBatch.poolSize || 0), - CounterPoolSize: toCNPY(value.dexBatch.counter_pool_size || value.dexBatch.counterPoolSize || 0), - LockedHeight: value.dexBatch.locked_height || value.dexBatch.lockedHeight || "null", - Receipts: `${successfulReceipts}/${totalReceipts}`, - }; - } - return value.block - ? { - height: value.block.blockHeader.height, - hash: value.block.blockHeader.hash, - proposer: value.block.blockHeader.proposerAddress, - } - : value.validator && !state.modalState.accOnly - ? { - address: value.validator.address, - publicKey: value.validator.publicKey, - netAddress: value.validator.netAddress, - outputAddress: value.validator.output, - } - : value.account; -} - -// convertPaginated() converts a paginated item into a display object for rendering -function convertPaginated(v) { - if (v == null || v === 0) return [0]; - if ("block" in v) return convertBlock(v) || { None: "" }; - if ("transaction" in v) return { ...v, transaction: undefined }; - return v; -} - -// convertTransactions() converts an array of transactions into a suitable display object -export function convertTransactions(txs) { - for (let i = 0; i < txs.length; i++) { - txs[i] = convertTx(txs[i]); - } - return txs; -} - -// convertBlock() converts a block item into a display object for rendering -export function convertBlock(blk) { - let { lastQuorumCertificate, nextValidatorRoot, stateRoot, transactionRoot, validatorRoot, vdf, ...value } = - blk.block.blockHeader; - return value; -} - -// convertCertificateResults() converts a qc item into a display object for rendering -export function convertCertificateResults(qc) { - return { - certificate_height: qc.header.height, - network_id: qc.header.networkID, - chain_id: qc.header.chainId, - block_hash: qc.blockHash, - results_hash: qc.resultsHash, - }; -} - -// convertTabData() converts the modal data into specific tab display object for rendering -function convertTabData(state, v, tab) { - if ("block" in v) { - switch (tab) { - case 0: - return convertBlock(v); - case 1: - return v.block.transactions ? convertTransactions(v.block.transactions) : 0; - default: - return v.block; - } - } else if ("transaction" in v) { - switch (tab) { - case 0: - if ("qc" in v.transaction.msg) return convertCertificateResults(v.transaction.msg.qc); - return v.transaction.msg; - case 1: - return { hash: v.txHash, time: v.transaction.time, sender: v.sender, type: v.messageType }; - default: - return v; - } - } else if ("validator" in v && !state.modalState.accOnly) { - let validator = cpyObj(v.validator); - if (validator.committees && Array.isArray(validator.committees)) { - validator.committees = validator.committees.join(","); - } - if (validator.stakedAmount) { - validator.stakedAmount = toCNPY(validator.stakedAmount); - } - return validator; - } else if ("account" in v) { - let txs = v.sent_transactions.results.length > 0 ? v.sent_transactions.results : v.rec_transactions.results; - switch (tab) { - case 0: - let account = cpyObj(v.account); - account.amount = toCNPY(account.amount); - return account; - case 1: - return convertTransactions(txs); - default: - return convertTransactions(txs); - } - } else if ("dexBatch" in v) { - switch (tab) { - case 0: // Orders - return v.dexBatch.orders || []; - case 1: // Deposits - return v.dexBatch.deposits || []; - case 2: // Withdrawals - return v.dexBatch.withdraws || []; - case 3: // Pool Points - return v.dexBatch.poolPoints || v.dexBatch.pool_points || []; - case 4: // Receipts - return v.dexBatch.receipts?.map((amount, index) => ({ - OrderIndex: index, - DistributedAmount: formatLocaleNumber(amount, 0, 6), - Status: amount > 0 ? "Success" : "Failed" - })) || []; - case 5: // Raw - default: - return v.dexBatch; - } - } -} - -// getModalTitle() extracts the modal title from the object -function getModalTitle(state, v) { - if ("transaction" in v) return "Transaction"; - if ("block" in v) return "Block"; - if ("dexBatch" in v) return "Dex Batch"; - if ("validator" in v && !state.modalState.accOnly) return "Validator"; - return "Account"; -} - -// getTabTitle() extracts the tab title from the object -function getTabTitle(state, data, tab) { - if ("transaction" in data) { - return tab === 0 ? "Message" : tab === 1 ? "Meta" : "Raw"; - } - if ("block" in data) { - return tab === 0 ? "Header" : tab === 1 ? "Transactions" : "Raw"; - } - if ("dexBatch" in data) { - switch (tab) { - case 0: return "Orders"; - case 1: return "Deposits"; - case 2: return "Withdrawals"; - case 3: return "Pool Points"; - case 4: return "Receipts"; - case 5: return "Raw"; - default: return "Raw"; - } - } - if ("validator" in data && !state.modalState.accOnly) { - return tab === 0 ? "Validator" : tab === 1 ? "Account" : "Raw"; - } - return tab === 0 ? "Account" : tab === 1 ? "Sent Transactions" : "Received Transactions"; -} - -// DetailModal() returns the main modal component for this file -export default function DetailModal({ state, setState }) { - const data = state.modalState.data; - const cards = convertCardData(state, data); - - // Local state for filtering and sorting - const [addressFilter, setAddressFilter] = React.useState(''); - const [sortBy, setSortBy] = React.useState('none'); - const [sortDirection, setSortDirection] = React.useState('asc'); - - // check if the data is empty or no results - if (isEmpty(data)) return <>; - - if (data === "no result found") { - return ( - - - - no results found - - - ); - } - - // resetState() resets the modal state back to initial - function resetState() { - setState({ ...state, modalState: { show: false, query: "", page: 0, data: {}, accOnly: false } }); - } - - // renderTab() renders a tab based on the state data and tab number - function renderTab(tab) { - if ("block" in data) { - return tab === 0 ? renderBasicTable(tab) : tab === 1 ? renderPageTable(tab) : renderJSONViewer(tab); - } - if ("transaction" in data) { - return tab === 0 ? renderBasicTable(tab) : tab === 1 ? renderBasicTable(tab) : renderJSONViewer(tab); - } - if ("dexBatch" in data) { - return tab === 5 ? renderJSONViewer(tab) : renderDexBatchList(tab); - } - if ("validator" in data && !state.modalState.accOnly) { - return tab === 0 ? renderBasicTable(tab) : tab === 1 ? renderTableButton() : renderJSONViewer(tab); - } - return tab === 0 ? renderBasicTable(tab) : renderPageTable(tab); - } - - // renderBasicTable() organizes the data into a table based on the tab number - function renderBasicTable(tab) { - const body = convertTabData(state, data, tab); - return ( - - - {Object.keys(body).map((k, i) => ( - - - - - ))} - -
{upperCaseAndRepUnderscore(k)}{convertIfTime(k, body[k])}
- ); - } - - // renderPageTable() organizes the data into a paginated table based on the tab number - function renderPageTable(tab) { - let start = 0, - end = 10, - page = [0], - d = data, - ms = state.modalState, - blk = d.block; - if ("block" in d) { - end = ms.page === 0 || ms.page === 1 ? 10 : ms.page * 10; - start = end - 10; - page = blk.transactions || page; - d = { pageNumber: ms.Page, perPage: 10, totalPages: Math.ceil(blk.blockHeader.num_txs / 10), ...d }; - } else if ("account" in d) { - page = - tab === 1 ? convertTransactions(d.sent_transactions.results) : convertTransactions(d.rec_transactions.results); - d = tab === 1 ? d.sent_transactions : d.rec_transactions; - } - return ( - <> - - - - {Object.keys(convertPaginated(convertTabData(state, data, 1)[0])).map((k, i) => ( - - ))} - - {page.slice(start, end).map((item, key) => ( - - {Object.keys(convertPaginated(item)).map((k, i) => ( - - ))} - - ))} - -
- {upperCaseAndRepUnderscore(k)} -
- {convertIfTime(k, item[k])} -
- {pagination(d, (p) => - API.getModalData(ms.query, p).then((r) => { - setState({ ...state, modalState: { ...ms, show: true, query: ms.query, page: p, data: r } }); - }), - )} - - ); - } - - // renderJSONViewer() renders a raw json display - function renderJSONViewer(tab) { - return ; - } - - // filterAndSortItems() filters and sorts items based on current filters - function filterAndSortItems(items, tab) { - if (!items || items.length === 0) return items; - - // Filter by address if filter is set - let filteredItems = items; - if (addressFilter) { - filteredItems = items.filter(item => { - // Check all fields for address-like values - return Object.values(item).some(value => - value && value.toString().toLowerCase().includes(addressFilter.toLowerCase()) - ); - }); - } - - // Sort by amount/points if sortBy is set - if (sortBy !== 'none') { - filteredItems = [...filteredItems].sort((a, b) => { - let aValue = 0; - let bValue = 0; - - // Determine sort field based on tab and sortBy selection - if (sortBy === 'amount') { - // Look for amount-related fields - const amountFields = ['amount', 'amountForSale', 'requestedAmount', 'points']; - const aField = amountFields.find(field => field in a); - const bField = amountFields.find(field => field in b); - aValue = aField ? parseFloat(a[aField]) || 0 : 0; - bValue = bField ? parseFloat(b[bField]) || 0 : 0; - } - - return sortDirection === 'asc' ? aValue - bValue : bValue - aValue; - }); - } - - return filteredItems; - } - - // renderDexBatchList() renders a list view for DexBatch components - function renderDexBatchList(tab) { - const items = convertTabData(state, data, tab); - - if (!items || items.length === 0) { - return
No items found
; - } - - const filteredAndSortedItems = filterAndSortItems(items, tab); - - return ( -
- {/* Filter and Sort Controls */} -
-
-
- - setAddressFilter(e.target.value)} - /> -
-
- - -
-
- - -
-
-
- Showing {filteredAndSortedItems.length} of {items.length} items -
-
- - {/* Filtered Items List */} - {filteredAndSortedItems.length === 0 ? ( -
No items match the current filter
- ) : ( - filteredAndSortedItems.map((item, index) => ( -
- - - {Object.keys(item).map((key, i) => ( - - - - - ))} - -
- {upperCaseAndRepUnderscore(key)} - - {key === 'address' || key === 'Address' ? ( - - ) : key.toLowerCase().includes('amount') || key.toLowerCase().includes('points') ? ( - toCNPY(item[key] || 0) - ) : ( - item[key]?.toString() || 'null' - )} -
-
- )) - )} -
- ); - } - - // renderTableButtons() renders a button to display the account - function renderTableButton() { - return ( - - ); - } - - let toCNPYFields = ["amount", "stakedAmount"]; - - // return the Modal - return ( - - - - {/* TITLE */} -

-
- - - - - -
- {getModalTitle(state, data)} Details -

- {/* CARDS */} - - {Object.keys(cards).map((k, i) => { - return withTooltip( - copy(state, setState, cards[k])} key={i} className="modal-cards"> - -
{k}
-
- -
- copy -
-
, - cards[k], - i, - "top", - ); - })} -
- {/* TABS */} - - {[...Array("dexBatch" in data ? 6 : 3)].map((_, i) => ( - - {renderTab(i)} - - ))} - -
-
- ); -} diff --git a/cmd/rpc/web/explorer/components/navbar.jsx b/cmd/rpc/web/explorer/components/navbar.jsx deleted file mode 100644 index 62def2930..000000000 --- a/cmd/rpc/web/explorer/components/navbar.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import { convertIfNumber } from "@/components/util"; -import { useState } from "react"; -import { Form } from "react-bootstrap"; -import Container from "react-bootstrap/Container"; -import Navbar from "react-bootstrap/Navbar"; - -export default function Navigation({ openModal }) { - let [query, setQuery] = useState(""); - let q = ""; - let urls = { discord: "https://discord.gg/pNcSJj7Wdh", x: "https://x.com/CNPYNetwork" }; - - return ( - <> - - - - Scanopy Logo - -
-
{ - e.preventDefault(); - openModal(convertIfNumber(query), 0); - }} - > - { - setQuery(e.target.value); - }} - /> - -
- -