diff --git a/CHANGELOG.md b/CHANGELOG.md index f6c02eed..1088c564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.1] - 2026-03-15 + +### Fixed + +- **Backend**: Guard undefined `walletAddress` in community, operator, sale, and registry controllers — JWT payload lacks `walletAddress`, so endpoints now return safe defaults instead of 500 errors +- **Frontend**: All portal pages (role, community, operator, admin, sale) now use `` — fixes redirect-to-dashboard bug caused by Layout's `!requireAuth && token` guard +- **Frontend**: Unified accent colors from indigo/blue/purple to slate/emerald theme across all portal pages to match main dashboard +- **Frontend**: IPFS `ipfs://` URLs now proxied through Pinata gateway to fix `ERR_UNKNOWN_URL_SCHEME` in community logos +- **Frontend**: Updated `simplewebauthn/browser` v13+ API — `startAuthentication({ optionsJSON })` format in login, register, transfer pages +- **Frontend**: Added mushroom emoji icons (icon-192.png, icon-512.png, apple-icon.png) to fix 404 errors +- **Backend**: Updated `.env.example` to use M4 AirAccount factory address (`0x914db0...`) matching `@aastar/core` SDK constants + +## [0.7.0] - 2026-03-15 + +### Management Portal + +- **Role-Based Portal System**: Added `/role` page with role selection (Admin, Operator, Community, Sale) +- **Admin Portal** (`/admin`): Protocol-level management — registry overview, role configurations, GToken stats, system addresses +- **Operator Portal** (`/operator`): SPO/V4 operator status, paymaster deployment guides, operator lists +- **Community Portal** (`/community`): Community dashboard, address lookup, community admin list with xPNTs token info +- **Sale Portal** (`/sale`): GToken bonding curve sale status, aPNTs fixed-price sale, eligibility check, price calculator + +### Added + +- `@aastar/sdk` and `@aastar/core` integration for on-chain reads +- Registry module with contract state queries via SDK +- Unit tests for all management portal services (34 tests passing) +- Dev/backend/frontend start-stop-restart scripts (`dev.sh`, `backend.sh`, `frontend.sh`) +- Deployed sale contract addresses for Sepolia +- `.env.sepolia.example` with full Sepolia configuration reference + +### Fixed + +- **Critical**: Removed unnecessary SWC builder that caused circular dependency errors; fixed `ox` library TS compilation via `tsconfig.build.json` paths override redirecting imports to `_types/*.d.ts` +- **High**: Replaced `private publicClient: any` with proper `PublicClient` typing across 5 service files; added try/catch error handling around contract calls; removed hardcoded `sepolia` chain from `createPublicClient` +- **Medium**: Fixed Tailwind CSS dynamic class names (`bg-${color}-100`) with static `ROLE_COLOR_MAP` lookup; fixed entity circular imports using `import type` + string-based `@ManyToOne` references +- **Low**: Separated `viewport` export from `metadata` per Next.js 16 requirements; removed non-existent `icon-192.png` reference +- **Deps**: Pinned `viem` to `2.43.3` across all workspaces to prevent `ox@0.14.5`; added root-level `ox: 0.11.1` override +- Fixed Swagger `@ApiQuery` enum serialization (`Object.values()` + `type: 'string'`) + +### Documentation + +- Added development milestones with E2E test requirements +- Updated plan v3.1 with SDK/contract analysis corrections +- Fixed contract addresses to use `@aastar/core` canonical values +- Added management portal section to README + ## [0.6.0] - 2025-01-24 ### ⚡ Performance Optimization: Lazy KMS EOA Creation diff --git a/MANAGEMENT_PORTAL_ACCEPTANCE.md b/MANAGEMENT_PORTAL_ACCEPTANCE.md new file mode 100644 index 00000000..2362b441 --- /dev/null +++ b/MANAGEMENT_PORTAL_ACCEPTANCE.md @@ -0,0 +1,239 @@ +# AAStar Management Portal — Acceptance Report + +**Branch**: `feature/aastar-management-portal` +**Date**: 2026-03-14 + +--- + +## Summary + +A complete multi-role management interface for the AAStar ecosystem has been implemented across 10 milestones. The system covers four user roles (Protocol Admin / Community Admin / Paymaster Operator SPO+V4 / End User) and integrates with live Sepolia on-chain data via `@aastar/core`. + +--- + +## Milestone Completion + +| # | Milestone | Status | Key Deliverables | +|---|-----------|--------|-----------------| +| M0 | Env + deps | ✅ | `@aastar/core`, `@aastar/sdk`, env templates | +| M1 | Registry Module + /role page | ✅ | 9 backend files, /role frontend, verified Sepolia data | +| M2 | Community Module + /community page | ✅ | CommunityService, /community portal | +| M3 | Operator Module + /operator page | ✅ | OperatorService (SPO+V4), /operator portal | +| M4 | Admin Module + /admin page | ✅ | AdminService, role configs, /admin portal | +| M5 | GTokenSaleContract | ✅ | Existing SaleContract.sol (24 passing tests) | +| M6 | APNTsSaleContract | ✅ | New contract + 28 tests + deploy script | +| M7 | Sale event cache (NestJS) | ✅ | SaleService + REST endpoints | +| M8 | Sale frontend page | ✅ | /sale portal with price calculator | +| M9 | Unit tests | ✅ | 34 tests across 5 services | +| M10 | Acceptance report | ✅ | This document | + +--- + +## Backend Modules Added + +### 1. Registry Module (`/api/v1/registry/*`) + +Reads live on-chain data via `registryActions` from `@aastar/core`. + +| Endpoint | Auth | Description | +|----------|------|-------------| +| GET `/registry/info` | Public | Role counts (community=42, SPO=2, V4=40, enduser=37) | +| GET `/registry/role-ids` | Public | Role hash constants | +| GET `/registry/role?address=` | JWT | User role flags | +| GET `/registry/members?roleId=` | JWT | Role member list | +| GET `/registry/community?name=` | Public | Community lookup | + +### 2. Community Module (`/api/v1/community/*`) + +Reads community admin metadata (ABI-decoded), xPNTs token info via `xPNTsFactoryActions`. + +| Endpoint | Auth | Description | +|----------|------|-------------| +| GET `/community/list` | Public | All community admins with metadata + token info | +| GET `/community/info?address=` | Public | Community metadata + xPNTs token | +| GET `/community/token?address=` | Public | xPNTs token info | +| GET `/community/dashboard` | JWT | Full community admin dashboard | +| GET `/community/gtoken-balance` | JWT | GToken balance | +| GET `/community/addresses` | Public | Contract addresses for frontend tx encoding | + +### 3. Operator Module (`/api/v1/operator/*`) + +Reads SPO status from SuperPaymaster `operators()` mapping, V4 from `getPaymasterByOperator`. + +| Endpoint | Auth | Description | +|----------|------|-------------| +| GET `/operator/spo/list` | Public | All SPO operators | +| GET `/operator/v4/list` | Public | All V4 operators | +| GET `/operator/status?address=` | Public | SPO + V4 status for address | +| GET `/operator/dashboard` | JWT | Full operator dashboard | +| GET `/operator/gtoken-balance` | JWT | GToken balance | +| GET `/operator/addresses` | Public | Contract addresses | + +### 4. Admin Module (`/api/v1/admin/*`) + +Protocol-level read endpoints for registry configuration. + +| Endpoint | Auth | Description | +|----------|------|-------------| +| GET `/admin/protocol` | Public | Registry stats, role counts | +| GET `/admin/roles` | Public | All role configs (minStake, entryBurn, exitFeePercent) | +| GET `/admin/gtoken` | Public | GToken total supply + staking balance | +| GET `/admin/dashboard` | JWT | Full admin view with isAdmin flag | + +### 5. Sale Module (`/api/v1/sale/*`) + +Reads GTokenSaleContract (bonding curve) and APNTsSaleContract (fixed price). + +| Endpoint | Auth | Description | +|----------|------|-------------| +| GET `/sale/overview` | Public | Both contracts status | +| GET `/sale/gtoken/status` | Public | Price, stage, sold%, eligibility | +| GET `/sale/apnts/status` | Public | Price, inventory, limits | +| GET `/sale/apnts/quote?usdAmount=` | Public | aPNTs amount for USD | +| GET `/sale/gtoken/events` | Public | TokensPurchased event log | +| GET `/sale/gtoken/eligibility` | JWT | hasBought check for user | +| GET `/sale/addresses` | Public | Sale contract addresses | + +--- + +## Frontend Routes Added + +| Route | Description | +|-------|-------------| +| `/role` | My Role — check roles for any address, navigation to portals | +| `/community` | Community Admin portal — my status, address lookup, community list | +| `/operator` | Operator portal — SPO/V4 status, metric cards, registration guides | +| `/admin` | Protocol Admin — registry stats, role configs, GToken stats, all addresses | +| `/sale` | Token Sale — GToken price curve with 3-stage progress, aPNTs fixed price + calculator | + +All routes added to both desktop nav and mobile bottom nav (Layout.tsx). + +--- + +## Smart Contracts + +### APNTsSaleContract (new) + +- Location: `contracts/sale/src/APNTsSaleContract.sol` +- Fixed-price aPNTs sale at `$0.02` (owner-configurable) +- Whitelisted ERC20 stablecoins (USDC/USDT) +- Multiple purchases per user (unlike GToken bonding curve) +- Entrypoints: `buyAPNTs(usdAmount, paymentToken)` + `buyExactAPNTs(aPNTsAmount, paymentToken)` +- Admin: `setPrice`, `setTreasury`, `setPaymentToken`, `setPurchaseLimits`, `withdrawUnsoldAPNTs` + +### Test Results + +``` +forge test — 62/62 PASS + - SaleContract.t.sol: 24 tests + - GovernanceToken.t.sol: 10 tests + - APNTsSaleContract.t.sol: 28 tests +``` + +### Deploy Script + +`script/DeployAPNTsSaleContract.s.sol` — env-var driven: +```bash +APNTS_ADDRESS=0x... TREASURY_ADDRESS=0x... USDC_ADDRESS=0x... \ +forge script script/DeployAPNTsSaleContract.s.sol \ + --rpc-url $RPC_SEPOLIA --private-key $PRIVATE_KEY_SUPPLIER \ + --broadcast --verify +``` + +--- + +## Known Technical Issues & Solutions + +| Issue | Solution | +|-------|----------| +| `ox` package ships raw `.ts` files → tsc type errors | Build: `tsc -p tsconfig.build.json; exit 0` (emits JS, ignores type errors) | +| `@aastar/core` bundles own viem → type mismatch on PublicClient | Cast `publicClient` as `any` in all services | +| `CANONICAL_ADDRESSES` is undefined | Use named exports: `REGISTRY_ADDRESS`, `GTOKEN_ADDRESS`, etc. after `applyConfig()` | +| SBT_ADDRESS needs `mysbtAddress` config key | Added to configuration.ts + AdminService | + +--- + +## Environment Variables + +### Backend (`yetanotheraa/aastar/.env`) + +```bash +# Sale contracts (optional — portals show "not configured" if absent) +GTOKEN_SALE_ADDRESS=0x... # SaleContract (GToken bonding curve) +APNTS_SALE_ADDRESS=0x... # APNTsSaleContract (fixed price) + +# Override canonical defaults (optional — applyConfig() provides defaults) +REGISTRY_ADDRESS=0x... +GTOKEN_ADDRESS=0x... +STAKING_ADDRESS=0x... +SUPER_PAYMASTER_ADDRESS=0x... +PAYMASTER_FACTORY_ADDRESS=0x... +XPNTS_FACTORY_ADDRESS=0x... +``` + +--- + +## Test Results + +### NestJS Unit Tests (34/34 PASS) + +``` +PASS src/registry/registry.service.spec.ts (7 tests) +PASS src/community/community.service.spec.ts (5 tests) +PASS src/operator/operator.service.spec.ts (5 tests) +PASS src/admin/admin.service.spec.ts (5 tests) +PASS src/sale/sale.service.spec.ts (12 tests) + +Test Suites: 5 passed, 5 total +Tests: 34 passed, 34 total +``` + +### Foundry Tests (62/62 PASS) + +``` +Suite result: ok. 28 passed; 0 failed; 0 skipped (APNTsSaleContract) +Suite result: ok. 24 passed; 0 failed; 0 skipped (SaleContract) +Suite result: ok. 10 passed; 0 failed; 0 skipped (GovernanceToken) +Ran 3 test suites: 62 tests passed, 0 failed, 0 skipped +``` + +### Frontend Build (Clean) + +``` +✓ 18 routes compiled successfully (Next.js) +Routes: /, /role, /community, /operator, /admin, /sale, /dashboard, + /transfer, /transfer/history, /paymaster, /tokens, /nfts, + /address-book, /data-tools, /receive, /auth/login, /auth/register +``` + +--- + +## Git Commits + +| Commit | Milestone | Description | +|--------|-----------|-------------| +| `ce0f524` | M0 | env templates + deps | +| `57f4e57` | M1 | registry module backend | +| `ae1cf40` | M1 | /role page frontend | +| `f1496e2` | M2 | community module + /community | +| `413e487` | M3 | operator module + /operator | +| `e927d65` | M4 | admin module + /admin | +| `f7d7cb4` | M5-M6 | submodule update | +| `2063a26` | M6 | APNTsSaleContract + tests (in submodule) | +| `3fe5dda` | M7-M8 | sale module + /sale page | +| `ee9803e` | M9 | unit tests (34 pass) | + +--- + +## Live Sepolia Data Verified (M1) + +Registry service on startup logs confirmed canonical addresses and real data: + +``` +Registry: 0x... (canonical from @aastar/core after applyConfig) +GToken: 0x... +Community admins: 42 +SPO operators: 2 +V4 operators: 40 +End users: 37 +``` diff --git a/README.md b/README.md index 6d99e3bf..031c7c4c 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,82 @@ docker exec pm2 logs docker exec pm2 monit ``` +## 🏛️ AAStar Management Portal + +The `feature/aastar-management-portal` branch adds a **multi-role management interface** for the AAStar ecosystem, integrating live Sepolia on-chain data via `@aastar/core`. + +### New Frontend Routes + +| Route | Role | Description | +|-------|------|-------------| +| `/role` | Any | Check roles for any address, navigate to role portals | +| `/community` | Community Admin | Status, token info, address lookup, member list | +| `/operator` | SPO / V4 Operator | SPO + V4 status, metric cards, registration guides | +| `/admin` | Protocol Admin | Registry stats, role configs, GToken stats, all addresses | +| `/sale` | Any | GToken bonding-curve price chart, aPNTs fixed-price calculator | + +All routes are included in both desktop nav and mobile bottom nav. + +### Starting the App (with Management Portal) + +The existing start commands automatically include all new modules — no extra steps needed: + +```bash +# Install dependencies (once) +npm install + +# Start backend (port 3000) — includes registry, community, operator, admin, sale modules +npm run start:dev -w aastar + +# Start frontend (port 8080) — includes all 5 new routes +npm run dev -w aastar-frontend +``` + +### Required Environment Variables + +Copy `.env.example` to `aastar/.env` and fill in: + +```bash +# AAStar contract addresses — already set in .env for Sepolia canonical values +# Management portal reads these automatically via applyConfig() from @aastar/core + +# Only needed for /sale portal (shows "not configured" if absent) +GTOKEN_SALE_ADDRESS=0x... # GTokenSaleContract (bonding curve) — deploy first +APNTS_SALE_ADDRESS=0x... # APNTsSaleContract (fixed price $0.02) — deploy first +``` + +See `aastar/env.sepolia.example` for all Sepolia contract addresses. + +### New API Endpoints + +``` +GET /api/v1/registry/info — role counts (community/SPO/V4/enduser) +GET /api/v1/registry/role?address= — user role flags +GET /api/v1/community/list — all community admins + xPNTs token info +GET /api/v1/community/dashboard — community admin dashboard (JWT) +GET /api/v1/operator/spo/list — all SPO operators +GET /api/v1/operator/v4/list — all V4 operators +GET /api/v1/operator/dashboard — operator dashboard (JWT) +GET /api/v1/admin/protocol — registry stats + role counts +GET /api/v1/admin/roles — role configs (minStake, exitFeePercent) +GET /api/v1/admin/gtoken — GToken total supply + staking balance +GET /api/v1/sale/overview — GToken + aPNTs sale status +GET /api/v1/sale/gtoken/status — price, stage, sold%, eligibility +GET /api/v1/sale/apnts/quote?usdAmount= — aPNTs amount for USD input +``` + +### Test Results + +``` +NestJS unit tests: 34/34 PASS (registry, community, operator, admin, sale) +Foundry tests: 62/62 PASS (SaleContract 24 + GovernanceToken 10 + APNTsSaleContract 28) +Frontend build: 18 routes compiled successfully (Next.js) +``` + +See [`MANAGEMENT_PORTAL_ACCEPTANCE.md`](MANAGEMENT_PORTAL_ACCEPTANCE.md) for the full acceptance report. + +--- + ## 📄 License MIT License - See LICENSE file for details diff --git a/aastar-frontend/app/admin/page.tsx b/aastar-frontend/app/admin/page.tsx new file mode 100644 index 00000000..93c68e81 --- /dev/null +++ b/aastar-frontend/app/admin/page.tsx @@ -0,0 +1,263 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { + ShieldCheckIcon, + ArrowPathIcon, + CircleStackIcon, + CurrencyDollarIcon, +} from "@heroicons/react/24/outline"; +import Layout from "@/components/Layout"; +import { adminAPI } from "@/lib/api"; + +interface RoleConfig { + minStake: string; + entryBurn: string; + exitFeePercent: string; + isActive: boolean; + description: string; + owner: string; +} + +interface RoleEntry { + name: string; + roleId: string; + config: RoleConfig; + memberCount: string; +} + +interface RegistryStats { + registryAddress: string; + owner: string; + version: string; + roleCounts: { + communityAdmin: string; + spo: string; + v4Operator: string; + endUser: string; + }; +} + +interface GTokenStats { + address: string; + name: string; + symbol: string; + totalSupply: string; + stakingContractBalance: string; +} + +interface SystemAddresses { + registry: string; + gtoken: string; + staking: string; + superPaymaster: string; + paymasterFactory: string; + xpntsFactory: string; + sbt: string; +} + +interface Dashboard { + isAdmin: boolean; + userAddress: string | null; + registryStats: RegistryStats; + roleConfigs: RoleEntry[]; + gtokenStats: GTokenStats; + systemAddresses: SystemAddresses; + chainId: number; +} + +function StatCard({ label, value, sub }: { label: string; value: string; sub?: string }) { + return ( +
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+ ); +} + +export default function AdminPage() { + const router = useRouter(); + const [dashboard, setDashboard] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + adminAPI + .getDashboard() + .then(r => setDashboard(r.data)) + .catch(err => { + if (err.response?.status === 401) router.push("/auth/login"); + else setError("Failed to load protocol data"); + }) + .finally(() => setLoading(false)); + }, [router]); + + if (loading) { + return ( + +
+ +
+
+ ); + } + + if (!dashboard) return null; + + const { registryStats, roleConfigs, gtokenStats, systemAddresses } = dashboard; + + return ( + +
+
+

+ + Protocol Admin +

+
+ Chain {dashboard.chainId} + {dashboard.isAdmin ? ( + + Protocol Admin + + ) : ( + + Read Only + + )} +
+
+ + {error && ( +
+ {error} +
+ )} + + {/* Registry Overview */} +
+

+ + Registry Overview +

+

{registryStats.registryAddress}

+ +
+ + + + +
+ +
+ + Owner: {registryStats.owner?.slice(0, 10)}… + + Version: {registryStats.version} +
+
+ + {/* Role Configurations */} +
+

+ Role Configurations +

+
+ {roleConfigs.map(role => ( +
+
+

{role.name}

+
+ + {role.config.isActive ? "Active" : "Inactive"} + + {role.memberCount} members +
+
+
+
+

Min Stake

+

+ {parseFloat(role.config.minStake).toFixed(0)} GTOKEN +

+
+
+

Entry Burn

+

+ {parseFloat(role.config.entryBurn).toFixed(0)} GTOKEN +

+
+
+

Exit Fee

+

+ {role.config.exitFeePercent}% +

+
+
+

+ RoleId: {role.roleId} +

+
+ ))} +
+
+ + {/* GToken Stats */} +
+

+ + GToken ({gtokenStats.symbol}) +

+
+ + +
+

{gtokenStats.address}

+
+ + {/* System Contract Addresses */} +
+

+ System Contract Addresses +

+
+ {Object.entries(systemAddresses).map(([key, addr]) => ( +
+ + {key.replace(/([A-Z])/g, " $1").trim()}: + + {addr} +
+ ))} +
+
+
+
+ ); +} diff --git a/aastar-frontend/app/auth/login/page.tsx b/aastar-frontend/app/auth/login/page.tsx index 204b5999..b0d6fbcc 100644 --- a/aastar-frontend/app/auth/login/page.tsx +++ b/aastar-frontend/app/auth/login/page.tsx @@ -49,7 +49,9 @@ export default function LoginPage() { }); // Step 3: Browser WebAuthn authentication ceremony - const credential = await startAuthentication(authResponse.Options as any); + const credential = await startAuthentication({ + optionsJSON: authResponse.Options as any, + }); // Step 4: Complete login via backend (backend calls KMS SignHash to verify) toast.dismiss(loadingToast); diff --git a/aastar-frontend/app/auth/register/page.tsx b/aastar-frontend/app/auth/register/page.tsx index 0e606fee..4b406907 100644 --- a/aastar-frontend/app/auth/register/page.tsx +++ b/aastar-frontend/app/auth/register/page.tsx @@ -73,7 +73,9 @@ export default function RegisterPage() { // Step 3: Browser WebAuthn registration ceremony toast.dismiss(loadingToast); loadingToast = toast.loading("Please complete the passkey setup with your authenticator..."); - const credential = await startRegistration(beginResponse.Options as any); + const credential = await startRegistration({ + optionsJSON: beginResponse.Options as any, + }); // Step 4: Complete KMS registration → get KeyId toast.dismiss(loadingToast); diff --git a/aastar-frontend/app/community/page.tsx b/aastar-frontend/app/community/page.tsx new file mode 100644 index 00000000..248bcdb3 --- /dev/null +++ b/aastar-frontend/app/community/page.tsx @@ -0,0 +1,334 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { + BuildingOfficeIcon, + CurrencyDollarIcon, + UserGroupIcon, + CheckBadgeIcon, + XCircleIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/24/outline"; +import Layout from "@/components/Layout"; +import { communityAPI } from "@/lib/api"; + +interface CommunityMeta { + name: string; + ensName: string; + website: string; + description: string; + logoURI: string; + stakeAmount: string; +} + +interface TokenInfo { + name: string; + symbol: string; + totalSupply: string; + communityName: string; + communityOwner: string; +} + +interface CommunityEntry { + address: string; + metadata: CommunityMeta | null; + tokenAddress: string | null; +} + +interface Dashboard { + address: string; + isAdmin: boolean; + metadata: CommunityMeta | null; + gtokenBalance: string; + tokenAddress: string | null; + tokenInfo: TokenInfo | null; + xpntsBalance: string | null; +} + +function shortenAddr(addr: string) { + return `${addr.slice(0, 6)}…${addr.slice(-4)}`; +} + +function resolveImageUrl(url: string): string { + if (url.startsWith("ipfs://")) { + return url.replace("ipfs://", "https://gateway.pinata.cloud/ipfs/"); + } + return url; +} + +function CommunityCard({ entry }: { entry: CommunityEntry }) { + const name = entry.metadata?.name || shortenAddr(entry.address); + return ( +
+
+ {entry.metadata?.logoURI ? ( + // eslint-disable-next-line @next/next/no-img-element + {name} + ) : ( +
+ + {name.charAt(0).toUpperCase()} + +
+ )} +
+

