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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ FAMILY_API_KEY=
FAMILY_API_URL=
COINGECKO_API_KEY=
PLAIN_API_KEY=
COMPLIANCE_API_URL=
COMPLIANCE_SECRET=
SENTRY_AUTH_TOKEN=
65 changes: 34 additions & 31 deletions pages/_app.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { TransactionEventHandler } from 'src/components/TransactionEventHandler'
import { GasStationProvider } from 'src/components/transactions/GasStation/GasStationProvider';
import { CowOrderToast } from 'src/components/transactions/Swap/modals/result/CowOrderToast';
import { AppDataProvider } from 'src/hooks/app-data-provider/useAppDataProvider';
import { ComplianceProvider } from 'src/hooks/compliance/compliance';
import { ModalContextProvider } from 'src/hooks/useModal';
import { SwapOrdersTrackingProvider } from 'src/hooks/useSwapOrdersTracking';
import { Web3ContextProvider } from 'src/libs/web3-data-provider/Web3Provider';
Expand Down Expand Up @@ -160,38 +161,40 @@ export default function MyApp(props: MyAppProps) {
>
<Web3ContextProvider>
<AppGlobalStyles>
<AddressBlocked>
<SwapOrdersTrackingProvider>
<ModalContextProvider>
<SharedDependenciesProvider>
<AppDataProvider>
<GasStationProvider>
{getLayout(<Component {...pageProps} />)}
<SupplyModal />
<WithdrawModal />
<BorrowModal />
<RepayModal />
<CollateralChangeModal />
<ClaimRewardsModal />
<EmodeModal />
<FaucetModal />
<TransactionEventHandler />
<StakingMigrateModal />
<BridgeModal />
<ReadOnlyModal />
<ComplianceProvider>
<AddressBlocked>
<SwapOrdersTrackingProvider>
<ModalContextProvider>
<SharedDependenciesProvider>
<AppDataProvider>
<GasStationProvider>
{getLayout(<Component {...pageProps} />)}
<SupplyModal />
<WithdrawModal />
<BorrowModal />
<RepayModal />
<CollateralChangeModal />
<ClaimRewardsModal />
<EmodeModal />
<FaucetModal />
<TransactionEventHandler />
<StakingMigrateModal />
<BridgeModal />
<ReadOnlyModal />

{/* Swap Modals */}
<SwapModal />
<CollateralSwapModal />
<DebtSwapModal />
<CancelCowOrderModal />
<CowOrderToast />
</GasStationProvider>
</AppDataProvider>
</SharedDependenciesProvider>
</ModalContextProvider>
</SwapOrdersTrackingProvider>
</AddressBlocked>
{/* Swap Modals */}
<SwapModal />
<CollateralSwapModal />
<DebtSwapModal />
<CancelCowOrderModal />
<CowOrderToast />
</GasStationProvider>
</AppDataProvider>
</SharedDependenciesProvider>
</ModalContextProvider>
</SwapOrdersTrackingProvider>
</AddressBlocked>
</ComplianceProvider>
</AppGlobalStyles>
</Web3ContextProvider>
</ConnectKitProvider>
Expand Down
88 changes: 88 additions & 0 deletions pages/api/preflight-compliance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { isAddress } from 'viem';

const COMPLIANCE_API_URL = process.env.COMPLIANCE_API_URL;
const COMPLIANCE_SECRET = process.env.COMPLIANCE_SECRET;

type ComplianceApiResponse = {
result: boolean;
lastChecked: string;
nextCheck: string;
};

type PreflightResponse = {
result: boolean;
nextCheck: string;
};

type ErrorResponse = {
error: string;
};

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<PreflightResponse | ErrorResponse>
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}

const { address } = req.query;

if (!address || typeof address !== 'string') {
return res.status(400).json({ error: 'Address is required' });
}

// Validate address format to prevent SSRF attacks
// const ethereumAddressRegex = /^0x[a-fA-F0-9]{40}$/;
if (!isAddress(address)) {
return res.status(400).json({ error: 'Invalid address format' });
}

if (!COMPLIANCE_API_URL || !COMPLIANCE_SECRET) {
console.error('Compliance API not configured');
// Fail open in development if not configured
if (process.env.NODE_ENV === 'development') {
return res.status(200).json({
result: true,
nextCheck: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
});
}
return res.status(500).json({ error: 'Service unavailable' });
}

