Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions docs/smart-contract-integration.md
Original file line number Diff line number Diff line change
@@ -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/<ContractName>.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`).
35 changes: 35 additions & 0 deletions src/app/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Skeleton } from "@/components/ui/skeleton";

export default function Loading() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6 space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-10 w-32" />
</div>

{/* Portfolio overview */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="bg-white dark:bg-gray-800 rounded-xl p-4 border border-gray-200 dark:border-gray-700 space-y-2"
>
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-32" />
</div>
))}
</div>

{/* Charts row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Skeleton className="h-72 rounded-xl" />
<Skeleton className="h-72 rounded-xl" />
</div>

{/* Transactions */}
<Skeleton className="h-56 rounded-xl" />
</div>
);
}
24 changes: 24 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
30 changes: 30 additions & 0 deletions src/app/properties/[id]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Skeleton } from "@/components/ui/skeleton";

export default function Loading() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* Back button + title */}
<div className="space-y-2">
<Skeleton className="h-8 w-24" />
<Skeleton className="h-10 w-96" />
</div>

{/* Main content grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left column - images + details */}
<div className="lg:col-span-2 space-y-6">
<Skeleton className="h-96 rounded-xl" />
<Skeleton className="h-64 rounded-xl" />
</div>

{/* Right column - purchase card */}
<div className="space-y-6">
<Skeleton className="h-80 rounded-xl" />
<Skeleton className="h-48 rounded-xl" />
</div>
</div>
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions src/app/properties/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import PropertyPageSkeleton from "@/components/PropertyPageSkeleton";

export default function Loading() {
return <PropertyPageSkeleton />;
}
57 changes: 57 additions & 0 deletions src/components/PropertyPageSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Header skeleton */}
<header className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 sticky top-0 z-30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<Skeleton className="h-8 w-32" />
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-24" />
<Skeleton className="h-8 w-24" />
<Skeleton className="h-8 w-8 rounded-full" />
</div>
</div>
</div>
</header>

<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Title + filter skeleton */}
<div className="mb-8 space-y-4">
<Skeleton className="h-9 w-72" />
<div className="flex flex-wrap gap-3">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-28 rounded-lg" />
))}
</div>
</div>

{/* Property card grid skeleton */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 9 }).map((_, i) => (
<div
key={i}
className="bg-white dark:bg-gray-800 rounded-xl overflow-hidden border border-gray-200 dark:border-gray-700"
>
<Skeleton className="h-48 w-full rounded-none" />
<div className="p-4 space-y-3">
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<div className="flex justify-between">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-6 w-16" />
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
119 changes: 118 additions & 1 deletion src/utils/security/auditLogger.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down