Skip to content

Commit

Permalink
Blocking mechanism (#658)
Browse files Browse the repository at this point in the history
- Onchain blocking list
- VPN & country block via vpnapi.io
  • Loading branch information
bpierre authored Dec 17, 2024
1 parent 1c38eed commit 18d22aa
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 20 deletions.
8 changes: 7 additions & 1 deletion frontend/app/.env
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
NEXT_PUBLIC_DEMO_MODE=false
NEXT_PUBLIC_VERCEL_ANALYTICS=false
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=1
NEXT_PUBLIC_DEPLOYMENT_FLAVOR=
# NEXT_PUBLIC_DEPLOYMENT_FLAVOR=

# use demo|API_KEY (demo API) or pro|API_KEY (pro API)
NEXT_PUBLIC_COINGECKO_API_KEY=

# the BLOCKING_LIST contract must implement isBlackListed(address)(bool)
# NEXT_PUBLIC_BLOCKING_LIST=0x97044531D0fD5B84438499A49629488105Dc58e6

# format: {vpnapi.io key}|{comma separated country codes} e.g. 1234|US,CA
# NEXT_PUBLIC_BLOCKING_VPNAPI=

# Ethereum (mainnet)
# NEXT_PUBLIC_CHAIN_ID=1
# NEXT_PUBLIC_CHAIN_NAME=Ethereum
Expand Down
17 changes: 10 additions & 7 deletions frontend/app/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ReactNode } from "react";