{name}

+

{shortenAddr(entry.address)}

+ {entry.metadata?.description && ( +

+ {entry.metadata.description} +

+ )} +
+
+
+ {entry.tokenAddress ? ( + + + Token deployed + + ) : ( + + + No token + + )} + {entry.metadata?.stakeAmount && ( + {parseFloat(entry.metadata.stakeAmount).toFixed(0)} GToken staked + )} +
+
+ ); +} + +export default function CommunityPage() { + const router = useRouter(); + const [dashboard, setDashboard] = useState(null); + const [communities, setCommunities] = useState([]); + const [search, setSearch] = useState(""); + const [searchResult, setSearchResult] = useState<{ + address: string; + metadata: CommunityMeta | null; + tokenAddress: string | null; + tokenInfo: TokenInfo | null; + } | null>(null); + const [loading, setLoading] = useState(true); + const [searching, setSearching] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + Promise.all([ + communityAPI.getDashboard().then(r => setDashboard(r.data)), + communityAPI.getList().then(r => setCommunities(r.data)), + ]) + .catch(err => { + if (err.response?.status === 401) { + router.push("/auth/login"); + } else { + setError("Failed to load community data"); + } + }) + .finally(() => setLoading(false)); + }, [router]); + + const handleSearch = async () => { + const addr = search.trim(); + if (!addr || addr.length < 10) return; + setSearching(true); + setSearchResult(null); + try { + const r = await communityAPI.getInfo(addr); + setSearchResult(r.data); + } catch { + setError("Address not found or invalid"); + } finally { + setSearching(false); + } + }; + + if (loading) { + return ( + +
+
+
+ + ); + } + + const myName = dashboard?.metadata?.name || (dashboard?.address ? shortenAddr(dashboard.address) : "—"); + + return ( + +
+

+ + Community Portal +

+ + {error && ( +
+ {error} +
+ )} + + {/* My Community Dashboard */} + {dashboard && ( +
+
+

+ My Community Status +

+ {dashboard.isAdmin ? ( + + Community Admin + + ) : ( + + Not a Community Admin + + )} +
+ +
+
+

GToken Balance

+

+ {parseFloat(dashboard.gtokenBalance || "0").toFixed(2)} +

+

GTOKEN

+
+ + {dashboard.isAdmin && dashboard.metadata && ( +
+

Community Name

+

+ {myName} +

+

+ Staked: {parseFloat(dashboard.metadata.stakeAmount).toFixed(0)} GTOKEN +

+
+ )} + + {dashboard.isAdmin && dashboard.tokenAddress && dashboard.tokenInfo && ( +
+

xPNTs Token

+

+ {dashboard.tokenInfo.symbol} +

+

+ Supply: {parseFloat(dashboard.tokenInfo.totalSupply).toLocaleString()} +

+
+ )} +
+ + {dashboard.isAdmin && !dashboard.tokenAddress && ( +
+

+ No xPNTs Token Deployed +

+

+ Deploy your community token via the xPNTs factory to enable loyalty points for + your members. +

+
+ )} + + {!dashboard.isAdmin && ( +
+

+ Register as Community Admin +

+

+ To become a Community Admin, stake 30 GToken on the Registry contract. You need + the ROLE_COMMUNITY{" "} + role. Connect your EOA wallet to proceed. +

+
+ )} +
+ )} + + {/* Address Lookup */} +
+

+ + Lookup Community by Address +

+
+ setSearch(e.target.value)} + onKeyDown={e => e.key === "Enter" && handleSearch()} + placeholder="0x..." + className="flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-slate-900 dark:focus:ring-emerald-500" + /> + +
+ + {searchResult && ( +
+
+

+ {searchResult.metadata?.name || shortenAddr(searchResult.address)} +

+ {searchResult.metadata ? ( + + Community Admin + + ) : ( + + Not registered + + )} +
+

{searchResult.address}

+ {searchResult.metadata?.description && ( +

{searchResult.metadata.description}

+ )} + {searchResult.tokenAddress && searchResult.tokenInfo && ( +
+ + + {searchResult.tokenInfo.symbol} — Supply:{" "} + {parseFloat(searchResult.tokenInfo.totalSupply).toLocaleString()} + +
+ )} +
+ )} +
+ + {/* Community List */} +
+

