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
11 changes: 7 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,15 @@ RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_MAX_REQUESTS_PER_WALLET=50

# -----------------------------------------------------------------------------
# Webhook Configuration
# -----------------------------------------------------------------------------
# Chainalysis Security API Configuration
# -----------------------------------------------------------------------------
# API key for Chainalysis address/transaction risk checks.
# IMPORTANT: This is read server-side only. Never expose it to the browser.
# CHAINALYSIS_API_KEY=your_chainalysis_api_key

# Secret used to sign and verify revalidation webhook requests
# This must be set in production to secure the /api/revalidate endpoint
REVALIDATE_WEBHOOK_SECRET=your-webhook-secret
# Chainalysis API base URL (optional, uses default if not set)
# CHAINALYSIS_API_URL=https://api.chainalysis.com/api/v2

# -----------------------------------------------------------------------------
# Secret Management (Production)
Expand Down
20 changes: 8 additions & 12 deletions src/components/PropertyCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,24 @@ const PropertyCardInner: React.FC<PropertyCardProps> = ({
const compareLimitReached = selectedIds.length >= 3 && !isCompared;
const { addFavorite, removeFavorite, isFavorite } = useFavoritesStore();

const handleAddToCart = useCallback((e: React.MouseEvent) => {
e.preventDefault();
const handleAddToCart = (e: React.MouseEvent) => {
e.stopPropagation();
addItem(property, 1);
}, [addItem, property]);

const handleComparisonToggle = useCallback((e: React.MouseEvent) => {
e.preventDefault();
const handleComparisonToggle = (e: React.MouseEvent) => {
e.stopPropagation();
toggleProperty(property);
}, [toggleProperty, property]);

const handleCompareToggle = useCallback((e: React.MouseEvent<HTMLInputElement>) => {
e.preventDefault();
const handleCompareToggle = (e: React.MouseEvent<HTMLInputElement>) => {
e.stopPropagation();
if (!compareLimitReached) {
togglePropertyId(property.id);
}
}, [compareLimitReached, togglePropertyId, property.id]);

const handleToggleFavorite = useCallback((e: React.MouseEvent) => {
e.preventDefault();
const handleToggleFavorite = (e: React.MouseEvent) => {
e.stopPropagation();
if (isFavorite(property.id)) {
removeFavorite(property.id);
Expand Down Expand Up @@ -196,12 +192,12 @@ const PropertyCardInner: React.FC<PropertyCardProps> = ({
</div>

{/* Title */}
<h3
id={`property-${property.id}-name`}
<Link
href={`/properties/${property.id}`}
className="text-base sm:text-lg font-bold text-gray-900 dark:text-white mb-1 sm:mb-2 hover:text-blue-600 dark:hover:text-blue-400 transition-colors line-clamp-2"
>
{property.name}
</h3>
</Link>

{/* Location */}
<div className="flex items-start gap-1 text-xs sm:text-sm text-gray-600 dark:text-gray-400 mb-2 sm:mb-3">
Expand Down Expand Up @@ -293,7 +289,7 @@ const PropertyCardInner: React.FC<PropertyCardProps> = ({
</button>
<Link
href={`/properties/${property.id}`}
className="px-2 sm:px-4 py-1.5 sm:py-2 bg-blue-600 hover:bg-blue-700 text-white text-xs sm:text-sm font-medium rounded-lg transition-colors flex-shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 inline-flex items-center justify-center"
className="px-2 sm:px-4 py-1.5 sm:py-2 bg-blue-600 hover:bg-blue-700 text-white text-xs sm:text-sm font-medium rounded-lg transition-colors flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 inline-flex items-center justify-center"
aria-label={`View details for ${property.name}`}
>
View
Expand Down
23 changes: 23 additions & 0 deletions src/components/ui/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ function buildChartCSS(id: string, config: ChartConfig): string {

if (!colorConfig.length) return ''

return Object.entries(THEMES)
.map(([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${encodeURIComponent(key)}: ${color};` : null
})
.filter(Boolean)
.join("\n")}
}
`)
const cssContent = Object.entries(THEMES)
.map(([theme, prefix]) => {
const rules = colorConfig
Expand All @@ -92,7 +106,16 @@ function buildChartCSS(id: string, config: ChartConfig): string {
})
.filter(Boolean)
.join("\n")
}

const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const css = React.useMemo(() => buildChartCSS(id, config), [id, config])

if (!css) return null

const sanitizedCss = DOMPurify.sanitize(css)

return <style dangerouslySetInnerHTML={{ __html: sanitizedCss }} />
if (!cssContent) {
return null
}
Expand Down
64 changes: 64 additions & 0 deletions src/lib/batchTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import type { CartItem } from '@/types/cart';
import type { BatchTransactionResult } from '@/types/cart';
import { logger } from '@/utils/logger';
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const IS_DEMO_MODE = process.env.NEXT_PUBLIC_DEMO_TX === 'true';

const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
import { publicClient } from '@/lib/viem-client';

const DEMO_MODE = process.env.NEXT_PUBLIC_DEMO_TX === 'true';
Expand Down Expand Up @@ -29,6 +38,29 @@ export class BatchTransactionService {
};
}

if (IS_DEMO_MODE) {
await new Promise(resolve => setTimeout(resolve, 2000));

const transactionHash = `0x${Array.from({length: 64}, () =>
Math.floor(Math.random() * 16).toString(16)).join('')}`;

const results = items.map(item => ({
propertyId: item.property.id,
success: Math.random() > 0.1,
transactionHash: Math.random() > 0.1 ? transactionHash : undefined,
error: Math.random() > 0.1 ? undefined : 'Transaction failed: Insufficient gas'
}));

const allSuccessful = results.every(result => result.success);
const totalGasUsed = items.length * 0.0025 + 0.005;

return {
success: allSuccessful,
transactionHash: allSuccessful ? transactionHash : undefined,
results,
totalGasUsed,
error: allSuccessful ? undefined : 'Some transactions failed'
};
if (DEMO_MODE) {
return this.executeDemoBatchPurchase(items);
}
Expand Down Expand Up @@ -106,6 +138,38 @@ export class BatchTransactionService {
blockNumber?: number;
confirmations?: number;
}> {
if (IS_DEMO_MODE) {
await new Promise(resolve => setTimeout(resolve, 1000));

const random = Math.random();
if (random < 0.7) {
return {
status: 'confirmed',
blockNumber: Math.floor(Math.random() * 1000000) + 18000000,
confirmations: Math.floor(Math.random() * 50) + 1
};
} else if (random < 0.9) {
return { status: 'pending' };
} else {
return { status: 'failed' };
}
}

try {
const receipt = await publicClient.waitForTransactionReceipt({
hash: transactionHash as `0x${string}`,
timeout: 30_000,
});

if (receipt.status === 'success') {
return {
status: 'confirmed',
blockNumber: Number(receipt.blockNumber),
confirmations: 1,
};
}

return { status: 'failed' };
try {
const receipt = await publicClient.getTransactionReceipt({
hash: transactionHash as `0x${string}`,
Expand Down
140 changes: 80 additions & 60 deletions src/utils/earlyErrorSuppression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,73 +3,93 @@ import { logger } from './logger';
const stringifyArgs = (args: readonly unknown[]): string =>
args.map((arg) => (typeof arg === 'string' ? arg : String(arg))).join(' ');

// Patterns for known-noisy wallet extension errors that we silently swallow
// instead of reporting to the structured logger. These come from third-party
// Web3 extensions injecting scripts into the page and have no impact on
// PropChain functionality.
const SUPPRESS_PATTERNS: readonly string[] = [
'bfnaelmomeimhlpmgjnjophhpkkoljpa',
'evmAsk.js',
'selectExtension',
'chrome-extension://bfnaelmomeimhlpmgjnjophhpkkoljpa',
];
const earlySuppressionCleanups: Array<() => void> = [];

const matchesSuppressionPattern = (...args: unknown[]): boolean => {
const message = stringifyArgs(args).toLowerCase();
return SUPPRESS_PATTERNS.some((pattern) => message.includes(pattern.toLowerCase()));
};

/**
* Early error suppression for wallet extension noise.
*
* This module is imported as the very first side-effect in the app bootstrap,
* so its global listeners are in place before React/wagmi/connectors emit
* any errors.
*
* Design note: we deliberately do NOT monkey-patch the global console API
* here. Overriding those methods would cause any logger sink that ultimately
* writes through the global console to either recurse (infinite loop) or
* silently lose the structured-logger pipeline. Instead we filter at the
* boundary where vendor code reaches us — the global error and
* unhandledrejection listeners — and route any non-suppressed event through
* the structured logger so production telemetry receives correlation IDs,
* redaction, and breadcrumbs via the existing pipeline.
*/
export const initializeEarlyErrorSuppression = (): void => {
if (typeof window === 'undefined') return;
// This file should be imported as early as possible in the application
// It immediately starts suppressing extension errors

// Suppress global errors that match extension noise patterns; route
// everything else through the structured logger.
window.addEventListener(
'error',
(event) => {
if (matchesSuppressionPattern(event.error, event.filename, event.message)) {
event.preventDefault();
event.stopPropagation();
return;
}
logger.error('Unhandled error', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error,
});
},
true,
);
if (typeof window !== 'undefined') {
// Immediate error suppression before React loads
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;

const suppressPatterns = [
'bfnaelmomeimhlpmgjnjophhpkkoljpa',
'evmAsk.js',
'selectExtension',
'chrome-extension://bfnaelmomeimhlpmgjnjophhpkkoljpa',
];

const shouldSuppress = (...args: unknown[]): boolean => {
const message = stringifyArgs(args).toLowerCase();
return suppressPatterns.some(pattern => message.includes(pattern.toLowerCase()));
};

// Override console methods immediately
console.error = (...args: unknown[]) => {
if (shouldSuppress(...args)) return;
originalConsoleError.apply(console, args);
};

console.warn = (...args: unknown[]) => {
if (shouldSuppress(...args)) return;
originalConsoleWarn.apply(console, args);
};

// Suppress unhandled promise rejections that match extension noise;
// route the rest through the structured logger.
window.addEventListener('unhandledrejection', (event) => {
if (matchesSuppressionPattern(event.reason)) {
earlySuppressionCleanups.push(() => {
console.error = originalConsoleError;
console.warn = originalConsoleWarn;
});

// Suppress global errors
const handleError = (event: ErrorEvent) => {
if (shouldSuppress(event.error, event.filename, event.message)) {
event.preventDefault();
event.stopPropagation();
}
};
window.addEventListener('error', handleError, true);
earlySuppressionCleanups.push(() => {
window.removeEventListener('error', handleError, true);
});

// Suppress unhandled promise rejections
const handleRejection = (event: PromiseRejectionEvent) => {
if (shouldSuppress(event.reason)) {
event.preventDefault();
event.stopPropagation();
return;
}
logger.error('Unhandled promise rejection', { reason: event.reason });
};
window.addEventListener('unhandledrejection', handleRejection);
earlySuppressionCleanups.push(() => {
window.removeEventListener('unhandledrejection', handleRejection);
});

// Override window.onerror
const originalOnError = window.onerror;
window.onerror = (message, source, lineno, colno, error) => {
if (shouldSuppress(message, source, error)) {
return true; // Suppress the error
}
if (originalOnError) {
return originalOnError.call(window, message, source, lineno, colno, error);
}
return false;
};
earlySuppressionCleanups.push(() => {
window.onerror = originalOnError;
});
}

export const initializeEarlyErrorSuppression = () => {
// This function can be called to ensure suppression is active
// Note: Early error suppression is already active from module load
};

// Auto-initialize on module import.
initializeEarlyErrorSuppression();
export const cleanupEarlyErrorSuppression = () => {
let fn: (() => void) | undefined;
while ((fn = earlySuppressionCleanups.pop()) !== undefined) {
fn();
}
};
Loading