import { About } from "@/src/comps/About/About";
import { AppLayout } from "@/src/comps/AppLayout/AppLayout";
import { Blocking } from "@/src/comps/Blocking/Blocking";
import content from "@/src/content";
import { DemoMode } from "@/src/demo-mode";
import { VERCEL_ANALYTICS } from "@/src/env";
Expand Down Expand Up @@ -35,13 +36,15 @@ export default function Layout({
<StoredState>
<DemoMode>
<Ethereum>
<TransactionFlow>
<About>
<AppLayout>
{children}
</AppLayout>
</About>
</TransactionFlow>
<Blocking>
<TransactionFlow>
<About>
<AppLayout>
{children}
</AppLayout>
</About>
</TransactionFlow>
</Blocking>
</Ethereum>
</DemoMode>
</StoredState>
Expand Down
144 changes: 144 additions & 0 deletions frontend/app/src/comps/Blocking/Blocking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"use client";

import type { ReactNode } from "react";

import { BLOCKING_LIST, BLOCKING_VPNAPI } from "@/src/env";
import { useAccount } from "@/src/services/Ethereum";
import type { Address } from "@/src/types";
import { css } from "@/styled-system/css";
import { useQuery } from "@tanstack/react-query";
import * as v from "valibot";
import { useReadContract } from "wagmi";

export function Blocking({
children,
}: {
children: ReactNode;
}) {
const account = useAccount();
const accountInBlockingList = useIsAccountInBlockingList(account.address ?? null);
const accountVpnapi = useVpnapiBlock();

let blocked: {
title: string;
message: string;
} | null = null;

if (accountInBlockingList.data) {
blocked = {
title: "Account blocked",
message: "This app cannot be accessed from this account.",
};
}

if (accountVpnapi.data?.isRouted) {
blocked = {
title: "Routed connection detected (VPN or similar)",
message: "This app cannot be accessed from a routed connection.",
};
}

if (accountVpnapi.data?.isCountryBlocked) {
blocked = {
title: "Blocked country",
message: `This app cannot be accessed from this country (${accountVpnapi.data.country}).`,
};
}

if (!blocked) {
return children;
}

return (
<main
className={css({
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
background: "background",
})}
>
<p
className={css({
display: "flex",
flexDirection: "column",
width: "100%",
maxWidth: 600,
padding: 16,
color: "negativeSurfaceContent",
textAlign: "center",
background: "negativeSurface",
border: "1px solid token(colors.negativeSurfaceBorder)",
borderRadius: 8,
})}
>
{blocked.message}
</p>
</main>
);
}

// blocking list contract
export function useIsAccountInBlockingList(account: Address | null) {
return useReadContract({
address: BLOCKING_LIST,
abi: [{
"inputs": [{ "internalType": "address", "name": "", "type": "address" }],
"name": "isBlacklisted",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function",
}],
functionName: "isBlacklisted",
args: [account ?? "0x"],
query: {
enabled: Boolean(account && BLOCKING_LIST),
retry: false,
refetchInterval: false,
},
});
}

// vpnapi.io blocking
const VpnapiResponseSchema = v.pipe(
v.object({
ip: v.string(),
security: v.object({
vpn: v.boolean(),
proxy: v.boolean(),
tor: v.boolean(),
relay: v.boolean(),
}),
location: v.object({
country_code: v.string(),
}),
}),
v.transform((value) => ({
isRouted: Object.values(value.security).some((is) => is),
country: value.location.country_code,
})),
);
export function useVpnapiBlock() {
return useQuery({
queryKey: ["vpnapi", BLOCKING_VPNAPI],
queryFn: async () => {
if (!BLOCKING_VPNAPI) {
return null;
}
const response = await fetch(
`https://vpnapi.io/api/?key=${BLOCKING_VPNAPI.apiKey}`,
);
const result = await response.json();
const { isRouted, country } = v.parse(VpnapiResponseSchema, result);
return {
country,
isCountryBlocked: BLOCKING_VPNAPI.countries.includes(country),
isRouted,
};
},
enabled: BLOCKING_VPNAPI !== null,
retry: false,
refetchInterval: false,
});
}
48 changes: 36 additions & 12 deletions frontend/app/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ export const CollateralSymbolSchema = v.union([
export const EnvSchema = v.pipe(
v.object({
APP_VERSION: v.string(),
BLOCKING_LIST: v.optional(vAddress()),
BLOCKING_VPNAPI: v.pipe(
v.optional(v.string(), ""),
v.transform((value) => {
if (!value.trim()) {
return null; // not set
}

const [apiKey, countries = ""] = value.split("|");
if (!apiKey) {
throw new Error(
`Invalid BLOCKING_VPNAPI value: ${value}. `
+ `Expected format: API_KEY or API_KEY|COUNTRY,COUNTRY,… `
+ `(e.g. 123456|US,CA)`,
);
}
return {
apiKey: apiKey.trim(),
countries: countries.split(",").map((c) => c.trim().toUpperCase()),
};
}),
),
CHAIN_ID: v.pipe(
v.string(),
v.transform((value) => {
Expand Down Expand Up @@ -181,18 +203,24 @@ export type Env = v.InferOutput<typeof EnvSchema>;

const parsedEnv = v.parse(EnvSchema, {
APP_VERSION: process.env.APP_VERSION, // set in next.config.js
CHAIN_ID: process.env.NEXT_PUBLIC_CHAIN_ID,
CHAIN_NAME: process.env.NEXT_PUBLIC_CHAIN_NAME,
CHAIN_CURRENCY: process.env.NEXT_PUBLIC_CHAIN_CURRENCY,
CHAIN_RPC_URL: process.env.NEXT_PUBLIC_CHAIN_RPC_URL,
BLOCKING_LIST: process.env.NEXT_PUBLIC_BLOCKING_LIST,
BLOCKING_VPNAPI: process.env.NEXT_PUBLIC_BLOCKING_VPNAPI,
CHAIN_BLOCK_EXPLORER: process.env.NEXT_PUBLIC_CHAIN_BLOCK_EXPLORER,
CHAIN_CONTRACT_ENS_REGISTRY: process.env.NEXT_PUBLIC_CHAIN_CONTRACT_ENS_REGISTRY,
CHAIN_CONTRACT_ENS_RESOLVER: process.env.NEXT_PUBLIC_CHAIN_CONTRACT_ENS_RESOLVER,
CHAIN_CONTRACT_MULTICALL: process.env.NEXT_PUBLIC_CHAIN_CONTRACT_MULTICALL,
CHAIN_CURRENCY: process.env.NEXT_PUBLIC_CHAIN_CURRENCY,
CHAIN_ID: process.env.NEXT_PUBLIC_CHAIN_ID,
CHAIN_NAME: process.env.NEXT_PUBLIC_CHAIN_NAME,
CHAIN_RPC_URL: process.env.NEXT_PUBLIC_CHAIN_RPC_URL,
COINGECKO_API_KEY: process.env.NEXT_PUBLIC_COINGECKO_API_KEY,
COMMIT_HASH: process.env.COMMIT_HASH, // set in next.config.js
SUBGRAPH_URL: process.env.NEXT_PUBLIC_SUBGRAPH_URL,

DELEGATE_AUTO: process.env.NEXT_PUBLIC_DELEGATE_AUTO,
DEMO_MODE: process.env.NEXT_PUBLIC_DEMO_MODE,
DEPLOYMENT_FLAVOR: process.env.NEXT_PUBLIC_DEPLOYMENT_FLAVOR,
SUBGRAPH_URL: process.env.NEXT_PUBLIC_SUBGRAPH_URL,
VERCEL_ANALYTICS: process.env.NEXT_PUBLIC_VERCEL_ANALYTICS,
WALLET_CONNECT_PROJECT_ID: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID,

CONTRACT_BOLD_TOKEN: process.env.NEXT_PUBLIC_CONTRACT_BOLD_TOKEN,
CONTRACT_COLLATERAL_REGISTRY: process.env.NEXT_PUBLIC_CONTRACT_COLLATERAL_REGISTRY,
Expand Down Expand Up @@ -245,16 +273,12 @@ const parsedEnv = v.parse(EnvSchema, {
COLL_2_CONTRACT_STABILITY_POOL: process.env.NEXT_PUBLIC_COLL_2_CONTRACT_STABILITY_POOL,
COLL_2_CONTRACT_TROVE_MANAGER: process.env.NEXT_PUBLIC_COLL_2_CONTRACT_TROVE_MANAGER,
COLL_2_CONTRACT_TROVE_NFT: process.env.NEXT_PUBLIC_COLL_2_CONTRACT_TROVE_NFT,

COINGECKO_API_KEY: process.env.NEXT_PUBLIC_COINGECKO_API_KEY,
DEMO_MODE: process.env.NEXT_PUBLIC_DEMO_MODE,
DEPLOYMENT_FLAVOR: process.env.NEXT_PUBLIC_DEPLOYMENT_FLAVOR,
VERCEL_ANALYTICS: process.env.NEXT_PUBLIC_VERCEL_ANALYTICS,
WALLET_CONNECT_PROJECT_ID: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID,
});

export const {
APP_VERSION,
BLOCKING_LIST,
BLOCKING_VPNAPI,
CHAIN_BLOCK_EXPLORER,
CHAIN_CONTRACT_ENS_REGISTRY,
CHAIN_CONTRACT_ENS_RESOLVER,
Expand Down

0 comments on commit 18d22aa

Please sign in to comment.