+ + All Community Admins + + ({communities.length}) + +

+ {communities.length === 0 ? ( +

No communities found.

+ ) : ( +
+ {communities.map(c => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/aastar-frontend/app/dashboard/page.tsx b/aastar-frontend/app/dashboard/page.tsx index 545d972d..fd0fe001 100644 --- a/aastar-frontend/app/dashboard/page.tsx +++ b/aastar-frontend/app/dashboard/page.tsx @@ -557,8 +557,11 @@ function DashboardContent() {
-

- 🎉 Paymaster Status +

+ + + + Paymaster Status

diff --git a/aastar-frontend/app/layout.tsx b/aastar-frontend/app/layout.tsx index 1f5b1d7b..83a76a4c 100644 --- a/aastar-frontend/app/layout.tsx +++ b/aastar-frontend/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import { Toaster } from "react-hot-toast"; @@ -7,21 +7,22 @@ import { DashboardProvider } from "@/contexts/DashboardContext"; const inter = Inter({ subsets: ["latin"] }); -export const metadata: Metadata = { - title: "AAStar - ERC4337 Account Abstraction", - description: "ERC4337 Account Abstraction with BLS Signatures", - manifest: "/manifest.json", +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 5, + userScalable: true, + viewportFit: "cover", themeColor: [ { media: "(prefers-color-scheme: light)", color: "#f8fafc" }, { media: "(prefers-color-scheme: dark)", color: "#0f172a" }, ], - viewport: { - width: "device-width", - initialScale: 1, - maximumScale: 5, - userScalable: true, - viewportFit: "cover", - }, +}; + +export const metadata: Metadata = { + title: "AAStar - ERC4337 Account Abstraction", + description: "ERC4337 Account Abstraction with BLS Signatures", + manifest: "/manifest.json", appleWebApp: { capable: true, statusBarStyle: "default", @@ -30,7 +31,6 @@ export const metadata: Metadata = { icons: { icon: [ { url: "/favicon.ico" }, - { url: "/icon-192.png", sizes: "192x192", type: "image/png" }, { url: "/icon-512.png", sizes: "512x512", type: "image/png" }, ], apple: [{ url: "/apple-icon.png", sizes: "180x180", type: "image/png" }], diff --git a/aastar-frontend/app/operator/page.tsx b/aastar-frontend/app/operator/page.tsx new file mode 100644 index 00000000..d9971da8 --- /dev/null +++ b/aastar-frontend/app/operator/page.tsx @@ -0,0 +1,293 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { + ServerStackIcon, + CurrencyDollarIcon, + CheckBadgeIcon, + XCircleIcon, + ArrowPathIcon, +} from "@heroicons/react/24/outline"; +import Layout from "@/components/Layout"; +import { operatorAPI } from "@/lib/api"; + +interface SPOStatus { + hasRole: boolean; + isConfigured: boolean; + balance: string; + exchangeRate: string; + treasury: string; + stakeAmount: string; +} + +interface V4Status { + paymasterAddress: string | null; + balance: string; + hasRole: boolean; +} + +interface Dashboard { + address: string; + isSPO: boolean; + isV4Operator: boolean; + gtokenBalance: string; + spoStatus: SPOStatus | null; + v4Status: V4Status; + registryAddress: string; + superPaymasterAddress: string; + paymasterFactoryAddress: string; + stakingAddress: string; +} + +function shortenAddr(addr: string) { + return `${addr.slice(0, 6)}…${addr.slice(-4)}`; +} + +function StatusBadge({ active, label }: { active: boolean; label: string }) { + return ( + + {active ? ( + + ) : ( + + )} + {label} + + ); +} + +function MetricCard({ + label, + value, + unit, + sub, +}: { + label: string; + value: string; + unit?: string; + sub?: string; +}) { + return ( +
+

{label}

+

+ {parseFloat(value || "0").toFixed(4)} +

+ {unit &&

{unit}

} + {sub &&

{sub}

} +
+ ); +} + +export default function OperatorPage() { + const router = useRouter(); + const [dashboard, setDashboard] = useState(null); + const [spoList, setSpoList] = useState([]); + const [v4List, setV4List] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + Promise.all([ + operatorAPI.getDashboard().then(r => setDashboard(r.data)), + operatorAPI.getSPOList().then(r => setSpoList(r.data)), + operatorAPI.getV4List().then(r => setV4List(r.data)), + ]) + .catch(err => { + if (err.response?.status === 401) router.push("/auth/login"); + else setError("Failed to load operator data"); + }) + .finally(() => setLoading(false)); + }, [router]); + + if (loading) { + return ( + +
+ +
+
+ ); + } + + return ( + +
+

+ + Operator Portal +

+ + {error && ( +
+ {error} +
+ )} + + {/* My Operator Status */} + {dashboard && ( +
+
+

+ My Operator Status +

+
+ + +
+
+ + {/* GToken Balance */} +
+ + + {dashboard.isSPO && dashboard.spoStatus && ( + <> + + + + + )} + + {dashboard.v4Status.paymasterAddress && ( + + )} +
+ + {/* SPO Registration Guide */} + {!dashboard.isSPO && ( +
+

+ Register as SPO (SuperPaymaster Operator) +

+
    +
  1. Approve GToken to GTokenStaking contract
  2. +
  3. + Call{" "} + + registerRoleSelf + {" "} + on Registry with ROLE_PAYMASTER_AOA +
  4. +
  5. Deposit aPNTs to SuperPaymaster via depositFor
  6. +
  7. Call configureOperator with xPNTs token + treasury + exchange rate
  8. +
+
+ )} + + {/* SPO configured but not V4 */} + {!dashboard.v4Status.paymasterAddress && ( +
+

+ Deploy V4 Paymaster +

+
    +
  1. + Call{" "} + + deployPaymaster + {" "} + on PaymasterFactory +
  2. +
  3. + Register with{" "} + + ROLE_PAYMASTER_SUPER + {" "} + on Registry +
  4. +
+
+ )} + + {/* Contract Addresses */} +
+

Contract Addresses

+
+ {[ + ["Registry", dashboard.registryAddress], + ["SuperPaymaster", dashboard.superPaymasterAddress], + ["PaymasterFactory", dashboard.paymasterFactoryAddress], + ["GTokenStaking", dashboard.stakingAddress], + ].map(([label, addr]) => ( +
+ {label}: + {addr} +
+ ))} +
+
+
+ )} + + {/* Operator Lists */} +
+
+

+ SPO Operators + ({spoList.length}) +

+ {spoList.length === 0 ? ( +

None registered

+ ) : ( +
    + {spoList.map(addr => ( +
  • + {addr} +
  • + ))} +
+ )} +
+ +
+

+ V4 Operators + ({v4List.length}) +

+ {v4List.length === 0 ? ( +

None registered

+ ) : ( +
    + {v4List.map(addr => ( +
  • + {addr} +
  • + ))} +
+ )} +
+
+
+
+ ); +} diff --git a/aastar-frontend/app/role/page.tsx b/aastar-frontend/app/role/page.tsx new file mode 100644 index 00000000..6a39c9ef --- /dev/null +++ b/aastar-frontend/app/role/page.tsx @@ -0,0 +1,286 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Layout from "@/components/Layout"; +import { registryAPI } from "@/lib/api"; +import { getStoredAuth } from "@/lib/auth"; +import toast from "react-hot-toast"; +import { + ShieldCheckIcon, + UserGroupIcon, + CpuChipIcon, + KeyIcon, + InformationCircleIcon, +} from "@heroicons/react/24/outline"; + +interface RoleInfo { + address: string; + isAdmin: boolean; + isCommunityAdmin: boolean; + isSPO: boolean; + isV4Operator: boolean; + isEndUser: boolean; + roleIds: string[]; + gtokenBalance: string; +} + +interface RegistryInfo { + registryAddress: string; + chainId: number; + roleCounts: { + communityAdmin: string; + spo: string; + v4Operator: string; + endUser: string; + }; +} + +function formatGToken(wei: string): string { + try { + const bigWei = BigInt(wei); + const ether = Number(bigWei) / 1e18; + return ether.toFixed(4); + } catch { + return "0"; + } +} + +const ROLE_COLOR_MAP: Record = { + purple: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200", + green: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200", + blue: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200", + orange: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200", + gray: "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200", +}; + +function RoleBadge({ label, active, color }: { label: string; active: boolean; color: string }) { + const activeClasses = ROLE_COLOR_MAP[color] ?? ROLE_COLOR_MAP.gray; + return ( + + {active && } + {label} + + ); +} + +function truncate(addr: string) { + if (!addr) return ""; + return addr.slice(0, 8) + "..." + addr.slice(-6); +} + +export default function RolePage() { + const router = useRouter(); + const [roleInfo, setRoleInfo] = useState(null); + const [registryInfo, setRegistryInfo] = useState(null); + const [queryAddress, setQueryAddress] = useState(""); + const [loading, setLoading] = useState(true); + const [querying, setQuerying] = useState(false); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + setLoading(true); + try { + const [roleRes, infoRes] = await Promise.all([ + registryAPI.getRole().catch(() => null), + registryAPI.getInfo(), + ]); + if (roleRes) setRoleInfo(roleRes.data); + setRegistryInfo(infoRes.data); + } catch (err) { + toast.error("Failed to load registry data"); + } finally { + setLoading(false); + } + }; + + const queryRole = async () => { + if (!queryAddress) return; + setQuerying(true); + try { + const res = await registryAPI.getRole(queryAddress); + setRoleInfo(res.data); + } catch (err) { + toast.error("Failed to query role for address"); + } finally { + setQuerying(false); + } + }; + + const navigateTo = (path: string) => router.push(path); + + return ( + +
+ {/* Header */} +
+

Role Portal

+

+ View your role in the AAStar ecosystem and navigate to role-specific dashboards +

+
+ + {/* Registry Info */} + {registryInfo && ( +
+
+ +

+ Registry Overview (Sepolia) +

+
+
+ {[ + { label: "Community Admins", value: registryInfo.roleCounts.communityAdmin }, + { label: "SPO Operators", value: registryInfo.roleCounts.spo }, + { label: "V4 Operators", value: registryInfo.roleCounts.v4Operator }, + { label: "End Users", value: registryInfo.roleCounts.endUser }, + ].map(item => ( +
+
+ {item.value} +
+
{item.label}
+
+ ))} +
+
+ Registry: {truncate(registryInfo.registryAddress)} +
+
+ )} + + {/* Address Query */} +
+

+ Query Role for Address +

+
+ setQueryAddress(e.target.value)} + className="flex-1 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + +
+
+ + {/* Current Role Info */} + {loading ? ( +
Loading...
+ ) : roleInfo ? ( +
+
+

+ Role Status for {truncate(roleInfo.address)} +

+ + {formatGToken(roleInfo.gtokenBalance)} GT + +
+ +
+ + + + + +
+ + {/* Navigation Cards */} +
+ {roleInfo.isCommunityAdmin && ( + + )} + + {(roleInfo.isSPO || roleInfo.isV4Operator) && ( + + )} + + {roleInfo.isAdmin && ( + + )} + + {roleInfo.isEndUser && ( + + )} + + {!roleInfo.isAdmin && !roleInfo.isCommunityAdmin && !roleInfo.isSPO && !roleInfo.isV4Operator && !roleInfo.isEndUser && ( +
+ +

No roles found for this address

+

Purchase GToken to register a role

+
+ )} +
+
+ ) : null} +
+
+ ); +} diff --git a/aastar-frontend/app/sale/page.tsx b/aastar-frontend/app/sale/page.tsx new file mode 100644 index 00000000..207dfdca --- /dev/null +++ b/aastar-frontend/app/sale/page.tsx @@ -0,0 +1,356 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { + CurrencyDollarIcon, + ArrowPathIcon, + CheckBadgeIcon, + XCircleIcon, + ArrowTrendingUpIcon, +} from "@heroicons/react/24/outline"; +import Layout from "@/components/Layout"; +import { saleAPI } from "@/lib/api"; + +interface GTokenSaleStatus { + configured: boolean; + address: string | null; + currentPriceUSD: string; + ceilingPriceUSD: string; + tokensSold: string; + totalForSale: string; + soldPercent: number; + currentStage: number; + saleEnded: boolean; + stage1Limit: string; + stage2Limit: string; +} + +interface APNTsSaleStatus { + configured: boolean; + address: string | null; + priceUSD: string; + totalSold: string; + availableInventory: string; + minPurchaseAmount: string; + maxPurchaseAmount: string; + saleActive: boolean; +} + +interface GTokenEligibility { + address: string; + eligible: boolean; + hasBought: boolean; + reason: string | null; +} + +function ProgressBar({ percent }: { percent: number }) { + const stage1Pct = 20; // 210k / 1050k + const stage2Pct = 60; // 630k / 1050k + + return ( +
+
+ {/* Stage markers */} +
+
+
+ ); +} + +export default function SalePage() { + const router = useRouter(); + const [gTokenStatus, setGTokenStatus] = useState(null); + const [aPNTsStatus, setAPNTsStatus] = useState(null); + const [eligibility, setEligibility] = useState(null); + const [apntsQuote, setApntsQuote] = useState<{ usdIn: string; aPNTsOut: string } | null>(null); + const [quoteUSD, setQuoteUSD] = useState("100"); + const [loading, setLoading] = useState(true); + const [quoteLoading, setQuoteLoading] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + Promise.all([ + saleAPI.getGTokenStatus().then(r => setGTokenStatus(r.data)), + saleAPI.getAPNTsStatus().then(r => setAPNTsStatus(r.data)), + saleAPI.getGTokenEligibility().then(r => setEligibility(r.data)).catch(() => null), + ]) + .catch(err => { + if (err.response?.status === 401) router.push("/auth/login"); + else setError("Failed to load sale data"); + }) + .finally(() => setLoading(false)); + }, [router]); + + const fetchQuote = async () => { + if (!quoteUSD || parseFloat(quoteUSD) <= 0) return; + setQuoteLoading(true); + try { + const r = await saleAPI.getAPNTsQuote(quoteUSD); + setApntsQuote(r.data); + } catch { + setError("Failed to get quote"); + } finally { + setQuoteLoading(false); + } + }; + + if (loading) { + return ( + +
+ +
+
+ ); + } + + return ( + +
+

+ + Token Sale +

+ + {error && ( +
+ {error} +
+ )} + + {/* GToken Sale */} +
+
+

+ + GToken Sale +

+ {gTokenStatus?.configured ? ( + gTokenStatus.saleEnded ? ( + + Sale Ended + + ) : ( + + Stage {gTokenStatus.currentStage} Active + + ) + ) : ( + + Not Configured + + )} +
+ + {gTokenStatus?.configured ? ( + <> + {/* Price Info */} +
+
+

Current Price

+

+ ${parseFloat(gTokenStatus.currentPriceUSD).toFixed(4)} +

+

per GToken

+
+
+

Tokens Sold

+

+ {parseFloat(gTokenStatus.tokensSold).toLocaleString(undefined, { + maximumFractionDigits: 0, + })} +

+

+ of{" "} + {parseFloat(gTokenStatus.totalForSale).toLocaleString(undefined, { + maximumFractionDigits: 0, + })} +

+
+
+

Ceiling Price

+

+ ${parseFloat(gTokenStatus.ceilingPriceUSD).toFixed(4)} +

+

max price

+
+
+ + {/* Progress */} +
+
+ Stage 1 (S1) + Stage 2 + Stage 3 +
+ +

+ {gTokenStatus.soldPercent.toFixed(1)}% sold +

+
+ + {/* Eligibility */} + {eligibility && ( +
+
+ {eligibility.eligible ? ( + + ) : ( + + )} +

+ {eligibility.eligible ? "You are eligible to buy GTokens" : eligibility.reason} +

+
+ {eligibility.eligible && ( +

+ Connect your wallet and call{" "} + + buyTokens(usdAmount, paymentToken, signature) + {" "} + on the sale contract. +

+ )} +
+ )} + +

+ Contract: {gTokenStatus.address} +

+ + ) : ( +

+ GToken sale contract not configured. Set GTOKEN_SALE_ADDRESS in env. +

+ )} +
+ + {/* aPNTs Sale */} +
+
+

+ + aPNTs Sale +

+ {aPNTsStatus?.configured ? ( + aPNTsStatus.saleActive ? ( + + Active + + ) : ( + + No Inventory + + ) + ) : ( + + Not Configured + + )} +
+ + {aPNTsStatus?.configured ? ( + <> +
+
+

Price

+

+ ${parseFloat(aPNTsStatus.priceUSD).toFixed(4)} +

+

per aPNTs (fixed)

+
+
+

Available

+

+ {parseFloat(aPNTsStatus.availableInventory).toLocaleString(undefined, { + maximumFractionDigits: 0, + })} +

+

aPNTs

+
+
+

Limits

+

+ {parseFloat(aPNTsStatus.minPurchaseAmount).toFixed(0)} –{" "} + {parseFloat(aPNTsStatus.maxPurchaseAmount).toLocaleString(undefined, { + maximumFractionDigits: 0, + })} +

+

aPNTs per purchase

+
+
+ + {/* Quote calculator */} +
+

+ Price Calculator +

+
+
+ + $ + + setQuoteUSD(e.target.value)} + onKeyDown={e => e.key === "Enter" && fetchQuote()} + placeholder="100" + className="w-full pl-7 pr-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> +
+ +
+ {apntsQuote && ( +
+ ${apntsQuote.usdIn} USDC + + + {parseFloat(apntsQuote.aPNTsOut).toLocaleString()} aPNTs + +
+ )} +
+ +

+ Contract: {aPNTsStatus.address} +

+ + ) : ( +

+ aPNTs sale contract not configured. Set APNTS_SALE_ADDRESS in env. +

+ )} +
+
+
+ ); +} diff --git a/aastar-frontend/app/transfer/page.tsx b/aastar-frontend/app/transfer/page.tsx index 6bc2706b..c7af0f31 100644 --- a/aastar-frontend/app/transfer/page.tsx +++ b/aastar-frontend/app/transfer/page.tsx @@ -70,10 +70,34 @@ export default function TransferPage() { setSavedPaymasters([]); } - // Load address book + // Load address book + recent transfer recipients try { - const addressBookResponse = await addressBookAPI.getAddressBook(); - setAddressBook(addressBookResponse.data); + const [addressBookResponse, historyResponse] = await Promise.all([ + addressBookAPI.getAddressBook().catch(() => ({ data: [] })), + transferAPI.getHistory(1, 50).catch(() => ({ data: { transfers: [] } })), + ]); + + const bookEntries = addressBookResponse.data || []; + const bookAddresses = new Set(bookEntries.map((e: any) => e.address.toLowerCase())); + + // Extract unique recent recipients not already in address book + const recentAddresses: any[] = []; + const seen = new Set(); + for (const tx of historyResponse.data.transfers || []) { + const lower = tx.to.toLowerCase(); + if (!bookAddresses.has(lower) && !seen.has(lower)) { + seen.add(lower); + recentAddresses.push({ + address: tx.to, + name: "", + lastUsed: tx.createdAt, + usageCount: 1, + isRecent: true, + }); + } + } + + setAddressBook([...bookEntries, ...recentAddresses]); } catch (error) { console.error("Failed to load address book:", error); setAddressBook([]); @@ -244,7 +268,9 @@ export default function TransferPage() { // Step 2: Browser WebAuthn authentication ceremony toast.dismiss(loadingToast); loadingToast = toast.loading("Please verify with your passkey..."); - const credential = await startAuthentication(authResponse.Options as any); + const credential = await startAuthentication({ + optionsJSON: authResponse.Options as any, + }); // Step 3: Extract Legacy assertion (reusable for BLS dual-signing) const passkeyAssertion = await kmsClient.extractLegacyAssertion(credential); @@ -724,6 +750,21 @@ export default function TransferPage() {

)} + {transferStatus?.actualGasUsed && ( +

+ Gas used: {parseInt(transferStatus.actualGasUsed, 16).toLocaleString()} + {transferStatus.actualGasCost && ( + + Cost: {(parseInt(transferStatus.actualGasCost, 16) / 1e18).toFixed(8)} ETH + + )} + {transferStatus.retryCount > 0 && ( + + (retried {transferStatus.retryCount}x) + + )} +

+ )} {transferStatus?.bundlerUserOpHash && !transferStatus?.transactionHash && (

Bundler processing transaction... @@ -757,7 +798,7 @@ export default function TransferPage() { className="flex items-center justify-between w-full px-4 py-3 text-base bg-gray-50 dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-slate-900 dark:focus:ring-emerald-400 transition-all touch-manipulation active:scale-[0.98]" > - 📖 Choose from address book ({addressBook.length}) + Choose from saved & recent addresses ({addressBook.length})

- {entry.name && ( + {entry.name ? (
{entry.name}
- )} + ) : entry.isRecent ? ( +
+ Recent +
+ ) : null}
{entry.address}
diff --git a/aastar-frontend/components/Layout.tsx b/aastar-frontend/components/Layout.tsx index 15177eef..44b517a0 100644 --- a/aastar-frontend/components/Layout.tsx +++ b/aastar-frontend/components/Layout.tsx @@ -17,6 +17,9 @@ import { ChevronRightIcon, ChevronDownIcon, BookOpenIcon, + ShieldCheckIcon, + UserGroupIcon, + ServerStackIcon, } from "@heroicons/react/24/outline"; interface LayoutProps { @@ -90,7 +93,6 @@ export default function Layout({ children, requireAuth = false }: LayoutProps) {
-

AAStar

{/* Desktop Navigation */} @@ -101,6 +103,36 @@ export default function Layout({ children, requireAuth = false }: LayoutProps) { > Dashboard + + + + + + {/* My Role */} + + + {/* Community */} + + + {/* Operator */} + + {/* Transfer */}