diff --git a/docs/smart-contract-integration.md b/docs/smart-contract-integration.md new file mode 100644 index 00000000..070f97eb --- /dev/null +++ b/docs/smart-contract-integration.md @@ -0,0 +1,98 @@ +# Smart Contract Integration Guide + +This document describes how the PropChain frontend integrates with smart contracts. + +## Contract Addresses + +Contract addresses are configured per network in `src/config/chains.ts`. The supported networks are: + +| Network | Chain ID | Explorer | +|---------|----------|----------| +| Ethereum Mainnet | 1 | https://etherscan.io | +| Polygon | 137 | https://polygonscan.com | +| Binance Smart Chain | 56 | https://bscscan.com | + +Set contract addresses via environment variables: + +```env +NEXT_PUBLIC_PROPERTY_NFT_ADDRESS=0x... +NEXT_PUBLIC_MARKETPLACE_ADDRESS=0x... +NEXT_PUBLIC_STAKING_ADDRESS=0x... +``` + +## ABI Files + +ABI files live in `src/lib/abis/`. To update an ABI after a contract upgrade: + +1. Export the ABI from your Hardhat/Foundry build artifacts. +2. Place the JSON file in `src/lib/abis/.json`. +3. Import it where needed: + +```ts +import PropertyNFT from '@/lib/abis/PropertyNFT.json'; +``` + +## Event Listeners + +The frontend subscribes to contract events using `wagmi`'s `useWatchContractEvent` hook. Key events and their handlers: + +| Event | Contract | Handler location | +|-------|----------|-----------------| +| `Transfer` | PropertyNFT | `src/hooks/useTransaction.ts` | +| `PropertyListed` | Marketplace | `src/hooks/usePropertySearch.ts` | +| `PropertySold` | Marketplace | `src/store/transactionStore.ts` | +| `RewardDistributed` | Staking | `src/hooks/useRewardDistribution.ts` | + +Example listener setup: + +```ts +import { useWatchContractEvent } from 'wagmi'; +import PropertyNFT from '@/lib/abis/PropertyNFT.json'; + +useWatchContractEvent({ + address: process.env.NEXT_PUBLIC_PROPERTY_NFT_ADDRESS as `0x${string}`, + abi: PropertyNFT, + eventName: 'Transfer', + onLogs(logs) { + // handle transfer event + }, +}); +``` + +## Transaction Flow + +``` +User action + → useTransaction hook (src/hooks/useTransaction.ts) + → wagmi writeContract / sendTransaction + → TransactionProgress component shows status + → On confirmation: update store + emit notification + → On failure: error boundary + retry via useTxRetry +``` + +### Purchase flow + +1. User clicks "Buy" on a property card. +2. `useTransaction` calls `writeContract` with the Marketplace ABI. +3. `TransactionProgress` (`src/components/TransactionProgress.tsx`) polls for receipt. +4. On success, `transactionStore` is updated and a notification is dispatched. +5. On failure, `useTxRetry` (`src/hooks/useTxRetry.ts`) handles automatic retries. + +## Error Codes + +Common contract revert reasons and how the frontend handles them: + +| Revert reason | User-facing message | Handler | +|---------------|--------------------|---------| +| `InsufficientFunds` | "Insufficient balance for this purchase" | `src/utils/errorFactory.ts` | +| `PropertyNotAvailable` | "This property is no longer available" | `src/utils/errorFactory.ts` | +| `KYCRequired` | "KYC verification required" | Redirects to KYC flow | +| `RateLimitExceeded` | "Too many requests, please wait" | `src/utils/security/rateLimiter.ts` | +| `Unauthorized` | "You are not authorized for this action" | `src/utils/errorHandling.ts` | + +## Security Considerations + +- All transaction parameters are validated client-side before submission (see `src/utils/security/blockchainSecurity.ts`). +- High-value transactions trigger an additional confirmation step via `TransactionConfirmation` component. +- All contract interactions are logged via `auditLogger` (see `src/utils/security/auditLogger.ts`). +- Phishing protection checks the current domain before any wallet interaction (see `src/utils/security/phishingProtection.ts`). diff --git a/src/app/dashboard/loading.tsx b/src/app/dashboard/loading.tsx new file mode 100644 index 00000000..59f6c1a1 --- /dev/null +++ b/src/app/dashboard/loading.tsx @@ -0,0 +1,35 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function Loading() { + return ( +
+ {/* Header */} +
+ + +
+ + {/* Portfolio overview */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+ + {/* Charts row */} +
+ + +
+ + {/* Transactions */} + +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 15a45ef4..9cd8b104 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -30,6 +30,30 @@ body { @import "tw-animate-css"; @import "../styles/mobile.css"; +/* Tailwind v4: explicit source scanning for all app code */ +@source "../../src/**/*.{ts,tsx}"; +@source "../../src/**/*.css"; + +/* + * Safelist: classes constructed dynamically at runtime that Tailwind's + * static scanner cannot detect. Add full class names here – never + * partial strings like "bg-{color}". + * + * Pattern: status / severity badges built from lookup objects + */ +@layer utilities { + /* Risk / severity colours used in audit-log and security components */ + .text-risk-low { color: oklch(0.627 0.194 142.495); } /* green */ + .text-risk-medium { color: oklch(0.769 0.188 70.08); } /* amber */ + .text-risk-high { color: oklch(0.577 0.245 27.325); } /* red */ + .text-risk-critical { color: oklch(0.5 0.28 20); } /* deep-red */ + + .bg-risk-low { background-color: oklch(0.95 0.05 142); } + .bg-risk-medium { background-color: oklch(0.97 0.05 80); } + .bg-risk-high { background-color: oklch(0.97 0.05 27); } + .bg-risk-critical { background-color: oklch(0.93 0.08 20); } +} + @custom-variant dark (&:is(.dark *)); @theme inline { diff --git a/src/app/properties/[id]/loading.tsx b/src/app/properties/[id]/loading.tsx new file mode 100644 index 00000000..d0d73241 --- /dev/null +++ b/src/app/properties/[id]/loading.tsx @@ -0,0 +1,30 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function Loading() { + return ( +
+
+ {/* Back button + title */} +
+ + +
+ + {/* Main content grid */} +
+ {/* Left column - images + details */} +
+ + +
+ + {/* Right column - purchase card */} +
+ + +
+
+
+
+ ); +} diff --git a/src/app/properties/loading.tsx b/src/app/properties/loading.tsx new file mode 100644 index 00000000..5f6122be --- /dev/null +++ b/src/app/properties/loading.tsx @@ -0,0 +1,5 @@ +import PropertyPageSkeleton from "@/components/PropertyPageSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/components/PropertyPageSkeleton.tsx b/src/components/PropertyPageSkeleton.tsx new file mode 100644 index 00000000..02e45968 --- /dev/null +++ b/src/components/PropertyPageSkeleton.tsx @@ -0,0 +1,57 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +/** + * Skeleton fallback for the properties listing page. + * Matches the layout of PropertiesContent so there is no layout shift. + */ +export default function PropertyPageSkeleton() { + return ( +
+ {/* Header skeleton */} +
+
+
+ +
+ + + +
+
+
+
+ +
+ {/* Title + filter skeleton */} +
+ +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+
+ + {/* Property card grid skeleton */} +
+ {Array.from({ length: 9 }).map((_, i) => ( +
+ +
+ + +
+ + +
+
+
+ ))} +
+
+
+ ); +} diff --git a/src/utils/security/auditLogger.ts b/src/utils/security/auditLogger.ts index 4e7caa53..d75f3975 100644 --- a/src/utils/security/auditLogger.ts +++ b/src/utils/security/auditLogger.ts @@ -1,7 +1,7 @@ export interface AuditLogEntry { id: string; timestamp: number; - eventType: 'wallet_connection' | 'wallet_disconnection' | 'transaction_signing' | 'signature_request' | 'network_switch' | 'account_switch' | 'security_alert'; + eventType: 'wallet_connection' | 'wallet_disconnection' | 'transaction_signing' | 'signature_request' | 'network_switch' | 'account_switch' | 'security_alert' | 'auth_failure' | 'settings_change' | 'kyc_status_change' | 'transaction_initiation' | 'transaction_completion'; userId?: string; walletAddress?: string; chainId?: number; @@ -278,6 +278,123 @@ export class SecurityAuditLogger { .slice(0, limit); } + /** + * Logs a failed authentication attempt + */ + logAuthFailure( + reason: string, + walletAddress?: string, + userId?: string + ): void { + const entry: AuditLogEntry = { + id: this.generateId(), + timestamp: Date.now(), + eventType: 'auth_failure', + userId, + walletAddress, + details: { reason }, + riskScore: 40, + userAgent: navigator.userAgent, + sessionId: this.sessionId, + }; + this.addLog(entry); + } + + /** + * Logs a user settings change + */ + logSettingsChange( + setting: string, + previousValue: unknown, + newValue: unknown, + walletAddress?: string, + userId?: string + ): void { + const entry: AuditLogEntry = { + id: this.generateId(), + timestamp: Date.now(), + eventType: 'settings_change', + userId, + walletAddress, + details: { setting, previousValue, newValue }, + riskScore: 5, + userAgent: navigator.userAgent, + sessionId: this.sessionId, + }; + this.addLog(entry); + } + + /** + * Logs a KYC status change + */ + logKycStatusChange( + previousStatus: string, + newStatus: string, + walletAddress?: string, + userId?: string + ): void { + const entry: AuditLogEntry = { + id: this.generateId(), + timestamp: Date.now(), + eventType: 'kyc_status_change', + userId, + walletAddress, + details: { previousStatus, newStatus }, + riskScore: 10, + userAgent: navigator.userAgent, + sessionId: this.sessionId, + }; + this.addLog(entry); + } + + /** + * Logs a transaction initiation event + */ + logTransactionInitiation( + from: string, + to: string, + value: string, + txType: string, + userId?: string + ): void { + const entry: AuditLogEntry = { + id: this.generateId(), + timestamp: Date.now(), + eventType: 'transaction_initiation', + userId, + walletAddress: from, + details: { to, value, txType }, + riskScore: 15, + userAgent: navigator.userAgent, + sessionId: this.sessionId, + }; + this.addLog(entry); + } + + /** + * Logs a transaction completion (success or failure) + */ + logTransactionCompletion( + txHash: string, + from: string, + success: boolean, + errorMessage?: string, + userId?: string + ): void { + const entry: AuditLogEntry = { + id: this.generateId(), + timestamp: Date.now(), + eventType: 'transaction_completion', + userId, + walletAddress: from, + details: { txHash, success, errorMessage }, + riskScore: success ? 0 : 20, + userAgent: navigator.userAgent, + sessionId: this.sessionId, + }; + this.addLog(entry); + } + /** * Exports logs for analysis */