try {
// use ?overrideResultStatus=false | true if you want to override the result

const response = await fetch(`${COMPLIANCE_API_URL}/check/${encodeURIComponent(address)}`, {
method: 'GET',
headers: {
'x-compliance-secret': COMPLIANCE_SECRET,
},
});

console.log('RESPONSE---', response);

if (!response.ok) {
if (response.status === 401) {
console.error('Compliance API: Invalid secret');
return res.status(500).json({ error: 'Service configuration error' });
}
if (response.status === 400) {
return res.status(400).json({ error: 'Invalid address format' });
}
return res.status(500).json({ error: 'Compliance check failed' });
}

const data: ComplianceApiResponse = await response.json();
console.log('DATA---', data);

return res.status(200).json({
result: data.result,
nextCheck: data.nextCheck,
});
} catch (error) {
console.error('Compliance API error:', error);
return res.status(500).json({ error: 'Service unavailable' });
}
}
44 changes: 28 additions & 16 deletions src/components/AddressBlocked.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
import { ReactNode } from 'react';
import { useAddressAllowed } from 'src/hooks/useAddressAllowed';
import { useCompliance } from 'src/hooks/compliance/compliance';
import { MainLayout } from 'src/layouts/MainLayout';
import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
import { ENABLE_TESTNET } from 'src/utils/marketsAndNetworksConfig';
import { useDisconnect } from 'wagmi';

import { AddressBlockedModal } from './AddressBlockedModal';

export const AddressBlocked = ({ children }: { children: ReactNode }) => {
type ComplianceGateProps = {
children: ReactNode;
};

export const AddressBlocked = ({ children }: ComplianceGateProps) => {
const { status, recheck, errorMessage } = useCompliance();
const { currentAccount, readOnlyMode } = useWeb3Context();
const { disconnect } = useDisconnect();
const screenAddress = readOnlyMode || ENABLE_TESTNET ? '' : currentAccount;
const { isAllowed, message } = useAddressAllowed(screenAddress);
const shouldCheck = !readOnlyMode && !ENABLE_TESTNET;

const showBlockedOverlay = status === 'non-compliant';
const showErrorOverlay = status === 'error';

if (!shouldCheck) {
return <>{children}</>;
}

if (!isAllowed) {
return (
<MainLayout>
<AddressBlockedModal
address={currentAccount}
onDisconnectWallet={() => disconnect()}
message={message}
/>
;
</MainLayout>
);
if (!showBlockedOverlay && !showErrorOverlay) {
return <>{children}</>;
}

return <>{children}</>;
return (
<MainLayout>
<AddressBlockedModal
address={currentAccount}
onDisconnectWallet={() => disconnect()}
isError={showErrorOverlay}
errorMessage={errorMessage}
onRetry={showErrorOverlay ? recheck : undefined}
/>
</MainLayout>
);
};
49 changes: 29 additions & 20 deletions src/components/AddressBlockedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,30 @@ import { BasicModal } from './primitives/BasicModal';
export interface AddressBlockedProps {
address: string;
onDisconnectWallet: () => void;
message?: string;
isError?: boolean;
errorMessage?: string;
onRetry?: () => void;
}

export const AddressBlockedModal = ({
address,
onDisconnectWallet,
message,
isError = false,
errorMessage,
onRetry,
}: AddressBlockedProps) => {
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
const setOpen = (_value: boolean) => {}; // ignore, we want the modal to not be dismissable

const description = isError ? (
errorMessage || <Trans>Something went wrong. Please try again later.</Trans>
) : (
<Trans>
Sorry, we are unable to connect your wallet. <br />
Please try again with another wallet or contact support.
</Trans>
);

return (
<BasicModal open={true} withCloseButton={false} setOpen={setOpen}>
<Box
Expand All @@ -38,25 +51,21 @@ export const AddressBlockedModal = ({
{address}
</Typography>
<Typography variant="description" sx={{ textAlign: 'center', mb: 4 }}>
{message ? (
message
) : (
<>
<Trans>Something went wrong. Please try again later.</Trans>
<br />
<Typography variant="helperText" sx={{ mb: 1 }}>
{' '}
<Trans>error code: 2455</Trans>{' '}
</Typography>
</>
)}
{description}
</Typography>
<Button variant="contained" onClick={onDisconnectWallet}>
<SvgIcon fontSize="small" sx={{ mx: 1 }}>
<LogoutIcon />
</SvgIcon>
<Trans>Disconnect Wallet</Trans>
</Button>
<Box sx={{ display: 'flex', gap: 2 }}>
{isError && onRetry && (
<Button variant="outlined" onClick={onRetry}>
<Trans>Retry</Trans>
</Button>
)}
<Button variant="contained" onClick={onDisconnectWallet}>
<SvgIcon fontSize="small" sx={{ mx: 1 }}>
<LogoutIcon />
</SvgIcon>
<Trans>Disconnect Wallet</Trans>
</Button>
</Box>
</Box>
</BasicModal>
);
Expand Down
Loading
Loading