From 0215eaf53a0b8ee68f7f249a5384c0c038b6d333 Mon Sep 17 00:00:00 2001 From: moscowchill Date: Mon, 23 Feb 2026 15:58:14 +0000 Subject: [PATCH 01/12] fix: normalize Z-prefix addresses and fix token contract display bugs - Normalize lowercase z to uppercase Z in /address/aggregate route handler - Fix GetBalance RPC call to use uppercase Z prefix (Zond node requirement) - Normalize contract addresses to uppercase Z in /contracts endpoint - Fix frontend address page to normalize lowercase z in URL params - Fix "QRL QRL" double-unit display in token contract view (formatAmount returns tuple) Co-Authored-By: Claude Opus 4.6 --- ExplorerFrontend/app/address/[query]/page.tsx | 5 ++++- .../app/address/[query]/token-contract-view.tsx | 4 ++-- backendAPI/db/address.go | 8 +++++++- backendAPI/routes/routes.go | 16 ++++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/ExplorerFrontend/app/address/[query]/page.tsx b/ExplorerFrontend/app/address/[query]/page.tsx index 8205403..f46ec7a 100644 --- a/ExplorerFrontend/app/address/[query]/page.tsx +++ b/ExplorerFrontend/app/address/[query]/page.tsx @@ -85,7 +85,10 @@ async function fetchAddressData(address: string): Promise { export default async function Page({ params }: PageProps): Promise { const resolvedParams = await params; - const address = resolvedParams.query; + // Normalize lowercase z prefix to uppercase Z + const address = resolvedParams.query.startsWith('z') && !resolvedParams.query.startsWith('0x') + ? 'Z' + resolvedParams.query.slice(1) + : resolvedParams.query; const addressData = await fetchAddressData(address); const handlerUrl = process.env.NEXT_PUBLIC_HANDLER_URL || process.env.HANDLER_URL || 'http://127.0.0.1:8080'; diff --git a/ExplorerFrontend/app/address/[query]/token-contract-view.tsx b/ExplorerFrontend/app/address/[query]/token-contract-view.tsx index 17d0a16..95bee8d 100644 --- a/ExplorerFrontend/app/address/[query]/token-contract-view.tsx +++ b/ExplorerFrontend/app/address/[query]/token-contract-view.tsx @@ -403,7 +403,7 @@ export default function TokenContractView({ address, contractData, handlerUrl }:
Transaction Fee
{creationTx?.GasUsed && creationTx?.GasPrice - ? `${formatAmount(`0x${(BigInt(creationTx.GasUsed) * BigInt(creationTx.GasPrice)).toString(16)}`)} QRL` + ? `${formatAmount(`0x${(BigInt(creationTx.GasUsed) * BigInt(creationTx.GasPrice)).toString(16)}`)[0]} QRL` : '-'}
@@ -412,7 +412,7 @@ export default function TokenContractView({ address, contractData, handlerUrl }:
Value
{creationTx?.Value - ? `${formatAmount(creationTx.Value)} QRL` + ? `${formatAmount(creationTx.Value)[0]} QRL` : '0 QRL'}
diff --git a/backendAPI/db/address.go b/backendAPI/db/address.go index 71e16a4..923e972 100644 --- a/backendAPI/db/address.go +++ b/backendAPI/db/address.go @@ -144,10 +144,16 @@ func ReturnRankAddress(address string) (int64, error) { func GetBalance(address string) (float64, string) { var result models.Balance + // Ensure address has uppercase Z prefix for RPC calls + rpcAddress := address + if strings.HasPrefix(rpcAddress, "z") { + rpcAddress = "Z" + rpcAddress[1:] + } + group := models.JsonRPC{ Jsonrpc: "2.0", Method: "zond_getBalance", - Params: []interface{}{address, "latest"}, + Params: []interface{}{rpcAddress, "latest"}, ID: 1, } b, err := json.Marshal(group) diff --git a/backendAPI/routes/routes.go b/backendAPI/routes/routes.go index 253721a..0ed3619 100644 --- a/backendAPI/routes/routes.go +++ b/backendAPI/routes/routes.go @@ -284,6 +284,11 @@ func UserRoute(router *gin.Engine) { router.GET("/address/aggregate/:query", func(c *gin.Context) { param := c.Param("query") + // Normalize address: convert lowercase z prefix to uppercase Z + if strings.HasPrefix(param, "z") && !strings.HasPrefix(param, "z0") { + param = "Z" + param[1:] + } + // Single Address data addressData, err := db.ReturnSingleAddress(param) if err != nil && err != mongo.ErrNoDocuments { @@ -605,6 +610,17 @@ func UserRoute(router *gin.Engine) { }) return } + + // Normalize addresses: ensure Z prefix is uppercase for display + for i := range query { + if strings.HasPrefix(query[i].ContractAddress, "z") { + query[i].ContractAddress = "Z" + query[i].ContractAddress[1:] + } + if strings.HasPrefix(query[i].ContractCreatorAddress, "z") { + query[i].ContractCreatorAddress = "Z" + query[i].ContractCreatorAddress[1:] + } + } + c.JSON(http.StatusOK, gin.H{ "response": query, "total": total, From ac00b4382e48137726c0fb90ec5656075d1853dd Mon Sep 17 00:00:00 2001 From: moscowchill Date: Mon, 23 Feb 2026 16:04:23 +0000 Subject: [PATCH 02/12] docs: add Z-to-Q address prefix migration plan Comprehensive audit and phased migration plan for the upcoming network upgrade from Z-prefix to Q-prefix addresses. Covers all 24 affected files across syncer, backend, frontend, and scripts. Co-Authored-By: Claude Opus 4.6 --- docs/Z-TO-Q-MIGRATION-PLAN.md | 356 ++++++++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 docs/Z-TO-Q-MIGRATION-PLAN.md diff --git a/docs/Z-TO-Q-MIGRATION-PLAN.md b/docs/Z-TO-Q-MIGRATION-PLAN.md new file mode 100644 index 0000000..8ba6d0f --- /dev/null +++ b/docs/Z-TO-Q-MIGRATION-PLAN.md @@ -0,0 +1,356 @@ +# Z-to-Q Address Prefix Migration Plan + +## Overview + +The QRL Zond network is upgrading from Z-prefix addresses to Q-prefix addresses. This document catalogs every change needed across the entire codebase and provides a phased migration strategy. + +**Scope:** 30+ files across 3 components (syncer, backend API, frontend) plus infrastructure. + +--- + +## Phase 1: Create Abstraction Layer (Do First) + +Before changing any prefix, centralize all prefix logic so the actual switch is a one-line change. + +### 1.1 Syncer — New helpers in `Zond2mongoDB/validation/hex.go` + +Create these functions to replace all scattered prefix logic: + +```go +// Configurable prefix - change this ONE constant when the network upgrades +const AddressPrefix = "Z" // Change to "Q" at network upgrade time + +func GetAddressPrefix() string { return AddressPrefix } + +func GetZeroAddress() string { + return AddressPrefix + "0000000000000000000000000000000000000000" +} + +func IsZeroAddress(addr string) bool { + stripped := strings.ToLower(StripAddressPrefix(addr)) + return stripped == "0000000000000000000000000000000000000000" || + stripped == "0" || stripped == "" +} + +func StripAddressPrefix(address string) string { + lower := strings.ToLower(address) + for _, prefix := range []string{"0x", "z", "q"} { + if strings.HasPrefix(lower, prefix) { + return address[len(prefix):] + } + } + return address +} + +func NormalizeAddress(address string) string { + hex := strings.ToLower(StripAddressPrefix(address)) + if hex == "" { return "" } + return strings.ToLower(AddressPrefix) + hex +} + +func NormalizeAddressUpper(address string) string { + hex := strings.ToLower(StripAddressPrefix(address)) + if hex == "" { return "" } + return AddressPrefix + hex +} + +func IsValidAddress(address string) bool { + lower := strings.ToLower(address) + if strings.HasPrefix(lower, "z") || strings.HasPrefix(lower, "q") { + hex := lower[1:] + if len(hex) != 40 { return false } + for _, c := range hex { + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { return false } + } + return true + } + if strings.HasPrefix(lower, "0x") { + hex := lower[2:] + if len(hex) != 40 { return false } + for _, c := range hex { + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { return false } + } + return true + } + return false +} +``` + +**Rename:** `ConvertToZAddress()` → call `NormalizeAddressUpper()` instead (update all 3 call sites in `rpc/calls.go`). + +**Delete:** `QRLZeroAddress` constant from `configs/const.go` → replace all 6 usages with `validation.GetZeroAddress()`. + +### 1.2 Backend API — New helpers in `backendAPI/db/helpers.go` + +```go +package db + +import "strings" + +const AddressPrefix = "Z" // Change to "Q" at network upgrade + +func stripPrefix(addr string) string { + lower := strings.ToLower(addr) + for _, p := range []string{"0x", "z", "q"} { + if strings.HasPrefix(lower, p) { return addr[len(p):] } + } + return addr +} + +func normalizeAddr(addr string) string { + return strings.ToLower(string(AddressPrefix[0])+strings.ToLower(stripPrefix(addr))) +} + +func normalizeAddrUpper(addr string) string { + return AddressPrefix + strings.ToLower(stripPrefix(addr)) +} + +func addrVariants(addr string) []string { + hex := strings.ToLower(stripPrefix(addr)) + return []string{ + "z" + hex, "Z" + hex, + "q" + hex, "Q" + hex, + } +} +``` + +Then refactor: +- `db/token.go` lines 111-125: Replace `normalizeAddress()` and `normalizeAddressBoth()` with these helpers +- `db/address.go` line 32: Use `normalizeAddr()` +- `db/contract.go` lines 98-100: Use `addrVariants()` +- `routes/routes.go` lines 288-289, 616-620: Use `normalizeAddrUpper()` + +### 1.3 Frontend — Update `ExplorerFrontend/app/lib/helpers.ts` + +```typescript +// Change this ONE constant when the network upgrades +export const ADDRESS_PREFIX = 'Z'; // Change to 'Q' at network upgrade + +export function stripAddressPrefix(addr: string): string { + if (!addr) return ''; + const lower = addr.toLowerCase(); + if (lower.startsWith('0x')) return addr.slice(2); + if (lower.startsWith('z') || lower.startsWith('q')) return addr.slice(1); + return addr; +} + +export function normalizeAddress(addr: string): string { + return ADDRESS_PREFIX + stripAddressPrefix(addr).toLowerCase(); +} + +export function isZondAddress(addr: string): boolean { + const lower = addr.toLowerCase(); + return lower.startsWith('z') || lower.startsWith('q'); +} +``` + +Then update: +- `formatAddress()` (lines 224-249): Use `ADDRESS_PREFIX` instead of hardcoded `'Z'` (4 occurrences) +- `normalizeHexString()` (lines 146-148): Use `isZondAddress()` check +- `decodeTokenTransferInput()` (lines 286, 314, 318): Use `ADDRESS_PREFIX` instead of `'Z'` + +--- + +## Phase 2: Update All Hardcoded Z References + +### 2.1 Syncer (`Zond2mongoDB/`) + +| File | Lines | Current Code | Change To | +|------|-------|-------------|-----------| +| `configs/const.go` | 13 | `QRLZeroAddress = "Z000..."` | Delete, use `validation.GetZeroAddress()` | +| `validation/hex.go` | 30-46 | `HasPrefix(addr, "Z")` | Use new `IsValidAddress()` | +| `validation/hex.go` | 96-105 | `StripAddressPrefix` with hardcoded Z | Use new version supporting Z/Q | +| `validation/hex.go` | 107-121 | `ConvertToZAddress()` | Replace with `NormalizeAddressUpper()` | +| `rpc/calls.go` | 346, 354, 416 | `ConvertToZAddress()` calls | `NormalizeAddressUpper()` | +| `rpc/tokenscalls.go` | 49-52 | `HasPrefix(addr, "Z")` + `"Z" + addr` | `NormalizeAddressUpper()` | +| `rpc/tokenscalls.go` | 329-334 | 6 hardcoded zero addr checks | `IsZeroAddress()` | +| `rpc/tokenscalls.go` | 341-346, 354-357 | Z prefix enforcement | `NormalizeAddressUpper()` | +| `rpc/tokenscalls.go` | 441-442 | `"Z" + TrimLeftZeros(...)` | `AddressPrefix + TrimLeftZeros(...)` | +| `rpc/tokenscalls.go` | 560-566 | Z prefix in ParseTransferEvent | Use helper | +| `rpc/tokenscalls.go` | 631 | `"Z" + addressHex` | `AddressPrefix + addressHex` | +| `db/tokentransfers.go` | 38, 42, 229, 233 | `configs.QRLZeroAddress` | `validation.GetZeroAddress()` | +| `db/tokentransfers.go` | 199-201 | `"Z" + TrimLeftZeros(...)` | `AddressPrefix + TrimLeftZeros(...)` | +| `db/tokentransfers.go` | 228, 232 | `from == "Z"` comparison | `from == AddressPrefix` | +| `db/tokenbalances.go` | 32-35 | Hardcoded zero addr checks | `IsZeroAddress()` | +| `db/tokenbalances.go` | 33 | `configs.QRLZeroAddress` | `validation.GetZeroAddress()` | +| `scripts/reindex_tokens.py` | 95-96 | `'Z' + topics[1][-40:]` | Make prefix configurable | +| `scripts/reindex_tokens.py` | 133, 158 | Z zero address comparisons | Use Q zero address | +| `scripts/reindex_contracts.py` | 147, 236-241 | `"Z" + hex` | Make prefix configurable | + +**No changes needed** (already prefix-agnostic via `.ToLower()`): +- `db/transactions.go` — uses `strings.ToLower()` normalization +- `db/coinbase.go` — uses `strings.ToLower()` normalization +- `db/contracts.go` — uses `strings.ToLower()` normalization + +### 2.2 Backend API (`backendAPI/`) + +| File | Lines | Current Code | Change To | +|------|-------|-------------|-----------| +| `db/address.go` | 32 | `strings.ToLower(query)` | Use `normalizeAddr()` | +| `db/address.go` | 104 | `TrimPrefix(addressHex, "z")` | `stripPrefix()` | +| `db/address.go` | 149-150 | Lowercase z→Z for RPC | `normalizeAddrUpper()` | +| `db/transaction.go` | 71-73, 172-174, 355, 564 | `HasPrefix(addr, "Z")` then prepend Z | `normalizeAddrUpper()` | +| `db/transaction.go` | 76-82 | TrimPrefix Z + case variants | Use `stripPrefix()` | +| `db/transaction.go` | 313 | `TrimPrefix(addr, "Z"), "z")` | `stripPrefix()` | +| `db/contract.go` | 34-40 | Search with z prefix | Use `addrVariants()` | +| `db/contract.go` | 98-100 | Both Z and z variants | Use `addrVariants()` | +| `db/token.go` | 111-125 | `normalizeAddress/Both` | Replace with new helpers | +| `routes/routes.go` | 288-289 | Lowercase z→Z normalization | `normalizeAddrUpper()` | +| `routes/routes.go` | 616-620 | Contract addr normalization | `normalizeAddrUpper()` | + +### 2.3 Frontend (`ExplorerFrontend/`) + +| File | Lines | Current Code | Change To | +|------|-------|-------------|-----------| +| `app/lib/helpers.ts` | 146-148 | `startsWith('Z') \|\| startsWith('z')` | `isZondAddress()` | +| `app/lib/helpers.ts` | 229, 239, 244 | `'Z' + ...` in formatAddress | `ADDRESS_PREFIX + ...` | +| `app/lib/helpers.ts` | 286, 314, 318 | `'Z' + hex` in decodeTokenTransfer | `ADDRESS_PREFIX + hex` | +| `app/components/SearchBar.tsx` | 14-16 | `startsWith('Z')` + regex `^Z[hex]{40}$` | Support both Z and Q | +| `app/components/SearchBar.tsx` | 77 | Placeholder `"Zxx"` | Update text | +| `app/address/[query]/page.tsx` | 89 | `startsWith('z')` → `'Z' + ...` | Handle both z/q → ADDRESS_PREFIX | +| `app/address/[query]/address-view.tsx` | 62, 69 | `startsWith("Z")` → "Zond Address" | Support Q prefix too | +| `app/validators/validators-client.tsx` | 104 | `startsWith('Z') ? addr : 'Z' + addr` | Use `normalizeAddress()` | +| `app/validators/components/ValidatorTable.tsx` | 231, 234 | Hardcoded `Z{addr.slice(...)}` | Remove hardcoded prefix | + +--- + +## Phase 3: MongoDB Data Migration + +### 3.1 Collections to migrate + +All collections storing addresses with z/Z prefix need updating: + +``` +addresses.id +transactionByAddress.from, .to +internalTransactionByAddress.from, .to +contractCode.address, .creatorAddress +tokenTransfers.from, .to, .contractAddress +tokenBalances.holderAddress, .contractAddress +pending_transactions.from, .to +``` + +### 3.2 Migration script (run at cutover) + +```javascript +// migrate-z-to-q.js +// Run: mongosh qrldata-z migrate-z-to-q.js + +const collections = { + 'addresses': ['id'], + 'contractCode': ['address', 'creatorAddress'], + 'tokenTransfers': ['from', 'to', 'contractAddress'], + 'tokenBalances': ['holderAddress', 'contractAddress'], + 'transactionByAddress': ['from', 'to'], + 'pending_transactions': ['from', 'to'], +}; + +for (const [collName, fields] of Object.entries(collections)) { + const coll = db.getCollection(collName); + for (const field of fields) { + // Lowercase z → lowercase q + const filter = {}; + filter[field] = /^z/; + const count = coll.countDocuments(filter); + print(`${collName}.${field}: ${count} documents with z-prefix`); + + if (count > 0) { + coll.find(filter).forEach(doc => { + const update = {}; + update[field] = 'q' + doc[field].slice(1); + coll.updateOne({_id: doc._id}, {$set: update}); + }); + print(` -> migrated ${count} documents`); + } + + // Uppercase Z → lowercase q + const filterUpper = {}; + filterUpper[field] = /^Z/; + const countUpper = coll.countDocuments(filterUpper); + print(`${collName}.${field}: ${countUpper} documents with Z-prefix`); + + if (countUpper > 0) { + coll.find(filterUpper).forEach(doc => { + const update = {}; + update[field] = 'q' + doc[field].slice(1).toLowerCase(); + coll.updateOne({_id: doc._id}, {$set: update}); + }); + print(` -> migrated ${countUpper} documents`); + } + } +} + +print('Migration complete!'); +``` + +### 3.3 Index rebuild + +After migration, rebuild indexes on address fields: + +```javascript +db.addresses.reIndex(); +db.contractCode.reIndex(); +db.tokenTransfers.reIndex(); +db.tokenBalances.reIndex(); +db.transactionByAddress.reIndex(); +``` + +--- + +## Phase 4: The Actual Switch + +Once all abstraction is in place, the switch is just changing constants: + +1. **Syncer:** `validation/hex.go` → `const AddressPrefix = "Q"` +2. **Backend:** `db/helpers.go` → `const AddressPrefix = "Q"` +3. **Frontend:** `app/lib/helpers.ts` → `export const ADDRESS_PREFIX = 'Q'` +4. **Run MongoDB migration script** +5. **Rebuild & restart all services** + +--- + +## Phase 5: Transition Period (Support Both) + +During the transition, the system should accept both Z and Q prefixes from users: + +- **SearchBar:** Accept both `Z...` and `Q...` as valid addresses +- **Backend routes:** Normalize both to the current canonical prefix +- **DB queries:** Use `addrVariants()` to search all prefix variants +- **Display:** Always show the canonical prefix (Q after migration) + +This is already partially implemented (the codebase handles both Z and z). Extending to Q/q follows the same pattern. + +--- + +## Deployment Order + +1. Deploy **backend** with dual Z/Q support (Phase 1-2) +2. Deploy **frontend** with dual Z/Q support (Phase 1-2) +3. **Coordinate with node upgrade** — confirm node returns Q-prefix addresses +4. Deploy **syncer** with Q-prefix output +5. Run **MongoDB migration** for historical data +6. Flip the `AddressPrefix` constant to `"Q"` +7. Rebuild and restart all services +8. After stabilization, remove Z-prefix backward compat code (Phase 5 cleanup) + +--- + +## Risk Mitigation + +- **Backup MongoDB** before running migration: `mongodump --db qrldata-z` +- **Test in staging** with a copy of production data +- **Feature flag approach**: The `AddressPrefix` constant acts as a feature flag +- **Rollback**: Change constant back to `"Z"`, re-run migration in reverse +- **Monitor**: Watch PM2 logs for address-related errors after deployment + +--- + +## File Count Summary + +| Component | Files to Change | Critical | Already Compatible | +|-----------|----------------|----------|--------------------| +| Syncer (Zond2mongoDB) | 10 | 5 | 3 (transactions, coinbase, contracts) | +| Backend (backendAPI) | 6 | 4 | 0 | +| Frontend (ExplorerFrontend) | 6 | 3 | rest (depend on helpers.ts) | +| Infrastructure | 2 (Python scripts) | 2 | deploy scripts (no change needed) | +| **Total** | **24 files** | **14 critical** | **3 already compatible** | From 558468d1d0c56b58f81d98398588cc96936d510a Mon Sep 17 00:00:00 2001 From: moscowchill Date: Tue, 24 Feb 2026 10:13:46 +0100 Subject: [PATCH 03/12] docs: move Z-to-Q migration plan to parent repo docs folder --- docs/Z-TO-Q-MIGRATION-PLAN.md | 356 ---------------------------------- 1 file changed, 356 deletions(-) delete mode 100644 docs/Z-TO-Q-MIGRATION-PLAN.md diff --git a/docs/Z-TO-Q-MIGRATION-PLAN.md b/docs/Z-TO-Q-MIGRATION-PLAN.md deleted file mode 100644 index 8ba6d0f..0000000 --- a/docs/Z-TO-Q-MIGRATION-PLAN.md +++ /dev/null @@ -1,356 +0,0 @@ -# Z-to-Q Address Prefix Migration Plan - -## Overview - -The QRL Zond network is upgrading from Z-prefix addresses to Q-prefix addresses. This document catalogs every change needed across the entire codebase and provides a phased migration strategy. - -**Scope:** 30+ files across 3 components (syncer, backend API, frontend) plus infrastructure. - ---- - -## Phase 1: Create Abstraction Layer (Do First) - -Before changing any prefix, centralize all prefix logic so the actual switch is a one-line change. - -### 1.1 Syncer — New helpers in `Zond2mongoDB/validation/hex.go` - -Create these functions to replace all scattered prefix logic: - -```go -// Configurable prefix - change this ONE constant when the network upgrades -const AddressPrefix = "Z" // Change to "Q" at network upgrade time - -func GetAddressPrefix() string { return AddressPrefix } - -func GetZeroAddress() string { - return AddressPrefix + "0000000000000000000000000000000000000000" -} - -func IsZeroAddress(addr string) bool { - stripped := strings.ToLower(StripAddressPrefix(addr)) - return stripped == "0000000000000000000000000000000000000000" || - stripped == "0" || stripped == "" -} - -func StripAddressPrefix(address string) string { - lower := strings.ToLower(address) - for _, prefix := range []string{"0x", "z", "q"} { - if strings.HasPrefix(lower, prefix) { - return address[len(prefix):] - } - } - return address -} - -func NormalizeAddress(address string) string { - hex := strings.ToLower(StripAddressPrefix(address)) - if hex == "" { return "" } - return strings.ToLower(AddressPrefix) + hex -} - -func NormalizeAddressUpper(address string) string { - hex := strings.ToLower(StripAddressPrefix(address)) - if hex == "" { return "" } - return AddressPrefix + hex -} - -func IsValidAddress(address string) bool { - lower := strings.ToLower(address) - if strings.HasPrefix(lower, "z") || strings.HasPrefix(lower, "q") { - hex := lower[1:] - if len(hex) != 40 { return false } - for _, c := range hex { - if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { return false } - } - return true - } - if strings.HasPrefix(lower, "0x") { - hex := lower[2:] - if len(hex) != 40 { return false } - for _, c := range hex { - if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { return false } - } - return true - } - return false -} -``` - -**Rename:** `ConvertToZAddress()` → call `NormalizeAddressUpper()` instead (update all 3 call sites in `rpc/calls.go`). - -**Delete:** `QRLZeroAddress` constant from `configs/const.go` → replace all 6 usages with `validation.GetZeroAddress()`. - -### 1.2 Backend API — New helpers in `backendAPI/db/helpers.go` - -```go -package db - -import "strings" - -const AddressPrefix = "Z" // Change to "Q" at network upgrade - -func stripPrefix(addr string) string { - lower := strings.ToLower(addr) - for _, p := range []string{"0x", "z", "q"} { - if strings.HasPrefix(lower, p) { return addr[len(p):] } - } - return addr -} - -func normalizeAddr(addr string) string { - return strings.ToLower(string(AddressPrefix[0])+strings.ToLower(stripPrefix(addr))) -} - -func normalizeAddrUpper(addr string) string { - return AddressPrefix + strings.ToLower(stripPrefix(addr)) -} - -func addrVariants(addr string) []string { - hex := strings.ToLower(stripPrefix(addr)) - return []string{ - "z" + hex, "Z" + hex, - "q" + hex, "Q" + hex, - } -} -``` - -Then refactor: -- `db/token.go` lines 111-125: Replace `normalizeAddress()` and `normalizeAddressBoth()` with these helpers -- `db/address.go` line 32: Use `normalizeAddr()` -- `db/contract.go` lines 98-100: Use `addrVariants()` -- `routes/routes.go` lines 288-289, 616-620: Use `normalizeAddrUpper()` - -### 1.3 Frontend — Update `ExplorerFrontend/app/lib/helpers.ts` - -```typescript -// Change this ONE constant when the network upgrades -export const ADDRESS_PREFIX = 'Z'; // Change to 'Q' at network upgrade - -export function stripAddressPrefix(addr: string): string { - if (!addr) return ''; - const lower = addr.toLowerCase(); - if (lower.startsWith('0x')) return addr.slice(2); - if (lower.startsWith('z') || lower.startsWith('q')) return addr.slice(1); - return addr; -} - -export function normalizeAddress(addr: string): string { - return ADDRESS_PREFIX + stripAddressPrefix(addr).toLowerCase(); -} - -export function isZondAddress(addr: string): boolean { - const lower = addr.toLowerCase(); - return lower.startsWith('z') || lower.startsWith('q'); -} -``` - -Then update: -- `formatAddress()` (lines 224-249): Use `ADDRESS_PREFIX` instead of hardcoded `'Z'` (4 occurrences) -- `normalizeHexString()` (lines 146-148): Use `isZondAddress()` check -- `decodeTokenTransferInput()` (lines 286, 314, 318): Use `ADDRESS_PREFIX` instead of `'Z'` - ---- - -## Phase 2: Update All Hardcoded Z References - -### 2.1 Syncer (`Zond2mongoDB/`) - -| File | Lines | Current Code | Change To | -|------|-------|-------------|-----------| -| `configs/const.go` | 13 | `QRLZeroAddress = "Z000..."` | Delete, use `validation.GetZeroAddress()` | -| `validation/hex.go` | 30-46 | `HasPrefix(addr, "Z")` | Use new `IsValidAddress()` | -| `validation/hex.go` | 96-105 | `StripAddressPrefix` with hardcoded Z | Use new version supporting Z/Q | -| `validation/hex.go` | 107-121 | `ConvertToZAddress()` | Replace with `NormalizeAddressUpper()` | -| `rpc/calls.go` | 346, 354, 416 | `ConvertToZAddress()` calls | `NormalizeAddressUpper()` | -| `rpc/tokenscalls.go` | 49-52 | `HasPrefix(addr, "Z")` + `"Z" + addr` | `NormalizeAddressUpper()` | -| `rpc/tokenscalls.go` | 329-334 | 6 hardcoded zero addr checks | `IsZeroAddress()` | -| `rpc/tokenscalls.go` | 341-346, 354-357 | Z prefix enforcement | `NormalizeAddressUpper()` | -| `rpc/tokenscalls.go` | 441-442 | `"Z" + TrimLeftZeros(...)` | `AddressPrefix + TrimLeftZeros(...)` | -| `rpc/tokenscalls.go` | 560-566 | Z prefix in ParseTransferEvent | Use helper | -| `rpc/tokenscalls.go` | 631 | `"Z" + addressHex` | `AddressPrefix + addressHex` | -| `db/tokentransfers.go` | 38, 42, 229, 233 | `configs.QRLZeroAddress` | `validation.GetZeroAddress()` | -| `db/tokentransfers.go` | 199-201 | `"Z" + TrimLeftZeros(...)` | `AddressPrefix + TrimLeftZeros(...)` | -| `db/tokentransfers.go` | 228, 232 | `from == "Z"` comparison | `from == AddressPrefix` | -| `db/tokenbalances.go` | 32-35 | Hardcoded zero addr checks | `IsZeroAddress()` | -| `db/tokenbalances.go` | 33 | `configs.QRLZeroAddress` | `validation.GetZeroAddress()` | -| `scripts/reindex_tokens.py` | 95-96 | `'Z' + topics[1][-40:]` | Make prefix configurable | -| `scripts/reindex_tokens.py` | 133, 158 | Z zero address comparisons | Use Q zero address | -| `scripts/reindex_contracts.py` | 147, 236-241 | `"Z" + hex` | Make prefix configurable | - -**No changes needed** (already prefix-agnostic via `.ToLower()`): -- `db/transactions.go` — uses `strings.ToLower()` normalization -- `db/coinbase.go` — uses `strings.ToLower()` normalization -- `db/contracts.go` — uses `strings.ToLower()` normalization - -### 2.2 Backend API (`backendAPI/`) - -| File | Lines | Current Code | Change To | -|------|-------|-------------|-----------| -| `db/address.go` | 32 | `strings.ToLower(query)` | Use `normalizeAddr()` | -| `db/address.go` | 104 | `TrimPrefix(addressHex, "z")` | `stripPrefix()` | -| `db/address.go` | 149-150 | Lowercase z→Z for RPC | `normalizeAddrUpper()` | -| `db/transaction.go` | 71-73, 172-174, 355, 564 | `HasPrefix(addr, "Z")` then prepend Z | `normalizeAddrUpper()` | -| `db/transaction.go` | 76-82 | TrimPrefix Z + case variants | Use `stripPrefix()` | -| `db/transaction.go` | 313 | `TrimPrefix(addr, "Z"), "z")` | `stripPrefix()` | -| `db/contract.go` | 34-40 | Search with z prefix | Use `addrVariants()` | -| `db/contract.go` | 98-100 | Both Z and z variants | Use `addrVariants()` | -| `db/token.go` | 111-125 | `normalizeAddress/Both` | Replace with new helpers | -| `routes/routes.go` | 288-289 | Lowercase z→Z normalization | `normalizeAddrUpper()` | -| `routes/routes.go` | 616-620 | Contract addr normalization | `normalizeAddrUpper()` | - -### 2.3 Frontend (`ExplorerFrontend/`) - -| File | Lines | Current Code | Change To | -|------|-------|-------------|-----------| -| `app/lib/helpers.ts` | 146-148 | `startsWith('Z') \|\| startsWith('z')` | `isZondAddress()` | -| `app/lib/helpers.ts` | 229, 239, 244 | `'Z' + ...` in formatAddress | `ADDRESS_PREFIX + ...` | -| `app/lib/helpers.ts` | 286, 314, 318 | `'Z' + hex` in decodeTokenTransfer | `ADDRESS_PREFIX + hex` | -| `app/components/SearchBar.tsx` | 14-16 | `startsWith('Z')` + regex `^Z[hex]{40}$` | Support both Z and Q | -| `app/components/SearchBar.tsx` | 77 | Placeholder `"Zxx"` | Update text | -| `app/address/[query]/page.tsx` | 89 | `startsWith('z')` → `'Z' + ...` | Handle both z/q → ADDRESS_PREFIX | -| `app/address/[query]/address-view.tsx` | 62, 69 | `startsWith("Z")` → "Zond Address" | Support Q prefix too | -| `app/validators/validators-client.tsx` | 104 | `startsWith('Z') ? addr : 'Z' + addr` | Use `normalizeAddress()` | -| `app/validators/components/ValidatorTable.tsx` | 231, 234 | Hardcoded `Z{addr.slice(...)}` | Remove hardcoded prefix | - ---- - -## Phase 3: MongoDB Data Migration - -### 3.1 Collections to migrate - -All collections storing addresses with z/Z prefix need updating: - -``` -addresses.id -transactionByAddress.from, .to -internalTransactionByAddress.from, .to -contractCode.address, .creatorAddress -tokenTransfers.from, .to, .contractAddress -tokenBalances.holderAddress, .contractAddress -pending_transactions.from, .to -``` - -### 3.2 Migration script (run at cutover) - -```javascript -// migrate-z-to-q.js -// Run: mongosh qrldata-z migrate-z-to-q.js - -const collections = { - 'addresses': ['id'], - 'contractCode': ['address', 'creatorAddress'], - 'tokenTransfers': ['from', 'to', 'contractAddress'], - 'tokenBalances': ['holderAddress', 'contractAddress'], - 'transactionByAddress': ['from', 'to'], - 'pending_transactions': ['from', 'to'], -}; - -for (const [collName, fields] of Object.entries(collections)) { - const coll = db.getCollection(collName); - for (const field of fields) { - // Lowercase z → lowercase q - const filter = {}; - filter[field] = /^z/; - const count = coll.countDocuments(filter); - print(`${collName}.${field}: ${count} documents with z-prefix`); - - if (count > 0) { - coll.find(filter).forEach(doc => { - const update = {}; - update[field] = 'q' + doc[field].slice(1); - coll.updateOne({_id: doc._id}, {$set: update}); - }); - print(` -> migrated ${count} documents`); - } - - // Uppercase Z → lowercase q - const filterUpper = {}; - filterUpper[field] = /^Z/; - const countUpper = coll.countDocuments(filterUpper); - print(`${collName}.${field}: ${countUpper} documents with Z-prefix`); - - if (countUpper > 0) { - coll.find(filterUpper).forEach(doc => { - const update = {}; - update[field] = 'q' + doc[field].slice(1).toLowerCase(); - coll.updateOne({_id: doc._id}, {$set: update}); - }); - print(` -> migrated ${countUpper} documents`); - } - } -} - -print('Migration complete!'); -``` - -### 3.3 Index rebuild - -After migration, rebuild indexes on address fields: - -```javascript -db.addresses.reIndex(); -db.contractCode.reIndex(); -db.tokenTransfers.reIndex(); -db.tokenBalances.reIndex(); -db.transactionByAddress.reIndex(); -``` - ---- - -## Phase 4: The Actual Switch - -Once all abstraction is in place, the switch is just changing constants: - -1. **Syncer:** `validation/hex.go` → `const AddressPrefix = "Q"` -2. **Backend:** `db/helpers.go` → `const AddressPrefix = "Q"` -3. **Frontend:** `app/lib/helpers.ts` → `export const ADDRESS_PREFIX = 'Q'` -4. **Run MongoDB migration script** -5. **Rebuild & restart all services** - ---- - -## Phase 5: Transition Period (Support Both) - -During the transition, the system should accept both Z and Q prefixes from users: - -- **SearchBar:** Accept both `Z...` and `Q...` as valid addresses -- **Backend routes:** Normalize both to the current canonical prefix -- **DB queries:** Use `addrVariants()` to search all prefix variants -- **Display:** Always show the canonical prefix (Q after migration) - -This is already partially implemented (the codebase handles both Z and z). Extending to Q/q follows the same pattern. - ---- - -## Deployment Order - -1. Deploy **backend** with dual Z/Q support (Phase 1-2) -2. Deploy **frontend** with dual Z/Q support (Phase 1-2) -3. **Coordinate with node upgrade** — confirm node returns Q-prefix addresses -4. Deploy **syncer** with Q-prefix output -5. Run **MongoDB migration** for historical data -6. Flip the `AddressPrefix` constant to `"Q"` -7. Rebuild and restart all services -8. After stabilization, remove Z-prefix backward compat code (Phase 5 cleanup) - ---- - -## Risk Mitigation - -- **Backup MongoDB** before running migration: `mongodump --db qrldata-z` -- **Test in staging** with a copy of production data -- **Feature flag approach**: The `AddressPrefix` constant acts as a feature flag -- **Rollback**: Change constant back to `"Z"`, re-run migration in reverse -- **Monitor**: Watch PM2 logs for address-related errors after deployment - ---- - -## File Count Summary - -| Component | Files to Change | Critical | Already Compatible | -|-----------|----------------|----------|--------------------| -| Syncer (Zond2mongoDB) | 10 | 5 | 3 (transactions, coinbase, contracts) | -| Backend (backendAPI) | 6 | 4 | 0 | -| Frontend (ExplorerFrontend) | 6 | 3 | rest (depend on helpers.ts) | -| Infrastructure | 2 (Python scripts) | 2 | deploy scripts (no change needed) | -| **Total** | **24 files** | **14 critical** | **3 already compatible** | From 8b95b8b5d7df1f414c7365ad97af463c3bf120b2 Mon Sep 17 00:00:00 2001 From: moscowchill Date: Tue, 24 Feb 2026 10:38:42 +0100 Subject: [PATCH 04/12] docs: add backend API and DB syncer documentation, rename zondexplorer to zondscan --- ExplorerFrontend/app/faq/faq-client.tsx | 2 +- README.md | 12 +- deploy-windowsgitbash.sh | 2 +- deploy.sh | 2 +- docs/backend-api.md | 1232 +++++++++++++++++++++++ docs/db-syncer.md | 1089 ++++++++++++++++++++ 6 files changed, 2330 insertions(+), 9 deletions(-) create mode 100644 docs/backend-api.md create mode 100644 docs/db-syncer.md diff --git a/ExplorerFrontend/app/faq/faq-client.tsx b/ExplorerFrontend/app/faq/faq-client.tsx index e96dc9d..dc96210 100644 --- a/ExplorerFrontend/app/faq/faq-client.tsx +++ b/ExplorerFrontend/app/faq/faq-client.tsx @@ -32,7 +32,7 @@ const faqs = [ }, { question: "Who created Zondscan?", - answer: "Zondscan was created by DigitalGuards, a company based in the Netherlands. The explorer is completely open-source and its code is available on GitHub at github.com/DigitalGuards/zondexplorer. You can learn more about DigitalGuards at digitalguards.nl." + answer: "Zondscan was created by DigitalGuards, a company based in the Netherlands. The explorer is completely open-source and its code is available on GitHub at github.com/DigitalGuards/zondscan. You can learn more about DigitalGuards at digitalguards.nl." }, { question: "How can I contact support?", diff --git a/README.md b/README.md index e1b105c..5f3aeb2 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Plus a Zond node (can be either external or local). Note: These instructions are only for the explorer related components. Are you trying to get your Zond up and running? Visit https://test-zond.theqrl.org/linux.html ``` -git clone https://github.com/DigitalGuards/zondexplorer.git +git clone https://github.com/DigitalGuards/zondscan.git ``` #### Requirements @@ -40,13 +40,13 @@ The easiest way to set up the QRL Explorer is by using the provided deployment s #### Windows (using Git Bash): ```bash -cd zondexplorer +cd zondscan ./deploy-windowsgitbash.sh ``` #### Linux/macOS: ```bash -cd zondexplorer +cd zondscan ./deploy.sh ``` @@ -68,7 +68,7 @@ If you prefer to set up components individually, follow the instructions below: Navigate to the frontend directory: ``` -cd zondexplorer/ExplorerFrontend +cd zondscan/ExplorerFrontend ``` Create the environment files: @@ -171,8 +171,8 @@ The fastest way to run all services locally: ```bash # Clone and navigate to the project -git clone https://github.com/DigitalGuards/zondexplorer.git -cd zondexplorer +git clone https://github.com/DigitalGuards/zondscan.git +cd zondscan # Start all services docker compose up -d diff --git a/deploy-windowsgitbash.sh b/deploy-windowsgitbash.sh index 94a8179..6c07c54 100755 --- a/deploy-windowsgitbash.sh +++ b/deploy-windowsgitbash.sh @@ -148,7 +148,7 @@ clone_repo() { fi else print_status "Cloning QRL Explorer repository..." - git clone https://github.com/DigitalGuards/zondexplorer.git ../zondexplorer || print_error "Failed to clone repository" + git clone https://github.com/DigitalGuards/zondscan.git ../zondscan || print_error "Failed to clone repository" cd ../backendAPI || print_error "Failed to enter project directory" fi export BASE_DIR=$(pwd) diff --git a/deploy.sh b/deploy.sh index 5f5143b..6a405c5 100755 --- a/deploy.sh +++ b/deploy.sh @@ -141,7 +141,7 @@ clone_repo() { fi else print_status "Cloning QRL Explorer repository..." - git clone https://github.com/DigitalGuards/zondexplorer.git || print_error "Failed to clone repository" + git clone https://github.com/DigitalGuards/zondscan.git || print_error "Failed to clone repository" cd ../backendAPI || print_error "Failed to enter project directory" fi diff --git a/docs/backend-api.md b/docs/backend-api.md new file mode 100644 index 0000000..4b71fe9 --- /dev/null +++ b/docs/backend-api.md @@ -0,0 +1,1232 @@ +# ZondScan Backend API Documentation + +Comprehensive documentation for the ZondScan backend API server -- a Go + Gin REST API that serves blockchain data for the QRL Zond network explorer at zondscan.com. + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [REST API Endpoints](#rest-api-endpoints) +3. [Database Collections & Query Patterns](#database-collections--query-patterns) +4. [Data Models](#data-models) +5. [Configuration](#configuration) +6. [Build & Run](#build--run) +7. [Key Implementation Details](#key-implementation-details) + +--- + +## Architecture Overview + +``` + ┌──────────────────────────┐ + │ QRL Zond Node (RPC) │ + │ http://localhost:8545 │ + └────────────┬─────────────┘ + │ + (zond_getBalance calls) + │ +┌──────────────┐ REST API ┌───────────────▼──────────────┐ Reads ┌───────────────┐ +│ Frontend │◀───────────────▶│ backendAPI (Go + Gin) │◀──────────────▶│ MongoDB │ +│ Next.js │ :8080/:8081 │ │ │ qrldata-z │ +└──────────────┘ │ handler/ - Middleware/CORS │ └───────────────┘ + │ routes/ - Endpoint routing │ + │ db/ - Query functions │ + │ models/ - Data structures │ + │ configs/ - DB + env setup │ + └──────────────────────────────┘ +``` + +### Component Responsibilities + +| Directory | Purpose | +|-----------|---------| +| `main.go` | Entry point. Sets up logging (stdout + `backendAPI.log`), panic recovery, and calls `handler.RequestHandler()`. | +| `configs/` | MongoDB connection (`setup.go`), collection references and constants (`const.go`), environment variable loading (`env.go`). | +| `handler/` | Gin router initialization, CORS config, middleware (panic recovery, request latency logging), TLS/HTTP mode selection. | +| `routes/` | All REST endpoint definitions and request/response handling. | +| `db/` | Database query functions organized by entity: addresses, blocks, transactions, contracts, tokens, validators, stats, pending transactions. | +| `models/` | Go struct definitions for all data entities. | + +### Request Flow + +1. HTTP request arrives at Gin router +2. Passes through middleware: Logger -> Recovery -> Monitor -> CORS +3. Matched route handler in `routes/routes.go` +4. Handler calls one or more `db/` functions +5. `db/` functions query MongoDB collections (configured in `configs/const.go`) +6. Results decoded into `models/` structs +7. JSON response returned to client + +--- + +## REST API Endpoints + +### Health & Overview + +#### `GET /health` +Kubernetes health check probe. + +**Response:** +```json +{ "status": "ok" } +``` + +#### `GET /overview` +Dashboard overview with market data and network stats. + +**Response:** +```json +{ + "marketcap": 1000000000000000000, + "currentPrice": 1000.0, + "countwallets": 1500, + "circulating": "65000000", + "volume": 42, + "tradingVolume": 50000.0, + "validatorCount": 128, + "contractCount": 35, + "status": { + "syncing": true, + "dataInitialized": true + } +} +``` + +**Notes:** +- `circulating` defaults to `"65000000"` when unavailable. +- `status.dataInitialized` is `true` if any non-zero data exists. +- All numeric fields default to `0` when data is unavailable. + +--- + +### Price Data + +#### `GET /price-history?interval=24h` +Historical price data for charts and wallet apps. + +**Query Parameters:** +| Param | Default | Valid Values | +|-------|---------|-------------| +| `interval` | `24h` | `4h`, `12h`, `24h`, `7d`, `30d`, `all` | + +**Response:** +```json +{ + "data": [ + { + "timestamp": "2026-02-24T10:00:00Z", + "priceUSD": 1.23, + "marketCapUSD": 80000000, + "volumeUSD": 50000 + } + ], + "interval": "24h", + "count": 48 +} +``` + +**Interval to data point mapping:** +| Interval | Max Points | Approximate Granularity | +|----------|-----------|------------------------| +| `4h` | 8 | ~30 min | +| `12h` | 24 | ~30 min | +| `24h` | 48 | ~30 min | +| `7d` | 336 | ~30 min | +| `30d` | 1440 | ~30 min | +| `all` | unlimited | all stored data | + +--- + +### Blocks + +#### `GET /blocks?page=1&limit=5` +Paginated list of latest blocks. + +**Query Parameters:** +| Param | Default | Description | +|-------|---------|-------------| +| `page` | `1` | Page number (1-indexed) | +| `limit` | `5` | Blocks per page | + +**Response:** +```json +{ + "blocks": [ + { + "baseFeePerGas": "0x3b9aca00", + "gasLimit": "0x1c9c380", + "gasUsed": "0x5208", + "hash": "0xabc...", + "number": "0x1a4", + "timestamp": "0x65abc123", + "transactions": [...] + } + ], + "total": 1500 +} +``` + +**Notes:** +- Total is capped at `300 * limit` pages maximum. +- Sorted by timestamp descending (newest first). +- Projection includes: `number`, `timestamp`, `hash`, `transactions`. + +#### `GET /block/:query` +Single block by number. Accepts both decimal (`420`) and hex (`0x1a4`) formats. + +**Response:** +```json +{ + "block": { + "jsonrpc": "2.0", + "id": 1, + "result": { + "baseFeePerGas": "0x3b9aca00", + "gasLimit": "0x1c9c380", + "hash": "0xabc...", + "number": "0x1a4", + "timestamp": "0x65abc123", + "transactions": [...], + "withdrawals": [...] + } + } +} +``` + +**Notes:** Falls back to zero-padded hex (`0x01a4`) if initial lookup fails. + +#### `GET /latestblock` +Returns the latest synced block number (from `sync_state` collection). + +**Response:** +```json +{ "blockNumber": 420 } +``` + +#### `GET /blocksizes` +Returns historical average block size data for charts. + +**Response:** +```json +{ + "response": [ + { "timestamp": "...", "size": 1234 } + ] +} +``` + +#### `GET /debug/blocks` +Debug endpoint returning total block count and latest block number. + +**Response:** +```json +{ + "total_blocks": 1500, + "latest_block": 420 +} +``` + +--- + +### Transactions + +#### `GET /txs?page=1` +Paginated network-wide transactions list. + +**Query Parameters:** +| Param | Required | Description | +|-------|----------|-------------| +| `page` | Yes | Page number (1-indexed) | + +**Response:** +```json +{ + "txs": [ + { + "InOut": 0, + "TxType": "transfer", + "From": "Z2019ea...", + "To": "Z5a330c...", + "TxHash": "0xabc...", + "TimeStamp": "1706123456", + "Amount": "1.500000000000000000", + "PaidFees": "0.000021000000000000", + "BlockNumber": "420" + } + ], + "total": 5000, + "latestBlock": 420 +} +``` + +**Notes:** +- Fixed page size of 5 transactions per page. +- Amount and PaidFees are serialized with 18 decimal places. +- BlockNumber is converted from hex to decimal in JSON output. + +#### `GET /tx/:query` +Single transaction by hash. + +**Response:** +```json +{ + "response": { + "blockNumber": "0x1a4", + "blockTimestamp": "0x65abc123", + "from": "Z2019ea...", + "to": "Z5a330c...", + "txHash": "0xabc...", + "value": "0x1bc16d674ec80000", + "gasUsed": "0x5208", + "gasPrice": "0x3b9aca00", + "nonce": "0x0", + "signature": "...", + "pk": "..." + }, + "latestBlock": 420, + "contractCreated": { + "address": "Z5a330c...", + "isToken": true, + "name": "MyToken", + "symbol": "MTK", + "decimals": 18 + }, + "tokenTransfer": { + "contractAddress": "Z5a330c...", + "from": "Z2019ea...", + "to": "Zaaabbb...", + "amount": "1000000000000000000", + "tokenName": "MyToken", + "tokenSymbol": "MTK", + "tokenDecimals": 18 + } +} +``` + +**Notes:** +- `contractCreated` is included only if the transaction deployed a contract. +- `tokenTransfer` is included only if the transaction is an ERC20 transfer. +- First looks up the transaction in the `blocks` collection (by matching `result.transactions[].hash`), then falls back to the `transfer` collection. + +#### `GET /transactions` +Returns all latest transactions (no pagination -- full scan). + +**Response:** +```json +{ + "response": [ + { + "InOut": 0, + "TxType": "transfer", + "TxHash": "0xabc...", + "TimeStamp": "1706123456", + "Amount": "1.500000000000000000", + "PaidFees": "0.000021000000000000" + } + ] +} +``` + +#### `GET /coinbase/:query` +Coinbase transaction lookup (uses the same `ReturnSingleTransfer` function as `/tx`). + +**Response:** +```json +{ + "response": { ... } +} +``` + +--- + +### Addresses + +#### `GET /address/aggregate/:query` +Aggregated address data: balance, rank, transactions, internal transactions, and contract code. + +**Path Parameter:** Address with `Z` prefix (e.g., `Z2019ea08f4e24201b98f9154906da4b924a04892`) + +**Response:** +```json +{ + "address": { + "id": "z2019ea08f4e24201b98f9154906da4b924a04892", + "balance": 100.5, + "nonce": 42 + }, + "transactions_count": 150, + "rank": 5, + "transactions_by_address": [ + { + "InOut": 0, + "TxType": "transfer", + "From": "Z2019ea...", + "To": "Z5a330c...", + "TxHash": "0xabc...", + "TimeStamp": "1706123456", + "Amount": "1.500000000000000000", + "PaidFees": "0.000021000000000000", + "BlockNumber": "420" + } + ], + "internal_transactions_by_address": [...], + "contract_code": { + "address": "Z2019ea...", + "creatorAddress": "Z5a330c...", + "contractCode": "0x606060...", + "isToken": false + }, + "latestBlock": 420 +} +``` + +**Notes:** +- Normalizes `z` prefix to `Z` on input. +- If the address is not found in the `addresses` collection, it queries the Zond node RPC (`zond_getBalance`) and creates a new entry. +- Transaction queries use case-insensitive regex matching. +- Internal transactions query the `internalTransactionByAddress` collection using hex-decoded byte-level matching. + +#### `GET /address/:address/transactions?page=1&limit=5` +Paginated non-zero-amount transactions for an address. + +**Query Parameters:** +| Param | Default | Description | +|-------|---------|-------------| +| `page` | `1` | Page number | +| `limit` | `5` | Results per page | + +**Response:** +```json +{ + "transactions": [...], + "total": 50, + "page": 1, + "limit": 5 +} +``` + +**Notes:** Filters to only transactions where `amount > 0`. + +#### `GET /address/:address/tokens` +All ERC20 token balances held by an address. Designed for wallet integration (e.g., qrlwallet auto-discovery). + +**Response:** +```json +{ + "address": "Z2019ea...", + "tokens": [ + { + "contractAddress": "Z5a330c...", + "holderAddress": "Z2019ea...", + "balance": "1000000000000000000", + "blockNumber": "0x1a4", + "name": "MyToken", + "symbol": "MTK", + "decimals": 18 + } + ], + "count": 1 +} +``` + +**Notes:** +- Uses MongoDB aggregation pipeline with `$lookup` to join `tokenBalances` with `contractCode` for metadata. +- Sorted by balance descending (highest value tokens first). +- Searches both `Z` and `z` prefix variants of the address. + +#### `POST /getBalance` +Get balance for an address directly from the Zond node RPC. + +**Request Body (form-encoded):** +``` +address=Z2019ea08f4e24201b98f9154906da4b924a04892 +``` + +**Response:** +```json +{ "balance": 100.5 } +``` + +**Notes:** Makes a live `zond_getBalance` RPC call to the Zond node. Balance is returned in QRL (divided by 1e18 from wei). + +#### `GET /walletdistribution/:query` +Count wallets with balance greater than the specified threshold (in units of 1e12 wei). + +**Example:** `/walletdistribution/1000` counts wallets with balance > 1000 * 1e12 wei. + +**Response:** +```json +{ "response": 42 } +``` + +#### `GET /richlist` +Top 50 addresses by balance. + +**Response:** +```json +{ + "richlist": [ + { "id": "z2019ea...", "balance": 1000000.5, "nonce": 100 } + ] +} +``` + +--- + +### Contracts + +#### `GET /contracts?page=0&limit=10&search=&isToken=true` +Paginated list of deployed smart contracts. + +**Query Parameters:** +| Param | Default | Description | +|-------|---------|-------------| +| `page` | `0` | Page number (0-indexed) | +| `limit` | `10` | Results per page | +| `search` | (none) | Search by contract address, creator address, or token name | +| `isToken` | (none) | Filter: `true` for tokens only, `false` for non-tokens only | + +**Response:** +```json +{ + "response": [ + { + "address": "Z5a330c...", + "creatorAddress": "Z2019ea...", + "contractCode": "0x606060...", + "creationTransaction": "0xabc...", + "creationBlockNumber": "0x1a4", + "isToken": true, + "name": "MyToken", + "symbol": "MTK", + "decimals": 18, + "totalSupply": "1000000000000000000000", + "status": "verified" + } + ], + "total": 35 +} +``` + +**Notes:** +- Search is case-insensitive. +- Addresses in the response are normalized to uppercase `Z` prefix. +- Sorted by `_id` descending (latest first). + +--- + +### Tokens + +#### `GET /token/:address/info` +Summary information about a specific ERC20 token. + +**Response:** +```json +{ + "contractAddress": "Z5a330c...", + "name": "MyToken", + "symbol": "MTK", + "decimals": 18, + "totalSupply": "1000000000000000000000", + "holderCount": 42, + "transferCount": 150, + "creatorAddress": "Z2019ea...", + "creationTxHash": "0xabc...", + "creationBlock": "0x1a4" +} +``` + +#### `GET /token/:address/holders?page=0&limit=25` +Paginated token holder list sorted by balance descending. + +**Query Parameters:** +| Param | Default | Max | +|-------|---------|-----| +| `page` | `0` | -- | +| `limit` | `25` | `100` | + +**Response:** +```json +{ + "contractAddress": "Z5a330c...", + "holders": [ + { + "contractAddress": "Z5a330c...", + "holderAddress": "Z2019ea...", + "balance": "500000000000000000000" + } + ], + "totalHolders": 42, + "page": 0, + "limit": 25 +} +``` + +**Notes:** Uses aggregation pipeline with `$toDecimal` for proper numeric sorting of string balances. + +#### `GET /token/:address/transfers?page=0&limit=25` +Paginated token transfer history. + +**Query Parameters:** +| Param | Default | Max | +|-------|---------|-----| +| `page` | `0` | -- | +| `limit` | `25` | `100` | + +**Response:** +```json +{ + "contractAddress": "Z5a330c...", + "transfers": [ + { + "contractAddress": "Z5a330c...", + "from": "Z2019ea...", + "to": "Zaaabbb...", + "amount": "1000000000000000000", + "blockNumber": "0x1a4", + "txHash": "0xabc...", + "timestamp": "1706123456", + "tokenSymbol": "MTK", + "tokenDecimals": 18, + "tokenName": "MyToken", + "transferType": "transfer" + } + ], + "totalTransfers": 150, + "page": 0, + "limit": 25 +} +``` + +--- + +### Validators + +#### `GET /validators?page_token=` +List all validators with status and staking info. + +**Query Parameters:** +| Param | Default | Description | +|-------|---------|-------------| +| `page_token` | (none) | Pagination token (currently unused in implementation) | + +**Response:** +```json +{ + "validators": [ + { + "index": "0", + "address": "0xabc123...", + "status": "active", + "age": 100, + "stakedAmount": "10000000000000", + "isActive": true + } + ], + "totalStaked": "1280000000000000" +} +``` + +**Notes:** +- Status is computed from activation/exit epochs relative to current epoch: `active`, `pending`, `exited`, `slashed`. +- Current epoch = `latestBlockNumber / 128`. +- All validators are stored in a single MongoDB document with `_id: "validators"`. + +#### `GET /validator/:id` +Individual validator details by index or public key hex. + +**Response:** +```json +{ + "index": "0", + "publicKeyHex": "0xabc123...", + "withdrawalCredentialsHex": "0xdef456...", + "effectiveBalance": "10000000000000", + "slashed": false, + "activationEligibilityEpoch": "0", + "activationEpoch": "0", + "exitEpoch": "18446744073709551615", + "withdrawableEpoch": "18446744073709551615", + "status": "active", + "age": 100, + "currentEpoch": "100" +} +``` + +#### `GET /validators/stats` +Aggregated validator statistics. + +**Response:** +```json +{ + "totalValidators": 128, + "activeCount": 120, + "pendingCount": 5, + "exitedCount": 2, + "slashedCount": 1, + "totalStaked": "1280000000000000", + "currentEpoch": "100" +} +``` + +#### `GET /validators/history?limit=100` +Historical validator count data for charts. + +**Query Parameters:** +| Param | Default | Description | +|-------|---------|-------------| +| `limit` | `100` | Max records to return | + +**Response:** +```json +{ + "history": [ + { + "epoch": "99", + "timestamp": 1706123456, + "validatorsCount": 128, + "activeCount": 120, + "pendingCount": 5, + "exitedCount": 2, + "slashedCount": 1, + "totalStaked": "1280000000000000" + } + ] +} +``` + +#### `GET /epoch` +Current epoch information. + +**Response:** +```json +{ + "headEpoch": "100", + "headSlot": "12800", + "finalizedEpoch": "98", + "justifiedEpoch": "99", + "slotsPerEpoch": 128, + "secondsPerSlot": 60, + "slotInEpoch": 50, + "timeToNextEpoch": 4680, + "updatedAt": 1706123456 +} +``` + +**Notes:** +- `slotsPerEpoch` = 128, `secondsPerSlot` = 60 (constants). +- `timeToNextEpoch` is computed as `(128 - slotInEpoch) * 60` seconds. + +--- + +### Pending Transactions + +#### `GET /pending-transactions?page=1&limit=10` +Paginated list of pending (mempool) transactions. + +**Query Parameters:** +| Param | Default | Description | +|-------|---------|-------------| +| `page` | `1` | Page number (1-indexed) | +| `limit` | `10` | Results per page | + +**Response:** +```json +{ + "transactions": [ + { + "hash": "0xabc...", + "from": "Z2019ea...", + "to": "Z5a330c...", + "value": "0x1bc16d674ec80000", + "gas": "0x5208", + "gasPrice": "0x3b9aca00", + "nonce": "0x0", + "status": "pending", + "lastSeen": 1706123456, + "createdAt": 1706123400 + } + ], + "total": 3, + "page": 1, + "limit": 10, + "totalPages": 1 +} +``` + +**Notes:** Excludes transactions with status `"mined"`. Timestamps are serialized as Unix timestamps. + +#### `GET /pending-transaction/:hash` +Single pending transaction by hash. Implements lifecycle management: +1. If found in `pending_transactions` with status `"mined"`, deletes it and returns 404 with `"status": "mined"`. +2. If not found in pending, checks the `transfer` collection for a mined transaction. +3. If found as mined, returns `"status": "mined"` with block number. +4. If not found anywhere, returns 404 with an explanation that the tx may have been dropped. + +--- + +## Database Collections & Query Patterns + +All collections are in the `qrldata-z` database. Collection references are initialized as package-level variables in `configs/const.go`. + +### Core Collections + +| Collection | Purpose | Key Query Patterns | +|------------|---------|-------------------| +| `blocks` | Full block data (header + transactions) | Find by `result.number` (hex), `result.hash`; sorted by `result.timestamp` desc | +| `transfer` | Individual transaction records | Find by `txHash` (byte array) | +| `transactionByAddress` | Indexed transactions by address | Find by `from`/`to` (case-insensitive regex); sorted by `timeStamp` desc | +| `internalTransactionByAddress` | Internal transactions (contract calls) | Find by `from`/`to` (hex-decoded bytes); sorted by `blockTimestamp` desc | +| `addresses` | Wallet balances and metadata | Find by `id` (lowercase hex string); sorted by `balance` desc for richlist | +| `pending_transactions` | Mempool transactions | Find by `_id` (hash); filter `status != "mined"`; sorted by `createdAt` desc | +| `sync_state` | Sync progress tracking | Find by `_id: "last_synced_block"` to get `block_number` | + +### Contract & Token Collections + +| Collection | Purpose | Key Query Patterns | +|------------|---------|-------------------| +| `contractCode` | Smart contract deployments & token metadata | Find by `address` (both Z/z prefix); search by `name` regex; filter by `isToken` | +| `tokenBalances` | Token holder balances per contract | Find by `holderAddress` or `contractAddress` (both prefix variants); aggregation with `$lookup` to `contractCode` | +| `tokenTransfers` | ERC20 transfer events | Find by `contractAddress` or `txHash`; sorted by `blockNumber` desc | + +### Analytics & Market Collections + +| Collection | Purpose | Key Query Patterns | +|------------|---------|-------------------| +| `coingecko` | CoinGecko market data (price, market cap, volume) | `FindOne` with empty filter | +| `priceHistory` | Historical price snapshots | Filter by `timestamp >= since`; sorted by `timestamp` desc; limit by interval | +| `walletCount` | Total wallet count | Find by `_id: "current_count"` | +| `dailyTransactionsVolume` | Daily transaction volume | `FindOne` with empty filter | +| `totalCirculatingSupply` | Circulating supply | `FindOne` with empty filter | +| `averageBlockSize` | Block size history | Full scan sorted by `timestamp` asc | + +### Validator Collections + +| Collection | Purpose | Key Query Patterns | +|------------|---------|-------------------| +| `validators` | Single document containing all validators per epoch | Find by `_id: "validators"` | +| `validator_history` | Historical validator counts per epoch | Full scan sorted by `epoch` desc; limited | +| `epoch_info` | Current epoch state | Find by `_id: "current"` | + +### Indexes + +Created on startup if missing: + +**`blocks` collection:** +- `result_number_timestamp`: `{ result.number: -1, result.timestamp: 1 }` +- `result_hash`: `{ result.hash: 1 }` + +**`transactionByAddress` collection:** +- `timestamp_desc`: `{ timeStamp: -1 }` +- `tx_hash`: `{ txHash: 1 }` + +### Initialized Collections + +On startup, `initializeCollections()` creates default documents (via `$setOnInsert` + upsert) for: +- `walletCount`: `{ _id: "current_count", count: 0 }` +- `dailyTransactionsVolume`: `{ volume: 0 }` +- `totalCirculatingSupply`: `{ circulating: "0" }` +- `coingecko`: `{ marketCapUSD: 1e18, priceUSD: 1000, lastUpdated: }` + +--- + +## Data Models + +### Address +```go +type Address struct { + ObjectId primitive.ObjectID `bson:"_id"` + ID string `json:"id"` // Lowercase hex with z prefix + Balance float64 `json:"balance"` // QRL units (not wei) + Nonce uint64 `json:"nonce"` +} +``` + +### Block (Result) +```go +type Result struct { + BaseFeePerGas string `json:"baseFeePerGas"` // Hex + GasLimit string `json:"gasLimit"` // Hex + GasUsed string `json:"gasUsed"` // Hex + Hash string `json:"hash"` // 0x-prefixed + Number string `json:"number"` // Hex + ParentHash string `json:"parentHash"` + Timestamp string `json:"timestamp"` // Hex Unix timestamp + Transactions []Transaction `json:"transactions"` + Withdrawals []Withdrawal `json:"withdrawals"` + Size string `json:"size"` // Hex + Miner string `json:"miner"` + // ... additional fields +} +``` + +### Transaction (in block) +```go +type Transaction struct { + BlockHash string `json:"blockHash"` + BlockNumber string `json:"blockNumber"` + From string `json:"from"` + Gas string `json:"gas"` + GasPrice string `json:"gasPrice"` + Hash string `json:"hash"` + Nonce string `json:"nonce"` + To string `json:"to"` + Value string `json:"value"` + Signature string `json:"signature"` + PublicKey string `json:"publicKey"` + Data string `json:"data"` + Status string `json:"status"` +} +``` + +### TransactionByAddress +```go +type TransactionByAddress struct { + InOut int `json:"InOut"` // 0=outgoing, 1=incoming + TxType string `json:"TxType"` + Address string `json:"Address"` // Counterparty address + From string `json:"From"` + To string `json:"To"` + TxHash string `json:"TxHash"` + TimeStamp string `json:"TimeStamp"` // Decimal Unix timestamp + Amount float64 `json:"-"` // Serialized as string with 18 decimals + PaidFees float64 `json:"-"` // Serialized as string with 18 decimals + BlockNumber string `json:"BlockNumber"` // Hex -> decimal in JSON +} +``` + +**Note:** Custom `MarshalJSON` converts `Amount` and `PaidFees` to `"%.18f"` format and `BlockNumber` from hex to decimal string. + +### Transfer +```go +type Transfer struct { + BlockNumber string `bson:"blockNumber"` // Hex + BlockTimestamp string `bson:"blockTimestamp"` // Hex + From string `bson:"from"` + To string `bson:"to"` + TxHash string `bson:"txHash"` + Value string `bson:"value"` // Hex wei + GasUsed string `bson:"gasUsed"` // Hex + GasPrice string `bson:"gasPrice"` // Hex + Nonce string `bson:"nonce"` // Hex + Signature string `bson:"signature"` + Pk string `bson:"pk"` + Size string `bson:"size"` // Hex +} +``` + +### ContractInfo +```go +type ContractInfo struct { + ContractCreatorAddress string `json:"creatorAddress" bson:"creatorAddress"` + ContractAddress string `json:"address" bson:"address"` + ContractCode string `json:"contractCode" bson:"contractCode"` + CreationTransaction string `json:"creationTransaction" bson:"creationTransaction"` + CreationBlockNumber string `json:"creationBlockNumber" bson:"creationBlockNumber"` + IsToken bool `json:"isToken" bson:"isToken"` + Status string `json:"status" bson:"status"` + TokenDecimals uint8 `json:"decimals" bson:"decimals"` + TokenName string `json:"name" bson:"name"` + TokenSymbol string `json:"symbol" bson:"symbol"` + TotalSupply string `json:"totalSupply" bson:"totalSupply"` + UpdatedAt string `json:"updatedAt" bson:"updatedAt"` +} +``` + +### TokenBalance +```go +type TokenBalance struct { + ContractAddress string `json:"contractAddress" bson:"contractAddress"` + HolderAddress string `json:"holderAddress" bson:"holderAddress"` + Balance string `json:"balance" bson:"balance"` // Raw integer string + BlockNumber string `json:"blockNumber" bson:"blockNumber"` + Name string `json:"name,omitempty"` // Via aggregation + Symbol string `json:"symbol,omitempty"` // Via aggregation + Decimals int `json:"decimals,omitempty"` // Via aggregation +} +``` + +### TokenTransfer +```go +type TokenTransfer struct { + ContractAddress string `json:"contractAddress" bson:"contractAddress"` + From string `json:"from" bson:"from"` + To string `json:"to" bson:"to"` + Amount string `json:"amount" bson:"amount"` + BlockNumber string `json:"blockNumber" bson:"blockNumber"` + TxHash string `json:"txHash" bson:"txHash"` + Timestamp string `json:"timestamp" bson:"timestamp"` + TokenSymbol string `json:"tokenSymbol" bson:"tokenSymbol"` + TokenDecimals int `json:"tokenDecimals" bson:"tokenDecimals"` + TokenName string `json:"tokenName" bson:"tokenName"` + TransferType string `json:"transferType" bson:"transferType"` +} +``` + +### Validator Models +```go +// Storage format (single document in MongoDB) +type ValidatorStorage struct { + ID string `bson:"_id"` // Always "validators" + Epoch string `bson:"epoch"` + Validators []ValidatorRecord `bson:"validators"` + UpdatedAt string `bson:"updatedAt"` +} + +type ValidatorRecord struct { + Index string `bson:"index"` + PublicKeyHex string `bson:"publicKeyHex"` + WithdrawalCredentialsHex string `bson:"withdrawalCredentialsHex"` + EffectiveBalance string `bson:"effectiveBalance"` // Decimal string + Slashed bool `bson:"slashed"` + ActivationEligibilityEpoch string `bson:"activationEligibilityEpoch"` + ActivationEpoch string `bson:"activationEpoch"` + ExitEpoch string `bson:"exitEpoch"` + WithdrawableEpoch string `bson:"withdrawableEpoch"` +} + +// API response format +type Validator struct { + Index string `json:"index"` + Address string `json:"address"` // PublicKeyHex + Status string `json:"status"` // active/pending/exited/slashed + Age int64 `json:"age"` // Epochs since activation + StakedAmount string `json:"stakedAmount"` + IsActive bool `json:"isActive"` +} +``` + +### CoinGecko / Price +```go +type CoinGecko struct { + MarketCapUSD float64 `bson:"marketCapUSD"` + PriceUSD float64 `bson:"priceUSD"` + VolumeUSD float64 `bson:"volumeUSD"` + LastUpdated time.Time `bson:"lastUpdated"` +} + +type PriceHistory struct { + Timestamp time.Time `json:"timestamp"` + PriceUSD float64 `json:"priceUSD"` + MarketCapUSD float64 `json:"marketCapUSD"` + VolumeUSD float64 `json:"volumeUSD"` +} +``` + +### PendingTransaction +```go +type PendingTransaction struct { + Hash string `json:"hash" bson:"_id"` // TX hash is the document ID + From string `json:"from" bson:"from"` + To string `json:"to,omitempty"` + Value string `json:"value" bson:"value"` + Gas string `json:"gas" bson:"gas"` + GasPrice string `json:"gasPrice" bson:"gasPrice"` + Nonce string `json:"nonce" bson:"nonce"` + Input string `json:"input" bson:"input"` + Status string `json:"status" bson:"status"` // "pending", "mined", "dropped" + LastSeen time.Time `json:"lastSeen"` // Serialized as Unix timestamp + CreatedAt time.Time `json:"createdAt"` // Serialized as Unix timestamp +} +``` + +--- + +## Configuration + +### Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `MONGOURI` | Yes | -- | MongoDB connection string (e.g., `mongodb://localhost:27017`) | +| `NODE_URL` | No | `http://127.0.0.1:8545` | Zond node RPC endpoint (used for `zond_getBalance`) | +| `APP_ENV` | No | `development` | Environment mode. Set to `production` for HTTPS. | +| `HTTP_PORT` | No | `:8080` | HTTP listen port (development mode) | +| `HTTPS_PORT` | No* | -- | HTTPS listen port (production mode; required if `APP_ENV=production`) | +| `CERT_PATH` | No* | -- | TLS certificate file path (required if `APP_ENV=production`) | +| `KEY_PATH` | No* | -- | TLS private key file path (required if `APP_ENV=production`) | + +### .env File + +The API loads environment variables from a `.env` file in the working directory. The filename is `.env` + `$APP_ENV` (e.g., `.envproduction`, `.envdevelopment`). If `MONGOURI` is already set as an environment variable (e.g., via Docker), the `.env` file is skipped. + +Minimal `.env` example: +``` +MONGOURI=mongodb://localhost:27017 +NODE_URL=http://localhost:8545 +HTTP_PORT=:8081 +``` + +### MongoDB Setup + +- **Database name:** `qrldata-z` +- **Connection:** Via `MONGOURI` environment variable +- **Connection singleton:** Uses `sync.Once` to ensure single connection across the application +- **Timeout:** 10-second context timeout on connection +- The database and collections are populated by the `Zond2mongoDB` synchronizer component (not the API). + +--- + +## Build & Run + +### Local Development + +```bash +cd backendAPI + +# Install dependencies +go mod download + +# Build +go build -o backendAPI main.go + +# Run (ensure .env or env vars are set) +./backendAPI +``` + +The server starts on `HTTP_PORT` (default `:8080`) in development mode. + +### Docker + +```bash +# Build image +docker build -t zondscan-backend . + +# Run container +docker run -d \ + -p 8080:8080 \ + -e MONGOURI=mongodb://host.docker.internal:27017 \ + -e NODE_URL=http://host.docker.internal:8545 \ + zondscan-backend +``` + +The Dockerfile uses a multi-stage build: +1. **Builder stage:** `golang:1.24-alpine` -- compiles with static linking (`CGO_ENABLED=0`) +2. **Production stage:** `alpine:latest` -- runs as non-root user (UID 1000) + +### Production (HTTPS) + +```bash +APP_ENV=production \ +HTTPS_PORT=:443 \ +CERT_PATH=/etc/ssl/cert.pem \ +KEY_PATH=/etc/ssl/key.pem \ +MONGOURI=mongodb://localhost:27017 \ +./backendAPI +``` + +### With PM2 + +```bash +# Via the deploy script at the repo root +./deploy.sh +``` + +--- + +## Key Implementation Details + +### Address Normalization + +QRL Zond addresses use a `Z` prefix (instead of Ethereum's `0x`). The codebase handles multiple address formats: + +- **Storage in MongoDB:** Most addresses are stored with lowercase `z` prefix by the synchronizer. +- **API input:** Accepts both `Z` and `z` prefixes. Routes normalize `z` to `Z` on input. +- **Database queries:** Use both variants via `normalizeAddressBoth()` which returns `["z...", "Z..."]`. +- **Case-insensitive matching:** Transaction lookups use MongoDB regex with the `i` option. +- **RPC calls:** `GetBalance` ensures uppercase `Z` prefix for Zond node RPC. + +### Pagination Patterns + +Two pagination styles are used: + +**1-indexed (page starts at 1):** +- `/txs?page=1` -- skip = `(page - 1) * limit` +- `/pending-transactions?page=1&limit=10` +- `/address/:addr/transactions?page=1&limit=5` + +**0-indexed (page starts at 0):** +- `/contracts?page=0&limit=10` -- skip = `page * limit` +- `/token/:addr/holders?page=0&limit=25` +- `/token/:addr/transfers?page=0&limit=25` + +Maximum limit is enforced at 100 for token holder and transfer endpoints. + +### Token Handling + +Token data comes from three linked collections: + +1. **`contractCode`** -- Contract metadata including `isToken`, `name`, `symbol`, `decimals`, `totalSupply` +2. **`tokenBalances`** -- Per-holder balances with `contractAddress` + `holderAddress` + `balance` +3. **`tokenTransfers`** -- Transfer events with full context (from, to, amount, tx hash) + +The `/address/:addr/tokens` endpoint uses a MongoDB aggregation pipeline: +``` +tokenBalances (match holderAddress) + -> $addFields (lowercase contractAddress) + -> $lookup (join contractCode on lowercase address match) + -> $unwind (flatten joined array) + -> $project (select fields + token metadata) + -> $addFields ($toDecimal for balance sorting) + -> $sort (balance descending) +``` + +### Hex/Decimal Conversions + +- Block numbers stored as hex strings (`"0x1a4"`) in MongoDB. Converted to decimal (`420`) in API responses where appropriate. +- Balances stored as hex wei strings. Converted to float64 QRL (divided by 1e18) for address balances. +- `TransactionByAddress.Amount` and `PaidFees` are `float64` internally but serialized as strings with 18 decimal places (`"1.500000000000000000"`). +- `TransactionByAddress.BlockNumber` is converted from hex to decimal in the custom JSON marshaler. + +### Validator Status Computation + +Validators are stored in a single document. Status is computed at query time: + +``` +if slashed -> "slashed" +if activationEpoch > currentEpoch -> "pending" +if exitEpoch <= currentEpoch -> "exited" +else -> "active" +``` + +- `currentEpoch = latestBlockNumber / 128` +- `FAR_FUTURE_EPOCH = "18446744073709551615"` (uint64 max) indicates a validator has not exited. +- Age in epochs = `currentEpoch - activationEpoch`. + +### Error Handling & Resilience + +- All database queries use 10-second (or 15-second for token aggregations) context timeouts. +- The handler includes a custom recovery middleware that catches panics and returns HTTP 500. +- A monitor middleware logs request latency for all endpoints. +- Main function writes panic stack traces to `crash_.log` files. +- Application log output goes to both stdout and `backendAPI.log`. +- Null arrays are converted to empty arrays (`[]`) before returning JSON responses. + +### CORS Configuration + +```go +cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, +} +``` + +### Constants + +```go +const QUANTA float64 = 1000000000000000000 // 1 QRL = 1e18 wei +const SlotsPerEpoch = 128 +const SecondsPerSlot = 60 +``` + +### Dependencies + +| Package | Version | Purpose | +|---------|---------|---------| +| `gin-gonic/gin` | v1.9.1 | HTTP framework | +| `gin-contrib/cors` | v1.6.0 | CORS middleware | +| `go-playground/validator` | v10.19.0 | Input validation | +| `joho/godotenv` | v1.5.1 | .env file loading | +| `mongo-driver` | v1.8.4 | MongoDB driver | diff --git a/docs/db-syncer.md b/docs/db-syncer.md new file mode 100644 index 0000000..985ef61 --- /dev/null +++ b/docs/db-syncer.md @@ -0,0 +1,1089 @@ +# Zond2mongoDB - Blockchain Database Synchronizer + +Comprehensive documentation for the QRL Zond blockchain synchronizer that powers [zondscan.com](https://zondscan.com). This service connects to a QRL Zond node via JSON-RPC, fetches blocks and transactions, and writes them to MongoDB (database: `qrldata-z`). + +**Source location:** `Zond2mongoDB/` + +--- + +## Table of Contents + +1. [Architecture Overview](#1-architecture-overview) +2. [Sync Process](#2-sync-process) +3. [MongoDB Collections and Schemas](#3-mongodb-collections-and-schemas) +4. [RPC Calls](#4-rpc-calls) +5. [Token Transfer Detection and Indexing](#5-token-transfer-detection-and-indexing) +6. [Contract Detection and Metadata Extraction](#6-contract-detection-and-metadata-extraction) +7. [Validator Data Synchronization](#7-validator-data-synchronization) +8. [Mempool / Pending Transaction Handling](#8-mempool--pending-transaction-handling) +9. [Configuration](#9-configuration) +10. [How to Build and Run](#10-how-to-build-and-run) +11. [Reindexing Scripts](#11-reindexing-scripts) +12. [Key Implementation Details](#12-key-implementation-details) + +--- + +## 1. Architecture Overview + +``` + Zond2mongoDB + ============ + + +-----------------+ +-------------------------------+ +----------------+ + | QRL Zond Node | JSON- | main.go | | MongoDB | + | (Execution) | RPC | | | | (qrldata-z) | + | :8545 |<-------->| +-- synchroniser/ |--------->| | + +-----------------+ | | sync.go | Insert | - blocks | + | | producer_consumer.go | Upsert | - transfer | + +-----------------+ | | pending_sync.go | Update | - addresses | + | Beacon Chain | HTTP | | periodic_tasks.go | | - contractCode | + | API | REST | | gap_detection.go | | - tokenTransfers| + | :3500 |<-------->| | token_sync.go | | - validators | + +-----------------+ | +-- db/ | | - ...18+ more | + | +-- rpc/ | +----------------+ + +-----------------+ | +-- services/ | + | CoinGecko API | HTTPS | +-- fetch/ | + | (market data) |<-------->| +-- configs/ | + +-----------------+ +-------------------------------+ + | + Health endpoint + :8081/health +``` + +### Data Flow + +1. **Initial Sync**: On startup, the syncer determines the last synced block from the `sync_state` collection. It then fetches all blocks from that point to the chain head using a producer/consumer pattern with concurrent block fetching. + +2. **Continuous Monitoring**: After the initial sync, it enters a polling loop (every 30 seconds) that checks for new blocks and processes them individually or in batches depending on how far behind it is. + +3. **Parallel Services**: Several background services run concurrently: + - Mempool sync (every 1 second) + - Market data updates (every 30 minutes) + - Validator updates (every 6 hours) + - Gap detection (every 5 minutes) + - Wallet count sync (every 4 hours) + - Contract reprocessing (every 1 hour) + +### Module Structure + +| Directory | Purpose | +|-----------|---------| +| `main.go` | Entry point, signal handling, health server | +| `synchroniser/` | Core sync logic, periodic tasks, token processing | +| `db/` | All MongoDB read/write operations | +| `rpc/` | Zond node JSON-RPC client | +| `services/` | Validator data processing and storage | +| `configs/` | MongoDB connection, collection references, constants | +| `models/` | Go struct definitions for all data types | +| `fetch/` | External API clients (CoinGecko) | +| `validation/` | Hex string and address validation | +| `utils/` | Hex math utilities | +| `logger/` | Structured logging (zap) configuration | +| `scripts/` | Python reindexing utilities | + +--- + +## 2. Sync Process + +### 2.1 Startup Sequence (`main.go`) + +``` +main() + |-- Start health check server (:8081/health) + |-- StartPendingTransactionSync() <-- background goroutine + |-- Sync() <-- blocks until caught up, then continuous +``` + +### 2.2 Initial Batch Sync (`synchroniser/sync.go`) + +The `Sync()` function performs the initial catch-up: + +1. **Determine starting block**: Reads the last synced block from `sync_state` collection. Falls back to finding the latest block in the `blocks` collection. If empty, starts from genesis (`0x0`). + +2. **Store initial sync start**: Records `0x1` in `sync_initial_state` collection (used later for token processing range). + +3. **Get chain head**: Calls `zond_blockNumber` to get the latest block on the network (with retry up to 5 times with exponential backoff). + +4. **Batch processing**: Uses a producer/consumer pattern to process blocks in parallel: + - Batch size: **64** blocks (normal) or **128** blocks (when >1000 blocks behind) + - Max concurrent producers: **8** + - Channel buffer: **32** producer channels + +5. **Post-sync tasks** (after initial sync completes): + - Calculate daily transaction volume + - Process token transfers for the entire synced range + - Start wallet count sync service + - Start contract reprocessing service + - Enter continuous block monitoring + +### 2.3 Producer/Consumer Pattern (`synchroniser/producer_consumer.go`) + +``` +Sync() + | + |-- Creates buffered channel of producer channels (cap 32) + |-- Starts single consumer goroutine + |-- Creates producers for each block range + | + +-- producer(start, end) -> <-chan Data + | |-- Acquires semaphore token (max 8 concurrent) + | |-- For each block in range: + | | |-- Skip if block already exists in DB + | | |-- Sleep (reduced RPC delay for bulk: 5-7ms) + | | |-- Fetch block with 3 retries (100ms backoff) + | | |-- Track failed blocks for later retry + | | |-- Update transaction statuses + | | |-- Accumulate block data and numbers + | |-- Send accumulated Data to channel + | |-- Release semaphore token + | + +-- consumer(ch <-chan (<-chan Data)) + |-- For each producer channel: + | |-- Spawn goroutine to consume Data + | |-- InsertManyBlockDocuments() + | |-- ProcessTransactions() for each block + | |-- Track processed blocks for gap detection + | |-- Atomically track highest processed block + |-- After all producers done: + |-- Force update sync state to highest block + |-- Check for gaps in processed blocks +``` + +**Data struct:** +```go +type Data struct { + blockData []interface{} // Block documents + blockNumbers []int // Corresponding block numbers +} +``` + +### 2.4 Single Block Insertion / Continuous Monitoring (`synchroniser/periodic_tasks.go`) + +After the initial sync, `singleBlockInsertion()` starts four concurrent tickers: + +| Task | Interval | Description | +|------|----------|-------------| +| Block processing | 30 seconds | Check for new blocks, process individually or batch | +| Data updates | 30 minutes | CoinGecko price, wallet count, volume, block sizes | +| Validator updates | 6 hours | Fetch validators from beacon chain API | +| Gap detection | 5 minutes | Find and fill missing blocks (after 1min initial delay) | + +**Block processing logic** (`processBlockPeriodically`): +- If more than **64 blocks behind** (BatchSyncThreshold): uses `batchSync()` for parallel fetching +- If fewer than 64 blocks behind: processes blocks one-by-one with `processSubsequentBlocks()` +- After processing, runs token transfer processing for the new blocks + +**Single block processing** (`processSubsequentBlocks`): +1. Fetches block from node (3 retries, 500ms backoff) +2. Verifies parent hash matches the previous block in DB +3. If parent hash mismatch: rolls back and resyncs from parent (chain reorg handling) +4. Inserts block document and processes transactions +5. Updates pending transaction statuses +6. Stores the block number as the last known synced block + +### 2.5 Gap Detection and Filling (`synchroniser/gap_detection.go`) + +Gaps can occur during concurrent batch processing if individual block fetches fail. The system has three layers of gap protection: + +1. **During batch sync**: After batch completes, `detectGaps()` queries the DB for the expected block range and identifies missing block numbers (limited to last 1000 blocks). + +2. **Periodic gap detection**: Every 5 minutes, scans the last 1000 blocks for gaps. + +3. **Failed block tracking**: A `sync.Map` tracks blocks that failed to sync with attempt counts and timestamps. Blocks are retried up to 3 times (`GapRetryAttempts`). + +```go +type FailedBlock struct { + BlockNumber string + Attempts int + LastError error + LastAttempt time.Time +} +``` + +--- + +## 3. MongoDB Collections and Schemas + +Database name: **`qrldata-z`** + +### 3.1 Core Data Collections + +#### `blocks` +Stores complete block data as fetched from the Zond node. + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "baseFeePerGas": "0x...", + "gasLimit": "0x...", + "gasUsed": "0x...", + "hash": "0x...", + "number": "0x1a", + "parentHash": "0x...", + "receiptsRoot": "0x...", + "stateRoot": "0x...", + "timestamp": "0x...", + "transactions": [ + { + "blockHash": "0x...", + "blockNumber": "0x1a", + "from": "Z...", + "gas": "0x...", + "gasPrice": "0x...", + "hash": "0x...", + "nonce": "0x...", + "to": "Z...", + "transactionIndex": "0x0", + "type": "0x0", + "value": "0x...", + "chainId": "0x...", + "signature": "...", + "publicKey": "...", + "data": "0x...", + "status": "0x1" + } + ], + "transactionsRoot": "0x...", + "size": "0x...", + "withdrawals": [], + "withdrawalsRoot": "0x..." + } +} +``` + +All numeric values are stored as hex strings with `0x` prefix. Block documents are the full JSON-RPC response including `jsonrpc` and `id` fields. + +#### `transfer` +Individual transaction records with derived values. One document per transaction. + +| Field | Type | Description | +|-------|------|-------------| +| `blockNumber` | string | Hex block number | +| `blockTimestamp` | string | Hex unix timestamp | +| `from` | string | Sender address (lowercase) | +| `to` | string | Recipient address (lowercase, absent for contract creation) | +| `txHash` | string | Transaction hash | +| `pk` | string | Public key | +| `signature` | string | Transaction signature | +| `nonce` | string | Hex nonce | +| `value` | float64 | Amount in QRL (converted from wei) | +| `status` | string | Hex status (`0x1` = success) | +| `size` | string | Block size (hex) | +| `paidFees` | float64 | Transaction fee in QRL | +| `contractAddress` | string | Contract address (for creation txs, replaces `to`) | +| `data` | string | Transaction input data | + +**Index**: `blockTimestamp` descending. + +#### `transactionByAddress` +Compact transaction index for address-based lookups. + +| Field | Type | Description | +|-------|------|-------------| +| `txType` | string | Transaction type | +| `from` | string | Sender address (lowercase) | +| `to` | string | Recipient address (lowercase) | +| `txHash` | string | Transaction hash | +| `timeStamp` | string | Block timestamp (hex) | +| `amount` | float64 | Value in QRL | +| `paidFees` | float64 | Fees in QRL | +| `blockNumber` | string | Block number (hex) | + +#### `internalTransactionByAddress` +Internal transactions from `debug_traceTransaction` calls. + +| Field | Type | Description | +|-------|------|-------------| +| `type` | string | Transaction type (e.g., "CALL") | +| `callType` | string | Call type (e.g., "delegatecall") | +| `hash` | string | Transaction hash | +| `from` | string | Internal caller (lowercase, Z-prefix) | +| `to` | string | Internal callee (lowercase, Z-prefix) | +| `input` | string | Input data (hex) | +| `output` | string | Output data (hex) | +| `traceAddress` | []int | Trace position | +| `value` | float64 | Value in QRL | +| `gas` | string | Gas limit (hex) | +| `gasUsed` | string | Gas used (hex) | +| `addressFunctionIdentifier` | string | Extracted function target address | +| `amountFunctionIdentifier` | string | Extracted function amount (hex) | +| `blockTimestamp` | string | Block timestamp (hex) | + +#### `addresses` +Wallet and contract address balances. + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Address (lowercase) | +| `balance` | float64 | Current balance in QRL | +| `isContract` | bool | Whether address is a contract | + +### 3.2 Contract and Token Collections + +#### `contractCode` +Smart contract deployments and token metadata. + +| Field | Type | Description | +|-------|------|-------------| +| `address` | string | Contract address (lowercase) | +| `status` | string | Deployment status (hex, `0x1` = success) | +| `isToken` | bool | Whether contract is an ERC20 token | +| `name` | string | Token name (if ERC20) | +| `symbol` | string | Token symbol (if ERC20) | +| `decimals` | uint8 | Token decimals (if ERC20) | +| `totalSupply` | string | Token total supply (decimal string) | +| `contractCode` | string | Bytecode (hex) | +| `creatorAddress` | string | Deployer address (lowercase) | +| `creationTransaction` | string | Deployment tx hash | +| `creationBlockNumber` | string | Deployment block (hex) | +| `updatedAt` | string | ISO 8601 timestamp | +| `maxSupply` | string | (Optional) Custom max supply | +| `maxWalletAmount` | string | (Optional) Custom max wallet size | +| `maxTxLimit` | string | (Optional) Custom max tx amount | + +#### `tokenTransfers` +ERC20 token transfer events extracted from transaction logs. + +| Field | Type | Description | +|-------|------|-------------| +| `contractAddress` | string | Token contract address (lowercase) | +| `from` | string | Sender address (lowercase) | +| `to` | string | Recipient address (lowercase) | +| `amount` | string | Transfer amount (hex or decimal) | +| `blockNumber` | string | Block number (hex) | +| `txHash` | string | Transaction hash (unique index) | +| `timestamp` | string | Block timestamp (hex) | +| `tokenSymbol` | string | Token symbol | +| `tokenDecimals` | uint8 | Token decimals | +| `tokenName` | string | Token name | +| `transferType` | string | `"direct"` or `"event"` | + +**Indexes**: `(contractAddress, blockNumber)`, `(from, blockNumber)`, `(to, blockNumber)`, `txHash` (unique). + +#### `tokenBalances` +Current token holder balances per contract. + +| Field | Type | Description | +|-------|------|-------------| +| `contractAddress` | string | Token contract address (lowercase) | +| `holderAddress` | string | Holder address (lowercase) | +| `balance` | string | Current balance (decimal string via RPC) | +| `blockNumber` | string | Last updated block (hex) | +| `updatedAt` | string | ISO 8601 timestamp | + +**Index**: `(contractAddress, holderAddress)` unique. + +**Schema validation** enforced on this collection (see `configs/setup.go`). + +#### `pending_token_contracts` +Queue for contracts awaiting token detection processing. + +| Field | Type | Description | +|-------|------|-------------| +| `contractAddress` | string | Contract address | +| `txHash` | string | Transaction hash | +| `blockNumber` | string | Block number (hex) | +| `blockTimestamp` | string | Block timestamp (hex) | +| `processed` | bool | Whether this has been processed | + +**Indexes**: `(contractAddress, txHash)` unique, `processed`. + +### 3.3 Validator Collections + +#### `validators` +Single document containing all current validators. + +| Field | Type | Description | +|-------|------|-------------| +| `_id` | string | Always `"validators"` | +| `epoch` | string | Current epoch (decimal) | +| `updatedAt` | string | Unix timestamp | +| `validators` | array | Array of ValidatorRecord objects | + +Each **ValidatorRecord**: + +| Field | Type | Description | +|-------|------|-------------| +| `index` | string | Validator index (decimal) | +| `publicKeyHex` | string | Public key (hex, converted from base64) | +| `withdrawalCredentialsHex` | string | Withdrawal credentials (hex) | +| `effectiveBalance` | string | Effective balance (decimal string) | +| `slashed` | bool | Slashing status | +| `activationEligibilityEpoch` | string | Eligibility epoch (decimal) | +| `activationEpoch` | string | Activation epoch (decimal) | +| `exitEpoch` | string | Exit epoch (decimal) | +| `withdrawableEpoch` | string | Withdrawable epoch (decimal) | +| `slotNumber` | string | Assigned slot (decimal) | +| `isLeader` | bool | Whether this validator is a slot leader | + +#### `validatorHistory` +Per-epoch validator statistics. + +| Field | Type | Description | +|-------|------|-------------| +| `epoch` | string | Epoch number (decimal, unique key) | +| `timestamp` | int64 | Unix timestamp | +| `validatorsCount` | int | Total validators | +| `activeCount` | int | Active validators | +| `pendingCount` | int | Pending validators | +| `exitedCount` | int | Exited validators | +| `slashedCount` | int | Slashed validators | +| `totalStaked` | string | Sum of effective balances (decimal) | + +#### `epoch_info` +Current beacon chain head information. + +| Field | Type | Description | +|-------|------|-------------| +| `_id` | string | Always `"current"` | +| `headEpoch` | string | Current head epoch | +| `headSlot` | string | Current head slot | +| `finalizedEpoch` | string | Last finalized epoch | +| `justifiedEpoch` | string | Last justified epoch | +| `finalizedSlot` | string | Last finalized slot | +| `justifiedSlot` | string | Last justified slot | +| `updatedAt` | int64 | Unix timestamp | + +### 3.4 Analytics Collections + +#### `coingecko` +Current QRL market data (single document, upserted). + +| Field | Type | Description | +|-------|------|-------------| +| `marketCapUSD` | float32 | Market cap in USD | +| `priceUSD` | float32 | Current price in USD | +| `volumeUSD` | float32 | 24h trading volume in USD | +| `lastUpdated` | Date | Last update timestamp | + +**Schema validation** enforced. + +#### `priceHistory` +Historical price snapshots for charts. + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | Date | Snapshot time | +| `priceUSD` | float32 | Price in USD | +| `marketCapUSD` | float32 | Market cap in USD | +| `volumeUSD` | float32 | 24h volume in USD | + +**Index**: `timestamp` descending. + +#### `walletCount` +Total non-contract address count. + +| Field | Type | Description | +|-------|------|-------------| +| `_id` | string | `"current_count"` | +| `count` | int64 | Number of non-contract addresses | +| `timestamp` | Date | Last update time | + +**Schema validation** enforced. + +#### `dailyTransactionsVolume` +24-hour transaction volume. + +| Field | Type | Description | +|-------|------|-------------| +| `type` | string | `"daily_volume"` | +| `volume` | float64 | Total QRL transferred in 24h | +| `timestamp` | string | Latest block timestamp (hex) | +| `transferCount` | int | Number of transfers | + +**Schema validation** enforced. + +#### `totalCirculatingQuanta` / `totalCirculatingSupply` +Total circulating supply. + +| Field | Type | Description | +|-------|------|-------------| +| `_id` | string | `"totalBalance"` | +| `circulating` | string | Total balance as decimal string | + +#### `averageBlockSize` / `blockSize` +Block size history for charts. Rebuilt periodically from `blocks` collection via aggregation pipeline. + +| Field | Type | Description | +|-------|------|-------------| +| `blockNumber` | string | Block number (hex) | +| `timestamp` | string | Block timestamp (hex) | +| `size` | string | Block size (hex) | +| `transactionCount` | int | Number of transactions | + +### 3.5 Sync State Collections + +#### `sync_state` +Tracks the synchronizer's progress. + +| Field | Type | Description | +|-------|------|-------------| +| `_id` | string | `"last_synced_block"` | +| `block_number` | string | Hex block number of last processed block | + +#### `sync_initial_state` +Records the starting block of the initial sync (used for token processing range). + +| Field | Type | Description | +|-------|------|-------------| +| `_id` | string | `"initial_sync_start"` | +| `block_number` | string | Hex block number (typically `"0x1"`) | + +### 3.6 Mempool Collection + +#### `pending_transactions` +Transactions currently in the node's mempool. + +| Field | Type | Description | +|-------|------|-------------| +| `_id` | string | Transaction hash | +| `from` | string | Sender address | +| `to` | string | Recipient address | +| `value` | string | Value (hex) | +| `gas` | string | Gas limit (hex) | +| `gasPrice` | string | Gas price (hex) | +| `maxFeePerGas` | string | EIP-1559 max fee (optional) | +| `maxPriorityFeePerGas` | string | EIP-1559 priority fee (optional) | +| `input` | string | Input data | +| `nonce` | string | Nonce (hex) | +| `type` | string | Transaction type | +| `chainId` | string | Chain ID | +| `lastSeen` | Date | Last time seen in mempool | +| `status` | string | `"pending"`, `"mined"`, or `"dropped"` | +| `createdAt` | Date | First seen timestamp | + +### 3.7 Other Collections + +#### `coinbase` +Block proposer rewards. + +| Field | Type | Description | +|-------|------|-------------| +| `blockhash` | string | Block hash | +| `blocknumber` | uint64 | Block number | +| `from` | string | Proposer address | +| `blockproposerreward` | uint64 | Proposer reward | +| `attestorreward` | uint64 | Attestor reward | +| `feereward` | uint64 | Fee reward | + +--- + +## 4. RPC Calls + +The synchronizer communicates with two external APIs: + +### 4.1 Zond Execution Layer (JSON-RPC via `NODE_URL`) + +All calls go through a shared HTTP client with connection pooling (100 max idle connections, 30s timeout). + +| RPC Method | File | Purpose | +|------------|------|---------| +| `zond_blockNumber` | `rpc/calls.go` | Get latest block number | +| `zond_getBlockByNumber` | `rpc/calls.go` | Fetch full block with transactions (`true` for full tx objects) | +| `zond_getTransactionReceipt` | `rpc/calls.go`, `rpc/tokenscalls.go` | Get tx receipt (contract address, status, logs) | +| `zond_getBalance` | `rpc/calls.go` | Get address balance | +| `zond_getCode` | `rpc/calls.go` | Get contract bytecode | +| `zond_call` | `rpc/calls.go`, `rpc/tokenscalls.go` | Call contract method (read-only) | +| `zond_getLogs` | `rpc/calls.go` | Get event logs for a block (Transfer events) | +| `zond_getTransactionByHash` | `rpc/calls.go` | Get transaction details by hash | +| `debug_traceTransaction` | `rpc/calls.go` | Trace internal calls (callTracer) | +| `txpool_content` | `rpc/pending.go` | Get mempool pending/queued transactions | + +### 4.2 Beacon Chain API (HTTP REST via `BEACONCHAIN_API`) + +| Endpoint | File | Purpose | +|----------|------|---------| +| `GET /zond/v1alpha1/validators` | `rpc/calls.go` | Fetch validator list (paginated, up to 3 pages) | +| `GET /zond/v1alpha1/beacon/chainhead` | `rpc/calls.go` | Get current chain head (epoch, slot, finality) | + +### 4.3 CoinGecko API + +| Endpoint | File | Purpose | +|----------|------|---------| +| `GET /api/v3/coins/quantum-resistant-ledger` | `fetch/coingecko.go` | Market data (price, market cap, volume) | + +### 4.4 ERC20 Token Method Calls (`rpc/tokenscalls.go`) + +These are made via `zond_call` to detect and query ERC20 tokens: + +| Method Signature | Function | Purpose | +|-----------------|----------|---------| +| `0x06fdde03` | `name()` | Get token name | +| `0x95d89b41` | `symbol()` | Get token symbol | +| `0x313ce567` | `decimals()` | Get token decimals | +| `0x70a08231` | `balanceOf(address)` | Get token balance for holder | +| `0x18160ddd` | `totalSupply()` | Get total token supply | +| `0x32668b54` | `maxSupply()` | Custom: max supply | +| `0x94303c2d` | `maxTxAmount()` | Custom: max transaction amount | +| `0x41d3014e` | `maxWalletSize()` | Custom: max wallet size | +| `0x8da5cb5b` | `owner()` | Custom: contract owner | + +--- + +## 5. Token Transfer Detection and Indexing + +Token transfer detection is a two-phase process that runs after block sync. + +### 5.1 Phase 1: Queue Potential Token Contracts + +During `ProcessTransactions()` for each block: +1. For each transaction, `processContracts()` checks if it's a contract creation (empty `to` field) or interaction with an existing contract. +2. If a contract is involved, `QueuePotentialTokenContract()` writes an entry to the `pending_token_contracts` collection with `processed: false`. + +### 5.2 Phase 2: Process Queued Contracts + +`ProcessTokenTransfersFromTransactions()` runs after transaction processing: +1. Queries all unprocessed entries from `pending_token_contracts`. +2. For each, checks if the contract exists in `contractCode` and `isToken == true`. +3. If it's a token: + - Gets transaction details via RPC + - Checks for **direct transfer calls** by decoding `tx.data` (function signature `0xa9059cbb`) + - Checks for **Transfer event logs** by getting the transaction receipt and filtering for the Transfer event signature (`0xddf252ad...`) + - Stores each transfer in `tokenTransfers` collection + - Updates sender and recipient balances in `tokenBalances` (via RPC `balanceOf` call) +4. Marks the entry as `processed: true`. + +### 5.3 Block-Level Token Transfer Processing + +`ProcessBlockTokenTransfers()` takes a different approach for bulk processing: +1. Calls `zond_getLogs` for the block with the Transfer event signature topic filter. +2. For each log with 3 topics (standard Transfer event): + - Extracts the contract address from `log.Address`. + - Calls `EnsureTokenInDatabase()` to verify/create the token in `contractCode`. + - Extracts `from` and `to` from `log.Topics[1]` and `log.Topics[2]` (last 20 bytes). + - Extracts `amount` from `log.Data`. + - Checks for duplicates via `TokenTransferExists()`. + - Stores the transfer and updates balances. + +### 5.4 Transfer Event Signature + +``` +keccak256("Transfer(address,address,uint256)") += 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef +``` + +Log topics layout: +- `topics[0]`: Event signature (Transfer) +- `topics[1]`: `from` address (padded to 32 bytes, last 20 bytes are the address) +- `topics[2]`: `to` address (padded to 32 bytes, last 20 bytes are the address) +- `data`: Transfer amount (uint256, hex encoded) + +### 5.5 Post-Initial-Sync Token Processing (`synchroniser/token_sync.go`) + +After the initial block sync completes, `ProcessTokensAfterInitialSync()`: +1. Queries `blocks` collection for all blocks that have at least one transaction. +2. Filters by hex range comparison in Go (not MongoDB, because hex strings are not zero-padded). +3. Processes token transfers in configurable batches (default: 10 blocks per batch, 86ms delay between batches). + +--- + +## 6. Contract Detection and Metadata Extraction + +### 6.1 New Contract Detection (`db/contracts.go`) + +When a transaction has an empty `to` field (contract creation): +1. Calls `zond_getTransactionReceipt` to get the deployed contract address and status. +2. Calls `zond_getCode` to fetch the contract bytecode. +3. Calls `GetTokenInfo()` which sequentially tries `name()`, `symbol()`, and `decimals()`. If all three succeed, the contract is flagged as an ERC20 token. +4. If it's a token, fetches `totalSupply()`. +5. Stores everything in the `contractCode` collection via `StoreContract()`. + +### 6.2 Existing Contract Detection + +When a transaction targets an existing address, `IsAddressContract()`: +1. Checks the `contractCode` collection in MongoDB. +2. If not found, calls `zond_getCode` via RPC. +3. If code exists (not `0x` or empty), it's a contract - stores it with token detection. + +### 6.3 Contract Reprocessing (`db/contracts.go`) + +`StartContractReprocessingJob()` runs every 1 hour: +- Queries for contracts with missing information (empty code, tokens without total supply, non-tokens without name/symbol). +- Re-fetches data from the node for each incomplete contract. +- Preserves existing creation information (creator address, creation tx, creation block). + +### 6.4 Token Detection (`db/token_detection.go`) + +The `DetectToken()` function provides a clean API: +```go +func DetectToken(contractAddress string) TokenDetectionResult { + name, symbol, decimals, isToken := rpc.GetTokenInfo(contractAddress) + // Also fetches totalSupply if isToken +} +``` + +`EnsureTokenInDatabase()` is the consolidated function that: +1. Detects if the contract is a token via RPC. +2. Gets or creates the contract entry in MongoDB. +3. Preserves existing creation information if updating. +4. Returns the contract info and whether it's a token. + +--- + +## 7. Validator Data Synchronization + +### 7.1 Sync Flow + +Every 6 hours (`updateValidatorsPeriodically`): + +1. **Get chain head**: `GET /zond/v1alpha1/beacon/chainhead` returns current epoch, slot, and finality info. Stored in the `epoch_info` collection. + +2. **Fetch validators**: `GET /zond/v1alpha1/validators` with pagination (up to 3 pages). Each page contains a list of validators with their details. + +3. **Store validators** (`services/validator_service.go`): + - Converts base64-encoded public keys and withdrawal credentials to hex. + - Determines leader status (simplified: `index % 128 == 0`). + - Merges with existing validators (updates mutable fields, adds new ones). + - All validators stored in a single document with `_id: "validators"`. + +4. **Store history** (`services/validator_service.go`): + - Computes per-epoch statistics: active, pending, exited, slashed counts. + - Calculates total staked by summing effective balances. + - Upserts into `validatorHistory` keyed by epoch. + +### 7.2 Epoch Calculation + +```go +currentEpoch = latestBlockNumber / 128 // 128 slots per epoch +``` + +### 7.3 Validator Status Logic + +```go +func GetValidatorStatus(activationEpoch, exitEpoch string, slashed bool, currentEpoch int64) string { + if slashed { return "slashed" } + if activation > currentEpoch { return "pending" } + if exit <= currentEpoch { return "exited" } + return "active" +} +``` + +--- + +## 8. Mempool / Pending Transaction Handling + +### 8.1 Three Concurrent Services + +Started by `StartPendingTransactionSync()` at application startup: + +| Service | Interval | Function | +|---------|----------|----------| +| Mempool sync | 1 second | `syncMempool()` | +| Old tx cleanup | 1 hour | `CleanupOldPendingTransactions(24h)` | +| Pending verification | 5 minutes | `verifyPendingTransactions()` | + +### 8.2 Mempool Sync (`syncMempool`) + +1. Calls `txpool_content` RPC (uses `MEMPOOL_NODE_URL` if set, else `NODE_URL`). +2. Parses the nested response format: `{pending: {address: {nonce: tx}}, queued: {address: {nonce: tx}}}`. +3. Upserts each transaction into `pending_transactions` with `status: "pending"`. +4. Processes both `pending` and `queued` pools. + +### 8.3 Pending Transaction Lifecycle + +``` +txpool_content -> UpsertPendingTransaction (status: "pending") + | + v + Block mined with tx + | + v + UpdatePendingTransactionsInBlock (status: "mined") + | + v + verifyPendingTransactions -> DeletePendingTransaction + (if receipt exists) + | + v (if not mined) + CleanupOldPendingTransactions + (delete if lastSeen > 24 hours ago) +``` + +### 8.4 Block-Level Pending Update + +When a new block is processed (`UpdatePendingTransactionsInBlock`): +1. Creates a map of all transaction hashes in the block. +2. Queries all pending transactions. +3. For any match, updates `status` to `"mined"` and records the block number. + +--- + +## 9. Configuration + +### 9.1 Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `MONGOURI` | Yes | `mongodb://localhost:27017` | MongoDB connection string (without database name) | +| `NODE_URL` | Yes | `http://localhost:8545` | Zond execution layer JSON-RPC endpoint | +| `MEMPOOL_NODE_URL` | No | Falls back to `NODE_URL` | Separate RPC for mempool access | +| `BEACONCHAIN_API` | Yes | `http://localhost:3500` | Beacon chain HTTP API endpoint | +| `HEALTH_PORT` | No | `8081` | Port for Kubernetes health check endpoint | +| `RPC_DELAY_MS` | No | `50` | Delay between RPC calls in ms | +| `RPC_DELAY_JITTER_MS` | No | `26` | Random jitter added to RPC delay | + +### 9.2 Sync Constants (`synchroniser/producer_consumer.go`) + +| Constant | Value | Description | +|----------|-------|-------------| +| `DefaultBatchSize` | 64 | Normal batch size for block fetching | +| `LargeBatchSize` | 128 | Batch size when >1000 blocks behind | +| `BatchSyncThreshold` | 64 | Blocks behind before switching to batch mode | +| `LargeSyncThreshold` | 1000 | Blocks behind before using large batch size | +| `MaxProducerConcurrency` | 8 | Max concurrent block-fetching goroutines | + +### 9.3 Mempool Constants (`synchroniser/pending_sync.go`) + +| Constant | Value | Description | +|----------|-------|-------------| +| `MEMPOOL_SYNC_INTERVAL` | 1 second | Mempool polling frequency | +| `CLEANUP_INTERVAL` | 1 hour | Old pending tx cleanup frequency | +| `VERIFY_PENDING_INTERVAL` | 5 minutes | Pending tx verification frequency | +| `MAX_PENDING_AGE` | 24 hours | Max age before pending tx is cleaned up | + +### 9.4 Gap Detection Constants (`synchroniser/gap_detection.go`) + +| Constant | Value | Description | +|----------|-------|-------------| +| `MaxGapDetectionBlocks` | 1000 | Maximum blocks to scan for gaps | +| `GapRetryAttempts` | 3 | Max retry attempts for failed blocks | + +### 9.5 Token Sync Defaults (`synchroniser/token_sync.go`) + +| Setting | Value | Description | +|---------|-------|-------------| +| `BatchSize` | 10 | Blocks per token processing batch | +| `BatchDelayMs` | 86 | Delay between batches (ms) | +| `QueryTimeoutSec` | 30 | Timeout for block queries (seconds) | + +### 9.6 Other Constants (`configs/const.go`) + +| Constant | Value | Description | +|----------|-------|-------------| +| `QUANTA` | 1e18 | Wei-to-QRL divisor | +| `QRLZeroAddress` | `Z000...000` | Zero address (40 hex chars + Z) | +| `LOG_FILENAME` | `zond_sync.log` | Log file name (in `logs/` directory) | + +--- + +## 10. How to Build and Run + +### 10.1 Prerequisites + +- Go 1.24+ +- MongoDB (accessible, database will be created automatically) +- QRL Zond node (execution layer + beacon chain) + +### 10.2 Build + +```bash +cd Zond2mongoDB + +# Download dependencies +go mod download + +# Build +go build -o synchroniser main.go +``` + +### 10.3 Configure + +Create a `.env` file from the example: + +```bash +cp .env.example .env +``` + +Edit `.env`: +```env +MONGOURI=mongodb://localhost:27017 +NODE_URL=http://localhost:8545 +MEMPOOL_NODE_URL=http://localhost:8545 +BEACONCHAIN_API=http://localhost:3500 +``` + +### 10.4 Run + +```bash +./synchroniser +``` + +The synchronizer will: +1. Connect to MongoDB and create/validate all collections with indexes and schema validators. +2. Start the health check server on port 8081. +3. Start mempool sync (background). +4. Begin block synchronization from the last known block. +5. After catching up, enter continuous monitoring mode. + +Logs are written to both stdout and `logs/zond_sync.log`. + +### 10.5 Docker + +```bash +# Build +docker build -t zond-syncer . + +# Run +docker run -e MONGOURI=mongodb://mongo:27017 \ + -e NODE_URL=http://node:8545 \ + -e BEACONCHAIN_API=http://beacon:3500 \ + zond-syncer +``` + +The Dockerfile uses a two-stage build (Go 1.24-alpine builder, alpine runner) with a non-root user (UID 1000). + +### 10.6 Kubernetes + +The `/health` endpoint (port 8081, configurable via `HEALTH_PORT`) returns `{"status":"ok"}` for liveness/readiness probes. + +--- + +## 11. Reindexing Scripts + +Located in `scripts/`, these Python scripts are used for one-time reindexing operations when the database needs to be repaired or backfilled. + +### 11.1 `reindex_contracts.py` + +**Purpose**: Scans the `transfer` collection for contract creation transactions (those with a `contractAddress` field) and rebuilds the `contractCode` collection. + +**What it does**: +1. Connects to MongoDB and the Zond RPC node. +2. Queries `transfer` collection for documents with `contractAddress`. +3. For each contract creation: + - Gets the contract bytecode via `zond_getCode`. + - Calls `name()`, `symbol()`, `decimals()` to detect ERC20 tokens. + - Upserts the contract data into `contractCode`. + +**Usage**: +```bash +cd scripts +pip install -r requirements.txt +MONGOURI=mongodb://localhost:27017 NODE_URL=http://localhost:8545 python reindex_contracts.py +``` + +### 11.2 `reindex_tokens.py` + +**Purpose**: Rebuilds the `tokenTransfers` and `tokenBalances` collections by replaying Transfer event logs from the blockchain. + +**What it does**: +1. Creates indexes on `tokenTransfers` collection. +2. Updates contracts with missing `creationBlockNumber` by looking up their creation transaction receipt. +3. For each token contract in `contractCode` where `isToken: true`: + - Fetches Transfer event logs from the creation block to the latest block (in batches of 50 blocks). + - Parses event topics to extract `from`, `to`, and `amount`. + - Stores transfers in `tokenTransfers` (skips duplicates). + - Calculates running balances and updates `tokenBalances`. + +**Dependencies** (in addition to `requirements.txt`): +- `web3` (for hex-to-int conversion) + +**Usage**: +```bash +cd scripts +pip install -r requirements.txt +pip install web3 +MONGOURI=mongodb://localhost:27017 NODE_URL=http://localhost:8545 python reindex_tokens.py +``` + +--- + +## 12. Key Implementation Details + +### 12.1 Concurrency Model + +- **Producer/consumer pattern** for batch block fetching. Producers run as goroutines, limited to 8 concurrent via a channel-based semaphore (`producerSem`). +- **Atomic operations** (`sync/atomic`) used to track the highest processed block number across goroutines. +- **Mutex** (`sync.Mutex`) protects sync state updates during consumer processing. +- **sync.Map** used for failed block tracking (lock-free concurrent map). +- **WaitGroups** coordinate goroutine completion. + +### 12.2 RPC Rate Limiting + +Two delay modes: +- **Normal**: 50ms + random(0-26ms) jitter between calls. +- **Bulk sync**: 1/10th of normal (min 5ms) for faster initial sync. + +Delays are configurable via `RPC_DELAY_MS` and `RPC_DELAY_JITTER_MS` environment variables. + +### 12.3 Retry Logic + +| Operation | Retries | Backoff | +|-----------|---------|---------| +| Get latest block (initial) | 5 | Exponential (1s, 2s, 4s, 8s, 16s) | +| Block fetch (producer) | 3 | Linear (100ms, 200ms, 300ms) | +| Block fetch (single) | 3 | Linear (500ms, 1000ms, 1500ms) | +| Periodic tasks | 5 | Exponential (1s, 2s, 4s, 8s, 16s) | +| Token balance RPC | 3 | Linear (500ms, 1000ms, 1500ms) | +| CoinGecko fetch | 3 | Exponential with jitter (30s base, max 5min) | + +### 12.4 Chain Reorg Handling + +In `processSubsequentBlocks()`: +1. After fetching a new block, checks if `block.parentHash` matches the hash of the previous block stored in MongoDB. +2. If there's a mismatch, calls `Rollback()` which: + - Deletes all blocks after the mismatched block number (in a MongoDB transaction). + - Updates the sync state to the rolled-back block. +3. Returns the parent block number so the sync loop reprocesses from there. + +### 12.5 Address Format + +QRL Zond uses two address formats: +- **Legacy**: `0x` prefix (standard Ethereum format) +- **Zond native**: `Z` prefix + +The synchronizer normalizes addresses: +- Stored in MongoDB as **lowercase** (via `strings.ToLower()`). +- The `validation` package handles both formats. +- `ConvertToZAddress()` converts `0x` to `Z` prefix. + +### 12.6 Hex Number Handling + +All block numbers, timestamps, gas values, and amounts from the Zond node come as non-zero-padded hex strings (e.g., `0x1a`, not `0x0000001a`). The `utils` package provides: +- `HexToInt()` / `IntToHex()` - Conversion using `math/big`. +- `CompareHexNumbers()` - Proper numeric comparison (not lexicographic). +- `AddHexNumbers()` / `SubtractHexNumbers()` - Hex arithmetic. + +This is critical because MongoDB lexicographic comparison of hex strings produces incorrect results for different-length strings (e.g., `0x9` > `0x10` lexicographically). The `getBlocksWithTransactions()` function in `token_sync.go` handles this by filtering in Go rather than MongoDB. + +### 12.7 Transaction Fee Calculation + +For each transaction: +1. Gets `gasPrice` from the transaction. +2. Gets `gasUsed` from `debug_traceTransaction` or falls back to the transaction receipt's `gasUsed`, or the gas limit. +3. Fee = `gasPrice * gasUsed / 1e18` (converted from wei to QRL). +4. If fee is zero for a successful transaction (`status: 0x1`), sets a minimum fee of `0.000001 QRL`. + +### 12.8 Logging + +Uses [uber-go/zap](https://github.com/uber-go/zap) structured logging: +- Console encoding with custom time format (`Jan 2 15:04:05`). +- Writes to both `logs/zond_sync.log` and stdout. +- Debug level enabled. +- All periodic tasks have panic recovery with automatic restart after 5 seconds. + +### 12.9 Health Check + +A simple HTTP health endpoint at `/health` (default port 8081) returns: +```json +{"status":"ok"} +``` +Used for Kubernetes liveness/readiness probes and Docker health checks. + +### 12.10 Duplicate Prevention + +- **Blocks**: `BlockExists()` checks before every insert (both single and batch). +- **Batch inserts**: `InsertManyBlockDocuments()` deduplicates within the batch and against the DB. +- **Token transfers**: `txHash` has a unique index; `TokenTransferExists()` checks before insert. +- **Token balances**: `(contractAddress, holderAddress)` compound unique index with upsert. +- **Sync state**: Only updates if new block number is higher than existing. +- **Pending contracts**: `(contractAddress, txHash)` compound unique index with upsert. + +### 12.11 Collection Initialization + +On MongoDB connection (`configs/setup.go`): +1. Creates collections with JSON schema validators for `dailyTransactionsVolume`, `coingecko`, `priceHistory`, `walletCount`, `totalCirculatingSupply`, and `tokenBalances`. +2. Creates compound and unique indexes for `tokenBalances`, `pending_token_contracts`, `tokenTransfers`, `priceHistory`, and `transfer`. +3. Initializes the `sync_state` collection with `block_number: "0x0"` if empty. +4. Initializes CoinGecko collection with zero values. From ffe9edc068d82ee75336346ca353908f226a0335 Mon Sep 17 00:00:00 2001 From: moscowchill Date: Mon, 2 Mar 2026 11:13:24 +0100 Subject: [PATCH 05/12] fix: deploy scripts and MUI dependency issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Standardize PM2 syncer name to "synchroniser" across deploy.sh and update-backend.sh so processes are properly cleaned up on VPS updates - Fix git repo detection in deploy.sh for submodules (use git rev-parse instead of checking for .git directory) - Make browserslist update non-fatal in deploy.sh - Fix broken absolute path in update-backend.sh (cd /BackendAPI → cd ../backendAPI) - Remove unused @mui/lab (v7 beta conflicting with v6 stack) - Downgrade @mui/x-data-grid from v8 to v7 (v8 uses v7-only @mui/system internals) - Add explicit @mui/system dep to fix hoisting with --legacy-peer-deps Co-Authored-By: Claude Opus 4.6 --- ExplorerFrontend/package.json | 4 ++-- deploy.sh | 10 +++++----- update-backend.sh | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ExplorerFrontend/package.json b/ExplorerFrontend/package.json index efdec16..c2f8d20 100644 --- a/ExplorerFrontend/package.json +++ b/ExplorerFrontend/package.json @@ -10,9 +10,9 @@ "@headlessui/react": "^2.2.0", "@heroicons/react": "^2.0.18", "@mui/icons-material": "^6.4.0", - "@mui/lab": "^7.0.1-beta.20", "@mui/material": "^6.4.0", - "@mui/x-data-grid": "^8.0.0", + "@mui/system": "^6.4.0", + "@mui/x-data-grid": "^7.29.0", "@tanstack/react-query": "^5.90.16", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", diff --git a/deploy.sh b/deploy.sh index 6a405c5..4f8917e 100755 --- a/deploy.sh +++ b/deploy.sh @@ -18,7 +18,7 @@ clean_pm2() { pm2 flush || print_status "No logs to flush" # Stop and delete only processes started by this deployment - for name in handler syncer frontend; do + for name in handler syncer synchroniser frontend; do pm2 delete $name || print_status "No process named $name to delete" done @@ -128,7 +128,7 @@ check_port() { # Clone the repository clone_repo() { - if [ -d ".git" ]; then + if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then print_status "Repository already exists. Checking git status..." git status @@ -142,7 +142,7 @@ clone_repo() { else print_status "Cloning QRL Explorer repository..." git clone https://github.com/DigitalGuards/zondscan.git || print_error "Failed to clone repository" - cd ../backendAPI || print_error "Failed to enter project directory" + cd zondscan || print_error "Failed to enter project directory" fi export BASE_DIR=$(pwd) @@ -335,7 +335,7 @@ EOL # Update browserslist database print_status "Updating browserslist database..." - npx update-browserslist-db@latest || print_error "Failed to update browserslist" + npx update-browserslist-db@latest || print_status "Failed to update browserslist, continuing..." # Build production frontend print_status "Building production frontend..." @@ -372,7 +372,7 @@ EOL # Start synchronizer with PM2, explicitly setting environment variables print_status "Starting synchronizer with PM2..." - pm2 start ./zsyncer --name "syncer" --cwd "$BASE_DIR/Zond2mongoDB" || print_error "Failed to start synchronizer" + pm2 start ./zsyncer --name "synchroniser" --cwd "$BASE_DIR/Zond2mongoDB" || print_error "Failed to start synchronizer" } # Save PM2 processes diff --git a/update-backend.sh b/update-backend.sh index f06bdff..36fc656 100755 --- a/update-backend.sh +++ b/update-backend.sh @@ -66,7 +66,7 @@ echo -e "${GREEN}Synchroniser deployed successfully${NC}" # Deploy BackendAPI server echo -e "${YELLOW}Building and deploying server...${NC}" -cd /BackendAPI +cd ../backendAPI if [ $? -ne 0 ]; then echo -e "${RED}Error: Could not find BackendAPI directory${NC}" exit 1 From a6bd86bdede0e61dcf882a2cd55bf56fc0fb669a Mon Sep 17 00:00:00 2001 From: moscowchill Date: Mon, 2 Mar 2026 13:01:37 +0100 Subject: [PATCH 06/12] feat: comprehensive UI component upgrades for ExplorerFrontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave 1 - Shared components: - Create Badge, Breadcrumbs, EmptyState components - Rewrite Sidebar with active states, Link elements, Escape key, a11y - Rewrite Alert with 4 variants, icons, role="alert", dismissible - Add skip-to-main-content link, fix layout structure - Delete dead Header.tsx and Layout.tsx Wave 2 - Accessibility & consolidation: - Fix SearchBar: remove global Enter hijack, add Cmd/K, aria-label - Full QRCodeModal a11y: focus trap, Escape, role="dialog", scroll lock - Delete dead TransactionsDisplay.tsx, Pagination.tsx - Fix AreaChart gradient ID collision, responsive NewBlockSizeChart - Style BlockSizeChart buttons, remove MUI from Charts.tsx - Batch ARIA pass: scope="col" on all tables, form input labels, pagination aria-labels, role="status" on loading states, tab roles - Merge CopyAddressButton/CopyHashButton into unified CopyButton - Style DownloadBtn, Footer nav landmark, StatCard outside render Wave 3 - Error handling, SEO, cleanup: - Create global error.tsx error boundary - Delete dead Transactions.tsx, TokensList.tsx, lib/styles.ts - Fix dangerouslySetInnerHTML XSS in pending redirect → use redirect() - Add richlist error handling, fix pending tx false auto-refresh text - Fix SEO: validators OG URL, canonical URLs, sharedMetadata spread - Remove 15+ console.log calls from production code paths - SearchBar accepts lowercase z-prefix, not-found uses EmptyState - Add 'use client' to TradingView/Charts, remove from tx/layout - ValidatorStatusChart keyboard a11y, contracts TabButton ARIA - TanStackTable tab aria-labelledby wiring - Fix converter off-spec bg color, "Bagholder" → "Wallet Count" --- .claude/agents/components.md | 1262 +++++++++++++++++ .claude/agents/frontend-developer.md | 145 +- .../app/address/[query]/address-client.tsx | 3 - .../app/address/[query]/address-view.tsx | 23 +- .../app/address/[query]/balance-display.tsx | 5 +- .../app/address/[query]/contract-display.tsx | 4 +- ExplorerFrontend/app/address/[query]/page.tsx | 2 +- .../address/[query]/token-contract-view.tsx | 39 +- .../app/block/[query]/block-detail-client.tsx | 12 +- ExplorerFrontend/app/block/[query]/page.tsx | 2 +- ExplorerFrontend/app/block/loading.tsx | 4 +- ExplorerFrontend/app/blocks/loading.tsx | 2 +- ExplorerFrontend/app/components/Alert.tsx | 88 +- ExplorerFrontend/app/components/AreaChart.tsx | 8 +- ExplorerFrontend/app/components/Badge.tsx | 38 + .../app/components/BalanceCheckTool.tsx | 11 +- .../app/components/BlockSizeChart.tsx | 20 +- .../app/components/Breadcrumbs.tsx | 42 + ExplorerFrontend/app/components/Charts.tsx | 42 +- .../app/components/CopyAddressButton.tsx | 60 - .../app/components/CopyButton.tsx | 113 ++ .../app/components/CopyHashButton.tsx | 96 -- .../app/components/DebouncedInput.tsx | 3 + .../app/components/DownloadBtn.tsx | 6 +- .../app/components/EmptyState.tsx | 37 + ExplorerFrontend/app/components/Footer.tsx | 8 +- ExplorerFrontend/app/components/Header.tsx | 225 --- ExplorerFrontend/app/components/Layout.tsx | 13 - .../app/components/NewBlockSizeChart.tsx | 28 +- .../app/components/Pagination.tsx | 44 - .../app/components/QRCodeButton.tsx | 30 +- .../app/components/QRCodeModal.tsx | 105 +- ExplorerFrontend/app/components/SearchBar.tsx | 20 +- ExplorerFrontend/app/components/Sidebar.tsx | 519 ++++--- .../app/components/TanStackTable.tsx | 21 +- .../app/components/TokensList.tsx | 49 - .../app/components/TradingViewWidget.tsx | 2 + .../app/components/Transactions.tsx | 155 -- .../app/components/TransactionsDisplay.tsx | 116 -- ExplorerFrontend/app/components/index.ts | 14 +- .../app/contracts/contracts-client.tsx | 50 +- .../app/contracts/contracts-wrapper.tsx | 8 +- .../app/converter/converter-client.tsx | 14 +- ExplorerFrontend/app/error.tsx | 31 + ExplorerFrontend/app/home-client.tsx | 100 +- ExplorerFrontend/app/layout.tsx | 14 +- ExplorerFrontend/app/lib/styles.ts | 10 - ExplorerFrontend/app/not-found.tsx | 24 +- .../app/pending/[query]/PendingList.tsx | 30 +- .../app/pending/tx/[hash]/page.tsx | 18 +- .../tx/[hash]/pending-transaction-view.tsx | 16 +- ExplorerFrontend/app/richlist/page.tsx | 15 +- .../app/richlist/richlist-client.tsx | 8 +- .../transactions/[query]/TransactionCard.tsx | 11 +- .../transactions/[query]/TransactionsList.tsx | 28 +- .../app/transactions/[query]/page.tsx | 13 +- .../[query]/transactions-client.tsx | 2 - ExplorerFrontend/app/transactions/loading.tsx | 2 +- ExplorerFrontend/app/tx/[query]/page.tsx | 7 - .../app/tx/[query]/transaction-view.tsx | 37 +- ExplorerFrontend/app/tx/layout.tsx | 2 - ExplorerFrontend/app/tx/loading.tsx | 4 +- .../[id]/validator-detail-client.tsx | 78 +- .../components/ValidatorHistoryChart.tsx | 8 +- .../components/ValidatorStatsCards.tsx | 4 +- .../components/ValidatorStatusChart.tsx | 18 +- .../validators/components/ValidatorTable.tsx | 76 +- ExplorerFrontend/app/validators/page.tsx | 2 +- .../app/validators/validators-client.tsx | 6 +- 69 files changed, 2564 insertions(+), 1488 deletions(-) create mode 100644 .claude/agents/components.md create mode 100644 ExplorerFrontend/app/components/Badge.tsx create mode 100644 ExplorerFrontend/app/components/Breadcrumbs.tsx delete mode 100644 ExplorerFrontend/app/components/CopyAddressButton.tsx create mode 100644 ExplorerFrontend/app/components/CopyButton.tsx delete mode 100644 ExplorerFrontend/app/components/CopyHashButton.tsx create mode 100644 ExplorerFrontend/app/components/EmptyState.tsx delete mode 100644 ExplorerFrontend/app/components/Header.tsx delete mode 100644 ExplorerFrontend/app/components/Layout.tsx delete mode 100644 ExplorerFrontend/app/components/Pagination.tsx delete mode 100644 ExplorerFrontend/app/components/TokensList.tsx delete mode 100644 ExplorerFrontend/app/components/Transactions.tsx delete mode 100644 ExplorerFrontend/app/components/TransactionsDisplay.tsx create mode 100644 ExplorerFrontend/app/error.tsx delete mode 100644 ExplorerFrontend/app/lib/styles.ts diff --git a/.claude/agents/components.md b/.claude/agents/components.md new file mode 100644 index 0000000..d86521b --- /dev/null +++ b/.claude/agents/components.md @@ -0,0 +1,1262 @@ +# UI Component Reference + +Complete reference for 60 UI components with best practices, common layouts, and aliases. +Sourced from [component.gallery](https://component.gallery) and enriched with production-grade guidance. + +--- + +## Contents + +- [Accordion](#accordion) +- [Alert](#alert) +- [Avatar](#avatar) +- [Badge](#badge) +- [Breadcrumbs](#breadcrumbs) +- [Button](#button) +- [Button group](#button-group) +- [Card](#card) +- [Carousel](#carousel) +- [Checkbox](#checkbox) +- [Color picker](#color-picker) +- [Combobox](#combobox) +- [Date input](#date-input) +- [Datepicker](#datepicker) +- [Drawer](#drawer) +- [Dropdown menu](#dropdown-menu) +- [Empty state](#empty-state) +- [Fieldset](#fieldset) +- [File](#file) +- [File upload](#file-upload) +- [Footer](#footer) +- [Form](#form) +- [Header](#header) +- [Heading](#heading) +- [Hero](#hero) +- [Icon](#icon) +- [Image](#image) +- [Label](#label) +- [Link](#link) +- [List](#list) +- [Modal](#modal) +- [Navigation](#navigation) +- [Pagination](#pagination) +- [Popover](#popover) +- [Progress bar](#progress-bar) +- [Progress indicator](#progress-indicator) +- [Quote](#quote) +- [Radio button](#radio-button) +- [Rating](#rating) +- [Rich text editor](#rich-text-editor) +- [Search input](#search-input) +- [Segmented control](#segmented-control) +- [Select](#select) +- [Separator](#separator) +- [Skeleton](#skeleton) +- [Skip link](#skip-link) +- [Slider](#slider) +- [Spinner](#spinner) +- [Stack](#stack) +- [Stepper](#stepper) +- [Table](#table) +- [Tabs](#tabs) +- [Text input](#text-input) +- [Textarea](#textarea) +- [Toast](#toast) +- [Toggle](#toggle) +- [Tooltip](#tooltip) +- [Tree view](#tree-view) +- [Video](#video) +- [Visually hidden](#visually-hidden) + +--- + +## Accordion + +**Also known as:** Arrow toggle · Collapse · Collapsible sections · Collapsible · Details · Disclosure · Expandable · Expander + +A vertically stacked set of collapsible sections — each heading toggles between showing a short label and revealing the full content beneath it. + +**Best practices:** +- Use for long-form content that benefits from progressive disclosure +- Keep headings concise and scannable — they are the primary navigation +- Allow multiple sections open simultaneously unless space is critically limited +- Include a subtle expand/collapse icon (chevron) aligned consistently on the right +- Animate open/close with a short ease-out transition (150–250 ms) +- Ensure keyboard navigation: Enter/Space toggles, arrow keys move between headers + +**Common layouts:** +- FAQ page with stacked question/answer pairs +- Settings panel with grouped preference sections +- Sidebar filter groups in e-commerce or dashboards +- Mobile navigation with expandable menu sections + +--- + +## Alert + +**Also known as:** Notification · Feedback · Message · Banner · Callout + +A prominent message used to communicate important information or status changes to the user. + +**Best practices:** +- Use semantic color coding: red for errors, amber for warnings, green for success, blue for info +- Include a clear, actionable message — not just a status label +- Provide a dismiss action for non-critical alerts +- Position inline alerts close to the relevant content, not floating arbitrarily +- Use an icon alongside color to ensure accessibility for color-blind users +- Keep alert text to one or two sentences maximum + +**Common layouts:** +- Top-of-page banner for system-wide announcements +- Inline form validation message beneath an input field +- Toast notification stack in the bottom-right corner +- Contextual warning inside a card or settings section + +--- + +## Avatar + +A visual representation of a user, typically displayed as a photo, illustration, or initials. + +**Best practices:** +- Support three sizes: small (24–32 px), medium (40–48 px), large (64–80 px) +- Fall back gracefully: image → initials → generic icon +- Use a subtle ring or border to separate the avatar from its background +- For groups, stack avatars with a slight overlap and a '+N' overflow indicator +- Ensure the image is loaded lazily with a placeholder shimmer + +**Common layouts:** +- User profile header with name and role +- Comment thread with avatar beside each message +- Team member list with stacked avatar group +- Navigation bar user menu trigger + +--- + +## Badge + +**Also known as:** Tag · Label · Chip + +A compact label that sits within or near a larger component to convey status, category, or other metadata. + +**Best practices:** +- Keep badge text to one or two words — they are labels, not sentences +- Use a limited palette of badge colors mapped to clear semantics +- Ensure sufficient contrast between badge text and background (WCAG AA minimum) +- Use pill shape (fully rounded corners) for status badges, rounded rectangles for tags +- Avoid overusing badges — if everything is badged, nothing stands out + +**Common layouts:** +- Status indicator on a table row (Active, Pending, Archived) +- Tag cloud beneath a blog post or product card +- Notification count on a nav icon +- Feature label on a pricing tier card + +--- + +## Breadcrumbs + +**Also known as:** Breadcrumb trail + +A trail of links that shows where the current page sits within the site's navigational hierarchy. + +**Best practices:** +- Show the full hierarchy path; truncate middle segments on mobile with an ellipsis menu +- The current page should be the last item and should not be a link +- Use a subtle separator (/ or ›) with adequate spacing +- Place breadcrumbs near the top of the content area, below the header +- Keep breadcrumb text lowercase or sentence-case for readability + +**Common layouts:** +- E-commerce category → subcategory → product page +- Documentation site section navigation +- Dashboard drill-down from overview to detail view +- File manager path display + +--- + +## Button + +An interactive control that triggers an action — submitting a form, opening a dialog, toggling visibility. + +**Best practices:** +- Establish a clear visual hierarchy: primary (filled), secondary (outlined), tertiary (text-only) +- Use verb-first labels: 'Save changes', 'Create project', not 'Okay' or 'Submit' +- Minimum touch target of 44×44 px; desktop buttons at least 36 px tall +- Show a loading spinner inside the button during async actions — disable to prevent double-clicks +- Limit to one primary button per visible viewport section +- Ensure focus ring is visible and high-contrast for keyboard users + +**Common layouts:** +- Form footer with primary action right-aligned and secondary action left-aligned +- Hero CTA button centered or left-aligned beneath headline +- Dialog footer with Cancel (secondary) and Confirm (primary) +- Floating action button (FAB) in bottom-right for mobile creation flows + +--- + +## Button group + +**Also known as:** Toolbar + +A container that groups related buttons together as a single visual unit. + +**Best practices:** +- Group only related actions — unrelated buttons should be separated +- Visually connect buttons with shared border or tight spacing (1–2 px gap) +- Clearly indicate the active/selected state in toggle-style groups +- Keep the group to 2–5 buttons; more options warrant a dropdown or overflow menu + +**Common layouts:** +- Text editor toolbar (bold, italic, underline) +- View switcher (grid view, list view) +- Segmented date range selector (Day, Week, Month) +- Split button with primary action and a dropdown for alternatives + +--- + +## Card + +**Also known as:** Tile + +A self-contained content block representing a single entity such as a contact, article, or task. + +**Best practices:** +- Use a single, clear visual hierarchy within each card: media → title → meta → action +- Keep cards a consistent height in grid layouts — use line clamping for variable text +- Make the entire card clickable when it represents a navigable entity +- Use subtle elevation (shadow) or a border — not both simultaneously +- Limit card content to essential info; let the detail page carry the rest + +**Common layouts:** +- Product grid with image, title, price, and CTA +- Blog post feed with thumbnail, headline, excerpt, and date +- Dashboard KPI cards with metric, delta, and sparkline +- Team member directory with avatar, name, and role + +--- + +## Carousel + +**Also known as:** Content slider + +A component that cycles through multiple content slides, navigable via swipe, scroll, or button controls. + +**Best practices:** +- Provide visible navigation arrows and pagination dots +- Support swipe gestures on touch devices +- Auto-advance only if the user hasn't interacted; pause on hover/focus +- Show a peek of the next slide to signal scrollability +- Keep slide count manageable (3–7) — carousels with many slides have low engagement +- Ensure accessibility: each slide should be reachable via keyboard + +**Common layouts:** +- Hero image slideshow on a marketing homepage +- Product image gallery on a detail page +- Testimonial carousel with quote, author, and avatar +- Horizontal scrolling feature highlights in a mobile app + +--- + +## Checkbox + +A selection control — use in groups for multi-select from a list, or standalone for a single on/off choice. + +**Best practices:** +- Use checkboxes for multi-select, not single toggles (use a switch for on/off) +- Align the checkbox to the first line of its label, not the center +- Support indeterminate state for 'select all' when children are partially selected +- Minimum 44 px touch target including label area +- Group related checkboxes under a fieldset with a legend for accessibility + +**Common layouts:** +- Filter panel with multi-select facets +- Terms & conditions single checkbox with long label +- To-do list with check/uncheck per item +- Table row multi-select with header 'select all' + +--- + +## Color picker + +A control that lets users select a color value. + +**Best practices:** +- Provide a spectrum picker, hue slider, and direct hex/RGB input +- Include a set of preset swatches for quick selection +- Show a real-time preview of the selected color +- Support copy-paste of hex/RGB/HSL values +- Remember recently used colors for convenience + +**Common layouts:** +- Design tool color picker with spectrum, sliders, and input fields +- Theme customizer with preset palette and custom override +- Annotation tool with color swatch row +- Brand settings with primary/secondary/accent color pickers + +--- + +## Combobox + +**Also known as:** Autocomplete · Autosuggest + +A select-like input enhanced with a free-text field that filters available options as you type. + +**Best practices:** +- Show suggestions after 1–2 characters to reduce noise +- Highlight matched text within each suggestion for scannability +- Allow keyboard navigation (arrow keys + Enter) through the dropdown +- Show a 'no results' message instead of an empty dropdown +- Debounce input to avoid excessive API calls (200–300 ms) + +**Common layouts:** +- Search bar with autocomplete suggestions +- Address input with location suggestions +- Tag input that suggests existing tags +- Assignee picker in a project management tool + +--- + +## Date input + +A date entry control, often split into separate day, month, and year fields. + +**Best practices:** +- Clearly label the expected format (DD/MM/YYYY or MM/DD/YYYY) +- Use separate fields for day, month, and year for unambiguous entry +- Validate in real-time and show errors inline +- Support auto-advancing between fields when a segment is complete + +**Common layouts:** +- Date of birth entry in a registration form +- Passport/ID expiry date input +- Invoice date field in a financial form + +--- + +## Datepicker + +**Also known as:** Calendar · Datetime picker + +A calendar-based control for selecting dates visually. + +**Best practices:** +- Allow both manual text entry and calendar selection +- Clearly indicate the expected date format (e.g., MM/DD/YYYY) +- Highlight today's date and the currently selected date +- Disable dates outside the valid range +- Support keyboard navigation through the calendar grid +- For date ranges, show both start and end in a connected picker + +**Common layouts:** +- Booking flow with check-in / check-out range picker +- Form field with calendar dropdown on focus +- Dashboard date range filter in a toolbar +- Event creation form with start date and optional end date + +--- + +## Drawer + +**Also known as:** Tray · Flyout · Sheet + +A panel that slides in from a screen edge to reveal secondary content or actions. + +**Best practices:** +- Use drawers for secondary content or focused sub-tasks that don't warrant a full page +- Slide in from the right for detail panels, from the left for navigation +- Include a clear close button and support Escape to dismiss +- Dim the background with a semi-transparent overlay to establish focus +- Width should be 320–480 px on desktop; full-width on mobile + +**Common layouts:** +- Mobile navigation menu sliding in from the left +- Shopping cart preview panel from the right +- Detail/edit panel in a master-detail layout +- Notification center sliding in from the right + +--- + +## Dropdown menu + +**Also known as:** Select menu + +A menu triggered by a button that reveals a list of actions or navigation options — unlike a select, it is not a form input. + +**Best practices:** +- Group related items with separators and optional group headings +- Support keyboard navigation: arrow keys to move, Enter to select, Escape to close +- Keep the menu to 7±2 items; use sub-menus or search for longer lists +- Position the menu to avoid viewport overflow — flip to top if near bottom edge +- Indicate destructive actions in red and place them last, separated + +**Common layouts:** +- User account menu in the top-right navigation +- Context menu on right-click or kebab icon +- Action menu on a table row (Edit, Duplicate, Delete) +- Sort/filter dropdown in a toolbar + +--- + +## Empty state + +A placeholder shown when a view has no data to display, typically paired with a helpful action or suggestion. + +**Best practices:** +- Include a clear illustration or icon to soften the empty feeling +- Write a helpful headline explaining the empty state +- Provide a primary CTA that guides the user toward the next step +- Avoid blame — frame it positively ('No projects yet' not 'You have no projects') +- Show the empty state in-place within the container, not as a full-page takeover + +**Common layouts:** +- Empty dashboard with 'Create your first project' CTA +- Search results page with 'No results found' and suggestions +- Empty inbox with illustration and encouraging message +- Empty table with inline prompt to add data + +--- + +## Fieldset + +A container that groups related form fields under a shared label or legend. + +**Best practices:** +- Use fieldsets to group related form fields under a descriptive legend +- Style the legend as a section heading within the form +- Ensure the fieldset is announced by screen readers for context + +**Common layouts:** +- Address section grouping street, city, state, and zip fields +- Payment information section with card number, expiry, and CVV +- Personal details section in a multi-part form + +--- + +## File + +**Also known as:** Attachment · Download + +A visual representation of a file — such as an uploaded attachment or a downloadable document. + +**Best practices:** +- Show file type icon, name, and size clearly +- Include a download action and optionally a preview action +- Display upload date or last modified date +- Use a progress indicator during upload + +**Common layouts:** +- Attachment list below a message or form +- File card with icon, name, size, and download button +- Document grid with thumbnails and metadata + +--- + +## File upload + +**Also known as:** File input · File uploader · Dropzone + +A control that lets users select and upload files from their device. + +**Best practices:** +- Support drag-and-drop with a clearly defined drop zone +- Show accepted file types and size limits before upload +- Display upload progress with a progress bar per file +- Allow cancellation of in-progress uploads +- Show a preview (thumbnail for images, icon + name for documents) after selection +- Validate file type and size client-side before uploading + +**Common layouts:** +- Profile photo upload with circular crop preview +- Document attachment area in a form +- Multi-file drag-and-drop zone with file list below +- Inline file field with browse button and filename display + +--- + +## Footer + +A region at the bottom of a page or section containing copyright info, legal links, or secondary navigation. + +**Best practices:** +- Organize links into clear columns by category +- Include essential legal links: Privacy Policy, Terms of Service +- Keep the footer visually distinct but not distracting — muted background +- Include social links and a newsletter signup if appropriate +- Ensure the footer is accessible and links are keyboard-navigable + +**Common layouts:** +- Multi-column footer with link groups, logo, and copyright +- Minimal SaaS footer with product links and social icons +- E-commerce footer with help, shipping, returns, and payment icons +- Single-line footer with copyright and key legal links + +--- + +## Form + +A collection of input controls that allows users to enter and submit structured data. + +**Best practices:** +- Use a single-column layout for most forms — it's faster to scan +- Place labels above inputs for mobile-friendly forms +- Group related fields with visual proximity and optional fieldset headings +- Show inline validation on blur, not on every keystroke +- Disable the submit button until required fields are valid, or show clear errors on submit +- Keep forms as short as possible — ask only what's necessary + +**Common layouts:** +- Sign-up form with name, email, password, and CTA +- Multi-step wizard form with progress indicator +- Settings form with grouped preference sections +- Contact form with name, email, subject, and message textarea + +--- + +## Header + +The persistent top-of-page region containing the site brand, primary navigation, and key actions. + +**Best practices:** +- Keep the header height compact (56–72 px) to preserve content space +- Place the logo/brand on the left, primary navigation in the center or right +- Use a sticky header on long pages but consider auto-hide on scroll-down +- Ensure the mobile header collapses into a hamburger menu gracefully +- Maintain clear visual separation from page content (border-bottom or subtle shadow) + +**Common layouts:** +- SaaS app header with logo, nav links, search, and user avatar +- Marketing site header with logo, nav links, and CTA button +- Dashboard header with breadcrumbs, page title, and action buttons +- Minimal header with centered logo and hamburger menu + +--- + +## Heading + +A title element that introduces and labels a content section. + +**Best practices:** +- Use a strict heading hierarchy (h1 → h2 → h3) for accessibility and SEO +- Limit to one h1 per page — it's the page title +- Keep headings concise and descriptive — they're the outline of your content +- Use consistent sizing, weight, and spacing across heading levels + +**Common layouts:** +- Page title (h1) with section headings (h2) and subsections (h3) +- Card title as an h3 within a page section +- Dashboard section headers separating widget groups + +--- + +## Hero + +**Also known as:** Jumbotron · Banner + +A prominent banner near the top of a page, typically featuring a full-width image or illustration with a headline. + +**Best practices:** +- Lead with a compelling headline — clarity over cleverness +- Limit to one primary CTA and optionally one secondary CTA +- Use a high-quality image or illustration that reinforces the message +- Ensure text contrast against the background image (overlay or safe text zone) +- Keep hero height proportional — it should invite scrolling, not dominate the viewport + +**Common layouts:** +- Split hero: headline + CTA on left, product screenshot on right +- Full-bleed background image with centered text overlay +- Minimal hero with large headline, subtext, and inline email capture +- Video background hero with centered headline and play button + +--- + +## Icon + +A small graphic symbol that communicates the purpose or meaning of an interface element at a glance. + +**Best practices:** +- Use a consistent icon style throughout the product (outlined or filled, not mixed) +- Size icons to align with adjacent text (typically 16–24 px) +- Pair icons with text labels for clarity — icon-only buttons need tooltips +- Use aria-hidden='true' for decorative icons and aria-label for functional ones + +**Common layouts:** +- Navigation item with icon + label +- Action button with icon + text ('Download report') +- Status indicator icon beside a label (check, warning, error) +- Icon-only toolbar with tooltips + +--- + +## Image + +**Also known as:** Picture + +A component for displaying embedded images within a page. + +**Best practices:** +- Always provide meaningful alt text for accessibility +- Use responsive images (srcset) to serve appropriate sizes +- Lazy-load images below the fold for performance +- Reserve space for images before they load to prevent layout shift +- Use modern formats (WebP, AVIF) with fallbacks + +**Common layouts:** +- Hero banner with full-width background image +- Product image gallery with thumbnails and zoom +- Blog post featured image above the title or below the headline +- Avatar or profile photo in a circular frame + +--- + +## Label + +**Also known as:** Form label + +A text element that identifies and describes a form input. + +**Best practices:** +- Always associate labels with their form inputs (htmlFor / id pairing) +- Place labels above the input for vertical forms, beside for horizontal +- Mark required fields clearly (asterisk or 'required' text) +- Keep label text concise — use helper text for additional guidance + +**Common layouts:** +- Form field with label above and helper text below +- Inline label beside a toggle or checkbox +- Floating label that moves to the top on input focus + +--- + +## Link + +**Also known as:** Anchor · Hyperlink + +A clickable reference to another resource — either an external page or a location within the current document. + +**Best practices:** +- Make link text descriptive — avoid 'click here' or 'learn more' in isolation +- Underline links in body text for discoverability; nav links may rely on context +- Use a distinct color from surrounding text (but avoid pure blue if it clashes with your palette) +- Show a visited state for content-heavy pages to aid navigation +- External links should indicate they open in a new tab (icon or aria-label) + +**Common layouts:** +- Inline text link within a paragraph +- Standalone link beneath a card or section as a 'read more' action +- Footer link columns for site navigation +- Breadcrumb links in a hierarchy path + +--- + +## List + +A component that groups related items into an ordered or unordered sequence. + +**Best practices:** +- Use consistent vertical rhythm — equal spacing between list items +- For interactive lists, ensure each row has a clear hover and active state +- Include dividers between items in dense lists; omit them in spacious ones +- Support keyboard navigation when the list is interactive +- Use virtualization (windowing) for lists exceeding ~100 items + +**Common layouts:** +- Email inbox with sender, subject, preview, and timestamp per row +- Settings list with label, value/toggle, and optional chevron +- Activity feed with avatar, description, and relative timestamp +- File list with icon, name, size, and date columns + +--- + +## Modal + +**Also known as:** Dialog · Popup · Modal window + +An overlay that demands the user's attention — interaction is required before returning to the content beneath. + +**Best practices:** +- Use modals sparingly — only for actions that require immediate attention or focused input +- Always provide a clear close mechanism: X button, Cancel, and Escape key +- Trap focus within the modal while it's open for accessibility +- Return focus to the trigger element when the modal closes +- Keep modal content concise — if it needs scrolling, consider a full page instead +- Use a semi-transparent backdrop to dim the underlying content + +**Common layouts:** +- Confirmation dialog with message and two action buttons +- Form modal for quick data entry (create, edit) +- Image/media preview lightbox +- Onboarding or announcement modal with illustration and CTA + +--- + +## Navigation + +**Also known as:** Nav · Menu + +A region containing links for moving between pages or jumping to sections within the current page. + +**Best practices:** +- Limit primary navigation to 5–7 items; group the rest under 'More' or sub-menus +- Clearly indicate the current/active page in the navigation +- Use consistent iconography alongside text labels for scannability +- Collapse to a hamburger or bottom tab bar on mobile +- Ensure all navigation items are reachable via keyboard (Tab + Enter) + +**Common layouts:** +- Horizontal top nav with logo, links, and user menu +- Vertical sidebar navigation with icon + label and collapsible groups +- Bottom tab bar for mobile apps (Home, Search, Create, Notifications, Profile) +- Mega-menu dropdown with categorized link columns + +--- + +## Pagination + +A control for navigating between pages of content when data is split across multiple views. + +**Best practices:** +- Show first, last, and a window of pages around the current one +- Use ellipsis to indicate skipped pages, not dozens of page numbers +- Provide Previous/Next buttons in addition to numbered pages +- Clearly style the current page as selected +- Consider infinite scroll or 'Load more' for content feeds + +**Common layouts:** +- Table footer with page numbers, rows-per-page selector, and total count +- Search results pagination centered below the results list +- Blog archive with Previous/Next navigation +- API documentation with page controls at top and bottom + +--- + +## Popover + +A floating panel that appears on click near its trigger element — unlike a tooltip, it can contain interactive content. + +**Best practices:** +- Trigger via click, not hover, to support touch devices and accessibility +- Position intelligently to avoid clipping at viewport edges +- Include a subtle arrow/caret pointing to the trigger element +- Dismiss when clicking outside or pressing Escape +- Keep popover content brief — it's not a modal + +**Common layouts:** +- Color picker dropdown triggered by a swatch +- User profile preview card on avatar hover/click +- Quick-edit popover for inline data modification +- Help tooltip with rich content (text + link) + +--- + +## Progress bar + +**Also known as:** Progress + +A horizontal indicator showing how far a long-running task has progressed toward completion. + +**Best practices:** +- Show a determinate bar when progress is measurable, indeterminate when unknown +- Include a percentage label for accessibility and clarity +- Use color to indicate state: blue/green for normal, red for error, amber for warning +- Animate smoothly — avoid jarring jumps between values +- Keep the bar visually proportional to its container (not too thin to see) + +**Common layouts:** +- File upload progress beneath the file name +- Onboarding completion bar in a sidebar or header +- Course progress bar at the top of a lesson page +- System resource usage bar in a monitoring dashboard + +--- + +## Progress indicator + +**Also known as:** Progress tracker · Stepper · Steps · Timeline · Meter + +A visual display of how far a user has advanced through a multi-step process. + +**Best practices:** +- Clearly distinguish completed, current, and upcoming steps +- Use numbered or labeled steps — not just dots +- Allow users to click back to completed steps if the flow permits +- Keep the total step count visible so users know the scope +- Vertically stack steps on mobile for readability + +**Common layouts:** +- Multi-step checkout (Cart → Shipping → Payment → Confirmation) +- Account setup wizard with profile, preferences, and verification +- Application form with multiple sections +- Project timeline with milestones + +--- + +## Quote + +**Also known as:** Pull quote · Block quote + +A styled block for displaying quotations — from a person, an external source, or a highlighted passage. + +**Best practices:** +- Use a distinct visual treatment — large quotation marks, left border, or italic text +- Always attribute the quote to its source +- Keep pull quotes short — they're attention-grabbers, not paragraphs + +**Common layouts:** +- Testimonial block with photo, quote, name, and title +- Pull quote in a blog post breaking up long text +- Customer quote in a case study with company logo + +--- + +## Radio button + +**Also known as:** Radio · Radio group + +A selection control where the user picks exactly one option from a predefined set. + +**Best practices:** +- Use radio buttons for mutually exclusive choices (select one from many) +- Always pre-select a sensible default when possible +- Group under a fieldset with a legend describing the choice +- Stack vertically for more than 2 options — horizontal only for 2–3 short-label options +- Provide sufficient spacing between options (at least 8 px) for easy tapping + +**Common layouts:** +- Shipping method selection (Standard, Express, Overnight) +- Payment method chooser with radio + icon + description +- Survey question with single-choice answers +- Plan/tier selection in a pricing form + +--- + +## Rating + +A control that displays or captures a star-based score for a product or item. + +**Best practices:** +- Use 5-star scale as the widely understood standard +- Allow half-star precision for display; use full stars for input +- Show the average rating and total review count together +- Use filled/empty stars with sufficient color contrast + +**Common layouts:** +- Product rating display with stars and review count +- Review submission with interactive star input and text area +- Summary rating card with distribution bar chart + +--- + +## Rich text editor + +**Also known as:** RTE · WYSIWYG editor + +A WYSIWYG editing surface for creating and formatting rich text content. + +**Best practices:** +- Provide a minimal default toolbar — reveal advanced formatting on demand +- Support keyboard shortcuts for common formatting (Cmd+B, Cmd+I) +- Ensure pasted content is sanitized to prevent layout-breaking HTML +- Show a word/character count for content with limits + +**Common layouts:** +- Blog post editor with formatting toolbar and preview +- Email composer with rich text and attachment support +- Comment editor with basic formatting (bold, italic, link, list) + +--- + +## Search input + +**Also known as:** Search + +A text field designed for entering search queries to find content. + +**Best practices:** +- Place a magnifying glass icon inside the field to signal purpose +- Support Cmd/Ctrl+K as a global shortcut to focus the search +- Show recent searches and suggested queries in a dropdown +- Debounce input and show a loading indicator during server queries +- Provide a clear/reset button (×) once text is entered + +**Common layouts:** +- Global search in the top navigation bar +- Command palette overlay (Cmd+K) with categorized results +- Inline search/filter above a data table +- Full-page search with prominent input and categorized results below + +--- + +## Segmented control + +**Also known as:** Toggle button group + +A compact row of mutually exclusive options — a hybrid of button groups, radio buttons, and tabs for switching views. + +**Best practices:** +- Limit to 2–5 segments — more options warrant tabs or a dropdown +- Use equal-width segments for visual balance +- Animate the selection indicator sliding between options +- Ensure the selected state has strong contrast against unselected +- Use sentence case for segment labels + +**Common layouts:** +- Map/list/grid view switcher +- Billing period toggle (Monthly / Annually) +- Light/dark mode toggle in settings +- Chart type selector (Line, Bar, Pie) + +--- + +## Select + +**Also known as:** Dropdown · Select input + +A form input that shows the current selection when collapsed and reveals a scrollable option list when expanded. + +**Best practices:** +- Use native select for simple use cases (better accessibility and mobile UX) +- For custom selects, ensure full keyboard support and ARIA attributes +- Show a placeholder label ('Select an option…') when no value is chosen +- Group long option lists with optgroups or headings +- For searchable selects with many options, combine with combobox behavior + +**Common layouts:** +- Country/region picker in an address form +- Sort-by dropdown in a product listing toolbar +- Role selector in a user invitation flow +- Language/locale switcher + +--- + +## Separator + +**Also known as:** Divider · Horizontal rule · Vertical rule + +A visual divider — typically a horizontal or vertical line — used to separate content sections. + +**Best practices:** +- Use subtle, low-contrast separators — they guide the eye, not dominate it +- Prefer spacing over separators when grouping is already clear +- Use horizontal rules between content sections, vertical rules between columns + +**Common layouts:** +- Horizontal divider between list items or content sections +- Vertical separator between sidebar and main content +- Section divider with centered label ('or', 'related content') + +--- + +## Skeleton + +**Also known as:** Skeleton loader + +A low-fidelity placeholder that mimics the shape of content while it loads, typically rendered as grey blocks. + +**Best practices:** +- Match the skeleton shape to the actual content layout as closely as possible +- Use a subtle shimmer/pulse animation to indicate loading — not a spinner +- Avoid skeletons for very fast loads (<300 ms) — they add visual noise +- Show skeleton immediately on navigation; replace atomically when data arrives +- Use muted, low-contrast colors (light gray on white) for skeleton blocks + +**Common layouts:** +- Card grid skeleton with image placeholder, title bar, and text lines +- List/feed skeleton with repeating row shapes +- Profile page skeleton with avatar circle and text blocks +- Dashboard skeleton with chart placeholder and metric blocks + +--- + +## Skip link + +Hidden navigation links that let keyboard users jump directly to the main content, bypassing repeated elements. + +**Best practices:** +- Make it the first focusable element in the DOM +- Visually hidden until focused — then clearly visible +- Link to the main content area with a descriptive label ('Skip to main content') + +**Common layouts:** +- Hidden link that appears on Tab focus at the very top of the page + +--- + +## Slider + +**Also known as:** Range input + +A draggable control for selecting a value from within a defined range. + +**Best practices:** +- Show the current value in a tooltip or adjacent label +- Use tick marks for discrete value sliders +- Support both dragging and clicking on the track to set value +- Ensure minimum touch target size for the thumb (44 px) +- Pair with a text input for precise value entry when needed + +**Common layouts:** +- Price range filter with dual thumbs (min/max) +- Volume/brightness control slider +- Image crop zoom level control +- Pricing page seat/usage slider with dynamic price display + +--- + +## Spinner + +**Also known as:** Loader · Loading + +An animated indicator showing that a background process is running and the interface isn't yet interactive. + +**Best practices:** +- Show spinners only after a delay (~300 ms) to avoid flicker on fast responses +- Size the spinner proportionally to the context: inline (16 px), button (20 px), page (40+ px) +- Use a single brand-consistent spinner design throughout the app +- Provide an aria-label or sr-only text for screen readers ('Loading…') +- Prefer skeleton screens over spinners when the layout is predictable + +**Common layouts:** +- Centered full-page spinner during initial app load +- Inline spinner inside a button during form submission +- Small spinner beside a table cell during lazy-loaded data fetch +- Overlay spinner on a card while its content refreshes + +--- + +## Stack + +A layout utility that applies uniform spacing between its child components. + +**Best practices:** +- Use a consistent spacing scale (4, 8, 12, 16, 24, 32, 48 px) +- Default to vertical stacking; support horizontal for inline element groups +- Use stack as a layout primitive to enforce consistent spacing across components + +**Common layouts:** +- Vertical stack of form fields with uniform gap +- Horizontal stack of action buttons with gap +- Card content layout with vertical stack of title, description, and meta + +--- + +## Stepper + +**Also known as:** Nudger · Quantity · Counter + +A numeric input with increment and decrement buttons for adjusting a value. + +**Best practices:** +- Use clear +/- buttons with adequate touch targets +- Allow direct number entry in addition to button interaction +- Set sensible min, max, and step values +- Disable the relevant button when at min or max value + +**Common layouts:** +- Quantity selector in an e-commerce cart +- Number input for seat count in a booking flow +- Portion size adjuster in a recipe app + +--- + +## Table + +A structured grid of rows and columns for displaying data — often called a data table when it supports sorting and filtering. + +**Best practices:** +- Use a sticky header row for scrollable tables +- Right-align numeric columns for easy comparison +- Provide sortable column headers with clear sort direction indicators +- Alternate row colors (zebra striping) or use horizontal dividers for readability +- Include a bulk-select checkbox column for actionable tables +- Make tables horizontally scrollable on mobile rather than hiding columns + +**Common layouts:** +- Admin data table with search, filters, sort, pagination, and row actions +- Pricing comparison table with feature rows and plan columns +- Financial ledger with date, description, amount, and running balance +- Leaderboard table with rank, name, avatar, and score + +--- + +## Tabs + +**Also known as:** Tabbed interface + +A set of selectable labels that switch between content panels, keeping the layout compact. + +**Best practices:** +- Limit to 2–7 tabs; more options need a scrollable tab bar or dropdown overflow +- Clearly indicate the active tab with a bottom border, background fill, or bold text +- Use short, descriptive tab labels (1–2 words) +- Place tab content immediately below the tab bar with no visual gap +- Support keyboard navigation: arrow keys between tabs, Tab to content +- Consider swapping tabs for an accordion on narrow viewports + +**Common layouts:** +- Product page with Description, Reviews, and Specifications tabs +- Settings page with General, Security, Notifications sections +- Profile page with Activity, Projects, and Settings tabs +- Dashboard with different report views (Overview, Analytics, Logs) + +--- + +## Text input + +A single-line form field for entering short text values. + +**Best practices:** +- Use appropriate input types (email, tel, url, number) for mobile keyboard optimization +- Show placeholder text only as an example format, never as a label replacement +- Display character count for length-limited fields +- Show inline validation errors below the input with a red border and message +- Support autofill attributes for common fields (name, email, address) + +**Common layouts:** +- Login form with email and password inputs +- Search bar with icon prefix and clear button +- Inline edit field that converts from text to input on click +- Settings form with labeled text inputs in a single column + +--- + +## Textarea + +**Also known as:** Textbox · Text box + +A multi-line text field for longer content entry. + +**Best practices:** +- Allow vertical resizing but consider setting a min and max height +- Show character count if there's a limit +- Use a taller default height (3–5 rows) to signal multi-line input is expected +- Auto-grow the textarea as the user types for a smoother experience + +**Common layouts:** +- Comment or reply input below a post +- Feedback form with a large message area +- Note-taking field in a CRM or project tool +- Code or JSON input with monospace font + +--- + +## Toast + +**Also known as:** Snackbar + +A brief, non-blocking notification that appears in a floating layer above the interface. + +**Best practices:** +- Auto-dismiss after 4–6 seconds for non-critical toasts +- Allow manual dismissal with a close button or swipe +- Stack multiple toasts with the newest on top +- Position in a consistent corner — bottom-right is most common for desktop +- Include an action link for undoable operations ('Undo' for delete) +- Limit to one line of text — toasts are for brief confirmations + +**Common layouts:** +- Success toast after saving a form ('Changes saved') +- Error toast with retry action after a failed request +- Undo toast after deleting an item ('Item deleted. Undo') +- Notification toast with avatar and brief message preview + +--- + +## Toggle + +**Also known as:** Switch · Lightswitch · Toggle button + +A binary switch control that toggles between two states — typically on and off. + +**Best practices:** +- Use for binary on/off settings that take effect immediately +- Label the toggle with what it controls, not 'On/Off' +- Show the current state visually (color, position) and with an optional text label +- Size the toggle to be easily tappable (44+ px wide) +- Avoid using toggles inside forms that require a Save action — use checkboxes instead + +**Common layouts:** +- Settings row with label on the left and toggle on the right +- Dark mode toggle in a header or settings panel +- Feature flag toggles in an admin panel +- Notification preference toggles in a list + +--- + +## Tooltip + +**Also known as:** Toggletip + +A small floating label that reveals supplementary information about an element, typically on hover. + +**Best practices:** +- Use tooltips for supplementary info — never for essential content +- Trigger on hover (desktop) and long-press (mobile); avoid click-to-show +- Show after a short delay (~300 ms) and hide on mouse leave +- Keep tooltip text to a single sentence or a few words +- Position to avoid obscuring the trigger element or important content +- Use a toggletip (click-triggered) when the content includes interactive elements + +**Common layouts:** +- Icon button tooltip showing the action name +- Truncated text tooltip revealing the full string on hover +- Info icon tooltip explaining a form field's purpose +- Chart data point tooltip showing exact values + +--- + +## Tree view + +A collapsible, nested hierarchy for browsing structured data like file trees or category taxonomies. + +**Best practices:** +- Use indentation (16–24 px per level) to show hierarchy +- Include expand/collapse toggles (chevron or triangle) for parent nodes +- Support keyboard navigation: arrows to traverse, Enter to select, +/- to expand/collapse +- Highlight the selected node and show a focus indicator +- Lazy-load deep children for performance in large trees + +**Common layouts:** +- File/folder browser in a code editor or CMS +- Category tree in an e-commerce sidebar +- Organization chart or reporting hierarchy +- Table of contents navigation for documentation + +--- + +## Video + +**Also known as:** Video player + +A media component for playing video content, typically with controls for playback, volume, and fullscreen. + +**Best practices:** +- Show a poster/thumbnail image before playback +- Include captions/subtitles for accessibility +- Provide standard controls: play/pause, volume, fullscreen, progress bar +- Lazy-load video content and avoid autoplay with sound + +**Common layouts:** +- Product demo video centered on a landing page +- Video player with title, description, and related videos +- Background video hero with muted autoplay +- Tutorial video embedded in documentation + +--- + +## Visually hidden + +**Also known as:** Screenreader only + +Content that is hidden visually but remains accessible to screen readers and other assistive technology. + +**Best practices:** +- Use for screen-reader-only text that provides context invisible users don't need +- Never use display:none or visibility:hidden — use a clip-rect technique +- Apply to skip links, icon-only button labels, and form field instructions + +**Common layouts:** +- Hidden label for an icon-only close button +- Screen-reader instructions for a complex widget + +--- diff --git a/.claude/agents/frontend-developer.md b/.claude/agents/frontend-developer.md index 56853ca..70047b5 100644 --- a/.claude/agents/frontend-developer.md +++ b/.claude/agents/frontend-developer.md @@ -5,28 +5,157 @@ tools: Read, Write, Edit, Bash model: sonnet --- -You are a frontend developer specializing in modern React applications and responsive design. +You are a senior frontend developer and UI designer specializing in modern React applications and responsive design. + +## UI Design Knowledge + +You have access to a comprehensive UI component reference at `.claude/agents/components.md` containing best practices, layout patterns, and design-system conventions for 60+ interface components. **Before writing any UI code**, read that file to select the right components and follow their best practices. + +## Design Philosophy + +Every generated interface should feel **modern, minimal, and production-ready** — not like a template. + +### Core Principles + +1. **Restraint over decoration.** Fewer elements, highly refined. White space is a feature. +2. **Typography carries hierarchy.** Maximize weight contrast between headings and labels. +3. **One strong color moment.** Neutral palette first (warm off-whites, near-blacks, muted mid-tones). One confident accent. +4. **Spacing is structure.** Use an 8px grid. Tighter gaps group related elements; generous gaps let hero content breathe. +5. **Accessibility is non-negotiable.** WCAG AA contrast minimums. Focus indicators. Semantic HTML. Keyboard navigation. +6. **No generic AI aesthetics.** Avoid: purple-on-white gradients, Inter/Roboto defaults, evenly-spaced card grids, and cookie-cutter layouts. Every interface should feel designed for its specific context. + +### Quality Bar + +Output should match what you'd expect from a senior product designer at a top SaaS company: +- Clean visual rhythm with intentional asymmetry +- Obvious interactive affordances (hover, focus, active states) +- Graceful edge cases (empty states, loading, error) +- Responsive without breakpoint artifacts + +## Workflow + +### Step 1 — Identify Components + +Read the user's request and determine which UI components are needed. Consult `.claude/agents/components.md` for each component by name or alias. + +Common mappings: +- "navigation" → Header, Navigation, Breadcrumbs, Tabs +- "form" → Form, Text input, Select, Checkbox, Radio button, Button +- "data display" → Table, Card, List, Badge, Avatar +- "feedback" → Alert, Toast, Modal, Spinner, Progress bar, Empty state +- "input" → Text input, Textarea, Select, Combobox, Datepicker, File upload, Slider +- "overlay" → Modal, Drawer, Popover, Tooltip, Dropdown menu + +### Step 2 — Apply Best Practices + +For each component, follow its best practices from the reference. Key rules that apply broadly: + +**Layout** +- Single-column forms — faster to scan +- Consistent vertical lanes in repeated rows (lists, tables) +- Fixed-width slots for icons and actions, even when empty +- Cards: media → title → meta → action hierarchy + +**Interaction** +- Buttons: verb-first labels ("Save changes", not "Submit"), one primary per section +- Modals: always provide X, Cancel, and Escape; trap focus; return focus on close +- Toasts: auto-dismiss 4–6s, allow manual dismiss, stack newest on top +- Toggles: immediate effect only — use checkboxes in forms that require Save + +**Typography & Spacing** +- Strict heading hierarchy (h1 → h2 → h3), one h1 per page +- Minimum 44px touch targets on mobile +- Labels above inputs (vertical forms) or beside (horizontal) +- Placeholder text as format hint, never as label replacement + +**States** +- Empty states: illustration + helpful headline + primary CTA +- Loading: skeleton screens > spinners (show after 300ms delay) +- Validation: inline on blur, not on every keystroke +- Disabled elements: visually distinct but still readable + +### Step 3 — Choose a Design Direction + +Select the style that best matches the user's intent, or ask if unclear: + +| Preset | When to use | +|--------|-------------| +| **Modern SaaS** (default) | Clean, spacious, professional — neutral palette, one strong accent, 8px grid | +| **Apple-level Minimal** | Near-monochrome, warm grays, large type hierarchy, abundant white space | +| **Enterprise / Corporate** | Information-dense, compact spacing (4/8/12/16/24px), fully keyboard-navigable | +| **Creative / Portfolio** | Bold, expressive, asymmetric layouts, editorial typography | +| **Data Dashboard** | Data-dense, consistent vertical alignment, clear metric hierarchy: KPI → trend → detail | + +### Step 4 — Generate Code + +Write production-ready code following these rules: + +- **Stack**: React + Tailwind CSS (unless user specifies otherwise) +- **Spacing**: Tailwind spacing scale on an 8px grid +- **Colors**: CSS variables or Tailwind config for palette consistency +- **Typography**: Tailwind text utilities; expressive font pairings +- **States**: Implement hover, focus, active, disabled for all interactive elements +- **Responsive**: Mobile-first; test at 375, 768, 1440px +- **Accessibility**: Semantic HTML, ARIA where needed, focus management + +## Component Quick Reference + +| Component | When to use | Key rule | +|-----------|------------|----------| +| **Button** | Trigger actions | Verb-first labels; one primary per section | +| **Card** | Represent an entity | Media → title → meta → action; shadow OR border, not both | +| **Modal** | Focused attention | Trap focus; X + Cancel + Escape to close | +| **Navigation** | Page/section links | 5–7 items max; clear active state | +| **Table** | Structured data | Sticky header; right-align numbers; sortable columns | +| **Tabs** | Switch panels | 2–7 tabs; active indicator; accordion on mobile | +| **Form** | Collect input | Single column; labels above; inline validation on blur | +| **Toast** | Brief confirmation | Auto-dismiss 4–6s; undo action for destructive ops | +| **Alert** | Important status | Semantic colors + icon; max 2 sentences | +| **Drawer** | Secondary panel | Right for detail, left for nav; 320–480px desktop | +| **Search input** | Find content | Cmd/Ctrl+K shortcut; debounce 200–300ms | +| **Empty state** | No data | Illustration + headline + CTA; positive framing | +| **Skeleton** | Loading placeholder | Match actual layout shape; shimmer animation | +| **Badge** | Status/metadata label | 1–2 words; pill shape for status; limited color palette | +| **Dropdown menu** | Action/nav options | 7±2 items; destructive actions last in red | + +## Anti-Patterns to Avoid + +Never generate these — they signal generic, low-quality UI: + +- **Rainbow badges** — every status a different bright color with no semantic meaning +- **Modal inside modal** — use a page or drawer for complex flows +- **Disabled submit with no explanation** — always indicate what's missing +- **Spinner for predictable layouts** — use skeleton screens instead +- **"Click here" links** — link text must describe the destination +- **Hamburger menu on desktop** — use visible navigation when space allows +- **Auto-advancing carousels** — let users control navigation +- **Placeholder-only form fields** — always use visible labels +- **Equal-weight buttons** — establish primary/secondary/tertiary hierarchy +- **Tiny text (< 12px)** — body text minimum 14px, prefer 16px ## Focus Areas + - React component architecture (hooks, context, performance) - Responsive CSS with Tailwind/CSS-in-JS -- State management (Redux, Zustand, Context API) +- State management (Redux, Zustand, Context API, MobX) - Frontend performance (lazy loading, code splitting, memoization) - Accessibility (WCAG compliance, ARIA labels, keyboard navigation) ## Approach -1. Component-first thinking - reusable, composable UI pieces -2. Mobile-first responsive design -3. Performance budgets - aim for sub-3s load times -4. Semantic HTML and proper ARIA attributes -5. Type safety with TypeScript when applicable + +1. **Component-first thinking** — reusable, composable UI pieces +2. **Mobile-first responsive design** +3. **Performance budgets** — aim for sub-3s load times +4. **Semantic HTML and proper ARIA attributes** +5. **Type safety with TypeScript** ## Output + - Complete React component with props interface - Styling solution (Tailwind classes or styled-components) - State management implementation if needed -- Basic unit test structure - Accessibility checklist for the component - Performance considerations and optimizations +- All interactive states (hover, focus, active, disabled, loading, empty, error) Focus on working code over explanations. Include usage examples in comments. diff --git a/ExplorerFrontend/app/address/[query]/address-client.tsx b/ExplorerFrontend/app/address/[query]/address-client.tsx index c9f38c4..4aaa33b 100644 --- a/ExplorerFrontend/app/address/[query]/address-client.tsx +++ b/ExplorerFrontend/app/address/[query]/address-client.tsx @@ -20,9 +20,7 @@ export default function AddressClient({ address }: AddressClientProps): JSX.Elem const fetchData = async (): Promise => { try { setIsLoading(true); - console.log('Fetching address data:', address); const response = await axios.get(`${config.handlerUrl}/address/aggregate/${address}`); - console.log('Raw API response:', JSON.stringify(response.data, null, 2)); // Process transactions to ensure gas values are in hex format if (response.data.transactions_by_address) { @@ -48,7 +46,6 @@ export default function AddressClient({ address }: AddressClientProps): JSX.Elem }; } - console.log('Processed transactions:', JSON.stringify(response.data.transactions_by_address, null, 2)); setAddressData(response.data); setError(null); } catch (error) { diff --git a/ExplorerFrontend/app/address/[query]/address-view.tsx b/ExplorerFrontend/app/address/[query]/address-view.tsx index c94f3bc..5aab592 100644 --- a/ExplorerFrontend/app/address/[query]/address-view.tsx +++ b/ExplorerFrontend/app/address/[query]/address-view.tsx @@ -1,13 +1,15 @@ 'use client'; import { useState, useEffect } from 'react'; -import CopyAddressButton from "../../components/CopyAddressButton"; +import CopyButton from "../../components/CopyButton"; import QRCodeButton from "../../components/QRCodeButton"; import TanStackTable from "../../components/TanStackTable"; import BalanceDisplay from "./balance-display"; import ActivityDisplay from "./activity-display"; import type { AddressData } from "@/app/types"; import Link from "next/link"; +import Breadcrumbs from "../../components/Breadcrumbs"; +import EmptyState from "../../components/EmptyState"; interface AddressViewProps { addressData: AddressData; @@ -84,6 +86,10 @@ export default function AddressView({ addressData, addressSegment }: AddressView return (
+
@@ -101,7 +107,7 @@ export default function AddressView({ addressData, addressSegment }: AddressView {addressSegment && (
- +
)} @@ -136,7 +142,7 @@ export default function AddressView({ addressData, addressSegment }: AddressView address={contractData.creatorAddress || 'Unknown'} /> {contractData.creatorAddress && ( - + )}
@@ -186,7 +192,7 @@ export default function AddressView({ addressData, addressSegment }: AddressView {contractData.creationTransaction} - + @@ -214,9 +220,12 @@ export default function AddressView({ addressData, addressSegment }: AddressView internalt={addressData.internal_transactions_by_address || []} /> ) : ( -
- No transactions found for this address -
+ )} diff --git a/ExplorerFrontend/app/address/[query]/balance-display.tsx b/ExplorerFrontend/app/address/[query]/balance-display.tsx index 3e8d76b..a34672e 100644 --- a/ExplorerFrontend/app/address/[query]/balance-display.tsx +++ b/ExplorerFrontend/app/address/[query]/balance-display.tsx @@ -3,6 +3,7 @@ import { formatAmount } from '../../lib/helpers'; import { STAKING_QUANTA } from '../../lib/constants'; import type { BalanceDisplayProps } from '@/app/types'; +import Badge from '../../components/Badge'; export default function BalanceDisplay({ balance }: BalanceDisplayProps): JSX.Element { const [formattedBalance, unit] = formatAmount(balance); @@ -17,8 +18,8 @@ export default function BalanceDisplay({ balance }: BalanceDisplayProps): JSX.El {unit} {balance > STAKING_QUANTA && ( -
- Qualified for Staking +
+ Qualified for Staking
)}
diff --git a/ExplorerFrontend/app/address/[query]/contract-display.tsx b/ExplorerFrontend/app/address/[query]/contract-display.tsx index 45cd3dd..722fa4f 100644 --- a/ExplorerFrontend/app/address/[query]/contract-display.tsx +++ b/ExplorerFrontend/app/address/[query]/contract-display.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { decodeToHex, formatAddress } from '../../lib/helpers'; -import CopyAddressButton from '../../components/CopyAddressButton'; +import CopyButton from '../../components/CopyButton'; interface ContractDisplayProps { contractCode: { @@ -25,7 +25,7 @@ export default function ContractDisplay({ contractCode }: ContractDisplayProps):
Creator Address
{creatorAddress} - +
diff --git a/ExplorerFrontend/app/address/[query]/page.tsx b/ExplorerFrontend/app/address/[query]/page.tsx index f46ec7a..db32a88 100644 --- a/ExplorerFrontend/app/address/[query]/page.tsx +++ b/ExplorerFrontend/app/address/[query]/page.tsx @@ -13,7 +13,7 @@ interface PageProps { export async function generateMetadata({ params }: { params: Promise<{ query: string }> }): Promise { const resolvedParams = await params; const address = resolvedParams.query; - const canonicalUrl = `https://zondscan.com/address`; + const canonicalUrl = `https://zondscan.com/address/${address}`; return { ...sharedMetadata, diff --git a/ExplorerFrontend/app/address/[query]/token-contract-view.tsx b/ExplorerFrontend/app/address/[query]/token-contract-view.tsx index 95bee8d..006706a 100644 --- a/ExplorerFrontend/app/address/[query]/token-contract-view.tsx +++ b/ExplorerFrontend/app/address/[query]/token-contract-view.tsx @@ -2,9 +2,10 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; -import CopyAddressButton from "../../components/CopyAddressButton"; +import CopyButton from "../../components/CopyButton"; import QRCodeButton from "../../components/QRCodeButton"; import { formatAmount } from "../../lib/helpers"; +import Breadcrumbs from "../../components/Breadcrumbs"; interface TokenInfo { contractAddress: string; @@ -227,6 +228,10 @@ export default function TokenContractView({ address, contractData, handlerUrl }: return (
+ {/* Token Header Card */}
@@ -246,7 +251,7 @@ export default function TokenContractView({ address, contractData, handlerUrl }:
{address} - +
@@ -319,7 +324,7 @@ export default function TokenContractView({ address, contractData, handlerUrl }:
{creatorAddress && ( - + )}
@@ -350,7 +355,7 @@ export default function TokenContractView({ address, contractData, handlerUrl }: {creationTxHash || 'Unknown'} {creationTxHash && ( - + )} @@ -432,13 +437,13 @@ export default function TokenContractView({ address, contractData, handlerUrl }: ) : ( <>
- +
- - - - + + + + @@ -478,6 +483,7 @@ export default function TokenContractView({ address, contractData, handlerUrl }:
#AddressBalanceShare#AddressBalanceShare
+
- - - - - + + + + + @@ -556,6 +563,7 @@ export default function TokenContractView({ address, contractData, handlerUrl }:
+ )} +
+ ) +} diff --git a/ExplorerFrontend/app/components/AreaChart.tsx b/ExplorerFrontend/app/components/AreaChart.tsx index d767e96..2c5ff92 100644 --- a/ExplorerFrontend/app/components/AreaChart.tsx +++ b/ExplorerFrontend/app/components/AreaChart.tsx @@ -36,6 +36,7 @@ const getStockValue = (d: Block): number => d.result.size; export default function AreaChart({ data, gradientColor, + gradientId = 'area-gradient', width, yMax, margin, @@ -49,6 +50,7 @@ export default function AreaChart({ }: { data: Block[]; gradientColor: string; + gradientId?: string; xScale: AxisScale; yScale: AxisScale; width: number; @@ -64,7 +66,7 @@ export default function AreaChart({ return ( yScale(getStockValue(d)) || 0} yScale={yScale} strokeWidth={1} - stroke="url(#gradient)" - fill="url(#gradient)" + stroke={`url(#${gradientId})`} + fill={`url(#${gradientId})`} curve={curveMonotoneX} /> {!hideBottomAxis && ( diff --git a/ExplorerFrontend/app/components/Badge.tsx b/ExplorerFrontend/app/components/Badge.tsx new file mode 100644 index 0000000..e75c8b3 --- /dev/null +++ b/ExplorerFrontend/app/components/Badge.tsx @@ -0,0 +1,38 @@ +interface BadgeProps { + variant: 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'brand' + children: React.ReactNode + size?: 'sm' | 'md' + dot?: boolean +} + +const VARIANT_STYLES = { + success: 'bg-green-900/30 text-green-400 border-green-800', + warning: 'bg-yellow-900/30 text-yellow-400 border-yellow-800', + error: 'bg-red-900/30 text-red-400 border-red-800', + info: 'bg-blue-900/30 text-blue-400 border-blue-800', + neutral: 'bg-gray-900/30 text-gray-400 border-gray-700', + brand: 'bg-[#ffa729]/20 text-[#ffa729] border-[#ffa729]/30', +} as const + +const DOT_COLORS = { + success: 'bg-green-400', + warning: 'bg-yellow-400', + error: 'bg-red-400', + info: 'bg-blue-400', + neutral: 'bg-gray-400', + brand: 'bg-[#ffa729]', +} as const + +const SIZE_STYLES = { + sm: 'px-2 py-0.5 text-xs', + md: 'px-3 py-1 text-sm', +} as const + +export default function Badge({ variant, children, size = 'sm', dot = false }: BadgeProps): JSX.Element { + return ( + + {dot && } + {children} + + ) +} diff --git a/ExplorerFrontend/app/components/BalanceCheckTool.tsx b/ExplorerFrontend/app/components/BalanceCheckTool.tsx index bfa8dc4..d0253fa 100644 --- a/ExplorerFrontend/app/components/BalanceCheckTool.tsx +++ b/ExplorerFrontend/app/components/BalanceCheckTool.tsx @@ -69,12 +69,13 @@ export default function BalanceCheckTool(): JSX.Element { >
+
{error}
)} diff --git a/ExplorerFrontend/app/components/BlockSizeChart.tsx b/ExplorerFrontend/app/components/BlockSizeChart.tsx index 380fbb8..211384b 100644 --- a/ExplorerFrontend/app/components/BlockSizeChart.tsx +++ b/ExplorerFrontend/app/components/BlockSizeChart.tsx @@ -189,6 +189,7 @@ function BrushChart({ xScale={dateScale} yScale={stockScale} gradientColor={background2} + gradientId="block-size-main-gradient" /> -   - +
+ + +
); } diff --git a/ExplorerFrontend/app/components/Breadcrumbs.tsx b/ExplorerFrontend/app/components/Breadcrumbs.tsx new file mode 100644 index 0000000..26966b0 --- /dev/null +++ b/ExplorerFrontend/app/components/Breadcrumbs.tsx @@ -0,0 +1,42 @@ +import Link from 'next/link' +import { ChevronRightIcon } from '@heroicons/react/20/solid' + +export interface BreadcrumbItem { + label: string + href?: string +} + +interface BreadcrumbsProps { + items: BreadcrumbItem[] +} + +export default function Breadcrumbs({ items }: BreadcrumbsProps): JSX.Element { + return ( + + ) +} diff --git a/ExplorerFrontend/app/components/Charts.tsx b/ExplorerFrontend/app/components/Charts.tsx index 74209f5..a36d8a4 100644 --- a/ExplorerFrontend/app/components/Charts.tsx +++ b/ExplorerFrontend/app/components/Charts.tsx @@ -1,38 +1,20 @@ -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import Typography from '@mui/material/Typography'; -import Box from '@mui/material/Box'; +'use client'; + import TradingViewWidget from './TradingViewWidget'; export default function Charts(): JSX.Element { return ( - - - - +
+
+
+

MEXC QRL/USDT Chart - - +

+
- - - - +
+
+
+
); } diff --git a/ExplorerFrontend/app/components/CopyAddressButton.tsx b/ExplorerFrontend/app/components/CopyAddressButton.tsx deleted file mode 100644 index cfe0948..0000000 --- a/ExplorerFrontend/app/components/CopyAddressButton.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { useState } from "react"; - -interface Props { - address: string; -} - -export default function CopyAddressButton({ address }: Props): JSX.Element { - const [copySuccess, setCopySuccess] = useState(''); - - const copyToClipboard = (): void => { - navigator.clipboard.writeText(address) - .then(() => { - setCopySuccess('Copied!'); - setTimeout(() => setCopySuccess(''), 2000); - }) - .catch(err => { - console.error('Failed to copy text: ', err); - }); - }; - - return ( -
- -
- ); -} diff --git a/ExplorerFrontend/app/components/CopyButton.tsx b/ExplorerFrontend/app/components/CopyButton.tsx new file mode 100644 index 0000000..1c0b80a --- /dev/null +++ b/ExplorerFrontend/app/components/CopyButton.tsx @@ -0,0 +1,113 @@ +'use client' + +import { useState } from 'react' +import type { MouseEvent } from 'react' + +interface CopyButtonProps { + value: string + label?: string + size?: 'sm' | 'md' + stopPropagation?: boolean +} + +// Usage examples: +// +// +// + +export default function CopyButton({ + value, + label = 'Copy to clipboard', + size = 'md', + stopPropagation = false, +}: CopyButtonProps): JSX.Element { + const [copySuccess, setCopySuccess] = useState(false) + + const copyToClipboard = (e: MouseEvent): void => { + if (stopPropagation) { + e.stopPropagation() + } + navigator.clipboard + .writeText(value) + .then(() => { + setCopySuccess(true) + setTimeout(() => setCopySuccess(false), 2000) + }) + .catch(err => { + console.error('Failed to copy text: ', err) + }) + } + + const copyIcon = copySuccess ? ( + + ) : ( + + ) + + if (size === 'sm') { + return ( + + ) + } + + return ( +
+ +
+ ) +} diff --git a/ExplorerFrontend/app/components/CopyHashButton.tsx b/ExplorerFrontend/app/components/CopyHashButton.tsx deleted file mode 100644 index 9bd23bf..0000000 --- a/ExplorerFrontend/app/components/CopyHashButton.tsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; - -import { useState } from "react"; -import type { MouseEvent } from "react"; - -interface Props { - hash: string; - size?: "small" | "normal"; -} - -export default function CopyHashButton({ hash, size = "normal" }: Props): JSX.Element { - const [copySuccess, setCopySuccess] = useState(''); - - const copyToClipboard = (e: MouseEvent): void => { - e.stopPropagation(); // Prevent card click when copying - navigator.clipboard.writeText(hash) - .then(() => { - setCopySuccess('Copied!'); - setTimeout(() => setCopySuccess(''), 2000); - }) - .catch(err => { - console.error('Failed to copy text: ', err); - }); - }; - - if (size === "small") { - return ( - - ); - } - - return ( - - ); -} diff --git a/ExplorerFrontend/app/components/DebouncedInput.tsx b/ExplorerFrontend/app/components/DebouncedInput.tsx index 4e92319..3659934 100644 --- a/ExplorerFrontend/app/components/DebouncedInput.tsx +++ b/ExplorerFrontend/app/components/DebouncedInput.tsx @@ -5,12 +5,14 @@ interface DebouncedInputProps extends Omit value: string; onChange: (value: string) => void; debounce?: number; + 'aria-label'?: string; } export default function DebouncedInput({ value: initialValue, onChange, debounce = 0, + 'aria-label': ariaLabel = 'Filter', ...props }: DebouncedInputProps): JSX.Element { const [value, setValue] = useState(initialValue); @@ -34,6 +36,7 @@ export default function DebouncedInput({ return ( diff --git a/ExplorerFrontend/app/components/DownloadBtn.tsx b/ExplorerFrontend/app/components/DownloadBtn.tsx index 9d2732f..2cbfa52 100644 --- a/ExplorerFrontend/app/components/DownloadBtn.tsx +++ b/ExplorerFrontend/app/components/DownloadBtn.tsx @@ -70,7 +70,8 @@ export function DownloadBtn({ data = [], fileName }: DownloadBtnProps): JSX.Elem return ( - - -
setIsDropDownBlockchain(true)} - onMouseLeave={() => setIsDropDownBlockchain(false)} - > -
- Blockchain -
- - {IsDropDownBlockchain && ( -
-
- {blockchain.map((item) => ( - -

-

- {item.imgSrc ? ( - - ) : ( - item.icon &&
-
- {item.name} -

{item.description}

-
-

- - ))} -
-
- )} -
- -
setisDropDownTools(true)} - onMouseLeave={() => setisDropDownTools(false)} - > -
- Tools -
- - {isDropDownTools && ( -
-
- {tools.map((item) => ( - -

-

- {item.imgSrc ? ( - - ) : ( - item.icon &&
-
- {item.name} -

{item.description}

-
-

- - ))} -
-
- )} -
- - - Richlist - -
- - -
- -
- setMobileMenuOpen(false)}> - Quanta Explorer - - - -
-
-
-
- - {({ open }) => ( - <> - - Blockchain - - - {blockchain.map((item) => ( - setMobileMenuOpen(false)} - > - {item.name} - - ))} - - - )} - -
- -
- - {({ open }) => ( - <> - - Tools - - - {tools.map((item) => ( - setMobileMenuOpen(false)} - > - {item.name} - - ))} - - - )} - - - setMobileMenuOpen(false)} - > - Richlist - -
-
-
-
-
- - - ); -} diff --git a/ExplorerFrontend/app/components/Layout.tsx b/ExplorerFrontend/app/components/Layout.tsx deleted file mode 100644 index 4d698b8..0000000 --- a/ExplorerFrontend/app/components/Layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { PropsWithChildren } from "react"; -import Header from "./Header"; - -const Layout = ({ children }: PropsWithChildren): JSX.Element => { - return ( - <> -
-
{children}
- - ); -}; - -export default Layout; \ No newline at end of file diff --git a/ExplorerFrontend/app/components/NewBlockSizeChart.tsx b/ExplorerFrontend/app/components/NewBlockSizeChart.tsx index ff1b34a..1761e02 100644 --- a/ExplorerFrontend/app/components/NewBlockSizeChart.tsx +++ b/ExplorerFrontend/app/components/NewBlockSizeChart.tsx @@ -1,13 +1,13 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import axios from "axios"; import config from '../../config.js'; import BrushChart from './BlockSizeChart'; -import Typography from '@mui/material/Typography'; -import Divider from '@mui/material/Divider'; const NewBlockSizeChart = (): JSX.Element => { const [loading, setLoading] = useState(true); const [blocks, setBlocks] = useState([]); + const containerRef = useRef(null); + const [width, setWidth] = useState(600); React.useEffect(() => { axios.get(config.handlerUrl + "/blocksizes").then((response) => { @@ -15,18 +15,24 @@ const NewBlockSizeChart = (): JSX.Element => { }).finally(() => setLoading(false)); }, []); - console.log(loading); - console.log(blocks); + useEffect(() => { + if (!containerRef.current) return; + const ro = new ResizeObserver(([entry]) => setWidth(Math.floor(entry.contentRect.width))); + ro.observe(containerRef.current); + return () => ro.disconnect(); + }, []); return ( <> - - - Average Block Size Chart - {loading ?
Loading....
: } -
+
+
+

+ Average Block Size Chart +

+ {loading ?
Loading....
: } +
); } -export default NewBlockSizeChart; \ No newline at end of file +export default NewBlockSizeChart; diff --git a/ExplorerFrontend/app/components/Pagination.tsx b/ExplorerFrontend/app/components/Pagination.tsx deleted file mode 100644 index 268271e..0000000 --- a/ExplorerFrontend/app/components/Pagination.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import Link from "next/link"; - -interface PaginationProps { - postsPerPage: number; - totalPosts: number; - paginate: (pageNumber: number) => void; -} - -export default function Pagination({ - postsPerPage, - totalPosts, - paginate -}: PaginationProps): JSX.Element { - const pageNumbers: number[] = []; - - for (let i = 1; i <= Math.ceil(totalPosts / postsPerPage); i++) { - pageNumbers.push(i); - } - - const handleClick = (number: number): void => { - paginate(number); - }; - - return ( - - ); -} diff --git a/ExplorerFrontend/app/components/QRCodeButton.tsx b/ExplorerFrontend/app/components/QRCodeButton.tsx index de07b1b..c5e917d 100644 --- a/ExplorerFrontend/app/components/QRCodeButton.tsx +++ b/ExplorerFrontend/app/components/QRCodeButton.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useRef, useCallback } from 'react'; import QRCodeModal from './QRCodeModal'; interface QRCodeButtonProps { @@ -9,10 +9,22 @@ interface QRCodeButtonProps { export default function QRCodeButton({ address }: QRCodeButtonProps): JSX.Element { const [isModalOpen, setIsModalOpen] = useState(false); + // M6: Store ref to the trigger button so focus returns after modal closes + const triggerRef = useRef(null); + + const handleClose = useCallback((): void => { + setIsModalOpen(false); + // Return focus to the trigger button after the modal unmounts + // Use setTimeout to allow React to complete the state update and unmount + setTimeout(() => { + triggerRef.current?.focus(); + }, 0); + }, []); return (
); diff --git a/ExplorerFrontend/app/components/QRCodeModal.tsx b/ExplorerFrontend/app/components/QRCodeModal.tsx index 6f1ba12..ac4c0e2 100644 --- a/ExplorerFrontend/app/components/QRCodeModal.tsx +++ b/ExplorerFrontend/app/components/QRCodeModal.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useEffect, useRef } from 'react'; import { QRCodeSVG } from 'qrcode.react'; interface QRCodeModalProps { @@ -10,34 +11,117 @@ interface QRCodeModalProps { export default function QRCodeModal({ address, isOpen, onClose }: QRCodeModalProps): JSX.Element | null { if (!isOpen) return null; - + // Generate the full zondscan URL const zondscanUrl = `https://zondscan.com/address/${address.toLowerCase()}`; - + // Format address for display (first 6 and last 4 chars) const displayAddress = `${address.slice(0, 6)}...${address.slice(-4)}`; + return ; +} + +interface ModalContentProps { + address: string; + displayAddress: string; + zondscanUrl: string; + onClose: () => void; +} + +function ModalContent({ address, displayAddress, zondscanUrl, onClose }: ModalContentProps): JSX.Element { + const containerRef = useRef(null); + const closeButtonRef = useRef(null); + + // M5: Body scroll lock + useEffect(() => { + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = previousOverflow; + }; + }, []); + + // M2: Escape key to close + useEffect(() => { + const listener = (event: KeyboardEvent): void => { + if (event.key === 'Escape') { + onClose(); + } + }; + window.addEventListener('keydown', listener); + return () => { + window.removeEventListener('keydown', listener); + }; + }, [onClose]); + + // M1: Focus trap - move focus to close button on mount, intercept Tab/Shift+Tab + useEffect(() => { + closeButtonRef.current?.focus(); + + const FOCUSABLE_SELECTORS = + 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'; + + const handleTab = (event: KeyboardEvent): void => { + if (event.key !== 'Tab') return; + const container = containerRef.current; + if (!container) return; + + const focusable = Array.from(container.querySelectorAll(FOCUSABLE_SELECTORS)); + if (focusable.length === 0) return; + + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + + if (event.shiftKey) { + if (document.activeElement === first) { + event.preventDefault(); + last.focus(); + } + } else { + if (document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + } + }; + + window.addEventListener('keydown', handleTab); + return () => { + window.removeEventListener('keydown', handleTab); + }; + }, []); + return (
{/* Backdrop */} -
- - {/* Modal */} -
+ + {/* M3: Dialog ARIA */} +
+ {/* M4: Close button label */} - +
-

Scan Address

+ {/* M3: id for aria-labelledby */} +

Scan Address

{displayAddress} - - setIsOpen(false)}> +
setIsOpen(false)} + aria-hidden="true" /> )} {/* Sidebar */} -
Tx HashFromToAmountTimeTx HashFromToAmountTime
{header.isPlaceholder @@ -458,8 +459,11 @@ export default function TanStackTable({ transactions, internalt }: TableProps):
-
+
-
+
{windowWidth < 768 ? (
{showInternal @@ -505,7 +512,7 @@ export default function TanStackTable({ transactions, internalt }: TableProps): }
) : ( - +
{showInternal ? renderTableHeader(internalTransactionTable) @@ -528,6 +535,7 @@ export default function TanStackTable({ transactions, internalt }: TableProps): {showInternal ? ( <>
+
- - - - - @@ -330,19 +339,19 @@ function TokensTable({ contracts }: { contracts: ContractData[] }) { function ContractsTable({ contracts }: { contracts: ContractData[] }) { return (
-
+ Token + Contract Address + Decimals + Total Supply + Creator
+
- - - - @@ -361,14 +370,9 @@ function ContractsTable({ contracts }: { contracts: ContractData[] }) {
+ Contract Address + Type + Creator + Created at Block
{contract.isToken ? ( - - Token - {contract.symbol && ({contract.symbol})} - + Token{contract.symbol ? ` (${contract.symbol})` : ''} ) : ( - - Contract - + Contract )} diff --git a/ExplorerFrontend/app/contracts/contracts-wrapper.tsx b/ExplorerFrontend/app/contracts/contracts-wrapper.tsx index 8fe3a80..674f67c 100644 --- a/ExplorerFrontend/app/contracts/contracts-wrapper.tsx +++ b/ExplorerFrontend/app/contracts/contracts-wrapper.tsx @@ -14,7 +14,13 @@ interface ContractsWrapperProps { export default function ContractsWrapper({ initialData, totalContracts }: ContractsWrapperProps): JSX.Element { return ( - Loading...}> + + {[...Array(5)].map((_, i) => ( +
+ ))} +
+ }>
) diff --git a/ExplorerFrontend/app/converter/converter-client.tsx b/ExplorerFrontend/app/converter/converter-client.tsx index 8a29815..0d9debb 100644 --- a/ExplorerFrontend/app/converter/converter-client.tsx +++ b/ExplorerFrontend/app/converter/converter-client.tsx @@ -40,14 +40,15 @@ function Converter(): JSX.Element {
{/* Quanta Input */}
- +
QRL @@ -64,14 +65,15 @@ function Converter(): JSX.Element { {/* Shor Input */}
- +
Shor @@ -81,7 +83,7 @@ function Converter(): JSX.Element { {/* Error Message */} {error && ( -
+
@@ -92,7 +94,7 @@ function Converter(): JSX.Element { )} {/* Info Box */} -
+

1 QRL = 1,000,000,000,000,000,000 Shor (10^18)

diff --git a/ExplorerFrontend/app/error.tsx b/ExplorerFrontend/app/error.tsx new file mode 100644 index 0000000..19b502b --- /dev/null +++ b/ExplorerFrontend/app/error.tsx @@ -0,0 +1,31 @@ +'use client' + +import { useEffect } from 'react' + +export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + useEffect(() => { + console.error(error) + }, [error]) + + return ( +
+
+
+ + + +
+

Something went wrong

+

An unexpected error occurred while loading this page.

+
+ + + Go home + +
+
+
+ ) +} diff --git a/ExplorerFrontend/app/home-client.tsx b/ExplorerFrontend/app/home-client.tsx index 7445b8a..10f794e 100644 --- a/ExplorerFrontend/app/home-client.tsx +++ b/ExplorerFrontend/app/home-client.tsx @@ -24,6 +24,53 @@ interface StatsData { error: boolean; } +interface StatItem { + data: string; + title: string; + loading: boolean; + error: boolean; + icon: React.ReactNode; +} + +const StatCard = ({ item }: { item: StatItem }): JSX.Element => ( +
+
+ {item.loading ? ( +
+
+
+
+ ) : item.error ? ( +
+ + + + Failed to load data +
+ ) : ( + <> +
+
+ {item.icon} +
+
+

+ {item.data} +

+

+ {item.title} +

+ + )} +
+
+); + interface DashboardData { walletCount: StatsData; volume: StatsData; @@ -115,7 +162,7 @@ export default function HomeClient({ pageTitle }: { pageTitle: string }): JSX.El const blockchainStats = [ { data: formatNumberWithCommas(data.walletCount.value), - title: "Network Bagholder Count", + title: "Wallet Count", loading: data.walletCount.isLoading, error: data.walletCount.error, icon: ( @@ -233,53 +280,6 @@ export default function HomeClient({ pageTitle }: { pageTitle: string }): JSX.El zIndex: -1, }; - interface StatItem { - data: string; - title: string; - loading: boolean; - error: boolean; - icon: React.ReactNode; - } - - const StatCard = ({ item }: { item: StatItem }): JSX.Element => ( -
-
- {item.loading ? ( -
-
-
-
- ) : item.error ? ( -
- - - - Failed to load data -
- ) : ( - <> -
-
- {item.icon} -
-
-

- {item.data} -

-

- {item.title} -

- - )} -
-
- ); - const seoTextItems = [ { title: "What is ZondScan?", @@ -340,7 +340,7 @@ export default function HomeClient({ pageTitle }: { pageTitle: string }): JSX.El {/* Blockchain Stats */}

Blockchain Statistics

-
+
{blockchainStats.map((item, idx) => ( ))} @@ -350,7 +350,7 @@ export default function HomeClient({ pageTitle }: { pageTitle: string }): JSX.El {/* Financial Stats */}

Financial Statistics

-
+
{financialStats.map((item, idx) => ( ))} diff --git a/ExplorerFrontend/app/layout.tsx b/ExplorerFrontend/app/layout.tsx index 51e60df..49a562d 100644 --- a/ExplorerFrontend/app/layout.tsx +++ b/ExplorerFrontend/app/layout.tsx @@ -120,16 +120,18 @@ export default function RootLayout({ children }: RootLayoutProps): JSX.Element { + + Skip to main content +
-
-
+
+
{children} -
-
-
+ +
+
- diff --git a/ExplorerFrontend/app/lib/styles.ts b/ExplorerFrontend/app/lib/styles.ts deleted file mode 100644 index a3ae3c5..0000000 --- a/ExplorerFrontend/app/lib/styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { styled } from '@mui/material/styles'; -import Paper from '@mui/material/Paper'; - -export const Item = styled(Paper)(({ theme }) => ({ - backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', - ...theme.typography.body2, - padding: theme.spacing(1), - textAlign: 'center', - color: theme.palette.text.secondary, -})); diff --git a/ExplorerFrontend/app/not-found.tsx b/ExplorerFrontend/app/not-found.tsx index a575040..40673a0 100644 --- a/ExplorerFrontend/app/not-found.tsx +++ b/ExplorerFrontend/app/not-found.tsx @@ -1,18 +1,20 @@ import Link from 'next/link' +import EmptyState from './components/EmptyState' export default function NotFound(): JSX.Element { return ( -
-
-

404 Not Found

-

Could not find the requested resource

- - Return Home - -
+
+ + + + } + title="404 — Page not found" + description="The block, transaction, or address you're looking for doesn't exist or may have been removed." + actionLabel="Return home" + actionHref="/" + />
) } diff --git a/ExplorerFrontend/app/pending/[query]/PendingList.tsx b/ExplorerFrontend/app/pending/[query]/PendingList.tsx index 894645c..183e2bc 100644 --- a/ExplorerFrontend/app/pending/[query]/PendingList.tsx +++ b/ExplorerFrontend/app/pending/[query]/PendingList.tsx @@ -7,6 +7,7 @@ import { useQuery } from '@tanstack/react-query'; import Link from 'next/link'; import { formatAmount } from '../../lib/helpers'; import type { PendingTransaction } from '@/app/types'; +import Badge from '../../components/Badge'; interface PaginatedResponse { // New format fields @@ -46,29 +47,35 @@ const TransactionCard: React.FC = ({ transaction }) => { }; const date = transaction.createdAt ? formatDateUTC(transaction.createdAt) : 'Pending'; + const truncateHash = (hash: string | undefined): string => + hash ? `${hash.slice(0, 10)}...${hash.slice(-8)}` : ''; + return (
- - {transaction.hash} + + {truncateHash(transaction.hash)} - + {transaction.status} - +

From

-

{transaction.from}

+

{truncateHash(transaction.from)}

To

-

{transaction.to}

+

{truncateHash(transaction.to)}

Value

@@ -96,15 +103,12 @@ interface PendingListProps { } const fetchPendingTransactions = async (page: number): Promise => { - console.log('Fetching pending transactions for page:', page); const response = await axios.get(`${config.handlerUrl}/pending-transactions`, { params: { page, limit: ITEMS_PER_PAGE } }); - - console.log('Received response:', response.data); return response.data; }; diff --git a/ExplorerFrontend/app/pending/tx/[hash]/page.tsx b/ExplorerFrontend/app/pending/tx/[hash]/page.tsx index f7257d1..a431bac 100644 --- a/ExplorerFrontend/app/pending/tx/[hash]/page.tsx +++ b/ExplorerFrontend/app/pending/tx/[hash]/page.tsx @@ -1,4 +1,5 @@ import axios from 'axios'; +import { redirect } from 'next/navigation'; import config from '../../../../config'; import type { PendingTransaction } from '@/app/types'; import PendingTransactionView from './pending-transaction-view'; @@ -8,7 +9,7 @@ interface PageProps { } function validateTransactionHash(hash: string): boolean { - const hashRegex = /^0x[0-9a-fA-F]+$/; + const hashRegex = /^0x[0-9a-fA-F]{64}$/; return hashRegex.test(hash); } @@ -20,7 +21,6 @@ async function getTransactionStatus(hash: string): Promise<{ try { // First try pending transactions endpoint const response = await axios.get(`${config.handlerUrl}/pending-transaction/${hash}`); - console.log('Transaction status response:', response.data); if (!response.data?.transaction) { return { status: 'dropped', transaction: null }; @@ -78,7 +78,6 @@ export default async function PendingTransactionPage({ params }: PageProps): Pro try { const resolvedParams = await params; const hash = resolvedParams.hash; - console.log('Transaction hash:', hash); if (!validateTransactionHash(hash)) { return ( @@ -96,18 +95,9 @@ export default async function PendingTransactionPage({ params }: PageProps): Pro const { status, transaction } = await getTransactionStatus(hash); - // If transaction is mined, use window.location for client-side redirect + // If transaction is mined, redirect to the confirmed transaction page if (status === 'mined' && transaction) { - return ( - <> -